| "use client"; |
|
|
| import { useState } from "react"; |
| import type { AnalysisResult, CompanyProfile, Tender } from "../lib/types"; |
| import { uploadDocument } from "../lib/api"; |
|
|
| type Props = { |
| tender: Tender | null; |
| companyProfile: CompanyProfile; |
| analysis: AnalysisResult | null; |
| onAnalyze: (documentText?: string) => Promise<void>; |
| }; |
|
|
| const agents = [ |
| { id: "legal", name: "Dra. Legal", role: "Compliance", avatar: "⚖️", color: "text-amber-400", desc: "Verifies administrative bases and legal risks." }, |
| { id: "tech", name: "Ing. Tech", role: "Architecture", avatar: "👨💻", color: "text-cyan", desc: "Evaluates technical feasibility and stack requirements." }, |
| { id: "risk", name: "Sra. Estrategia", role: "ROI & Risk", avatar: "🕵️♀️", color: "text-purple-400", desc: "Calculates commercial impact and win probability." }, |
| ]; |
|
|
| export default function AgentAnalysis({ tender, companyProfile, analysis, onAnalyze }: Props) { |
| const [approved, setApproved] = useState(false); |
| const [isRunning, setIsRunning] = useState(false); |
| const [file, setFile] = useState<File | null>(null); |
| const [isUploading, setIsUploading] = useState(false); |
| const [documentText, setDocumentText] = useState<string | "">(""); |
|
|
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { |
| if (event.target.files && event.target.files[0]) { |
| setFile(event.target.files[0]); |
| } |
| }; |
|
|
| const handleAnalyzeClick = async () => { |
| if (!approved || !tender) return; |
| setIsRunning(true); |
| let extractedText = documentText; |
| if (file && !extractedText) { |
| setIsUploading(true); |
| try { |
| const uploadResult = await uploadDocument(file); |
| extractedText = uploadResult.text; |
| setDocumentText(extractedText); |
| } catch (error) { |
| console.error("Error uploading document:", error); |
| } finally { |
| setIsUploading(false); |
| } |
| } |
| await onAnalyze(extractedText); |
| setIsRunning(false); |
| }; |
|
|
| if (!tender && !analysis) { |
| return ( |
| <div className="flex flex-col items-center justify-center min-h-[60vh] space-y-12 animate-in fade-in duration-1000"> |
| <div className="text-center space-y-4"> |
| <div className="inline-block p-4 rounded-3xl bg-white/5 border border-white/10 mb-6"> |
| <span className="text-5xl">🤖</span> |
| </div> |
| <h2 className="text-4xl font-bold text-white tracking-tight">Agent War Room</h2> |
| <p className="text-slate-400 max-w-md mx-auto text-lg leading-relaxed"> |
| Our specialized agents are ready to analyze your next big opportunity. |
| Select a tender from <span className="text-purple-400 font-bold italic">Tender Search</span> to begin. |
| </p> |
| </div> |
| |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl w-full px-4"> |
| {agents.map(agent => ( |
| <div key={agent.id} className="glass-card rounded-3xl p-8 space-y-4 border border-white/5"> |
| <div className="text-4xl">{agent.avatar}</div> |
| <div> |
| <h4 className={`text-xs font-bold uppercase tracking-widest ${agent.color} mb-1`}>{agent.role}</h4> |
| <h3 className="text-xl font-bold text-white mb-2">{agent.name}</h3> |
| <p className="text-sm text-slate-500 leading-relaxed">{agent.desc}</p> |
| </div> |
| </div> |
| ))} |
| </div> |
| |
| <div className="flex items-center gap-3 text-slate-600 animate-bounce"> |
| <span className="text-sm font-bold uppercase tracking-widest">Go to Tender Search</span> |
| <span className="text-xl">↓</span> |
| </div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700"> |
| {/* Tender Header Card */} |
| <div className="glass-card relative overflow-hidden rounded-3xl p-8 border border-white/10"> |
| <div className="absolute -right-20 -top-20 h-64 w-64 rounded-full bg-purple-500/10 blur-[100px]" /> |
| <div className="relative z-10 flex flex-col gap-8 lg:flex-row lg:items-center lg:justify-between"> |
| <div className="max-w-3xl"> |
| <div className="flex items-center gap-3 mb-4"> |
| <span className="rounded-full bg-purple-500/20 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-purple-300 border border-purple-500/30">Active Opportunity</span> |
| <span className="text-xs text-slate-500 font-mono">{tender?.code}</span> |
| </div> |
| <h2 className="text-4xl font-bold text-white tracking-tight leading-tight mb-4">{tender?.name}</h2> |
| <p className="text-slate-400 text-lg leading-relaxed">{tender?.buyer}</p> |
| |
| {tender && ( |
| <div className="mt-8 flex flex-wrap gap-4"> |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5"> |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1">Estimated Amount</p> |
| <p className="text-lg font-semibold text-white"> |
| {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount) : "Not disclosed"} |
| </p> |
| </div> |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5"> |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1">Closing Date</p> |
| <p className="text-lg font-semibold text-white">{tender.closing_date}</p> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| <div className="flex flex-col gap-4 lg:w-80"> |
| <div className="glass-card rounded-2xl p-6 bg-white/5 border border-white/10"> |
| <h4 className="text-[10px] font-bold uppercase text-slate-400 mb-4 tracking-widest">Administrative Bases (PDF)</h4> |
| <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" /> |
| {file && <p className="mt-2 text-[10px] text-green-400 font-bold">✓ Ready for extraction</p>} |
| </div> |
| |
| <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"> |
| <input type="checkbox" checked={approved} onChange={(e) => setApproved(e.target.checked)} className="h-5 w-5 rounded border-white/20 bg-black text-purple-500 outline-none accent-purple-500" /> |
| <span className="text-xs font-semibold text-slate-300">Authorize Agent War Room</span> |
| </label> |
| |
| <button |
| onClick={handleAnalyzeClick} |
| disabled={!tender || !approved || isRunning || isUploading} |
| 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]" |
| > |
| {isUploading ? "Extracting Text..." : isRunning ? "Agents Debating..." : "Launch Analysis Pipeline"} |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Agents Row (Visual feedback) */} |
| <div className="grid gap-6 md:grid-cols-3"> |
| {agents.map((agent) => ( |
| <div key={agent.id} className={`glass-card rounded-3xl p-6 flex items-center gap-4 transition-all duration-700 ${isRunning ? 'ring-2 ring-purple-500/50 animate-pulse' : ''} ${analysis ? 'border-purple-500/30' : 'border-white/5'}`}> |
| <div className={`text-4xl ${isRunning ? 'animate-bounce' : ''}`}>{agent.avatar}</div> |
| <div> |
| <div className={`text-[10px] font-bold uppercase tracking-widest ${agent.color}`}>{agent.role}</div> |
| <div className="text-sm font-bold text-white">{agent.name}</div> |
| {analysis && <div className="text-[9px] text-green-400 font-bold mt-1 uppercase tracking-tighter tracking-widest">Analysis Ready ✓</div>} |
| </div> |
| </div> |
| ))} |
| </div> |
| |
| {/* Analysis Results View */} |
| {analysis && ( |
| <div className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-1000"> |
| <div className="lg:col-span-8 space-y-8"> |
| <div className="glass-card rounded-3xl p-10 bg-white/[0.02]"> |
| <div className="flex items-start justify-between mb-8"> |
| <div> |
| <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div> |
| <h3 className="text-6xl font-black text-white">{analysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3> |
| </div> |
| <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'}`}> |
| {analysis.decision} |
| </div> |
| </div> |
| <div className="prose prose-invert max-w-none"> |
| <p className="text-slate-300 text-xl leading-relaxed italic border-l-4 border-purple-500 pl-8">{analysis.executive_summary}</p> |
| </div> |
| </div> |
| |
| <div className="grid gap-6 md:grid-cols-2"> |
| <div className="glass-card rounded-3xl p-8 bg-white/[0.01]"> |
| <h4 className="text-[11px] font-bold uppercase tracking-widest text-amber-400 mb-6 flex items-center gap-2"> |
| <span>⚠️</span> Legal Compliance Gaps |
| </h4> |
| <ul className="space-y-4"> |
| {analysis.compliance_gaps.map((gap, i) => ( |
| <li key={i} className="flex gap-4 text-sm text-slate-400 leading-relaxed"> |
| <span className="text-amber-500 font-bold">•</span> {gap} |
| </li> |
| ))} |
| </ul> |
| </div> |
| <div className="glass-card rounded-3xl p-8 bg-white/[0.01]"> |
| <h4 className="text-[11px] font-bold uppercase tracking-widest text-cyan mb-6 flex items-center gap-2"> |
| <span>💎</span> Technical Requirements |
| </h4> |
| <ul className="space-y-4"> |
| {analysis.key_requirements.map((req, i) => ( |
| <li key={i} className="flex gap-4 text-sm text-slate-400 leading-relaxed"> |
| <span className="text-cyan font-bold">▹</span> {req} |
| </li> |
| ))} |
| </ul> |
| </div> |
| </div> |
| |
| <div className="glass-card rounded-3xl p-10 bg-white/[0.01]"> |
| <h4 className="text-[11px] font-bold uppercase tracking-widest text-purple-400 mb-8 text-center">Neural Risk Matrix</h4> |
| <div className="grid gap-6 md:grid-cols-2"> |
| {analysis.risks.map((risk, i) => ( |
| <div key={i} className="group rounded-3xl bg-white/[0.02] p-6 border border-white/5 hover:border-purple-500/30 transition-all duration-300"> |
| <div className="flex items-center justify-between mb-4"> |
| <span className="font-bold text-white text-lg group-hover:text-purple-400 transition">{risk.title}</span> |
| <span className={`text-[9px] font-black px-3 py-1 rounded-full uppercase tracking-widest ${risk.severity === 'High' ? 'bg-red-500/20 text-red-500 border border-red-500/20' : 'bg-white/5 text-slate-500 border border-white/5'}`}>{risk.severity}</span> |
| </div> |
| <p className="text-xs text-slate-500 leading-relaxed">{risk.explanation}</p> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| <div className="lg:col-span-4"> |
| <div className="glass-card rounded-3xl p-8 bg-black/40 h-full sticky top-32"> |
| <div className="flex items-center gap-3 mb-8 border-b border-white/5 pb-6"> |
| <div className="h-2 w-2 rounded-full bg-purple-500 animate-pulse shadow-[0_0_12px_rgba(168,85,247,0.8)]" /> |
| <h4 className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Agent Intelligence Log</h4> |
| </div> |
| <div className="space-y-8 overflow-y-auto max-h-[700px] pr-2 custom-scrollbar"> |
| {analysis.audit_log?.map((log, i) => ( |
| <div key={i} className="flex gap-5 group"> |
| <div className="flex flex-col items-center"> |
| <div className="h-8 w-8 rounded-xl bg-white/5 flex items-center justify-center text-sm border border-white/10 group-hover:border-purple-500/50 transition-all duration-300 shadow-lg">🤖</div> |
| {i < (analysis.audit_log?.length ?? 0) - 1 && <div className="w-px flex-1 bg-gradient-to-b from-purple-500/40 to-transparent my-3" />} |
| </div> |
| <div className="pb-6"> |
| <p className="text-[13px] text-slate-400 leading-relaxed group-hover:text-white transition-colors duration-300">{log}</p> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|