'use client'; import { useState } from 'react'; import { client } from '@/lib/api'; import { useBank } from '@/lib/bank-context'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Info } from 'lucide-react'; import JsonView from 'react18-json-view'; import 'react18-json-view/src/style.css'; type Phase = 'retrieval' | 'rrf' | 'rerank' | 'final' | 'json'; type RetrievalMethod = 'semantic' | 'bm25' | 'graph' | 'temporal'; type FactType = 'world' | 'bank' | 'opinion'; type Budget = 'low' | 'mid' | 'high'; // Helper component for column headers with tooltips const ColumnHeader = ({ label, tooltip }: { label: string; tooltip: string }) => (
{label} {tooltip}
); interface SearchPane { id: number; query: string; factTypes: FactType[]; budget: Budget; maxTokens: number; includeChunks: boolean; includeEntities: boolean; results: any[] | null; trace: any | null; loading: boolean; currentPhase: Phase; currentRetrievalMethod: RetrievalMethod; currentRetrievalFactType: FactType | null; showRawJson: boolean; } export function SearchDebugView() { const { currentBank } = useBank(); const [panes, setPanes] = useState([ { id: 1, query: '', factTypes: ['world'], budget: 'mid', maxTokens: 4096, includeChunks: false, includeEntities: false, results: null, trace: null, loading: false, currentPhase: 'retrieval', currentRetrievalMethod: 'semantic', currentRetrievalFactType: null, showRawJson: false, }, ]); const [nextPaneId, setNextPaneId] = useState(2); const addPane = () => { setPanes([ ...panes, { id: nextPaneId, query: '', factTypes: ['world'], budget: 'mid', maxTokens: 4096, includeChunks: false, includeEntities: false, results: null, trace: null, loading: false, currentPhase: 'retrieval', currentRetrievalMethod: 'semantic', currentRetrievalFactType: null, showRawJson: false, }, ]); setNextPaneId(nextPaneId + 1); }; const removePane = (id: number) => { if (panes.length > 1) { setPanes(panes.filter((p) => p.id !== id)); } }; const updatePane = (id: number, updates: Partial) => { setPanes(panes.map((p) => (p.id === id ? { ...p, ...updates } : p))); }; const runSearch = async (paneId: number) => { if (!currentBank) { alert('Please select a memory bank first'); return; } const pane = panes.find((p) => p.id === paneId); if (!pane || !pane.query || pane.factTypes.length === 0) { if (pane?.factTypes.length === 0) { alert('Please select at least one fact type'); } return; } updatePane(paneId, { loading: true }); try { // Always pass fact types as array for consistent behavior const requestBody: any = { bank_id: currentBank, query: pane.query, types: pane.factTypes, budget: pane.budget, max_tokens: pane.maxTokens, trace: true, include: { entities: pane.includeEntities ? { max_tokens: 500 } : null, chunks: pane.includeChunks ? { max_tokens: 8192 } : null } }; const data: any = await client.recall(requestBody); // Set default fact type for retrieval view (first selected fact type) const defaultFactType = pane.currentRetrievalFactType || pane.factTypes[0]; updatePane(paneId, { results: data.results || [], trace: data.trace || null, loading: false, currentRetrievalFactType: defaultFactType, currentPhase: 'final', }); } catch (error) { console.error('Error running search:', error); alert('Error running search: ' + (error as Error).message); updatePane(paneId, { loading: false }); } }; const renderRetrievalResults = (pane: SearchPane) => { if (!pane.trace || !pane.trace.retrieval_results) { return
No retrieval data available
; } // Filter by retrieval method const methodData = pane.trace.retrieval_results.find( (m: any) => m.method_name === pane.currentRetrievalMethod ); if (!methodData || !methodData.results || methodData.results.length === 0) { return (
No results from this retrieval method
); } // Filter by fact type if multiple fact types are selected let filteredResults = methodData.results; if (pane.factTypes.length > 1 && pane.currentRetrievalFactType) { filteredResults = methodData.results.filter( (result: any) => result.fact_type === pane.currentRetrievalFactType ); } // Get method-specific score description const scoreTooltips: Record = { semantic: "Vector similarity score - measures conceptual similarity and paraphrasing (higher = more relevant)", bm25: "BM25 exact match score - measures keyword/phrase overlap for names, technical terms (higher = more exact matches)", graph: "Entity traversal score - measures connection strength through related entities and indirect relationships (higher = stronger connection)", temporal: "Time-filtered relevance score - combines temporal proximity with semantic relevance for time-based queries (higher = better match in timeframe)" }; const scoreTooltip = scoreTooltips[pane.currentRetrievalMethod] || "Relevance score from this retrieval method (higher = more relevant)"; return (

