NewsAnalyze / src /components /ContinuousLiveStreamWidget.tsx
GeoStrat-CI
Deploy 5924376 to Hugging Face Space [skip ci]
2c0e275
Raw History Blame Contribute Delete
12.3 kB
import React, { useState, useEffect, useRef } from "react";
import { LiveWireArticle, fetchLiveWireArticles } from "../connectors/news_client.js";
import { useLanguage } from "../context/LanguageContext.js";
import {
Radio,
RefreshCw,
Zap,
Clock,
Globe,
TrendingUp,
ShieldCheck,
Pause,
Play,
ArrowRight,
Flame,
Layers,
ChevronRight,
ChevronLeft,
} from "lucide-react";
interface ContinuousLiveStreamWidgetProps {
onAnalyzeNews: (headline: string, countryIso3: string) => void;
onOpenFullWire: () => void;
isStreaming: boolean;
}
export const ContinuousLiveStreamWidget: React.FC<ContinuousLiveStreamWidgetProps> = ({
onAnalyzeNews,
onOpenFullWire,
isStreaming,
}) => {
const { language } = useLanguage();
const isArabic = language === "ar";
const [articles, setArticles] = useState<LiveWireArticle[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [loading, setLoading] = useState(false);
const [isAutoSync, setIsAutoSync] = useState(true);
const [countdown, setCountdown] = useState(25);
const [lastUpdated, setLastUpdated] = useState<string>("");
const timerRef = useRef<NodeJS.Timeout | null>(null);
const loadStream = async (cat = selectedCategory, silent = false) => {
if (!silent) setLoading(true);
try {
const targetCountry = cat === "local" || cat === "saudi_gulf" ? "SAU" : "GLOBAL";
const data = await fetchLiveWireArticles(cat, targetCountry);
if (Array.isArray(data) && data.length > 0) {
setArticles(data);
setLastUpdated(new Date().toLocaleTimeString(isArabic ? "ar-SA" : "en-US"));
}
} catch {
// Keep existing data on error
} finally {
if (!silent) setLoading(false);
}
};
useEffect(() => {
loadStream(selectedCategory, false);
setCountdown(25);
}, [selectedCategory]);
// Continuous auto-sync cycle every 25 seconds
useEffect(() => {
if (timerRef.current) clearInterval(timerRef.current);
if (isAutoSync) {
timerRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
loadStream(selectedCategory, true);
return 25;
}
return prev - 1;
});
}, 1000);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isAutoSync, selectedCategory]);
const categories = [
{ id: "all", label: isArabic ? "🌐 كافة التدفقات" : "🌐 All Wires", icon: Globe },
{ id: "sovereign_decisions", label: isArabic ? "🏛️ قرارات سيادية" : "🏛️ Sovereign", icon: ShieldCheck },
{ id: "economic", label: isArabic ? "📊 اقتصاد وأسواق" : "📊 Economic", icon: TrendingUp },
{ id: "political", label: isArabic ? "⚖️ سياسي ودولي" : "⚖️ Geopolitics", icon: Radio },
{ id: "energy_markets", label: isArabic ? "🛢️ أسواق الطاقة" : "🛢️ Energy", icon: Flame },
];
const getSourceBadgeColor = (source: string) => {
if (source.includes("SPA") || source.includes("واس")) {
return "bg-emerald-950/80 text-emerald-300 border-emerald-700/60";
}
if (source.includes("الشرق") || source.includes("بلومبرغ")) {
return "bg-sky-950/80 text-sky-300 border-sky-700/60";
}
if (source.includes("أرقام")) {
return "bg-amber-950/80 text-amber-300 border-amber-700/60";
}
if (source.includes("العربية")) {
return "bg-indigo-950/80 text-indigo-300 border-indigo-700/60";
}
if (source.includes("Reuters") || source.includes("رويترز")) {
return "bg-orange-950/80 text-orange-300 border-orange-700/60";
}
if (source.includes("Axios")) {
return "bg-purple-950/80 text-purple-300 border-purple-700/60";
}
if (source.includes("BBC")) {
return "bg-rose-950/80 text-rose-300 border-rose-700/60";
}
return "bg-slate-800 text-slate-300 border-slate-700";
};
const displayedArticles = articles.slice(0, 6);
return (
<div className="bg-slate-900/90 border border-slate-800 rounded-xl p-4 sm:p-5 shadow-lg relative overflow-hidden mb-6">
{/* Top Header Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b border-slate-800">
<div className="flex items-center gap-2.5 flex-wrap">
<div className="flex items-center gap-1.5 bg-emerald-950/70 border border-emerald-800 text-emerald-400 font-mono text-[11px] font-bold px-2 py-0.5 rounded">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
<span>{isArabic ? "تغذية إخبارية مستمرة" : "CONTINUOUS STREAM"}</span>
</div>
<h2 className="text-sm font-bold text-white font-display flex items-center gap-1.5">
<Radio className="w-4 h-4 text-cyan-400" />
<span>{isArabic ? "التدفق الإخباري اللحظي للقرارات السيادية والأسواق" : "Real-Time Sovereign & Macro Wire Stream"}</span>
</h2>
{lastUpdated && (
<span className="text-[11px] font-mono text-slate-400 flex items-center gap-1">
<Clock className="w-3 h-3 text-slate-500" />
<span>{lastUpdated}</span>
</span>
)}
</div>
{/* Sync Controls */}
<div className="flex items-center gap-2 shrink-0">
<div className="flex items-center gap-1.5 text-[11px] font-mono bg-slate-950/80 px-2.5 py-1 rounded border border-slate-800 text-slate-300">
<button
onClick={() => setIsAutoSync(!isAutoSync)}
className="flex items-center gap-1 text-slate-400 hover:text-cyan-300 transition-colors cursor-pointer"
title={isAutoSync ? (isArabic ? "إيقاف التحديث المستمر" : "Pause Auto-Sync") : (isArabic ? "تشغيل التحديث المستمر" : "Resume Auto-Sync")}
>
{isAutoSync ? (
<>
<Pause className="w-3 h-3 text-cyan-400" />
<span className="text-cyan-400">{countdown}s</span>
</>
) : (
<>
<Play className="w-3 h-3 text-amber-400" />
<span className="text-amber-400">{isArabic ? "متوقف" : "Paused"}</span>
</>
)}
</button>
</div>
<button
onClick={() => loadStream(selectedCategory, false)}
disabled={loading}
className="p-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-cyan-300 rounded border border-slate-700 transition-all cursor-pointer disabled:opacity-40"
title={isArabic ? "تحديث يدوي فوري" : "Force Refresh"}
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin text-cyan-400" : ""}`} />
</button>
<button
onClick={onOpenFullWire}
className="flex items-center gap-1 text-[11px] font-mono text-cyan-400 hover:text-cyan-300 bg-cyan-950/50 hover:bg-cyan-950 border border-cyan-800/80 px-2.5 py-1 rounded transition-all cursor-pointer"
>
<span>{isArabic ? "رصد كامل" : "Full Feed"}</span>
<ChevronRight className="w-3 h-3 rtl:rotate-180" />
</button>
</div>
</div>
{/* Category Filter Pills */}
<div className="flex items-center gap-1.5 overflow-x-auto py-2.5 scrollbar-none">
{categories.map((cat) => {
const active = selectedCategory === cat.id;
return (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`px-2.5 py-1 rounded text-[11px] font-mono shrink-0 transition-all cursor-pointer ${
active
? "bg-cyan-500/20 text-cyan-300 border border-cyan-500/50 font-semibold"
: "bg-slate-950/60 text-slate-400 hover:text-slate-200 border border-slate-800/80 hover:bg-slate-800/60"
}`}
>
{cat.label}
</button>
);
})}
</div>
{/* Live Articles Stream Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2.5 mt-1">
{loading && articles.length === 0 ? (
Array.from({ length: 3 }).map((_, idx) => (
<div
key={idx}
className="bg-slate-950/60 border border-slate-800/60 rounded-lg p-3 animate-pulse space-y-2"
>
<div className="h-4 bg-slate-800 rounded w-1/3"></div>
<div className="h-5 bg-slate-800 rounded w-full"></div>
<div className="h-3 bg-slate-800 rounded w-2/3"></div>
</div>
))
) : displayedArticles.length > 0 ? (
displayedArticles.map((article) => (
<div
key={article.id}
className="bg-slate-950/70 hover:bg-slate-950 border border-slate-800/80 hover:border-cyan-800/60 rounded-lg p-3 transition-all flex flex-col justify-between group shadow-sm"
>
<div>
<div className="flex items-center justify-between gap-2 mb-1.5">
<span
className={`text-[10px] font-mono px-2 py-0.5 rounded border font-semibold ${getSourceBadgeColor(
article.source
)}`}
>
{article.source}
</span>
<span className="text-[10px] font-mono text-slate-400 flex items-center gap-1">
<Clock className="w-2.5 h-2.5 text-slate-500" />
{article.formatted_date}
</span>
</div>
<h3
className="text-xs font-semibold text-slate-200 group-hover:text-cyan-300 transition-colors line-clamp-2 leading-snug cursor-pointer mb-1.5"
onClick={() => onAnalyzeNews(article.title, article.country_iso3)}
title={article.title}
>
{article.title}
</h3>
{article.summary && article.summary !== article.title && (
<p className="text-[11px] text-slate-400 line-clamp-2 leading-relaxed mb-3">
{article.summary}
</p>
)}
</div>
{/* Instant Launch Action Button */}
<div className="pt-2 border-t border-slate-800/60 flex items-center justify-between mt-auto">
<span className="text-[10px] font-mono text-slate-400">
{article.country_iso3 !== "GLOBAL" ? `ISO: ${article.country_iso3}` : "🌍 GLOBAL"}
</span>
<button
onClick={() => onAnalyzeNews(article.title, article.country_iso3)}
disabled={isStreaming}
className="flex items-center gap-1 px-2.5 py-1 rounded bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-300 hover:text-cyan-200 border border-cyan-500/30 hover:border-cyan-400 font-mono text-[11px] font-semibold transition-all cursor-pointer disabled:opacity-40"
title={isArabic ? "تشغيل النموذج واستشراف الأثر فوراً" : "Run instant forecast on this event"}
>
<Zap className="w-3 h-3 text-cyan-400 fill-cyan-400/30" />
<span>{isArabic ? "تحليل فوري" : "Forecast"}</span>
</button>
</div>
</div>
))
) : (
<div className="col-span-full py-8 text-center text-xs font-mono text-slate-400 bg-slate-950/40 rounded-lg border border-slate-800">
{isArabic ? "لا توجد أخبار جديدة في هذا التصنيف حالياً" : "No live articles found in this category"}
</div>
)}
</div>
</div>
);
};