Álvaro Valenzuela Valdes commited on
Commit
dc78036
·
1 Parent(s): b7ad7a5

Implement Agent Chat feature with expert selection and real-time context

Browse files
backend/app/routers/analysis.py CHANGED
@@ -3,8 +3,9 @@ from typing import List
3
 
4
  from fastapi import APIRouter
5
 
6
- from app.schemas.analysis import AnalysisRecord, AnalysisRequest, AnalysisResult
7
  from app.services.agents import run_full_analysis
 
8
  from app.services.persistence import save_to_json, load_from_json
9
 
10
  router = APIRouter()
@@ -35,3 +36,21 @@ async def analyze_opportunity(request: AnalysisRequest):
35
  @router.get("/analysis-history", response_model=List[AnalysisRecord])
36
  def get_analysis_history():
37
  return analysis_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  from fastapi import APIRouter
5
 
6
+ from app.schemas.analysis import AnalysisRecord, AnalysisRequest, AnalysisResult, ChatRequest
7
  from app.services.agents import run_full_analysis
8
+ from app.services.llm import call_gemini_with_model
9
  from app.services.persistence import save_to_json, load_from_json
10
 
11
  router = APIRouter()
 
36
  @router.get("/analysis-history", response_model=List[AnalysisRecord])
37
  def get_analysis_history():
38
  return analysis_history
39
+
40
+
41
+ @router.post("/chat")
42
+ async def agent_chat(request: ChatRequest):
43
+ # Construct context
44
+ history_str = "\n".join([f"{m.role.upper()}: {m.content}" for m in request.history])
45
+
46
+ prompt = (
47
+ f"Eres {request.agent} en AndesOps AI.\n"
48
+ f"CONTEXTO DE LA LICITACIÓN:\n{request.tender.model_dump_json()}\n\n"
49
+ f"DATOS DE MI EMPRESA:\n{request.company_profile.model_dump_json()}\n\n"
50
+ f"HISTORIAL DE CHAT:\n{history_str}\n\n"
51
+ f"PREGUNTA DEL USUARIO: {request.message}\n\n"
52
+ f"INSTRUCCIONES: Responde como el experto {request.agent}. Sé directo, profesional y usa los datos de la empresa para dar respuestas personalizadas. Si te preguntan sobre requisitos, búscalos en la licitación."
53
+ )
54
+
55
+ response = await call_gemini_with_model(prompt, request.model)
56
+ return {"response": response}
backend/app/schemas/analysis.py CHANGED
@@ -6,6 +6,20 @@ from app.schemas.company import CompanyProfile
6
  from app.schemas.tender import Tender
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  class RiskItem(BaseModel):
10
  title: str
11
  severity: str
 
6
  from app.schemas.tender import Tender
7
 
8
 
9
+ class ChatMessage(BaseModel):
10
+ role: str
11
+ content: str
12
+
13
+
14
+ class ChatRequest(BaseModel):
15
+ tender: Tender
16
+ company_profile: CompanyProfile
17
+ message: str
18
+ agent: str
19
+ model: str
20
+ history: List[ChatMessage]
21
+
22
+
23
  class RiskItem(BaseModel):
24
  title: str
25
  severity: str
frontend/app/page.tsx CHANGED
@@ -260,6 +260,7 @@ export default function HomePage() {
260
  forceShowFollowed={activeTab === "My Portfolio"}
261
  initialKeyword={searchKeyword}
262
  lang={lang}
 
263
  />
264
  )}
265
  {activeTab === "Market Monitor" && <MarketMonitor />}
 
260
  forceShowFollowed={activeTab === "My Portfolio"}
261
  initialKeyword={searchKeyword}
262
  lang={lang}
263
+ companyProfile={companyProfile}
264
  />
265
  )}
266
  {activeTab === "Market Monitor" && <MarketMonitor />}
