Spaces:
Sleeping
Sleeping
Download src/components/ContinuousLiveStreamWidget.tsx from vooom/NewsAnalyze: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/vooom/NewsAnalyze/resolve/main/src/components/ContinuousLiveStreamWidget.tsx
- Command line
-
hf download hf://spaces/vooom/NewsAnalyze/src/components/ContinuousLiveStreamWidget.tsx
-
curl -L -o ContinuousLiveStreamWidget.tsx https://huggingface.co/spaces/vooom/NewsAnalyze/resolve/main/src/components/ContinuousLiveStreamWidget.tsx
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> | |
| ); | |
| }; | |