feat(web): add KG extraction settings to system settings modal and tighten code block spacing

- Knowledge Graph section: provider/model picker + min confidence threshold
- Saves via builtin tools API (knowledge_graph_search settings)
- Reduce code block vertical margin and padding in markdown renderer
This commit is contained in:
viettranx committed 2026-03-29 09:24:06 +07:00
1 parent 028ea8a7d8
commit db03bf92e0
5 files changed
+126 -7

No files matched your search

@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
Settings2, Loader2, Save, AlertTriangle, Info, ExternalLink,
Brain, Eye, MessageSquareText, Archive, Clock, Hash, CheckCircle2, XCircle,
Brain, Eye, MessageSquareText, Archive, Clock, Hash, CheckCircle2, XCircle, Network,
} from "lucide-react";
import { Link } from "react-router";
import {
@@ -74,6 +74,9 @@ interface InitState {
compThreshold: string;
compKeepRecent: string;
compMaxTokens: string;
kgProvider: string;
kgModel: string;
kgMinConfidence: string;
}
const DEFAULTS: InitState = {
@@ -82,6 +85,7 @@ const DEFAULTS: InitState = {
toolStatus: true, blockReply: false, intentClassify: true,
compProvider: "", compModel: "",
compThreshold: "", compKeepRecent: "", compMaxTokens: "",
kgProvider: "", kgModel: "", kgMinConfidence: "0.75",
};
function parseBool(v: string | undefined, fallback: boolean): boolean {
@@ -117,7 +121,15 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
const [compKeepRecent, setCompKeepRecent] = useState("");
const [compMaxTokens, setCompMaxTokens] = useState("");
const applyConfigs = useCallback((configs: Record<string, string>) => {
// Knowledge Graph
const [kgProvider, setKgProvider] = useState("");
const [kgModel, setKgModel] = useState("");
const [kgMinConfidence, setKgMinConfidence] = useState("0.75");
const applyConfigs = useCallback((
configs: Record<string, string>,
kgSettings?: { extraction_provider?: string; extraction_model?: string; min_confidence?: number },
) => {
const s: InitState = {
embProvider: configs["embedding.provider"] ?? "",
embModel: configs["embedding.model"] ?? "",
@@ -131,6 +143,9 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
compThreshold: configs["compaction.threshold"] ?? "",
compKeepRecent: configs["compaction.keep_recent"] ?? "",
compMaxTokens: configs["compaction.max_tokens"] ?? "",
kgProvider: kgSettings?.extraction_provider ?? "",
kgModel: kgSettings?.extraction_model ?? "",
kgMinConfidence: String(kgSettings?.min_confidence ?? 0.75),
};
setInit(s);
setEmbProvider(s.embProvider);
@@ -145,15 +160,22 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
setCompThreshold(s.compThreshold);
setCompKeepRecent(s.compKeepRecent);
setCompMaxTokens(s.compMaxTokens);
setKgProvider(s.kgProvider);
setKgModel(s.kgModel);
setKgMinConfidence(s.kgMinConfidence);
resetEmb();
}, [resetEmb]);
useEffect(() => {
if (!open) return;
setLoading(true);
http
.get<Record<string, string>>("/v1/system-configs")
.then(applyConfigs)
Promise.all([
http.get<Record<string, string>>("/v1/system-configs"),
http.get<{ settings?: Record<string, unknown> }>("/v1/tools/builtin/knowledge_graph_search")
.then((r) => r.settings as { extraction_provider?: string; extraction_model?: string; min_confidence?: number } | undefined)
.catch(() => undefined),
])
.then(([configs, kgSettings]) => applyConfigs(configs, kgSettings))
.catch((err) => toast.error(err instanceof Error ? err.message : t("loadFailed")))
.finally(() => setLoading(false));
}, [open, http, applyConfigs, t]);
@@ -195,6 +217,19 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
for (const [key, value] of Object.entries(updates)) {
await http.put(`/v1/system-configs/${key}`, { value });
}
// KG extraction — save via builtin tools API
const kgChanged = kgProvider !== init.kgProvider || kgModel !== init.kgModel || kgMinConfidence !== init.kgMinConfidence;
if (kgChanged) {
await http.put("/v1/tools/builtin/knowledge_graph_search", {
settings: {
extraction_provider: kgProvider,
extraction_model: kgModel,
min_confidence: Number(kgMinConfidence) || 0.75,
extract_on_memory_write: !!(kgProvider && kgModel),
},
});
}
toast.success(t("saved"));
onOpenChange(false);
} catch (err) {
@@ -344,6 +379,51 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
</CardContent>
</Card>
{/* ── Knowledge Graph Extraction ── */}
<Card className="border-violet-200 dark:border-violet-800">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Network className="h-4 w-4 text-violet-500" />
{t("kg.title")}
</CardTitle>
<CardDescription>{t("kg.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-0">
<ProviderModelSelect
provider={kgProvider}
onProviderChange={(v) => { setKgProvider(v); setKgModel(""); }}
model={kgModel}
onModelChange={setKgModel}
allowEmpty
providerLabel={t("kg.provider")}
modelLabel={t("kg.model")}
providerTip={t("kg.providerTip")}
modelTip={t("kg.modelTip")}
providerPlaceholder={t("kg.providerPlaceholder")}
modelPlaceholder={t("kg.modelPlaceholder")}
/>
<div className="space-y-1.5">
<Label htmlFor="kgMinConf" className="text-xs">{t("kg.minConfidence")}</Label>
<Input
id="kgMinConf"
type="number"
min={0} max={1} step={0.05}
placeholder="0.75"
value={kgMinConfidence}
onChange={(e) => setKgMinConfidence(e.target.value)}
className="max-w-[120px] text-base md:text-sm"
/>
<p className="text-xs text-muted-foreground">{t("kg.minConfidenceHint")}</p>
</div>
<div className="flex items-start gap-2 rounded-md border border-violet-200 bg-violet-50 px-3 py-2 text-xs text-violet-700 dark:border-violet-800 dark:bg-violet-950/30 dark:text-violet-300">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{t("kg.info")}</span>
</div>
</CardContent>
</Card>
{/* ── UX Behavior — uses FeatureSwitchGroup (same as config page) ── */}
<FeatureSwitchGroup
title={t("ux.title")}
@@ -33,7 +33,7 @@ function CodeBlock({
const lang = className?.replace("language-", "") ?? "";
return (
<div className="not-prose group relative my-6 overflow-hidden rounded-lg border border-border/60">
<div className="not-prose group relative my-3 overflow-hidden rounded-lg border border-border/60">
<div className="flex items-center justify-between border-b border-border/40 bg-muted/70 px-3 py-1.5 text-[11px] font-medium tracking-wide text-muted-foreground uppercase">
<span>{lang || "code"}</span>
<button
@@ -45,7 +45,7 @@ function CodeBlock({
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</button>
</div>
<pre className="overflow-x-auto bg-muted/30 p-4 text-[13px] leading-relaxed text-foreground whitespace-pre">
<pre className="overflow-x-auto bg-muted/30 p-3 text-[13px] leading-normal text-foreground whitespace-pre">
<code className={className} style={{ fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, monospace", wordWrap: "normal", overflowWrap: "normal" }}>{children}</code>
</pre>
</div>
@@ -40,6 +40,19 @@
"intentClassifyHint": "Classify user intent before routing to the agent.",
"intentClassifyInfo": "Messages are classified to optimize agent routing and behavior."
},
"kg": {
"title": "Knowledge Graph",
"description": "Extract entities and relationships from agent memory into a searchable knowledge graph.",
"provider": "Extraction Provider",
"model": "Extraction Model",
"providerTip": "LLM provider used for extracting entities from memory.",
"modelTip": "Model for entity/relation extraction (e.g. claude-sonnet-4-20250514).",
"providerPlaceholder": "Select provider",
"modelPlaceholder": "Select model",
"minConfidence": "Min Confidence",
"minConfidenceHint": "Minimum confidence threshold (0–1) for extracted entities.",
"info": "Automatically extracts entities and relationships when agents write to memory. Leave provider empty to disable."
},
"compaction": {
"title": "Pending Message Compaction",
"description": "Summarize long pending message history to stay within context limits.",
@@ -40,6 +40,19 @@
"intentClassifyHint": "Phân loại ý định người dùng trước khi chuyển tới agent.",
"intentClassifyInfo": "Tin nhắn được phân loại để tối ưu định tuyến agent."
},
"kg": {
"title": "Đồ thị tri thức",
"description": "Trích xuất thực thể và quan hệ từ bộ nhớ agent vào đồ thị tri thức có thể tìm kiếm.",
"provider": "Provider trích xuất",
"model": "Model trích xuất",
"providerTip": "Provider LLM dùng để trích xuất thực thể từ bộ nhớ.",
"modelTip": "Model trích xuất thực thể/quan hệ (vd: claude-sonnet-4-20250514).",
"providerPlaceholder": "Chọn provider",
"modelPlaceholder": "Chọn model",
"minConfidence": "Độ tin cậy tối thiểu",
"minConfidenceHint": "Ngưỡng độ tin cậy (0–1) cho thực thể được trích xuất.",
"info": "Tự động trích xuất thực thể và quan hệ khi agent ghi vào bộ nhớ. Để trống provider để tắt."
},
"compaction": {
"title": "Nén tin nhắn chờ",
"description": "Tóm tắt lịch sử tin nhắn chờ dài để nằm trong giới hạn context.",
@@ -40,6 +40,19 @@
"intentClassifyHint": "在路由到Agent之前分类用户意图。",
"intentClassifyInfo": "消息被分类以优化Agent路由和行为。"
},
"kg": {
"title": "知识图谱",
"description": "从Agent记忆中提取实体和关系到可搜索的知识图谱。",
"provider": "提取Provider",
"model": "提取模型",
"providerTip": "用于从记忆中提取实体的LLM Provider。",
"modelTip": "实体/关系提取模型(如claude-sonnet-4-20250514)。",
"providerPlaceholder": "选择Provider",
"modelPlaceholder": "选择模型",
"minConfidence": "最低置信度",
"minConfidenceHint": "提取实体的最低置信度阈值(0–1)。",
"info": "当Agent写入记忆时自动提取实体和关系。留空Provider以禁用。"
},
"compaction": {
"title": "待处理消息压缩",
"description": "压缩长待处理消息历史以保持在上下文限制内。",