Dr-Swopt пре 1 месец
родитељ
комит
e9eaf1b9ca

+ 79 - 0
src/app/components/ffb-vector-search-dialog/ffb-vector-search-dialog.component.css

@@ -0,0 +1,79 @@
+.dialog-content {
+    display: flex;
+    flex-direction: column;
+    gap: 24px;
+    /* more space between rows */
+    width: 100%;
+    max-width: 1140px;
+    padding: 16px 24px;
+    /* padding inside the dialog */
+}
+
+.search-row {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 16px;
+    /* space between items */
+}
+
+.search-row mat-form-field {
+    flex: 1 1 auto;
+    min-width: 200px;
+    /* ensures small screens are okay */
+}
+
+.search-row-with-close {
+    display: flex;
+    align-items: flex-start;
+    /* align search row and close button */
+    gap: 16px;
+    /* space between search and close */
+    flex-wrap: wrap;
+    /* wrap on smaller screens */
+    margin-bottom: 16px;
+}
+
+.top-k-field {
+    width: 100px;
+}
+
+.search-button {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    /* more spacing between search button and spinner */
+    margin-bottom: 30px;
+}
+
+.results-table {
+    width: 100%;
+    margin-top: 16px;
+    border-collapse: separate;
+    border-spacing: 0 4px;
+    /* subtle row spacing */
+}
+
+.results-table th {
+    background-color: #f5f5f5;
+    text-align: left;
+    padding: 10px 12px;
+}
+
+.results-table td {
+    padding: 10px 12px;
+    background-color: #fff;
+    border-bottom: 1px solid #e0e0e0;
+}
+
+.no-results {
+    text-align: center;
+    color: rgba(0, 0, 0, 0.6);
+    font-style: italic;
+    margin-top: 16px;
+}
+
+mat-dialog-actions {
+    margin-top: 24px;
+    gap: 12px;
+}

+ 70 - 0
src/app/components/ffb-vector-search-dialog/ffb-vector-search-dialog.component.html

