This commit is contained in:
Mikan
2026-06-20 19:13:05 +03:00
parent 32575e217e
commit 8514c63ec6
193 changed files with 22105 additions and 11660 deletions

View File

@@ -0,0 +1,132 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { AdminApi } from "@/lib/api";
import { useToastStore } from "@/stores/toastStore";
import type { AdminSettingsResponse } from "@/types";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Input } from "@/components/ui/Input";
import { Spinner } from "@/components/ui/Spinner";
const GROUP_PREFIXES: Array<{ group: string; prefixes: string[]; labelKey: string }> = [
{ group: "llm", prefixes: ["llm_", "llm."], labelKey: "admin.group_llm" },
{ group: "embeddings", prefixes: ["embeddings_", "embeddings."], labelKey: "admin.group_embeddings" },
{ group: "qdrant", prefixes: ["qdrant_", "qdrant."], labelKey: "admin.group_qdrant" },
{ group: "ui", prefixes: ["ui_", "ui.", "site_", "site."], labelKey: "admin.group_ui" },
{ group: "game", prefixes: ["game_", "game."], labelKey: "admin.group_game" },
];
function groupFor(key: string): string {
const lower = key.toLowerCase();
for (const g of GROUP_PREFIXES) {
if (g.prefixes.some((p) => lower.startsWith(p))) return g.group;
}
return "other";
}
export function SettingsPanel() {
const { t } = useTranslation();
const pushToast = useToastStore((s) => s.push);
const [data, setData] = useState<AdminSettingsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [draft, setDraft] = useState<Record<string, string>>({});
useEffect(() => {
setLoading(true);
AdminApi.settings()
.then((res) => {
setData(res);
setDraft({ ...res.settings });
})
.catch(() => pushToast("error", t("admin.settings_load_failed")))
.finally(() => setLoading(false));
}, [pushToast, t]);
const grouped = useMemo(() => {
if (!data) return {} as Record<string, Array<{ key: string; description?: string }>>;
const out: Record<string, Array<{ key: string; description?: string }>> = {};
for (const key of Object.keys(data.settings)) {
const g = groupFor(key);
(out[g] ||= []).push({ key, description: data.descriptions?.[key] });
}
return out;
}, [data]);
const handleSave = async () => {
if (!data) return;
setSaving(true);
try {
// Save only changed keys
const diff: Record<string, string> = {};
for (const [k, v] of Object.entries(draft)) {
if (data.settings[k] !== v) diff[k] = v;
}
if (Object.keys(diff).length === 0) {
pushToast("info", "No changes to save.");
return;
}
const res = await AdminApi.updateSettings(diff);
setData(res);
setDraft({ ...res.settings });
pushToast("success", t("admin.settings_saved"));
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed";
pushToast("error", msg);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<Spinner /> <span className="ml-2 text-sm text-fg-muted">{t("common.loading")}</span>
</div>
);
}
if (!data) {
return <p className="text-sm text-fg-muted">{t("common.no_data")}</p>;
}
const groupOrder = ["llm", "embeddings", "qdrant", "ui", "game", "other"];
const groupLabelKey: Record<string, string> = {
llm: "admin.group_llm",
embeddings: "admin.group_embeddings",
qdrant: "admin.group_qdrant",
ui: "admin.group_ui",
game: "admin.group_game",
other: "common.details",
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-fg">{t("admin.tab_settings")}</h2>
<Button onClick={handleSave} loading={saving}>
{t("common.save")}
</Button>
</div>
{groupOrder.map((g) => {
const entries = grouped[g];
if (!entries || entries.length === 0) return null;
return (
<Card key={g} title={t(groupLabelKey[g] || "common.details")}>
<div className="grid gap-3 sm:grid-cols-2">
{entries.map(({ key, description }) => (
<Input
key={key}
label={key}
hint={description}
value={draft[key] ?? ""}
onChange={(e) => setDraft((d) => ({ ...d, [key]: e.target.value }))}
/>
))}
</div>
</Card>
);
})}
</div>
);
}