78.2 Panel Enhancement

Documentation for Sessions

78.2 Panel Enhancement

Session: 78 Focus: Tabbed Interface, Panel Intelligence, UX Improvements Status: Completed


Overview

Session 78 transformed the GoGrab AI Panel from a simple chat interface into a comprehensive command center with four tabs, panel intelligence integration, and enhanced UX features.


Tabbed Interface

Tab Structure

Tab Icon Description Keyboard Shortcut
Chat 💬 AI conversation with streaming responses Cmd+1
Agents 🤖 Grid view of connected agents Cmd+2
Handoffs 📋 Kanban board for task handoffs Cmd+3
Mind 🧠 Agent thoughts, knowledge, decisions Cmd+4

Implementation

// apps/gograb-ai/src/app/pages/panel/panel.component.ts

// Tab State
readonly activeTab = signal<'chat' | 'agents' | 'handoffs' | 'mind'>('chat');

/**
 * Set active tab and initialize dashboard/mind if needed
 */
setActiveTab(tab: 'chat' | 'agents' | 'handoffs' | 'mind'): void {
  this.activeTab.set(tab);

  // Lazy-initialize dashboard when first accessing agents/handoffs tab
  if ((tab === 'agents' || tab === 'handoffs') && !this.dashboardInitialized) {
    this.initializeDashboard();
  }

  // Clear new handoff count when viewing handoffs tab (mark as "read")
  if (tab === 'handoffs') {
    this.newHandoffCount.set(0);
  }

  // Lazy-initialize mind when first accessing mind tab
  if (tab === 'mind' && !this.mindInitialized) {
    this.initializeMind();
  }
}

/**
 * Handle keyboard shortcuts for tab switching
 */
@HostListener('document:keydown', ['$event'])
handleKeyboardShortcut(event: KeyboardEvent): void {
  if (!(event.metaKey || event.ctrlKey)) return;

  switch (event.key) {
    case '1':
      event.preventDefault();
      this.setActiveTab('chat');
      break;
    case '2':
      event.preventDefault();
      this.setActiveTab('agents');
      break;
    case '3':
      event.preventDefault();
      this.setActiveTab('handoffs');
      break;
    case '4':
      event.preventDefault();
      this.setActiveTab('mind');
      break;
  }
}

Panel Intelligence Integration

Services Used

Service Purpose
IntentDetectionService Detects user intent from input text
PanelSessionPersistenceService Persists chat sessions across reloads
OutputFactoryService Creates consistent draggable outputs

Intent Detection

import {
  IntentDetectionService,
  PanelSessionPersistenceService,
  OutputFactoryService,
  IntelligentMessage,
  DraggableOutput,
  DetectedIntent,
  PanelContext,
} from '@libs-ai/panel-intelligence';

// In sendMessage()
sendMessage(): void {
  const text = this._inputText().trim();
  if (!text || this.aiApi.isLoading()) return;

  // Detect user intent before processing
  const context = this.determineContext();
  const detectedIntent = this.intentService.detectIntent(text, context);
  this.currentIntent.set(detectedIntent);

  console.log('[Panel] 🎯 Detected intent:', detectedIntent.intent, 
              'confidence:', detectedIntent.confidence.toFixed(2));

  // Add user message
  const userMessageId = this.addMessage('user', text);
  this._inputText.set('');

  // Persist user message to session
  this.persistMessage(userMessageId, 'user', text);

  // Use streaming API with intent-aware context
  const baseContext = this.embeddedAdapter.context();
  this.streamSubscription = this.aiApi.streamChat({
    prompt: text,
    context: baseContext ? {
      ...baseContext,
      metadata: {
        ...baseContext.metadata,
        detectedIntent: detectedIntent.intent,
        intentConfidence: detectedIntent.confidence,
        extractedData: detectedIntent.extractedData,
      },
    } : undefined,
  }).subscribe({
    // ... handlers
  });
}

Session Persistence

Sessions are automatically restored when the panel loads:

/**
 * Restore session from persistence
 */