{methodData.method_name.toUpperCase()} Retrieval {pane.currentRetrievalFactType && pane.factTypes.length > 1 && ( {pane.currentRetrievalFactType} facts only )} {' '}({filteredResults.length} results{pane.factTypes.length > 1 && ` of ${methodData.results.length}`}, {methodData.duration_seconds?.toFixed(3)}s)

{pane.factTypes.length > 1 && ( )} {filteredResults.map((result: any, idx: number) => ( #{result.rank} {result.text} {pane.factTypes.length > 1 && ( {result.fact_type || 'unknown'} )} {result.score?.toFixed(4)} ))}
); }; const renderRRFMerge = (pane: SearchPane) => { if (!pane.trace || !pane.trace.rrf_merged || pane.trace.rrf_merged.length === 0) { return
No RRF merge data available
; } return (

RRF Merge Results ({pane.trace.rrf_merged.length} candidates) {pane.factTypes.length > 1 && ( Unified across all fact types )}

Reciprocal Rank Fusion combines rankings from different retrieval methods {pane.factTypes.length > 1 ? ' and fact types' : ''}.

{pane.trace.rrf_merged.map((result: any, idx: number) => { // Try multiple possible field names for source ranks const sourceRanksData = result.source_ranks || result.sourceRanks || result.ranks || {}; const sourceRanks = Object.entries(sourceRanksData).length > 0 ? Object.entries(sourceRanksData) .map(([method, rank]) => `${method}: #${rank}`) .join(', ') : 'N/A'; return ( #{result.final_rrf_rank || result.finalRrfRank || result.rank} {result.text} {(result.rrf_score || result.rrfScore || result.score)?.toFixed(4)} {sourceRanks} ); })}
); }; const renderReranking = (pane: SearchPane) => { if (!pane.trace || !pane.trace.reranked || pane.trace.reranked.length === 0) { return
No reranking data available
; } return (

Reranking Results ({pane.trace.reranked.length} results) {pane.factTypes.length > 1 && ( Unified across all fact types )}

Cross-encoder reranker adjusts scores based on semantic relevance.{' '} Highlight = rank improved vs RRF

{pane.trace.reranked.map((result: any, idx: number) => { const improved = result.rank_change > 0; const rowBg = improved ? 'bg-secondary/20' : ''; const changeDisplay = result.rank_change > 0 ? `↑${result.rank_change}` : result.rank_change < 0 ? `↓${Math.abs(result.rank_change)}` : '='; const changeColor = result.rank_change > 0 ? 'text-green-700' : result.rank_change < 0 ? 'text-red-700' : 'text-gray-600'; // Format score components with better structure const components = result.score_components || {}; const crossEncoder = components.cross_encoder || components.crossEncoder || 0; const heuristics = Object.entries(components) .filter(([key]) => key !== 'cross_encoder' && key !== 'crossEncoder') .map(([key, val]: [string, any]) => `${key}: ${val.toFixed(3)}`) .join(', '); const componentDisplay = (
Cross-Encoder: {crossEncoder.toFixed(4)}
{heuristics &&
Heuristics: {heuristics}
}
); return ( #{result.rerank_rank} #{result.rrf_rank} {changeDisplay} {result.text} {result.rerank_score?.toFixed(4)} {componentDisplay} ); })}
); }; const renderFinalResults = (pane: SearchPane) => { if (!pane.results || pane.results.length === 0) { return
No final results
; } const calculateRanks = (values: number[]) => { const indexed = values.map((val, idx) => ({ idx, val })); indexed.sort((a, b) => b.val - a.val); const ranks = new Map(); indexed.forEach((item, rank) => { ranks.set(item.idx, rank + 1); }); return ranks; }; const frequencies = pane.results.map((result: any) => { const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id); return visit ? visit.weights.frequency || 0 : 0; }); const frequencyRanks = calculateRanks(frequencies); return (

Final Results ({pane.results.length} memories)

Query: "{pane.trace?.query?.query_text || pane.query}"

{pane.results.map((result: any, idx: number) => { const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id); const finalScore = visit ? visit.weights.final_weight : result.score || 0; // Format temporal range with clearer display let occurredDisplay: React.ReactNode = 'N/A'; if (result.occurred_start && result.occurred_end) { const start = new Date(result.occurred_start).toLocaleDateString(); const end = new Date(result.occurred_end).toLocaleDateString(); occurredDisplay = start === end ? start : (
Start: {start}
End: {end}
); } else if (result.event_date) { occurredDisplay = new Date(result.event_date).toLocaleDateString(); } const mentionedDisplay = result.mentioned_at ? new Date(result.mentioned_at).toLocaleDateString() : 'N/A'; return ( #{idx + 1} {result.text} {result.context || 'N/A'} {occurredDisplay} {mentionedDisplay} {finalScore.toFixed(4)} ); })}
); }; if (!currentBank) { return (

No Bank Selected

Please select a memory bank from the dropdown above to use recall debug.

); } return (
{panes.map((pane) => (
{/* Header */}
Recall Trace #{pane.id} {panes.length > 1 && ( )}
{/* Recall Controls */}
{/* Query */}
updatePane(pane.id, { query: e.target.value })} placeholder="Enter recall query..." onKeyDown={(e) => e.key === 'Enter' && runSearch(pane.id)} />
{/* Parameters Grid */}
{(['world', 'bank', 'opinion'] as FactType[]).map((ft) => (
{ const newFactTypes = checked ? [...pane.factTypes, ft] : pane.factTypes.filter((t) => t !== ft); updatePane(pane.id, { factTypes: newFactTypes }); }} />
))}
updatePane(pane.id, { maxTokens: parseInt(e.target.value) }) } className="w-full" />
updatePane(pane.id, { includeChunks: checked as boolean }) } />
updatePane(pane.id, { includeEntities: checked as boolean }) } />
{/* Status Bar */} {pane.trace?.summary && (
✓ Search complete | Nodes visited: {pane.trace.summary.total_nodes_visited} | Entry points: {pane.trace.summary.entry_points_found} | Budget used: {pane.trace.summary.budget_used} /{' '} {pane.trace.summary.budget_used + pane.trace.summary.budget_remaining} | Results: {pane.trace.summary.results_returned} | Duration: {pane.trace.summary.total_duration_seconds?.toFixed(2)} s
)} {!pane.trace?.summary && !pane.loading && (
Ready to search
)} {/* Phase Controls */} {pane.trace && (
updatePane(pane.id, { currentPhase: value as Phase })} className="flex gap-3" >
)} {/* Content */}
{pane.loading && (
🔄
Recalling...
)} {!pane.loading && !pane.trace && (
🔍
Enter a query and click Search
)} {!pane.loading && pane.trace && ( <> {/* Retrieval Phase */} {pane.currentPhase === 'retrieval' && (
{/* Fact Type Tabs (only show if multiple fact types) */} {pane.factTypes.length > 1 && (
{pane.factTypes.map((ft) => ( ))}
)} {/* Retrieval Method Tabs */}
{['semantic', 'bm25', 'graph', 'temporal'].map((method) => ( ))}
{renderRetrievalResults(pane)}
)} {/* RRF Merge Phase */} {pane.currentPhase === 'rrf' && renderRRFMerge(pane)} {/* Reranking Phase */} {pane.currentPhase === 'rerank' && renderReranking(pane)} {/* Final Results Phase */} {pane.currentPhase === 'final' && renderFinalResults(pane)} {/* Raw JSON Phase */} {pane.currentPhase === 'json' && (

Raw JSON Response

Results from the API (trace data excluded)

)} )}
))}
); }