VIBN Frontend for Coolify deployment
This commit is contained in:
318
components/ai/extraction-results-editable.tsx
Normal file
318
components/ai/extraction-results-editable.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user