Álvaro Valenzuela Valdes commited on
Commit
63cdff5
·
1 Parent(s): 48f7497

feat: persistent search history and professional PDF export with branded headers

Browse files
backend/app/routers/analysis.py CHANGED
@@ -12,6 +12,7 @@ router = APIRouter()
12
 
13
  # Load initial history from disk
14
  analysis_history: List[AnalysisRecord] = load_from_json(AnalysisRecord, "analysis_history.json")
 
15
 
16
 
17
  @router.post("/analyze", response_model=AnalysisResult)
@@ -62,3 +63,15 @@ async def agent_chat(request: ChatRequest):
62
  if not response:
63
  response = "Lo siento, tuve un problema procesando tu solicitud. ¿Podrías intentar de nuevo?"
64
  return {"response": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # Load initial history from disk
14
  analysis_history: List[AnalysisRecord] = load_from_json(AnalysisRecord, "analysis_history.json")
15
+ search_history: List[SearchRecord] = load_from_json(SearchRecord, "search_history.json")
16
 
17
 
18
  @router.post("/analyze", response_model=AnalysisResult)
 
63
  if not response:
64
  response = "Lo siento, tuve un problema procesando tu solicitud. ¿Podrías intentar de nuevo?"
65
  return {"response": response}
66
+
67
+ @router.post("/search-history")
68
+ def save_search_history(record: SearchRecord):
69
+ search_history.insert(0, record)
70
+ if len(search_history) > 50:
71
+ search_history.pop()
72
+ save_to_json(search_history, "search_history.json")
73
+ return {"status": "ok"}
74
+
75
+ @router.get("/search-history", response_model=List[SearchRecord])
76
+ def get_search_history():
77
+ return search_history
backend/app/schemas/analysis.py CHANGED
@@ -67,3 +67,9 @@ class AnalysisRecord(BaseModel):
67
  tender_name: str
68
  analyzed_at: datetime
69
  analysis: AnalysisResult
 
 
 
 
 
 
 
67
  tender_name: str
68
  analyzed_at: datetime
69
  analysis: AnalysisResult
70
+
71
+ class SearchRecord(BaseModel):
72
+ query: str
73
+ results_count: int
74
+ searched_at: datetime
75
+ is_agile: bool = False
frontend/app/page.tsx CHANGED
@@ -47,6 +47,7 @@ export default function HomePage() {
47
  });
48
  const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
49
  const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
 
50
  const [status, setStatus] = useState("listening");
51
  const [searchKeyword, setSearchKeyword] = useState("");
52
  const [lang, setLang] = useState<Language>("en");
@@ -123,6 +124,14 @@ export default function HomePage() {
123
  console.error("History load error", e);
124
  }
125
 
 
 
 
 
 
 
 
 
126
  try {
127
  const initialTenders = await searchTenders({ status: 'activas' });
128
  setTenders(initialTenders);
@@ -157,6 +166,13 @@ export default function HomePage() {
157
  results = await searchTenders(params);
158
  }
159
  setTenders(results);
 
 
 
 
 
 
 
160
  } catch (error) {
161
  console.error("Search error:", error);
162
  }
@@ -306,7 +322,7 @@ export default function HomePage() {
306
  />
307
  )}
308
  {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
309
- {activeTab === "History" && <AnalysisHistory history={analysisHistory} />}
310
  {activeTab === "About" && <SystemInfo />}
311
  </div>
312
  </main>
 
47
  });
48
  const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
49
  const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
50
+ const [searchHistory, setSearchHistory] = useState<any[]>([]);
51
  const [status, setStatus] = useState("listening");
52
  const [searchKeyword, setSearchKeyword] = useState("");
53
  const [lang, setLang] = useState<Language>("en");
 
124
  console.error("History load error", e);
125
  }
126
 
127
+ try {
128
+ const { fetchSearchHistory } = await import("../lib/api");
129
+ const sHistory = await fetchSearchHistory();
130
+ setSearchHistory(sHistory);
131
+ } catch (e) {
132
+ console.error("Search history load error", e);
133
+ }
134
+
135
  try {
136
  const initialTenders = await searchTenders({ status: 'activas' });
137
  setTenders(initialTenders);
 
166
  results = await searchTenders(params);
167
  }
