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
This commit is contained in:
Stefan
2025-11-27 18:20:12 +01:00
parent 54d92eaae5
commit c319904f00
6 changed files with 352 additions and 3 deletions
+51
View File
@@ -0,0 +1,51 @@
# Debug Mode
Debug mode allows you to inspect the exact prompts sent to the LLM and the responses received. This is useful for troubleshooting AI behavior and understanding how the tutor works.
## Enabling Debug Mode
1. Create a `.env.local` file in the root directory (if it doesn't exist)
2. Add the following line:
```
NEXT_PUBLIC_DEBUG=true
```
3. Restart the development server (`npm run dev`)
## Using Debug Mode
When debug mode is enabled, you'll see:
### Floating Debug Panel
- A purple "Debug Mode" button appears in the bottom-right corner
- Click to expand and see all LLM interactions
- Shows a list of all prompts sent during your session
- Click any entry to see the full prompt and response
### Features
- **Copy to Clipboard**: Copy prompts or responses for analysis
- **Clear Entries**: Clear all debug entries
- **Metadata**: View additional context like FEN, personality, language, etc.
- **Timestamps**: See when each interaction occurred
### What's Tracked
**Tutor Component:**
- Move Analysis (automatic after each move)
- Best Move Requests (when user asks for the best move)
- Hint Requests (when user asks for a hint)
- General Questions (any other user question)
**Analysis Page:**
- Move Analysis for each move in the game
- Includes move number, color, FEN before/after, evaluation changes
## Disabling Debug Mode
1. Remove or comment out the `NEXT_PUBLIC_DEBUG=true` line in `.env.local`
2. Or set it to `false`: `NEXT_PUBLIC_DEBUG=false`
3. Restart the development server
## Privacy Note
Debug mode only runs locally in your browser. No debug data is sent to any server.
+23 -1
View File
@@ -17,6 +17,7 @@ import { lookupPossibleOpenings, buildMoveSequenceFromSteps, OpeningMetadata } f
import { getGenAIModel } from "@/lib/gemini";
import { ChatSession } from "@google/generative-ai";
import ReactMarkdown from "react-markdown";
import { useDebug } from "@/contexts/DebugContext";
interface MoveStep {
san: string;
@@ -42,6 +43,7 @@ export default function AnalysisPage() {
const [language, setLanguage] = useState<SupportedLanguage>("en");
const [apiKey, setApiKey] = useState<string | null>(null);
const t = useTranslation(language);
const { addEntry } = useDebug();
const [input, setInput] = useState("");
const [detectedFormat, setDetectedFormat] = useState<ChessFormat | null>(null);
@@ -286,8 +288,28 @@ INSTRUCTIONS:
- Keep it educational and stay true to your personality tone.`;
const result = await chatSession.sendMessage(prompt);
const responseText = result.response.text();
if (!cancelled) {
setComments(prev => ({ ...prev, [currentIndex]: result.response.text() }));
setComments(prev => ({ ...prev, [currentIndex]: responseText }));
// Track debug entry
addEntry({
type: 'analysis',
action: `Move ${step.moveNumber} Analysis (${step.color})`,
prompt,
response: responseText,
metadata: {
moveNumber: step.moveNumber,
san: step.san,
color: step.color,
fenBefore: step.fenBefore,
fenAfter: step.fenAfter,
cpLoss: delta,
personality: selectedPersonality.name,
language,
}
});
}
} catch (err) {
console.error("Commentary failed", err);
+7 -2
View File
@@ -3,6 +3,8 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Footer from "@/components/Footer";
import { DebugProvider } from "@/contexts/DebugContext";
import DebugPanel from "@/components/DebugPanel";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -29,8 +31,11 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
>
{children}
<Footer />
<DebugProvider>
{children}
<Footer />
<DebugPanel />
</DebugProvider>
</body>
</html>
);
+192
View File
@@ -0,0 +1,192 @@
"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>
);
}
+20
View File
@@ -14,6 +14,7 @@ import ReactMarkdown from "react-markdown";
import { useTranslation } from '@/lib/i18n/useTranslation';
import { SupportedLanguage } from '@/lib/i18n/translations';
import { DetectedTactic } from '@/lib/tacticDetection';
import { useDebug } from '@/contexts/DebugContext';
interface TutorProps {
game: Chess;
@@ -45,6 +46,7 @@ export function Tutor({ game, currentFen, userMove, computerMove, stockfish, eva
const [isLoading, setIsLoading] = useState(false);
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const { addEntry } = useDebug();
const t = useTranslation(language);
@@ -372,6 +374,24 @@ INSTRUCTIONS:
const response = await result.response;
const textResponse = response.text();
// Track debug entry
const actionType = isSystemMessage ? "Move Analysis" :
text.toLowerCase().includes("best move") ? "Best Move Request" :
text.toLowerCase().includes("hint") ? "Hint Request" :
"General Question";
addEntry({
type: 'tutor',
action: actionType,
prompt: finalPrompt,
response: textResponse,
metadata: {
fen: currentFen,
personality: personality.name,
language,
}
});
setMessages(prev => [...prev, { role: "model", text: textResponse, timestamp: Date.now() }]);
} catch (error) {
console.error("Chat Error:", error);
+59
View File
@@ -0,0 +1,59 @@
"use client";
import React, { createContext, useContext, useState, ReactNode } from 'react';
export interface DebugEntry {
id: string;
timestamp: number;
type: 'tutor' | 'analysis';
action: string; // e.g., "Best Move", "Hint", "General Question", "Move Analysis"
prompt: string;
response?: string;
metadata?: Record<string, any>;
}
interface DebugContextType {
isDebugMode: boolean;
entries: DebugEntry[];
addEntry: (entry: Omit<DebugEntry, 'id' | 'timestamp'>) => void;
clearEntries: () => void;
}
const DebugContext = createContext<DebugContextType | undefined>(undefined);
export function DebugProvider({ children }: { children: ReactNode }) {
// Check if debug mode is enabled via environment variable
const isDebugMode = process.env.NEXT_PUBLIC_DEBUG === 'true';
const [entries, setEntries] = useState<DebugEntry[]>([]);
const addEntry = (entry: Omit<DebugEntry, 'id' | 'timestamp'>) => {
if (!isDebugMode) return; // Don't track if debug mode is off
const newEntry: DebugEntry = {
...entry,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
};
setEntries(prev => [...prev, newEntry]);
};
const clearEntries = () => {
setEntries([]);
};
return (
<DebugContext.Provider value={{ isDebugMode, entries, addEntry, clearEntries }}>
{children}
</DebugContext.Provider>
);
}
export function useDebug() {
const context = useContext(DebugContext);
if (context === undefined) {
throw new Error('useDebug must be used within a DebugProvider');
}
return context;
}