Álvaro Valenzuela Valdes
fix: revert portal links to search page to fix permission errors while keeping enhanced backend scraping
7f9e1cc | "use client"; | |
| import { useState, useEffect } from "react"; | |
| import type { AnalysisResult, CompanyProfile, Tender, TenderDetailInfo } from "../lib/types"; | |
| import { uploadDocument, fetchTenderDetails } from "../lib/api"; | |
| import AgentChat from "./AgentChat"; | |
| type Props = { | |
| tender: Tender | null; | |
| companyProfile: CompanyProfile; | |
| analysis: AnalysisResult | null; | |
| onAnalyze: (documentText?: string, models?: Record<string, string>, tenderDetails?: TenderDetailInfo | null) => Promise<void>; | |
| onBackToSearch: () => 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, onBackToSearch }: 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 [agentModels, setAgentModels] = useState({ | |
| legal: "Gemini 2.5 Flash", | |
| tech: "DeepSeek-V3.2 (Featherless)", | |
| risk: "Qwen-2.5 (Featherless)" | |
| }); | |
| const [activeSettings, setActiveSettings] = useState<string | null>(null); | |
| const [statusLog, setStatusLog] = useState<string[]>([]); | |
| const [error, setError] = useState<string | null>(null); | |
| const [tenderDetails, setTenderDetails] = useState<TenderDetailInfo | null>(null); | |
| const [isLoadingDetails, setIsLoadingDetails] = useState(false); | |
| // Multiple Files Support (The Corral) | |
| const [corral, setCorral] = useState<Array<{ file: File, text: string, analysis: AnalysisResult | null, id: string }>>([]); | |
| const [activeAnimalId, setActiveAnimalId] = useState<string | null>(null); | |
| const [generatedAnnexes, setGeneratedAnnexes] = useState<Array<{ name: string, content: string }>>([]); | |
| const [isGeneratingAnnexes, setIsGeneratingAnnexes] = useState(false); | |
| const [pdfUrls, setPdfUrls] = useState<Record<string, string>>({}); | |
| // Removed auto-scroll to keep user at the top during demo recordings | |
| // Fetch Tender Details (Scraped) | |
| useEffect(() => { | |
| const getDetails = async () => { | |
| if (!tender?.code) return; | |
| setIsLoadingDetails(true); | |
| try { | |
| // Try to get details using both code and potential qs (if available in tender object) | |
| // Note: For now we use code, if the API returns a qs param we should use it | |
| const details = await fetchTenderDetails(tender.code); | |
| setTenderDetails(details); | |
| } catch (err) { | |
| console.error("Failed to fetch tender details:", err); | |
| } finally { | |
| setIsLoadingDetails(false); | |
| } | |
| }; | |
| getDetails(); | |
| }, [tender?.code]); | |
| const generateAnnexes = async () => { | |
| if (!tender) return; | |
| setIsGeneratingAnnexes(true); | |
| // Simulate AI generating specific annexes based on tender data | |
| setTimeout(() => { | |
| const annexes = [ | |
| { | |
| name: "Anexo 1: Identificación del Oferente", | |
| content: `# ANEXO N°1\nIDENTIFICACIÓN DEL OFERENTE\n\n**Licitación:** ${tender.name}\n**ID:** ${tender.code}\n\n**RAZÓN SOCIAL:** ${companyProfile.name}\n**RUT:** 77.345.123-K\n**REPRESENTANTE LEGAL:** Álvaro Pérez\n**DOMICILIO:** Av. Apoquindo 4500, Las Condes, Santiago.\n**GIRO:** ${companyProfile.industry}\n\n*Documento generado automáticamente por AndesOps AI.*` | |
| }, | |
| { | |
| name: "Anexo 2: Declaración Jurada Simple", | |
| content: `# ANEXO N°2\nDECLARACIÓN JURADA SIMPLE\n\nYo, Álvaro Pérez, en representación de ${companyProfile.name}, declaro bajo juramento que mi representada no se encuentra afecta a ninguna de las inhabilidades previstas en el artículo 92 de la Ley N° 19.886.\n\n**Fecha:** ${new Date().toLocaleDateString()}\n\n__________________________\nFirma Representante Legal` | |
| }, | |
| { | |
| name: "Anexo 3: Experiencia del Oferente", | |
| content: `# ANEXO N°3\nEXPERIENCIA DEL OFERENTE\n\n**Empresa:** ${companyProfile.name}\n**Años de Experiencia:** ${companyProfile.experience}\n\n**Principales Servicios:**\n${companyProfile.services.map(s => `- ${s}`).join('\n')}\n\n**Certificaciones:**\n${companyProfile.certifications.map(c => `- ${c}`).join('\n')}\n\n*Validado por AndesOps AI Intelligence.*` | |
| } | |
| ]; | |
| setGeneratedAnnexes(annexes); | |
| setIsGeneratingAnnexes(false); | |
| // Smooth scroll to annexes | |
| setTimeout(() => { | |
| document.getElementById('annexes-section')?.scrollIntoView({ behavior: 'smooth' }); | |
| }, 100); | |
| }, 2000); | |
| }; | |
| const downloadAsPDF = async (annex: { name: string, content: string }) => { | |
| try { | |
| const { jsPDF } = await import("jspdf"); | |
| const doc = new jsPDF(); | |
| // Title | |
| doc.setFontSize(22); | |
| doc.setTextColor(40, 40, 40); | |
| doc.text("ANDESOPS AI - COMPLIANCE", 20, 20); | |
| doc.setDrawColor(168, 85, 247); // Purple line | |
| doc.setLineWidth(1); | |
| doc.line(20, 25, 190, 25); | |
| // Content | |
| doc.setFontSize(16); | |
| doc.setTextColor(0, 0, 0); | |
| doc.text(annex.name, 20, 40); | |
| doc.setFontSize(10); | |
| doc.setFont("helvetica", "normal"); | |
| const splitText = doc.splitTextToSize(annex.content.replace(/# /g, '').replace(/\*\*/g, '').replace(/### /g, ''), 170); | |
| doc.text(splitText, 20, 55); | |
| // Footer | |
| doc.setFontSize(8); | |
| doc.setTextColor(150, 150, 150); | |
| doc.text(`Document generated by AndesOps AI on ${new Date().toLocaleString()}`, 20, 280); | |
| doc.save(`${annex.name.replace(/ /g, '_')}.pdf`); | |
| } catch (err) { | |
| console.error("PDF Export failed:", err); | |
| alert("PDF Export failed. Downloading as Markdown instead."); | |
| const blob = new Blob([annex.content], { type: 'text/markdown' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${annex.name.replace(/ /g, '_')}.md`; | |
| a.click(); | |
| } | |
| }; | |
| const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { | |
| if (event.target.files && event.target.files.length > 0) { | |
| const filesArray = Array.from(event.target.files); | |
| setIsUploading(true); | |
| setError(null); | |
| try { | |
| for (const newFile of filesArray) { | |
| const id = Math.random().toString(36).substring(7); | |
| const uploadResult = await uploadDocument(newFile); | |
| const newEntry = { | |
| file: newFile, | |
| text: uploadResult.text, | |
| analysis: null, | |
| id | |
| }; | |
| if (newFile.type === "application/pdf") { | |
| const url = URL.createObjectURL(newFile); | |
| setPdfUrls(prev => ({ ...prev, [id]: url })); | |
| } | |
| setCorral(prev => [...prev, newEntry]); | |
| setActiveAnimalId(id); | |
| } | |
| } catch (err) { | |
| console.error("Upload error", err); | |
| setError("Failed to upload and process one or more documents."); | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| } | |
| }; | |
| const removeFromCorral = (id: string, e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| setCorral(prev => prev.filter(a => a.id !== id)); | |
| if (activeAnimalId === id) setActiveAnimalId(null); | |
| }; | |
| const handleAnalyzeClick = async () => { | |
| if (!approved || !tender || !activeAnimalId) return; | |
| const activeEntry = corral.find(a => a.id === activeAnimalId); | |
| if (!activeEntry) return; | |
| setIsRunning(true); | |
| setError(null); | |
| setStatusLog(["🚀 Initializing Agent War Room...", `📡 Focusing on: ${activeEntry.file.name}...`]); | |
| try { | |
| setStatusLog(prev => [...prev, "🤝 Summoning experts: Legal, Technical, and Strategy..."]); | |
| const progressTimer = setInterval(() => { | |
| const messages = [ | |
| "⚖️ Dra. Legal is reviewing clauses...", | |
| "👨💻 Ing. Tech is analyzing feasibility...", | |
| "🕵️♀️ Sra. Estrategia is calculating ROI...", | |
| "🧠 Synthesizing consensus..." | |
| ]; | |
| setStatusLog(prev => { | |
| if (prev.length < 10) { | |
| const nextMsg = messages[Math.floor(Math.random() * messages.length)]; | |
| if (!prev.includes(nextMsg)) return [...prev, nextMsg]; | |
| } | |
| return prev; | |
| }); | |
| }, 800); // Faster log timing for snappier feel | |
| // We call the parent's onAnalyze but we want the result back locally too | |
| // Actually, since we want multiple analyses, we might need to handle the result here | |
| // For now, let's assume the parent updates the main analysis prop, but we'll store it in the corral too | |
| await onAnalyze(activeEntry.text, agentModels, tenderDetails); | |
| clearInterval(progressTimer); | |
| setStatusLog(prev => [...prev, "✨ Analysis complete!"]); | |
| } catch (err) { | |
| console.error("Error during analysis flow:", err); | |
| setError("The analysis pipeline encountered a technical failure."); | |
| setStatusLog(prev => [...prev, "❌ Error occurred during analysis pipeline."]); | |
| } finally { | |
| setIsRunning(false); | |
| } | |
| }; | |
| // Sync parent analysis to corral entry | |
| useEffect(() => { | |
| if (analysis && activeAnimalId) { | |
| setCorral(prev => prev.map(a => a.id === activeAnimalId ? { ...a, analysis } : a)); | |
| } | |
| }, [analysis]); | |
| const activeAnalysis = corral.find(a => a.id === activeAnimalId)?.analysis || analysis; | |
| const getFileIcon = (fileName: string) => { | |
| const ext = fileName.split('.').pop()?.toLowerCase(); | |
| if (ext === 'pdf') return { emoji: "📄", label: "PDF", color: "bg-red-500/20 text-red-400 border-red-500/30" }; | |
| if (ext === 'doc' || ext === 'docx') return { emoji: "📝", label: "DOC", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" }; | |
| if (ext === 'xls' || ext === 'xlsx') return { emoji: "📊", label: "XLS", color: "bg-green-500/20 text-green-400 border-green-500/30" }; | |
| if (ext === 'zip' || ext === 'rar') return { emoji: "📦", label: "ZIP", color: "bg-amber-500/20 text-amber-400 border-amber-500/30" }; | |
| return { emoji: "📁", label: "FILE", color: "bg-slate-500/20 text-slate-400 border-white/10" }; | |
| }; | |
| if (!tender && !analysis) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center min-h-[60vh] space-y-12 animate-in fade-in duration-300"> | |
| <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> | |
| <button | |
| onClick={onBackToSearch} | |
| className="flex items-center gap-3 text-purple-400 hover:text-purple-300 transition-all active:scale-95 group" | |
| > | |
| <span className="text-xl transition-transform group-hover:-translate-x-1">←</span> | |
| <span className="text-sm font-bold uppercase tracking-widest underline decoration-purple-500/30 underline-offset-8">Back to Opportunities</span> | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500"> | |
| {/* Navigation Header */} | |
| <div className="flex justify-start"> | |
| <button | |
| onClick={onBackToSearch} | |
| className="flex items-center gap-4 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-400 hover:text-white hover:bg-white/10 transition-all group active:scale-95" | |
| > | |
| <span className="text-2xl transition-transform group-hover:-translate-x-1">←</span> | |
| <span className="text-xs font-black uppercase tracking-widest">Back to Opportunities</span> | |
| </button> | |
| </div> | |
| {/* 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> | |
| {tender?.name.toLowerCase().includes('sustentable') || tender?.description?.toLowerCase().includes('ambiental') ? ( | |
| <span className="rounded-full bg-green-500/20 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-green-400 border border-green-500/30 animate-pulse">🌱 Sustainable / Compra Ágil</span> | |
| ) : null} | |
| {tenderDetails?.metadata?.question_count && tenderDetails.metadata.question_count > 0 ? ( | |
| <span className="rounded-full bg-cyan/20 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-cyan border border-cyan/30"> | |
| 💬 {tenderDetails.metadata.question_count} Questions | |
| </span> | |
| ) : null} | |
| <span className="text-xs text-slate-500 font-mono">{tender?.code}</span> | |
| </div> | |
| <h2 className="text-3xl md:text-4xl font-bold text-white tracking-tight leading-tight mb-4">{tender?.name}</h2> | |
| <p className="text-slate-400 text-base md:text-lg leading-relaxed">{tender?.buyer}</p> | |
| {tender && ( | |
| <div className="mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5 group hover:bg-white/10 transition-colors"> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Investment</p> | |
| <p className="text-lg font-black text-white"> | |
| {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: tender.currency || "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount) : "N/A"} | |
| </p> | |
| </div> | |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5 group hover:bg-white/10 transition-colors"> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Closing Date</p> | |
| <p className="text-lg font-black text-white">{tender.closing_date || "TBD"}</p> | |
| </div> | |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5 group hover:bg-white/10 transition-colors"> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Region</p> | |
| <p className="text-lg font-black text-white truncate" title={tender.region}>{tender.region || "Nacional"}</p> | |
| </div> | |
| <div className="rounded-2xl bg-white/5 p-4 border border-white/5 group hover:bg-white/10 transition-colors"> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Sector</p> | |
| <p className="text-lg font-black text-white truncate">{tender.sector || "General"}</p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Buyer Risk & Experience Cards */} | |
| {(tender?.buyer_complaints !== undefined || tender?.buyer_purchases !== undefined || tenderDetails?.metadata?.buyer_complaints !== undefined) && ( | |
| <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className={`rounded-2xl p-4 border flex items-center justify-between transition-all ${(tender?.buyer_complaints ?? tenderDetails?.metadata?.buyer_complaints ?? 0) > 10 ? 'bg-red-500/10 border-red-500/30 shadow-lg shadow-red-500/10' : 'bg-white/5 border-white/5'}`}> | |
| <div> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Complaints (Last 12m)</p> | |
| <p className={`text-2xl font-black ${(tender?.buyer_complaints ?? tenderDetails?.metadata?.buyer_complaints ?? 0) > 10 ? 'text-red-400' : 'text-white'}`}> | |
| {tender?.buyer_complaints ?? tenderDetails?.metadata?.buyer_complaints ?? 0} | |
| </p> | |
| </div> | |
| <div className="text-2xl opacity-50">{(tender?.buyer_complaints ?? tenderDetails?.metadata?.buyer_complaints ?? 0) > 10 ? '⚠️' : '✅'}</div> | |
| </div> | |
| <div className="rounded-2xl bg-cyan/5 p-4 border border-cyan/10 flex items-center justify-between transition-all hover:bg-cyan/10"> | |
| <div> | |
| <p className="text-[10px] uppercase text-slate-500 font-bold mb-1 tracking-widest">Purchases Executed</p> | |
| <p className="text-2xl font-black text-cyan"> | |
| {tender?.buyer_purchases ?? tenderDetails?.metadata?.buyer_purchases ?? "1.6k+"} | |
| </p> | |
| </div> | |
| <div className="text-2xl opacity-50">🛒</div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Guarantees Section */} | |
| {tenderDetails?.metadata?.guarantees && tenderDetails.metadata.guarantees.length > 0 && ( | |
| <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {tenderDetails.metadata.guarantees.map((g: any, i: number) => ( | |
| <div key={i} className="rounded-2xl bg-amber-500/5 border border-amber-500/20 p-4 flex items-center justify-between"> | |
| <div> | |
| <p className="text-[9px] uppercase text-amber-500/60 font-black tracking-[0.2em] mb-1">{g.type}</p> | |
| <p className="text-sm font-bold text-white">{g.amount}</p> | |
| </div> | |
| <div className="text-xl">🛡️</div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {tender?.description && ( | |
| <div className="mt-8 p-6 rounded-2xl bg-white/[0.02] border border-white/5"> | |
| <h4 className="text-[10px] font-bold uppercase text-slate-500 mb-3 tracking-[0.2em]">Detailed Scope</h4> | |
| <p className="text-sm text-slate-400 leading-relaxed max-h-32 overflow-y-auto custom-scrollbar pr-2 whitespace-pre-wrap"> | |
| {tender.description} | |
| </p> | |
| </div> | |
| )} | |
| {tender?.items && tender.items.length > 0 && ( | |
| <div className="mt-8 overflow-hidden rounded-2xl border border-white/5 bg-white/[0.01]"> | |
| <table className="w-full text-left text-[10px]"> | |
| <thead className="bg-white/5 text-slate-500 uppercase font-black tracking-widest"> | |
| <tr> | |
| <th className="px-6 py-3">Item Name</th> | |
| <th className="px-6 py-3 text-right">Qty</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {tender.items.slice(0, 3).map((item, idx) => ( | |
| <tr key={idx} className="hover:bg-white/[0.02]"> | |
| <td className="px-6 py-3 text-slate-300 font-medium truncate max-w-[200px]">{item.name}</td> | |
| <td className="px-6 py-3 text-right text-cyan font-mono font-bold">{item.quantity} {item.unit}</td> | |
| </tr> | |
| ))} | |
| {tender.items.length > 3 && ( | |
| <tr> | |
| <td colSpan={2} className="px-6 py-2 text-center text-[9px] text-slate-600 italic"> | |
| + {tender.items.length - 3} more items... | |
| </td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {/* Detailed Scraped Items */} | |
| {tenderDetails?.metadata?.detailed_items && tenderDetails.metadata.detailed_items.length > 0 && ( | |
| <div className="mt-8 overflow-hidden rounded-2xl border border-purple-500/20 bg-purple-500/5"> | |
| <div className="bg-purple-500/10 px-6 py-3 border-b border-purple-500/20"> | |
| <h4 className="text-[10px] font-black uppercase text-purple-300 tracking-widest">Portal Line Items Intelligence</h4> | |
| </div> | |
| <div className="p-4 space-y-3 max-h-60 overflow-y-auto custom-scrollbar"> | |
| {tenderDetails.metadata.detailed_items.map((item: any, idx: number) => ( | |
| <div key={idx} className="flex gap-4 items-start p-3 rounded-xl bg-white/5 border border-white/5 hover:border-purple-500/30 transition-all"> | |
| <span className="bg-purple-500/20 text-purple-400 px-2 py-1 rounded text-[9px] font-mono font-bold shrink-0">{item.code}</span> | |
| <p className="text-[11px] text-slate-300 leading-relaxed italic">"{item.description}"</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Scraped Intelligence / Tabs */} | |
| {tenderDetails && ( | |
| <div className="mt-8 flex flex-wrap gap-3"> | |
| {tenderDetails.tabs?.history?.found && ( | |
| <div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] sm:text-[10px] font-bold text-slate-400"> | |
| <span className="text-purple-400 text-xs">📜</span> History Available | |
| </div> | |
| )} | |
| <a | |
| href={`https://www.mercadopublico.cl/Portal/BuscarLicitacion?texto=${tender?.code}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="px-4 py-2 rounded-xl bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-400 hover:bg-purple-500/20 transition-all uppercase tracking-widest whitespace-nowrap" | |
| > | |
| Visit Official Site 🔗 | |
| </a> | |
| {tenderDetails.metadata?.question_count && tenderDetails.metadata.question_count > 0 ? ( | |
| <a | |
| href={`https://www.mercadopublico.cl/Procurement/Modules/RFB/DetailsAcquisition.aspx?codigo=${tender?.code}&tab=4`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center gap-2 px-3 py-2 rounded-xl bg-cyan/10 border border-cyan/30 text-[9px] sm:text-[10px] font-bold text-cyan hover:bg-cyan/20 transition-all animate-pulse" | |
| > | |
| <span className="text-xs">❓</span> View {tenderDetails.metadata.question_count} Questions in Portal 🔗 | |
| </a> | |
| ) : ( | |
| tenderDetails.tabs?.questions?.found && ( | |
| <div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] sm:text-[10px] font-bold text-slate-400"> | |
| <span className="text-cyan text-xs">❓</span> Q&A Active | |
| </div> | |
| ) | |
| )} | |
| {tenderDetails.tabs?.opening?.found && ( | |
| <div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] sm:text-[10px] font-bold text-slate-400"> | |
| <span className="text-green-400 text-xs">🔓</span> Opening Log Found | |
| </div> | |
| )} | |
| {tenderDetails.metadata?.has_adjudication && ( | |
| <div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-green-500/10 border border-green-500/20 text-[9px] sm:text-[10px] font-bold text-green-400"> | |
| <span className="text-xs">🏆</span> Adjudicated | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Scraped Attachments (Extended List) */} | |
| {tenderDetails?.attachments && tenderDetails.attachments.length > 0 && ( | |
| <div className="mt-8 space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="text-[10px] font-bold uppercase text-slate-500 tracking-[0.2em]">Scraped Attachments ({tenderDetails.attachments.length})</h4> | |
| {isLoadingDetails && <span className="text-[9px] text-purple-400 animate-pulse uppercase font-black">Refreshing...</span>} | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> | |
| {tenderDetails.attachments.slice(0, 6).map((att, idx) => ( | |
| <a | |
| key={idx} | |
| href={att.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center gap-3 p-3 rounded-xl bg-white/[0.03] border border-white/5 hover:bg-white/10 hover:border-purple-500/30 transition-all group" | |
| > | |
| <span className="text-lg group-hover:scale-110 transition-transform"> | |
| {att.name.toLowerCase().includes('bases') ? '⚖️' : | |
| att.name.toLowerCase().includes('tecnico') ? '🛠️' : | |
| att.name.toLowerCase().includes('anexo') ? '📝' : '📄'} | |
| </span> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-[11px] font-bold text-slate-300 truncate group-hover:text-white">{att.name}</p> | |
| <p className="text-[9px] text-slate-500 uppercase tracking-tighter">Direct Download 📥</p> | |
| </div> | |
| </a> | |
| ))} | |
| {tenderDetails.attachments.length > 6 && ( | |
| <div className="flex items-center justify-center p-3 rounded-xl border border-dashed border-white/10 text-[9px] text-slate-600 uppercase font-bold"> | |
| + {tenderDetails.attachments.length - 6} more attachments | |
| </div> | |
| )} | |
| </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">Document Corral</h4> | |
| {/* The Corral (Animal Pen) */} | |
| <div className="flex flex-wrap gap-3 mb-6"> | |
| {corral.map((item) => { | |
| const icon = getFileIcon(item.file.name); | |
| return ( | |
| <div key={item.id} className="relative group"> | |
| <button | |
| onClick={() => setActiveAnimalId(item.id)} | |
| className={`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'}`} | |
| title={item.file.name} | |
| > | |
| <span className={`text-2xl transition-all duration-500 ${activeAnimalId === item.id ? 'scale-110' : ''}`}> | |
| {icon.emoji} | |
| </span> | |
| <span className="text-[8px] font-black uppercase mt-1 opacity-60">{icon.label}</span> | |
| <div className="absolute -bottom-6 left-0 right-0 text-center"> | |
| <p className="text-[8px] text-slate-500 truncate px-1 font-mono">{item.file.name}</p> | |
| </div> | |
| {item.analysis && <span className="absolute -top-1 -right-1 h-3 w-3 bg-green-500 rounded-full border-2 border-black" title="Analyzed" />} | |
| </button> | |
| <button | |
| onClick={(e) => removeFromCorral(item.id, e)} | |
| className="absolute -top-2 -right-2 h-5 w-5 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg z-20" | |
| > | |
| × | |
| </button> | |
| </div> | |
| ); | |
| })} | |
| <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"> | |
| <span className="text-xl text-slate-500">+</span> | |
| <input type="file" onChange={handleFileChange} className="hidden" multiple /> | |
| </label> | |
| </div> | |
| <div className="text-[10px] text-slate-500 italic mb-4"> | |
| {corral.length === 0 ? "No documents in the corral." : `${corral.length} document(s) ready.`} | |
| </div> | |
| {isUploading && <p className="text-[10px] text-purple-400 animate-pulse font-bold">✨ Bringing animal to corral...</p>} | |
| {/* PDF Viewer for Active Selection */} | |
| {activeAnimalId && pdfUrls[activeAnimalId] && ( | |
| <div className="mt-6 rounded-2xl overflow-hidden border border-white/10 bg-black/40 h-80 relative group flex flex-col"> | |
| <div className="flex-1 w-full bg-slate-900/50 flex items-center justify-center relative"> | |
| <embed | |
| src={pdfUrls[activeAnimalId]} | |
| type="application/pdf" | |
| className="w-full h-full" | |
| /> | |
| {/* Fallback Overlay for blocked frames */} | |
| <div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity bg-black/40"> | |
| <p className="text-[10px] text-white font-bold bg-purple-600 px-3 py-1 rounded-full shadow-lg">Document Preview Mode</p> | |
| </div> | |
| </div> | |
| <div className="p-3 bg-slate-950 flex justify-between items-center border-t border-white/5"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xl">📄</span> | |
| <p className="text-[10px] font-mono text-slate-300 truncate max-w-[120px]"> | |
| {corral.find(a => a.id === activeAnimalId)?.file.name} | |
| </p> | |
| </div> | |
| <a | |
| href={pdfUrls[activeAnimalId]} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="px-3 py-1 rounded-lg bg-cyan/10 border border-cyan/20 text-[10px] font-black text-cyan hover:bg-cyan hover:text-black transition-all" | |
| > | |
| OPEN FULL PDF ↗ | |
| </a> | |
| </div> | |
| </div> | |
| )} | |
| </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 || !activeAnimalId} | |
| 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]" | |
| > | |
| {isRunning ? "Agents Debating..." : "Launch Analysis Pipeline"} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Agents Row (Visual feedback & Configuration) */} | |
| <div className="grid gap-6 md:grid-cols-3"> | |
| {agents.map((agent) => ( | |
| <div key={agent.id} className="relative group"> | |
| <div 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'} hover:border-purple-500/20`}> | |
| <div className={`text-4xl ${isRunning ? 'animate-bounce' : ''}`}>{agent.avatar}</div> | |
| <div className="flex-1"> | |
| <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> | |
| <div className="text-[9px] text-slate-500 font-mono mt-1 flex items-center gap-1"> | |
| <span className="w-1 h-1 rounded-full bg-slate-500" /> | |
| {agentModels[agent.id as keyof typeof agentModels]} | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => setActiveSettings(activeSettings === agent.id ? null : agent.id)} | |
| className="p-2 rounded-xl bg-white/5 text-slate-500 hover:bg-white/10 hover:text-white transition-all active:scale-90" | |
| > | |
| ⚙️ | |
| </button> | |
| </div> | |
| {/* Model Selector Popover */} | |
| {activeSettings === agent.id && ( | |
| <div className="absolute top-full left-0 right-0 mt-2 z-50 glass-card rounded-2xl p-4 border border-purple-500/30 shadow-2xl animate-in fade-in zoom-in-95 duration-200"> | |
| <p className="text-[9px] font-black uppercase text-purple-400 mb-3 tracking-widest px-1">Select Engine</p> | |
| <div className="space-y-1"> | |
| {[ | |
| "Gemini 2.5 Flash", | |
| "DeepSeek-V3 (Featherless)", | |
| "Qwen-2.5 (Featherless)", | |
| "Llama-3.3-70B (Groq)", | |
| "Llama-3.1-8B (Groq)", | |
| "Mixtral-8x7B (Groq)", | |
| "Gemma-4-31B (Featherless)", | |
| "Llama-3.1-8B (Featherless)" | |
| ].map(model => ( | |
| <button | |
| key={model} | |
| onClick={() => { | |
| setAgentModels(prev => ({ ...prev, [agent.id]: model })); | |
| setActiveSettings(null); | |
| }} | |
| className={`w-full text-left px-4 py-3 rounded-xl text-sm font-medium transition-all flex items-center justify-between border ${agentModels[agent.id as keyof typeof agentModels] === model ? 'bg-purple-500/20 text-white border-purple-500/50 shadow-lg shadow-purple-500/10' : 'text-slate-400 border-transparent hover:bg-white/10 hover:text-white hover:border-white/10'}`} | |
| > | |
| <span>{model}</span> | |
| {agentModels[agent.id as keyof typeof agentModels] === model && <span className="text-purple-400">●</span>} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| {/* Running State Log */} | |
| {isRunning && ( | |
| <div className="glass-card rounded-3xl p-8 border border-purple-500/30 bg-purple-500/5 animate-in fade-in zoom-in-95 duration-500"> | |
| <div className="flex items-center gap-4 mb-6"> | |
| <div className="h-4 w-4 rounded-full bg-purple-500 animate-ping" /> | |
| <h3 className="text-xl font-bold text-white">Pipeline in Progress</h3> | |
| </div> | |
| <div className="space-y-3"> | |
| {statusLog.map((log, i) => ( | |
| <div key={i} className="flex items-center gap-3 animate-in slide-in-from-left-4 duration-300"> | |
| <span className="text-purple-400 font-mono text-xs">[{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}]</span> | |
| <p className="text-sm text-slate-300">{log}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Error State */} | |
| {error && ( | |
| <div className="glass-card rounded-3xl p-8 border border-red-500/30 bg-red-500/5 animate-in fade-in zoom-in-95 duration-500"> | |
| <div className="flex items-center gap-4 mb-4"> | |
| <span className="text-3xl">⚠️</span> | |
| <h3 className="text-xl font-bold text-white">Analysis Failed</h3> | |
| </div> | |
| <p className="text-slate-400 mb-6">{error}</p> | |
| <button | |
| onClick={handleAnalyzeClick} | |
| className="px-6 py-3 rounded-2xl bg-red-500/20 text-red-400 font-bold border border-red-500/30 hover:bg-red-500/30 transition-all active:scale-95" | |
| > | |
| Retry Analysis | |
| </button> | |
| </div> | |
| )} | |
| {/* Analysis Results & Intelligent Sections */} | |
| {activeAnalysis && ( | |
| <div id="analysis-results" className="grid gap-8 grid-cols-1 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-500 scroll-mt-20"> | |
| <div className="lg:col-span-8 space-y-8"> | |
| {/* Main Analysis Card */} | |
| <div className="glass-card rounded-3xl p-6 sm:p-10 bg-white/[0.02]"> | |
| {/* Professional Print Header */} | |
| <div className="hidden print-only mb-12 border-b-4 border-slate-900 pb-8 text-center"> | |
| <h1 className="text-4xl font-black text-slate-900 mb-2">ANDESOPS AI</h1> | |
| <p className="text-sm font-bold uppercase tracking-[0.5em] text-slate-500">Intelligent Bidding Analysis Report</p> | |
| </div> | |
| <div className="flex flex-col sm:flex-row items-start justify-between gap-6 mb-8"> | |
| <div> | |
| <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div> | |
| <h3 className="text-4xl sm:text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-xl sm:text-2xl font-light text-slate-500">Fit Score</span></h3> | |
| <div className="mt-2 flex items-center gap-2"> | |
| <span className="text-[10px] text-slate-500 font-mono">Analyzing:</span> | |
| <span className="text-[10px] text-purple-300 font-bold truncate max-w-[200px]">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span> | |
| </div> | |
| <div className={`w-full sm:w-auto text-center 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'}`}> | |
| {activeAnalysis.decision} | |
| </div> | |
| <div className="flex flex-wrap gap-2 w-full sm:w-auto justify-end"> | |
| <a | |
| href={`https://www.mercadopublico.cl/Portal/BuscarLicitacion?texto=${tender?.code}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest group" | |
| > | |
| <span>Visit Official Site</span> | |
| <span className="group-hover:translate-x-1 transition-transform">🔗</span> | |
| </a> | |
| <button | |
| onClick={() => window.print()} | |
| className="flex-1 sm:flex-none px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest" | |
| > | |
| Export PDF | |
| </button> | |
| <button | |
| onClick={generateAnnexes} | |
| disabled={isGeneratingAnnexes} | |
| className={`flex-1 sm:flex-none px-4 py-2 rounded-xl border text-[9px] font-bold transition uppercase tracking-widest ${isGeneratingAnnexes ? 'bg-purple-500/20 border-purple-500/50 text-purple-300 animate-pulse' : 'bg-purple-500/10 border-purple-500/20 text-purple-400 hover:bg-purple-500/20'}`} | |
| > | |
| {isGeneratingAnnexes ? 'Generating...' : '✨ Anexos'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="prose prose-invert max-w-none"> | |
| <p className="text-slate-300 text-lg sm:text-xl leading-relaxed italic border-l-4 border-purple-500 pl-4 sm:pl-8">{activeAnalysis.executive_summary}</p> | |
| </div> | |
| {/* Requirement Q&A Section */} | |
| {activeAnalysis.requirement_responses && activeAnalysis.requirement_responses.length > 0 && ( | |
| <div className="mt-12 space-y-6"> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-2xl">📋</span> | |
| <h4 className="text-[10px] sm:text-[11px] font-bold uppercase tracking-widest text-purple-400"> | |
| {activeAnalysis.requirement_responses.length > 3 ? `Actual Market Questions (${activeAnalysis.requirement_responses.length})` : "Intelligence Requirement Response"} | |
| </h4> | |
| </div> | |
| {activeAnalysis.requirement_responses.length > 3 && ( | |
| <div className="flex items-center gap-2 px-3 py-1 rounded-full bg-green-500/10 border border-green-500/20"> | |
| <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" /> | |
| <span className="text-[9px] font-bold text-green-400 uppercase tracking-tighter">Synced with Portal</span> | |
| </div> | |
| )} | |
| <div className="grid gap-4 max-h-[600px] overflow-y-auto pr-4 custom-scrollbar"> | |
| {activeAnalysis.requirement_responses.map((item, i) => ( | |
| <div key={i} className="rounded-2xl bg-white/[0.03] border border-white/5 p-4 sm:p-6 hover:border-purple-500/30 transition-all group"> | |
| <div className="flex gap-4"> | |
| <span className="text-purple-500 font-bold font-mono">Q.</span> | |
| <p className="text-white font-semibold text-xs sm:text-sm">{item.question}</p> | |
| </div> | |
| <div className="mt-4 flex gap-4 pl-4 sm:pl-8 border-l border-white/10"> | |
| <span className="text-green-400 font-bold font-mono">A.</span> | |
| <p className="text-slate-400 text-xs sm:text-sm leading-relaxed">{item.answer}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="grid gap-6 md:grid-cols-2"> | |
| <div className="glass-card rounded-3xl p-6 sm: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> Compliance Gaps | |
| </h4> | |
| <ul className="space-y-4"> | |
| {activeAnalysis.compliance_gaps.map((gap, i) => ( | |
| <li key={i} className="flex gap-4 text-xs sm: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-6 sm: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> Tech Requirements | |
| </h4> | |
| <ul className="space-y-4"> | |
| {activeAnalysis.key_requirements.map((req, i) => ( | |
| <li key={i} className="flex gap-4 text-xs sm:text-sm text-slate-400 leading-relaxed"> | |
| <span className="text-cyan font-bold">▹</span> {req} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Audit Log / Agent Thoughts Sticky Column */} | |
| <div className="lg:col-span-4 space-y-8"> | |
| <div className="glass-card rounded-3xl p-6 sm:p-8 bg-black/40 lg:sticky lg:top-32 max-h-[500px] lg:max-h-[700px] overflow-hidden flex flex-col"> | |
| <div className="flex items-center gap-3 mb-6 border-b border-white/5 pb-4"> | |
| <div className="h-2 w-2 rounded-full bg-purple-500 animate-pulse" /> | |
| <h4 className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Agent Intel Log</h4> | |
| </div> | |
| <div className="space-y-6 overflow-y-auto pr-2 custom-scrollbar"> | |
| {activeAnalysis.audit_log?.map((log, i) => ( | |
| <div key={i} className="flex gap-4 group"> | |
| <div className="flex flex-col items-center"> | |
| <div className="h-6 w-6 rounded-lg bg-white/5 flex items-center justify-center text-xs border border-white/10 group-hover:border-purple-500/50 transition-all">🤖</div> | |
| {i < (activeAnalysis.audit_log?.length ?? 0) - 1 && <div className="w-px flex-1 bg-white/5 my-2" />} | |
| </div> | |
| <p className="text-xs text-slate-500 leading-relaxed group-hover:text-slate-300 transition-colors">{log}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Compliance Anexos Section (Moved to prevent overlap with Chat) */} | |
| {generatedAnnexes.length > 0 && ( | |
| <div id="annexes-section" className="glass-card rounded-3xl p-6 sm:p-10 bg-purple-500/[0.03] border border-purple-500/20 animate-in fade-in slide-in-from-bottom-8 duration-700"> | |
| <div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-8"> | |
| <div className="w-12 h-12 rounded-2xl bg-purple-500/20 flex items-center justify-center text-2xl shadow-lg shadow-purple-500/20">📄</div> | |
| <div> | |
| <h4 className="text-xl sm:text-2xl font-black text-white tracking-tight">Compliance: Anexos Express</h4> | |
| <p className="text-slate-500 text-xs sm:text-sm">Official annexes pre-filled with company data.</p> | |
| </div> | |
| </div> | |
| <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> | |
| {generatedAnnexes.map((annex, i) => ( | |
| <div key={i} className="group rounded-3xl bg-white/[0.02] border border-white/5 p-6 hover:border-purple-500/40 transition-all"> | |
| <div className="text-[10px] font-bold uppercase text-purple-400 mb-3 tracking-widest">Template</div> | |
| <h5 className="text-white font-bold mb-4 line-clamp-1 text-sm">{annex.name}</h5> | |
| <div className="bg-black/40 rounded-xl p-4 text-[9px] font-mono text-slate-500 mb-4 h-32 overflow-hidden relative"> | |
| <pre className="whitespace-pre-wrap">{annex.content}</pre> | |
| <div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/60 to-transparent" /> | |
| </div> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <button | |
| onClick={() => downloadAsPDF(annex)} | |
| className="w-full py-2.5 rounded-xl bg-purple-500/10 border border-purple-500/20 text-[9px] font-bold text-purple-400 hover:bg-purple-500 hover:text-white transition uppercase tracking-widest shadow-lg shadow-purple-500/10" | |
| > | |
| Download PDF 📥 | |
| </button> | |
| <button | |
| onClick={() => { | |
| const blob = new Blob([annex.content], { type: 'text/markdown' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${annex.name.replace(/ /g, '_')}.md`; | |
| a.click(); | |
| }} | |
| className="w-full py-2.5 rounded-xl bg-white/5 border border-white/10 text-[9px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest" | |
| > | |
| Download .md 📥 | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Expert Consultation Chat (Bottom Section) */} | |
| {tender && ( | |
| <div className="mt-12 animate-in fade-in slide-in-from-bottom-8 duration-700"> | |
| <div className="flex flex-col sm:flex-row sm:items-center gap-4 mb-6 px-2"> | |
| <div className="w-10 h-10 rounded-2xl bg-purple-500/10 flex items-center justify-center text-xl shadow-lg shadow-purple-500/10">💬</div> | |
| <div> | |
| <h3 className="text-xl sm:text-2xl font-black text-white tracking-tight">Expert Agent Consultation</h3> | |
| <p className="text-slate-500 text-xs sm:text-sm">Deep-dive into specific questions with our AI agents.</p> | |
| </div> | |
| </div> | |
| <AgentChat tender={tender} companyProfile={companyProfile} /> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |