Álvaro Valenzuela Valdes commited on
Commit
d1d31de
·
1 Parent(s): e1d119c

feat: Intelligent document ingestion workflow and final syntax fix

Browse files
Files changed (1) hide show
  1. frontend/components/TenderSearch.tsx +154 -120
frontend/components/TenderSearch.tsx CHANGED
@@ -16,6 +16,9 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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') {
21
  const saved = localStorage.getItem('andes_followed_codes');
@@ -23,9 +26,9 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
23
  }
24
  return [];
25
  });
 
26
  const [showOnlyFollowed, setShowOnlyFollowed] = useState(forceShowFollowed);
27
  const [isLoading, setIsLoading] = useState(false);
28
- const [currentPage, setCurrentPage] = useState(1);
29
  const itemsPerPage = 50;
30
 
31
  useEffect(() => {
@@ -48,11 +51,27 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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,
@@ -77,24 +96,35 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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"
@@ -104,7 +134,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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]"
@@ -155,10 +185,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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
  ) : (
@@ -167,7 +197,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
167
  <table className="w-full text-left text-sm table-fixed border-collapse">
168
  <thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
169
  <tr>
170
- <th className="px-6 py-5 w-[120px]">Code</th>
171
  <th className="px-6 py-5 w-[300px]">Opportunity</th>
172
  <th className="px-6 py-5 w-[200px]">Buyer</th>
173
  <th className="px-6 py-5 text-center w-[120px]">Status</th>
@@ -175,49 +205,55 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
175
  <th className="px-6 py-5 text-right pr-10 w-[120px]">Actions</th>
176
  </tr>
177
  </thead>
178
- <tbody className="divide-y divide-white/5">
179
- {filteredTenders.map((tender) => (
180
- <Fragment key={tender.code}>
181
- <tr
182
- className="hover:bg-white/[0.04] cursor-pointer transition-colors group"
183
- onClick={() => toggleExpanded(tender.code)}
184
- >
185
- <td className="px-6 py-5">
186
- <div className="flex items-center gap-3">
187
- <button
188
- onClick={(e) => {
189
- e.stopPropagation();
190
- toggleFollow(tender.code);
191
- }}
192
- 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'}`}
193
- >
194
- {followedCodes.includes(tender.code) ? "★" : "☆"}
195
- </button>
196
- <span className="font-mono text-purple-400 text-xs">{tender.code}</span>
197
- </div>
198
- </td>
199
- <td className="px-6 py-5 max-w-xs">
200
- <div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate">{tender.name}</div>
201
- <div className="flex items-center gap-2 mt-1">
202
- <span className="text-[10px] text-slate-500">{tender.region || "Multiregional"}</span>
203
- <span className="text-[8px] px-1.5 py-0.5 rounded-md bg-white/5 text-slate-600 border border-white/5 uppercase tracking-tighter">{tender.sector}</span>
204
- </div>
205
- </td>
206
- <td className="px-6 py-5 text-slate-400 text-xs">{tender.buyer}</td>
207
- <td className="px-6 py-5 text-center">
208
- <span className={`inline-block rounded-full px-3 py-1 text-[10px] font-bold ${
209
- tender.status.toLowerCase().includes('abierto') || tender.status.toLowerCase().includes('publicada')
210
- ? 'bg-green-500/10 text-green-400 border border-green-500/20'
211
- : 'bg-slate-800/50 text-slate-500 border border-white/5'
212
- }`}>
213
- {tender.status}
214
- </span>
215
- </td>
216
- <td className="px-6 py-5 text-right font-mono text-xs text-slate-400">
217
- {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
218
- </td>
219
- <td className="px-6 py-5 text-right pr-10">
220
- <div className="flex items-center justify-end gap-3">
 
 
 
 
 
 
221
  <button
222
  onClick={(e) => {
223
  e.stopPropagation();
@@ -227,76 +263,74 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
227
  >
228
  Analyze
229
  </button>
230
- </div>
231
- </td>
232
- </tr>
233
- {expandedTenderCodes.includes(tender.code) && (
234
- <tr className="bg-white/[0.01] animate-in fade-in duration-500">
235
- <td colSpan={6} className="px-8 md:px-12 py-10">
236
- <div className="grid gap-10 lg:grid-cols-5 items-start">
237
- <div className="lg:col-span-3 space-y-8">
238
- <div>
239
- <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
240
- <span className="w-4 h-[1px] bg-purple-500/50" />
241
- Project Description
242
- </h4>
243
- <p className="text-slate-300 leading-relaxed text-sm bg-white/[0.02] p-5 rounded-2xl border border-white/5">{tender.description}</p>
244
- </div>
245
- <div className="grid grid-cols-2 gap-6">
246
- <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
247
- <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Estimated Amount</div>
248
- <div className="text-sm text-white font-bold">
249
- {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP" }).format(tender.estimated_amount) : "Not Disclosed"}
250
- </div>
251
  </div>
252
- <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
253
- <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Market Sector</div>
254
- <div className="text-sm text-white font-bold">{tender.sector || "General"}</div>
 
 
 
 
 
 
 
 
255
  </div>
256
  </div>
257
- </div>
258
- <div className="lg:col-span-2 space-y-8">
259
- <div>
260
- <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
261
- <span className="w-4 h-[1px] bg-purple-500/50" />
262
- Resources & Direct Links
263
- </h4>
264
- <div className="grid gap-3">
265
- {tender.attachments?.map((att, i) => (
266
- <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/[0.03] hover:bg-white/[0.08] border border-white/5 transition-all group/file">
267
- <div className="flex items-center gap-4">
268
- <span className="text-2xl">
269
- {att.name.endsWith('.pdf') ? "📕" : "📘"}
270
- </span>
271
- <div className="flex flex-col">
272
- <span className="text-xs font-bold text-slate-200 group-hover/file:text-white transition-colors">{att.name}</span>
273
- <span className="text-[10px] text-slate-500 uppercase tracking-tighter">Official Document</span>
274
- </div>
275
- </div>
276
- <span className="text-[10px] font-black text-purple-400 opacity-0 group-hover/file:opacity-100 transition-all">DOWNLOAD</span>
277
- </a>
278
- ))}
279
- <a
280
- href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
281
- target="_blank"
282
- 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"
283
- >
284
- Open official portal
285
- </a>
286
  </div>
287
  </div>
288
- </div>
289
- </td>
290
- </tr>
291
- )}
292
- </Fragment>
293
- ))}
294
- </tbody>
295
- </table>
296
  </div>
297
- </div>
298
- )}
299
- </div>
300
 
301
  {isLoading && <BrandLoader />}
302
  </div>
 
16
  const [buyerCode, setBuyerCode] = useState("");
17
  const [date, setDate] = useState("");
18
  const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
19
+ const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
20
+ const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
21
+
22
  const [followedCodes, setFollowedCodes] = useState<string[]>(() => {
23
  if (typeof window !== 'undefined') {
24
  const saved = localStorage.getItem('andes_followed_codes');
 
26
  }
27
  return [];
28
  });
29
+
30
  const [showOnlyFollowed, setShowOnlyFollowed] = useState(forceShowFollowed);
31
  const [isLoading, setIsLoading] = useState(false);
 
32
  const itemsPerPage = 50;
33
 
34
  useEffect(() => {
 
51
  );
52
  };
53
 
54
+ const toggleSelect = (code: string) => {
55
+ setSelectedCodes(prev =>
56
+ prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]
57
+ );
58
+ };
59
+
60
+ const handleSyncToAgents = () => {
61
+ setIsSyncingToAgents(true);
62
+ // Simulate system ingestion
63
+ setTimeout(() => {
64
+ setIsSyncingToAgents(false);
65
+ const firstSelected = tenders.find(t => t.code === selectedCodes[0]);
66
+ if (firstSelected) onAnalyze(firstSelected);
67
+ setSelectedCodes([]);
68
+ }, 2500);
69
+ };
70
+
71
  const handleSubmit = async (e?: React.FormEvent, page = 1) => {
72
  e?.preventDefault();
73
  setShowOnlyFollowed(false);
74
  setIsLoading(true);
 
75
  try {
76
  await onSearch({
77
  keyword,
 
96
  <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
97
  {/* Search Bar Section */}
98
  <div className="glass-card rounded-3xl p-8">
99
+ <div className="mb-6 flex justify-between items-start">
100
+ <div>
101
+ <h2 className="text-2xl font-bold text-white mb-2">Tender Discovery</h2>
102
+ <p className="text-slate-400 text-sm">Real-time access to the Chilean public procurement market.</p>
103
+ </div>
104
+ {selectedCodes.length > 0 && (
105
+ <button
106
+ onClick={handleSyncToAgents}
107
+ disabled={isSyncingToAgents}
108
+ className="premium-gradient text-white px-6 py-3 rounded-2xl font-black text-xs tracking-widest shadow-xl shadow-purple-500/40 animate-bounce transition-all active:scale-95 disabled:opacity-50"
109
+ >
110
+ {isSyncingToAgents ? "📥 INGESTING DOCUMENTS..." : `⚡ ANALYZE ${selectedCodes.length} SELECTED`}
111
+ </button>
112
+ )}
113
  </div>
114
 
115
  <form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-6">
116
  <div className="space-y-2">
117
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Keyword</label>
118
  <input
119
  type="text"
120
+ placeholder="e.g. Software, Cloud..."
121
  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"
122
  value={keyword}
123
  onChange={(e) => setKeyword(e.target.value)}
124
  />
125
  </div>
126
  <div className="space-y-2">
127
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Buyer Code</label>
128
  <input
129
  type="text"
130
  placeholder="e.g. 6945"
 
134
  />
135
  </div>
136
  <div className="space-y-2">
137
+ <label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Date Limit</label>
138
  <input
139
  type="date"
140
  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]"
 
185
  <div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center text-3xl mb-4 opacity-50">
186
  {showOnlyFollowed ? "🌟" : "📡"}
187
  </div>
188
+ <p className="text-slate-400 max-w-xs mx-auto text-sm">
189
  {showOnlyFollowed
190
+ ? "No followed opportunities found."
191
+ : "Enter keywords above to fetch real-time data."}
192
  </p>
193
  </div>
194
  ) : (
 
197
  <table className="w-full text-left text-sm table-fixed border-collapse">
198
  <thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
199
  <tr>
200
+ <th className="px-6 py-5 w-[140px]">Selection</th>
201
  <th className="px-6 py-5 w-[300px]">Opportunity</th>
202
  <th className="px-6 py-5 w-[200px]">Buyer</th>
203
  <th className="px-6 py-5 text-center w-[120px]">Status</th>
 
205
  <th className="px-6 py-5 text-right pr-10 w-[120px]">Actions</th>
206
  </tr>
207
  </thead>
208
+ <tbody className="divide-y divide-white/5">
209
+ {filteredTenders.map((tender) => (
210
+ <Fragment key={tender.code}>
211
+ <tr
212
+ className={`hover:bg-white/[0.04] cursor-pointer transition-colors group ${selectedCodes.includes(tender.code) ? 'bg-purple-500/5' : ''}`}
213
+ onClick={() => toggleExpanded(tender.code)}
214
+ >
215
+ <td className="px-6 py-5">
216
+ <div className="flex items-center gap-3">
217
+ <input
218
+ type="checkbox"
219
+ checked={selectedCodes.includes(tender.code)}
220
+ onChange={(e) => {
221
+ e.stopPropagation();
222
+ toggleSelect(tender.code);
223
+ }}
224
+ className="w-4 h-4 rounded border-white/10 bg-white/5 text-purple-500 focus:ring-purple-500/40"
225
+ />
226
+ <button
227
+ onClick={(e) => {
228
+ e.stopPropagation();
229
+ toggleFollow(tender.code);
230
+ }}
231
+ 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'}`}
232
+ >
233
+ {followedCodes.includes(tender.code) ? "★" : "☆"}
234
+ </button>
235
+ <span className="font-mono text-purple-400 text-[10px]">{tender.code}</span>
236
+ </div>
237
+ </td>
238
+ <td className="px-6 py-5 max-w-xs">
239
+ <div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate">{tender.name}</div>
240
+ <div className="flex items-center gap-2 mt-1">
241
+ <span className="text-[10px] text-slate-500">{tender.region || "Nacional"}</span>
242
+ <span className="text-[8px] px-1.5 py-0.5 rounded-md bg-white/5 text-slate-600 border border-white/5 uppercase tracking-tighter">{tender.sector}</span>
243
+ </div>
244
+ </td>
245
+ <td className="px-6 py-5 text-slate-400 text-xs">{tender.buyer}</td>
246
+ <td className="px-6 py-5 text-center">
247
+ <span className={`inline-block rounded-full px-3 py-1 text-[10px] font-bold ${
248
+ tender.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800/50 text-slate-500'
249
+ }`}>
250
+ {tender.status}
251
+ </span>
252
+ </td>
253
+ <td className="px-6 py-5 text-right font-mono text-xs text-slate-400">
254
+ {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
255
+ </td>
256
+ <td className="px-6 py-5 text-right pr-10">
257
  <button
258
  onClick={(e) => {
259
  e.stopPropagation();
 
263
  >
264
  Analyze
265
  </button>
266
+ </td>
267
+ </tr>
268
+ {expandedTenderCodes.includes(tender.code) && (
269
+ <tr className="bg-white/[0.01] animate-in fade-in duration-500 border-l-2 border-purple-500">
270
+ <td colSpan={6} className="px-8 md:px-12 py-10">
271
+ <div className="grid gap-10 lg:grid-cols-5 items-start">
272
+ <div className="lg:col-span-3 space-y-8">
273
+ <div>
274
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
275
+ <span className="w-4 h-[1px] bg-purple-500/50" />
276
+ Project Description
277
+ </h4>
278
+ <p className="text-slate-300 leading-relaxed text-sm bg-white/[0.02] p-5 rounded-2xl border border-white/5">{tender.description}</p>
 
 
 
 
 
 
 
 
279
  </div>
280
+ <div className="grid grid-cols-2 gap-6">
281
+ <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
282
+ <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Estimated Amount</div>
283
+ <div className="text-sm text-white font-bold">
284
+ {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP" }).format(tender.estimated_amount) : "Not Disclosed"}
285
+ </div>
286
+ </div>
287
+ <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
288
+ <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Market Sector</div>
289
+ <div className="text-sm text-white font-bold">{tender.sector || "General"}</div>
290
+ </div>
291
  </div>
292
  </div>
293
+ <div className="lg:col-span-2 space-y-8">
294
+ <div>
295
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
296
+ <span className="w-4 h-[1px] bg-purple-500/50" />
297
+ Resources & Direct Links
298
+ </h4>
299
+ <div className="grid gap-3">
300
+ {tender.attachments?.map((att, i) => (
301
+ <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/[0.03] hover:bg-white/[0.08] border border-white/5 transition-all group/file">
302
+ <div className="flex items-center gap-4">
303
+ <span className="text-2xl">{att.name.endsWith('.pdf') ? "📕" : "📘"}</span>
304
+ <div className="flex flex-col">
305
+ <span className="text-xs font-bold text-slate-200 group-hover/file:text-white transition-colors">{att.name}</span>
306
+ <span className="text-[10px] text-slate-500 uppercase tracking-tighter">Official Document</span>
307
+ </div>
308
+ </div>
309
+ <span className="text-[10px] font-black text-purple-400 opacity-0 group-hover/file:opacity-100 transition-all">DOWNLOAD</span>
310
+ </a>
311
+ ))}
312
+ <a
313
+ href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
314
+ target="_blank"
315
+ 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"
316
+ >
317
+ Open official portal
318
+ </a>
319
+ </div>
320
+ </div>
 
321
  </div>
322
  </div>
323
+ </td>
324
+ </tr>
325
+ )}
326
+ </Fragment>
327
+ ))}
328
+ </tbody>
329
+ </table>
330
+ </div>
331
  </div>
332
+ )}
333
+ </div>
 
334
 
335
  {isLoading && <BrandLoader />}
336
  </div>