AndesOps-AI / frontend /app /page.tsx
Álvaro Valenzuela Valdes
deploy: clean base64 build for hf v7
4801010
"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) => {
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>
);
}