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