186 lines
6.8 KiB
TypeScript
186 lines
6.8 KiB
TypeScript
|
|
import { Brain, RotateCcw, Save } from "lucide-react";
|
|||
|
|
import React, { useEffect, useState } from "react";
|
|||
|
|
import { Button } from "../../../ui/button";
|
|||
|
|
import { SettingCard, SettingsSection } from "../../settings-ui";
|
|||
|
|
import {
|
|||
|
|
DEFAULT_PHASE1_SYSTEM,
|
|||
|
|
DEFAULT_PHASE2_SYSTEM,
|
|||
|
|
} from "../../../../infrastructure/ai/rcaAgent/rcaRunner";
|
|||
|
|
|
|||
|
|
const STORAGE_KEY = "netcatty_rca_prompts_v1";
|
|||
|
|
|
|||
|
|
interface RcaPromptsConfig {
|
|||
|
|
phase1?: string;
|
|||
|
|
phase2?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadConfig(): RcaPromptsConfig {
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|||
|
|
if (raw) return JSON.parse(raw);
|
|||
|
|
} catch (_) {}
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveConfig(cfg: RcaPromptsConfig) {
|
|||
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const RcaPromptSettings: React.FC = () => {
|
|||
|
|
const defaults = {
|
|||
|
|
phase1: DEFAULT_PHASE1_SYSTEM,
|
|||
|
|
phase2: DEFAULT_PHASE2_SYSTEM,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const [phase1, setPhase1] = useState("");
|
|||
|
|
const [phase2, setPhase2] = useState("");
|
|||
|
|
const [savedVersion, setSavedVersion] = useState<{ p1: string; p2: string }>({ p1: "", p2: "" });
|
|||
|
|
|
|||
|
|
// 加载 localStorage 中的自定义值(无则空字符串,表示"使用默认")
|
|||
|
|
useEffect(() => {
|
|||
|
|
const cfg = loadConfig();
|
|||
|
|
const p1 = cfg.phase1 ?? "";
|
|||
|
|
const p2 = cfg.phase2 ?? "";
|
|||
|
|
setPhase1(p1);
|
|||
|
|
setPhase2(p2);
|
|||
|
|
setSavedVersion({ p1, p2 });
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const dirty = phase1 !== savedVersion.p1 || phase2 !== savedVersion.p2;
|
|||
|
|
const phase1IsDefault = !phase1.trim();
|
|||
|
|
const phase2IsDefault = !phase2.trim();
|
|||
|
|
|
|||
|
|
const handleSave = () => {
|
|||
|
|
const cfg: RcaPromptsConfig = {
|
|||
|
|
phase1: phase1.trim() ? phase1 : undefined,
|
|||
|
|
phase2: phase2.trim() ? phase2 : undefined,
|
|||
|
|
};
|
|||
|
|
saveConfig(cfg);
|
|||
|
|
setSavedVersion({ p1: phase1, p2: phase2 });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleResetAll = () => {
|
|||
|
|
setPhase1("");
|
|||
|
|
setPhase2("");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SettingsSection
|
|||
|
|
anchorId="ai-rca-prompts"
|
|||
|
|
title="RCA 智能诊断提示词"
|
|||
|
|
>
|
|||
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|||
|
|
自定义 AI 根因分析(RCA)的系统提示词。留空表示使用内置默认值。
|
|||
|
|
Phase 1 是诊断命令规划,Phase 2 是根因综合分析。
|
|||
|
|
</p>
|
|||
|
|
<SettingCard divided={false}>
|
|||
|
|
<div className="flex items-center justify-between gap-3 mb-4">
|
|||
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|||
|
|
<Brain size={16} />
|
|||
|
|
<span>自定义提示词会覆盖内置默认值,点击"恢复默认"可清空编辑内容。</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Button
|
|||
|
|
variant="ghost"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={handleResetAll}
|
|||
|
|
disabled={phase1IsDefault && phase2IsDefault}
|
|||
|
|
>
|
|||
|
|
<RotateCcw size={14} className="mr-1" />
|
|||
|
|
全部恢复默认
|
|||
|
|
</Button>
|
|||
|
|
<Button size="sm" onClick={handleSave} disabled={!dirty}>
|
|||
|
|
<Save size={14} className="mr-1" />
|
|||
|
|
保存
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Phase 1 */}
|
|||
|
|
<div className="space-y-2 mb-5">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<label className="text-sm font-medium">
|
|||
|
|
Phase 1 — 诊断规划 Agent
|
|||
|
|
<span className="ml-2 text-xs text-muted-foreground font-normal">
|
|||
|
|
(AI 根据告警规划要执行的诊断命令)
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<div className="flex items-center gap-2 text-xs">
|
|||
|
|
{phase1IsDefault ? (
|
|||
|
|
<span className="text-green-600 dark:text-green-400">使用默认</span>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-amber-600 dark:text-amber-400">已自定义</span>
|
|||
|
|
)}
|
|||
|
|
{!phase1IsDefault && (
|
|||
|
|
<Button
|
|||
|
|
variant="ghost"
|
|||
|
|
size="sm"
|
|||
|
|
className="h-7 px-2 text-xs"
|
|||
|
|
onClick={() => setPhase1("")}
|
|||
|
|
>
|
|||
|
|
<RotateCcw size={12} className="mr-1" /> 恢复默认
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<textarea
|
|||
|
|
value={phase1}
|
|||
|
|
onChange={(e) => setPhase1(e.target.value)}
|
|||
|
|
placeholder={defaults.phase1}
|
|||
|
|
className="w-full h-56 rounded-md border border-input bg-background px-3 py-2 text-sm font-mono resize-y focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|||
|
|
spellCheck={false}
|
|||
|
|
/>
|
|||
|
|
{phase1IsDefault && (
|
|||
|
|
<details className="text-xs text-muted-foreground">
|
|||
|
|
<summary className="cursor-pointer hover:text-foreground">预览内置默认提示词</summary>
|
|||
|
|
<pre className="mt-2 p-3 bg-muted/50 rounded text-xs whitespace-pre-wrap max-h-48 overflow-auto font-mono leading-relaxed">{defaults.phase1}</pre>
|
|||
|
|
</details>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Phase 2 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<label className="text-sm font-medium">
|
|||
|
|
Phase 2 — 根因综合 Agent
|
|||
|
|
<span className="ml-2 text-xs text-muted-foreground font-normal">
|
|||
|
|
(AI 综合告警 + metrics + 命令输出,生成根因 + 建议)
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<div className="flex items-center gap-2 text-xs">
|
|||
|
|
{phase2IsDefault ? (
|
|||
|
|
<span className="text-green-600 dark:text-green-400">使用默认</span>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-amber-600 dark:text-amber-400">已自定义</span>
|
|||
|
|
)}
|
|||
|
|
{!phase2IsDefault && (
|
|||
|
|
<Button
|
|||
|
|
variant="ghost"
|
|||
|
|
size="sm"
|
|||
|
|
className="h-7 px-2 text-xs"
|
|||
|
|
onClick={() => setPhase2("")}
|
|||
|
|
>
|
|||
|
|
<RotateCcw size={12} className="mr-1" /> 恢复默认
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<textarea
|
|||
|
|
value={phase2}
|
|||
|
|
onChange={(e) => setPhase2(e.target.value)}
|
|||
|
|
placeholder={defaults.phase2}
|
|||
|
|
className="w-full h-56 rounded-md border border-input bg-background px-3 py-2 text-sm font-mono resize-y focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|||
|
|
spellCheck={false}
|
|||
|
|
/>
|
|||
|
|
{phase2IsDefault && (
|
|||
|
|
<details className="text-xs text-muted-foreground">
|
|||
|
|
<summary className="cursor-pointer hover:text-foreground">预览内置默认提示词</summary>
|
|||
|
|
<pre className="mt-2 p-3 bg-muted/50 rounded text-xs whitespace-pre-wrap max-h-48 overflow-auto font-mono leading-relaxed">{defaults.phase2}</pre>
|
|||
|
|
</details>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</SettingCard>
|
|||
|
|
</SettingsSection>
|
|||
|
|
);
|
|||
|
|
};
|