Files
chess-project/src/components/DebugPanel.tsx
T
Stefan c319904f00 feat: Add debug mode to inspect LLM prompts and responses
- Add DebugContext to track all LLM interactions
- Create DebugPanel component with floating UI
- Track prompts/responses in Tutor component (move analysis, hints, questions)
- Track prompts/responses in Analysis page (move commentary)
- Add NEXT_PUBLIC_DEBUG environment variable to enable/disable
- Include metadata (FEN, personality, language, etc.) in debug entries
- Add copy-to-clipboard and clear functionality
- Add DEBUG_MODE.md documentation
2025-11-27 18:20:12 +01:00

193 lines
8.9 KiB
TypeScript

"use client";
import React, { useState } from 'react';
import { Bug, ChevronDown, ChevronUp, Copy, Trash2 } from 'lucide-react';
import { useDebug, DebugEntry } from '@/contexts/DebugContext';
interface DebugPanelProps {
/** Optional: Filter to show only specific entry ID */
entryId?: string;
/** Optional: Show inline next to content (default: false, shows as floating panel) */
inline?: boolean;
}
export default function DebugPanel({ entryId, inline = false }: DebugPanelProps) {
const { isDebugMode, entries, clearEntries } = useDebug();
const [isExpanded, setIsExpanded] = useState(false);
const [selectedEntry, setSelectedEntry] = useState<DebugEntry | null>(null);
if (!isDebugMode) return null;
const displayEntries = entryId
? entries.filter(e => e.id === entryId)
: entries;
const latestEntry = displayEntries[displayEntries.length - 1];
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
};
// Inline mode: Show icon next to content
if (inline && latestEntry) {
return (
<div className="inline-flex items-center ml-2">
<button
onClick={() => setSelectedEntry(selectedEntry?.id === latestEntry.id ? null : latestEntry)}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-purple-600 dark:text-purple-400"
title="Show debug info"
>
<Bug size={16} />
</button>
{selectedEntry?.id === latestEntry.id && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={() => setSelectedEntry(null)}>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-4xl max-h-[80vh] overflow-auto" onClick={(e) => e.stopPropagation()}>
<DebugEntryDetail entry={selectedEntry} onClose={() => setSelectedEntry(null)} />
</div>
</div>
)}
</div>
);
}
// Floating panel mode: Show all entries
return (
<div className="fixed bottom-4 right-4 z-50 max-w-md">
<div className="bg-purple-600 dark:bg-purple-700 text-white rounded-lg shadow-lg">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-2 flex items-center justify-between hover:bg-purple-700 dark:hover:bg-purple-800 rounded-t-lg"
>
<div className="flex items-center gap-2">
<Bug size={20} />
<span className="font-semibold">Debug Mode</span>
<span className="text-xs bg-purple-800 dark:bg-purple-900 px-2 py-1 rounded">
{entries.length} {entries.length === 1 ? 'entry' : 'entries'}
</span>
</div>
{isExpanded ? <ChevronDown size={20} /> : <ChevronUp size={20} />}
</button>
{isExpanded && (
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-b-lg max-h-96 overflow-auto">
<div className="p-2 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-sm font-semibold">LLM Interactions</span>
<button
onClick={clearEntries}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
title="Clear all entries"
>
<Trash2 size={16} />
</button>
</div>
{entries.length === 0 ? (
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
No debug entries yet
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{entries.map((entry) => (
<button
key={entry.id}
onClick={() => setSelectedEntry(entry)}
className="w-full p-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex justify-between items-start mb-1">
<span className="font-semibold text-sm">{entry.action}</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
{formatTimestamp(entry.timestamp)}
</span>
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{entry.type === 'tutor' ? '🎓 Tutor' : '📊 Analysis'}
</div>
</button>
))}
</div>
)}
</div>
)}
</div>
{selectedEntry && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={() => setSelectedEntry(null)}>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-4xl max-h-[80vh] overflow-auto" onClick={(e) => e.stopPropagation()}>
<DebugEntryDetail entry={selectedEntry} onClose={() => setSelectedEntry(null)} />
</div>
</div>
)}
</div>
);
}
function DebugEntryDetail({ entry, onClose }: { entry: DebugEntry; onClose: () => void }) {
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
return (
<div className="space-y-4">
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-bold">{entry.action}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{entry.type === 'tutor' ? '🎓 Tutor' : '📊 Analysis'} {new Date(entry.timestamp).toLocaleString()}
</p>
</div>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
</button>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<h4 className="font-semibold">Prompt</h4>
<button
onClick={() => copyToClipboard(entry.prompt)}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
>
<Copy size={14} /> Copy
</button>
</div>
<pre className="bg-gray-100 dark:bg-gray-900 p-3 rounded text-xs overflow-auto max-h-64 whitespace-pre-wrap">
{entry.prompt}
</pre>
</div>
{entry.response && (
<div>
<div className="flex justify-between items-center mb-2">
<h4 className="font-semibold">Response</h4>
<button
onClick={() => copyToClipboard(entry.response!)}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
>
<Copy size={14} /> Copy
</button>
</div>
<pre className="bg-gray-100 dark:bg-gray-900 p-3 rounded text-xs overflow-auto max-h-64 whitespace-pre-wrap">
{entry.response}
</pre>
</div>
)}
{entry.metadata && Object.keys(entry.metadata).length > 0 && (
<div>
<h4 className="font-semibold mb-2">Metadata</h4>
<pre className="bg-gray-100 dark:bg-gray-900 p-3 rounded text-xs overflow-auto max-h-32">
{JSON.stringify(entry.metadata, null, 2)}
</pre>
</div>
)}
</div>
);
}