This commit is contained in:
Mikan
2026-06-19 17:32:21 +03:00
parent 81bbf8aa69
commit 2167493887
6 changed files with 484 additions and 32 deletions

View File

@@ -6,7 +6,7 @@ import type { LlmLog, SettingsOut } from "@/types";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Card, CardBody, CardHeader } from "@/components/ui/Card";
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2 } from "lucide-react";
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2, Terminal, Wrench } from "lucide-react";
export function AdminPanelPage() {
const { t } = useTranslation();
@@ -22,6 +22,10 @@ export function AdminPanelPage() {
const [error, setError] = useState("");
const [embeddingTest, setEmbeddingTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingEmbeddings, setTestingEmbeddings] = useState(false);
const [llmTest, setLlmTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingLlm, setTestingLlm] = useState(false);
const [llmToolsTest, setLlmToolsTest] = useState<null | { ok: boolean; msg: string }>(null);
const [testingLlmTools, setTestingLlmTools] = useState(false);
const [userActionError, setUserActionError] = useState("");
useEffect(() => {
@@ -111,6 +115,92 @@ export function AdminPanelPage() {
}
};
// Build LLM overrides from current form values (excluding masked api_key).
// Used by both testLlm and testLlmTools so the operator can tweak base_url
// / model / api_key in the form and test before saving.
const buildLlmOverrides = (): Record<string, any> => {
const overrides: Record<string, any> = {};
for (const k of ["llm.base_url", "llm.api_key", "llm.model", "llm.request_timeout"]) {
const v = values[k];
if (v !== undefined && v !== null && !(typeof v === "string" && v.includes("***"))) {
overrides[k] = v;
}
}
return overrides;
};
const testLlm = async () => {
setError("");
setTestingLlm(true);
setLlmTest(null);
try {
const r = await adminApi.testLlm(buildLlmOverrides());
if (r.ok) {
setLlmTest({
ok: true,
msg: t("admin.llm_test_ok", {
latency: r.latency_ms ?? 0,
preview: (r.response_preview || "").slice(0, 80),
}),
});
} else {
setLlmTest({
ok: false,
msg: t("admin.llm_test_fail", { error: r.error || `(${r.error_type || "unknown"})` }),
});
}
} catch (err: any) {
setLlmTest({
ok: false,
msg: t("admin.llm_test_fail", { error: err.response?.data?.detail || err.message }),
});
} finally {
setTestingLlm(false);
}
};
const testLlmTools = async () => {
setError("");
setTestingLlmTools(true);
setLlmToolsTest(null);
try {
const r = await adminApi.testLlmTools(buildLlmOverrides());
if (r.ok && r.tool_calls_returned) {
// Model returned a proper tool_call — function-calling works.
setLlmToolsTest({
ok: true,
msg: t("admin.llm_test_tools_ok_with_call", {
name: r.tool_call_name || "?",
args: JSON.stringify(r.tool_call_args || {}),
latency: r.latency_ms ?? 0,
}),
});
} else if (r.ok && !r.tool_calls_returned) {
// Model responded but did NOT use the tool — function-calling is NOT
// supported. The fallback JSON parser will still work, but tool-based
// flows (orchestrator, step-writer) will be unreliable.
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_tools_ok_no_call", {
text: (r.text || "").slice(0, 120),
}),
});
} else {
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_fail", { error: r.error || `(${r.error_type || "unknown"})` }),
});
}
} catch (err: any) {
setLlmToolsTest({
ok: false,
msg: t("admin.llm_test_fail", { error: err.response?.data?.detail || err.message }),
});
} finally {
setTestingLlmTools(false);
}
};
const toggleUserActive = async (userId: string, currentActive: boolean) => {
setUserActionError("");
try {
@@ -217,6 +307,30 @@ export function AdminPanelPage() {
{t("admin.streaming")}
</label>
</div>
{/* LLM connectivity tests — runs against current form values
(so the operator can tweak base_url / model / api_key and
test BEFORE saving). */}
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button variant="ghost" onClick={testLlm} disabled={testingLlm}>
<Terminal size={14} className="mr-1" />
{testingLlm ? t("admin.llm_test_testing") : t("admin.llm_test")}
</Button>
<Button variant="ghost" onClick={testLlmTools} disabled={testingLlmTools}>
<Wrench size={14} className="mr-1" />
{testingLlmTools ? t("admin.llm_test_tools_testing") : t("admin.llm_test_tools")}
</Button>
{llmTest && (
<span className={`text-xs ${llmTest.ok ? "text-green-400" : "text-red-400"}`}>
{llmTest.msg}
</span>
)}
{llmToolsTest && (
<span className={`text-xs ${llmToolsTest.ok ? "text-green-400" : "text-red-400"}`}>
{llmToolsTest.msg}
</span>
)}
</div>
</CardBody>
</Card>