import { useState, useEffect, useRef } from "react"; import { debounce } from "lodash"; import type { Project, OrganizationLocation, FilterState, ChatMessage,AvailableFilters } from "./types"; interface Stats { [key: string]: { labels: string[]; values: number[]; }; } type SortOrder = "asc" | "desc"; export const useAppState = () => { const [projects, setProjects] = useState([]); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [page, setPage] = useState(0); const [question, setQuestion] = useState(""); const [selectedProject, setSelectedProject] = useState(null); const [stats, setStats] = useState({}); const [legalFilter, setLegalFilter] = useState(''); const [orgFilter, setOrgFilter] = useState(''); const [countryFilter, setCountryFilter] = useState(''); const [fundingSchemeFilter, setFundingSchemeFilter ] = useState(''); const [idFilter, setIdFilter] = useState(''); const [topicsFilter, setTopicsFilter] = useState(''); const [sortField, setSortField] = useState(''); const [sortOrder, setSortOrder] = useState("asc"); const [filters, setFilters] = useState({ status: "", organization: "", country: "", legalBasis: "", topic: "", fundingScheme: "", minYear: "2000", maxYear: "2025", minEndYear: "2000", maxEndYear: "2035", minFunding: "0", maxFunding: "10000000", }); const [chatHistory, setChatHistory] = useState([]); const [loading, setLoading] = useState(false); const [availableFilters, setAvailableFilters] = useState({ statuses: ["SIGNED", "CLOSED", "TERMINATED","UNKNOWN"], organizations: [], countries: [], legalBases: [], fundingSchemes:[], ids:[], topics: [] }); const messagesEndRef = useRef(null); const fetchProjects = () => { fetch(`/api/projects?page=${page}&search=${encodeURIComponent(search)}&status=${statusFilter}&legalBasis=${legalFilter}&organization=${orgFilter}&country=${countryFilter}&fundingScheme=${fundingSchemeFilter}&proj_id=${idFilter}&topic=${topicsFilter}&sortField=${sortField}&sortOrder=${sortOrder}`) .then(res => res.json()) .then((data: Project[]) => setProjects(data)) .catch(console.error); }; const fetchStats = debounce((filters: FilterState) => { const params = new URLSearchParams(filters); fetch(`/api/stats?${params.toString()}`) .then(res => res.json()) .then((data: Stats) => setStats(data)) .catch(console.error); }, 500); const fetchAvailableFilters = (filters: FilterState) => { const params = new URLSearchParams(filters); fetch(`/api/filters?${params.toString()}`) .then(res => res.json()) .then((data: Omit) => { setAvailableFilters({ statuses: ["SIGNED", "CLOSED", "TERMINATED", "UNKNOWN"], organizations: data.organizations, countries: data.countries, legalBases: data.legalBases, fundingSchemes: data.fundingSchemes, ids: [], topics: data.topics }); }); }; interface RagResponse { answer: string; source_ids: string[]; } const askChatbot = async () => { if (!question.trim() || loading) return; const newChat: ChatMessage[] = [ ...chatHistory, { role: "user", content: question }, ]; setChatHistory(newChat); setQuestion(""); setLoading(true); // 1) placeholder setChatHistory((h) => [ ...h, { role: "assistant", content: "Generating answer..." }, ]); try { const res = await fetch("/api/rag", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: question }), }); const text = await res.text(); if (!res.ok) { let errDetail = text; try { errDetail = JSON.parse(text).detail; } catch {} throw new Error(errDetail); } const data: RagResponse = JSON.parse(text); const idList = data.source_ids.join(", ") || "none"; const assistantContent = `${data.answer} The output was based on the following Project IDs: ${idList}`; // 2) replace placeholder with real answer setChatHistory((h) => [ ...h.slice(0, -1), { role: "assistant", content: assistantContent }, ]); } catch (err: any) { // replace placeholder with error message setChatHistory((h) => [ ...h.slice(0, -1), { role: "assistant", content: `Something went wrong: ${err.message}`, }, ]); } finally { setLoading(false); // scroll to bottom messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } }; useEffect(() => { // If the user has typed something but it's too short, don't refetch fetchProjects(); }, [ page, search, statusFilter, legalFilter, orgFilter, countryFilter, fundingSchemeFilter, idFilter, sortField, sortOrder, ]); useEffect(() => { console.log("Updated filters:", filters); fetchStats(filters); }, [filters]); useEffect(() => fetchAvailableFilters(filters), [filters]); return { selectedProject, dashboardProps: { stats, filters, setFilters, availableFilters }, explorerProps: { projects, search, setSearch, statusFilter, setStatusFilter, legalFilter, setLegalFilter, orgFilter, setOrgFilter, countryFilter, setCountryFilter, fundingSchemeFilter, setFundingSchemeFilter, idFilter, setIdFilter, topicsFilter, setTopicsFilter, setSortField, sortField, setSortOrder, sortOrder, page, setPage, setSelectedProject, question, setQuestion, chatHistory, setChatHistory, askChatbot, loading, messagesEndRef }, detailsProps: { project: selectedProject!, question, setQuestion, chatHistory, askChatbot, loading, messagesEndRef } }; };