NewsAnalyze / src /components /DocumentLibraryView.tsx
GeoStrat-CI
Deploy 5924376 to Hugging Face Space [skip ci]
2c0e275
Raw History Blame Contribute Delete
6.95 kB
import React, { useState, useEffect } from "react";
import { DocumentChunk } from "../contracts/tool_outputs.js";
import { FileText, Search, ExternalLink, RefreshCw } from "lucide-react";
import { useLanguage } from "../context/LanguageContext.js";
import { getBackendUrl } from "../utils/api.js";
const COUNTRIES = ["SAU", "ARE", "EGY", "USA", "DEU", "JPN", "BRA", "CHN"];
export const DocumentLibraryView: React.FC = () => {
const { language, t } = useLanguage();
const isAr = language === "ar";
const [query, setQuery] = useState(isAr ? "الميزانية العامة والدين السيادي" : "sovereign debt budget");
const [debouncedQuery, setDebouncedQuery] = useState(query);
const [selectedCountry, setSelectedCountry] = useState<string>("SAU");
const [alpha, setAlpha] = useState(0.6);
const [documents, setDocuments] = useState<DocumentChunk[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
const id = setTimeout(() => setDebouncedQuery(query), 500);
return () => clearTimeout(id);
}, [query]);
useEffect(() => {
if (!debouncedQuery.trim()) return;
const controller = new AbortController();
setLoading(true);
setError("");
const params = new URLSearchParams({ query: debouncedQuery, alpha: String(alpha), country: selectedCountry });
fetch(`${getBackendUrl()}/api/rag/documents?${params}`, { signal: controller.signal })
.then(async (res) => {
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
setDocuments(Array.isArray(json) ? json : []);
})
.catch((err: Error) => {
if (err.name !== "AbortError") setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [debouncedQuery, selectedCountry, alpha]);
return (
<div className="space-y-6 text-slate-100">
<div className="bg-slate-900 border border-slate-800 rounded-xl p-6 shadow-xl">
<div className="flex items-center gap-3 mb-4 border-b border-slate-800 pb-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 text-purple-400 flex items-center justify-center shrink-0">
<FileText className="w-5 h-5" />
</div>
<div>
<h1 className="text-lg font-bold text-white font-display">{t.documents.title}</h1>
<p className="text-xs text-slate-400">{t.documents.subtitle}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 mb-6 p-4 bg-slate-950/80 rounded-xl border border-slate-800">
<div className="md:col-span-6">
<label className="text-[10px] font-mono text-slate-400 block mb-1">{isAr ? "عبارة البحث" : "Search query"}</label>
<div className="relative">
<Search className="w-4 h-4 text-slate-500 absolute start-3 top-2.5" />
<input
type="text"
value={query}
maxLength={1000}
onChange={(e) => setQuery(e.target.value)}
placeholder={t.documents.searchPlaceholder}
className="w-full bg-slate-900 border border-slate-800 rounded-lg ps-9 pe-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-purple-500"
/>
</div>
</div>
<div className="md:col-span-3">
<label className="text-[10px] font-mono text-slate-400 block mb-1">{isAr ? "الدولة" : "Country"}</label>
<select
value={selectedCountry}
onChange={(e) => setSelectedCountry(e.target.value)}
className="w-full bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono text-slate-200"
>
{COUNTRIES.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
<div className="md:col-span-3 flex flex-col justify-center">
<div className="flex items-center justify-between text-[10px] font-mono text-slate-400 mb-1">
<span>{isAr ? "وزن العنوان مقابل النص" : "Title vs body weight"}</span>
<span className="text-purple-300 font-bold">{alpha.toFixed(1)}</span>
</div>
<input
type="range"
min={0}
max={1}
step={0.1}
value={alpha}
onChange={(e) => setAlpha(parseFloat(e.target.value))}
className="w-full accent-purple-400 cursor-pointer"
/>
</div>
</div>
{loading && (
<div className="flex items-center gap-2 text-xs font-mono text-slate-400 mb-3">
<RefreshCw className="w-3.5 h-3.5 animate-spin" /> {isAr ? "جارٍ البحث..." : "Searching..."}
</div>
)}
{error && <div className="text-xs text-rose-400 mb-3">{error}</div>}
{!loading && !error && documents.length === 0 && (
<div className="p-8 text-center text-xs text-slate-500 bg-slate-950/60 rounded-xl border border-slate-800">{t.documents.noDocsFound}</div>
)}
<div className="space-y-3">
{documents.map((doc) => (
<a
key={doc.chunk_id}
href={doc.url || undefined}
target="_blank"
rel="noopener noreferrer"
className="block p-4 bg-slate-950/70 hover:bg-slate-800/60 border border-slate-800 hover:border-purple-500/50 rounded-xl transition-all group"
>
<div className="flex flex-wrap items-center justify-between gap-2 mb-2">
<div className="flex flex-wrap items-center gap-2">
<span className="px-2 py-0.5 bg-purple-950 text-purple-300 border border-purple-800/80 text-[10px] font-mono font-bold rounded">
[{doc.chunk_id}]
</span>
<span className="text-xs text-slate-400">{doc.source_authority}</span>
<span className="text-[11px] font-mono text-slate-500">
{new Date(doc.publication_timestamp).toLocaleDateString(isAr ? "ar-SA" : "en-US")}
</span>
</div>
<div className="text-xs font-mono text-purple-400 flex items-center gap-1">
<span>{isAr ? "الصلة" : "Relevance"}: {doc.score ?? 0}</span>
{doc.url && <ExternalLink className="w-3 h-3" />}
</div>
</div>
<h3 className="text-sm font-bold text-slate-200 group-hover:text-purple-300 transition-colors mb-1">{doc.title}</h3>
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">{doc.content}</p>
</a>
))}
</div>
</div>
</div>
);
};