Files
enclava/frontend/src/app/rag-demo/page.tsx

569 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from 'react';
import { useAuth } from '@/components/providers/auth-provider';
import { tokenManager } from '@/lib/token-manager';
interface SearchResult {
document: {
id: string;
content: string;
metadata: Record<string, any>;
};
score: number;
debug_info?: Record<string, any>;
}
interface DebugInfo {
query_embedding?: number[];
embedding_dimension?: number;
score_stats?: {
min: number;
max: number;
avg: number;
stddev: number;
};
collection_stats?: {
total_documents: number;
total_chunks: number;
languages: string[];
};
}
export default function RAGDemoPage() {
const { user, loading } = useAuth();
const [query, setQuery] = useState('are sd card backups encrypted?');
const [results, setResults] = useState<SearchResult[]>([]);
const [debugInfo, setDebugInfo] = useState<DebugInfo>({});
const [searchTime, setSearchTime] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
// Configuration state
const [config, setConfig] = useState({
max_results: 10,
score_threshold: 0.3,
collection_name: '',
chunk_size: 300,
chunk_overlap: 50,
enable_hybrid: false,
vector_weight: 0.7,
bm25_weight: 0.3,
use_query_prefix: true,
use_passage_prefix: true,
show_timing: true,
show_embeddings: false,
});
// Available collections
const [collections, setCollections] = useState<string[]>([]);
const [collectionsLoading, setCollectionsLoading] = useState(false);
const presets = {
default: {
max_results: 10,
score_threshold: 0.3,
chunk_size: 300,
chunk_overlap: 50,
enable_hybrid: false,
vector_weight: 0.7,
bm25_weight: 0.3,
},
high_precision: {
max_results: 5,
score_threshold: 0.5,
chunk_size: 200,
chunk_overlap: 30,
enable_hybrid: true,
vector_weight: 0.8,
bm25_weight: 0.2,
},
high_recall: {
max_results: 20,
score_threshold: 0.1,
chunk_size: 400,
chunk_overlap: 100,
enable_hybrid: true,
vector_weight: 0.6,
bm25_weight: 0.4,
},
hybrid: {
max_results: 10,
score_threshold: 0.2,
chunk_size: 300,
chunk_overlap: 50,
enable_hybrid: true,
vector_weight: 0.5,
bm25_weight: 0.5,
},
};
useEffect(() => {
// Check if we have tokens in localStorage but not in tokenManager
const syncTokens = async () => {
const rawTokens = localStorage.getItem('auth_tokens');
if (rawTokens && !tokenManager.isAuthenticated()) {
try {
const tokens = JSON.parse(rawTokens);
// Sync tokens to tokenManager
tokenManager.setTokens(
tokens.access_token,
tokens.refresh_token,
Math.floor((tokens.access_expires_at - Date.now()) / 1000)
);
console.log('RAG Demo: Tokens synced from localStorage to tokenManager');
} catch (e) {
console.error('RAG Demo: Failed to sync tokens:', e);
}
}
loadCollections();
};
syncTokens();
}, [user]);
const loadCollections = async () => {
setCollectionsLoading(true);
try {
console.log('RAG Demo: Loading collections...');
console.log('RAG Demo: User authenticated:', !!user);
console.log('RAG Demo: TokenManager authenticated:', tokenManager.isAuthenticated());
const token = await tokenManager.getAccessToken();
console.log('RAG Demo: Token retrieved:', token ? 'Yes' : 'No');
console.log('RAG Demo: Token expiry:', tokenManager.getTokenExpiry());
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
console.log('RAG Demo: Authorization header set');
} else {
console.warn('RAG Demo: No token available');
}
const response = await fetch('/api/rag/debug/collections', { headers });
console.log('RAG Demo: Collections response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('RAG Demo: Collections loaded:', data.collections);
setCollections(data.collections || []);
// Auto-select first collection if none selected
if (data.collections && data.collections.length > 0 && !config.collection_name) {
setConfig(prev => ({ ...prev, collection_name: data.collections[0] }));
}
} else {
const errorText = await response.text();
console.error('RAG Demo: Collections failed:', response.status, errorText);
}
} catch (err) {
console.error('RAG Demo: Failed to load collections:', err);
} finally {
setCollectionsLoading(false);
}
};
const loadPreset = (presetName: keyof typeof presets) => {
setConfig(prev => ({
...prev,
...presets[presetName],
}));
};
const performSearch = async () => {
if (!query.trim()) return;
if (!config.collection_name) {
setError('Please select a collection');
return;
}
setIsLoading(true);
setError('');
setResults([]);
try {
const token = await tokenManager.getAccessToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch('/api/rag/debug/search', {
method: 'POST',
headers,
body: JSON.stringify({
query,
max_results: config.max_results,
score_threshold: config.score_threshold,
collection_name: config.collection_name,
config,
}),
});
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const data = await response.json();
setResults(data.results || []);
setDebugInfo(data.debug_info || {});
setSearchTime(data.search_time_ms || 0);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
};
const updateConfig = (key: string, value: any) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);
}
if (!user) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">RAG Demo</h1>
<p>Please log in to access the RAG demo interface.</p>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 max-w-7xl">
<h1 className="text-3xl font-bold mb-2">🔍 RAG Search Demo</h1>
<p className="text-gray-600 mb-6">Test and tune your RAG system with real-time search and debugging</p>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Search Results - Main Content */}
<div className="lg:col-span-3 space-y-6">
{/* Preset Buttons */}
<div className="flex flex-wrap gap-2">
{Object.entries(presets).map(([name, _]) => (
<button
key={name}
onClick={() => loadPreset(name as keyof typeof presets)}
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md text-sm capitalize"
>
{name.replace('_', ' ')}
</button>
))}
</div>
{/* Search Box */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && performSearch()}
placeholder="Enter your search query..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={performSearch}
disabled={isLoading || !config.collection_name}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? 'Searching...' : 'Search'}
</button>
</div>
{error && (
<div className="mt-4 p-4 bg-red-100 text-red-700 rounded-md">
Error: {error}
</div>
)}
{/* Results Summary */}
{results.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 rounded-md">
<p className="text-sm">
Found <strong>{results.length}</strong> results in <strong>{searchTime.toFixed(0)}ms</strong>
{config.enable_hybrid && (
<span className="ml-2 text-green-600"> Hybrid Search Enabled</span>
)}
</p>
</div>
)}
</div>
{/* Search Results */}
<div className="space-y-4">
{results.map((result, index) => (
<div key={index} className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-semibold">Result {index + 1}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
result.score >= 0.5 ? 'bg-green-100 text-green-800' :
result.score >= 0.3 ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
Score: {result.score.toFixed(4)}
</span>
</div>
<div className="text-gray-700 mb-4 whitespace-pre-wrap">
{result.document.content}
</div>
{/* Metadata */}
<div className="text-sm text-gray-500 mb-3">
{result.document.metadata.content_type && (
<span>Type: {result.document.metadata.content_type}</span>
)}
{result.document.metadata.language && (
<span className="ml-3">Language: {result.document.metadata.language}</span>
)}
{result.document.metadata.filename && (
<span className="ml-3">File: {result.document.metadata.filename}</span>
)}
{result.document.metadata.chunk_index !== undefined && (
<span className="ml-3">
Chunk: {result.document.metadata.chunk_index + 1}/{result.document.metadata.chunk_count || '?'}
</span>
)}
</div>
{/* Debug Details */}
{config.show_timing && result.debug_info && (
<div className="mt-4 p-3 bg-gray-50 rounded-md text-xs font-mono">
<p><strong>Debug Information:</strong></p>
{result.debug_info.vector_score !== undefined && (
<p>Vector Score: {result.debug_info.vector_score.toFixed(4)}</p>
)}
{result.debug_info.bm25_score !== undefined && (
<p>BM25 Score: {result.debug_info.bm25_score.toFixed(4)}</p>
)}
{result.document.metadata.question && (
<div className="mt-2">
<p><strong>Question:</strong> {result.document.metadata.question}</p>
</div>
)}
</div>
)}
</div>
))}
</div>
{/* Debug Section */}
{debugInfo && Object.keys(debugInfo).length > 0 && (
<div className="bg-gray-900 text-green-400 rounded-lg shadow p-6 font-mono text-sm">
<h3 className="text-lg font-semibold mb-4">Debug Information</h3>
{debugInfo.score_stats && (
<div className="mb-4">
<p className="font-semibold mb-2">Score Statistics:</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div>Min: {debugInfo.score_stats.min?.toFixed(4)}</div>
<div>Max: {debugInfo.score_stats.max?.toFixed(4)}</div>
<div>Avg: {debugInfo.score_stats.avg?.toFixed(4)}</div>
<div>StdDev: {debugInfo.score_stats.stddev?.toFixed(4)}</div>
</div>
</div>
)}
{debugInfo.collection_stats && (
<div className="mb-4">
<p className="font-semibold mb-2">Collection Stats:</p>
<div className="text-xs">
<p>Total Documents: {debugInfo.collection_stats.total_documents}</p>
<p>Total Chunks: {debugInfo.collection_stats.total_chunks}</p>
<p>Languages: {debugInfo.collection_stats.languages?.join(', ')}</p>
</div>
</div>
)}
{debugInfo.query_embedding && config.show_embeddings && (
<div>
<p className="font-semibold mb-2">Query Embedding (first 10 dims):</p>
<p className="text-xs">
[{debugInfo.query_embedding.slice(0, 10).map(x => x.toFixed(6)).join(', ')}...]
</p>
</div>
)}
</div>
)}
</div>
{/* Configuration Panel */}
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4"> Configuration</h2>
<div className="space-y-4">
{/* Search Settings */}
<div>
<h3 className="font-medium mb-2">Search Settings</h3>
<div className="space-y-3">
<div>
<label className="block text-sm mb-1">Max Results: {config.max_results}</label>
<input
type="range"
min="1"
max="50"
value={config.max_results}
onChange={(e) => updateConfig('max_results', parseInt(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm mb-1">Score Threshold: {config.score_threshold}</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={config.score_threshold}
onChange={(e) => updateConfig('score_threshold', parseFloat(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm mb-1">Collection Name</label>
{collectionsLoading ? (
<select
disabled
className="w-full px-3 py-1 border border-gray-300 rounded-md text-sm bg-gray-50"
>
<option>Loading collections...</option>
</select>
) : (
<select
value={config.collection_name}
onChange={(e) => updateConfig('collection_name', e.target.value)}
className="w-full px-3 py-1 border border-gray-300 rounded-md text-sm"
>
<option value="">Select a collection...</option>
{collections.map(collection => (
<option key={collection} value={collection}>
{collection}
</option>
))}
</select>
)}
</div>
</div>
</div>
{/* Chunking Settings */}
<div>
<h3 className="font-medium mb-2">Chunking Settings</h3>
<div className="space-y-3">
<div>
<label className="block text-sm mb-1">Chunk Size: {config.chunk_size}</label>
<input
type="range"
min="100"
max="1000"
step="50"
value={config.chunk_size}
onChange={(e) => updateConfig('chunk_size', parseInt(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm mb-1">Chunk Overlap: {config.chunk_overlap}</label>
<input
type="range"
min="0"
max="200"
step="10"
value={config.chunk_overlap}
onChange={(e) => updateConfig('chunk_overlap', parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
</div>
{/* Hybrid Search */}
<div>
<h3 className="font-medium mb-2">Hybrid Search</h3>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={config.enable_hybrid}
onChange={(e) => updateConfig('enable_hybrid', e.target.checked)}
className="mr-2"
/>
<span className="text-sm">Enable Hybrid Search</span>
</label>
{config.enable_hybrid && (
<>
<div>
<label className="block text-sm mb-1">Vector Weight: {config.vector_weight}</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={config.vector_weight}
onChange={(e) => updateConfig('vector_weight', parseFloat(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm mb-1">BM25 Weight: {config.bm25_weight}</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={config.bm25_weight}
onChange={(e) => updateConfig('bm25_weight', parseFloat(e.target.value))}
className="w-full"
/>
</div>
</>
)}
</div>
</div>
{/* Debug Options */}
<div>
<h3 className="font-medium mb-2">Debug Options</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={config.show_timing}
onChange={(e) => updateConfig('show_timing', e.target.checked)}
className="mr-2"
/>
<span className="text-sm">Show Timing</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={config.show_embeddings}
onChange={(e) => updateConfig('show_embeddings', e.target.checked)}
className="mr-2"
/>
<span className="text-sm">Show Embeddings</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}