Álvaro Valenzuela Valdes commited on
Commit
77efd75
·
1 Parent(s): 99af502

style: Premium UI overhaul with glassmorphism and modern aesthetics

Browse files
frontend/app/page.tsx CHANGED
@@ -43,7 +43,6 @@ export default function HomePage() {
43
  const [status, setStatus] = useState("listening");
44
 
45
  useEffect(() => {
46
- // Initial tab from URL
47
  if (typeof window !== 'undefined') {
48
  const params = new URLSearchParams(window.location.search);
49
  const tabParam = params.get('tab');
@@ -86,18 +85,6 @@ export default function HomePage() {
86
  init();
87
  }, []);
88
 
89
- const recommendedCount = useMemo(
90
- () => (analysisResult?.decision === "Recommended" ? 1 : 0),
91
- [analysisResult]
92
- );
93
-
94
- const highRiskItems = useMemo(
95
- () => analysisResult?.risks.filter((item) => item.severity === "High").length ?? 0,
96
- [analysisResult]
97
- );
98
-
99
- const reportsGenerated = useMemo(() => (analysisResult ? 1 : 0), [analysisResult]);
100
-
101
  const handleTenderSelect = (tender: Tender) => {
102
  setSelectedTender(tender);
103
  setActiveTab("Agent Analysis");
@@ -132,54 +119,85 @@ export default function HomePage() {
132
  };
133
 
134
  return (
135
- <div className="min-h-screen bg-navy text-slate-100">
136
- <div className="mx-auto flex min-h-screen max-w-[1440px] gap-6 px-6 py-6">
137
  <Sidebar
138
  tabs={tabs}
139
  activeTab={activeTab}
140
  onTabSelect={setActiveTab}
141
  status={status}
142
  />
143
- <main className="flex-1 rounded-3xl border border-slate-800 bg-surface p-6 shadow-2xl shadow-slate-900/20">
144
- {activeTab === "Dashboard" && (
145
- <Dashboard
146
- tendersFound={tenders.length}
147
- recommendedOpportunities={recommendedCount}
148
- highRiskItems={highRiskItems}
149
- reportsGenerated={reportsGenerated}
150
- tenders={tenders}
151
- />
152
- )}
153
- {(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
154
- <TenderSearch
155
- tenders={tenders}
156
- onSearch={handleSearch}
157
- onAnalyze={handleTenderSelect}
158
- forceShowFollowed={activeTab === "My Portfolio"}
159
- />
160
- )}
161
- {activeTab === "Company Profile" && (
162
- <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
163
- )}
164
- {activeTab === "Agent Analysis" && (
165
- <AgentAnalysis
166
- tender={selectedTender}
167
- companyProfile={companyProfile}
168
- analysis={analysisResult}
169
- onAnalyze={handleRunAnalysis}
170
- />
171
- )}
172
- {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
173
- {activeTab === "Reports" && <Reports reportMarkdown={analysisResult?.report_markdown ?? ""} />}
174
- {activeTab === "History" && <AnalysisHistory history={analysisHistory} />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </main>
176
  </div>
177
- <footer className="mt-8 border-t border-slate-800 bg-navy/50 py-12 text-center">
178
- <p className="text-xs font-bold uppercase tracking-[0.4em] text-slate-500 mb-2">
179
- Architecting the Agentic Future
 
180
  </p>
181
- <p className="text-sm font-medium text-slate-400">
182
- © 2026 <a href="https://rew.cl" target="_blank" className="text-cyan hover:text-sky transition-colors font-bold">REW 2026</a> | IA TECH PREMIUM
183
  </p>
184
  </footer>
185
  </div>
 
43
  const [status, setStatus] = useState("listening");
44
 
45
  useEffect(() => {
 
46
  if (typeof window !== 'undefined') {
47
  const params = new URLSearchParams(window.location.search);
48
  const tabParam = params.get('tab');
 
85
  init();
86
  }, []);
87
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  const handleTenderSelect = (tender: Tender) => {
89
  setSelectedTender(tender);
90
  setActiveTab("Agent Analysis");
 
119
  };
120
 
121
  return (
122
+ <div className="min-h-screen">
123
+ <div className="mx-auto flex min-h-screen max-w-[1500px] gap-8 px-8 py-6">
124
  <Sidebar
125
  tabs={tabs}
126
  activeTab={activeTab}
127
  onTabSelect={setActiveTab}
128
  status={status}
129
  />
130
+
131
+ <main className="flex-1 flex flex-col gap-6 overflow-hidden">
132
+ {/* Dashboard Header */}
133
+ <header className="flex items-center justify-between px-2">
134
+ <div>
135
+ <h2 className="text-3xl font-bold text-white mb-1">{activeTab}</h2>
136
+ <p className="text-slate-500 text-sm">
137
+ {activeTab === "Dashboard" && "Overview of your tender ecosystem."}
138
+ {activeTab === "Tender Search" && "Explore new opportunities from the market."}
139
+ {activeTab === "Agent Analysis" && "Deep-dive into tender documentation."}
140
+ {activeTab === "My Portfolio" && "Manage your followed opportunities."}
141
+ </p>
142
+ </div>
143
+ <div className="flex items-center gap-4">
144
+ <div className="flex -space-x-3">
145
+ {[1, 2, 3].map(i => (
146
+ <div key={i} className="w-8 h-8 rounded-full border-2 border-[#030303] bg-slate-800 flex items-center justify-center text-[10px] font-bold text-slate-400">
147
+ A{i}
148
+ </div>
149
+ ))}
150
+ </div>
151
+ <div className="h-8 w-px bg-white/10" />
152
+ <button className="bg-white/5 hover:bg-white/10 text-white px-4 py-2 rounded-xl text-xs font-bold border border-white/10 transition-all">
153
+ Help Center
154
+ </button>
155
+ </div>
156
+ </header>
157
+
158
+ {/* Content Area */}
159
+ <div className="flex-1 overflow-y-auto pr-2 custom-scrollbar pb-12">
160
+ {activeTab === "Dashboard" && (
161
+ <Dashboard
162
+ tendersFound={tenders.length}
163
+ recommendedOpportunities={analysisResult?.decision === "Recommended" ? 1 : 0}
164
+ highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
165
+ reportsGenerated={analysisResult ? 1 : 0}
166
+ tenders={tenders}
167
+ />
168
+ )}
169
+ {(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
170
+ <TenderSearch
171
+ tenders={tenders}
172
+ onSearch={handleSearch}
173
+ onAnalyze={handleTenderSelect}
174
+ forceShowFollowed={activeTab === "My Portfolio"}
175
+ />
176
+ )}
177
+ {activeTab === "Company Profile" && (
178
+ <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
179
+ )}
180
+ {activeTab === "Agent Analysis" && (
181
+ <AgentAnalysis
182
+ tender={selectedTender}
183
+ companyProfile={companyProfile}
184
+ analysis={analysisResult}
185
+ onAnalyze={handleRunAnalysis}
186
+ />
187
+ )}
188
+ {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
189
+ {activeTab === "Reports" && <Reports reportMarkdown={analysisResult?.report_markdown ?? ""} />}
190
+ {activeTab === "History" && <AnalysisHistory history={analysisHistory} />}
191
+ </div>
192
  </main>
193
  </div>
194
+
195
+ <footer className="py-8 border-t border-white/5 bg-black/20 text-center">
196
+ <p className="text-[10px] font-bold uppercase tracking-[0.5em] text-slate-600 mb-2">
197
+ Intelligence Orchestrated
198
  </p>
199
+ <p className="text-xs text-slate-500 font-medium">
200
+ AndesOps AI Enterprise 2026 | Powered by REW
201
  </p>
202
  </footer>
203
  </div>
frontend/components/Sidebar.tsx CHANGED
@@ -21,39 +21,51 @@ type Props = {
21
 
22
  export default function Sidebar({ tabs, activeTab, onTabSelect, status }: Props) {
23
  return (
24
- <aside className="w-80 rounded-3xl border border-slate-800 bg-[#081822] p-6 text-slate-100 shadow-xl shadow-slate-900/20">
25
- <div className="mb-8">
26
- <div className="text-sm uppercase tracking-[0.3em] text-cyan-300/80">AndesOps AI</div>
27
- <h1 className="mt-4 text-3xl font-semibold text-white">Enterprise Tender Intelligence</h1>
28
- <p className="mt-3 text-slate-400">Chile-focused procurement insights for enterprise teams.</p>
 
29
  </div>
30
- <div className="space-y-2">
 
31
  {tabs.map((tab) => {
 
32
  const tabSlug = tab.toLowerCase().replace(/ /g, "_");
 
33
  return (
34
- <a
35
  key={tab}
36
- href={`?tab=${tabSlug}`}
37
- onClick={(e) => {
38
- e.preventDefault();
39
  onTabSelect(tab);
40
  window.history.pushState({}, '', `?tab=${tabSlug}`);
41
  }}
42
- className={`flex w-full items-center justify-between rounded-2xl px-4 py-3 text-left transition ${
43
- activeTab === tab
44
- ? "bg-gradient-to-r from-cyan/20 to-slate-700 text-white shadow-inner"
45
- : "text-slate-300 hover:bg-slate-800/80"
46
  }`}
47
  >
48
- <span>{tab}</span>
49
- {activeTab === tab && <span className="text-cyan-300">●</span>}
50
- </a>
 
 
51
  );
52
  })}
53
- </div>
54
- <div className="mt-8 rounded-2xl bg-slate-900/70 p-4 text-sm text-slate-300">
55
- <div className="font-semibold text-slate-100">Conexión</div>
56
- <div className="mt-2 text-cyan-300">{status === "connected" ? "Backend disponible" : "Conectando..."}</div>
 
 
 
 
 
 
 
 
57
  </div>
58
  </aside>
59
  );
 
21
 
22
  export default function Sidebar({ tabs, activeTab, onTabSelect, status }: Props) {
23
  return (
24
+ <aside className="w-72 glass-card rounded-3xl h-[calc(100vh-3rem)] sticky top-6 p-6 flex flex-col gap-8">
25
+ <div className="flex items-center gap-3 px-2">
26
+ <div className="w-10 h-10 premium-gradient rounded-xl flex items-center justify-center shadow-lg shadow-purple-500/20">
27
+ <span className="text-white font-bold text-xl">A</span>
28
+ </div>
29
+ <h1 className="text-xl font-bold tracking-tight text-white">AndesOps AI</h1>
30
  </div>
31
+
32
+ <nav className="flex-1 flex flex-col gap-2">
33
  {tabs.map((tab) => {
34
+ const isActive = activeTab === tab;
35
  const tabSlug = tab.toLowerCase().replace(/ /g, "_");
36
+
37
  return (
38
+ <button
39
  key={tab}
40
+ onClick={() => {
 
 
41
  onTabSelect(tab);
42
  window.history.pushState({}, '', `?tab=${tabSlug}`);
43
  }}
44
+ className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group ${
45
+ isActive
46
+ ? "bg-white/10 text-white shadow-inner"
47
+ : "text-slate-400 hover:bg-white/5 hover:text-white"
48
  }`}
49
  >
50
+ <div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${
51
+ isActive ? "bg-purple-500 scale-125 shadow-[0_0_8px_rgba(168,85,247,0.8)]" : "bg-transparent group-hover:bg-slate-600"
52
+ }`} />
53
+ <span className="font-medium text-sm">{tab}</span>
54
+ </button>
55
  );
56
  })}
57
+ </nav>
58
+
59
+ <div className="mt-auto pt-6 border-t border-white/5">
60
+ <div className="px-4 py-3 rounded-xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 border border-indigo-500/20">
61
+ <p className="text-[10px] uppercase tracking-widest text-indigo-300 font-bold mb-1">Status</p>
62
+ <div className="flex items-center gap-2">
63
+ <div className={`w-2 h-2 rounded-full ${status === "connected" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`} />
64
+ <span className={`text-xs font-medium ${status === "connected" ? "text-green-300" : "text-yellow-300"}`}>
65
+ {status === "connected" ? "Systems Nominal" : "Connecting..."}
66
+ </span>
67
+ </div>
68
+ </div>
69
  </div>
70
  </aside>
71
  );
frontend/components/TenderSearch.tsx CHANGED
@@ -14,10 +14,7 @@ type Props = {
14
  export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false }: Props) {
15
  const [keyword, setKeyword] = useState("");
16
  const [buyerCode, setBuyerCode] = useState("");
17
- const [providerCode, setProviderCode] = useState("");
18
- const [searchDate, setSearchDate] = useState("");
19
-
20
- const [searchMode, setSearchMode] = useState<"keyword" | "intelligence">("keyword");
21
  const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
22
  const [followedCodes, setFollowedCodes] = useState<string[]>(() => {
23
  if (typeof window !== 'undefined') {
@@ -31,12 +28,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
31
  const [currentPage, setCurrentPage] = useState(1);
32
  const itemsPerPage = 50;
33
 
34
- // Sync with forceShowFollowed prop
35
  useEffect(() => {
36
  if (forceShowFollowed) setShowOnlyFollowed(true);
37
  }, [forceShowFollowed]);
38
 
39
- // Persistence effect
40
  useEffect(() => {
41
  localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
42
  }, [followedCodes]);
@@ -53,22 +48,19 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
53
  );
54
  };
55
 
56
- const handleSearchClick = async (page = 1) => {
57
- setShowOnlyFollowed(false); // Reset filter on new search
 
58
  setIsLoading(true);
59
  setCurrentPage(page);
60
  try {
61
- if (searchMode === "keyword") {
62
- await onSearch({ keyword, skip: (page - 1) * itemsPerPage, limit: itemsPerPage });
63
- } else {
64
- await onSearch({
65
- buyer_code: buyerCode,
66
- provider_code: providerCode,
67
- date: searchDate,
68
- skip: (page - 1) * itemsPerPage,
69
- limit: itemsPerPage
70
- });
71
- }
72
  } finally {
73
  setIsLoading(false);
74
  }
@@ -82,198 +74,149 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
82
  }, [tenders, showOnlyFollowed, followedCodes]);
83
 
84
  return (
85
- <div className="space-y-6">
86
- {/* Header & Modes */}
87
- <div className="flex items-center justify-between border-b border-slate-800 pb-4">
88
- <div>
89
- <h2 className="text-2xl font-bold text-white">Discovery & Intelligence</h2>
90
- <p className="text-sm text-slate-400">Encuentra oportunidades reales o analiza a tu competencia.</p>
91
  </div>
92
- <div className="flex rounded-2xl bg-slate-900 p-1">
93
- <button
94
- onClick={() => setSearchMode("keyword")}
95
- className={`rounded-xl px-4 py-2 text-xs font-semibold transition ${
96
- searchMode === "keyword" ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-400 hover:text-slate-200"
97
- }`}
98
- >
99
- Búsqueda General
100
- </button>
101
- <button
102
- onClick={() => setSearchMode("intelligence")}
103
- className={`rounded-xl px-4 py-2 text-xs font-semibold transition ${
104
- searchMode === "intelligence" ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-400 hover:text-slate-200"
105
- }`}
106
- >
107
- Market Intelligence
108
- </button>
109
- </div>
110
- </div>
111
-
112
- {/* Search Forms */}
113
- <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-6">
114
- {searchMode === "keyword" ? (
115
- <div className="flex flex-wrap items-end gap-4">
116
- <div className="flex-1 min-w-[300px]">
117
- <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">Palabra clave o código</label>
118
- <input
119
- value={keyword}
120
- onChange={(event) => setKeyword(event.target.value)}
121
- onKeyDown={(event) => event.key === "Enter" && handleSearchClick()}
122
- placeholder="Ej: software, 7210-24-LE23"
123
- className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-5 py-4 text-white outline-none focus:border-cyan transition shadow-inner"
124
- />
125
- </div>
126
- <button
127
- onClick={() => handleSearchClick()}
128
- disabled={isLoading}
129
- className="rounded-2xl bg-cyan px-8 py-4 font-bold text-slate-950 transition hover:bg-sky hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50"
130
- >
131
- {isLoading ? "Buscando..." : "Search"}
132
- </button>
133
  </div>
134
- ) : (
135
- <div className="grid gap-4 md:grid-cols-4 items-end">
136
- <div>
137
- <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">RUT Proveedor (Competencia)</label>
138
- <input
139
- value={providerCode}
140
- onChange={(event) => setProviderCode(event.target.value)}
141
- placeholder="Ej: 17793"
142
- className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
143
- />
144
- </div>
145
- <div>
146
- <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">ID Organismo (Comprador)</label>
147
- <input
148
- value={buyerCode}
149
- onChange={(event) => setBuyerCode(event.target.value)}
150
- placeholder="Ej: 6945"
151
- className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
152
- />
153
- </div>
154
- <div>
155
- <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">Fecha (ddmmaaaa)</label>
156
- <input
157
- value={searchDate}
158
- onChange={(event) => setSearchDate(event.target.value)}
159
- placeholder="Ej: 29042024"
160
- className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
161
- />
162
- </div>
163
  <button
164
- onClick={() => handleSearchClick()}
165
- className="rounded-2xl bg-sky px-6 py-3 font-bold text-slate-950 transition hover:bg-cyan"
 
166
  >
167
- Get Insights
168
  </button>
169
  </div>
170
- )}
171
  </div>
172
 
173
- {/* Results Header & Follow Filter */}
174
  <div className="flex items-center justify-between px-2">
175
- <h3 className="text-lg font-bold text-white flex items-center gap-2">
176
- {showOnlyFollowed ? "Mis Seguimientos" : "Resultados Encontrados"}
177
- <span className="text-xs bg-slate-800 text-slate-500 px-2 py-0.5 rounded-full font-mono">
178
- {filteredTenders.length}
 
 
179
  </span>
180
- </h3>
181
  {followedCodes.length > 0 && (
182
  <button
183
  onClick={() => setShowOnlyFollowed(!showOnlyFollowed)}
184
- className={`flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition border ${
185
  showOnlyFollowed
186
- ? "bg-amber-400/10 border-amber-400/30 text-amber-400"
187
- : "bg-slate-900 border-slate-800 text-slate-400 hover:border-slate-700"
188
  }`}
189
  >
190
- {showOnlyFollowed ? "★ Viendo Seguimientos" : "☆ Ver solo Seguidos"}
191
  </button>
192
  )}
193
  </div>
194
 
195
- {/* Results Table */}
196
  <div className="space-y-4">
197
  {filteredTenders.length === 0 ? (
198
- <div className="flex flex-col items-center justify-center rounded-3xl border border-slate-800 bg-slate-950/30 p-20 text-center">
199
- <div className="text-5xl mb-4 opacity-50">{showOnlyFollowed ? "🌟" : "📡"}</div>
200
- <p className="text-slate-400">
 
 
201
  {showOnlyFollowed
202
- ? "No tienes licitaciones marcadas como favoritas todavía."
203
- : "Inicia una búsqueda para conectar con la API de Mercado Público."}
204
  </p>
205
  </div>
206
  ) : (
207
- <div className="overflow-hidden rounded-3xl border border-slate-800 bg-slate-950/80 shadow-2xl">
208
- <table className="w-full min-w-[900px] border-collapse text-left text-sm">
209
- <thead className="bg-slate-900 text-slate-400 uppercase text-[10px] tracking-widest font-bold">
210
  <tr>
211
- <th className="px-6 py-5">Identificador</th>
212
- <th className="px-6 py-5">Oportunidad</th>
213
- <th className="px-6 py-5">Entidad Compradora</th>
214
- <th className="px-6 py-5">Cierre</th>
215
- <th className="px-6 py-5 text-center">Estado</th>
216
- <th className="px-6 py-5">Monto</th>
217
- <th className="px-6 py-5 text-right pr-10">Acciones</th>
218
  </tr>
219
  </thead>
220
- <tbody>
221
  {filteredTenders.map((tender) => (
222
  <Fragment key={tender.code}>
223
- <tr className="border-t border-slate-800 hover:bg-slate-900/50 transition-colors group">
224
  <td className="px-6 py-5">
225
  <div className="flex items-center gap-3">
226
  <button
227
  onClick={() => toggleFollow(tender.code)}
228
- className={`text-lg transition-transform hover:scale-125 ${followedCodes.includes(tender.code) ? 'text-amber-400' : 'text-slate-600 hover:text-slate-400'}`}
229
  >
230
  {followedCodes.includes(tender.code) ? "★" : "☆"}
231
  </button>
232
- <div>
233
- <div className="font-mono text-cyan">{tender.code}</div>
234
- <div className="text-[10px] text-slate-500">{tender.source}</div>
235
- </div>
236
  </div>
237
  </td>
238
  <td className="px-6 py-5 max-w-xs">
239
- <div className="font-semibold text-white group-hover:text-cyan transition-colors truncate">{tender.name}</div>
240
- <div className="text-xs text-slate-500">{tender.region || "Multiregional"}</div>
241
- </td>
242
- <td className="px-6 py-5 text-slate-300">{tender.buyer}</td>
243
- <td className="px-6 py-5">
244
- <div className={`text-xs font-mono ${
245
- tender.closing_date && new Date(tender.closing_date).getTime() - new Date().getTime() < 3 * 24 * 60 * 60 * 1000
246
- ? "text-red-400 font-bold"
247
- : "text-slate-400"
248
- }`}>
249
- {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
250
- </div>
251
  </td>
 
252
  <td className="px-6 py-5 text-center">
253
- <span className={`rounded-full px-3 py-1 text-[10px] font-bold ${
254
  tender.status.toLowerCase().includes('abierto') || tender.status.toLowerCase().includes('publicada')
255
  ? 'bg-green-500/10 text-green-400 border border-green-500/20'
256
- : 'bg-slate-800 text-slate-400'
257
  }`}>
258
  {tender.status}
259
  </span>
260
  </td>
261
- <td className="px-6 py-5 font-semibold text-slate-200">
262
- {tender.estimated_amount
263
- ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount)
264
- : "---"}
265
  </td>
266
  <td className="px-6 py-5 text-right pr-10">
267
  <div className="flex items-center justify-end gap-3">
268
  <button
269
  onClick={() => toggleExpanded(tender.code)}
270
- className="rounded-xl border border-slate-700 bg-slate-900 px-5 py-2.5 text-xs font-bold text-slate-200 hover:border-cyan hover:text-cyan transition whitespace-nowrap"
271
  >
272
- {expandedTenderCodes.includes(tender.code) ? "Cerrar" : "Detalle"}
273
  </button>
274
  <button
275
  onClick={() => onAnalyze(tender)}
276
- className="rounded-xl bg-cyan px-5 py-2.5 text-xs font-bold text-slate-950 hover:bg-sky transition shadow-lg shadow-cyan/10 whitespace-nowrap"
277
  >
278
  Analyze
279
  </button>
@@ -281,74 +224,45 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
281
  </td>
282
  </tr>
283
  {expandedTenderCodes.includes(tender.code) && (
284
- <tr className="bg-slate-950/90 animate-in fade-in duration-300">
285
- <td colSpan={7} className="px-10 py-8 border-t border-slate-800">
286
- <div className="grid gap-10 lg:grid-cols-2">
287
- {/* Left Col: Info */}
288
- <div className="space-y-8">
289
- <div>
290
- <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Descripción del Proyecto</h4>
291
- <p className="text-slate-300 leading-relaxed text-sm">{tender.description}</p>
292
- </div>
293
-
294
- <div>
295
- <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Ítems Solicitados</h4>
296
- <div className="space-y-2 max-h-[200px] overflow-y-auto pr-2 custom-scrollbar">
297
- {tender.items?.length ? tender.items.map((it, i) => (
298
- <div key={i} className="flex items-center justify-between p-3 rounded-xl bg-slate-900/50 border border-slate-800">
299
- <span className="text-xs text-slate-200">{it.name}</span>
300
- <span className="text-xs font-mono text-cyan">{it.quantity} {it.unit}</span>
301
- </div>
302
- )) : <p className="text-xs text-slate-600 italic">No hay ítems detallados disponibles.</p>}
303
- </div>
304
- </div>
305
- </div>
306
-
307
- {/* Right Col: Documents & Meta */}
308
- <div className="space-y-8">
309
  <div>
310
- <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Bases y Documentos</h4>
311
- <div className="grid gap-3">
312
- {tender.attachments && tender.attachments.length > 0 ? (
313
- tender.attachments.map((att, i) => (
314
- <a key={i} href={att.url} target="_blank" className="flex items-center gap-3 p-4 rounded-2xl bg-slate-900 hover:bg-slate-800 border border-slate-800 transition group">
315
- <div className="text-2xl group-hover:scale-110 transition">📄</div>
316
- <div className="overflow-hidden">
317
- <div className="text-xs font-semibold text-slate-200 truncate group-hover:text-cyan">{att.name}</div>
318
- <div className="text-[10px] text-slate-500">Descargar desde Mercado Público</div>
319
- </div>
320
- </a>
321
- ))
322
- ) : (
323
- <div className="space-y-3">
324
- <div className="rounded-2xl border border-dashed border-slate-800 p-6 text-center">
325
- <p className="text-xs text-slate-500">No hay enlaces directos.</p>
326
- <p className="mt-2 text-xs text-cyan">Usa 'Upload PDF' en el análisis agéntico.</p>
327
- </div>
328
- <a
329
- href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
330
- target="_blank"
331
- rel="noopener noreferrer"
332
- className="flex items-center justify-center gap-2 w-full p-4 rounded-2xl bg-cyan/10 border border-cyan/20 text-cyan text-xs font-bold hover:bg-cyan/20 transition"
333
- >
334
- 🌐 Ir al Portal de Mercado Público
335
- </a>
336
- </div>
337
- )}
338
- </div>
339
  </div>
340
-
341
  <div className="grid grid-cols-2 gap-4">
342
- <div className="p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
343
- <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Cierre Licitación</div>
344
- <div className="text-xs text-slate-100">{tender.closing_date}</div>
 
 
345
  </div>
346
- <div className="p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
347
- <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Rubro / Sector</div>
348
- <div className="text-xs text-slate-100">{tender.sector || "General"}</div>
349
  </div>
350
  </div>
351
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  </div>
353
  </td>
354
  </tr>
@@ -357,59 +271,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
357
  ))}
358
  </tbody>
359
  </table>
360
- {/* Pagination Controls */}
361
- {!showOnlyFollowed && tenders.length > 0 && (
362
- <div className="flex items-center justify-between p-6 border-t border-slate-800">
363
- <div className="text-xs text-slate-500">
364
- Mostrando <span className="text-white font-bold">{tenders.length}</span> resultados (Página {currentPage})
365
- </div>
366
- <div className="flex gap-2">
367
- <button
368
- onClick={() => handleSearchClick(currentPage - 1)}
369
- disabled={currentPage === 1 || isLoading}
370
- className="px-4 py-2 rounded-xl border border-slate-700 text-xs font-bold text-white hover:bg-slate-800 disabled:opacity-30 transition"
371
- >
372
- ← Anterior
373
- </button>
374
- <button
375
- onClick={() => handleSearchClick(currentPage + 1)}
376
- disabled={tenders.length < itemsPerPage || isLoading}
377
- className="px-4 py-2 rounded-xl border border-slate-700 text-xs font-bold text-white hover:bg-slate-800 disabled:opacity-30 transition"
378
- >
379
- Siguiente →
380
- </button>
381
- </div>
382
- </div>
383
- )}
384
  </div>
385
  )}
386
  </div>
387
 
388
- {/* Agent Documentation Section */}
389
- <div className="mt-12 rounded-3xl border border-slate-800 bg-slate-900/20 p-8">
390
- <h3 className="text-xl font-bold text-white mb-6 flex items-center gap-2">
391
- <span className="text-cyan">◈</span> Intelligent Agent Orchestration
392
- </h3>
393
- <div className="flex items-center gap-3 mb-6">
394
- <span className="text-cyan">◈</span>
395
- <h3 className="text-sm font-bold uppercase tracking-widest text-white">Intelligent Agent Orchestration</h3>
396
- </div>
397
- <div className="grid gap-4 md:grid-cols-3">
398
- <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
399
- <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Legal Agent</div>
400
- <p className="text-[11px] text-slate-400">Analiza bases administrativas y riesgos de cumplimiento legal en tiempo real.</p>
401
- </div>
402
- <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
403
- <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Technical Agent</div>
404
- <p className="text-[11px] text-slate-400">Evalúa especificaciones técnicas y determina la factibilidad del proyecto.</p>
405
- </div>
406
- <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
407
- <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Strategy Agent</div>
408
- <p className="text-[11px] text-slate-400">Calcula el ROI proyectado y define la mejor táctica comercial para ganar.</p>
409
- </div>
410
- </div>
411
- </div>
412
-
413
  {isLoading && <BrandLoader />}
414
  </div>
415
  );
 
14
  export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false }: Props) {
15
  const [keyword, setKeyword] = useState("");
16
  const [buyerCode, setBuyerCode] = useState("");
17
+ const [date, setDate] = useState("");
 
 
 
18
  const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
19
  const [followedCodes, setFollowedCodes] = useState<string[]>(() => {
20
  if (typeof window !== 'undefined') {
 
28
  const [currentPage, setCurrentPage] = useState(1);
29
  const itemsPerPage = 50;
30
 
 
31
  useEffect(() => {
32
  if (forceShowFollowed) setShowOnlyFollowed(true);
33
  }, [forceShowFollowed]);
34
 
 
35
  useEffect(() => {
36
  localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
37
  }, [followedCodes]);
 
48
  );
49
  };
50
 
51
+ const handleSubmit = async (e?: React.FormEvent, page = 1) => {
52
+ e?.preventDefault();
53
+ setShowOnlyFollowed(false);
54
  setIsLoading(true);
55
  setCurrentPage(page);
56
  try {
57
+ await onSearch({
58
+ keyword,
59
+ buyer_code: buyerCode,
60
+ date,
61
+ skip: (page - 1) * itemsPerPage,
62
+ limit: itemsPerPage
63
+ });
 
 
 
 
64
  } finally {
65
  setIsLoading(false);
66
  }
 
74
  }, [tenders, showOnlyFollowed, followedCodes]);
75
 
76
  return (
77
+ <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
78
+ {/* Search Bar Section */}
79
+ <div className="glass-card rounded-3xl p-8">
80
+ <div className="mb-6">
81
+ <h2 className="text-2xl font-bold text-white mb-2">Tender Discovery</h2>
82
+ <p className="text-slate-400 text-sm">Real-time access to the Chilean public procurement market.</p>
83
  </div>
84
+
85
+ <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-6">
86
+ <div className="space-y-2">
87
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Search Keyword</label>
88
+ <input
89
+ type="text"
90
+ placeholder="e.g. Software, Construction..."
91
+ className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all"
92
+ value={keyword}
93
+ onChange={(e) => setKeyword(e.target.value)}
94
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  </div>
96
+ <div className="space-y-2">
97
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Buyer Code (Optional)</label>
98
+ <input
99
+ type="text"
100
+ placeholder="e.g. 6945"
101
+ className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all"
102
+ value={buyerCode}
103
+ onChange={(e) => setBuyerCode(e.target.value)}
104
+ />
105
+ </div>
106
+ <div className="space-y-2">
107
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Specific Date</label>
108
+ <input
109
+ type="date"
110
+ className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all [color-scheme:dark]"
111
+ value={date}
112
+ onChange={(e) => setDate(e.target.value)}
113
+ />
114
+ </div>
115
+ <div className="flex items-end">
 
 
 
 
 
 
 
 
 
116
  <button
117
+ type="submit"
118
+ disabled={isLoading}
119
+ className="w-full premium-gradient hover:opacity-90 text-white font-bold py-3.5 rounded-xl transition-all shadow-lg shadow-purple-500/20 active:scale-[0.98] disabled:opacity-50"
120
  >
121
+ {isLoading ? "Searching..." : "Fetch Opportunities"}
122
  </button>
123
  </div>
124
+ </form>
125
  </div>
126
 
127
+ {/* Results Controls */}
128
  <div className="flex items-center justify-between px-2">
129
+ <div className="flex items-center gap-3">
130
+ <h3 className="text-lg font-bold text-white">
131
+ {showOnlyFollowed ? "Saved Opportunities" : "Market Results"}
132
+ </h3>
133
+ <span className="text-[10px] bg-white/5 text-slate-400 px-2 py-0.5 rounded-full border border-white/5">
134
+ {filteredTenders.length} items
135
  </span>
136
+ </div>
137
  {followedCodes.length > 0 && (
138
  <button
139
  onClick={() => setShowOnlyFollowed(!showOnlyFollowed)}
140
+ className={`flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition-all border ${
141
  showOnlyFollowed
142
+ ? "bg-purple-500/20 border-purple-500/40 text-purple-300"
143
+ : "bg-white/5 border-white/10 text-slate-400 hover:border-white/20"
144
  }`}
145
  >
146
+ {showOnlyFollowed ? "★ Viewing Portfolio" : "☆ Show Favorites Only"}
147
  </button>
148
  )}
149
  </div>
150
 
151
+ {/* Results List */}
152
  <div className="space-y-4">
153
  {filteredTenders.length === 0 ? (
154
+ <div className="flex flex-col items-center justify-center rounded-3xl border border-white/5 bg-white/[0.02] p-20 text-center">
155
+ <div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center text-3xl mb-4 opacity-50">
156
+ {showOnlyFollowed ? "🌟" : "📡"}
157
+ </div>
158
+ <p className="text-slate-400 max-w-xs mx-auto">
159
  {showOnlyFollowed
160
+ ? "You haven't followed any tenders yet. Start by searching for opportunities."
161
+ : "Enter keywords above to connect with the Mercado Público API."}
162
  </p>
163
  </div>
164
  ) : (
165
+ <div className="glass-card rounded-3xl overflow-hidden overflow-x-auto shadow-2xl">
166
+ <table className="w-full min-w-[1000px] text-left text-sm">
167
+ <thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold">
168
  <tr>
169
+ <th className="px-6 py-5">Code</th>
170
+ <th className="px-6 py-5">Opportunity</th>
171
+ <th className="px-6 py-5">Buyer</th>
172
+ <th className="px-6 py-5 text-center">Status</th>
173
+ <th className="px-6 py-5 text-right">Deadline</th>
174
+ <th className="px-6 py-5 text-right pr-10">Actions</th>
 
175
  </tr>
176
  </thead>
177
+ <tbody className="divide-y divide-white/5">
178
  {filteredTenders.map((tender) => (
179
  <Fragment key={tender.code}>
180
+ <tr className="hover:bg-white/[0.02] transition-colors group">
181
  <td className="px-6 py-5">
182
  <div className="flex items-center gap-3">
183
  <button
184
  onClick={() => toggleFollow(tender.code)}
185
+ className={`text-lg transition-all hover:scale-125 ${followedCodes.includes(tender.code) ? 'text-purple-400 drop-shadow-[0_0_8px_rgba(168,85,247,0.4)]' : 'text-slate-600 hover:text-slate-400'}`}
186
  >
187
  {followedCodes.includes(tender.code) ? "★" : "☆"}
188
  </button>
189
+ <span className="font-mono text-purple-400 text-xs">{tender.code}</span>
 
 
 
190
  </div>
191
  </td>
192
  <td className="px-6 py-5 max-w-xs">
193
+ <div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate">{tender.name}</div>
194
+ <div className="text-[10px] text-slate-500">{tender.region || "Multiregional"}</div>
 
 
 
 
 
 
 
 
 
 
195
  </td>
196
+ <td className="px-6 py-5 text-slate-400 text-xs">{tender.buyer}</td>
197
  <td className="px-6 py-5 text-center">
198
+ <span className={`inline-block rounded-full px-3 py-1 text-[10px] font-bold ${
199
  tender.status.toLowerCase().includes('abierto') || tender.status.toLowerCase().includes('publicada')
200
  ? 'bg-green-500/10 text-green-400 border border-green-500/20'
201
+ : 'bg-slate-800/50 text-slate-500 border border-white/5'
202
  }`}>
203
  {tender.status}
204
  </span>
205
  </td>
206
+ <td className="px-6 py-5 text-right font-mono text-xs text-slate-400">
207
+ {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
 
 
208
  </td>
209
  <td className="px-6 py-5 text-right pr-10">
210
  <div className="flex items-center justify-end gap-3">
211
  <button
212
  onClick={() => toggleExpanded(tender.code)}
213
+ className="text-[11px] font-bold text-slate-400 hover:text-white transition"
214
  >
215
+ {expandedTenderCodes.includes(tender.code) ? "Close" : "Detail"}
216
  </button>
217
  <button
218
  onClick={() => onAnalyze(tender)}
219
+ className="bg-white/10 hover:bg-white/20 text-white text-[11px] font-bold px-4 py-2 rounded-lg transition-all border border-white/10"
220
  >
221
  Analyze
222
  </button>
 
224
  </td>
225
  </tr>
226
  {expandedTenderCodes.includes(tender.code) && (
227
+ <tr className="bg-white/[0.01] animate-in fade-in duration-500">
228
+ <td colSpan={6} className="px-10 py-8">
229
+ <div className="grid gap-12 lg:grid-cols-2">
230
+ <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  <div>
232
+ <h4 className="text-[10px] font-bold uppercase tracking-widest text-purple-400 mb-3">Project Description</h4>
233
+ <p className="text-slate-400 leading-relaxed text-xs">{tender.description}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  </div>
 
235
  <div className="grid grid-cols-2 gap-4">
236
+ <div className="p-4 rounded-2xl bg-white/[0.03] border border-white/5">
237
+ <div className="text-[9px] uppercase text-slate-500 font-bold mb-1">Estimated Amount</div>
238
+ <div className="text-xs text-slate-200 font-semibold">
239
+ {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP" }).format(tender.estimated_amount) : "Not Disclosed"}
240
+ </div>
241
  </div>
242
+ <div className="p-4 rounded-2xl bg-white/[0.03] border border-white/5">
243
+ <div className="text-[9px] uppercase text-slate-500 font-bold mb-1">Sector</div>
244
+ <div className="text-xs text-slate-200 font-semibold">{tender.sector || "General"}</div>
245
  </div>
246
  </div>
247
  </div>
248
+ <div className="space-y-6">
249
+ <h4 className="text-[10px] font-bold uppercase tracking-widest text-purple-400 mb-3">Resources & Direct Links</h4>
250
+ <div className="grid gap-3">
251
+ {tender.attachments?.map((att, i) => (
252
+ <a key={i} href={att.url} target="_blank" className="flex items-center gap-3 p-3 rounded-xl bg-white/[0.03] hover:bg-white/[0.06] border border-white/5 transition group">
253
+ <span className="text-lg">📄</span>
254
+ <span className="text-xs text-slate-300 group-hover:text-white truncate">{att.name}</span>
255
+ </a>
256
+ ))}
257
+ <a
258
+ href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
259
+ target="_blank"
260
+ className="flex items-center justify-center gap-2 p-3 rounded-xl bg-purple-500/10 border border-purple-500/20 text-purple-300 text-xs font-bold hover:bg-purple-500/20 transition mt-2"
261
+ >
262
+ Open official portal
263
+ </a>
264
+ </div>
265
+ </div>
266
  </div>
267
  </td>
268
  </tr>
 
271
  ))}
272
  </tbody>
273
  </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </div>
275
  )}
276
  </div>
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  {isLoading && <BrandLoader />}
279
  </div>
280
  );
frontend/globals.css CHANGED
@@ -3,23 +3,65 @@
3
  @tailwind utilities;
4
 
5
  :root {
6
- color-scheme: dark;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  }
8
 
9
- html,
10
- body {
11
- min-height: 100%;
 
 
 
 
 
12
  }
13
 
14
- body {
15
- margin: 0;
16
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
- background: radial-gradient(circle at top, rgba(34, 211, 238, 0.14), transparent 30%),
18
- linear-gradient(180deg, #06121d 0%, #081727 40%, #081a29 100%);
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
- * {
22
- box-sizing: border-box;
 
 
 
 
 
 
 
23
  }
24
 
25
  ::selection {
 
3
  @tailwind utilities;
4
 
5
  :root {
6
+ --background: 240 10% 3.9%;
7
+ --foreground: 0 0% 98%;
8
+ --card: 240 10% 3.9%;
9
+ --card-foreground: 0 0% 98%;
10
+ --popover: 240 10% 3.9%;
11
+ --popover-foreground: 0 0% 98%;
12
+ --primary: 263.4 70% 50.4%;
13
+ --primary-foreground: 210 20% 98%;
14
+ --secondary: 240 3.7% 15.9%;
15
+ --secondary-foreground: 0 0% 98%;
16
+ --muted: 240 3.7% 15.9%;
17
+ --muted-foreground: 240 5% 64.9%;
18
+ --accent: 240 3.7% 15.9%;
19
+ --accent-foreground: 0 0% 98%;
20
+ --destructive: 0 62.8% 30.6%;
21
+ --destructive-foreground: 0 0% 98%;
22
+ --border: 240 3.7% 15.9%;
23
+ --input: 240 3.7% 15.9%;
24
+ --ring: 263.4 70% 50.4%;
25
+ --radius: 0.75rem;
26
  }
27
 
28
+ @layer base {
29
+ body {
30
+ @apply bg-[#030303] text-foreground antialiased;
31
+ background-image:
32
+ radial-gradient(at 0% 0%, hsla(263, 70%, 50%, 0.15) 0px, transparent 50%),
33
+ radial-gradient(at 100% 100%, hsla(190, 70%, 50%, 0.1) 0px, transparent 50%);
34
+ background-attachment: fixed;
35
+ }
36
  }
37
 
38
+ @layer components {
39
+ .glass-card {
40
+ @apply bg-black/40 backdrop-blur-md border border-white/10 shadow-xl transition-all duration-300;
41
+ }
42
+
43
+ .glass-card:hover {
44
+ @apply border-white/20 bg-black/50;
45
+ }
46
+
47
+ .premium-gradient {
48
+ background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
49
+ }
50
+
51
+ .text-gradient {
52
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400;
53
+ }
54
  }
55
 
56
+ /* Custom scrollbar */
57
+ ::-webkit-scrollbar {
58
+ width: 8px;
59
+ }
60
+ ::-webkit-scrollbar-track {
61
+ @apply bg-transparent;
62
+ }
63
+ ::-webkit-scrollbar-thumb {
64
+ @apply bg-white/10 rounded-full hover:bg-white/20 transition-colors;
65
  }
66
 
67
  ::selection {