| "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); |
|
|
| |
| useEffect(() => { |
| const updateFollowed = () => { |
| const saved = localStorage.getItem('andes_followed_tenders_full'); |
| if (saved) { |
| setFollowedCount(JSON.parse(saved).length); |
| } |
| }; |
| updateFollowed(); |
| window.addEventListener('storage', updateFollowed); |
| |
| const interval = setInterval(updateFollowed, 2000); |
| return () => { |
| window.removeEventListener('storage', updateFollowed); |
| clearInterval(interval); |
| }; |
| }, []); |
| |
| const t = translations[lang]; |
|
|
| const handleGlobalSyncComplete = useMemo(() => () => setShowSync(false), []); |
| |
| useEffect(() => { |
| |
| window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); |
| if (contentRef.current) { |
| contentRef.current.scrollTo({ top: 0, left: 0, behavior: 'instant' }); |
| } |
| |
| |
| 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"); |
| setTimeout(() => init(retries - 1), 3000); |
| return; |
| } |
| setStatus("offline"); |
| } |
|
|
| try { |
| const profile = await fetchCompanyProfile(); |
| if (profile) { |
| |
| 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); |
| |
| 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(); |
| }, []); |
|
|
| |
| 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); |
| |
| 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); |
| |
| 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) => { |
| if (!selectedTender) return; |
| const result = await analyzeTender(selectedTender, companyProfile, documentText, models, tenderDetails); |
| 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 REW |
| </p> |
| </footer> |
| </div> |
| ); |
| } |
|
|