# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What This Is PalmOilAI — an Angular 20 single-page application for palm oil fruit bunch (FFB) ripeness detection. Supports three inference engines: `tflite` and `onnx` run 100% client-side in the browser via WASM; `socket` streams to the NestJS backend for server-side inference. The NestJS backend is required for the socket engine, chat proxy, and remote history. Detection classes: `Ripe`, `Unripe`, `Underripe`, `Overripe`, `Abnormal`, `Empty_Bunch`. ## Commands ```bash npm install ng serve # Dev server at http://localhost:4200 ng serve --host 0.0.0.0 # Expose to LAN (device testing) ng build # Production build → dist/ ng test # Karma + Jasmine unit tests ``` ## Architecture ### Routing `/analyzer` → `AnalyzerComponent` (main scanner UI, default route) `/history` → `HistoryComponent` (local + remote vault of past scans) `/chatbot` → `ChatbotComponent` (chat interface backed by n8n via WebSocket) ### Components (`src/app/components/`) **`AnalyzerComponent`** — Three-engine scan hub: - Engine selector: `tflite` (browser WASM), `onnx` (browser WASM), `socket` (NestJS remote) - Browser engines: file upload → local WASM inference → bounding-box canvas draw - Socket engine: webcam snap or gallery image → Base64 → `vision:analyze` → canvas draw - Batch ingestion: queues multiple files, sends one-by-one via socket, collects results into `FullSessionReport` - Evidence canvas: separate from snap canvas; renders bounding boxes for audit drill-down - Computes `BatchResult[]` and `FullSessionReport` (timing, success/fail counts, raw tensor sample) **`HistoryComponent`** — Dual-tab vault: - **Local tab**: browser `localStorage` via `LocalHistoryService`, up to 20 records - **Remote tab**: NestJS SQLite records via `RemoteInferenceService`, grouped by `batch_id` into `BatchSessionGroup` with aggregate summaries - Per-record delete and clear-all (with confirmation) **`ChatbotComponent`** — n8n RAG chat: - Messages sent via `ChatSocketService.send()` (promise-based one-shot listener) - NestJS proxies to n8n server-to-server (no browser CORS) - Unwraps n8n array response to first element - Shows per-message response latency; error fallback with NestJS→n8n diagnostic hint **`PerformanceHudComponent`** — Floating, draggable, collapsible overlay (CDK drag-drop): - Live CPU/RAM for NestJS, n8n, and Ollama processes via `SurveillanceService` - Mounts in app root and never unmounts (zombie socket pattern) **`HeaderComponent`** — App header with dark/light theme toggle via `ThemeService`. **`BottomNavComponent`** — Mobile fixed tab bar (hidden on desktop). Three tabs: Scanner | Intelligence | Vault. ### Services (`src/app/services/` and `src/app/core/services/`) **`LocalInferenceService`** — Core browser AI engine. Dispatches to ONNX or TFLite backend based on model file extension: - ONNX path: `onnxruntime-web` with WASM execution provider. Input: `[1, 3, 640, 640]` (CHW). - TFLite path: Uses the globally-injected `window.tflite` object. Input is transposed CHW→HWC to `[1, 640, 640, 3]` before prediction. **`ImageProcessorService`** — Resizes any image to 640×640 via offscreen Canvas, then converts RGBA→CHW `Float32Array` normalized to `[0.0, 1.0]`. **`LocalHistoryService`** — Persists up to 20 scan records (FIFO) to `localStorage` key `palm_oil_vault`. Each record includes detection summary, latency, engine type, Base64 thumbnail, and bounding boxes. **`InferenceService`** (`src/app/core/services/`) — Hub dispatcher using Angular Signals: - Signals: `mode` (local | api), `localEngine` (onnx | tflite), `detections`, `summary` - `analyze(base64, w, h)` dispatches to local (`LocalInferenceService`) or remote (`RemoteInferenceService`) - Local path: base64 → blob → file → `LocalInferenceService` → parse detections - Remote path: blob → `RemoteInferenceService.analyze()` → map detections - Computes class-count summary **`RemoteInferenceService`** (`src/app/core/services/`) — HTTP client for NestJS: - `analyze(blob)` → POST `/analyze` → `AnalysisResponse` - `getHistory()`, `deleteRecord(archiveId)`, `clearAll()` - Hits `http://localhost:3000` **`VisionSocketService`** — Socket.io client for NestJS `/vision` namespace: - `snapAndSend(videoEl, batchId?)` — captures webcam frame and emits as raw Base64 - `sendBase64(dataUrl, batchId?)` — emits gallery image as raw Base64 - Hard rule: raw, uncompressed Base64 strings only — no binary frames, no WebRTC - Signals: `connected`, `analyzing`, `lastResult`, `lastError` - Zombie socket pattern: socket is never explicitly closed **`ChatSocketService`** — Socket.io client sharing NestJS `/vision` namespace for chat: - `send(message): Promise` — one-shot listener; emits `chat:send`, resolves on `chat:result` or rejects on `chat:error` - Unwraps n8n response variants (`output | answer | response | text`) - Zombie socket pattern **`SurveillanceService`** — Socket.io monitoring client for NestJS `/monitor` namespace: - Emits `monitor:subscribe` on connect; receives `monitor:data` (500ms ticks) and `monitor:status` - Signals: `metrics`, `connected`, `nestStatus` (computed), `n8nStatus` (checking | ready | not-ready) - `formatBytes(bytes)` helper for memory display - Zombie socket pattern **`ThemeService`** — Dark/light theme toggle: - Adds/removes `theme-dark` / `theme-light` class on `document.body` via `Renderer2` - Persists preference to `localStorage` (`palm-ai-theme`) - Exposes `currentTheme$` observable and `isDark()` boolean ### Key Interfaces (`src/app/core/interfaces/`) **`BatchResult`** — Single scan audit record: - `image_id`, `timestamp`, `status` (ok | error) - `detections[]`: `bunch_id`, `ripeness_class`, `confidence_pct`, `is_health_alert`, `bounding_box`, `norm_box` - `performance`: `inference_ms`, `processing_ms`, `round_trip_ms` - `technical_evidence`: engine, archive_id, total_count, threshold, industrial_summary, raw_tensor_sample - `localBlobUrl?`, `error?` **`FullSessionReport`** — Batch run summary: - `session_id` (UUID), `generated_at` - `meta`: total_images, successful, failed, total_time_ms, avg_inference_ms, avg_round_trip_ms - `results: BatchResult[]` ### TFLite Bundler Constraint `@tensorflow/tfjs-tflite` is a legacy CommonJS/UMD hybrid incompatible with the Vite/esbuild ESM bundler. Both `tf.min.js` and `tf-tflite.min.js` are loaded as **global scripts** in `angular.json`, not as ES modules. This populates `window.tflite` and `window.tf` before Angular bootstraps. Do not import them via `import` statements. ### Required Manual Assets (not installed by npm) These binary files must be placed manually after `npm install`: | Path | Source | |---|---| | `src/assets/models/onnx/best.onnx` | YOLOv8 model file | | `src/assets/models/tflite/best_float32.tflite` | Full-precision TFLite model | | `src/assets/models/tflite/best_float16.tflite` | Half-precision TFLite model | | `src/assets/wasm/` | Copy from `node_modules/onnxruntime-web/dist/` | | `src/assets/tflite-wasm/` | Copy 7 files from `node_modules/@tensorflow/tfjs-tflite/dist/` | ### Environment - `environment.nestWsUrl` = `http://localhost:3000` - Angular 20, standalone components, Signals-based reactivity ### Inference Pipeline (socket engine) 1. User uploads image or webcam snap → `VisionSocketService.sendBase64()` → NestJS 2. NestJS ONNX inference + SQLite write → `vision:result` with `BatchResult` payload 3. `AnalyzerComponent` appends to `BatchResult[]`, draws bounding boxes on evidence canvas 4. On session end → `FullSessionReport` assembled from collected results ### Inference Pipeline (browser engines) 1. User uploads image → `ImageProcessorService.processImage()` → CHW Float32Array 2. `LocalInferenceService.loadModel(modelPath)` → creates ONNX session or loads TFLite model 3. `LocalInferenceService.runInference(input)` → raw output tensor 4. `LocalInferenceService.parseDetections(rawData, threshold)` → filtered detections with class labels and bounding boxes 5. `AnalyzerComponent` draws bounding boxes on canvas 6. `LocalHistoryService.save()` → persists to localStorage