Ruperth commited on
Commit
035d8b3
·
1 Parent(s): 38ee3c1

feat: localise Hub Settings and ModelBanner

Browse files

Routes every string in the moderator pages through the dictionary and reworks the Settings status message so error vs success is tracked as a boolean rather than by sniffing English substrings. The Hub charts now read their fill and stroke colours from the active CSS theme tokens so recharts re-tints when the user toggles dark mode.

frontend/src/components/ModelBanner.tsx CHANGED
@@ -1,25 +1,30 @@
1
  import { useEffect, useState } from "react";
2
  import { getModelInfo } from "../api/client";
 
3
 
4
  export function ModelBanner() {
 
5
  const [banner, setBanner] = useState<string | null>(null);
6
 
7
  useEffect(() => {
 
8
  getModelInfo()
9
  .then((info) => {
10
- const text =
11
- (info as { display_banner?: string }).display_banner ??
12
- (info.name?.includes("Meta-Feature Stacking")
13
- ? "Currently using: Meta-Feature Stacking Model (F1: 0.805, Gap: 2.54%)"
14
- : null);
15
- setBanner(text);
 
 
 
 
16
  })
17
  .catch(() => {
18
- setBanner(
19
- "Currently using: Meta-Feature Stacking Model (F1: 0.805, Gap: 2.54%)"
20
- );
21
  });
22
- }, []);
23
 
24
  if (!banner) return null;
25
 
 
1
  import { useEffect, useState } from "react";
2
  import { getModelInfo } from "../api/client";
3
+ import { useI18n } from "../i18n/I18nContext";
4
 
5
  export function ModelBanner() {
6
+ const { t } = useI18n();
7
  const [banner, setBanner] = useState<string | null>(null);
8
 
9
  useEffect(() => {
10
+ const fallback = t.modelBanner.current("Meta-Feature Stacking", "0.805", "2.54");
11
  getModelInfo()
12
  .then((info) => {
13
+ const apiBanner = (info as { display_banner?: string }).display_banner;
14
+ if (apiBanner) {
15
+ setBanner(apiBanner);
16
+ return;
17
+ }
18
+ if (info.name?.includes("Meta-Feature Stacking")) {
19
+ setBanner(fallback);
20
+ return;
21
+ }
22
+ setBanner(null);
23
  })
24
  .catch(() => {
25
+ setBanner(fallback);
 
 
26
  });
27
+ }, [t]);
28
 
29
  if (!banner) return null;
30
 
frontend/src/pages/HubPage.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import {
2
  Bar,
3
  BarChart,
@@ -10,18 +11,39 @@ import {
10
  YAxis,
11
  } from "recharts";
12
  import { useApp } from "../context/AppContext";
 
 
13
 
14
- const COLORS = ["#2ba640", "#ff4e45"];
 
 
 
 
 
 
15
 
16
  export function HubPage() {
17
  const { hubHistory, threshold } = useApp();
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  const toxic = hubHistory.filter((h) => h.score >= threshold).length;
20
  const safe = hubHistory.length - toxic;
21
  const pieData = [
22
- { name: "Safe", value: safe || 1 },
23
- { name: "Toxic", value: toxic || 0 },
24
  ];
 
25
 
26
  const barData = hubHistory.slice(0, 12).map((h, i) => ({
27
  name: `#${i + 1}`,
@@ -30,30 +52,30 @@ export function HubPage() {
30
 
31
  return (
32
  <div className="hub-page">
33
- <h1>Moderator Hub</h1>
34
  <div className="hub-cards">
35
  <div className="hub-stat">
36
- <span className="stat-label">Threshold</span>
37
  <span className="stat-value">{threshold.toFixed(2)}</span>
38
  </div>
39
  <div className="hub-stat">
40
- <span className="stat-label">Events logged</span>
41
  <span className="stat-value">{hubHistory.length}</span>
42
  </div>
43
  <div className="hub-stat">
44
- <span className="stat-label">Toxic (session)</span>
45
  <span className="stat-value">{toxic}</span>
46
  </div>
47
  </div>
48
 
49
  <div className="hub-charts">
50
  <div className="chart-box">
51
- <h2>Safe vs Toxic</h2>
52
  <ResponsiveContainer width="100%" height={220}>
53
  <PieChart>
54
  <Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80}>
55
  {pieData.map((_, i) => (
56
- <Cell key={i} fill={COLORS[i % COLORS.length]} />
57
  ))}
58
  </Pie>
59
  <Tooltip />
@@ -61,33 +83,33 @@ export function HubPage() {
61
  </ResponsiveContainer>
62
  </div>
63
  <div className="chart-box">
64
- <h2>Recent scores (%)</h2>
65
  <ResponsiveContainer width="100%" height={220}>
66
  <BarChart data={barData}>
67
- <XAxis dataKey="name" stroke="#aaa" />
68
- <YAxis stroke="#aaa" domain={[0, 100]} />
69
  <Tooltip />
70
- <Bar dataKey="score" fill="#3ea6ff" />
71
  </BarChart>
72
  </ResponsiveContainer>
73
  </div>
74
  </div>
75
 
76
  <div className="hub-table-wrap">
77
- <h2>Recent actions</h2>
78
  <table className="hub-table">
79
  <thead>
80
  <tr>
81
- <th>User</th>
82
- <th>Comment</th>
83
- <th>Score</th>
84
- <th>Action</th>
85
  </tr>
86
  </thead>
87
  <tbody>
88
  {hubHistory.length === 0 ? (
89
  <tr>
90
- <td colSpan={4}>Post comments on the Watch page to populate history.</td>
91
  </tr>
92
  ) : (
93
  hubHistory.map((h, i) => (
 
1
+ import { useMemo } from "react";
2
  import {
3
  Bar,
4
  BarChart,
 
11
  YAxis,
12
  } from "recharts";
13
  import { useApp } from "../context/AppContext";
14
+ import { useTheme } from "../context/ThemeContext";
15
+ import { useI18n } from "../i18n/I18nContext";
16
 
17
+ function cssVar(name: string, fallback: string): string {
18
+ if (typeof window === "undefined") return fallback;
19
+ return (
20
+ getComputedStyle(document.documentElement).getPropertyValue(name).trim() ||
21
+ fallback
22
+ );
23
+ }
24
 
25
  export function HubPage() {
26
  const { hubHistory, threshold } = useApp();
27
+ const { theme } = useTheme();
28
+ const { t } = useI18n();
29
+
30
+ const colors = useMemo(
31
+ () => ({
32
+ safe: cssVar("--safe", "#2f8f3e"),
33
+ toxic: cssVar("--toxic", "#c2331f"),
34
+ accent: cssVar("--accent", "#d83d2c"),
35
+ muted: cssVar("--muted", "#6b6256"),
36
+ }),
37
+ [theme]
38
+ );
39
 
40
  const toxic = hubHistory.filter((h) => h.score >= threshold).length;
41
  const safe = hubHistory.length - toxic;
42
  const pieData = [
43
+ { name: t.hub.safe, value: safe || 1 },
44
+ { name: t.hub.toxic, value: toxic || 0 },
45
  ];
46
+ const pieColors = [colors.safe, colors.toxic];
47
 
48
  const barData = hubHistory.slice(0, 12).map((h, i) => ({
49
  name: `#${i + 1}`,
 
52
 
53
  return (
54
  <div className="hub-page">
55
+ <h1>{t.hub.title}</h1>
56
  <div className="hub-cards">
57
  <div className="hub-stat">
58
+ <span className="stat-label">{t.hub.threshold}</span>
59
  <span className="stat-value">{threshold.toFixed(2)}</span>
60
  </div>
61
  <div className="hub-stat">
62
+ <span className="stat-label">{t.hub.eventsLogged}</span>
63
  <span className="stat-value">{hubHistory.length}</span>
64
  </div>
65
  <div className="hub-stat">
66
+ <span className="stat-label">{t.hub.toxicSession}</span>
67
  <span className="stat-value">{toxic}</span>
68
  </div>
69
  </div>
70
 
71
  <div className="hub-charts">
72
  <div className="chart-box">
73
+ <h2>{t.hub.safeVsToxic}</h2>
74
  <ResponsiveContainer width="100%" height={220}>
75
  <PieChart>
76
  <Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80}>
77
  {pieData.map((_, i) => (
78
+ <Cell key={i} fill={pieColors[i % pieColors.length]} />
79
  ))}
80
  </Pie>
81
  <Tooltip />
 
83
  </ResponsiveContainer>
84
  </div>
85
  <div className="chart-box">
86
+ <h2>{t.hub.recentScores}</h2>
87
  <ResponsiveContainer width="100%" height={220}>
88
  <BarChart data={barData}>
89
+ <XAxis dataKey="name" stroke={colors.muted} />
90
+ <YAxis stroke={colors.muted} domain={[0, 100]} />
91
  <Tooltip />
92
+ <Bar dataKey="score" fill={colors.accent} />
93
  </BarChart>
94
  </ResponsiveContainer>
95
  </div>
96
  </div>
97
 
98
  <div className="hub-table-wrap">
99
+ <h2>{t.hub.recentActions}</h2>
100
  <table className="hub-table">
101
  <thead>
102
  <tr>
103
+ <th>{t.hub.colUser}</th>
104
+ <th>{t.hub.colComment}</th>
105
+ <th>{t.hub.colScore}</th>
106
+ <th>{t.hub.colAction}</th>
107
  </tr>
108
  </thead>
109
  <tbody>
110
  {hubHistory.length === 0 ? (
111
  <tr>
112
+ <td colSpan={4}>{t.hub.emptyHistory}</td>
113
  </tr>
114
  ) : (
115
  hubHistory.map((h, i) => (
frontend/src/pages/SettingsPage.tsx CHANGED
@@ -1,17 +1,20 @@
1
  import { useEffect, useState } from "react";
2
  import { getModelInfo, getModelsStatus, predict, setModel } from "../api/client";
3
  import { useApp } from "../context/AppContext";
 
4
  import type { ModelStatusEntry } from "../types/api";
5
 
6
  export function SettingsPage() {
7
  const { threshold, setThreshold } = useApp();
 
8
  const [modelStatus, setModelStatus] = useState<ModelStatusEntry[]>([]);
9
  const [active, setActive] = useState("");
10
- const [testText, setTestText] = useState("You are an idiot");
11
  const [testResult, setTestResult] = useState<string | null>(null);
12
  const [testError, setTestError] = useState<string | null>(null);
13
  const [testing, setTesting] = useState(false);
14
  const [message, setMessage] = useState<string | null>(null);
 
15
  const [switching, setSwitching] = useState(false);
16
 
17
  const loadStatus = () => {
@@ -20,11 +23,15 @@ export function SettingsPage() {
20
  setModelStatus(r.models);
21
  setActive(r.active);
22
  })
23
- .catch(() => setMessage("Could not load model status"));
 
 
 
24
  };
25
 
26
  useEffect(() => {
27
  loadStatus();
 
28
  }, []);
29
 
30
  useEffect(() => {
@@ -35,22 +42,26 @@ export function SettingsPage() {
35
  const switchModel = async (name: string) => {
36
  const entry = modelStatus.find((m) => m.name === name);
37
  if (entry && !entry.available) {
38
- setMessage(entry.reason ?? "Model unavailable");
 
39
  return;
40
  }
41
  setMessage(null);
 
42
  setSwitching(true);
43
  try {
44
  await setModel(name);
45
  setActive(name);
46
- setMessage(`Active model: ${name}`);
 
47
  const info = await getModelInfo();
48
  if (info.recommended_threshold != null) {
49
  setThreshold(info.recommended_threshold);
50
  }
51
  loadStatus();
52
  } catch (e) {
53
- setMessage(e instanceof Error ? e.message : "Failed to switch model");
 
54
  loadStatus();
55
  } finally {
56
  setSwitching(false);
@@ -63,9 +74,14 @@ export function SettingsPage() {
63
  setTestError(null);
64
  try {
65
  const r = await predict(testText, threshold);
66
- setTestResult(`${r.status} — ${Math.round(r.probability * 100)}% toxic`);
 
 
 
 
 
67
  } catch (e) {
68
- setTestError(e instanceof Error ? e.message : "Analysis failed");
69
  } finally {
70
  setTesting(false);
71
  }
@@ -73,22 +89,13 @@ export function SettingsPage() {
73
 
74
  return (
75
  <div className="settings-page">
76
- <h1>Settings</h1>
77
  <section className="settings-card">
78
- <h2>Active model</h2>
79
- <p className="production-model-note">
80
- Default: <strong>Meta-Feature Stacking (Production)</strong> (F1 0.805, gap 2.54%).
81
- Baselines: <strong>LR + TF-IDF</strong> (F1 0.758) and{" "}
82
- <strong>Frozen Toxic-BERT</strong> (F1 0.790, gap 0.16%).
83
- </p>
84
- <p className="hint">
85
- Production and frozen BERT need <code>uv sync --extra hf</code> (or Docker{" "}
86
- <code>INSTALL_HF=1</code>). LR baseline uses joblib only. First transformer load may
87
- download weights (~1 min).
88
- </p>
89
- {switching && (
90
- <p className="hint">Switching model… production may take up to a minute on first load.</p>
91
- )}
92
  <div className="model-list">
93
  {modelStatus.map((m) => (
94
  <label
@@ -112,14 +119,14 @@ export function SettingsPage() {
112
  ))}
113
  </div>
114
  {message && (
115
- <p className={message.includes("Failed") || message.includes("Install") ? "error-text" : "settings-msg"}>
116
  {message}
117
  </p>
118
  )}
119
  </section>
120
 
121
  <section className="settings-card">
122
- <h2>Toxicity threshold</h2>
123
  <input
124
  type="range"
125
  min={0.1}
@@ -128,11 +135,13 @@ export function SettingsPage() {
128
  value={threshold}
129
  onChange={(e) => setThreshold(Number(e.target.value))}
130
  />
131
- <p>{threshold.toFixed(2)} — comments at or above this probability are <strong>Toxic</strong>.</p>
 
 
132
  </section>
133
 
134
  <section className="settings-card">
135
- <h2>Quick test</h2>
136
  <textarea value={testText} onChange={(e) => setTestText(e.target.value)} rows={2} />
137
  <button
138
  type="button"
@@ -140,7 +149,7 @@ export function SettingsPage() {
140
  disabled={testing || !testText.trim()}
141
  onClick={() => void runTest()}
142
  >
143
- {testing ? "Analyzing…" : "Analyze"}
144
  </button>
145
  {testResult && <p className="settings-msg">{testResult}</p>}
146
  {testError && <p className="error-text">{testError}</p>}
 
1
  import { useEffect, useState } from "react";
2
  import { getModelInfo, getModelsStatus, predict, setModel } from "../api/client";
3
  import { useApp } from "../context/AppContext";
4
+ import { useI18n } from "../i18n/I18nContext";
5
  import type { ModelStatusEntry } from "../types/api";
6
 
7
  export function SettingsPage() {
8
  const { threshold, setThreshold } = useApp();
9
+ const { t } = useI18n();
10
  const [modelStatus, setModelStatus] = useState<ModelStatusEntry[]>([]);
11
  const [active, setActive] = useState("");
12
+ const [testText, setTestText] = useState<string>(() => t.settings.defaultTestText);
13
  const [testResult, setTestResult] = useState<string | null>(null);
14
  const [testError, setTestError] = useState<string | null>(null);
15
  const [testing, setTesting] = useState(false);
16
  const [message, setMessage] = useState<string | null>(null);
17
+ const [messageIsError, setMessageIsError] = useState(false);
18
  const [switching, setSwitching] = useState(false);
19
 
20
  const loadStatus = () => {
 
23
  setModelStatus(r.models);
24
  setActive(r.active);
25
  })
26
+ .catch(() => {
27
+ setMessage(t.settings.couldNotLoadStatus);
28
+ setMessageIsError(true);
29
+ });
30
  };
31
 
32
  useEffect(() => {
33
  loadStatus();
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
  }, []);
36
 
37
  useEffect(() => {
 
42
  const switchModel = async (name: string) => {
43
  const entry = modelStatus.find((m) => m.name === name);
44
  if (entry && !entry.available) {
45
+ setMessage(entry.reason ?? "");
46
+ setMessageIsError(true);
47
  return;
48
  }
49
  setMessage(null);
50
+ setMessageIsError(false);
51
  setSwitching(true);
52
  try {
53
  await setModel(name);
54
  setActive(name);
55
+ setMessage(t.settings.activeModelMsg(name));
56
+ setMessageIsError(false);
57
  const info = await getModelInfo();
58
  if (info.recommended_threshold != null) {
59
  setThreshold(info.recommended_threshold);
60
  }
61
  loadStatus();
62
  } catch (e) {
63
+ setMessage(e instanceof Error ? e.message : t.settings.failedSwitch);
64
+ setMessageIsError(true);
65
  loadStatus();
66
  } finally {
67
  setSwitching(false);
 
74
  setTestError(null);
75
  try {
76
  const r = await predict(testText, threshold);
77
+ setTestResult(
78
+ t.settings.testResult(
79
+ r.is_toxic ? t.badges.toxic : t.badges.safe,
80
+ String(Math.round(r.probability * 100))
81
+ )
82
+ );
83
  } catch (e) {
84
+ setTestError(e instanceof Error ? e.message : t.settings.analysisFailed);
85
  } finally {
86
  setTesting(false);
87
  }
 
89
 
90
  return (
91
  <div className="settings-page">
92
+ <h1>{t.settings.title}</h1>
93
  <section className="settings-card">
94
+ <h2>{t.settings.activeModel}</h2>
95
+ <p className="production-model-note">{t.settings.productionNote("0.805", "2.54")}</p>
96
+ <p className="production-model-note">{t.settings.baselinesNote("0.758", "0.790", "0.16")}</p>
97
+ <p className="hint">{t.settings.installHint}</p>
98
+ {switching && <p className="hint">{t.settings.switching}</p>}
 
 
 
 
 
 
 
 
 
99
  <div className="model-list">
100
  {modelStatus.map((m) => (
101
  <label
 
119
  ))}
120
  </div>
121
  {message && (
122
+ <p className={messageIsError ? "error-text" : "settings-msg"}>
123
  {message}
124
  </p>
125
  )}
126
  </section>
127
 
128
  <section className="settings-card">
129
+ <h2>{t.settings.thresholdTitle}</h2>
130
  <input
131
  type="range"
132
  min={0.1}
 
135
  value={threshold}
136
  onChange={(e) => setThreshold(Number(e.target.value))}
137
  />
138
+ <p>
139
+ {threshold.toFixed(2)} — {t.settings.thresholdNote}
140
+ </p>
141
  </section>
142
 
143
  <section className="settings-card">
144
+ <h2>{t.settings.quickTest}</h2>
145
  <textarea value={testText} onChange={(e) => setTestText(e.target.value)} rows={2} />
146
  <button
147
  type="button"
 
149
  disabled={testing || !testText.trim()}
150
  onClick={() => void runTest()}
151
  >
152
+ {testing ? t.settings.analyzing : t.settings.analyze}
153
  </button>
154
  {testResult && <p className="settings-msg">{testResult}</p>}
155
  {testError && <p className="error-text">{testError}</p>}