Dr-Swopt hai 1 semana
pai
achega
7022ebabb3

+ 4 - 4
frontend/src/app/components/analyzer/analyzer.component.html

@@ -17,7 +17,7 @@
       <div class="controls glass-panel">
         <div class="field">
           <label>Processing Mode</label>
-          <select [ngModel]="inferenceService.mode()" (ngModelChange)="inferenceService.mode.set($event)" class="input">
+          <select [ngModel]="inferenceService.mode()" (ngModelChange)="onModeChange($event)" class="input">
             <option value="local">Edge AI (Low Latency)</option>
             <option value="api">API AI (High Fidelity)</option>
           </select>
@@ -25,7 +25,7 @@
 
         <div class="field" *ngIf="inferenceService.mode() === 'local'">
           <label>Local Engine Model</label>
-          <select [ngModel]="inferenceService.localEngine()" (ngModelChange)="inferenceService.localEngine.set($event)" class="input">
+          <select [ngModel]="inferenceService.localEngine()" (ngModelChange)="onEngineChange($event)" class="input">
             <option value="onnx">YOLOv8 Industrial (ONNX)</option>
             <option value="tflite">Standard PoC (TFLite FP32)</option>
           </select>
@@ -50,9 +50,9 @@
 
         <div *ngIf="results" class="results-data">
           <div class="summary-cards">
-            <div *ngFor="let key of getSummaryKeys()" class="card" [class.alert]="results.industrial_summary[key] > 0 && (key === 'Abnormal' || key === 'Empty_Bunch')">
+            <div *ngFor="let key of getSummaryKeys()" class="card" [class.alert]="inferenceService.summary()[key] > 0 && (key === 'Abnormal' || key === 'Empty_Bunch')">
               <span class="card-label">{{ key }}</span>
-              <span class="card-value">{{ results.industrial_summary[key] }}</span>
+              <span class="card-value">{{ inferenceService.summary()[key] }}</span>
             </div>
             <div class="card highlight">
               <span class="card-label">Latency</span>

+ 42 - 34
frontend/src/app/components/analyzer/analyzer.component.ts

@@ -1,9 +1,8 @@
 import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { ImageProcessorService } from '../../services/image-processor.service';
-import { LocalInferenceService } from '../../services/local-inference.service';
 import { LocalHistoryService } from '../../services/local-history.service';
