mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(i18n): migrate Settings page batch 1 (7 files, 28+ strings)
- Main page.tsx: tab labels, footer - AppearanceTab: dark mode, health check logs - CacheStatsCard: prompt cache stats - ProxyTab: global proxy - SystemPromptTab: global system prompt - ThinkingBudgetTab: mode labels, effort levels - Expanded settings namespace from 63 to 100 keys
This commit is contained in:
@@ -4,9 +4,11 @@ import { useState, useEffect } from "react";
|
||||
import { Card, Toggle } from "@/shared/components";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AppearanceTab() {
|
||||
const { theme, setTheme, isDark } = useTheme();
|
||||
const t = useTranslations("settings");
|
||||
const [settings, setSettings] = useState<Record<string, any>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -48,13 +50,13 @@ export default function AppearanceTab() {
|
||||
palette
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Appearance</h3>
|
||||
<h3 className="text-lg font-semibold">{t("appearance")}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Dark Mode</p>
|
||||
<p className="text-sm text-text-muted">Switch between light and dark themes</p>
|
||||
<p className="font-medium">{t("darkMode")}</p>
|
||||
<p className="text-sm text-text-muted">{t("switchThemes")}</p>
|
||||
</div>
|
||||
<Toggle checked={isDark} onChange={() => setTheme(isDark ? "light" : "dark")} />
|
||||
</div>
|
||||
@@ -90,10 +92,8 @@ export default function AppearanceTab() {
|
||||
<div className="pt-4 border-t border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Hide Health Check Logs</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
When ON, suppress [HealthCheck] messages in server console
|
||||
</p>
|
||||
<p className="font-medium">{t("hideHealthLogs")}</p>
|
||||
<p className="text-sm text-text-muted">{t("hideHealthLogsDesc")}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.hideHealthCheckLogs === true}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function CacheStatsCard() {
|
||||
const [cache, setCache] = useState(null);
|
||||
const [flushing, setFlushing] = useState(false);
|
||||
const t = useTranslations("settings");
|
||||
|
||||
const fetchStats = () => {
|
||||
fetch("/api/cache/stats")
|
||||
@@ -31,40 +33,40 @@ export default function CacheStatsCard() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-text-main flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[20px]">cached</span>
|
||||
Prompt Cache
|
||||
{t("promptCache")}
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleFlush}
|
||||
disabled={flushing}
|
||||
className="px-3 py-1.5 text-xs rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{flushing ? "Flushing…" : "Flush Cache"}
|
||||
{flushing ? t("flushing") : t("flushCache")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cache ? (
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-text-muted">Size</p>
|
||||
<p className="text-text-muted">{t("size")}</p>
|
||||
<p className="font-mono text-lg text-text-main">
|
||||
{cache.size}/{cache.maxSize}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-muted">Hit Rate</p>
|
||||
<p className="text-text-muted">{t("hitRate")}</p>
|
||||
<p className="font-mono text-lg text-text-main">{cache.hitRate?.toFixed(1) ?? 0}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-muted">Hits</p>
|
||||
<p className="text-text-muted">{t("hits")}</p>
|
||||
<p className="font-mono text-text-main">{cache.hits ?? 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-muted">Evictions</p>
|
||||
<p className="text-text-muted">{t("evictions")}</p>
|
||||
<p className="font-mono text-text-main">{cache.evictions ?? 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">Loading cache stats…</p>
|
||||
<p className="text-sm text-text-muted">{t("loadingCacheStats")}</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ProxyConfigModal } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ProxyTab() {
|
||||
const [proxyModalOpen, setProxyModalOpen] = useState(false);
|
||||
const [globalProxy, setGlobalProxy] = useState(null);
|
||||
const mountedRef = useRef(true);
|
||||
const t = useTranslations("settings");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
const loadGlobalProxy = async () => {
|
||||
try {
|
||||
@@ -44,12 +47,9 @@ export default function ProxyTab() {
|
||||
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
|
||||
vpn_lock
|
||||
</span>
|
||||
<h2 className="text-lg font-bold">Global Proxy</h2>
|
||||
<h2 className="text-lg font-bold">{t("globalProxy")}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
Configure a global outbound proxy for all API calls. Individual providers, combos, and
|
||||
keys can override this.
|
||||
</p>
|
||||
<p className="text-sm text-text-muted mb-4">{t("globalProxyDesc")}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{globalProxy ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -58,7 +58,7 @@ export default function ProxyTab() {
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-text-muted">No global proxy configured</span>
|
||||
<span className="text-sm text-text-muted">{t("noGlobalProxy")}</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -69,7 +69,7 @@ export default function ProxyTab() {
|
||||
setProxyModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{globalProxy ? "Edit" : "Configure"}
|
||||
{globalProxy ? tc("edit") : t("configure")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Toggle } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function SystemPromptTab() {
|
||||
const [config, setConfig] = useState({ enabled: false, prompt: "" });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [status, setStatus] = useState("");
|
||||
const [debounceTimer, setDebounceTimer] = useState(null);
|
||||
const t = useTranslations("settings");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/system-prompt")
|
||||
@@ -57,13 +59,14 @@ export default function SystemPromptTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Global System Prompt</h3>
|
||||
<p className="text-sm text-text-muted">Injected into all requests at proxy level</p>
|
||||
<h3 className="text-lg font-semibold">{t("globalSystemPrompt")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("systemPromptDesc")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{status === "saved" && (
|
||||
<span className="text-xs font-medium text-emerald-500 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">check_circle</span> Saved
|
||||
<span className="material-symbols-outlined text-[14px]">check_circle</span>{" "}
|
||||
{t("saved")}
|
||||
</span>
|
||||
)}
|
||||
<Toggle
|
||||
@@ -80,7 +83,7 @@ export default function SystemPromptTab() {
|
||||
<textarea
|
||||
value={config.prompt}
|
||||
onChange={(e) => handlePromptChange(e.target.value)}
|
||||
placeholder="Enter system prompt to inject into all requests..."
|
||||
placeholder={t("systemPromptPlaceholder")}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border/50 bg-surface/30 text-sm
|
||||
placeholder:text-text-muted/50 resize-y min-h-[120px]
|
||||
@@ -94,8 +97,9 @@ export default function SystemPromptTab() {
|
||||
</div>
|
||||
<p className="text-xs text-text-muted/70 flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined text-[14px]">info</span>
|
||||
This prompt is prepended to the system message of every request. Use for global instructions,
|
||||
safety guidelines, or response formatting rules. Send <code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a request to bypass.
|
||||
{t("systemPromptHint")} Send{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a
|
||||
request to bypass.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Select } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const MODES = [
|
||||
{
|
||||
@@ -46,6 +47,7 @@ export default function ThinkingBudgetTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [status, setStatus] = useState("");
|
||||
const t = useTranslations("settings");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/thinking-budget")
|
||||
@@ -90,12 +92,12 @@ export default function ThinkingBudgetTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Thinking Budget</h3>
|
||||
<p className="text-sm text-text-muted">Control AI reasoning token usage across all requests</p>
|
||||
<h3 className="text-lg font-semibold">{t("thinkingBudgetTitle")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("thinkingBudgetDesc")}</p>
|
||||
</div>
|
||||
{status === "saved" && (
|
||||
<span className="ml-auto text-xs font-medium text-emerald-500 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">check_circle</span> Saved
|
||||
<span className="material-symbols-outlined text-[14px]">check_circle</span> {t("saved")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -121,7 +123,9 @@ export default function ThinkingBudgetTab() {
|
||||
{m.icon}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className={`text-sm font-medium ${config.mode === m.value ? "text-violet-400" : ""}`}>
|
||||
<p
|
||||
className={`text-sm font-medium ${config.mode === m.value ? "text-violet-400" : ""}`}
|
||||
>
|
||||
{m.label}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-0.5 leading-relaxed">{m.desc}</p>
|
||||
@@ -134,9 +138,9 @@ export default function ThinkingBudgetTab() {
|
||||
{config.mode === "custom" && (
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-medium">Token Budget</p>
|
||||
<p className="text-sm font-medium">{t("tokenBudget")}</p>
|
||||
<span className="text-sm font-mono tabular-nums text-violet-400">
|
||||
{config.customBudget.toLocaleString()} tokens
|
||||
{config.customBudget.toLocaleString()} {t("tokens")}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -161,10 +165,8 @@ export default function ThinkingBudgetTab() {
|
||||
{/* Adaptive effort level */}
|
||||
{config.mode === "adaptive" && (
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30">
|
||||
<p className="text-sm font-medium mb-3">Base Effort Level</p>
|
||||
<p className="text-xs text-text-muted mb-3">
|
||||
Adaptive mode scales from this base level based on message count, tool usage, and prompt length.
|
||||
</p>
|
||||
<p className="text-sm font-medium mb-3">{t("baseEffortLevel")}</p>
|
||||
<p className="text-xs text-text-muted mb-3">{t("adaptiveHint")}</p>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{EFFORTS.map((e) => (
|
||||
<button
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SystemStorageTab from "./components/SystemStorageTab";
|
||||
import SecurityTab from "./components/SecurityTab";
|
||||
import RoutingTab from "./components/RoutingTab";
|
||||
@@ -17,15 +18,16 @@ import CacheStatsCard from "./components/CacheStatsCard";
|
||||
import ResilienceTab from "./components/ResilienceTab";
|
||||
|
||||
const tabs = [
|
||||
{ id: "general", label: "General", icon: "settings" },
|
||||
{ id: "ai", label: "AI", icon: "smart_toy" },
|
||||
{ id: "security", label: "Security", icon: "shield" },
|
||||
{ id: "routing", label: "Routing", icon: "route" },
|
||||
{ id: "resilience", label: "Resilience", icon: "electrical_services" },
|
||||
{ id: "advanced", label: "Advanced", icon: "tune" },
|
||||
{ id: "general", labelKey: "general", icon: "settings" },
|
||||
{ id: "ai", labelKey: "ai", icon: "smart_toy" },
|
||||
{ id: "security", labelKey: "security", icon: "shield" },
|
||||
{ id: "routing", labelKey: "routing", icon: "route" },
|
||||
{ id: "resilience", labelKey: "resilience", icon: "electrical_services" },
|
||||
{ id: "advanced", labelKey: "advanced", icon: "tune" },
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const t = useTranslations("settings");
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams.get("tab");
|
||||
const [userSelectedTab, setUserSelectedTab] = useState(null);
|
||||
@@ -57,13 +59,16 @@ export default function SettingsPage() {
|
||||
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
|
||||
{tab.icon}
|
||||
</span>
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
<span className="hidden sm:inline">{t(tab.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab contents */}
|
||||
<div role="tabpanel" aria-label={tabs.find((t) => t.id === activeTab)?.label}>
|
||||
<div
|
||||
role="tabpanel"
|
||||
aria-label={t(tabs.find((t2) => t2.id === activeTab)?.labelKey || "general")}
|
||||
>
|
||||
{activeTab === "general" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -100,7 +105,7 @@ export default function SettingsPage() {
|
||||
<p>
|
||||
{APP_CONFIG.name} v{APP_CONFIG.version}
|
||||
</p>
|
||||
<p className="mt-1">Local Mode — All data stored on your machine</p>
|
||||
<p className="mt-1">{t("localMode")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -629,7 +629,44 @@
|
||||
"ipWhitelist": "IP Whitelist",
|
||||
"ipBlacklist": "IP Blacklist",
|
||||
"addIP": "Add IP",
|
||||
"savedSuccessfully": "Settings saved successfully"
|
||||
"savedSuccessfully": "Settings saved successfully",
|
||||
"ai": "AI",
|
||||
"advanced": "Advanced",
|
||||
"localMode": "Local Mode — All data stored on your machine",
|
||||
"switchThemes": "Switch between light and dark themes",
|
||||
"hideHealthLogs": "Hide Health Check Logs",
|
||||
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
|
||||
"promptCache": "Prompt Cache",
|
||||
"flushCache": "Flush Cache",
|
||||
"flushing": "Flushing…",
|
||||
"size": "Size",
|
||||
"hits": "Hits",
|
||||
"evictions": "Evictions",
|
||||
"loadingCacheStats": "Loading cache stats…",
|
||||
"globalProxy": "Global Proxy",
|
||||
"globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.",
|
||||
"noGlobalProxy": "No global proxy configured",
|
||||
"configure": "Configure",
|
||||
"globalSystemPrompt": "Global System Prompt",
|
||||
"systemPromptDesc": "Injected into all requests at proxy level",
|
||||
"saved": "Saved",
|
||||
"systemPromptPlaceholder": "Enter system prompt to inject into all requests...",
|
||||
"systemPromptHint": "This prompt is prepended to the system message of every request. Use for global instructions, safety guidelines, or response formatting rules.",
|
||||
"chars": "{count} chars",
|
||||
"thinkingBudgetTitle": "Thinking Budget",
|
||||
"thinkingBudgetDesc": "Control AI reasoning token usage across all requests",
|
||||
"passthrough": "Passthrough",
|
||||
"passthroughDesc": "No changes — client controls thinking budget",
|
||||
"auto": "Auto",
|
||||
"autoDesc": "Strip all thinking config — let provider decide",
|
||||
"custom": "Custom",
|
||||
"customDesc": "Set a fixed token budget for all requests",
|
||||
"adaptive": "Adaptive",
|
||||
"adaptiveDesc": "Scale budget based on request complexity",
|
||||
"tokenBudget": "Token Budget",
|
||||
"tokens": "tokens",
|
||||
"baseEffortLevel": "Base Effort Level",
|
||||
"adaptiveHint": "Adaptive mode scales from this base level based on message count, tool usage, and prompt length."
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
|
||||
@@ -629,7 +629,44 @@
|
||||
"ipWhitelist": "Lista de IPs Permitidos",
|
||||
"ipBlacklist": "Lista de IPs Bloqueados",
|
||||
"addIP": "Adicionar IP",
|
||||
"savedSuccessfully": "Configurações salvas com sucesso"
|
||||
"savedSuccessfully": "Configurações salvas com sucesso",
|
||||
"ai": "IA",
|
||||
"advanced": "Avançado",
|
||||
"localMode": "Modo Local — Todos os dados armazenados na sua máquina",
|
||||
"switchThemes": "Alternar entre temas claro e escuro",
|
||||
"hideHealthLogs": "Ocultar Logs de Health Check",
|
||||
"hideHealthLogsDesc": "Quando ATIVADO, suprime mensagens [HealthCheck] no console do servidor",
|
||||
"promptCache": "Cache de Prompt",
|
||||
"flushCache": "Limpar Cache",
|
||||
"flushing": "Limpando…",
|
||||
"size": "Tamanho",
|
||||
"hits": "Acertos",
|
||||
"evictions": "Remoções",
|
||||
"loadingCacheStats": "Carregando estatísticas do cache…",
|
||||
"globalProxy": "Proxy Global",
|
||||
"globalProxyDesc": "Configure um proxy de saída global para todas as chamadas de API. Provedores individuais, combos e chaves podem sobrescrever.",
|
||||
"noGlobalProxy": "Nenhum proxy global configurado",
|
||||
"configure": "Configurar",
|
||||
"globalSystemPrompt": "Prompt de Sistema Global",
|
||||
"systemPromptDesc": "Injetado em todas as requisições no nível do proxy",
|
||||
"saved": "Salvo",
|
||||
"systemPromptPlaceholder": "Digite o prompt de sistema para injetar em todas as requisições...",
|
||||
"systemPromptHint": "Este prompt é adicionado antes da mensagem de sistema de cada requisição. Use para instruções globais, diretrizes de segurança ou regras de formatação.",
|
||||
"chars": "{count} caracteres",
|
||||
"thinkingBudgetTitle": "Orçamento de Raciocínio",
|
||||
"thinkingBudgetDesc": "Controle o uso de tokens de raciocínio da IA em todas as requisições",
|
||||
"passthrough": "Passagem Direta",
|
||||
"passthroughDesc": "Sem alterações — cliente controla orçamento de raciocínio",
|
||||
"auto": "Automático",
|
||||
"autoDesc": "Remove toda configuração de raciocínio — provedor decide",
|
||||
"custom": "Personalizado",
|
||||
"customDesc": "Define um orçamento fixo de tokens para todas as requisições",
|
||||
"adaptive": "Adaptativo",
|
||||
"adaptiveDesc": "Escala orçamento baseado na complexidade da requisição",
|
||||
"tokenBudget": "Orçamento de Tokens",
|
||||
"tokens": "tokens",
|
||||
"baseEffortLevel": "Nível de Esforço Base",
|
||||
"adaptiveHint": "Modo adaptativo escala a partir deste nível base baseado na quantidade de mensagens, uso de ferramentas e tamanho do prompt."
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
|
||||
Reference in New Issue
Block a user