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

319 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { CheckCircle2, AlertTriangle, HelpCircle, Users, Lightbulb, Wrench, AlertCircle, Sparkles, X, Plus, Save, Edit2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { auth } from "@/lib/firebase/config";
import { toast } from "sonner";
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 ExtractionResultsEditableProps {
projectId: string;
className?: string;
}
export function ExtractionResultsEditable({ projectId, className }: ExtractionResultsEditableProps) {
const [extraction, setExtraction] = useState<ExtractionHandoff | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Local editable state
const [editedProblems, setEditedProblems] = useState<string[]>([]);
const [editedUsers, setEditedUsers] = useState<string[]>([]);
const [editedFeatures, setEditedFeatures] = useState<string[]>([]);
const [editedConstraints, setEditedConstraints] = useState<string[]>([]);
const [editedOpportunities, setEditedOpportunities] = useState<string[]>([]);
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();
const handoff = data.handoff;
setExtraction(handoff);
// Initialize editable state
setEditedProblems(handoff.confirmed.problems || []);
setEditedUsers(handoff.confirmed.targetUsers || []);
setEditedFeatures(handoff.confirmed.features || []);
setEditedConstraints(handoff.confirmed.constraints || []);
setEditedOpportunities(handoff.confirmed.opportunities || []);
} catch (err) {
console.error("[ExtractionResults] Error:", err);
setError(err instanceof Error ? err.message : "Failed to load extraction");
} finally {
setLoading(false);
}
};
if (projectId) {
fetchExtraction();
}
}, [projectId]);
const handleSave = async () => {
setIsSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error("Please sign in to save changes");
return;
}
const token = await user.getIdToken();
const response = await fetch(`/api/projects/${projectId}/extraction-handoff`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
confirmed: {
problems: editedProblems.filter(p => p.trim()),
targetUsers: editedUsers.filter(u => u.trim()),
features: editedFeatures.filter(f => f.trim()),
constraints: editedConstraints.filter(c => c.trim()),
opportunities: editedOpportunities.filter(o => o.trim()),
},
}),
});
if (!response.ok) {
throw new Error("Failed to save changes");
}
// Update local state
if (extraction) {
setExtraction({
...extraction,
confirmed: {
problems: editedProblems.filter(p => p.trim()),
targetUsers: editedUsers.filter(u => u.trim()),
features: editedFeatures.filter(f => f.trim()),
constraints: editedConstraints.filter(c => c.trim()),
opportunities: editedOpportunities.filter(o => o.trim()),
},
});
}
setIsEditing(false);
toast.success("Changes saved");
} catch (err) {
console.error("[ExtractionResults] Save error:", err);
toast.error("Failed to save changes");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
// Reset to original values
if (extraction) {
setEditedProblems(extraction.confirmed.problems || []);
setEditedUsers(extraction.confirmed.targetUsers || []);
setEditedFeatures(extraction.confirmed.features || []);
setEditedConstraints(extraction.confirmed.constraints || []);
setEditedOpportunities(extraction.confirmed.opportunities || []);
}
setIsEditing(false);
};
const renderEditableList = (
items: string[],
setItems: (items: string[]) => void,
Icon: any,
iconColor: string,
title: string
) => {
const safeItems = items || [];
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm flex items-center gap-2">
<Icon className={cn("h-4 w-4", iconColor)} />
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{safeItems.map((item, index) => (
<div key={index} className="flex items-center gap-2">
{isEditing ? (
<>
<Input
value={item}
onChange={(e) => {
const newItems = [...items];
newItems[index] = e.target.value;
setItems(newItems);
}}
className="flex-1 text-sm"
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
const newItems = items.filter((_, i) => i !== index);
setItems(newItems);
}}
>
<X className="h-4 w-4" />
</Button>
</>
) : (
<p className="text-sm text-muted-foreground">{item}</p>
)}
</div>
))}
{isEditing && (
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={() => setItems([...items, ""])}
>
<Plus className="h-4 w-4 mr-2" />
Add Item
</Button>
)}
{!isEditing && items.length === 0 && (
<p className="text-xs text-muted-foreground italic">None</p>
)}
</CardContent>
</Card>
);
};
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>
);
}
return (
<div className={cn("space-y-4", className)}>
{/* Header */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Extracted Insights</CardTitle>
<p className="text-xs text-muted-foreground mt-1">
Review and edit the extracted information
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant={extraction.readyForNextPhase ? "default" : "secondary"}>
{Math.round(extraction.confidence * 100)}% confidence
</Badge>
{!isEditing ? (
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
>
<Edit2 className="h-4 w-4 mr-2" />
Edit
</Button>
) : (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCancel}
disabled={isSaving}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={isSaving}
>
<Save className="h-4 w-4 mr-2" />
{isSaving ? "Saving..." : "Save"}
</Button>
</div>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Editable Lists */}
{renderEditableList(editedProblems, setEditedProblems, AlertTriangle, "text-orange-600", "Problems & Pain Points")}
{renderEditableList(editedUsers, setEditedUsers, Users, "text-blue-600", "Target Users")}
{renderEditableList(editedFeatures, setEditedFeatures, Lightbulb, "text-yellow-600", "Key Features")}
{renderEditableList(editedConstraints, setEditedConstraints, Wrench, "text-purple-600", "Constraints & Requirements")}
{renderEditableList(editedOpportunities, setEditedOpportunities, Sparkles, "text-green-600", "Opportunities")}
</div>
);
}