Просмотр исходного кода

refined web UI for dispalying agents thoughts

Dr-Swopt 2 недель назад
Родитель
Сommit
390beec1bb

+ 136 - 116
src/app/chat/chat.component.css

@@ -1,15 +1,15 @@
-/* 1. Container Setup - Fixed to Viewport */
+/* 1. Container & Layout Root */
 .chat-container {
   display: grid;
   grid-template-columns: 1fr 1fr;
-  /* Use 100% of the parent tab's height, not vh */
   height: 100%;
   width: 100%;
   overflow: hidden;
   background-color: #f0f2f5;
+  font-family: 'Segoe UI', Roboto, sans-serif;
 }
 
-/* Ensure the cards take up exactly the height of the container */
+/* 2. Universal Panel & Header Styles */
 .chat-card,
 .agent-card {
   display: flex !important;
@@ -17,40 +17,34 @@
   height: 100% !important;
   min-height: 0;
   border-radius: 0 !important;
-}
-
-.message-area,
-.log-area {
-  flex: 1;
-  overflow-y: auto !important;
-  min-height: 0;
-  /* Prevents content from pushing parent height */
+  box-shadow: none !important;
 }
 
 .card-header {
-  flex-shrink: 0;
-  /* Header stays at the top */
-  padding: 16px 24px;
+  flex: 0 0 64px;
+  padding: 0 24px;
   display: flex;
   align-items: center;
   justify-content: space-between;
-  height: 64px;
   box-sizing: border-box;
   border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 }
 
-/* 3. Left Panel: Chat */
+.card-header h2 {
+  margin: 0;
+  font-size: 1.1rem;
+  font-weight: 600;
+}
+
+/* 3. Left Panel: Chat Section */
 .chat-card {
   background: #ffffff !important;
   border-right: 1px solid #e0e0e0 !important;
 }
 
-/* This is the magic part that fixes your scroll issue */
 .message-area {
-  flex: 1 1 auto;
-  /* Grow and shrink as needed */
+  flex: 1;
   overflow-y: auto !important;
-  /* Only this part scrolls */
   min-height: 0;
   padding: 24px !important;
   display: flex;
@@ -59,31 +53,6 @@
   background-color: #ffffff;
 }
 
-.input-section {
-  flex-shrink: 0;
-  /* Input stays at the bottom */
-  padding: 16px 24px !important;
-  border-top: 1px solid #f0f0f0;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  margin: 0 !important;
-  /* Material actions sometimes have weird margins */
-}
-
-/* 4. Right Panel: Trace & Logic */
-.agent-card {
-  background: #1e1e1e !important;
-  color: #d4d4d4;
-}
-
-.agent-card .card-header {
-  background: #252526;
-  border-bottom: 1px solid #333;
-}
-
-/* --- Rest of your visual styles remain the same --- */
-
 .message-bubble {
   max-width: 80%;
   padding: 12px 16px;
@@ -114,71 +83,72 @@
   text-transform: uppercase;
 }
 
+.input-section {
+  flex: 0 0 auto;
+  padding: 16px 24px !important;
+  border-top: 1px solid #f0f0f0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: white;
+}
+
 .input-section mat-form-field {
   flex: 1;
 }
 