private restoreSession(): void {
  const currentSession = this.sessionPersistence.currentSession();

  if (currentSession && currentSession.messages.length > 0) {
    // Convert IntelligentMessage to DisplayMessage
    const restoredMessages: DisplayMessage[] = currentSession.messages.map(msg =>
      this.intelligentMessageToDisplay(msg)
    );

    this.messages.set(restoredMessages);
    this.sessionRestored = true;

    console.log('[Panel] 💾 Restored session with', restoredMessages.length, 'messages');

    // Restore draggable outputs from session
    const outputs = currentSession.outputs;
    if (outputs && outputs.length > 0) {
      const draggableItems = outputs.map(output => this.outputToDraggableItem(output));
      this.draggableItems.set([
        ...this.draggableItems(), // Keep default items
        ...draggableItems,
      ]);
    }
  } else {
    // Create a new session for the current context
    const context = this.determineContext();
    this.sessionPersistence.createSession(context);
  }
}

Agent Mind Tab

Components Used

Component Purpose
ActiveThoughtsComponent Shows real-time agent thoughts
KnowledgeBrowserComponent Visualizes knowledge graph
DecisionTimelineComponent Shows decision history

Implementation

import {
  ActiveThoughtsComponent,
  KnowledgeBrowserComponent,
  DecisionTimelineComponent,
  AgentMindService,
  KnowledgeGraphData,
  TimelineDecision,
} from '@libs-ai/agent-mind';

// Mind Tab State
readonly selectedAgentId = signal<string>('cursor-angular-agent');
readonly mindLoading = signal<boolean>(false);
readonly mindKnowledgeGraph = signal<KnowledgeGraphData>({
  nodes: [],
  edges: [],
  metadata: { totalNodes: 0, totalEdges: 0, density: 0, lastUpdated: 0 }
});
readonly mindDecisions = signal<TimelineDecision[]>([]);

/**
 * Load agent mind data
 */
private async loadMindData(): Promise<void> {
  this.mindLoading.set(true);

  try {
    const agentId = this.selectedAgentId();
    const mindState = await this.agentMindService.getAgentMind(agentId);

    this.mindKnowledgeGraph.set(mindState.knowledgeGraph);
    this.mindDecisions.set(mindState.timeline);

    console.log('[Panel] 🧠 Mind loaded:', 
                mindState.timeline.length, 'decisions,', 
                mindState.knowledgeGraph.nodes.length, 'nodes');
  } catch (error) {
    console.error('[Panel] Failed to load mind data:', error);
  } finally {
    this.mindLoading.set(false);
  }
}

Real-Time Notifications

In-Panel Toasts

// Notifications State
readonly notifications = signal<Array<{
  id: string;
  title: string;
  message: string;
  type: 'handoff' | 'info' | 'error';
  timestamp: number;
}>>([]);

/**
 * Show a handoff notification both in-panel and to host (extension)
 */
private showHandoffNotification(title: string, message: string, handoff: any): void {
  const notificationId = `notification-${Date.now()}`;

  // If embedded, send notification to parent/host
  if (this.isConnected()) {
    window.parent.postMessage({
      type: 'AI_NOTIFICATION',
      timestamp: Date.now(),
      payload: {
        title,
        message,
        type: 'handoff',
        handoffId: handoff.handoffId,
        priority: handoff.priority,
        status: handoff.status,
      },
    }, '*');
  }

  // Add to in-panel notifications
  this.notifications.update(list => [
    {
      id: notificationId,
      title,
      message,
      type: 'handoff',
      timestamp: Date.now(),
    },
    ...list.slice(0, 9), // Keep last 10 notifications
  ]);

  // Auto-remove notification after 8 seconds
  setTimeout(() => {
    this.dismissNotification(notificationId);
  }, 8000);
}

Handoff Subscriptions

/**
 * Setup real-time handoff notification subscriptions
 */
private setupHandoffNotifications(): void {
  // Subscribe to new handoff creation events
  const createdSub = this.dashboardWs.handoffCreated$.subscribe(handoff => {
    console.log('[Panel] 📥 New handoff received:', handoff.handoffId);

    // Increment new handoff counter (only if not on handoffs tab)
    if (this.activeTab() !== 'handoffs') {
      this.newHandoffCount.update(count => count + 1);
    }

    // Show notification
    this.showHandoffNotification(
      'New Handoff',
      handoff.summary || 'A new task has been assigned',
      handoff
    );
  });

  // Subscribe to handoff updates
  const updatedSub = this.dashboardWs.handoffUpdated$.subscribe(handoff => {
    if (handoff.status === 'acknowledged' || handoff.status === 'in_progress') {
      this.showHandoffNotification(
        `Handoff ${handoff.status === 'acknowledged' ? 'Acknowledged' : 'In Progress'}`,
        handoff.summary || 'Handoff status updated',
        handoff
      );
    } else if (handoff.status === 'completed') {
      this.showHandoffNotification(
        'Handoff Completed',
        handoff.summary || 'A handoff has been completed',
        handoff
      );
    }
  });

  this.handoffSubscriptions.push(createdSub, updatedSub);
}