168
  setTenders(results);
169
+ // Log search to history
170
+ if (params.keyword || params.code) {
171
+ const { saveSearchHistory, fetchSearchHistory } = await import("../lib/api");
172
+ await saveSearchHistory(params.keyword || params.code || "Active Tenders", results.length, params.isAgile);
173
+ const sHistory = await fetchSearchHistory();
174
+ setSearchHistory(sHistory);
175
+ }
176
  } catch (error) {
177
  console.error("Search error:", error);
178
  }
 
322
  />
323
  )}
324
  {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
325
+ {activeTab === "History" && <AnalysisHistory history={analysisHistory} searchHistory={searchHistory} />}
326
  {activeTab === "About" && <SystemInfo />}
327
  </div>
328
  </main>
frontend/components/AgentAnalysis.tsx CHANGED
@@ -565,7 +565,18 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
565
  <div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-500 scroll-mt-20">
566
  <div className="lg:col-span-8 space-y-8">
567
  <div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
568
- <div className="flex items-start justify-between mb-8">
 
 
 
 
 
 
 
 
 
 
 
569
  <div>
570
  <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
571
  <h3 className="text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
 
565
  <div id="analysis-results" className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-500 scroll-mt-20">
566
  <div className="lg:col-span-8 space-y-8">
567
  <div className="glass-card rounded-3xl p-10 bg-white/[0.02]">
568
+ + {/* Professional Print Header */}
569
+ + <div className="hidden print-only mb-12 border-b-4 border-slate-900 pb-8 text-center">
570
+ + <h1 className="text-4xl font-black text-slate-900 mb-2">ANDESOPS AI</h1>
571
+ + <p className="text-sm font-bold uppercase tracking-[0.5em] text-slate-500">Intelligent Bidding Analysis Report</p>
572
+ + <div className="mt-6 flex justify-between text-[10px] font-mono text-slate-400">
573
+ + <span>DATE: {new Date().toLocaleDateString()}</span>
574
+ + <span>REF ID: {tender?.code}</span>
575
+ + <span>CONFIDENTIAL - FOR INTERNAL USE ONLY</span>
576
+ + </div>
577
+ + </div>
578
+ +
579
+ <div className="flex items-start justify-between mb-8">
580
  <div>
581
  <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-purple-400 mb-2">Agent Consensus</div>
582
  <h3 className="text-6xl font-black text-white">{activeAnalysis.fit_score}% <span className="text-2xl font-light text-slate-500">Fit Score</span></h3>
frontend/components/AnalysisHistory.tsx CHANGED
@@ -3,6 +3,7 @@ import type { AnalysisHistoryItem } from "../lib/types";
3
 
4
  type Props = {
5
  history: AnalysisHistoryItem[];
 
6
  };
7
 
8
  export default function AnalysisHistory({ history }: Props) {
@@ -14,6 +15,8 @@ export default function AnalysisHistory({ history }: Props) {
14
  );
15
  };
16
 
 
 
17
  if (!history.length) {
18
  return (
19
  <div className="flex flex-col items-center justify-center min-h-[40vh] glass-card rounded-3xl border border-white/5 p-12 text-center animate-in fade-in duration-700">
@@ -26,7 +29,25 @@ export default function AnalysisHistory({ history }: Props) {
26
 
27
  return (
28
  <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
29
- <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  {history.map((item) => {
31
  const itemId = `${item.tender_code}-${item.analyzed_at}`;
32
  const isExpanded = expandedItems.includes(itemId);
@@ -98,7 +119,18 @@ export default function AnalysisHistory({ history }: Props) {
98
 
99
  {isExpanded && (
100
  <div className="mt-8 p-6 rounded-3xl bg-black/40 border border-white/5 animate-in slide-in-from-top-4 duration-500">
101
- <h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4">Agent Intelligence Log (Full Audit)</h4>
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  {item.analysis.raw_responses && (
104
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
@@ -132,7 +164,40 @@ export default function AnalysisHistory({ history }: Props) {
132
  </div>
133
  );
134
  })}
135
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
  );
138
  }
 
3
 
4
  type Props = {
5
  history: AnalysisHistoryItem[];
6
+ searchHistory?: any[];
7
  };
8
 
9
  export default function AnalysisHistory({ history }: Props) {
 
15
  );
16
  };
17
 
18
+ const [activeHistoryTab, setActiveHistoryTab] = useState<"Analysis" | "Searches">("Analysis");
19
+
20
  if (!history.length) {
21
  return (
22
  <div className="flex flex-col items-center justify-center min-h-[40vh] glass-card rounded-3xl border border-white/5 p-12 text-center animate-in fade-in duration-700">
 
29
 
30
  return (
31
  <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
32
+ <div className="flex items-center justify-between mb-4">
33
+ <div className="flex bg-white/5 p-1 rounded-2xl border border-white/10">
34
+ <button
35
+ onClick={() => setActiveHistoryTab("Analysis")}
36
+ className={`px-6 py-2 rounded-xl text-xs font-black uppercase transition-all ${activeHistoryTab === "Analysis" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}
37
+ >
38
+ Analysis Logs
39
+ </button>
40
+ <button
41
+ onClick={() => setActiveHistoryTab("Searches")}
42
+ className={`px-6 py-2 rounded-xl text-xs font-black uppercase transition-all ${activeHistoryTab === "Searches" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}
43
+ >
44
+ Search Audit
45
+ </button>
46
+ </div>
47
+ </div>
48
+
49
+ {activeHistoryTab === "Analysis" ? (
50
+ <div className="space-y-6">
51
  {history.map((item) => {
52
  const itemId = `${item.tender_code}-${item.analyzed_at}`;
53
  const isExpanded = expandedItems.includes(itemId);
 
119
 
120
  {isExpanded && (
121
  <div className="mt-8 p-6 rounded-3xl bg-black/40 border border-white/5 animate-in slide-in-from-top-4 duration-500">
122
+ {/* Professional Print Header */}
123
+ <div className="hidden print-only mb-12 border-b-4 border-slate-900 pb-8 text-center">
124
+ <h1 className="text-4xl font-black text-slate-900 mb-2">ANDESOPS AI</h1>
125
+ <p className="text-sm font-bold uppercase tracking-[0.5em] text-slate-500">Historical Audit Report</p>
126
+ <div className="mt-6 flex justify-between text-[10px] font-mono text-slate-400">
127
+ <span>DATE: {new Date().toLocaleDateString()}</span>
128
+ <span>REF ID: {item.tender_code}</span>
129
+ <span>ORIGINAL ANALYSIS: {new Date(item.analyzed_at).toLocaleString()}</span>
130
+ </div>
131
+ </div>
132
+
133
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4 no-print">Agent Intelligence Log (Full Audit)</h4>
134
 
135
  {item.analysis.raw_responses && (
136
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
 
164
  </div>
165
  );
166
  })}
167
+ </div>
168
+ ) : (
169
+ <div className="glass-card rounded-3xl overflow-hidden border border-white/5">
170
+ <table className="w-full text-left text-sm">
171
+ <thead className="bg-white/5 text-slate-500 uppercase text-[10px] font-bold border-b border-white/5">
172
+ <tr>
173
+ <th className="px-6 py-5">Search Query</th>
174
+ <th className="px-6 py-5">Results</th>
175
+ <th className="px-6 py-5">Type</th>
176
+ <th className="px-6 py-5 text-right">Timestamp</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody className="divide-y divide-white/5">
180
+ {searchHistory?.map((s, idx) => (
181
+ <tr key={idx} className="hover:bg-white/[0.04] transition-colors">
182
+ <td className="px-6 py-5 font-bold text-white">{s.query}</td>
183
+ <td className="px-6 py-5 text-cyan font-mono">{s.results_count}</td>
184
+ <td className="px-6 py-5">
185
+ <span className={`px-2 py-0.5 rounded text-[10px] font-black uppercase ${s.is_agile ? 'bg-green-500/10 text-green-400' : 'bg-white/5 text-slate-500'}`}>
186
+ {s.is_agile ? "Agile" : "Standard"}
187
+ </span>
188
+ </td>
189
+ <td className="px-6 py-5 text-right text-slate-500 text-xs">{new Date(s.searched_at).toLocaleString()}</td>
190
+ </tr>
191
+ ))}
192
+ {!searchHistory?.length && (
193
+ <tr>
194
+ <td colSpan={4} className="px-6 py-12 text-center text-slate-600 italic">No search logs recorded yet.</td>
195
+ </tr>
196
+ )}
197
+ </tbody>
198
+ </table>
199
+ </div>
200
+ )}
201
  </div>
202
  );
203
  }
frontend/globals.css CHANGED
@@ -109,3 +109,72 @@ textarea,
109
  select {
110
  font: inherit;
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  select {
110
  font: inherit;
111
  }
112
+ /* Professional PDF Print Styles for AndesOps AI reports */
113
+ @media print {
114
+ @page {
115
+ margin: 2cm;
116
+ size: A4;
117
+ }
118
+
119
+ body {
120
+ background: white !important;
121
+ color: black !important;
122
+ font-family: 'Inter', system-ui, sans-serif !important;
123
+ }
124
+
125
+ .glass-card {
126
+ background: white !important;
127
+ border: 1px solid #e2e8f0 !important;
128
+ box-shadow: none !important;
129
+ backdrop-filter: none !important;
130
+ page-break-inside: avoid;
131
+ margin-bottom: 20px !important;
132
+ color: black !important;
133
+ }
134
+
135
+ .premium-gradient,
136
+ .bg-purple-600,
137
+ .bg-cyan {
138
+ background: #f8fafc !important;
139
+ color: black !important;
140
+ border: 1px solid #000 !important;
141
+ }
142
+
143
+ .text-white,
144
+ .text-slate-300,
145
+ .text-slate-400,
146
+ .text-purple-400,
147
+ .text-cyan {
148
+ color: black !important;
149
+ }
150
+
151
+ /* Hide UI elements */
152
+ nav,
153
+ aside,
154
+ footer,
155
+ button,
156
+ .no-print {
157
+ display: none !important;
158
+ }
159
+
160
+ /* Force display of hidden elements in print */
161
+ .print-only {
162
+ display: block !important;
163
+ }
164
+
165
+ /* Professional spacing */
166
+ h1, h2, h3, h4 {
167
+ color: #1e293b !important;
168
+ margin-top: 1.5rem !important;
169
+ margin-bottom: 0.75rem !important;
170
+ }
171
+
172
+ .prose {
173
+ color: #334155 !important;
174
+ line-height: 1.6 !important;
175
+ }
176
+
177
+ .border-white\/5 {
178
+ border-color: #e2e8f0 !important;
179
+ }
180
+ }
frontend/lib/api.ts CHANGED
@@ -167,6 +167,25 @@ export async function fetchAnalysisHistory(): Promise<AnalysisHistoryItem[]> {
167
  return res.json();
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  export async function syncDatabase() {
171
  const res = await fetch(`${API_BASE}/api/tenders/sync`, { method: "POST" });
172
  if (!res.ok) {
 
167
  return res.json();
168
  }
169
 
170
+ export async function saveSearchHistory(query: string, resultsCount: number, isAgile: boolean = false) {
171
+ return fetch(`${API_BASE}/api/search-history`, {
172
+ method: "POST",
173
+ headers: jsonHeaders,
174
+ body: JSON.stringify({
175
+ query,
176
+ results_count: resultsCount,
177
+ searched_at: new Date().toISOString(),
178
+ is_agile: isAgile
179
+ })
180
+ });
181
+ }
182
+
183
+ export async function fetchSearchHistory(): Promise<any[]> {
184
+ const res = await fetch(`${API_BASE}/api/search-history`);
185
+ if (!res.ok) return [];
186
+ return res.json();
187
+ }
188
+
189
  export async function syncDatabase() {
190
  const res = await fetch(`${API_BASE}/api/tenders/sync`, { method: "POST" });
191
  if (!res.ok) {