frontend/components/AgentChat.tsx ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import type { Tender, CompanyProfile } from "../lib/types";
5
+
6
+ type Message = {
7
+ role: "user" | "assistant";
8
+ content: string;
9
+ agent?: string;
10
+ };
11
+
12
+ type Props = {
13
+ tender: Tender;
14
+ companyProfile: CompanyProfile;
15
+ };
16
+
17
+ const agents = [
18
+ { id: "legal", name: "Dra. Legal", avatar: "⚖️", color: "text-amber-400" },
19
+ { id: "tech", name: "Ing. Tech", avatar: "👨‍💻", color: "text-cyan" },
20
+ { id: "risk", name: "Sra. Estrategia", avatar: "🕵️‍♀️", color: "text-purple-400" },
21
+ ];
22
+
23
+ const models = [
24
+ "Gemini 2.5 Flash",
25
+ "DeepSeek-V3.2 (Featherless)",
26
+ "Qwen-2.5 (Featherless)",
27
+ ];
28
+
29
+ export default function AgentChat({ tender, companyProfile }: Props) {
30
+ const [messages, setMessages] = useState<Message[]>([]);
31
+ const [input, setInput] = useState("");
32
+ const [selectedAgent, setSelectedAgent] = useState(agents[0]);
33
+ const [selectedModel, setSelectedModel] = useState(models[0]);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const scrollRef = useRef<HTMLDivElement>(null);
36
+
37
+ useEffect(() => {
38
+ if (scrollRef.current) {
39
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
40
+ }
41
+ }, [messages]);
42
+
43
+ const handleSend = async () => {
44
+ if (!input.trim() || isLoading) return;
45
+
46
+ const userMsg: Message = { role: "user", content: input };
47
+ setMessages(prev => [...prev, userMsg]);
48
+ setInput("");
49
+ setIsLoading(true);
50
+
51
+ try {
52
+ const response = await fetch("/api/chat", {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({
56
+ tender,
57
+ company_profile: companyProfile,
58
+ message: input,
59
+ agent: selectedAgent.id,
60
+ model: selectedModel,
61
+ history: messages,
62
+ }),
63
+ });
64
+
65
+ if (!response.ok) throw new Error("Failed to chat");
66
+
67
+ const data = await response.json();
68
+ const assistantMsg: Message = {
69
+ role: "assistant",
70
+ content: data.response,
71
+ agent: selectedAgent.name
72
+ };
73
+ setMessages(prev => [...prev, assistantMsg]);
74
+ } catch (error) {
75
+ console.error(error);
76
+ setMessages(prev => [...prev, { role: "assistant", content: "⚠️ Error connecting to the agent. Please try again." }]);
77
+ } finally {
78
+ setIsLoading(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <div className="flex flex-col h-[600px] glass-card rounded-[2rem] overflow-hidden border border-white/10 bg-slate-900/60 backdrop-blur-xl">
84
+ {/* Chat Header */}
85
+ <div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/5">
86
+ <div className="flex items-center gap-4">
87
+ <div className="text-3xl">{selectedAgent.avatar}</div>
88
+ <div>
89
+ <h4 className="text-white font-bold">{selectedAgent.name}</h4>
90
+ <p className="text-[10px] text-slate-500 uppercase tracking-widest font-black">Expert Consultant</p>
91
+ </div>
92
+ </div>
93
+ <div className="flex items-center gap-3">
94
+ <select
95
+ value={selectedAgent.id}
96
+ onChange={(e) => setSelectedAgent(agents.find(a => a.id === e.target.value) || agents[0])}
97
+ className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:ring-1 focus:ring-purple-500"
98
+ >
99
+ {agents.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
100
+ </select>
101
+ <select
102
+ value={selectedModel}
103
+ onChange={(e) => setSelectedModel(e.target.value)}
104
+ className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:ring-1 focus:ring-purple-500"
105
+ >
106
+ {models.map(m => <option key={m} value={m}>{m}</option>)}
107
+ </select>
108
+ </div>
109
+ </div>
110
+
111
+ {/* Messages Area */}
112
+ <div
113
+ ref={scrollRef}
114
+ className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar bg-black/20"
115
+ >
116
+ {messages.length === 0 && (
117
+ <div className="h-full flex flex-col items-center justify-center text-center space-y-4 opacity-40">
118
+ <div className="text-5xl">💬</div>
119
+ <p className="text-slate-400 text-sm max-w-xs">
120
+ Hi! I'm your {selectedAgent.name}. Ask me anything about this tender's requirements, risks, or strategy.
121
+ </p>
122
+ </div>
123
+ )}
124
+ {messages.map((msg, i) => (
125
+ <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
126
+ <div className={`max-w-[80%] rounded-2xl px-5 py-3 text-sm ${
127
+ msg.role === 'user'
128
+ ? 'bg-purple-600 text-white rounded-tr-none'
129
+ : 'bg-white/10 text-slate-200 border border-white/10 rounded-tl-none'
130
+ }`}>
131
+ {msg.role === 'assistant' && (
132
+ <div className="text-[10px] font-black uppercase text-purple-400 mb-1 tracking-widest">
133
+ {msg.agent}
134
+ </div>
135
+ )}
136
+ <p className="leading-relaxed whitespace-pre-wrap">{msg.content}</p>
137
+ </div>
138
+ </div>
139
+ ))}
140
+ {isLoading && (
141
+ <div className="flex justify-start">
142
+ <div className="bg-white/5 rounded-2xl rounded-tl-none px-5 py-3 border border-white/10">
143
+ <div className="flex gap-1">
144
+ <div className="w-1.5 h-1.5 bg-slate-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
145
+ <div className="w-1.5 h-1.5 bg-slate-500 rounded-full animate-bounce" style={{ animationDelay: '200ms' }} />
146
+ <div className="w-1.5 h-1.5 bg-slate-500 rounded-full animate-bounce" style={{ animationDelay: '400ms' }} />
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>
152
+
153
+ {/* Input Area */}
154
+ <div className="p-6 bg-white/5 border-t border-white/5">
155
+ <div className="flex gap-3">
156
+ <input
157
+ type="text"
158
+ value={input}
159
+ onChange={(e) => setInput(e.target.value)}
160
+ onKeyDown={(e) => e.key === 'Enter' && handleSend()}
161
+ placeholder={`Message ${selectedAgent.name}...`}
162
+ className="flex-1 bg-black/40 border border-white/10 rounded-2xl px-5 py-3 text-white text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all"
163
+ />
164
+ <button
165
+ onClick={handleSend}
166
+ disabled={!input.trim() || isLoading}
167
+ className="w-12 h-12 rounded-2xl premium-gradient text-white flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 shadow-lg shadow-purple-500/20"
168
+ >
169
+ <span className="text-xl">✈️</span>
170
+ </button>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ );
175
+ }
frontend/components/TenderSearch.tsx CHANGED
@@ -4,6 +4,8 @@ import { Fragment, useMemo, useState, useRef, useEffect } from "react";
4
  import BrandLoader from "./BrandLoader";
5
  import type { Tender } from "../lib/types";
6
  import { Language, translations } from "../lib/translations";
 
 
7
 
8
  type Props = {
9
  tenders: Tender[];
@@ -12,9 +14,10 @@ type Props = {
12
  forceShowFollowed?: boolean;
13
  initialKeyword?: string;
14
  lang: Language;
 
15
  };
16
 
17
- export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "", lang }: Props) {
18
  const t = translations[lang];
19
  const [keyword, setKeyword] = useState(initialKeyword);
20
  const [buyerCode, setBuyerCode] = useState("");
@@ -25,6 +28,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
25
  const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
26
  const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
27
  const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
 
28
 
29
  const [followedTenders, setFollowedTenders] = useState<Tender[]>(() => {
30
  if (typeof window !== 'undefined') {
@@ -432,16 +436,33 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
432
  /* Immersive Detail View (Replaces List) */
433
  <div className="animate-in slide-in-from-right-8 fade-in duration-700 w-full max-w-[1600px] mx-auto pt-4 pb-20">
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
- <div className="flex flex-col gap-6 mb-8">
437
- <button
438
- onClick={() => setSelectedTenderForModal(null)}
439
- className="flex items-center gap-2 text-slate-400 hover:text-white transition group w-fit"
440
- >
441
- <span className="text-xl group-hover:-translate-x-1 transition-transform">←</span>
442
- <span className="text-sm font-bold uppercase tracking-widest">{lang === 'en' ? 'Back to search' : 'Volver a búsqueda'}</span>
443
- </button>
444
- <div className="grid grid-cols-2 gap-4">
445
  <div className="px-4 py-3 rounded-lg bg-white/5 border border-white/10">
446
  <div className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 mb-2">Search</div>
447
  <p className="text-xs text-slate-300">View detailed information</p>
@@ -462,202 +483,206 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
462
  </div>
463
  </div>
464
 
465
- <div className="glass-card rounded-[2.5rem] overflow-hidden border border-white/5 bg-slate-900/40 backdrop-blur-xl shadow-2xl">
466
- {/* Header Section */}
467
- <div className="p-10 md:p-14 border-b border-white/5 relative overflow-hidden">
468
- <div className="absolute top-0 right-0 w-64 h-64 bg-purple-500/10 blur-[100px] -translate-y-1/2 translate-x-1/2" />
469
- <div className="relative z-10">
470
- <div className="flex items-center gap-3 mb-6">
471
- <span className="text-sm font-mono text-purple-400 bg-purple-400/10 px-3 py-1 rounded-lg border border-purple-400/20">{selectedTenderForModal.code}</span>
472
- <span className={`px-3 py-1 rounded-lg text-xs font-black uppercase tracking-widest ${
473
- selectedTenderForModal.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800 text-slate-500'
474
- }`}>
475
- {selectedTenderForModal.status}
476
- </span>
477
- </div>
478
- <h3 className="text-3xl md:text-4xl font-black text-white leading-tight tracking-tight mb-4">{selectedTenderForModal.name}</h3>
479
-
480
- <div className="flex flex-wrap items-center gap-x-8 gap-y-4">
481
- <div className="flex items-center gap-3">
482
- <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">🏢</div>
483
- <span className="text-slate-400 font-medium">{selectedTenderForModal.buyer}</span>
484
- </div>
485
- <div className="flex items-center gap-3">
486
- <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">📍</div>
487
- <span className="text-slate-400 font-medium">{selectedTenderForModal.buyer_region || selectedTenderForModal.region || "Nacional"}</span>
488
  </div>
489
- <div className="flex items-center gap-3">
490
- <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">🏷️</div>
491
- <span className="text-slate-400 font-medium">Type: {selectedTenderForModal.type || "N/A"}</span>
 
 
 
 
 
 
 
 
 
 
 
 
492
  </div>
493
  </div>
494
  </div>
495
- </div>
496
 
497
- {/* Content Section */}
498
- <div className="p-10 md:p-14">
499
- <div className="grid gap-16 lg:grid-cols-3">
500
- {/* Main Column */}
501
- <div className="lg:col-span-2 space-y-12">
502
- <section>
503
- <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-3">
504
- <span className="w-8 h-[1px] bg-slate-700" />
505
- Project Scope & Description
506
- </h4>
507
- <div className="text-slate-300 leading-relaxed text-lg bg-white/[0.02] p-8 rounded-[2rem] border border-white/5 whitespace-pre-wrap font-light mb-12">
508
- {selectedTenderForModal.description || "No detailed description provided."}
509
- </div>
510
 
511
- {selectedTenderForModal.items && selectedTenderForModal.items.length > 0 && (
512
- <section>
513
- <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-3">
514
- <span className="w-8 h-[1px] bg-slate-700" />
515
- Line Items & Requirements
516
- </h4>
517
- <div className="overflow-hidden rounded-3xl border border-white/5 bg-white/[0.01]">
518
- <table className="w-full text-left text-xs">
519
- <thead className="bg-white/5 text-slate-500 uppercase font-black tracking-tighter">
520
- <tr>
521
- <th className="px-6 py-4">Item / Product</th>
522
- <th className="px-6 py-4">Category</th>
523
- <th className="px-6 py-4 text-right">Quantity</th>
524
- </tr>
525
- </thead>
526
- <tbody className="divide-y divide-white/5">
527
- {selectedTenderForModal.items.map((item, idx) => (
528
- <tr key={idx} className="hover:bg-white/[0.02]">
529
- <td className="px-6 py-4">
530
- <div className="text-slate-200 font-bold">{item.name}</div>
531
- <div className="text-[10px] text-slate-500 mt-1">{item.description}</div>
532
- </td>
533
- <td className="px-6 py-4 text-slate-400">{item.category || "N/A"}</td>
534
- <td className="px-6 py-4 text-right">
535
- <span className="text-cyan font-mono font-bold">{item.quantity}</span>
536
- <span className="ml-1 text-slate-500 uppercase text-[10px]">{item.unit}</span>
537
- </td>
538
  </tr>
539
- ))}
540
- </tbody>
541
- </table>
542
- </div>
543
- </section>
544
- )}
545
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
- <div className="grid grid-cols-2 gap-6">
548
- <div className="p-6 rounded-3xl bg-white/[0.03] border border-white/5 group hover:bg-white/[0.05] transition-colors">
549
- <div className="text-[10px] uppercase text-slate-500 font-black mb-2 tracking-widest">Estimated Investment</div>
550
- <div className="text-xl text-white font-bold tracking-tight">
551
- {selectedTenderForModal.estimated_amount
552
- ? new Intl.NumberFormat("es-CL", { style: "currency", currency: selectedTenderForModal.currency || "CLP" }).format(selectedTenderForModal.estimated_amount)
553
- : "Not Disclosed"}
 
 
 
 
 
 
 
 
554
  </div>
555
- {selectedTenderForModal.currency && selectedTenderForModal.currency !== 'CLP' && (
556
- <div className="text-[10px] text-cyan mt-1 font-bold">Currency: {selectedTenderForModal.currency}</div>
557
- )}
558
- </div>
559
- <div className="p-6 rounded-3xl bg-white/[0.03] border border-white/5 group hover:bg-white/[0.05] transition-colors">
560
- <div className="text-[10px] uppercase text-slate-500 font-black mb-2 tracking-widest">Industry Classification</div>
561
- <div className="text-xl text-white font-bold tracking-tight">{selectedTenderForModal.sector || "General Procurement"}</div>
562
  </div>
563
  </div>
564
- </div>
565
 
566
- {/* Sidebar Column */}
567
- <div className="space-y-12">
568
- <div className="p-8 rounded-[2rem] bg-purple-600/10 border border-purple-500/20 shadow-2xl shadow-purple-500/5 relative overflow-hidden group">
569
- <div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/20 blur-[60px] opacity-0 group-hover:opacity-100 transition-opacity" />
570
- <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-purple-400 mb-6">Timeline</h4>
571
-
572
- <div className="space-y-4">
573
- <div>
574
- <div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest mb-1">Closing Deadline</div>
575
- <div className="text-2xl font-black text-white font-mono">
576
- {selectedTenderForModal.closing_date ? new Date(selectedTenderForModal.closing_date).toLocaleDateString() : "---"}
577
- </div>
578
- </div>
579
 
580
- {selectedTenderForModal.publication_date && (
581
  <div>
582
- <div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest mb-1">Published On</div>
583
- <div className="text-sm font-bold text-slate-300">
584
- {new Date(selectedTenderForModal.publication_date).toLocaleDateString()}
585
  </div>
586
  </div>
587
- )}
 
 
 
 
 
 
 
 
 
 
 
588
  </div>
589
-
590
- <p className="mt-6 text-[10px] text-purple-400/60 font-bold uppercase tracking-tighter border-t border-purple-400/10 pt-4">Final Window for Submission</p>
591
- </div>
592
 
593
- <div>
594
- <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6">Official Documentation</h4>
595
- <div className="grid gap-3">
596
- {selectedTenderForModal.attachments?.map((att, i) => (
597
- <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-5 rounded-2xl bg-white/[0.03] hover:bg-white/[0.08] border border-white/5 transition-all group/file">
598
- <div className="flex items-center gap-4">
599
- <div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center text-2xl">
600
- {att.name.endsWith('.pdf') ? "📕" : "📘"}
601
- </div>
602
- <div className="flex flex-col">
603
- <span className="text-sm font-bold text-slate-200 group-hover/file:text-white transition-colors truncate max-w-[150px]">{att.name}</span>
604
- <span className="text-[9px] text-slate-600 uppercase font-black">Official Basis</span>
 
605
  </div>
 
 
 
 
 
 
606
  </div>
607
- <span className="text-xl text-slate-600 group-hover/file:text-purple-400 transition-colors">↓</span>
608
- </a>
609
- ))}
610
- {!selectedTenderForModal.attachments?.length && (
611
- <div className="p-8 text-center rounded-2xl border border-white/5 bg-white/[0.01]">
612
- <p className="text-xs text-slate-600 italic font-medium">No external files registered.</p>
613
- </div>
614
- )}
615
  </div>
616
- </div>
617
 
618
- <a
619
- href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${selectedTenderForModal.code}`}
620
- target="_blank"
621
- className="flex items-center justify-center gap-3 w-full p-5 rounded-2xl bg-white/5 border border-white/10 text-white text-sm font-black uppercase tracking-widest hover:bg-white/10 transition-all shadow-xl active:scale-[0.98]"
622
- >
623
- Mercado Público Portal ↗
624
- </a>
 
625
  </div>
626
  </div>
627
- </div>
628
 
629
- {/* Action Footer */}
630
- <div className="p-10 md:p-14 bg-slate-950/40 border-t border-white/5 flex flex-col md:flex-row items-center justify-between gap-8">
631
- <div className="flex items-center gap-4">
632
- <div className="w-12 h-12 rounded-full bg-cyan/10 border border-cyan/20 flex items-center justify-center text-cyan">🤖</div>
633
- <div>
634
- <p className="text-white font-bold text-sm">Ready for Analysis</p>
635
- <p className="text-slate-500 text-xs">Run AI compliance check and opportunity fit assessment.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  </div>
637
- </div>
638
- <div className="flex items-center gap-4 w-full md:w-auto">
639
- <button
640
- onClick={() => toggleFollow(selectedTenderForModal)}
641
- className={`flex-1 md:flex-none px-6 py-3 text-xs font-bold uppercase tracking-widest rounded-lg border transition-all ${
642
- followedCodes.includes(selectedTenderForModal.code)
643
- ? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
644
- : 'border-white/10 text-slate-400 hover:border-purple-400/40 hover:text-purple-300'
645
- }`}
646
- >
647
- {followedCodes.includes(selectedTenderForModal.code) ? "★ In Portfolio" : "☆ Add to Portfolio"}
648
- </button>
649
- <button
650
- onClick={() => {
651
- onAnalyze(selectedTenderForModal);
652
- setSelectedTenderForModal(null);
653
- }}
654
- className="flex-1 md:flex-none premium-gradient text-white px-8 py-3 rounded-lg font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-purple-500/30 hover:scale-105 active:scale-95 transition-all"
655
- >
656
- {t.analyze}
657
- </button>
658
  </div>
659
  </div>
660
- </div>
 
 
661
  </div>
662
  )}
663
 
 
4
  import BrandLoader from "./BrandLoader";
5
  import type { Tender } from "../lib/types";
6
  import { Language, translations } from "../lib/translations";
7
+ import AgentChat from "./AgentChat";
8
+ import type { CompanyProfile } from "../lib/types";
9
 
10
  type Props = {
11
  tenders: Tender[];
 
14
  forceShowFollowed?: boolean;
15
  initialKeyword?: string;
16
  lang: Language;
17
+ companyProfile: CompanyProfile;
18
  };
19
 
20
+ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "", lang, companyProfile }: Props) {
21
  const t = translations[lang];
22
  const [keyword, setKeyword] = useState(initialKeyword);
23
  const [buyerCode, setBuyerCode] = useState("");
 
28
  const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
29
  const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
30
  const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
31
+ const [activeDetailTab, setActiveDetailTab] = useState<"Overview" | "Agent Chat">("Overview");
32
 
33
  const [followedTenders, setFollowedTenders] = useState<Tender[]>(() => {
34
  if (typeof window !== 'undefined') {
 
436
  /* Immersive Detail View (Replaces List) */
437
  <div className="animate-in slide-in-from-right-8 fade-in duration-700 w-full max-w-[1600px] mx-auto pt-4 pb-20">
438
 
439
+ <div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-8">
440
+ <div className="flex flex-col gap-4">
441
+ <button
442
+ onClick={() => setSelectedTenderForModal(null)}
443
+ className="flex items-center gap-2 text-slate-400 hover:text-white transition group w-fit"
444
+ >
445
+ <span className="text-xl group-hover:-translate-x-1 transition-transform">←</span>
446
+ <span className="text-sm font-bold uppercase tracking-widest">{lang === 'en' ? 'Back to search' : 'Volver a búsqueda'}</span>
447
+ </button>
448
+
449
+ <div className="flex bg-white/5 p-1 rounded-2xl border border-white/10 w-fit">
450
+ <button
451
+ onClick={() => setActiveDetailTab("Overview")}
452
+ className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeDetailTab === "Overview" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500 hover:text-slate-300"}`}
453
+ >
454
+ Overview
455
+ </button>
456
+ <button
457
+ onClick={() => setActiveDetailTab("Agent Chat")}
458
+ className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeDetailTab === "Agent Chat" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500 hover:text-slate-300"}`}
459
+ >
460
+ Agent Chat
461
+ </button>
462
+ </div>
463
+ </div>
464
 
465
+ <div className="grid grid-cols-2 gap-4 w-full md:w-auto">
 
 
 
 
 
 
 
 
466
  <div className="px-4 py-3 rounded-lg bg-white/5 border border-white/10">
467
  <div className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 mb-2">Search</div>
468
  <p className="text-xs text-slate-300">View detailed information</p>
 
483
  </div>
484
  </div>
485
 
486
+ {activeDetailTab === "Overview" ? (
487
+ <div className="glass-card rounded-[2.5rem] overflow-hidden border border-white/5 bg-slate-900/40 backdrop-blur-xl shadow-2xl">
488
+ {/* Header Section */}
489
+ <div className="p-10 md:p-14 border-b border-white/5 relative overflow-hidden">
490
+ <div className="absolute top-0 right-0 w-64 h-64 bg-purple-500/10 blur-[100px] -translate-y-1/2 translate-x-1/2" />
491
+ <div className="relative z-10">
492
+ <div className="flex items-center gap-3 mb-6">
493
+ <span className="text-sm font-mono text-purple-400 bg-purple-400/10 px-3 py-1 rounded-lg border border-purple-400/20">{selectedTenderForModal.code}</span>
494
+ <span className={`px-3 py-1 rounded-lg text-xs font-black uppercase tracking-widest ${
495
+ selectedTenderForModal.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800 text-slate-500'
496
+ }`}>
497
+ {selectedTenderForModal.status}
498
+ </span>
 
 
 
 
 
 
 
 
 
 
499
  </div>
500
+ <h3 className="text-3xl md:text-4xl font-black text-white leading-tight tracking-tight mb-4">{selectedTenderForModal.name}</h3>
501
+
502
+ <div className="flex flex-wrap items-center gap-x-8 gap-y-4">
503
+ <div className="flex items-center gap-3">
504
+ <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">🏢</div>
505
+ <span className="text-slate-400 font-medium">{selectedTenderForModal.buyer}</span>
506
+ </div>
507
+ <div className="flex items-center gap-3">
508
+ <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">📍</div>
509
+ <span className="text-slate-400 font-medium">{selectedTenderForModal.buyer_region || selectedTenderForModal.region || "Nacional"}</span>
510
+ </div>
511
+ <div className="flex items-center gap-3">
512
+ <div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-lg">🏷️</div>
513
+ <span className="text-slate-400 font-medium">Type: {selectedTenderForModal.type || "N/A"}</span>
514
+ </div>
515
  </div>
516
  </div>
517
  </div>
 
518
 
519
+ {/* Content Section */}
520
+ <div className="p-10 md:p-14">
521
+ <div className="grid gap-16 lg:grid-cols-3">
522
+ {/* Main Column */}
523
+ <div className="lg:col-span-2 space-y-12">
524
+ <section>
525
+ <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-3">
526
+ <span className="w-8 h-[1px] bg-slate-700" />
527
+ Project Scope & Description
528
+ </h4>
529
+ <div className="text-slate-300 leading-relaxed text-lg bg-white/[0.02] p-8 rounded-[2rem] border border-white/5 whitespace-pre-wrap font-light mb-12">
530
+ {selectedTenderForModal.description || "No detailed description provided."}
531
+ </div>
532
 
533
+ {selectedTenderForModal.items && selectedTenderForModal.items.length > 0 && (
534
+ <section>
535
+ <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6 flex items-center gap-3">
536
+ <span className="w-8 h-[1px] bg-slate-700" />
537
+ Line Items & Requirements
538
+ </h4>
539
+ <div className="overflow-hidden rounded-3xl border border-white/5 bg-white/[0.01]">
540
+ <table className="w-full text-left text-xs">
541
+ <thead className="bg-white/5 text-slate-500 uppercase font-black tracking-tighter">
542
+ <tr>
543
+ <th className="px-6 py-4">Item / Product</th>
544
+ <th className="px-6 py-4">Category</th>
545
+ <th className="px-6 py-4 text-right">Quantity</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  </tr>
547
+ </thead>
548
+ <tbody className="divide-y divide-white/5">
549
+ {selectedTenderForModal.items.map((item, idx) => (
550
+ <tr key={idx} className="hover:bg-white/[0.02]">
551
+ <td className="px-6 py-4">
552
+ <div className="text-slate-200 font-bold">{item.name}</div>
553
+ <div className="text-[10px] text-slate-500 mt-1">{item.description}</div>
554
+ </td>
555
+ <td className="px-6 py-4 text-slate-400">{item.category || "N/A"}</td>
556
+ <td className="px-6 py-4 text-right">
557
+ <span className="text-cyan font-mono font-bold">{item.quantity}</span>
558
+ <span className="ml-1 text-slate-500 uppercase text-[10px]">{item.unit}</span>
559
+ </td>
560
+ </tr>
561
+ ))}
562
+ </tbody>
563
+ </table>
564
+ </div>
565
+ </section>
566
+ )}
567
+ </section>
568
 
569
+ <div className="grid grid-cols-2 gap-6">
570
+ <div className="p-6 rounded-3xl bg-white/[0.03] border border-white/5 group hover:bg-white/[0.05] transition-colors">
571
+ <div className="text-[10px] uppercase text-slate-500 font-black mb-2 tracking-widest">Estimated Investment</div>
572
+ <div className="text-xl text-white font-bold tracking-tight">
573
+ {selectedTenderForModal.estimated_amount
574
+ ? new Intl.NumberFormat("es-CL", { style: "currency", currency: selectedTenderForModal.currency || "CLP" }).format(selectedTenderForModal.estimated_amount)
575
+ : "Not Disclosed"}
576
+ </div>
577
+ {selectedTenderForModal.currency && selectedTenderForModal.currency !== 'CLP' && (
578
+ <div className="text-[10px] text-cyan mt-1 font-bold">Currency: {selectedTenderForModal.currency}</div>
579
+ )}
580
+ </div>
581
+ <div className="p-6 rounded-3xl bg-white/[0.03] border border-white/5 group hover:bg-white/[0.05] transition-colors">
582
+ <div className="text-[10px] uppercase text-slate-500 font-black mb-2 tracking-widest">Industry Classification</div>
583
+ <div className="text-xl text-white font-bold tracking-tight">{selectedTenderForModal.sector || "General Procurement"}</div>
584
  </div>
 
 
 
 
 
 
 
585
  </div>
586
  </div>
 
587
 
588
+ {/* Sidebar Column */}
589
+ <div className="space-y-12">
590
+ <div className="p-8 rounded-[2rem] bg-purple-600/10 border border-purple-500/20 shadow-2xl shadow-purple-500/5 relative overflow-hidden group">
591
+ <div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/20 blur-[60px] opacity-0 group-hover:opacity-100 transition-opacity" />
592
+ <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-purple-400 mb-6">Timeline</h4>
 
 
 
 
 
 
 
 
593
 
594
+ <div className="space-y-4">
595
  <div>
596
+ <div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest mb-1">Closing Deadline</div>
597
+ <div className="text-2xl font-black text-white font-mono">
598
+ {selectedTenderForModal.closing_date ? new Date(selectedTenderForModal.closing_date).toLocaleDateString() : "---"}
599
  </div>
600
  </div>
601
+
602
+ {selectedTenderForModal.publication_date && (
603
+ <div>
604
+ <div className="text-[10px] text-slate-500 font-bold uppercase tracking-widest mb-1">Published On</div>
605
+ <div className="text-sm font-bold text-slate-300">
606
+ {new Date(selectedTenderForModal.publication_date).toLocaleDateString()}
607
+ </div>
608
+ </div>
609
+ )}
610
+ </div>
611
+
612
+ <p className="mt-6 text-[10px] text-purple-400/60 font-bold uppercase tracking-tighter border-t border-purple-400/10 pt-4">Final Window for Submission</p>
613
  </div>
 
 
 
614
 
615
+ <div>
616
+ <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-6">Official Documentation</h4>
617
+ <div className="grid gap-3">
618
+ {selectedTenderForModal.attachments?.map((att, i) => (
619
+ <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-5 rounded-2xl bg-white/[0.03] hover:bg-white/[0.08] border border-white/5 transition-all group/file">
620
+ <div className="flex items-center gap-4">
621
+ <div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center text-2xl">
622
+ {att.name.endsWith('.pdf') ? "📕" : "📘"}
623
+ </div>
624
+ <div className="flex flex-col">
625
+ <span className="text-sm font-bold text-slate-200 group-hover/file:text-white transition-colors truncate max-w-[150px]">{att.name}</span>
626
+ <span className="text-[9px] text-slate-600 uppercase font-black">Official Basis</span>
627
+ </div>
628
  </div>
629
+ <span className="text-xl text-slate-600 group-hover/file:text-purple-400 transition-colors">↓</span>
630
+ </a>
631
+ ))}
632
+ {!selectedTenderForModal.attachments?.length && (
633
+ <div className="p-8 text-center rounded-2xl border border-white/5 bg-white/[0.01]">
634
+ <p className="text-xs text-slate-600 italic font-medium">No external files registered.</p>
635
  </div>
636
+ )}
637
+ </div>
 
 
 
 
 
 
638
  </div>
 
639
 
640
+ <a
641
+ href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${selectedTenderForModal.code}`}
642
+ target="_blank"
643
+ className="flex items-center justify-center gap-3 w-full p-5 rounded-2xl bg-white/5 border border-white/10 text-white text-sm font-black uppercase tracking-widest hover:bg-white/10 transition-all shadow-xl active:scale-[0.98]"
644
+ >
645
+ Mercado Público Portal ↗
646
+ </a>
647
+ </div>
648
  </div>
649
  </div>
 
650
 
651
+ {/* Action Footer */}
652
+ <div className="p-10 md:p-14 bg-slate-950/40 border-t border-white/5 flex flex-col md:flex-row items-center justify-between gap-8">
653
+ <div className="flex items-center gap-4">
654
+ <div className="w-12 h-12 rounded-full bg-cyan/10 border border-cyan/20 flex items-center justify-center text-cyan">🤖</div>
655
+ <div>
656
+ <p className="text-white font-bold text-sm">Ready for Analysis</p>
657
+ <p className="text-slate-500 text-xs">Run AI compliance check and opportunity fit assessment.</p>
658
+ </div>
659
+ </div>
660
+ <div className="flex items-center gap-4 w-full md:w-auto">
661
+ <button
662
+ onClick={() => toggleFollow(selectedTenderForModal)}
663
+ className={`flex-1 md:flex-none px-6 py-3 text-xs font-bold uppercase tracking-widest rounded-lg border transition-all ${
664
+ followedCodes.includes(selectedTenderForModal.code)
665
+ ? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
666
+ : 'border-white/10 text-slate-400 hover:border-purple-400/40 hover:text-purple-300'
667
+ }`}
668
+ >
669
+ {followedCodes.includes(selectedTenderForModal.code) ? "★ In Portfolio" : "☆ Add to Portfolio"}
670
+ </button>
671
+ <button
672
+ onClick={() => {
673
+ onAnalyze(selectedTenderForModal);
674
+ setSelectedTenderForModal(null);
675
+ }}
676
+ className="flex-1 md:flex-none premium-gradient text-white px-8 py-3 rounded-lg font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-purple-500/30 hover:scale-105 active:scale-95 transition-all"
677
+ >
678
+ {t.analyze}
679
+ </button>
680
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  </div>
682
  </div>
683
+ ) : (
684
+ <AgentChat tender={selectedTenderForModal} companyProfile={companyProfile} />
685
+ )}
686
  </div>
687
  )}
688