rebase
This commit is contained in:
132
frontend/src/components/admin/SettingsPanel.tsx
Normal file
132
frontend/src/components/admin/SettingsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user