浏览代码

fixed analysis positioning issue

Dr-Swopt 6 天之前
父节点
当前提交
6c9a0d6765

+ 28 - 8
src/app/components/analyzer/analyzer.component.ts

@@ -294,9 +294,23 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
     reader.onload = (e) => {
       const base64 = e.target?.result as string;
       if (!base64) return;
-      this.snappedFrame = base64;
-      this.visionSocket.sendBase64(base64);
-      this.waitForSocketResult();
+
+      // Rescale the gallery image to 640×640 before sending and storing as
+      // snappedFrame. The backend always runs inference in 640×640 space, so
+      // the canvas background must match that same square crop to keep bounding
+      // boxes aligned with the displayed image.
+      const img = new Image();
+      img.onload = () => {
+        const offscreen = document.createElement('canvas');
+        offscreen.width = 640;
+        offscreen.height = 640;
+        offscreen.getContext('2d')!.drawImage(img, 0, 0, 640, 640);
+        const scaled640 = offscreen.toDataURL('image/jpeg');
+        this.snappedFrame = scaled640;
+        this.visionSocket.sendBase64(scaled640);
+        this.waitForSocketResult();
+      };
+      img.src = base64;
     };
     reader.readAsDataURL(this.socketGalleryFile);
   }
@@ -346,19 +360,25 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
     const img = new Image();
     img.src = this.snappedFrame;
     img.onload = () => {
+      // The canvas display width matches the container.
+      // The canvas logical size is always set to 640×640 because the backend
+      // always runs inference in 640×640 space — coords are always 640-relative
+      // regardless of whether the source was a webcam snap (already 640×640) or
+      // a gallery image (arbitrary size sent as-is; backend rescales internally).
       const containerWidth = canvas.parentElement!.clientWidth || 640;
-      const scale = containerWidth / img.width;
       canvas.width = containerWidth;
-      canvas.height = img.height * scale;
+      canvas.height = containerWidth; // square: 640px inference space
 
       const ctx = canvas.getContext('2d');
       if (!ctx) return;
+      // Draw the source image stretched to fill the square canvas
       ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
 
+      // Map 640-space coords → canvas pixels via percentage
+      const scaleX = canvas.width / 640;
+      const scaleY = canvas.height / 640;
+
       detections.forEach((det: any) => {
-        // Backend returns absolute coords in 640×640 space
-        const scaleX = canvas.width / 640;
-        const scaleY = canvas.height / 640;
         const [x1, y1, x2, y2] = det.box;
         const color = GRADE_COLORS[det.class] || '#00A651';
 

+ 28 - 1
src/app/components/chatbot/chatbot.component.html

@@ -26,6 +26,21 @@
           Connect
         </button>
       </div>
+      <div class="setup-gate__status-row">
+        <span class="webhook-config__status-dot"
+              [class.dot-green]="surveillance.n8nStatus() === 'online'"
+              [class.dot-yellow]="surveillance.n8nStatus() === 'untested'"
+              [class.dot-red]="surveillance.n8nStatus() === 'offline'">
+        </span>
+        <span class="setup-gate__status-label"
+              [class.state-online]="surveillance.n8nStatus() === 'online'"
+              [class.state-warn]="surveillance.n8nStatus() === 'untested'"
+              [class.state-offline]="surveillance.n8nStatus() === 'offline'">
+          n8n: {{ surveillance.n8nStatus() === 'online' ? 'Reachable'
+                : surveillance.n8nStatus() === 'offline' ? 'Unreachable'
+                : 'Untested — click Connect to verify' }}
+        </span>
+      </div>
       <div class="setup-gate__hint">Lego 06 · Webhook URL · n8n → Ollama → Angular</div>
     </div>
   }
@@ -98,7 +113,14 @@
         <!-- Inline webhook URL reconfigure overlay -->
         @if (showWebhookConfig()) {
           <div class="webhook-config">
-            <div class="webhook-config__label">n8n Webhook URL</div>
+            <div class="webhook-config__label-row">
+              <span class="webhook-config__label">n8n Webhook URL</span>
+              <span class="webhook-config__status-dot"
+                    [class.dot-green]="surveillance.n8nStatus() === 'online'"
+                    [class.dot-yellow]="surveillance.n8nStatus() === 'untested'"
+                    [class.dot-red]="surveillance.n8nStatus() === 'offline'">
+              </span>
+            </div>
             <div class="webhook-config__row">
               <input
                 class="webhook-config__input"
@@ -106,6 +128,11 @@
                 [(ngModel)]="webhookInputDraft"
                 placeholder="http://localhost:5678/webhook/..."
               />
+              <button class="btn btn-outline webhook-config__verify"
+                      (click)="testConnection()"
+                      [disabled]="testingConnection() || !webhookUrl()">
+                {{ testingConnection() ? '...' : '🔍 Verify' }}
+              </button>
               <button class="btn btn-primary webhook-config__save" (click)="saveWebhookUrl()">Save</button>
               <button class="btn btn-outline webhook-config__cancel" (click)="toggleWebhookConfig()">✕</button>
             </div>

+ 39 - 1
src/app/components/chatbot/chatbot.component.scss

@@ -65,6 +65,20 @@
   padding: 12px 24px;
 }
 
+.setup-gate__status-row {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  font-size: 0.72rem;
+}
+
+.setup-gate__status-label {
+  color: var(--text-secondary);
+  &.state-online  { color: #00a651; }
+  &.state-warn    { color: #ffc107; }
+  &.state-offline { color: #dc3545; }
+}
+
 .setup-gate__hint {
   font-size: 0.62rem;
   color: var(--text-secondary);
@@ -345,13 +359,37 @@
   animation: slideUp 0.15s ease-out;
 }
 
+.webhook-config__label-row {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  margin-bottom: 8px;
+}
+
 .webhook-config__label {
   font-size: 0.68rem;
   font-weight: 700;
   color: var(--accent-gold);
   text-transform: uppercase;
   letter-spacing: 0.08em;
-  margin-bottom: 8px;
+}
+
+.webhook-config__status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+
+  &.dot-green  { background: #00a651; box-shadow: 0 0 5px rgba(0,166,81,0.7); }
+  &.dot-yellow { background: #ffc107; box-shadow: 0 0 5px rgba(255,193,7,0.6); }
+  &.dot-red    { background: #dc3545; box-shadow: 0 0 5px rgba(220,53,69,0.6); }
+}
+
+.webhook-config__verify {
+  padding: 9px 12px;
+  font-size: 0.8rem;
+  flex-shrink: 0;
+  white-space: nowrap;
 }
 
 .webhook-config__row {

+ 18 - 3
src/app/components/performance-hud/performance-hud.component.html

@@ -12,9 +12,24 @@
       <div class="hud-header" cdkDragHandle>
         <span class="hud-drag-grip">⠿</span>
         <span class="hud-title">SYSTEM MONITOR</span>
-        <span class="hud-badge" [class.online]="surveillance.connected()">
-          {{ surveillance.connected() ? 'LIVE' : 'OFFLINE' }}
-        </span>
+        <!-- Status LEDs — Nest + n8n always visible in the header -->
+        <div class="hud-header-leds">
+          <span class="hud-led-group" title="NestJS Socket: {{ surveillance.connected() ? 'Connected' : 'Offline' }}">
+            <span class="hud-led-dot"
+                  [class.led-green]="surveillance.connected()"
+                  [class.led-red]="!surveillance.connected()">
+            </span>
+            <span class="hud-led-label">Nest</span>
+          </span>
+          <span class="hud-led-group" title="n8n Webhook: {{ surveillance.n8nStatus() }}">
+            <span class="hud-led-dot"
+                  [class.led-green]="surveillance.n8nStatus() === 'online'"
+                  [class.led-yellow]="surveillance.n8nStatus() === 'untested'"
+                  [class.led-red]="surveillance.n8nStatus() === 'offline'">
+            </span>
+            <span class="hud-led-label">n8n</span>
+          </span>
+        </div>
       </div>
 
       @if (!surveillance.connected()) {

+ 48 - 0
src/app/components/performance-hud/performance-hud.component.scss

@@ -118,6 +118,54 @@
   }
 }
 
+// ── Header LED status lights ───────────────────────────────────────────────────
+.hud-header-leds {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-left: auto;
+}
+
+.hud-led-group {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  cursor: default;
+}
+
+.hud-led-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+  background: #333;
+  transition: background 0.3s ease, box-shadow 0.3s ease;
+
+  &.led-green {
+    background: #00a651;
+    box-shadow: 0 0 6px rgba(0, 166, 81, 0.8);
+    animation: pulse 1.5s infinite;
+  }
+
+  &.led-yellow {
+    background: #ffc107;
+    box-shadow: 0 0 5px rgba(255, 193, 7, 0.7);
+  }
+
+  &.led-red {
+    background: #dc3545;
+    box-shadow: 0 0 5px rgba(220, 53, 69, 0.7);
+  }
+}
+
+.hud-led-label {
+  font-size: 0.58rem;
+  font-weight: 700;
+  color: #444;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+}
+
 // Per-service row
 .hud-row {
   padding: 8px 0;