fazeel007's picture
initial commit
7c012de
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
Brain,
Sparkles,
FileText,
Search,
Loader2,
TrendingUp,
Lightbulb,
Target,
CheckCircle,
AlertCircle
} from "lucide-react";
interface AIAssistantProps {
onDocumentSelect?: (documentId: number) => void;
}
interface EnhancedSearchResult {
results: any[];
enhancedQuery?: {
enhancedQuery: string;
intent: string;
keywords: string[];
suggestions: string[];
};
searchInsights?: {
totalResults: number;
avgRelevanceScore: number;
modalResultsCount: number;
localResultsCount: number;
};
}
interface ResearchSynthesis {
synthesis: string;
keyFindings: string[];
gaps: string[];
recommendations: string[];
}
export default function AIAssistant({ onDocumentSelect }: AIAssistantProps) {
const [query, setQuery] = useState("");
const [selectedDocuments, setSelectedDocuments] = useState<number[]>([]);
const [analysisText, setAnalysisText] = useState("");
const queryClient = useQueryClient();
// Enhanced AI Search
const aiSearchMutation = useMutation({
mutationFn: async (searchQuery: string): Promise<EnhancedSearchResult> => {
const response = await fetch("/api/ai-search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: searchQuery,
maxResults: 10,
useQueryEnhancement: true
}),
});
if (!response.ok) throw new Error("Enhanced search failed");
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/search"] });
},
});
// Query Enhancement
const queryEnhancementMutation = useMutation({
mutationFn: async (originalQuery: string) => {
const response = await fetch("/api/enhance-query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: originalQuery }),
});
if (!response.ok) throw new Error("Query enhancement failed");
return response.json();
},
});
// Document Analysis
const documentAnalysisMutation = useMutation({
mutationFn: async ({ content, analysisType }: { content: string; analysisType: string }) => {
const response = await fetch("/api/analyze-document", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, analysisType }),
});
if (!response.ok) throw new Error("Document analysis failed");
return response.json();
},
});
// Research Synthesis
const researchSynthesisMutation = useMutation({
mutationFn: async ({ query, documentIds }: { query: string; documentIds: number[] }): Promise<ResearchSynthesis> => {
const response = await fetch("/api/research-synthesis", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, documentIds }),
});
if (!response.ok) throw new Error("Research synthesis failed");
return response.json();
},
});
// Generate Embeddings
const embeddingsMutation = useMutation({
mutationFn: async (input: string) => {
const response = await fetch("/api/embeddings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input }),
});
if (!response.ok) throw new Error("Embedding generation failed");
return response.json();
},
});
const handleEnhancedSearch = () => {
if (!query.trim()) return;
aiSearchMutation.mutate(query);
};
const handleQueryEnhancement = () => {
if (!query.trim()) return;
queryEnhancementMutation.mutate(query);
};
const handleDocumentAnalysis = (analysisType: string) => {
if (!analysisText.trim()) return;
documentAnalysisMutation.mutate({ content: analysisText, analysisType });
};
const handleResearchSynthesis = () => {
if (!query.trim() || selectedDocuments.length === 0) return;
researchSynthesisMutation.mutate({ query, documentIds: selectedDocuments });
};
const handleGenerateEmbeddings = () => {
if (!query.trim()) return;
embeddingsMutation.mutate(query);
};
return (
<div className="space-y-6">
<Card className="border-gradient-to-r from-blue-200 to-purple-200 dark:from-blue-800 dark:to-purple-800">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<Brain className="w-6 h-6 text-blue-600" />
AI Research Assistant
<Badge variant="secondary" className="ml-2">Powered by Nebius & Modal</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="search" className="w-full">
<TabsList className="grid grid-cols-4 w-full mb-6">
<TabsTrigger value="search" className="flex items-center gap-2">
<Search className="w-4 h-4" />
Smart Search
</TabsTrigger>
<TabsTrigger value="analysis" className="flex items-center gap-2">
<FileText className="w-4 h-4" />
Analysis
</TabsTrigger>
<TabsTrigger value="synthesis" className="flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
Synthesis
</TabsTrigger>
<TabsTrigger value="embeddings" className="flex items-center gap-2">
<Sparkles className="w-4 h-4" />
Embeddings
</TabsTrigger>
</TabsList>
{/* Enhanced Search Tab */}
<TabsContent value="search" className="space-y-4">
<div className="space-y-3">
<div className="flex gap-2">
<Input
placeholder="Enter research query for AI-enhanced search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleEnhancedSearch()}
className="flex-1"
/>
<Button
onClick={handleEnhancedSearch}
disabled={!query.trim() || aiSearchMutation.isPending}
>
{aiSearchMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Search className="w-4 h-4" />
)}
</Button>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleQueryEnhancement}
disabled={!query.trim() || queryEnhancementMutation.isPending}
>
{queryEnhancementMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin mr-1" />
) : (
<Target className="w-3 h-3 mr-1" />
)}
Enhance Query
</Button>
</div>
</div>
{/* Query Enhancement Results */}
{queryEnhancementMutation.data && (
<Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<CardContent className="pt-4">
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">Enhanced Query</h4>
<p className="text-sm mb-3 font-mono bg-white dark:bg-gray-800 p-2 rounded">
{queryEnhancementMutation.data.enhancedQuery}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div>
<span className="font-medium text-blue-800 dark:text-blue-200">Intent:</span>
<span className="ml-2">{queryEnhancementMutation.data.intent}</span>
</div>
<div>
<span className="font-medium text-blue-800 dark:text-blue-200">Keywords:</span>
<div className="flex flex-wrap gap-1 mt-1">
{queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
<Badge key={i} variant="outline" className="text-xs">
{keyword}
</Badge>
))}
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* Enhanced Search Results */}
{aiSearchMutation.data && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<TrendingUp className="w-5 h-5 text-green-600" />
AI-Enhanced Results
{aiSearchMutation.data.searchInsights && (
<Badge variant="secondary">
{aiSearchMutation.data.searchInsights.totalResults} results
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{aiSearchMutation.data.searchInsights && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm">
<div>
<span className="font-medium">Avg Relevance:</span>
<span className="ml-1 text-green-600">
{(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
</span>
</div>
<div>
<span className="font-medium">Modal Results:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.modalResultsCount}</span>
</div>
<div>
<span className="font-medium">Local Results:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.localResultsCount}</span>
</div>
<div>
<span className="font-medium">Total:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.totalResults}</span>
</div>
</div>
)}
<div className="space-y-2 max-h-96 overflow-y-auto">
{aiSearchMutation.data.results.map((result: any, index: number) => (
<Card key={index} className="p-3 hover:bg-gray-50 dark:hover:bg-gray-800">
<div className="flex justify-between items-start mb-2">
<h5 className="font-medium text-sm">{result.title}</h5>
<div className="flex items-center gap-2">
{result.relevanceScore && (
<Badge variant="outline" className="text-xs">
{(result.relevanceScore * 100).toFixed(0)}%
</Badge>
)}
{result.aiExplanation && (
<CheckCircle className="w-4 h-4 text-green-500" />
)}
</div>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{result.snippet}
</p>
{result.keyReasons && (
<div className="text-xs">
<span className="font-medium">AI Analysis:</span>
<ul className="list-disc list-inside ml-2 mt-1">
{result.keyReasons.slice(0, 2).map((reason: string, i: number) => (
<li key={i} className="text-gray-600 dark:text-gray-400">{reason}</li>
))}
</ul>
</div>
)}
</Card>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* Document Analysis Tab */}
<TabsContent value="analysis" className="space-y-4">
<div className="space-y-3">
<Textarea
placeholder="Paste document content for AI analysis..."
value={analysisText}
onChange={(e) => setAnalysisText(e.target.value)}
className="min-h-32"
/>
<div className="flex gap-2 flex-wrap">
{['summary', 'classification', 'key_points', 'quality_score'].map((type) => (
<Button
key={type}
variant="outline"
size="sm"
onClick={() => handleDocumentAnalysis(type)}
disabled={!analysisText.trim() || documentAnalysisMutation.isPending}
>
{documentAnalysisMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin mr-1" />
) : (
<FileText className="w-3 h-3 mr-1" />
)}
{type.replace('_', ' ').toUpperCase()}
</Button>
))}
</div>
</div>
{documentAnalysisMutation.data && (
<Card className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
Analysis Result
</CardTitle>
</CardHeader>
<CardContent>
<div className="whitespace-pre-wrap text-sm">
{documentAnalysisMutation.data.analysis}
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* Research Synthesis Tab */}
<TabsContent value="synthesis" className="space-y-4">
<div className="space-y-3">
<Input
placeholder="Research question for synthesis..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Selected Documents:</span>
<Badge variant="outline">{selectedDocuments.length}</Badge>
<Button
size="sm"
variant="outline"
onClick={() => setSelectedDocuments([])}
>
Clear
</Button>
</div>
<Button
onClick={handleResearchSynthesis}
disabled={!query.trim() || selectedDocuments.length === 0 || researchSynthesisMutation.isPending}
className="w-full"
>
{researchSynthesisMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : (
<Lightbulb className="w-4 h-4 mr-2" />
)}
Generate Research Synthesis
</Button>
</div>
{researchSynthesisMutation.data && (
<Card className="bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800">
<CardContent className="pt-4 space-y-4">
<div>
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Synthesis</h4>
<p className="text-sm">{researchSynthesisMutation.data.synthesis}</p>
</div>
{researchSynthesisMutation.data.keyFindings.length > 0 && (
<div>
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Key Findings</h4>
<ul className="list-disc list-inside text-sm space-y-1">
{researchSynthesisMutation.data.keyFindings.map((finding: string, i: number) => (
<li key={i}>{finding}</li>
))}
</ul>
</div>
)}
{researchSynthesisMutation.data.recommendations.length > 0 && (
<div>
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Recommendations</h4>
<ul className="list-disc list-inside text-sm space-y-1">
{researchSynthesisMutation.data.recommendations.map((rec: string, i: number) => (
<li key={i}>{rec}</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
)}
</TabsContent>
{/* Embeddings Tab */}
<TabsContent value="embeddings" className="space-y-4">
<div className="space-y-3">
<Input
placeholder="Text to generate embeddings..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button
onClick={handleGenerateEmbeddings}
disabled={!query.trim() || embeddingsMutation.isPending}
className="w-full"
>
{embeddingsMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
Generate Embeddings with Nebius
</Button>
</div>
{embeddingsMutation.data && (
<Card className="bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
<CardContent className="pt-4 space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Model:</span>
<span className="ml-2">{embeddingsMutation.data.model}</span>
</div>
<div>
<span className="font-medium">Dimensions:</span>
<span className="ml-2">{embeddingsMutation.data.data[0].embedding.length}</span>
</div>
</div>
<div>
<span className="font-medium text-sm">Vector (first 10 dimensions):</span>
<div className="font-mono text-xs bg-white dark:bg-gray-800 p-2 rounded mt-1 overflow-x-auto">
[{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
</div>
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
Token usage: {embeddingsMutation.data.usage.total_tokens} tokens
</div>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Error States */}
{(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error) && (
<Card className="border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
<CardContent className="pt-4">
<div className="flex items-center gap-2 text-red-700 dark:text-red-300">
<AlertCircle className="w-4 h-4" />
<span className="font-medium">Error occurred</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 mt-1">
{(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error)?.message}
</p>
</CardContent>
</Card>
)}
</div>
);
}