306 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|