Spaces:
Sleeping
Sleeping
Naprawa wykresów, Dagbladet RSS, usunięcie Nettavisen
Browse files- TruthScan AI_backend/app/config.py +1 -1
- TruthScan AI_frontend/app/dashboard/components/ChartsSection.tsx +29 -15
- TruthScan AI_frontend/app/dashboard/components/SourceComparison.tsx +1 -1
- TruthScan AI_frontend/app/dashboard/page.tsx +2 -2
- TruthScan AI_frontend/app/hooks/useDashboardCharts.ts +111 -46
- TruthScan AI_frontend/app/hooks/useNewsCache.ts +21 -13
- TruthScan AI_frontend/app/hooks/useNewsStream.ts +3 -2
- TruthScan AI_frontend/app/stores/newsCache.tsx +4 -2
- TruthScan AI_frontend/components/SourceSelector.tsx +1 -1
- TruthScan AI_frontend/lib/fetchNews.ts +10 -22
TruthScan AI_backend/app/config.py
CHANGED
|
@@ -36,7 +36,7 @@ NEWS_FEEDS = {
|
|
| 36 |
# Norweskie
|
| 37 |
"NRK": "https://www.nrk.no/toppsaker.rss",
|
| 38 |
"VG": "https://www.vg.no/rss/feed/?limit=10",
|
| 39 |
-
"
|
| 40 |
"Aftenposten": "https://www.aftenposten.no/rss",
|
| 41 |
}
|
| 42 |
|
|
|
|
| 36 |
# Norweskie
|
| 37 |
"NRK": "https://www.nrk.no/toppsaker.rss",
|
| 38 |
"VG": "https://www.vg.no/rss/feed/?limit=10",
|
| 39 |
+
"E24": "https://e24.no/rss",
|
| 40 |
"Aftenposten": "https://www.aftenposten.no/rss",
|
| 41 |
}
|
| 42 |
|
TruthScan AI_frontend/app/dashboard/components/ChartsSection.tsx
CHANGED
|
@@ -73,11 +73,17 @@ export default function ChartsSection({
|
|
| 73 |
|
| 74 |
useEffect(() => {
|
| 75 |
if (!isClient || hasLoadedRef.current) return;
|
| 76 |
-
|
| 77 |
const el = chartsSectionRef.current;
|
| 78 |
if (!el) return;
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
console.log("✅ Używam cache dla wykresów");
|
| 82 |
return;
|
| 83 |
}
|
|
@@ -162,19 +168,27 @@ export default function ChartsSection({
|
|
| 162 |
const effectiveEmotionData = cachedCharts?.emotionData ?? emotionData;
|
| 163 |
const effectiveTotalSources = cachedCharts?.totalSources ?? totalSources;
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
const
|
| 174 |
-
const
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
const t = locales[language] ?? locales.pl;
|
| 180 |
|
|
|
|
| 73 |
|
| 74 |
useEffect(() => {
|
| 75 |
if (!isClient || hasLoadedRef.current) return;
|
| 76 |
+
|
| 77 |
const el = chartsSectionRef.current;
|
| 78 |
if (!el) return;
|
| 79 |
|
| 80 |
+
// Traktuj cache jako ważny tylko gdy zawiera dane
|
| 81 |
+
const cacheIsValid = !!(
|
| 82 |
+
cachedCharts &&
|
| 83 |
+
(cachedCharts.barData.length > 0 || cachedCharts.emotionData.length > 0)
|
| 84 |
+
);
|
| 85 |
+
|
| 86 |
+
if (cacheIsValid) {
|
| 87 |
console.log("✅ Używam cache dla wykresów");
|
| 88 |
return;
|
| 89 |
}
|
|
|
|
| 168 |
const effectiveEmotionData = cachedCharts?.emotionData ?? emotionData;
|
| 169 |
const effectiveTotalSources = cachedCharts?.totalSources ?? totalSources;
|
| 170 |
|
| 171 |
+
// Warunek oparty wyłącznie na długości tablicy — wartości 0 nie blokują wykresu
|
| 172 |
+
const effectiveHasBars = effectiveBarData.length > 0;
|
| 173 |
+
const effectiveHasEmos = effectiveEmotionData.length > 0;
|
| 174 |
+
|
| 175 |
+
const cacheIsValid = !!(
|
| 176 |
+
cachedCharts &&
|
| 177 |
+
(cachedCharts.barData.length > 0 || cachedCharts.emotionData.length > 0)
|
| 178 |
+
);
|
| 179 |
+
const effectiveChartsStarted = cacheIsValid ? true : chartsStarted;
|
| 180 |
+
const effectiveChartsLoading = cacheIsValid ? false : chartsLoading;
|
| 181 |
+
const effectiveProgressCount = cacheIsValid ? effectiveTotalSources : progressCount;
|
| 182 |
+
const effectiveProgressPct = cacheIsValid ? 100 : progressPct;
|
| 183 |
+
|
| 184 |
+
console.log("[ChartsSection] render →", {
|
| 185 |
+
cachedCharts: !!cachedCharts,
|
| 186 |
+
barData: barData.length, emotionData: emotionData.length,
|
| 187 |
+
effectiveBarData: effectiveBarData.length, effectiveEmotionData: effectiveEmotionData.length,
|
| 188 |
+
effectiveHasBars, effectiveHasEmos,
|
| 189 |
+
effectiveChartsStarted, effectiveChartsLoading,
|
| 190 |
+
progressCount, effectiveProgressCount,
|
| 191 |
+
});
|
| 192 |
|
| 193 |
const t = locales[language] ?? locales.pl;
|
| 194 |
|
TruthScan AI_frontend/app/dashboard/components/SourceComparison.tsx
CHANGED
|
@@ -20,7 +20,7 @@ interface Props {
|
|
| 20 |
const ALL_SOURCES = [
|
| 21 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 22 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 23 |
-
"NRK", "VG", "
|
| 24 |
] as const;
|
| 25 |
|
| 26 |
export default function SourceComparison({ language }: Props) {
|
|
|
|
| 20 |
const ALL_SOURCES = [
|
| 21 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 22 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 23 |
+
"NRK", "VG", "E24", "Aftenposten",
|
| 24 |
] as const;
|
| 25 |
|
| 26 |
export default function SourceComparison({ language }: Props) {
|
TruthScan AI_frontend/app/dashboard/page.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import { useNewsCache, newsKey } from "../../app/stores/newsCache";
|
|
| 15 |
const ALL_SOURCES = [
|
| 16 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 17 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 18 |
-
"NRK", "VG", "
|
| 19 |
] as const;
|
| 20 |
|
| 21 |
export default function DashboardPage() {
|
|
@@ -37,7 +37,7 @@ export default function DashboardPage() {
|
|
| 37 |
|
| 38 |
const handleRefresh = () => {
|
| 39 |
ALL_SOURCES.forEach((src) => {
|
| 40 |
-
cache.del(newsKey(src
|
| 41 |
});
|
| 42 |
cache.del("dashboard:charts");
|
| 43 |
setReloadKey((k) => k + 1);
|
|
|
|
| 15 |
const ALL_SOURCES = [
|
| 16 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 17 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 18 |
+
"NRK", "VG", "E24", "Aftenposten",
|
| 19 |
] as const;
|
| 20 |
|
| 21 |
export default function DashboardPage() {
|
|
|
|
| 37 |
|
| 38 |
const handleRefresh = () => {
|
| 39 |
ALL_SOURCES.forEach((src) => {
|
| 40 |
+
cache.del(newsKey(src));
|
| 41 |
});
|
| 42 |
cache.del("dashboard:charts");
|
| 43 |
setReloadKey((k) => k + 1);
|
TruthScan AI_frontend/app/hooks/useDashboardCharts.ts
CHANGED
|
@@ -1,31 +1,37 @@
|
|
| 1 |
/**
|
| 2 |
* Hook odpowiedzialny za pobieranie i agregację danych do wykresów dashboardu.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
-
import { useCallback, useRef, useState } from "react";
|
| 6 |
-
import { fetchOneSource
|
|
|
|
|
|
|
| 7 |
import type { Lang } from "../../lib/types";
|
| 8 |
|
| 9 |
const ALL_SOURCES = [
|
| 10 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 11 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 12 |
-
"NRK", "VG", "
|
| 13 |
] as const;
|
| 14 |
|
|
|
|
|
|
|
| 15 |
export function useDashboardCharts(language: Lang) {
|
| 16 |
-
const [barData, setBarData]
|
| 17 |
const [emotionData, setEmotionData] = useState<{ name: string; value: number }[]>([]);
|
| 18 |
const [progressCount, setProgressCount] = useState(0);
|
| 19 |
const [chartsLoading, setChartsLoading] = useState(false);
|
| 20 |
-
const [chartsError, setChartsError]
|
| 21 |
-
|
| 22 |
-
// Struktury w useRef – pozwalają aktualizować dane bez nadmiarowych re-renderów
|
| 23 |
-
const barsMapRef = useRef<Map<string, number>>(new Map());
|
| 24 |
-
const emosMapRef = useRef<Record<string, number>>({
|
| 25 |
-
POSITIVE: 0, NEGATIVE: 0, NEUTRAL: 0
|
| 26 |
-
});
|
| 27 |
-
const chartsStartedRef = useRef(false);
|
| 28 |
|
|
|
|
| 29 |
const progressPct = Math.round((progressCount / ALL_SOURCES.length) * 100);
|
| 30 |
|
| 31 |
const normalizeSent = (s: string): "POSITIVE" | "NEGATIVE" | "NEUTRAL" => {
|
|
@@ -36,62 +42,121 @@ export function useDashboardCharts(language: Lang) {
|
|
| 36 |
return "NEUTRAL";
|
| 37 |
};
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
const emoArr = [
|
| 48 |
-
{ name: "POSITIVE", value:
|
| 49 |
-
{ name: "NEUTRAL",
|
| 50 |
-
{ name: "NEGATIVE", value:
|
| 51 |
];
|
|
|
|
|
|
|
|
|
|
| 52 |
setEmotionData(emoArr);
|
| 53 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
const loadCharts = useCallback(async () => {
|
| 56 |
-
if (chartsStartedRef.current)
|
|
|
|
|
|
|
|
|
|
| 57 |
chartsStartedRef.current = true;
|
| 58 |
setChartsLoading(true);
|
| 59 |
setChartsError(null);
|
| 60 |
setProgressCount(0);
|
| 61 |
-
barsMapRef.current.clear();
|
| 62 |
-
emosMapRef.current = { POSITIVE: 0, NEGATIVE: 0, NEUTRAL: 0 };
|
| 63 |
|
| 64 |
-
console.log(`
|
|
|
|
|
|
|
| 65 |
|
| 66 |
const promises = ALL_SOURCES.map(async (src) => {
|
| 67 |
try {
|
| 68 |
-
const
|
| 69 |
|
| 70 |
-
if (
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
const normalized = normalizeSent(emotionKey);
|
| 76 |
-
emosMapRef.current[normalized] = (emosMapRef.current[normalized] || 0) + (count as number);
|
| 77 |
-
}
|
| 78 |
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
} catch (error) {
|
| 81 |
-
console.warn(`
|
| 82 |
-
// Ustaw 0 dla niedostępnych źródeł tak żeby słupek się pojawił
|
| 83 |
-
barsMapRef.current.set(src, 0);
|
| 84 |
} finally {
|
| 85 |
-
// Zawsze aktualizuj stan — nawet przy błędzie żeby wykresy się pokazały
|
| 86 |
-
refreshChartsFromRefs();
|
| 87 |
setProgressCount((c) => c + 1);
|
| 88 |
}
|
| 89 |
});
|
| 90 |
|
| 91 |
await Promise.allSettled(promises);
|
| 92 |
setChartsLoading(false);
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
|
| 96 |
return {
|
| 97 |
barData,
|
|
@@ -103,7 +168,7 @@ export function useDashboardCharts(language: Lang) {
|
|
| 103 |
chartsStarted: chartsStartedRef.current,
|
| 104 |
loadCharts,
|
| 105 |
totalSources: ALL_SOURCES.length,
|
| 106 |
-
hasBars:
|
| 107 |
-
hasEmos:
|
| 108 |
};
|
| 109 |
-
}
|
|
|
|
| 1 |
/**
|
| 2 |
* Hook odpowiedzialny za pobieranie i agregację danych do wykresów dashboardu.
|
| 3 |
+
*
|
| 4 |
+
* Architektura reaktywna:
|
| 5 |
+
* 1. Subskrypcja Zustand (useNewsCache) — wykresy aktualizują się automatycznie
|
| 6 |
+
* gdy SSE lub HTTP fallback zapisze artykuły do cache.
|
| 7 |
+
* 2. loadCharts() pobiera przez HTTP źródła nieobecne w cache i zapisuje artykuły;
|
| 8 |
+
* subskrypcja wyłapuje zapis i przelicza wykresy.
|
| 9 |
+
*
|
| 10 |
+
* Dzięki temu fake_probability pochodzi zawsze z tych samych artykułów co stream SSE.
|
| 11 |
*/
|
| 12 |
|
| 13 |
+
import { useCallback, useEffect, useRef, useState } from "react";
|
| 14 |
+
import { fetchOneSource } from "../../lib/fetchNews";
|
| 15 |
+
import { useNewsCache, newsKey } from "../../app/stores/newsCache";
|
| 16 |
+
import type { Article } from "../../lib/fetchNews";
|
| 17 |
import type { Lang } from "../../lib/types";
|
| 18 |
|
| 19 |
const ALL_SOURCES = [
|
| 20 |
"BBC", "CNN", "NYTimes", "Guardian", "AlJazeera",
|
| 21 |
"Money", "PolsatNews", "TVN24", "SpidersWeb", "Bankier",
|
| 22 |
+
"NRK", "VG", "E24", "Aftenposten",
|
| 23 |
] as const;
|
| 24 |
|
| 25 |
+
const SOURCE_ORDER = new Map(ALL_SOURCES.map((s, i) => [s as string, i]));
|
| 26 |
+
|
| 27 |
export function useDashboardCharts(language: Lang) {
|
| 28 |
+
const [barData, setBarData] = useState<{ label: string; value: number }[]>([]);
|
| 29 |
const [emotionData, setEmotionData] = useState<{ name: string; value: number }[]>([]);
|
| 30 |
const [progressCount, setProgressCount] = useState(0);
|
| 31 |
const [chartsLoading, setChartsLoading] = useState(false);
|
| 32 |
+
const [chartsError, setChartsError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
const chartsStartedRef = useRef(false);
|
| 35 |
const progressPct = Math.round((progressCount / ALL_SOURCES.length) * 100);
|
| 36 |
|
| 37 |
const normalizeSent = (s: string): "POSITIVE" | "NEGATIVE" | "NEUTRAL" => {
|
|
|
|
| 42 |
return "NEUTRAL";
|
| 43 |
};
|
| 44 |
|
| 45 |
+
/**
|
| 46 |
+
* Przelicza barData i emotionData na podstawie tego co aktualnie jest w cache.
|
| 47 |
+
* Wywoływana przy każdej zmianie store (subskrypcja) oraz po mount.
|
| 48 |
+
*/
|
| 49 |
+
const recomputeFromCache = useCallback(() => {
|
| 50 |
+
const cacheState = useNewsCache.getState();
|
| 51 |
+
const barsMap = new Map<string, number>();
|
| 52 |
+
const emosMap = { POSITIVE: 0, NEGATIVE: 0, NEUTRAL: 0 };
|
| 53 |
+
|
| 54 |
+
for (const src of ALL_SOURCES) {
|
| 55 |
+
const articles = cacheState.get<Article[]>(newsKey(src));
|
| 56 |
+
if (!articles || articles.length === 0) continue;
|
| 57 |
+
|
| 58 |
+
// DEBUG: typ i wartość fake_probability pierwszego artykułu
|
| 59 |
+
const first = articles[0];
|
| 60 |
+
console.log(
|
| 61 |
+
`[CHARTS] [${src}] article[0].fake_probability:`,
|
| 62 |
+
typeof first.fake_probability, "=", first.fake_probability,
|
| 63 |
+
"| sentiment:", first.sentiment
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
const avgFake =
|
| 67 |
+
articles.reduce((sum, a) => {
|
| 68 |
+
const num = Number(a.fake_probability);
|
| 69 |
+
return sum + (Number.isFinite(num) ? num : 0);
|
| 70 |
+
}, 0) / articles.length;
|
| 71 |
+
|
| 72 |
+
console.log(`[CHARTS] [${src}] avgFake=${avgFake.toFixed(4)} (${articles.length} art)`);
|
| 73 |
+
barsMap.set(src, avgFake);
|
| 74 |
+
|
| 75 |
+
for (const art of articles) {
|
| 76 |
+
if (art.sentiment) {
|
| 77 |
+
const norm = normalizeSent(art.sentiment);
|
| 78 |
+
emosMap[norm] = (emosMap[norm] || 0) + 1;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if (barsMap.size === 0) return; // nic w cache — nie nadpisuj pustą tablicą
|
| 84 |
+
|
| 85 |
+
const arr = Array.from(barsMap.entries())
|
| 86 |
+
.map(([label, value]) => ({ label, value }))
|
| 87 |
+
.sort((a, b) => (SOURCE_ORDER.get(a.label) ?? 999) - (SOURCE_ORDER.get(b.label) ?? 999));
|
| 88 |
|
| 89 |
const emoArr = [
|
| 90 |
+
{ name: "POSITIVE", value: emosMap.POSITIVE },
|
| 91 |
+
{ name: "NEUTRAL", value: emosMap.NEUTRAL },
|
| 92 |
+
{ name: "NEGATIVE", value: emosMap.NEGATIVE },
|
| 93 |
];
|
| 94 |
+
|
| 95 |
+
console.log(`[CHARTS] recompute → bars=${arr.length}`, arr.map(x => `${x.label}:${x.value.toFixed(1)}`));
|
| 96 |
+
setBarData(arr);
|
| 97 |
setEmotionData(emoArr);
|
| 98 |
+
}, []);
|
| 99 |
+
|
| 100 |
+
/** Subskrypcja Zustand — reaguje na każdą zmianę cache (SSE lub HTTP fallback). */
|
| 101 |
+
useEffect(() => {
|
| 102 |
+
// Przelicz raz od razu (cache może już mieć dane z poprzedniej sesji)
|
| 103 |
+
recomputeFromCache();
|
| 104 |
+
|
| 105 |
+
const unsub = useNewsCache.subscribe(() => {
|
| 106 |
+
recomputeFromCache();
|
| 107 |
+
});
|
| 108 |
|
| 109 |
+
return () => unsub();
|
| 110 |
+
}, [recomputeFromCache]);
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Pobiera przez HTTP artykuły dla źródeł nieobecnych w cache
|
| 114 |
+
* i zapisuje je do store — co wyzwala subskrypcję i przeliczenie wykresów.
|
| 115 |
+
*/
|
| 116 |
const loadCharts = useCallback(async () => {
|
| 117 |
+
if (chartsStartedRef.current) {
|
| 118 |
+
console.log("[CHARTS] loadCharts: już uruchomione, pomijam");
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
chartsStartedRef.current = true;
|
| 122 |
setChartsLoading(true);
|
| 123 |
setChartsError(null);
|
| 124 |
setProgressCount(0);
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
console.log(`[CHARTS] start: ${ALL_SOURCES.length} źródeł, lang=${language}`);
|
| 127 |
+
|
| 128 |
+
const cacheState = useNewsCache.getState();
|
| 129 |
|
| 130 |
const promises = ALL_SOURCES.map(async (src) => {
|
| 131 |
try {
|
| 132 |
+
const cached = cacheState.get<Article[]>(newsKey(src));
|
| 133 |
|
| 134 |
+
if (cached && cached.length > 0) {
|
| 135 |
+
console.log(`[CHARTS] [${src}] → CACHE HIT (${cached.length} art)`);
|
| 136 |
+
// recomputeFromCache działa reaktywnie; nic więcej nie trzeba robić
|
| 137 |
+
} else {
|
| 138 |
+
console.log(`[CHARTS] [${src}] → CACHE MISS → HTTP`);
|
| 139 |
+
const { articles } = await fetchOneSource(src, language);
|
| 140 |
|
| 141 |
+
console.log(`[CHARTS] [${src}] HTTP: ${articles.length} art`);
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
if (articles.length > 0) {
|
| 144 |
+
// Zapis do cache wyzwala subskrypcję → recomputeFromCache()
|
| 145 |
+
useNewsCache.getState().set(newsKey(src), articles);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
} catch (error) {
|
| 149 |
+
console.warn(`[CHARTS] [${src}] błąd:`, error);
|
|
|
|
|
|
|
| 150 |
} finally {
|
|
|
|
|
|
|
| 151 |
setProgressCount((c) => c + 1);
|
| 152 |
}
|
| 153 |
});
|
| 154 |
|
| 155 |
await Promise.allSettled(promises);
|
| 156 |
setChartsLoading(false);
|
| 157 |
+
|
| 158 |
+
console.log("[CHARTS] gotowe. barsMap rozmiar po HTTP:", barData.length);
|
| 159 |
+
}, [language, barData.length]);
|
| 160 |
|
| 161 |
return {
|
| 162 |
barData,
|
|
|
|
| 168 |
chartsStarted: chartsStartedRef.current,
|
| 169 |
loadCharts,
|
| 170 |
totalSources: ALL_SOURCES.length,
|
| 171 |
+
hasBars: barData.length > 0,
|
| 172 |
+
hasEmos: emotionData.length > 0,
|
| 173 |
};
|
| 174 |
+
}
|
TruthScan AI_frontend/app/hooks/useNewsCache.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
/**
|
| 2 |
* Hook pobierający listę newsów dla wybranego źródła.
|
| 3 |
* Wykorzystuje cache po stronie klienta w celu ograniczenia liczby zapytań do API.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
import { useEffect, useState } from "react";
|
|
@@ -13,13 +17,15 @@ type Article = {
|
|
| 13 |
summary: string;
|
| 14 |
published: string;
|
| 15 |
source: string;
|
| 16 |
-
sentiment: string;
|
| 17 |
fake_probability: number;
|
| 18 |
sentiment_score?: number;
|
| 19 |
};
|
| 20 |
|
| 21 |
type NewsResponse = { source: string; articles: Article[] };
|
| 22 |
|
|
|
|
|
|
|
| 23 |
export function useCachedNews(source: string, lang: Lang = "pl", model: string = "roberta") {
|
| 24 |
const [data, setData] = useState<Article[] | null>(null);
|
| 25 |
const [loading, setLoading] = useState<boolean>(true);
|
|
@@ -28,9 +34,9 @@ export function useCachedNews(source: string, lang: Lang = "pl", model: string =
|
|
| 28 |
|
| 29 |
useEffect(() => {
|
| 30 |
let mounted = true;
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
// Odczyt danych z cache przed wykonaniem zapytania HTTP
|
| 34 |
const cached = cache.get<Article[]>(key);
|
| 35 |
if (cached) {
|
| 36 |
setData(cached);
|
|
@@ -41,14 +47,16 @@ export function useCachedNews(source: string, lang: Lang = "pl", model: string =
|
|
| 41 |
(async () => {
|
| 42 |
try {
|
| 43 |
setLoading(true);
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 46 |
const json: NewsResponse = await res.json();
|
| 47 |
const articles = json.articles ?? [];
|
| 48 |
if (!mounted) return;
|
| 49 |
setData(articles);
|
| 50 |
-
|
| 51 |
-
// Zapis danych w cache po poprawnym pobraniu
|
| 52 |
cache.set(key, articles);
|
| 53 |
} catch (e: any) {
|
| 54 |
if (!mounted) return;
|
|
@@ -58,19 +66,19 @@ export function useCachedNews(source: string, lang: Lang = "pl", model: string =
|
|
| 58 |
}
|
| 59 |
})();
|
| 60 |
|
| 61 |
-
return () => {
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
};
|
| 65 |
-
}, [source, lang, model]);
|
| 66 |
|
| 67 |
const refresh = async () => {
|
| 68 |
-
const key = newsKey(source,
|
| 69 |
cache.del(key);
|
| 70 |
setData(null);
|
| 71 |
setLoading(true);
|
| 72 |
try {
|
| 73 |
-
const res = await fetch(
|
|
|
|
|
|
|
| 74 |
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 75 |
const json: NewsResponse = await res.json();
|
| 76 |
const articles = json.articles ?? [];
|
|
|
|
| 1 |
/**
|
| 2 |
* Hook pobierający listę newsów dla wybranego źródła.
|
| 3 |
* Wykorzystuje cache po stronie klienta w celu ograniczenia liczby zapytań do API.
|
| 4 |
+
*
|
| 5 |
+
* Klucz cache zawiera tylko source i model — NIE lang.
|
| 6 |
+
* Etykiety sentymentu są tłumaczone po stronie frontendu przez useSentiment,
|
| 7 |
+
* więc zmiana języka UI nie wymusza ponownego pobierania artykułów.
|
| 8 |
*/
|
| 9 |
|
| 10 |
import { useEffect, useState } from "react";
|
|
|
|
| 17 |
summary: string;
|
| 18 |
published: string;
|
| 19 |
source: string;
|
| 20 |
+
sentiment: string;
|
| 21 |
fake_probability: number;
|
| 22 |
sentiment_score?: number;
|
| 23 |
};
|
| 24 |
|
| 25 |
type NewsResponse = { source: string; articles: Article[] };
|
| 26 |
|
| 27 |
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000";
|
| 28 |
+
|
| 29 |
export function useCachedNews(source: string, lang: Lang = "pl", model: string = "roberta") {
|
| 30 |
const [data, setData] = useState<Article[] | null>(null);
|
| 31 |
const [loading, setLoading] = useState<boolean>(true);
|
|
|
|
| 34 |
|
| 35 |
useEffect(() => {
|
| 36 |
let mounted = true;
|
| 37 |
+
// Klucz cache bez lang — artykuły są language-agnostic po stronie cache
|
| 38 |
+
const key = newsKey(source, model);
|
| 39 |
|
|
|
|
| 40 |
const cached = cache.get<Article[]>(key);
|
| 41 |
if (cached) {
|
| 42 |
setData(cached);
|
|
|
|
| 47 |
(async () => {
|
| 48 |
try {
|
| 49 |
setLoading(true);
|
| 50 |
+
// Lang przekazywany do backendu (wpływa na format dat itp.),
|
| 51 |
+
// ale nie jest częścią klucza cache
|
| 52 |
+
const res = await fetch(
|
| 53 |
+
`${API_URL}/news/${encodeURIComponent(source)}?lang=${lang}&model=${model}`
|
| 54 |
+
);
|
| 55 |
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 56 |
const json: NewsResponse = await res.json();
|
| 57 |
const articles = json.articles ?? [];
|
| 58 |
if (!mounted) return;
|
| 59 |
setData(articles);
|
|
|
|
|
|
|
| 60 |
cache.set(key, articles);
|
| 61 |
} catch (e: any) {
|
| 62 |
if (!mounted) return;
|
|
|
|
| 66 |
}
|
| 67 |
})();
|
| 68 |
|
| 69 |
+
return () => { mounted = false; };
|
| 70 |
+
// lang pominięty z deps — zmiana języka nie triggeruje re-fetch
|
| 71 |
+
}, [source, model]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
| 72 |
|
| 73 |
const refresh = async () => {
|
| 74 |
+
const key = newsKey(source, model);
|
| 75 |
cache.del(key);
|
| 76 |
setData(null);
|
| 77 |
setLoading(true);
|
| 78 |
try {
|
| 79 |
+
const res = await fetch(
|
| 80 |
+
`${API_URL}/news/${encodeURIComponent(source)}?lang=${lang}&model=${model}`
|
| 81 |
+
);
|
| 82 |
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 83 |
const json: NewsResponse = await res.json();
|
| 84 |
const articles = json.articles ?? [];
|
TruthScan AI_frontend/app/hooks/useNewsStream.ts
CHANGED
|
@@ -32,7 +32,8 @@ export function useNewsStream(source: string, lang: Lang = "pl", limit: number =
|
|
| 32 |
useEffect(() => {
|
| 33 |
if (!source) return;
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
const cached = cache.get<Article[]>(cacheKey);
|
| 37 |
|
| 38 |
// Jeżeli dane są w cache, pomijamy połączenie SSE
|
|
@@ -100,7 +101,7 @@ export function useNewsStream(source: string, lang: Lang = "pl", limit: number =
|
|
| 100 |
setState((s) => ({ ...s, loading: false }));
|
| 101 |
// Zapis pełnego wyniku do cache po zakończeniu strumienia
|
| 102 |
if (collected.length > 0) {
|
| 103 |
-
cache.set(cacheKey, collected);
|
| 104 |
}
|
| 105 |
es.close();
|
| 106 |
};
|
|
|
|
| 32 |
useEffect(() => {
|
| 33 |
if (!source) return;
|
| 34 |
|
| 35 |
+
// Klucz cache bez lang — etykiety tłumaczone przez useSentiment po stronie UI
|
| 36 |
+
const cacheKey = newsKey(source);
|
| 37 |
const cached = cache.get<Article[]>(cacheKey);
|
| 38 |
|
| 39 |
// Jeżeli dane są w cache, pomijamy połączenie SSE
|
|
|
|
| 101 |
setState((s) => ({ ...s, loading: false }));
|
| 102 |
// Zapis pełnego wyniku do cache po zakończeniu strumienia
|
| 103 |
if (collected.length > 0) {
|
| 104 |
+
cache.set(cacheKey, collected); // zapisane bez lang w kluczu
|
| 105 |
}
|
| 106 |
es.close();
|
| 107 |
};
|
TruthScan AI_frontend/app/stores/newsCache.tsx
CHANGED
|
@@ -84,7 +84,9 @@ export const useNewsCache = create<NewsCacheState>((set, get) => ({
|
|
| 84 |
},
|
| 85 |
}));
|
| 86 |
|
| 87 |
-
// helper: klucz cache dla /news
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
// helper: klucz cache dla /emotion-stats
|
| 90 |
export const statsKey = (source: string) => `stats:${source}`;
|
|
|
|
| 84 |
},
|
| 85 |
}));
|
| 86 |
|
| 87 |
+
// helper: klucz cache dla /news — lang celowo pominięty, bo etykiety
|
| 88 |
+
// sentymentu są tłumaczone po stronie frontendu przez useSentiment.
|
| 89 |
+
// Zmiana języka UI nie powinna wymuszać ponownego pobrania artykułów.
|
| 90 |
+
export const newsKey = (source: string, model = "roberta") => `news:${source}:${model}`;
|
| 91 |
// helper: klucz cache dla /emotion-stats
|
| 92 |
export const statsKey = (source: string) => `stats:${source}`;
|
TruthScan AI_frontend/components/SourceSelector.tsx
CHANGED
|
@@ -45,7 +45,7 @@ export default function SourceSelector({ selectedSource, setSelectedSource, lang
|
|
| 45 |
<optgroup label="🇳🇴 Norsk">
|
| 46 |
<option value="NRK">NRK</option>
|
| 47 |
<option value="VG">VG</option>
|
| 48 |
-
<option value="
|
| 49 |
<option value="Aftenposten">Aftenposten</option>
|
| 50 |
</optgroup>
|
| 51 |
</select>
|
|
|
|
| 45 |
<optgroup label="🇳🇴 Norsk">
|
| 46 |
<option value="NRK">NRK</option>
|
| 47 |
<option value="VG">VG</option>
|
| 48 |
+
<option value="E24">E24</option>
|
| 49 |
<option value="Aftenposten">Aftenposten</option>
|
| 50 |
</optgroup>
|
| 51 |
</select>
|
TruthScan AI_frontend/lib/fetchNews.ts
CHANGED
|
@@ -71,7 +71,7 @@ const SOURCE_MAP: Record<string, string> = {
|
|
| 71 |
bankier: "Bankier",
|
| 72 |
nrk: "NRK",
|
| 73 |
vg: "VG",
|
| 74 |
-
|
| 75 |
aftenposten: "Aftenposten",
|
| 76 |
};
|
| 77 |
|
|
@@ -97,15 +97,16 @@ function normalizeArticle(raw: any, sourceLabel: string): Article {
|
|
| 97 |
export async function fetchOneSource(
|
| 98 |
source: string,
|
| 99 |
language: "pl" | "en" | "no" = "pl"
|
| 100 |
-
): Promise<{ news?: NewsData; emotions: Emotions }> {
|
| 101 |
const src = normalizeSource(source);
|
| 102 |
const emotions: Emotions = {};
|
|
|
|
| 103 |
|
| 104 |
try {
|
| 105 |
const controller = new AbortController();
|
| 106 |
-
const timeoutId = setTimeout(() => controller.abort(),
|
| 107 |
const res = await fetch(
|
| 108 |
-
`
|
| 109 |
{ signal: controller.signal }
|
| 110 |
);
|
| 111 |
clearTimeout(timeoutId);
|
|
@@ -115,35 +116,22 @@ export async function fetchOneSource(
|
|
| 115 |
const rawArticles = Array.isArray(data?.articles) ? (data.articles as any[]) : [];
|
| 116 |
const articles = rawArticles.map((a) => normalizeArticle(a, src));
|
| 117 |
|
| 118 |
-
// DEBUG: logi pomocnicze używane podczas walidacji danych z backendu
|
| 119 |
-
console.log(`📊 Source: ${src}, Articles: ${articles.length}`);
|
| 120 |
-
articles.forEach((article, index) => {
|
| 121 |
-
console.log(` Article ${index + 1}:`, {
|
| 122 |
-
title: article.title?.substring(0, 50) + '...',
|
| 123 |
-
sentiment: article.sentiment,
|
| 124 |
-
fake_probability: article.fake_probability
|
| 125 |
-
});
|
| 126 |
-
});
|
| 127 |
-
|
| 128 |
if (!articles.length) {
|
| 129 |
-
return { news: { source: src, fake_news: "0" }, emotions };
|
| 130 |
}
|
| 131 |
|
| 132 |
const avgFake =
|
| 133 |
-
articles.reduce((sum: number, a: any) => sum + (a.fake_probability
|
| 134 |
-
|
| 135 |
|
| 136 |
for (const art of articles) {
|
| 137 |
if (art.sentiment) emotions[art.sentiment] = (emotions[art.sentiment] ?? 0) + 1;
|
| 138 |
}
|
| 139 |
|
| 140 |
-
|
| 141 |
-
console.log(`🎭 Emotions for ${src}:`, emotions);
|
| 142 |
-
|
| 143 |
-
return { news: { source: src, fake_news: avgFake.toFixed(2) }, emotions };
|
| 144 |
} catch (err) {
|
| 145 |
console.warn(`❌ Błąd źródła: ${src}`, err);
|
| 146 |
-
return { news: { source: src, fake_news: "0" }, emotions };
|
| 147 |
}
|
| 148 |
}
|
| 149 |
|
|
|
|
| 71 |
bankier: "Bankier",
|
| 72 |
nrk: "NRK",
|
| 73 |
vg: "VG",
|
| 74 |
+
e24: "E24",
|
| 75 |
aftenposten: "Aftenposten",
|
| 76 |
};
|
| 77 |
|
|
|
|
| 97 |
export async function fetchOneSource(
|
| 98 |
source: string,
|
| 99 |
language: "pl" | "en" | "no" = "pl"
|
| 100 |
+
): Promise<{ news?: NewsData; emotions: Emotions; articles: Article[] }> {
|
| 101 |
const src = normalizeSource(source);
|
| 102 |
const emotions: Emotions = {};
|
| 103 |
+
const apiBase = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000";
|
| 104 |
|
| 105 |
try {
|
| 106 |
const controller = new AbortController();
|
| 107 |
+
const timeoutId = setTimeout(() => controller.abort(), 45_000);
|
| 108 |
const res = await fetch(
|
| 109 |
+
`${apiBase}/news/${src}?lang=${language}`,
|
| 110 |
{ signal: controller.signal }
|
| 111 |
);
|
| 112 |
clearTimeout(timeoutId);
|
|
|
|
| 116 |
const rawArticles = Array.isArray(data?.articles) ? (data.articles as any[]) : [];
|
| 117 |
const articles = rawArticles.map((a) => normalizeArticle(a, src));
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
if (!articles.length) {
|
| 120 |
+
return { news: { source: src, fake_news: "0" }, emotions, articles: [] };
|
| 121 |
}
|
| 122 |
|
| 123 |
const avgFake =
|
| 124 |
+
articles.reduce((sum: number, a: any) => sum + (Number(a.fake_probability) || 0), 0) /
|
| 125 |
+
articles.length;
|
| 126 |
|
| 127 |
for (const art of articles) {
|
| 128 |
if (art.sentiment) emotions[art.sentiment] = (emotions[art.sentiment] ?? 0) + 1;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
return { news: { source: src, fake_news: avgFake.toFixed(2) }, emotions, articles };
|
|
|
|
|
|
|
|
|
|
| 132 |
} catch (err) {
|
| 133 |
console.warn(`❌ Błąd źródła: ${src}`, err);
|
| 134 |
+
return { news: { source: src, fake_news: "0" }, emotions, articles: [] };
|
| 135 |
}
|
| 136 |
}
|
| 137 |
|