Γlvaro Valenzuela Valdes commited on
Commit Β·
e3b7ebb
1
Parent(s): ce0ad80
Implement Document Corral with animal icons and per-document analysis
Browse files
frontend/components/AgentAnalysis.tsx
CHANGED
|
@@ -32,6 +32,10 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 32 |
const [activeSettings, setActiveSettings] = useState<string | null>(null);
|
| 33 |
const [statusLog, setStatusLog] = useState<string[]>([]);
|
| 34 |
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
// Auto-scroll to results when analysis is ready
|
| 37 |
useEffect(() => {
|
|
@@ -44,34 +48,43 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 44 |
}, [analysis, isRunning]);
|
| 45 |
|
| 46 |
|
| 47 |
-
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 48 |
if (event.target.files && event.target.files[0]) {
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
};
|
| 52 |
|
| 53 |
const handleAnalyzeClick = async () => {
|
| 54 |
-
if (!approved || !tender) return;
|
|
|
|
|
|
|
|
|
|
| 55 |
setIsRunning(true);
|
| 56 |
setError(null);
|
| 57 |
-
setStatusLog(["π Initializing Agent War Room...",
|
| 58 |
-
|
| 59 |
-
let extractedText = documentText;
|
| 60 |
|
| 61 |
try {
|
| 62 |
-
if (file && !extractedText) {
|
| 63 |
-
setIsUploading(true);
|
| 64 |
-
setStatusLog(prev => [...prev, "π Extracting text from PDF bases..."]);
|
| 65 |
-
const uploadResult = await uploadDocument(file);
|
| 66 |
-
extractedText = uploadResult.text;
|
| 67 |
-
setDocumentText(extractedText);
|
| 68 |
-
setStatusLog(prev => [...prev, "β
PDF text extracted successfully."]);
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
setIsUploading(false);
|
| 72 |
setStatusLog(prev => [...prev, "π€ Summoning experts: Legal, Technical, and Strategy..."]);
|
| 73 |
|
| 74 |
-
// Simulate partial progress since we can't do real-time backend updates easily without SSE
|
| 75 |
const progressTimer = setInterval(() => {
|
| 76 |
const messages = [
|
| 77 |
"βοΈ Dra. Legal is reviewing clauses...",
|
|
@@ -88,20 +101,39 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 88 |
});
|
| 89 |
}, 3000);
|
| 90 |
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
clearInterval(progressTimer);
|
| 94 |
setStatusLog(prev => [...prev, "β¨ Analysis complete!"]);
|
| 95 |
} catch (err) {
|
| 96 |
console.error("Error during analysis flow:", err);
|
| 97 |
-
setError("The analysis pipeline encountered a technical failure.
|
| 98 |
setStatusLog(prev => [...prev, "β Error occurred during analysis pipeline."]);
|
| 99 |
} finally {
|
| 100 |
-
setIsUploading(false);
|
| 101 |
setIsRunning(false);
|
| 102 |
}
|
| 103 |
};
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
if (!tender && !analysis) {
|
| 106 |
return (
|
| 107 |
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-12 animate-in fade-in duration-1000">
|
|
@@ -186,21 +218,39 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 186 |
|
| 187 |
<div className="flex flex-col gap-4 lg:w-80">
|
| 188 |
<div className="glass-card rounded-2xl p-6 bg-white/5 border border-white/10">
|
| 189 |
-
<h4 className="text-[10px] font-bold uppercase text-slate-400 mb-4 tracking-widest">
|
| 190 |
|
| 191 |
-
{
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
|
| 203 |
-
{file && <p className="mt-2 text-[10px] text-green-400 font-bold">β Ready for extraction</p>}
|
| 204 |
</div>
|
| 205 |
|
| 206 |
<label className="flex items-center gap-3 p-4 rounded-2xl bg-white/5 cursor-pointer hover:bg-white/10 transition border border-white/5">
|
|
@@ -210,10 +260,10 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 210 |
|
| 211 |
<button
|
| 212 |
onClick={handleAnalyzeClick}
|
| 213 |
-
disabled={!tender || !approved || isRunning ||
|
| 214 |
className="w-full rounded-2xl premium-gradient py-5 font-bold text-white transition hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed shadow-xl shadow-purple-500/20 active:scale-[0.98]"
|
| 215 |
>
|
| 216 |
-
{
|
| 217 |
</button>
|
| 218 |
</div>
|
| 219 |
</div>
|
|
@@ -309,18 +359,22 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
|
|
| 309 |
)}
|
| 310 |
|
| 311 |
{/* Analysis Results View */}
|
| 312 |
-
{
|
| 313 |
<div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-1000 scroll-mt-20">
|
| 314 |
<div className="lg:col-span-8 space-y-8">
|
| 315 |
<div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
|
| 316 |
<div className="flex items-start justify-between mb-8">
|
| 317 |
<div>
|
| 318 |
<div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
|
| 319 |
-
<h3 className="text-6xl font-black text-white">{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
</div>
|
| 321 |
<div className="flex flex-col items-end gap-3">
|
| 322 |
-
<div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${
|
| 323 |
-
{
|
| 324 |
</div>
|
| 325 |
<div className="flex gap-2">
|
| 326 |
<button
|
|
|
|
| 32 |
const [activeSettings, setActiveSettings] = useState<string | null>(null);
|
| 33 |
const [statusLog, setStatusLog] = useState<string[]>([]);
|
| 34 |
const [error, setError] = useState<string | null>(null);
|
| 35 |
+
|
| 36 |
+
// Multiple Files Support (The Corral)
|
| 37 |
+
const [corral, setCorral] = useState<Array<{ file: File, text: string, analysis: AnalysisResult | null, id: string }>>([]);
|
| 38 |
+
const [activeAnimalId, setActiveAnimalId] = useState<string | null>(null);
|
| 39 |
|
| 40 |
// Auto-scroll to results when analysis is ready
|
| 41 |
useEffect(() => {
|
|
|
|
| 48 |
}, [analysis, isRunning]);
|
| 49 |
|
| 50 |
|
| 51 |
+
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 52 |
if (event.target.files && event.target.files[0]) {
|
| 53 |
+
const newFile = event.target.files[0];
|
| 54 |
+
const id = Math.random().toString(36).substring(7);
|
| 55 |
+
|
| 56 |
+
setIsUploading(true);
|
| 57 |
+
try {
|
| 58 |
+
const uploadResult = await uploadDocument(newFile);
|
| 59 |
+
const newEntry = {
|
| 60 |
+
file: newFile,
|
| 61 |
+
text: uploadResult.text,
|
| 62 |
+
analysis: null,
|
| 63 |
+
id
|
| 64 |
+
};
|
| 65 |
+
setCorral(prev => [...prev, newEntry]);
|
| 66 |
+
setActiveAnimalId(id);
|
| 67 |
+
} catch (err) {
|
| 68 |
+
console.error("Upload error", err);
|
| 69 |
+
setError("Failed to upload and process document.");
|
| 70 |
+
} finally {
|
| 71 |
+
setIsUploading(false);
|
| 72 |
+
}
|
| 73 |
}
|
| 74 |
};
|
| 75 |
|
| 76 |
const handleAnalyzeClick = async () => {
|
| 77 |
+
if (!approved || !tender || !activeAnimalId) return;
|
| 78 |
+
const activeEntry = corral.find(a => a.id === activeAnimalId);
|
| 79 |
+
if (!activeEntry) return;
|
| 80 |
+
|
| 81 |
setIsRunning(true);
|
| 82 |
setError(null);
|
| 83 |
+
setStatusLog(["π Initializing Agent War Room...", `π‘ Focusing on: ${activeEntry.file.name}...`]);
|
|
|
|
|
|
|
| 84 |
|
| 85 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
setStatusLog(prev => [...prev, "π€ Summoning experts: Legal, Technical, and Strategy..."]);
|
| 87 |
|
|
|
|
| 88 |
const progressTimer = setInterval(() => {
|
| 89 |
const messages = [
|
| 90 |
"βοΈ Dra. Legal is reviewing clauses...",
|
|
|
|
| 101 |
});
|
| 102 |
}, 3000);
|
| 103 |
|
| 104 |
+
// We call the parent's onAnalyze but we want the result back locally too
|
| 105 |
+
// Actually, since we want multiple analyses, we might need to handle the result here
|
| 106 |
+
// For now, let's assume the parent updates the main analysis prop, but we'll store it in the corral too
|
| 107 |
+
await onAnalyze(activeEntry.text, agentModels);
|
| 108 |
|
| 109 |
clearInterval(progressTimer);
|
| 110 |
setStatusLog(prev => [...prev, "β¨ Analysis complete!"]);
|
| 111 |
} catch (err) {
|
| 112 |
console.error("Error during analysis flow:", err);
|
| 113 |
+
setError("The analysis pipeline encountered a technical failure.");
|
| 114 |
setStatusLog(prev => [...prev, "β Error occurred during analysis pipeline."]);
|
| 115 |
} finally {
|
|
|
|
| 116 |
setIsRunning(false);
|
| 117 |
}
|
| 118 |
};
|
| 119 |
|
| 120 |
+
// Sync parent analysis to corral entry
|
| 121 |
+
useEffect(() => {
|
| 122 |
+
if (analysis && activeAnimalId) {
|
| 123 |
+
setCorral(prev => prev.map(a => a.id === activeAnimalId ? { ...a, analysis } : a));
|
| 124 |
+
}
|
| 125 |
+
}, [analysis]);
|
| 126 |
+
|
| 127 |
+
const activeAnalysis = corral.find(a => a.id === activeAnimalId)?.analysis || analysis;
|
| 128 |
+
|
| 129 |
+
const getFileIcon = (fileName: string) => {
|
| 130 |
+
const ext = fileName.split('.').pop()?.toLowerCase();
|
| 131 |
+
if (ext === 'pdf') return { emoji: "π", animal: "π¦’", color: "bg-red-500/10 text-red-400" };
|
| 132 |
+
if (ext === 'doc' || ext === 'docx') return { emoji: "π", animal: "π", color: "bg-blue-500/10 text-blue-400" };
|
| 133 |
+
if (ext === 'xls' || ext === 'xlsx') return { emoji: "π", animal: "π", color: "bg-green-500/10 text-green-400" };
|
| 134 |
+
return { emoji: "π", animal: "π", color: "bg-slate-500/10 text-slate-400" };
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
if (!tender && !analysis) {
|
| 138 |
return (
|
| 139 |
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-12 animate-in fade-in duration-1000">
|
|
|
|
| 218 |
|
| 219 |
<div className="flex flex-col gap-4 lg:w-80">
|
| 220 |
<div className="glass-card rounded-2xl p-6 bg-white/5 border border-white/10">
|
| 221 |
+
<h4 className="text-[10px] font-bold uppercase text-slate-400 mb-4 tracking-widest">Document Corral</h4>
|
| 222 |
|
| 223 |
+
{/* The Corral (Animal Pen) */}
|
| 224 |
+
<div className="flex flex-wrap gap-3 mb-6">
|
| 225 |
+
{corral.map((item) => {
|
| 226 |
+
const icon = getFileIcon(item.file.name);
|
| 227 |
+
return (
|
| 228 |
+
<button
|
| 229 |
+
key={item.id}
|
| 230 |
+
onClick={() => setActiveAnimalId(item.id)}
|
| 231 |
+
className={`group relative flex flex-col items-center justify-center h-16 w-16 rounded-2xl border transition-all duration-500 hover:scale-110 active:scale-95 ${activeAnimalId === item.id ? 'bg-purple-500/20 border-purple-500 shadow-lg shadow-purple-500/20' : 'bg-white/5 border-white/10'}`}
|
| 232 |
+
title={item.file.name}
|
| 233 |
+
>
|
| 234 |
+
<span className={`text-2xl transition-all duration-500 group-hover:rotate-12 ${activeAnimalId === item.id ? 'animate-bounce' : 'group-hover:animate-wiggle'}`}>
|
| 235 |
+
{icon.animal}
|
| 236 |
+
</span>
|
| 237 |
+
<span className="absolute -bottom-1 -right-1 text-[10px]">{icon.emoji}</span>
|
| 238 |
+
{item.analysis && <span className="absolute -top-1 -right-1 h-3 w-3 bg-green-500 rounded-full border-2 border-black" title="Analyzed" />}
|
| 239 |
+
</button>
|
| 240 |
+
);
|
| 241 |
+
})}
|
| 242 |
+
|
| 243 |
+
<label className="flex flex-col items-center justify-center h-16 w-16 rounded-2xl border border-dashed border-white/20 bg-white/5 cursor-pointer hover:bg-white/10 hover:border-purple-500/50 transition-all">
|
| 244 |
+
<span className="text-xl text-slate-500">+</span>
|
| 245 |
+
<input type="file" onChange={handleFileChange} className="hidden" />
|
| 246 |
+
</label>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
<div className="text-[10px] text-slate-500 italic mb-4">
|
| 250 |
+
{corral.length === 0 ? "No documents in the corral." : `${corral.length} document(s) ready.`}
|
| 251 |
+
</div>
|
| 252 |
|
| 253 |
+
{isUploading && <p className="text-[10px] text-purple-400 animate-pulse font-bold">β¨ Bringing animal to corral...</p>}
|
|
|
|
| 254 |
</div>
|
| 255 |
|
| 256 |
<label className="flex items-center gap-3 p-4 rounded-2xl bg-white/5 cursor-pointer hover:bg-white/10 transition border border-white/5">
|
|
|
|
| 260 |
|
| 261 |
<button
|
| 262 |
onClick={handleAnalyzeClick}
|
| 263 |
+
disabled={!tender || !approved || isRunning || !activeAnimalId}
|
| 264 |
className="w-full rounded-2xl premium-gradient py-5 font-bold text-white transition hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed shadow-xl shadow-purple-500/20 active:scale-[0.98]"
|
| 265 |
>
|
| 266 |
+
{isRunning ? "Agents Debating..." : "Launch Analysis Pipeline"}
|
| 267 |
</button>
|
| 268 |
</div>
|
| 269 |
</div>
|
|
|
|
| 359 |
)}
|
| 360 |
|
| 361 |
{/* Analysis Results View */}
|
| 362 |
+
{activeAnalysis && (
|
| 363 |
<div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-1000 scroll-mt-20">
|
| 364 |
<div className="lg:col-span-8 space-y-8">
|
| 365 |
<div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
|
| 366 |
<div className="flex items-start justify-between mb-8">
|
| 367 |
<div>
|
| 368 |
<div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
|
| 369 |
+
<h3 className="text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
|
| 370 |
+
<div className="mt-2 flex items-center gap-2">
|
| 371 |
+
<span className="text-[10px] text-slate-500 font-mono">Analyzing:</span>
|
| 372 |
+
<span className="text-[10px] text-purple-300 font-bold">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span>
|
| 373 |
+
</div>
|
| 374 |
</div>
|
| 375 |
<div className="flex flex-col items-end gap-3">
|
| 376 |
+
<div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${activeAnalysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
|
| 377 |
+
{activeAnalysis.decision}
|
| 378 |
</div>
|
| 379 |
<div className="flex gap-2">
|
| 380 |
<button
|