UX Enhancements

Loading Overlay

// Loading State for async operations
readonly isAsyncLoading = signal(false);
readonly loadingMessage = signal('');

// In template
@if (isAsyncLoading()) {
  <ux-loading-overlay
    [visible]="isAsyncLoading()"
    [config]="{
      title: loadingMessage(),
      subtitle: 'Processing your request...',
      variant: 'elegant',
      transparentBg: true
    }"
  />
}

Error Banner

// Connection Error State
readonly connectionError = signal<string | null>(null);

// In template
@if (connectionError()) {
  <div class="error-banner">
    <span>⚠️ {{ connectionError() }}</span>
    <button (click)="retry()">Retry</button>
  </div>
}

// Check for connection errors
if (err.message?.includes('Failed to fetch') || err.status === 0) {
  this.connectionError.set('Backend unavailable. Please ensure the AI service is running.');
}

Quick Actions Bar

<div class="quick-actions-bar">
  <button class="action-btn" (click)="quickGenerate()" title="Quick Generate">
    🎯 Generate
  </button>
  <button class="action-btn" (click)="quickCode()" title="Generate Code">
    📝 Code
  </button>
  <button class="action-btn" (click)="quickSearch()" title="Search Knowledge">
    🔍 Search
  </button>
  <button class="action-btn" (click)="quickAsk()" title="Quick Question">
    ⚡ Quick
  </button>
</div>
/**
 * Quick generate action - prompts AI to generate content
 */
quickGenerate(): void {
  this._inputText.set('Generate a new ');
  setTimeout(() => {
    const input = document.querySelector<HTMLInputElement>('.input-area input');
    input?.focus();
  }, 0);
}

/**
 * Quick code action - prompts AI to generate code
 */
quickCode(): void {
  this._inputText.set('Write code for ');
  setTimeout(() => {
    const input = document.querySelector<HTMLInputElement>('.input-area input');
    input?.focus();
  }, 0);
}

CSS Styling Highlights

Tab Animation

.tab-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: tabFadeIn 0.2s ease-out;
}

@keyframes tabFadeIn {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Notification Toast

.notification-toast {
  display: flex;
  flex-direction: column;
  padding: 0.625rem 0.75rem;
  border-radius: 8px;
  background: var(--ai-surface);
  border: 1px solid var(--ai-border);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  animation: slideInRight 0.3s ease-out;
  cursor: pointer;
  transition: all 0.2s;

  &:hover {
    border-color: var(--ai-primary);
    transform: translateX(-2px);
  }

  &.handoff {
    border-left: 3px solid #f59e0b;
  }
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

Badge Pulse

.badge {
  &.new-badge {
    background: #ef4444;
    animation: pulse-badge 1.5s ease-in-out infinite;
  }
}

@keyframes pulse-badge {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}

Component Imports

@Component({
  selector: 'gograb-ai-panel',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    // Multi-Agent Dashboard
    AgentGridComponent,
    HandoffKanbanComponent,
    // Agent Mind
    ActiveThoughtsComponent,
    KnowledgeBrowserComponent,
    DecisionTimelineComponent,
    // UX
    LoadingOverlayComponent,
  ],
  // ...
})

Performance Considerations

Lazy Initialization

Components are initialized only when their tab is first accessed:

// Lazy-initialize dashboard when first accessing agents/handoffs tab
if ((tab === 'agents' || tab === 'handoffs') && !this.dashboardInitialized) {
  this.initializeDashboard();
}

// Lazy-initialize mind when first accessing mind tab
if (tab === 'mind' && !this.mindInitialized) {
  this.initializeMind();
}

Subscription Cleanup

All subscriptions are properly cleaned up on destroy:

ngOnDestroy(): void {
  this.cleanupDragListeners();
  this.streamSubscription?.unsubscribe();

  // Clean up handoff subscriptions
  this.handoffSubscriptions.forEach(sub => sub.unsubscribe());
  this.handoffSubscriptions = [];
}