File size: 16,669 Bytes
ad8d299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
"use client";

import { useEffect, useMemo, useState, useRef } from "react";
import Dashboard from "../components/Dashboard";
import TenderSearch from "../components/TenderSearch";
import CompanyProfile from "../components/CompanyProfile";
import AgentAnalysis from "../components/AgentAnalysis";
import ProposalDraft from "../components/ProposalDraft";
import Reports from "../components/Reports";
import Sidebar from "../components/Sidebar";
import AnalysisHistory from "../components/AnalysisHistory";
import GlobalSync from "../components/GlobalSync";
import MarketMonitor from "../components/MarketMonitor";
import SystemInfo from "../components/SystemInfo";
import DBManager from "../components/DBManager";
import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
import { translations, Language } from "../lib/translations";

const tabs = [
  "Dashboard",
  "Tender Search",
  "My Portfolio",
  "Market Monitor",
  "Company Profile",
  "Agent Analysis",
  "Proposal Draft",
  "History",
  "Database",
  "About",
] as const;

type Tab = (typeof tabs)[number];

export default function HomePage() {
  const [activeTab, setActiveTab] = useState<Tab>("Dashboard");
  const [showSync, setShowSync] = useState(true);
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
  const [tenders, setTenders] = useState<Tender[]>([]);
  const [selectedTender, setSelectedTender] = useState<Tender | null>(null);
  const [companyProfile, setCompanyProfile] = useState<CompanyProfileType>({
    name: "Andes Digital",
    industry: "Software development",
    services: ["AI automation", "web apps", "data dashboards"],
    experience: "5 years building enterprise software",
    certifications: [],
    regions: ["Metropolitana", "Valparaíso"],
    documents_available: ["RUT", "Portfolio", "Financial Statements"],
    keywords: ["software", "IA", "automatización"],
  });
  const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
  const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
  const [searchHistory, setSearchHistory] = useState<any[]>([]);
  const [status, setStatus] = useState("listening");
  const [searchKeyword, setSearchKeyword] = useState("");
  const [lang, setLang] = useState<Language>("en");
  const [followedCount, setFollowedCount] = useState(0);
  const contentRef = useRef<HTMLDivElement>(null);

  // Sync followed count from localStorage
  useEffect(() => {
    const updateFollowed = () => {
      const saved = localStorage.getItem('andes_followed_tenders_full');
      if (saved) {
        setFollowedCount(JSON.parse(saved).length);
      }
    };
    updateFollowed();
    window.addEventListener('storage', updateFollowed);
    // Also poll slightly for local changes if needed
    const interval = setInterval(updateFollowed, 2000);
    return () => {
      window.removeEventListener('storage', updateFollowed);
      clearInterval(interval);
    };
  }, []);
  
  const t = translations[lang];

  const handleGlobalSyncComplete = useMemo(() => () => setShowSync(false), []);
  // Scroll to top when tab changes
  useEffect(() => {
    // Force immediate scroll
    window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
    if (contentRef.current) {
      contentRef.current.scrollTo({ top: 0, left: 0, behavior: 'instant' });
    }
    
    // Safety delay for async renders
    const timer = setTimeout(() => {
      window.scrollTo(0, 0);
      if (contentRef.current) contentRef.current.scrollTo(0, 0);
    }, 100);
    
    return () => clearTimeout(timer);
  }, [activeTab]);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      const params = new URLSearchParams(window.location.search);
      const tabParam = params.get('tab');
      if (tabParam) {
        const foundTab = tabs.find(t => t.toLowerCase().replace(/ /g, "_") === tabParam);
        if (foundTab) setActiveTab(foundTab);
      }
    }

    async function init(retries = 3) {
      try {
        await healthCheck();
        setStatus("connected");
      } catch (e) {
        console.error("Connection attempt failed", e);
        if (retries > 0) {
          setStatus("listening"); // Show trying state
          setTimeout(() => init(retries - 1), 3000);
          return;
        }
        setStatus("offline");
      }

      try {
        const profile = await fetchCompanyProfile();
        if (profile) {
          // HYBRID PERSISTENCE CHECK: If backend is default but we have a local backup, restore it
          const localBackup = localStorage.getItem('andes_profile_backup');
          if (profile.name === "Andes Digital" && localBackup) {
            console.log("!!! PERSISTENCE: Restoring profile from local backup !!!");
            const backupData = JSON.parse(localBackup);
            await saveCompanyProfile(backupData);
            setCompanyProfile(backupData);
          } else {
            setCompanyProfile(profile);
            // Update backup if we got fresh real data
            if (profile.name !== "Andes Digital") {
              localStorage.setItem('andes_profile_backup', JSON.stringify(profile));
            }
          }
        }
      } catch (e) {
        console.error("Profile load error", e);
      }

      try {
        let history = await fetchAnalysisHistory();
        if (history.length === 0) {
          const localHistory = localStorage.getItem('andes_analysis_history_backup');
          if (localHistory) history = JSON.parse(localHistory);
        }
        setAnalysisHistory(history);
      } catch (e) {
        console.error("History load error", e);
      }

      try {
        const { fetchSearchHistory } = await import("../lib/api");
        let sHistory = await fetchSearchHistory();
        if (sHistory.length === 0) {
          const localSearch = localStorage.getItem('andes_search_history_backup');
          if (localSearch) sHistory = JSON.parse(localSearch);
        }
        setSearchHistory(sHistory);
      } catch (e) {
        console.error("Search history load error", e);
      }

      try {
        const initialTenders = await searchTenders({});
        setTenders(initialTenders);
      } catch (e) {
        console.error("Tenders load error", e);
      }
    }

    init();
  }, []);

  // Backup history to localStorage to survive HF Space restarts
  useEffect(() => {
    if (analysisHistory.length > 0) {
      localStorage.setItem('andes_analysis_history_backup', JSON.stringify(analysisHistory));
    }
  }, [analysisHistory]);

  useEffect(() => {
    if (searchHistory.length > 0) {
      localStorage.setItem('andes_search_history_backup', JSON.stringify(searchHistory));
    }
  }, [searchHistory]);

  const handleTenderSelect = (tender: Tender) => {
    setSelectedTender(tender);
    setActiveTab("Agent Analysis");
    window.history.pushState({}, '', `?tab=agent_analysis`);
  };

  const handleFilterClick = (type: "sector" | "region" | "buyer", value: string) => {
    setSearchKeyword(value);
    setActiveTab("Tender Search");
    if (type === "buyer") {
      handleSearch({ buyer: value });
    } else {
      handleSearch({ keyword: value });
    }
    window.history.pushState({}, '', `?tab=tender_search&q=${encodeURIComponent(value)}`);
  };

  const handleSearch = async (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; type_code?: string; skip?: number; limit?: number; isAgile?: boolean }) => {
    try {
      let results: Tender[];
      if (params.isAgile && params.keyword) {
        const { scrapeTenders } = await import("../lib/api");
        results = await scrapeTenders(params.keyword);
      } else {
        results = await searchTenders(params);
      }
      setTenders(results);
      // Log search to history
      if (params.keyword || params.code) {
        const { saveSearchHistory, fetchSearchHistory } = await import("../lib/api");
        await saveSearchHistory(params.keyword || params.code || "Active Tenders", results.length, params.isAgile);
        const sHistory = await fetchSearchHistory();
        setSearchHistory(sHistory);
      }
    } catch (error) {
      console.error("Search error:", error);
    }
  };

  const handleProfileSave = async (profile: CompanyProfileType) => {
    try {
      const savedProfile = await saveCompanyProfile(profile);
      setCompanyProfile(savedProfile);
      // Sync with localStorage for hybrid persistence
      localStorage.setItem('andes_profile_backup', JSON.stringify(savedProfile));
      console.log("!!! PERSISTENCE: Profile backed up to localStorage !!!");
    } catch {
      setCompanyProfile(profile);
    }
  };

  const handleRunAnalysis = async (documentText?: string, models?: Record<string, string>, tenderDetails?: any, amdSettings?: { url: string, key: string }) => {
    if (!selectedTender) return;
    const result = await analyzeTender(selectedTender, companyProfile, documentText, models, tenderDetails, amdSettings);
    setAnalysisResult(result);

    try {
      const history = await fetchAnalysisHistory();
      setAnalysisHistory(history);
    } catch (e) {
      console.error(e);
    }
  };

  return (
    <div className="min-h-screen selection:bg-cyan/30">
      {showSync && <GlobalSync onComplete={handleGlobalSyncComplete} />}
      
      {/* Mobile Header */}
      <div className="md:hidden flex items-center justify-between px-6 py-4 bg-black/40 backdrop-blur-lg border-b border-white/5 sticky top-0 z-50">
        <div className="flex items-center gap-2">
          <div className="w-8 h-8 premium-gradient rounded-lg flex items-center justify-center text-white font-bold text-sm shadow-lg shadow-purple-500/20">A</div>
          <span className="font-bold text-white text-sm tracking-tight">AndesOps AI</span>
        </div>
        <button 
          onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
          className="p-2 text-white bg-white/5 rounded-lg border border-white/10 active:scale-95 transition-all"
        >
          {isMobileMenuOpen ? "✕" : "☰"}
        </button>
      </div>

      <div className="flex flex-col md:flex-row min-h-screen gap-8 p-6 md:p-10">
        {/* Sidebar Container */}
        <div className={`${isMobileMenuOpen ? "fixed inset-0 z-[100] flex" : "hidden"} md:block md:w-[84px] md:shrink-0 md:sticky md:top-8 md:h-[calc(100vh-4rem)] transition-all duration-500 z-[100]`}>
          {isMobileMenuOpen && (
            <div className="absolute inset-0 bg-black/80 backdrop-blur-sm md:hidden" onClick={() => setIsMobileMenuOpen(false)} />
          )}
          <div className="relative z-10 w-72 h-full">
            <Sidebar
              tabs={tabs}
              activeTab={activeTab}
              onTabSelect={(tab) => {
                setActiveTab(tab);
                setIsMobileMenuOpen(false);
              }}
              status={status}
              lang={lang}
              forceExpanded={isMobileMenuOpen}
            />
          </div>
        </div>
        
        {/* Main Content */}
        <main className="flex-1 min-w-0 flex flex-col gap-8">
          {/* Dashboard Header */}
          <header className="flex flex-col md:flex-row md:items-center justify-between gap-4 px-2">
            <div>
              <h2 className="text-2xl md:text-3xl font-bold text-white mb-1">{activeTab}</h2>
              <p className="text-slate-500 text-xs md:text-sm">
                {activeTab === "Dashboard" && "Overview of your tender ecosystem."}
                {activeTab === "Tender Search" && "Explore new opportunities from the market."}
                {activeTab === "Agent Analysis" && "Deep-dive into tender documentation."}
                {activeTab === "My Portfolio" && "Manage your followed opportunities."}
              </p>
            </div>
            <div className="hidden md:flex items-center gap-6">
              {/* ESG Monitor */}
              <div className="flex flex-col items-end">
                <span className="text-[9px] font-black uppercase tracking-widest text-slate-500 mb-1">{t.esgScore}</span>
                <div className="flex gap-2">
                  <div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-green-500/10 border border-green-500/20">
                    <span className="text-[10px] font-bold text-green-400">E</span>
                    <span className="text-[10px] font-mono text-white">92</span>
                  </div>
                  <div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-blue-500/10 border border-blue-500/20">
                    <span className="text-[10px] font-bold text-blue-400">S</span>
                    <span className="text-[10px] font-mono text-white">85</span>
                  </div>
                  <div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-purple-500/10 border border-purple-500/20">
                    <span className="text-[10px] font-bold text-purple-400">G</span>
                    <span className="text-[10px] font-mono text-white">96</span>
                  </div>
                </div>
              </div>
              <div className="h-10 w-px bg-white/10" />
              <button 
                onClick={() => setLang(lang === "en" ? "es" : "en")}
                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 flex items-center gap-2"
              >
                <span>{lang === "en" ? "🇺🇸" : "🇪🇸"}</span>
                <span>{lang === "en" ? "English" : "Español"}</span>
              </button>
            </div>
          </header>

          {/* Content Area */}
          <div ref={contentRef} className="flex-1 overflow-y-auto pr-2 custom-scrollbar pb-12">
            {activeTab === "Dashboard" && (
              <Dashboard
                key={`dashboard-${activeTab}`}
                tendersFound={tenders.length}
                recommendedOpportunities={analysisResult?.decision === "Recommended" ? 1 : 0}
                highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
                reportsGenerated={analysisHistory.length}
                followedTendersCount={followedCount}
                tenders={tenders}
                onFilterClick={handleFilterClick}
                onTenderClick={handleTenderSelect}
                lang={lang}
              />
            )}
            {(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
              <TenderSearch
                tenders={tenders}
                onSearch={handleSearch}
                onAnalyze={handleTenderSelect}
                forceShowFollowed={activeTab === "My Portfolio"}
                initialKeyword={searchKeyword}
                lang={lang}
                companyProfile={companyProfile}
              />
            )}
            {activeTab === "Market Monitor" && <MarketMonitor />}
            {activeTab === "Company Profile" && (
              <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
            )}
            {activeTab === "Agent Analysis" && (
              <AgentAnalysis
                tender={selectedTender}
                companyProfile={companyProfile}
                analysis={analysisResult}
                onAnalyze={handleRunAnalysis}
                onBackToSearch={() => setActiveTab("Tender Search")}
              />
            )}
            {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
            {activeTab === "History" && <AnalysisHistory history={analysisHistory} searchHistory={searchHistory} />}
            {activeTab === "Database" && <DBManager onFilterClick={handleFilterClick} />}
            {activeTab === "About" && <SystemInfo lang={lang} />}
          </div>
        </main>
      </div>

      <footer className="py-8 border-t border-white/5 bg-black/20 text-center">
        <p className="text-[10px] font-bold uppercase tracking-[0.5em] text-slate-600 mb-2">
          Intelligence Orchestrated
        </p>
        <p className="text-xs text-slate-500 font-medium">
          AndesOps AI Enterprise 2026 | Powered by <a href="https://www.rew.cl" target="_blank" rel="noopener noreferrer" className="text-cyan hover:text-white transition-colors font-bold">REW</a>
        </p>
      </footer>
    </div>
  );
}