Álvaro Valenzuela Valdes commited on
Commit
e3b7ebb
Β·
1 Parent(s): ce0ad80

Implement Document Corral with animal icons and per-document analysis

Browse files
Files changed (1) hide show
  1. frontend/components/AgentAnalysis.tsx +93 -39
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
- setFile(event.target.files[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
  };
52
 
53
  const handleAnalyzeClick = async () => {
54
- if (!approved || !tender) return;
 
 
 
55
  setIsRunning(true);
56
  setError(null);
57
- setStatusLog(["πŸš€ Initializing Agent War Room...", "πŸ“‘ Connecting to neural providers..."]);
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
- await onAnalyze(extractedText, agentModels);
 
 
 
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. This could be due to API rate limits or connectivity issues with the neural providers.");
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">Administrative Bases (PDF)</h4>
190
 
191
- {tender?.attachments && tender.attachments.length > 0 && (
192
- <div className="mb-4 space-y-2">
193
- {tender.attachments.map((att, i) => (
194
- <div key={i} className="flex items-center gap-2 p-2 rounded-lg bg-purple-500/10 border border-purple-500/20 text-[10px] text-purple-300">
195
- <span>πŸ“„</span>
196
- <span className="truncate">{att.name}</span>
197
- </div>
198
- ))}
199
- </div>
200
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- <input type="file" accept=".pdf" onChange={handleFileChange} className="w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-[10px] file:font-bold file:bg-white/10 file:text-purple-300 hover:file:bg-white/20 transition cursor-pointer" />
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 || isUploading}
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
- {isUploading ? "Extracting Text..." : isRunning ? "Agents Debating..." : "Launch Analysis Pipeline"}
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
- {analysis && (
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">{analysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
 
 
 
 
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 ${analysis.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'}`}>
323
- {analysis.decision}
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