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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user