-/* Log Entry Container */
-.log-entry {
-  border-bottom: 1px solid #333;
-  padding: 16px 20px;
-  background: #1e1e1e;
+/* Status Indicator Dot */
+.status-indicator {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  background: #ccc;
+  transition: background 0.3s;
 }
 
-/* Individual Sections (Input vs Message) */
-.log-section {
-  margin-top: 12px;
+.status-indicator.active {
+  background: #4caf50;
+  box-shadow: 0 0 8px #4caf50;
+  animation: pulse-dot 1.5s infinite;
 }
 
-/* Metadata Labels (INPUT, MESSAGE) */
-.log-section .label {
-  display: block;
-  font-family: 'Segoe UI', sans-serif;
-  font-size: 0.65rem;
-  font-weight: 800;
-  color: #888;
-  margin-bottom: 4px;
-  letter-spacing: 1px;
+@keyframes pulse-dot {
+  0% { opacity: 1; }
+  50% { opacity: 0.4; }
+  100% { opacity: 1; }
 }
 
-/* Code block for raw input data */
-.code-block {
-  margin: 0;
-  font-family: 'Fira Code', 'Consolas', monospace;
-  font-size: 0.8rem;
-  background: #121212;
-  color: #9cdcfe;
-  padding: 8px;
-  border-radius: 4px;
-  border: 1px solid #333;
-  overflow-x: auto;
-  white-space: pre-wrap;
-  /* Wraps long JSON strings */
+/* 4. Right Panel: Trace & Logic (Dark Theme) */
+.agent-card {
+  background: #1e1e1e !important;
+  color: #d4d4d4;
 }
 
-/* Text block for human-readable messages */
-.message-text {
-  font-family: 'Segoe UI', sans-serif;
-  font-size: 0.9rem;
-  line-height: 1.4;
-  color: #d4d4d4;
-  padding-left: 4px;
+.agent-card .card-header {
+  background: #252526;
+  border-bottom: 1px solid #333;
+  color: white;
 }
 
-/* Logic Status Pilling */
-.status-pill.completed {
-  background: #2e7d32;
-  color: #fff;
+.log-area {
+  flex: 1;
+  overflow-y: auto !important;
+  min-height: 0;
+  padding: 0 !important;
+  background: #1e1e1e;
 }
 
-.status-pill.processing {
-  background: #e65100;
-  color: #fff;
+.log-entry {
+  border-bottom: 1px solid #333;
+  padding: 20px;
+  background: #1e1e1e;
 }
 
-.status-pill.error {
-  background: #c62828;
-  color: #fff;
+.log-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
 }
 
 .node-badge {
@@ -188,23 +158,82 @@
   background: rgba(78, 201, 176, 0.1);
   padding: 4px 8px;
   border-radius: 4px;
+  border: 1px solid rgba(78, 201, 176, 0.2);
 }
 
+.status-pill {
+  font-size: 0.7rem;
+  padding: 2px 8px;
+  border-radius: 12px;
+  text-transform: uppercase;
+  font-weight: 700;
+}
+
+.status-pill.completed { background: #2e7d32; color: #fff; }
+.status-pill.processing { background: #e65100; color: #fff; }
+.status-pill.error { background: #c62828; color: #fff; }
+
+/* Log Body Content */
 .log-body {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.message-text {
+  font-family: 'Segoe UI', sans-serif;
+  font-size: 0.95rem;
+  line-height: 1.4;
+  color: #d4d4d4;
+  border-left: 3px solid #3f51b5;
+  padding-left: 12px;
+  margin-bottom: 8px;
+}
+
+.message-text.processing-text {
+  border-left-color: #e65100;
+  color: #ffcc80;
+}
+
+.log-section .label {
+  display: block;
+  font-size: 0.65rem;
+  font-weight: 800;
+  color: #777;
   margin-top: 10px;
-  font-family: 'Fira Code', monospace;
+  margin-bottom: 4px;
+  letter-spacing: 1px;
+}
+
+.simple-value {
+  font-family: 'Roboto Mono', monospace;
   font-size: 0.85rem;
-  color: #9cdcfe;
+  color: #ce9178;
+  padding-left: 4px;
+}
+
+.code-block {
+  margin: 0;
+  font-family: 'Fira Code', 'Consolas', monospace;
+  font-size: 0.8rem;
   background: #121212;
+  color: #9cdcfe;
   padding: 12px;
   border-radius: 6px;
+  border: 1px solid #333;
+  overflow-x: auto;
   white-space: pre-wrap;
+  word-break: break-all;
 }
 
+/* Typing Animation */
 .typing-indicator {
   display: flex;
   gap: 4px;
-  padding: 8px;
+  padding: 12px;
+  align-self: flex-start;
+  background: #f1f3f4;
+  border-radius: 12px;
 }
 
 .typing-indicator span {
@@ -212,32 +241,23 @@
   height: 6px;
   background: #90a4ae;
   border-radius: 50%;
-  animation: pulse 1.5s infinite;
+  animation: pulse-typing 1.5s infinite;
 }
 
-@keyframes pulse {
+.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
+.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
 
-  0%,
-  100% {
-    transform: translateY(0);
-    opacity: 0.4;
-  }
-
-  50% {
-    transform: translateY(-4px);
-    opacity: 1;
-  }
+@keyframes pulse-typing {
+  0%, 100% { transform: translateY(0); opacity: 0.4; }
+  50% { transform: translateY(-4px); opacity: 1; }
 }
 
+/* Scrollbar Customization */
+.message-area::-webkit-scrollbar,
 .log-area::-webkit-scrollbar {
-  width: 8px;
-}
-
-.log-area::-webkit-scrollbar-track {
-  background: #1e1e1e;
+  width: 6px;
 }
 
-.log-area::-webkit-scrollbar-thumb {
-  background: #333;
-  border-radius: 4px;
-}
+.log-area::-webkit-scrollbar-track { background: #1e1e1e; }
+.log-area::-webkit-scrollbar-thumb { background: #444; border-radius: 10px; }
+.message-area::-webkit-scrollbar-thumb { background: #ccc; border-radius: 10px; }

+ 22 - 11
src/app/chat/chat.component.html

@@ -5,7 +5,7 @@
       <span class="status-indicator" [class.active]="loading"></span>
     </div>
 
-    <mat-card-content class="message-area">
+    <mat-card-content class="message-area" #scrollContainer>
       <div *ngFor="let msg of messages" [ngClass]="['message-bubble', msg.sender]">
         <div class="content">{{ msg.content }}</div>
         <div class="timestamp">{{ msg.sender === 'user' ? 'You' : 'Assistant' }}</div>
@@ -18,9 +18,16 @@
 
     <mat-card-actions class="input-section">
       <mat-form-field appearance="outline" subscriptSizing="dynamic">
-        <input matInput placeholder="Ask anything..." [(ngModel)]="inputMessage" (keyup.enter)="sendMessage()" />
+        <input matInput 
+               placeholder="Ask anything..." 
+               [(ngModel)]="inputMessage" 
+               (keyup.enter)="sendMessage()" 
+               [disabled]="loading" />
       </mat-form-field>
-      <button mat-flat-button color="primary" (click)="sendMessage()" [disabled]="!inputMessage.trim()">
+      <button mat-flat-button 
+              color="primary" 
+              (click)="sendMessage()" 
+              [disabled]="!inputMessage.trim() || loading">
         Send
       </button>
     </mat-card-actions>
@@ -30,6 +37,7 @@
     <div class="card-header">
       <h2>Trace & Logic</h2>
     </div>
+
     <mat-card-content class="log-area">
       <div *ngFor="let thought of agentThoughts" class="log-entry">
         <div class="log-header">
@@ -38,17 +46,20 @@
         </div>
 
         <div class="log-body">
-          <div class="log-section">
-            <span class="label">INPUT</span>
-            <pre class="code-block">{{ thought.input ? (thought.input | json) : 'N/A' }}</pre>
+          <div class="message-text" [class.processing-text]="thought.status === 'processing'">
+            {{ thought.message || (thought.status === 'processing' ? 'Node is executing...' : 'Step completed.') }}
           </div>
 
-          <div class="log-section">
-            <span class="label">MESSAGE</span>
-            <div class="message-text">
-              {{ thought.message || 'Processing node output...' }}
+          <ng-container *ngFor="let entry of $any(thought) | keyvalue">
+            <div class="log-section" *ngIf="shouldShowAttribute(entry.key, entry.value)">
+              <span class="label">{{ asString(entry.key) | uppercase }}</span>
+
+              <pre class="code-block" *ngIf="isObject(entry.value); else plainValue">{{ entry.value | json }}</pre>
+              <ng-template #plainValue>
+                <div class="simple-value">{{ entry.value }}</div>
+              </ng-template>
             </div>
-          </div>
+          </ng-container>
         </div>
       </div>
     </mat-card-content>

+ 66 - 34
src/app/chat/chat.component.ts

@@ -1,12 +1,13 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { MatCardModule } from '@angular/material/card';
 import { MatInputModule } from '@angular/material/input';
 import { MatButtonModule } from '@angular/material/button';
 import { MatListModule } from '@angular/material/list';
-import { webConfig } from '../config';
 import { io, Socket } from 'socket.io-client';
+import { webConfig } from '../config';
+import { ThoughtPayload } from '../interfaces/interface';
 
 interface ChatMessage {
   content: string;
@@ -20,60 +21,91 @@ interface ChatMessage {
   templateUrl: './chat.component.html',
   styleUrls: ['./chat.component.css']
 })
-export class ChatComponent {
+export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
+  @ViewChild('scrollContainer') private scrollContainer!: ElementRef;
+
+  // Configuration constants
+  public readonly mandatoryKeys = ['node', 'status'];
+  
   messages: ChatMessage[] = [];
-  inputMessage: string = '';
+  inputMessage = '';
   loading = false;
+  agentThoughts: ThoughtPayload[] = [];
+  
+  private socket!: Socket;
 
-  agentThoughts: any[] = [];
+  ngOnInit() {
+    this.initSocketConnection();
+  }
 
-  private socket: Socket;
+  ngOnDestroy() {
+    if (this.socket) {
+      this.socket.disconnect();
+      console.log('Socket disconnected and cleaned up.');
+    }
+  }
 
-  constructor() {
-    // Connect to FFB socket namespace
-    this.socket = io(`${webConfig.exposedUrl}/ffb`);
+  /**
+   * Reality Check: Auto-scrolling is essential for chat. 
+   * This ensures the user doesn't have to manually scroll for every response.
+   */
+  ngAfterViewChecked() {
+    this.scrollToBottom();
+  }
 
-    this.socket.on('connect', () => {
-      console.log('Connected to FFB Gateway');
-    });
+  private initSocketConnection() {
+    this.socket = io(`${webConfig.exposedUrl}/ffb`);
 
-    this.socket.on('disconnect', () => {
-      console.log('Disconnected from FFB Gateway');
-    });
+    this.socket.on('connect', () => console.log('Connected to FFB Gateway'));
 
-    // Listen for agent output
-    this.socket.on('agent_thought', (payload) => {
-      console.log('Received agent thought:', payload);
+    this.socket.on('agent_thought', (payload: ThoughtPayload) => {
       this.agentThoughts.push(payload);
     });
 
-    // Listen for chat response
     this.socket.on('chat_response', (payload: { message: string }) => {
-      console.log('Received chat response:', payload);
       this.messages.push({ content: payload.message, sender: 'bot' });
       this.loading = false;
     });
 
-    // Optional: handle errors
-    this.socket.on('error', (payload) => {
-      console.error('Socket error:', payload);
-    });
+    this.socket.on('error', (err) => console.error('Socket error:', err));
   }
 
+  // --- Helper Methods ---
+
   sendMessage() {
-    if (!this.inputMessage.trim()) return;
+    const trimmedMessage = this.inputMessage.trim();
+    if (!trimmedMessage || this.loading) return;
 
-    const userMessage: ChatMessage = { content: this.inputMessage, sender: 'user' };
-    this.messages.push(userMessage);
+    this.messages.push({ content: trimmedMessage, sender: 'user' });
+    this.loading = true;
+    this.agentThoughts = []; // Reset trace for new turn
 
-    const messageToSend = this.inputMessage;
+    this.socket.emit('chat', { message: trimmedMessage });
     this.inputMessage = '';
-    this.loading = true;
+  }
+
+  private scrollToBottom(): void {
+    try {
+      const el = this.scrollContainer.nativeElement;
+      el.scrollTop = el.scrollHeight;
+    } catch (err) {}
+  }
 
-    // Clear previous agent thoughts
-    this.agentThoughts = [];
+  // --- Template Logic Helpers ---
 
-    // Emit chat event via socket
-    this.socket.emit('chat', { message: messageToSend });
+  asString(key: unknown): string {
+    return String(key);
   }
-}
+
+  shouldShowAttribute(key: unknown, value: any): boolean {
+    const sKey = String(key);
+    return !this.mandatoryKeys.includes(sKey) && 
+           value !== null && 
+           value !== undefined && 
+           value !== '';
+  }
+
+  isObject(value: any): boolean {
+    return value !== null && typeof value === 'object';
+  }
+}

+ 16 - 0
src/app/interfaces/interface.ts

@@ -19,4 +19,20 @@ export interface IncomingMessage {
   action: `Attendance` | `Payment`,
   name: string,
   time: Date
+}
+
+export interface ThoughtPayload {
+  node: string;
+  status: 'processing' | 'completed' | 'error';
+  message?: string;    // Human-readable status (e.g., "Analyzing intent...")
+  context?: any;
+  query?: any;
+  filter?: any;
+  pipeline?: any;
+  results?: any;
+  resultsCount?: number;
+  dataContextLength?: number;
+  input?: any;         // The raw data going IN
+  output?: any;        // The data coming OUT (replacing 'result', 'results', 'pipeline')
+  metadata?: Record<string, any>; // For extra stuff like resultsCount or contextLength
 }