-import { InferenceService } from '../../core/services/inference.service';
+import { InferenceService, InferenceMode, LocalEngine } from '../../core/services/inference.service';
 import { FormsModule } from '@angular/forms';
 
 @Component({
@@ -19,7 +18,6 @@ export class AnalyzerComponent implements OnInit {
   previewUrl: string | null = null;
   results: any = null;
   loading = false;
-  modelType = 'onnx';
   confidence = 0.25;
   isDragging = false;
 
@@ -30,7 +28,6 @@ export class AnalyzerComponent implements OnInit {
   ) {}
 
   ngOnInit(): void {
-    // Confidence is now managed locally for the PoC
     this.confidence = 0.25;
   }
 
@@ -76,6 +73,14 @@ export class AnalyzerComponent implements OnInit {
     reader.readAsDataURL(file);
   }
 
+  onModeChange(newMode: any): void {
+    this.inferenceService.mode.set(newMode as InferenceMode);
+  }
+
+  onEngineChange(newEngine: any): void {
+    this.inferenceService.localEngine.set(newEngine as LocalEngine);
+  }
+
   async analyze(): Promise<void> {
     if (!this.selectedFile || !this.previewUrl) return;
 
@@ -83,30 +88,17 @@ export class AnalyzerComponent implements OnInit {
     const start = performance.now();
 
     try {
-      // Get image dimensions
+      // Get image dimensions for scaling help
       const img = await this.loadImageDimensions(this.selectedFile);
 
       // Use the Master Inference Service Hub
       this.inferenceService.analyze(this.previewUrl, img.width, img.height).subscribe({
         next: (detections) => {
-          // Calculate Industrial Summary
-          const summary: any = { 
-            'Empty_Bunch': 0, 
-            'Underripe': 0, 
-            'Abnormal': 0, 
-            'Ripe': 0, 
-            'Unripe': 0, 
-            'Overripe': 0 
-          };
-          
-          detections.forEach((d: any) => {
-            if (summary[d.class] !== undefined) summary[d.class]++;
-          });
-          
           this.results = {
-            industrial_summary: summary,
+            industrial_summary: this.inferenceService.summary(),
             inference_ms: performance.now() - start,
-            detections: detections
+            detections: detections,
+            original_dimensions: img
           };
 
           // Persist to local vault
@@ -132,7 +124,6 @@ export class AnalyzerComponent implements OnInit {
     }
   }
 
-  // Helper to get raw file dimensions
   private loadImageDimensions(file: File): Promise<{width: number, height: number}> {
     return new Promise((resolve) => {
       const img = new Image();
@@ -146,43 +137,60 @@ export class AnalyzerComponent implements OnInit {
   }
 
   drawDetections(): void {
-    if (!this.results || !this.results.detections || !this.canvas) return;
+    const detections = this.inferenceService.detections();
+    if (!detections || !this.canvas || !this.previewUrl) return;
 
     const ctx = this.canvas.nativeElement.getContext('2d');
     if (!ctx) return;
 
     const img = new Image();
-    img.src = this.previewUrl!;
+    img.src = this.previewUrl;
     img.onload = () => {
       const canvas = this.canvas.nativeElement;
       const containerWidth = canvas.parentElement!.clientWidth;
-      const scale = containerWidth / img.width;
+      
+      // Calculate display scale factor
+      const displayScale = containerWidth / img.width;
       
       canvas.width = containerWidth;
-      canvas.height = img.height * scale;
+      canvas.height = img.height * displayScale;
       
       ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
 
-      this.results.detections.forEach((det: any) => {
-        const [x1, y1, x2, y2] = det.box.map((v: number) => v * scale);
+      detections.forEach((det: any) => {
+        let x1: number, y1: number, x2: number, y2: number;
+
+        if (this.inferenceService.mode() === 'api') {
+          // API returns absolute pixels (e.g. 450 on a 640px base, but relative to original)
+          // We need to scale them to the current canvas display size
+          [x1, y1, x2, y2] = det.box.map((v: number) => v * displayScale);
+        } else {
+          // Local mode often uses normalized coordinates or absolute pixels depending on parser
+          // Assuming the InferenceService already normalized or scaled these for us
+          [x1, y1, x2, y2] = det.box.map((v: number) => v * displayScale);
+        }
         
-        ctx.strokeStyle = det.color;
+        ctx.strokeStyle = det.color || '#00A651';
         ctx.lineWidth = 3;
         ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
 
-        ctx.fillStyle = det.color;
-        ctx.font = '14px Outfit';
+        // Drawing Label
+        ctx.fillStyle = det.color || '#00A651';
+        ctx.font = 'bold 14px Outfit';
         const label = `${det.class} ${Math.round(det.confidence * 100)}%`;
         const textWidth = ctx.measureText(label).width;
-        ctx.fillRect(x1, y1 - 20, textWidth + 10, 20);
+        
+        // Background for text
+        ctx.fillRect(x1, y1 - 25, textWidth + 10, 25);
         
         ctx.fillStyle = '#FFFFFF';
-        ctx.fillText(label, x1 + 5, y1 - 5);
+        ctx.fillText(label, x1 + 5, y1 - 7);
       });
     };
   }
 
   getSummaryKeys(): string[] {
-    return this.results ? Object.keys(this.results.industrial_summary) : [];
+    const summary = this.inferenceService.summary();
+    return summary ? Object.keys(summary) : [];
   }
 }

+ 60 - 50
frontend/src/app/components/history/history.component.html

@@ -1,59 +1,69 @@
-<div class="container main-content">
-  <div class="glass-panel">
-    <h2 class="panel-title">Industrial Vault</h2>
-    <p class="panel-subtitle">Archived analysis history from local SQLite storage.</p>
+<div class="history-container">
+  <div class="history-header glass-panel">
+    <h1>Vault & History</h1>
+    <p>View previous detections and industrial reports.</p>
     
-    <div *ngIf="loading" class="loading-state">
-      <div class="spinner"></div>
-      <p>Synchronizing with Local DB...</p>
+    <div class="vault-tabs">
+      <button [class.active]="viewMode === 'local'" (click)="switchTab('local')" class="tab-btn">
+        <span class="icon">💻</span> Browser Cache
+      </button>
+      <button [class.active]="viewMode === 'remote'" (click)="switchTab('remote')" class="tab-btn">
+        <span class="icon">☁️</span> Industrial Cloud (API)
+      </button>
     </div>
+  </div>
 
-    <div *ngIf="!loading && history.length === 0" class="empty-state">
-      No previous records found.
-    </div>
+  <div *ngIf="loading" class="loading-state glass-panel">
+    <div class="spinner"></div>
+    <p>Synchronizing vault data...</p>
+  </div>
 
-    <div *ngIf="!loading && history.length > 0" class="history-grid">
-      <div *ngFor="let record of history" 
-           class="history-item" 
-           [class.expanded]="isExpanded(record.timestamp, record.filename)">
-        <div class="item-header" (click)="toggleExpand(record.timestamp, record.filename)">
-          <div class="record-info">
-            <span class="record-date">{{ record.timestamp }}</span>
-            <h3 class="record-filename">{{ record.filename }}</h3>
-            <div class="summary-badges">
-              <span *ngFor="let badge of getSummaryBadge(record.summary)" class="badge-item">{{ badge }}</span>
-            </div>
-          </div>
-          <div class="record-meta">
-            <div class="meta-box">
-              <span class="meta-label">Engine</span>
-              <span class="meta-value">{{ record.engine || 'onnx' }}</span>
-            </div>
-            <div class="meta-box">
-              <span class="meta-label">Latency</span>
-              <span class="meta-value">{{ record.inference_ms.toFixed(1) }} ms</span>
-            </div>
-            <div class="expand-icon">
-              <i class="chevron"></i>
-            </div>
-          </div>
+  <div *ngIf="!loading && currentHistory.length === 0" class="empty-state glass-panel">
+    <div class="empty-icon">📭</div>
+    <p *ngIf="viewMode === 'local'">Your browser cache is empty. Run a local analysis to save records.</p>
+    <p *ngIf="viewMode === 'remote'">No industrial records found on the server.</p>
+  </div>
+
+  <div *ngIf="!loading && currentHistory.length > 0" class="history-list">
+    <div *ngFor="let record of currentHistory" 
+         class="history-card glass-panel"
+         [class.expanded]="isExpanded(record.timestamp, record.filename, record.archive_id)">
+      
+      <div class="card-header" (click)="toggleExpand(record.timestamp, record.filename, record.archive_id)">
+        <div class="card-main-info">
+          <span class="timestamp">{{ record.timestamp }}</span>
+          <span class="filename">{{ record.filename }}</span>
+          <span class="engine-badge">{{ record.engine }}</span>
+        </div>
+        <div class="summary-mini">
+          <span *ngFor="let badge of getSummaryBadge(record.summary)" class="badge">{{ badge }}</span>
         </div>
+        <div class="expand-icon">{{ isExpanded(record.timestamp, record.filename, record.archive_id) ? '▾' : '▸' }}</div>
+      </div>
 
-        <!-- Expanded View: Result Visualization -->
-        <div *ngIf="isExpanded(record.timestamp, record.filename)" class="item-details">
-          <div class="result-visualization">
-            <div class="image-wrapper">
-              <img [src]="record.imageData" class="result-img" alt="Scan result">
-              <div *ngFor="let det of record.detections" 
-                   class="box-overlay"
-                   [ngStyle]="getBoxStyles(det.box, record.dimensions)"
-                   [style.border-color]="det.color"
-                   [style.background-color]="det.color + '33'">
-                <span class="box-label" [style.background-color]="det.color">
-                  {{ det.class }} {{ (det.confidence * 100).toFixed(0) }}%
-                </span>
-              </div>
-            </div>
+      <div *ngIf="isExpanded(record.timestamp, record.filename, record.archive_id)" class="card-details">
+        <hr>
+        <div class="details-grid">
+          <div class="preview-side">
+             <div *ngIf="record.imageData" class="image-wrapper">
+                <img [src]="record.imageData" alt="Analysis Preview">
+                <div *ngFor="let det of record.detections" 
+                     class="detection-box"
+                     [ngStyle]="getBoxStyles(det.box, record.dimensions)">
+                </div>
+             </div>
+             <div *ngIf="!record.imageData" class="no-image-preview">
+                <p>Cloud Archive: Metadata Only</p>
+             </div>
+          </div>
+          <div class="data-side">
+            <h3>Industrial Metrics</h3>
+            <ul>
+              <li><strong>Total Bunches:</strong> {{ record.detections.length }}</li>
+              <li><strong>Archive ID:</strong> {{ record.archive_id || 'LOCAL_ONLY' }}</li>
+              <li><strong>Inference:</strong> {{ record.inference_ms }} ms</li>
+              <li><strong>Processing:</strong> {{ (record.processing_ms || 0).toFixed(1) }} ms</li>
+            </ul>
           </div>
         </div>
       </div>

+ 63 - 13
frontend/src/app/components/history/history.component.ts

@@ -1,6 +1,7 @@
 import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { LocalHistoryService } from '../../services/local-history.service';
+import { RemoteInferenceService } from '../../core/services/remote-inference.service';
 
 @Component({
   selector: 'app-history',
@@ -10,46 +11,95 @@ import { LocalHistoryService } from '../../services/local-history.service';
   styleUrls: ['./history.component.scss']
 })
 export class HistoryComponent implements OnInit {
-  history: any[] = [];
+  localHistoryRecords: any[] = [];
+  remoteHistoryRecords: any[] = [];
+  viewMode: 'local' | 'remote' = 'local';
   loading = true;
   expandedId: string | null = null;
 
-  constructor(private localHistory: LocalHistoryService) {}
+  constructor(
+    private localHistory: LocalHistoryService,
+    private remoteInference: RemoteInferenceService
+  ) {}
 
   ngOnInit(): void {
-    // Synchronous load from LocalStorage
-    this.history = this.localHistory.getRecords();
+    this.loadLocalHistory();
+  }
+
+  loadLocalHistory(): void {
+    this.loading = true;
+    this.localHistoryRecords = this.localHistory.getRecords();
     this.loading = false;
   }
 
+  loadRemoteHistory(): void {
+    this.loading = true;
+    this.remoteInference.getHistory().subscribe({
+      next: (data) => {
+        // Normalize remote data if necessary
+        this.remoteHistoryRecords = data.map(record => ({
+          timestamp: new Date(record.created_at).toLocaleString(),
+          filename: record.filename,
+          summary: JSON.stringify(record.industrial_summary),
+          inference_ms: record.inference_ms,
+          engine: 'API AI',
+          detections: record.detections,
+          archive_id: record.archive_id,
+          // Remote records usually don't have base64 image data for privacy/storage reasons
+          // but they have the detection counts and metadata
+          imageData: null 
+        }));
+        this.loading = false;
+      },
+      error: (err) => {
+        console.error('Failed to load remote history:', err);
+        this.loading = false;
+      }
+    });
+  }
+
+  switchTab(mode: 'local' | 'remote'): void {
+    this.viewMode = mode;
+    this.expandedId = null;
+    if (mode === 'local') {
+      this.loadLocalHistory();
+    } else {
+      this.loadRemoteHistory();
+    }
+  }
+
+  get currentHistory(): any[] {
+    return this.viewMode === 'local' ? this.localHistoryRecords : this.remoteHistoryRecords;
+  }
+
   parseJSON(jsonStr: string): any {
     try {
-      return JSON.parse(jsonStr);
+      return typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;
     } catch {
       return {};
     }
   }
 
-  getSummaryBadge(summaryStr: string): string[] {
-    const summary = this.parseJSON(summaryStr);
-    return Object.entries(summary)
+  getSummaryBadge(summary: any): string[] {
+    const data = typeof summary === 'string' ? this.parseJSON(summary) : summary;
+    return Object.entries(data)
       .filter(([_, count]) => (count as number) > 0)
       .map(([key, count]) => `${key}: ${count}`);
   }
 
-  toggleExpand(timestamp: string, filename: string): void {
-    const id = `${timestamp}-${filename}`;
+  toggleExpand(timestamp: string, filename: string, archiveId?: string): void {
+    const id = archiveId || `${timestamp}-${filename}`;
     this.expandedId = this.expandedId === id ? null : id;
   }
 
-  isExpanded(timestamp: string, filename: string): boolean {
-    return this.expandedId === `${timestamp}-${filename}`;
+  isExpanded(timestamp: string, filename: string, archiveId?: string): boolean {
+    const id = archiveId || `${timestamp}-${filename}`;
+    return this.expandedId === id;
   }
 
   getBoxStyles(box: number[], dimensions: {width: number, height: number}): any {
     if (!dimensions) return {};
     
-    // Convert absolute pixels to percentage for responsive overlay
     const left = (box[0] / dimensions.width) * 100;
     const top = (box[1] / dimensions.height) * 100;
     const width = ((box[2] - box[0]) / dimensions.width) * 100;

+ 43 - 15
frontend/src/app/core/services/inference.service.ts

@@ -1,5 +1,6 @@
 import { Injectable, signal } from '@angular/core';
-import { Observable, from, map, catchError, of, switchMap } from 'rxjs';
+import { Observable, from, of } from 'rxjs';
+import { map, catchError, switchMap, tap } from 'rxjs/operators';
 import { LocalInferenceService } from '../../services/local-inference.service';
 import { RemoteInferenceService } from './remote-inference.service';
 import { ImageProcessorService } from '../../services/image-processor.service';
@@ -12,9 +13,11 @@ export type LocalEngine = 'onnx' | 'tflite';
   providedIn: 'root'
 })
 export class InferenceService {
-  // Use Signal to track processing mode and local engine
+  // Use Signals for reactive UI updates
   public mode = signal<InferenceMode>('local');
   public localEngine = signal<LocalEngine>('onnx');
+  public detections = signal<any[]>([]);
+  public summary = signal<any>({});
 
   constructor(
     private readonly localInferenceService: LocalInferenceService,
@@ -35,6 +38,8 @@ export class InferenceService {
 
   private runLocalAnalysis(imageData: string, width: number, height: number): Observable<any[]> {
     const blob = this.base64ToBlob(imageData);
+    if (!blob) return of([]);
+
     const file = new File([blob], 'capture.jpg', { type: 'image/jpeg' });
 
     const modelPath = this.localEngine() === 'onnx' 
@@ -48,6 +53,10 @@ export class InferenceService {
         if (!rawData) return [];
         return this.localInferenceService.parseDetections(rawData, 0.25, width, height);
       }),
+      tap(dets => {
+        this.detections.set(dets);
+        this.summary.set(this.calculateSummary(dets));
+      }),
       catchError(err => {
         console.error('Local Inference Hub Error:', err);
         return of([]);
@@ -57,17 +66,22 @@ export class InferenceService {
 
   private runRemoteAnalysis(imageData: string): Observable<any[]> {
     const blob = this.base64ToBlob(imageData);
+    if (!blob) return of([]);
+
     return this.remoteInferenceService.analyze(blob).pipe(
       map((response: AnalysisResponse) => {
-        // Map Result to the internal UI format
-        // Coordinate Sync: Use absolute pixels directly from API
-        return response.detections.map(det => ({
-          box: det.box, // Already [x1, y1, x2, y2]
+        const mappedDets = response.detections.map(det => ({
+          box: det.box,
           confidence: det.confidence,
           class: det.class,
           color: this.getGradeColor(det.class),
           isHealthAlert: det.is_health_alert
         }));
+        
+        this.detections.set(mappedDets);
+        this.summary.set(response.industrial_summary);
+        
+        return mappedDets;
       }),
       catchError(err => {
         console.error('Remote Inference Hub Error:', err);
@@ -76,16 +90,30 @@ export class InferenceService {
     );
   }
 
-  private base64ToBlob(base64: string): Blob {
-    const parts = base64.split(',');
-    const byteString = atob(parts[1]);
-    const mimeString = parts[0].split(':')[1].split(';')[0];
-    const ab = new ArrayBuffer(byteString.length);
-    const ia = new Uint8Array(ab);
-    for (let i = 0; i < byteString.length; i++) {
-        ia[i] = byteString.charCodeAt(i);
+  private calculateSummary(detections: any[]): any {
+    const summary: any = { 
+        'Empty_Bunch': 0, 'Underripe': 0, 'Abnormal': 0, 'Ripe': 0, 'Unripe': 0, 'Overripe': 0 
+    };
+    detections.forEach(d => { if (summary[d.class] !== undefined) summary[d.class]++; });
+    return summary;
+  }
+
+  private base64ToBlob(base64: string): Blob | null {
+    try {
+      if (!base64 || !base64.includes(',')) return null;
+      const parts = base64.split(',');
+      const byteString = atob(parts[1]);
+      const mimeString = parts[0].split(':')[1].split(';')[0];
+      const ab = new ArrayBuffer(byteString.length);
+      const ia = new Uint8Array(ab);
+      for (let i = 0; i < byteString.length; i++) {
+          ia[i] = byteString.charCodeAt(i);
+      }
+      return new Blob([ab], { type: mimeString });
+    } catch (e) {
+      console.error('Blob Conversion Failed:', e);
+      return null;
     }
-    return new Blob([ab], { type: mimeString });
   }
 
   private getGradeColor(className: string): string {

+ 10 - 1
frontend/src/app/core/services/remote-inference.service.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
-import { Observable, catchError, throwError } from 'rxjs';
+import { Observable, catchError, throwError, of } from 'rxjs';
 import { environment } from '../../../environments/environment';
 import { AnalysisResponse } from '../interfaces/palm-analysis.interface';
 
@@ -24,4 +24,13 @@ export class RemoteInferenceService {
       })
     );
   }
+
+  getHistory(): Observable<any[]> {
+    return this.http.get<any[]>(`${environment.apiUrl}/palm-oil/history`).pipe(
+      catchError((error) => {
+        console.error('Remote History Error:', error);
+        return of([]);
+      })
+    );
+  }
 }