CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
index.tsx296 linesDownload Raw Back to debug-panel
1'use client';2 3import { useState, useEffect, useRef, useMemo, useSyncExternalStore } from 'react';4import { Button } from '@/components/ui/button';5import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';6import { ChevronDown, ChevronUp, ChevronsUpDown, ChevronsDownUp, Bug, Trash2, Copy, Download } from 'lucide-react';7import { PanelContainer, PanelHeader } from '@/components/ui/panel';8import { MemoryMonitor } from './memory-monitor';9import { MessagesTab, type ForceState } from './messages-tab';10import { configManager } from '@/lib/config/storage';11import { requestSnapshotStore } from '@/lib/llm/request-snapshot';12import { toast } from 'sonner';13 14import type { DebugEvent } from '@/lib/stores/types';15 16interface DebugPanelProps {17  events: DebugEvent[];18  onClear?: () => void;19  onClose?: () => void;20}21 22export function DebugPanel({ events, onClear, onClose }: DebugPanelProps) {23  const [activeTab, setActiveTab] = useState<'events' | 'messages'>('events');24  const [filter, setFilter] = useState<string>('');25  const eventsEndRef = useRef<HTMLDivElement>(null);26  const [autoScroll, setAutoScroll] = useState(true);27  const [streamDebug, setStreamDebug] = useState<boolean>(() => configManager.getDebugStreamEnabled());28  const [eventsForce, setEventsForce] = useState<ForceState>(null);29  const messageSnapshot = useSyncExternalStore(30    (l) => requestSnapshotStore.subscribe(l),31    () => requestSnapshotStore.getSnapshot(),32    () => requestSnapshotStore.getSnapshot(),33  );34 35  // Compress consecutive assistant_delta, tool_param_delta, and reasoning_delta events36  // Only store count, not individual events - prevents O(N²) memory growth37  const compressedEvents = useMemo(() => {38    const result: DebugEvent[] = [];39    let currentDeltaGroup: DebugEvent | null = null;40 41    const COMPRESSIBLE_EVENTS = new Set(['assistant_delta', 'tool_param_delta', 'reasoning_delta']);42 43    for (const event of events) {44      const shouldCompress = COMPRESSIBLE_EVENTS.has(event.event);45 46      if (shouldCompress) {47        // If we're already in a group of the same type, just increment count48        if (currentDeltaGroup && currentDeltaGroup.event === event.event) {49          currentDeltaGroup.count = (currentDeltaGroup.count || 1) + 1;50          // Don't accumulate data.all - that causes O(N²) memory usage51          // The count is sufficient for debugging purposes52        } else {53          // Start a new group54          if (currentDeltaGroup) {55            result.push(currentDeltaGroup);56          }57          currentDeltaGroup = { ...event, count: 1 };58        }59      } else {60        // Non-compressible event, flush any current group and add this event61        if (currentDeltaGroup) {62          result.push(currentDeltaGroup);63          currentDeltaGroup = null;64        }65        result.push(event);66      }67    }68 69    // Flush any remaining group70    if (currentDeltaGroup) {71      result.push(currentDeltaGroup);72    }73 74    return result;75  }, [events]);76 77  // Scroll to bottom when new events arrive78  useEffect(() => {79    if (autoScroll && eventsEndRef.current) {80      eventsEndRef.current.scrollIntoView({ behavior: 'smooth' });81    }82  }, [compressedEvents, autoScroll]);83 84  // Clear all events85  const handleClear = () => {86    onClear?.();87  };88 89  // Export events as JSON90  const handleExport = () => {91    const json = JSON.stringify(events, null, 2);92    const blob = new Blob([json], { type: 'application/json' });93    const url = URL.createObjectURL(blob);94    const a = document.createElement('a');95    a.href = url;96    a.download = `debug-events-${Date.now()}.json`;97    a.click();98    URL.revokeObjectURL(url);99  };100 101  const handleCopyEvents = async () => {102    try {103      await navigator.clipboard.writeText(JSON.stringify(events, null, 2));104      toast.success('Events copied');105    } catch {106      toast.error('Copy failed');107    }108  };109 110  // Filter events111  const filteredEvents = filter112    ? compressedEvents.filter(e => e.event.toLowerCase().includes(filter.toLowerCase()))113    : compressedEvents;114 115  // Group events by type (use original events for accurate counts)116  const eventCounts = events.reduce((acc, e) => {117    acc[e.event] = (acc[e.event] || 0) + 1;118    return acc;119  }, {} as Record<string, number>);120 121  return (122    <PanelContainer>123      <PanelHeader124        icon={Bug}125        title="Debug Events"126        onClose={onClose}127        panelKey="debug"128      >129        <MemoryMonitor />130      </PanelHeader>131 132      {/* Tabs */}133      <div className="flex border-b border-border text-xs">134        {([135          ['events', `Events (${filteredEvents.length}/${events.length})`],136          ['messages', messageSnapshot ? `Messages (${messageSnapshot.messages.length})` : 'Messages'],137        ] as const).map(([key, label]) => (138          <button139            key={key}140            onClick={() => setActiveTab(key)}141            className={`px-3 py-1.5 border-b-2 -mb-px whitespace-nowrap ${142              activeTab === key143                ? 'border-primary text-foreground font-semibold'144                : 'border-transparent text-muted-foreground hover:text-foreground'145            }`}146          >147            {label}148          </button>149        ))}150      </div>151 152      {activeTab === 'messages' ? (153        <MessagesTab />154      ) : (155        <>156          {/* Event Counts */}157          <div className="p-2 border-b border-border bg-muted/20 text-xs">158            <div className="flex flex-wrap gap-2">159              {Object.entries(eventCounts).map(([event, count]) => (160                <button161                  key={event}162                  onClick={() => setFilter(filter === event ? '' : event)}163                  className={`px-2 py-1 rounded ${164                    filter === event165                      ? 'bg-primary text-primary-foreground'166                      : 'bg-muted hover:bg-muted/80'167                  }`}168                >169                  {event} ({count})170                </button>171              ))}172            </div>173          </div>174 175          {/* Filter Input */}176          <div className="p-2 border-b border-border">177            <input178              type="text"179              placeholder="Filter events..."180              value={filter}181              onChange={(e) => setFilter(e.target.value)}182              className="w-full px-2 py-1 text-xs rounded bg-background border border-border"183            />184          </div>185 186          {/* Toggles + actions */}187          <div className="p-2 border-b border-border flex items-center gap-3 flex-wrap">188            <label className="text-xs flex items-center gap-1 cursor-pointer">189              <input190                type="checkbox"191                checked={autoScroll}192                onChange={(e) => setAutoScroll(e.target.checked)}193                className="rounded"194              />195              Auto-scroll196            </label>197            <label198              className="text-xs flex items-center gap-1 cursor-pointer"199              title="Emit llm_request and stream_raw_chunk events. Ephemeral, not persisted."200            >201              <input202                type="checkbox"203                checked={streamDebug}204                onChange={(e) => {205                  setStreamDebug(e.target.checked);206                  configManager.setDebugStreamEnabled(e.target.checked);207                }}208                className="rounded"209              />210              Stream debug211            </label>212            <div className="ml-auto flex items-center">213              <Button variant="ghost" size="sm" className="h-5 w-5 p-0" title="Expand all"214                onClick={() => setEventsForce(v => ({ open: true, v: (v?.v ?? 0) + 1 }))}>215                <ChevronsUpDown className="h-3 w-3" />216              </Button>217              <Button variant="ghost" size="sm" className="h-5 w-5 p-0" title="Collapse all"218                onClick={() => setEventsForce(v => ({ open: false, v: (v?.v ?? 0) + 1 }))}>219                <ChevronsDownUp className="h-3 w-3" />220              </Button>221              <Button variant="ghost" size="sm" className="h-5 w-5 p-0" title="Copy events as JSON" onClick={handleCopyEvents}>222                <Copy className="h-3 w-3" />223              </Button>224              <Button variant="ghost" size="sm" className="h-5 w-5 p-0" title="Export to JSON file" onClick={handleExport}>225                <Download className="h-3 w-3" />226              </Button>227              <Button variant="ghost" size="sm" className="h-5 w-5 p-0" title="Clear all events" onClick={handleClear}>228                <Trash2 className="h-3 w-3" />229              </Button>230            </div>231          </div>232 233          {/* Events List */}234          <div className="flex-1 overflow-y-auto p-2 space-y-1">235            {filteredEvents.length === 0 ? (236              <div className="text-xs text-muted-foreground text-center p-4">237                No events yet. Events will appear here as they occur.238              </div>239            ) : (240              filteredEvents.map((event) => (241                <EventItem key={event.id} event={event} force={eventsForce} />242              ))243            )}244            <div ref={eventsEndRef} />245          </div>246        </>247      )}248 249    </PanelContainer>250  );251}252 253function EventItem({ event, force }: { event: DebugEvent; force?: ForceState }) {254  const [isOpen, setIsOpen] = useState(false);255  // Expand/collapse-all override — rows remain individually toggleable after256  useEffect(() => {257    if (force) setIsOpen(force.open);258  }, [force]);259  const time = new Date(event.timestamp).toLocaleTimeString();260 261  // Color code by event type262  const getEventColor = (eventType: string) => {263    if (eventType.includes('error') || eventType.includes('failed')) return 'text-red-500';264    if (eventType.includes('retry')) return 'text-yellow-500';265    if (eventType.includes('completed') || eventType.includes('success')) return 'text-green-500';266    if (eventType.includes('tool')) return 'text-blue-500';267    if (eventType.includes('agent')) return 'text-purple-500';268    if (eventType.includes('plan')) return 'text-orange-500';269    return 'text-foreground';270  };271 272  return (273    <Collapsible open={isOpen} onOpenChange={setIsOpen}>274      <CollapsibleTrigger className="w-full text-left">275        <div className="flex items-center gap-2 p-1.5 rounded hover:bg-muted/50 text-xs">276          {isOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}277          <span className="text-muted-foreground font-mono">{time}</span>278          <span className={`font-semibold ${getEventColor(event.event)}`}>279            {event.event}280          </span>281          {event.count && event.count > 1 && (282            <span className="text-muted-foreground font-mono">283              ({event.count})284            </span>285          )}286        </div>287      </CollapsibleTrigger>288      <CollapsibleContent>289        <div className="ml-6 p-2 bg-muted/30 rounded text-xs font-mono overflow-x-auto">290          <pre>{JSON.stringify(event.data, null, 2)}</pre>291        </div>292      </CollapsibleContent>293    </Collapsible>294  );295}296