Bjornpool commited on
Commit
c347b35
·
1 Parent(s): 9d414c3

Naprawa wykresów, Dagbladet RSS, usunięcie Nettavisen

Browse files
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
- "TV2": "https://www.tv2.no/rss/alt",
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
- if (cachedCharts) {
 
 
 
 
 
 
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
- const effectiveHasBars = cachedCharts
166
- ? cachedCharts.barData.length > 0
167
- : hasBars;
168
- const effectiveHasEmos = cachedCharts
169
- ? cachedCharts.emotionData.length > 0
170
- : hasEmos;
171
-
172
- const effectiveChartsStarted = cachedCharts ? true : chartsStarted;
173
- const effectiveChartsLoading = cachedCharts ? false : chartsLoading;
174
- const effectiveProgressCount = cachedCharts
175
- ? effectiveTotalSources
176
- : progressCount;
177
- const effectiveProgressPct = cachedCharts ? 100 : progressPct;
 
 
 
 
 
 
 
 
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", "TV2", "Aftenposten",
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", "TV2", "Aftenposten",
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, language));
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, NewsData } from "../../lib/fetchNews";
 
 
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", "TV2", "Aftenposten",
13
  ] as const;
14
 
 
 
15
  export function useDashboardCharts(language: Lang) {
16
- const [barData, setBarData] = useState<{ label: string; value: number }[]>([]);
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] = useState<string | null>(null);
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
- const refreshChartsFromRefs = () => {
40
- const order = new Map(ALL_SOURCES.map((s, i) => [s as string, i]));
41
- const arr = Array.from(barsMapRef.current.entries()).map(([label, value]) => ({
42
- label, value,
43
- }));
44
- arr.sort((a, b) => (order.get(a.label) ?? 999) - (order.get(b.label) ?? 999));
45
- setBarData(arr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  const emoArr = [
48
- { name: "POSITIVE", value: emosMapRef.current.POSITIVE },
49
- { name: "NEUTRAL", value: emosMapRef.current.NEUTRAL },
50
- { name: "NEGATIVE", value: emosMapRef.current.NEGATIVE }
51
  ];
 
 
 
52
  setEmotionData(emoArr);
53
- };
 
 
 
 
 
 
 
 
 
54
 
 
 
 
 
 
 
 
55
  const loadCharts = useCallback(async () => {
56
- if (chartsStartedRef.current) return;
 
 
 
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(`📊 Ładuję wykresy: ${ALL_SOURCES.length} źródeł, lang=${language}`);
 
 
65
 
66
  const promises = ALL_SOURCES.map(async (src) => {
67
  try {
68
- const { news, emotions } = await fetchOneSource(src, language);
69
 
70
- if (news) {
71
- barsMapRef.current.set(news.source, parseFloat(news.fake_news));
72
- }
 
 
 
73
 
74
- for (const [emotionKey, count] of Object.entries(emotions)) {
75
- const normalized = normalizeSent(emotionKey);
76
- emosMapRef.current[normalized] = (emosMapRef.current[normalized] || 0) + (count as number);
77
- }
78
 
79
- console.log(`✅ [${src}] fake=${news?.fake_news}, emotions=`, emotions);
 
 
 
 
80
  } catch (error) {
81
- console.warn(`⚠️ [${src}] błąd pobierania:`, error);
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
- console.log(`📊 Wykresy gotowe. Słupki:`, barsMapRef.current.size, `Emocje:`, emosMapRef.current);
94
- }, [language]);
 
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: barData.length > 0,
107
- hasEmos: emotionData.length > 0,
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
- const key = newsKey(source, lang, model);
 
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
- const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/news/${encodeURIComponent(source)}?lang=${lang}&model=${model}`);
 
 
 
 
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
- // Zabezpieczenie przed aktualizacją stanu po odmontowaniu komponentu
63
- mounted = false;
64
- };
65
- }, [source, lang, model]);
66
 
67
  const refresh = async () => {
68
- const key = newsKey(source, lang, model);
69
  cache.del(key);
70
  setData(null);
71
  setLoading(true);
72
  try {
73
- const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/news/${encodeURIComponent(source)}?lang=${lang}&model=${model}`);
 
 
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
- const cacheKey = newsKey(source, lang);
 
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 (uwzględnia model NLP)
88
- export const newsKey = (source: string, lang = "pl", model = "roberta") => `news:${source}:${lang}:${model}`;
 
 
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="TV2">TV2</option>
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
- tv2: "TV2",
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(), 30_000);
107
  const res = await fetch(
108
- `http://127.0.0.1:8000/news/${src}?lang=${language}`,
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 ?? 0), 0) /
134
- (articles.length || 1);
135
 
136
  for (const art of articles) {
137
  if (art.sentiment) emotions[art.sentiment] = (emotions[art.sentiment] ?? 0) + 1;
138
  }
139
 
140
- // 🆕 DEBUG: sprawdźmy jakie emocje zebraliśmy
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