Files
vibn-frontend/components/ai/extraction-results.tsx

306 lines
10 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { CheckCircle2, AlertTriangle, HelpCircle, Users, Lightbulb, Wrench, AlertCircle, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
interface ExtractionHandoff {
phase: string;
readyForNextPhase: boolean;
confidence: number;
confirmed: {
problems?: string[];
targetUsers?: string[];
features?: string[];
constraints?: string[];
opportunities?: string[];
};
uncertain: Record<string, any>;
missing: string[];
questionsForUser: string[];
timestamp: string;
}
interface ExtractionResultsProps {
projectId: string;
className?: string;
}
export function ExtractionResults({ projectId, className }: ExtractionResultsProps) {
const [extraction, setExtraction] = useState<ExtractionHandoff | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchExtraction = async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/extraction-handoff`);
if (!response.ok) {
if (response.status === 404) {
setExtraction(null);
return;
}
throw new Error(`Failed to fetch extraction: ${response.statusText}`);
}
const data = await response.json();
setExtraction(data.handoff);
} catch (err) {
console.error("[ExtractionResults] Error:", err);
setError(err instanceof Error ? err.message : "Failed to load extraction");
} finally {
setLoading(false);
}
};
if (projectId) {
fetchExtraction();
}
}, [projectId]);
if (loading) {
return (
<div className={cn("space-y-4", className)}>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
}
if (error) {
return (
<Card className={cn("border-destructive", className)}>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
<p>{error}</p>
</div>
</CardContent>
</Card>
);
}
if (!extraction) {
return (
<Card className={cn("border-dashed", className)}>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
<Sparkles className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No extraction results yet</p>
<p className="text-xs mt-1">Upload documents and trigger extraction to see insights</p>
</div>
</CardContent>
</Card>
);
}
const { confirmed } = extraction;
const confidencePercent = Math.round(extraction.confidence * 100);
return (
<div className={cn("space-y-4", className)}>
{/* Header with Confidence Score */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Extraction Results</CardTitle>
<div className="flex items-center gap-2">
<Badge variant={extraction.readyForNextPhase ? "default" : "secondary"}>
{extraction.readyForNextPhase ? (
<>
<CheckCircle2 className="h-3 w-3 mr-1" />
Ready
</>
) : (
<>
<HelpCircle className="h-3 w-3 mr-1" />
Incomplete
</>
)}
</Badge>
<Badge variant="outline" className={cn(
confidencePercent >= 70 ? "border-green-500 text-green-600" :
confidencePercent >= 40 ? "border-yellow-500 text-yellow-600" :
"border-red-500 text-red-600"
)}>
{confidencePercent}% confidence
</Badge>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Extracted on {new Date(extraction.timestamp).toLocaleString()}
</p>
</CardContent>
</Card>
{/* Problems / Pain Points */}
{confirmed.problems && confirmed.problems.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-orange-500" />
Problems & Pain Points
<Badge variant="secondary">{confirmed.problems.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.problems.map((problem, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-orange-500 mt-1"></span>
<span className="text-sm">{problem}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Target Users */}
{confirmed.targetUsers && confirmed.targetUsers.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Users className="h-4 w-4 text-blue-500" />
Target Users
<Badge variant="secondary">{confirmed.targetUsers.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.targetUsers.map((user, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-blue-500 mt-1"></span>
<span className="text-sm">{user}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Features */}
{confirmed.features && confirmed.features.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Lightbulb className="h-4 w-4 text-yellow-500" />
Key Features
<Badge variant="secondary">{confirmed.features.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.features.map((feature, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-yellow-500 mt-1"></span>
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Constraints */}
{confirmed.constraints && confirmed.constraints.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Wrench className="h-4 w-4 text-purple-500" />
Constraints & Requirements
<Badge variant="secondary">{confirmed.constraints.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.constraints.map((constraint, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-purple-500 mt-1"></span>
<span className="text-sm">{constraint}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Opportunities */}
{confirmed.opportunities && confirmed.opportunities.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Sparkles className="h-4 w-4 text-green-500" />
Opportunities
<Badge variant="secondary">{confirmed.opportunities.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{confirmed.opportunities.map((opportunity, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-green-500 mt-1"></span>
<span className="text-sm">{opportunity}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Missing Information */}
{extraction.missing && extraction.missing.length > 0 && (
<Card className="border-yellow-200 bg-yellow-50/50">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-yellow-800">
<HelpCircle className="h-4 w-4" />
Missing Information
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-1">
{extraction.missing.map((item, idx) => (
<li key={idx} className="text-sm text-yellow-800 flex items-center gap-2">
<span>-</span>
<span>{item}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* Questions for User */}
{extraction.questionsForUser && extraction.questionsForUser.length > 0 && (
<Card className="border-blue-200 bg-blue-50/50">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-blue-800">
<HelpCircle className="h-4 w-4" />
Questions for Clarification
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{extraction.questionsForUser.map((question, idx) => (
<li key={idx} className="text-sm text-blue-800 flex items-start gap-2">
<span className="font-medium">{idx + 1}.</span>
<span>{question}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
</div>
);
}