@@ -0,0 +1,70 @@
+<div mat-dialog-content class="dialog-content">
+  <h2 mat-dialog-title>Vector Search</h2>
+  <div class="search-row-with-close">
+    <div class="search-row">
+      <mat-form-field appearance="outline">
+        <mat-label>Query</mat-label>
+        <input matInput [formControl]="queryControl" placeholder="Type your search...">
+      </mat-form-field>
+
+      <mat-form-field appearance="outline" class="top-k-field">
+        <mat-label>Top k</mat-label>
+        <input matInput type="number" [formControl]="kControl" min="1">
+      </mat-form-field>
+
+      <div class="search-button">
+        <button mat-flat-button color="primary" (click)="search()">
+          <mat-icon>search</mat-icon> Search
+        </button>
+        <mat-progress-spinner *ngIf="loading" diameter="36" mode="indeterminate"></mat-progress-spinner>
+      </div>
+    </div>
+
+    <!-- Close button next to search -->
+    <button mat-stroked-button color="warn" (click)="close()">Close</button>
+  </div>
+
+  <table mat-table [dataSource]="results" class="mat-elevation-z8 results-table" *ngIf="results.length > 0">
+    <ng-container matColumnDef="productionDate">
+      <th mat-header-cell *matHeaderCellDef>Date</th>
+      <td mat-cell *matCellDef="let h">{{ formatDate(h.productionDate) }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="site">
+      <th mat-header-cell *matHeaderCellDef>Site</th>
+      <td mat-cell *matCellDef="let h">{{ h.site }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="phase">
+      <th mat-header-cell *matHeaderCellDef>Phase</th>
+      <td mat-cell *matCellDef="let h">{{ h.phase }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="block">
+      <th mat-header-cell *matHeaderCellDef>Block</th>
+      <td mat-cell *matCellDef="let h">{{ h.block }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="weight">
+      <th mat-header-cell *matHeaderCellDef>Weight</th>
+      <td mat-cell *matCellDef="let h">{{ h.weight }} {{ h.weightUom }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="quantity">
+      <th mat-header-cell *matHeaderCellDef>Quantity</th>
+      <td mat-cell *matCellDef="let h">{{ h.quantity }} {{ h.quantityUom }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="score">
+      <th mat-header-cell *matHeaderCellDef>Score</th>
+      <td mat-cell *matCellDef="let h">{{ h.score ?? '-' }}</td>
+    </ng-container>
+
+    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+  </table>
+
+  <p class="no-results" *ngIf="!loading && results.length === 0 && queryControl.value">
+    No results found
+  </p>
+</div>

+ 82 - 0
src/app/components/ffb-vector-search-dialog/ffb-vector-search-dialog.component.ts

@@ -0,0 +1,82 @@
+import { Component, Inject, inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { HttpClient } from '@angular/common/http';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatTableModule } from '@angular/material/table';
+import { webConfig } from '../../config';
+import { MatIconModule } from '@angular/material/icon';
+import { FFBProduction } from '../../ffb/ffb-production.interface';
+
+@Component({
+  selector: 'app-ffb-vector-search-dialog',
+  templateUrl: './ffb-vector-search-dialog.component.html',
+  styleUrls: ['./ffb-vector-search-dialog.component.css'],
+  standalone: true,
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatProgressSpinnerModule,
+    MatTableModule,
+    MatIconModule
+  ],
+})
+export class FfbVectorSearchDialogComponent {
+  private http = inject(HttpClient);
+  dialogRef = inject(MatDialogRef<FfbVectorSearchDialogComponent>);
+
+  queryControl = new FormControl('');
+  kControl = new FormControl(5);
+
+  loading = false;
+  results: FFBProduction[] = [];
+
+  displayedColumns: string[] = ['productionDate', 'site', 'phase', 'block', 'weight', 'quantity', 'score'];
+
+  constructor(@Inject(MAT_DIALOG_DATA) public data: any) {}
+
+  search() {
+    const q = this.queryControl.value?.trim();
+    const k = this.kControl.value || 5;
+
+    if (!q) return;
+
+    this.loading = true;
+    this.results = [];
+
+    this.http.get<FFBProduction[]>(`${webConfig.exposedUrl}/api/ffb-production/search?q=${encodeURIComponent(q)}&k=${k}`)
+      .subscribe({
+        next: (data) => {
+          console.log(data)
+          // Strip vector to save memory/UI
+          this.results = data.map(h => ({ ...h, vector: undefined, productionDate: new Date(h.productionDate) }));
+          this.loading = false;
+        },
+        error: (err) => {
+          console.error(err);
+          this.loading = false;
+        }
+      });
+  }
+
+  close() {
+    this.dialogRef.close();
+  }
+
+  applyResults() {
+    // Return results to parent
+    this.dialogRef.close(this.results);
+  }
+
+  formatDate(date: Date | string) {
+    const d = new Date(date);
+    return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' });
+  }
+}

+ 1 - 1
src/app/config.ts

@@ -1,3 +1,3 @@
 export const webConfig = {
-    exposedUrl: `https://192.168.100.100:4000`,
+    exposedUrl: `http://localhost:4000`,
 }

+ 4 - 0
src/app/ffb/ffb-production.component.html

@@ -72,6 +72,10 @@
     <button mat-stroked-button color="primary" (click)="openCalculateDialog()">
       Calculate Totals & Averages
     </button>
+
+    <button mat-stroked-button color="accent" (click)="openVectorSearchDialog()">
+      <mat-icon>smart_toy</mat-icon> Vector Search
+    </button>
   </div>
 </div>
 

+ 18 - 0
src/app/ffb/ffb-production.component.ts

@@ -18,6 +18,7 @@ import { FFBProduction } from './ffb-production.interface';
 import { webConfig } from '../config';
 import { CreateFfbProductionDialogComponent } from '../components/ffb-production-dialog/create-ffb-production-dialog.component';
 import { FfbHarvestCalculateDialogComponent } from '../components/ffb-calculation/ffb-production-calculate-dialog.component';
+import { FfbVectorSearchDialogComponent } from '../components/ffb-vector-search-dialog/ffb-vector-search-dialog.component';
 
 @Component({
   selector: 'app-ffb-production',
@@ -219,4 +220,21 @@ export class FfbProductionComponent implements OnInit {
       width: '400px'
     });
   }
+
+  openVectorSearchDialog() {
+    const dialogRef = this.dialog.open(FfbVectorSearchDialogComponent, {
+      width: '1200px',
+      maxWidth: '95vw', // makes it responsive
+      minWidth: '800px', // optional: ensures it doesn’t get too small
+      height: '80vh',    // optional: taller dialog for table
+      data: {}
+    });
+
+    dialogRef.afterClosed().subscribe((results: FFBProduction[] | undefined) => {
+      if (results?.length) {
+        this.filteredProductions = results;
+      }
+    });
+  }
+
 }

+ 1 - 0
src/app/ffb/ffb-production.interface.ts

@@ -8,4 +8,5 @@ export interface FFBProduction {
   weightUom: string;
   quantity: number;
   quantityUom: string;
+  score?: number
 }

+ 2 - 4
src/app/services/auth.service.ts

@@ -4,7 +4,6 @@ import { Router } from '@angular/router';
 import { Observable } from 'rxjs';
 import { AuthResponse, LoginPayload, RegisterPayload } from '../interfaces/interface';
 import { AuthenticationResponseJSON, RegistrationResponseJSON, startAuthentication, startRegistration } from '@simplewebauthn/browser';
-import { A11yModule } from '@angular/cdk/a11y';
 import { webConfig } from '../config';
 
 @Injectable({ providedIn: 'root' })
@@ -23,7 +22,6 @@ export class AuthService {
   }
 
   // -- API Calls --
-
   register(payload: RegisterPayload): Observable<AuthResponse> {
     return new Observable<AuthResponse>((observer) => {
       this.http.post<AuthResponse>(`${this.baseUrl}/auth/register`, payload)
@@ -66,7 +64,7 @@ export class AuthService {
     return new Promise(async (resolve, reject) => {
 
       const options = await this.http
-        .post<any>(`${this.baseUrl}/auth/webauthn-register-options`, { username })
+        .post<any>(`${this.baseUrl}/auth/webauthn-register-options`, { username }, { withCredentials: true, })
         .toPromise();
 
       if (!options) {
@@ -92,7 +90,7 @@ export class AuthService {
       };
 
       // POST to /webauthn/register
-      this.http.post<AuthResponse>(`${this.baseUrl}/auth/webauthn-register`, credentialJSON).toPromise().then(res => {
+      this.http.post<AuthResponse>(`${this.baseUrl}/auth/webauthn-register`, credentialJSON, { withCredentials: true, }).toPromise().then(res => {
         if (!res) {
           reject(res)
         } else {

+ 7 - 0
src/styles.css

@@ -2,3 +2,10 @@
 
 html, body { height: 100%; }
 body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+
+/* Ensure dialogs can use full width & height */
+.cdk-overlay-pane {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+}