PMFlow/frontend/src/pages/ai-config/AIConfigPage.tsx

790 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Suspense, lazy, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import {
AlertCircle, CheckCircle2, ShieldCheck, Sparkles
} from "lucide-react";
import { apiBaseURL, getAuthToken } from "@/lib/api";
import { devSyncApi } from "@/api/devSync";
import {
Card, CardContent
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SectionLoadingShell } from "@/components/layout/LazyBoundary";
import { type FeishuEventMode } from "@/lib/feishuEventMode";
import { buildIntegrationSources } from "./constants";
import type {
AIConfigSuggestion,
ConfigStatus,
FeishuConfigForm,
GithubConfigForm,
KnowledgeDoc,
MonitorSettings,
PageMessage,
WecomConfigForm,
} from "./types";
const PromptRoleSettings = lazy(() => import("@/components/ai-steward/PromptRoleSettings"));
const GovernanceToolsCard = lazy(() => import("./components/GovernanceToolsCard"));
const AdvancedTab = lazy(() => import("./sections/AdvancedTab"));
const ConfigureTab = lazy(() => import("./sections/ConfigureTab"));
const KnowledgeTab = lazy(() => import("./sections/KnowledgeTab"));
const OverviewTab = lazy(() => import("./sections/OverviewTab"));
export default function AIConfigPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [, setLoading] = useState(true);
const [checking, setChecking] = useState<string | null>(null);
const [status, setStatus] = useState<ConfigStatus | null>(null);
const [suggestion, setSuggestion] = useState<AIConfigSuggestion | null>(null);
const [activeTab, setActiveTab] = useState("overview");
const [scrollToSection, setScrollToSection] = useState<string | null>(null);
// Feishu config form
const [feishuConfig, setFeishuConfig] = useState<FeishuConfigForm>({
app_id: "",
app_secret: "",
event_mode: "long_connection" as FeishuEventMode
});
// WeChat Work config form
const [wecomConfig, setWecomConfig] = useState<WecomConfigForm>({
corp_id: "",
agent_id: "",
app_secret: "",
token: "",
encoding_aes_key: ""
});
// GitHub config form
const [githubConfig, setGithubConfig] = useState<GithubConfigForm>({
install_url: ""
});
// Monitor settings
const [monitorSettings, setMonitorSettings] = useState<MonitorSettings>({
hn_enabled: false,
hn_interval: 30,
browser_enabled: false
});
const [message, setMessage] = useState<PageMessage | null>(null);
const [wecomCopyTip, setWecomCopyTip] = useState("");
const [knowledgeDocs, setKnowledgeDocs] = useState<KnowledgeDoc[]>([]);
const [knowledgeFile, setKnowledgeFile] = useState<File | null>(null);
const [knowledgeURL, setKnowledgeURL] = useState("");
const [knowledgeURLTitle, setKnowledgeURLTitle] = useState("");
// Guide expansion state
const [showFeishuGuide, setShowFeishuGuide] = useState(false);
const [showWecomGuide, setShowWecomGuide] = useState(false);
const [editingFeishu, setEditingFeishu] = useState(false);
const [editingWecom, setEditingWecom] = useState(false);
const integrationSources = buildIntegrationSources(status);
// ==================== API Calls ====================
const loadStatus = async () => {
setLoading(true);
try {
const token = getAuthToken();
if (!token) {
navigate("/login");
return;
}
// Check Feishu config
const feishuRes = await fetch(`${apiBaseURL}/feishu/config`, {
headers: { Authorization: `Bearer ${token}` }
});
if (feishuRes.ok) {
const feishuData = await feishuRes.json();
const mode = "long_connection" as FeishuEventMode;
setStatus(prev => ({
...prev!,
feishu: {
configured: feishuData.is_configured,
app_id: feishuData.app_id,
event_mode: mode,
},
}));
setFeishuConfig({
app_id: feishuData.app_id || "",
app_secret: feishuData.app_secret || "",
event_mode: mode,
});
}
// Check WeChat Work config
const wecomRes = await fetch(`${apiBaseURL}/wecom/config`, {
headers: { Authorization: `Bearer ${token}` }
});
if (wecomRes.ok) {
const wecomData = await wecomRes.json();
setStatus(prev => ({
...prev!,
wecom: {
configured: wecomData.is_configured,
corp_id: wecomData.corp_id,
agent_id: wecomData.agent_id,
token_preview: wecomData.token_preview || "",
encoding_aes_key_preview: wecomData.encoding_aes_key_preview || "",
},
}));
setWecomConfig({
corp_id: wecomData.corp_id || "",
agent_id: wecomData.agent_id || "",
app_secret: wecomData.app_secret || "",
token: wecomData.token || "",
encoding_aes_key: wecomData.encoding_aes_key || "",
});
}
// Get AI suggestions
const suggestRes = await fetch(`${apiBaseURL}/ai-steward/config/suggest`, {
headers: { Authorization: `Bearer ${token}` }
});
if (suggestRes.ok) {
const suggestData = await suggestRes.json();
setSuggestion(suggestData);
}
try {
const githubData = await devSyncApi.getGithubInstallations();
setStatus(prev => ({
...prev!,
github: {
configured: Boolean(githubData.installations?.length),
install_url: githubData.install_url,
callback_url: githubData.callback_url,
webhook_signature_configured: githubData.webhook_signature_configured,
app_slug: githubData.app_slug,
installations: githubData.installations.map((item) => ({
id: item.id,
installation_id: item.installation_id,
account_login: item.account_login,
account_type: item.account_type,
status: item.status,
last_synced_at: item.last_synced_at,
})),
recent_deliveries: githubData.recent_deliveries,
},
}));
setGithubConfig({
install_url: githubData.install_url || "",
});
} catch (err) {
console.warn("Failed to load GitHub App status:", err);
}
} catch (err) {
console.error("Failed to load status:", err);
} finally {
setLoading(false);
}
};
const saveFeishuConfig = async () => {
setChecking("feishu");
setMessage(null);
if (!feishuConfig.app_id || !feishuConfig.app_secret) {
setMessage({ type: "error", text: "请填写应用编号和应用密钥" });
setChecking(null);
return;
}
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/feishu/config`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(feishuConfig)
});
if (response.ok) {
setMessage({ type: "success", text: "飞书配置已保存" });
setEditingFeishu(false);
await loadStatus();
} else {
const error = await response.json();
setMessage({ type: "error", text: error.detail || "保存失败" });
}
} catch (err) {
setMessage({ type: "error", text: "网络错误,请重试" });
} finally {
setChecking(null);
}
};
const testFeishuConnection = async () => {
setChecking("feishu-test");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/feishu/test-connection`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
});
const data = await response.json();
if (data.status === "success") {
setMessage({ type: "success", text: "飞书连接测试成功!" });
} else {
setMessage({ type: "error", text: data.message || "连接测试失败" });
}
} catch (err) {
setMessage({ type: "error", text: "测试失败" });
} finally {
setChecking(null);
}
};
const deleteFeishuConfig = async () => {
if (!confirm("确定要删除飞书配置吗?删除后将无法从飞书群聊捕获信号。")) return;
setChecking("feishu-delete");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/feishu/config`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
setMessage({ type: "success", text: "配置已删除" });
setFeishuConfig({ app_id: "", app_secret: "", event_mode: "long_connection" });
setEditingFeishu(false);
await loadStatus();
} else {
setMessage({ type: "error", text: "删除失败" });
}
} catch (err) {
setMessage({ type: "error", text: "网络错误" });
} finally {
setChecking(null);
}
};
const saveWecomConfig = async () => {
setChecking("wecom");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/wecom/config`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(wecomConfig)
});
if (response.ok) {
setMessage({ type: "success", text: "企业微信配置已保存" });
setEditingWecom(false);
await loadStatus();
} else {
const error = await response.json();
setMessage({ type: "error", text: error.detail || "保存失败" });
}
} catch (err) {
setMessage({ type: "error", text: "网络错误,请重试" });
} finally {
setChecking(null);
}
};
const testWecomConnection = async () => {
setChecking("wecom-test");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/wecom/test-connection`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
});
const data = await response.json();
if (data.status === "success") {
setMessage({ type: "success", text: "企业微信连接测试成功!" });
} else {
setMessage({ type: "error", text: data.message || "连接测试失败" });
}
} catch (err) {
setMessage({ type: "error", text: "测试失败" });
} finally {
setChecking(null);
}
};
const deleteWecomConfig = async () => {
if (!confirm("确定要删除企业微信配置吗?删除后将无法从企业微信群聊捕获信号。")) return;
setChecking("wecom-delete");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/wecom/config`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
setMessage({ type: "success", text: "配置已删除" });
setWecomConfig({ corp_id: "", agent_id: "", app_secret: "", token: "", encoding_aes_key: "" });
setEditingWecom(false);
await loadStatus();
} else {
setMessage({ type: "error", text: "删除失败" });
}
} catch (err) {
setMessage({ type: "error", text: "网络错误" });
} finally {
setChecking(null);
}
};
const getPublicBaseUrl = () => {
const normalized = apiBaseURL.replace(/\/api\/?$/, "");
if (/^https?:\/\//i.test(normalized)) {
return normalized.replace(/\/$/, "");
}
return window.location.origin.replace(/\/$/, "");
};
const getWecomWebhookUrl = () => `${getPublicBaseUrl()}/wecom/webhook`;
const startHNMonitor = async () => {
setChecking("hn");
setMessage(null);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/ai-steward/signal/start-hn-monitor?interval_minutes=${monitorSettings.hn_interval}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
setMessage({ type: "success", text: "技术资讯监控已启动" });
setMonitorSettings(prev => ({ ...prev!, hn_enabled: true }));
} else {
setMessage({ type: "error", text: "启动失败" });
}
} catch (err) {
setMessage({ type: "error", text: "网络错误" });
} finally {
setChecking(null);
}
};
const copyText = async (text: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// fallback below
}
try {
const input = document.createElement("textarea");
input.value = text;
input.style.position = "fixed";
input.style.opacity = "0";
input.style.left = "-9999px";
document.body.appendChild(input);
input.focus();
input.select();
const ok = document.execCommand("copy");
document.body.removeChild(input);
return ok;
} catch {
return false;
}
};
const handleCopyWebhook = async (text: string) => {
const ok = await copyText(text);
if (ok) {
setMessage({ type: "success", text: "已复制到剪切板" });
setWecomCopyTip("已复制到剪切板");
setTimeout(() => setWecomCopyTip(""), 1500);
return;
}
setMessage({ type: "error", text: "复制失败,请手动复制地址" });
setWecomCopyTip("复制失败");
setTimeout(() => setWecomCopyTip(""), 1500);
};
const loadKnowledgeDocs = async () => {
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/rag/knowledge-base/docs`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) return;
const payload = await response.json();
const docs = (payload?.data ?? payload) || [];
setKnowledgeDocs(Array.isArray(docs) ? docs : []);
} catch (err) {
console.error("Failed to load knowledge docs:", err);
}
};
const uploadKnowledgeFile = async () => {
if (!knowledgeFile) {
setMessage({ type: "error", text: "请先选择文件" });
return;
}
setChecking("knowledge-upload");
try {
const token = getAuthToken();
const formData = new FormData();
formData.append("file", knowledgeFile);
const response = await fetch(`${apiBaseURL}/rag/knowledge-base/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: formData,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
setMessage({ type: "error", text: payload?.detail || "上传失败" });
return;
}
setKnowledgeFile(null);
setMessage({ type: "success", text: "文件已提交,正在向量化处理" });
await loadKnowledgeDocs();
} catch (err) {
setMessage({ type: "error", text: "上传失败,请检查网络" });
} finally {
setChecking(null);
}
};
const addKnowledgeURL = async () => {
if (!knowledgeURL.trim()) {
setMessage({ type: "error", text: "请输入 URL" });
return;
}
setChecking("knowledge-url");
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/rag/knowledge-base/urls`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
url: knowledgeURL.trim(),
title: knowledgeURLTitle.trim() || undefined,
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
setMessage({ type: "error", text: payload?.detail || "URL 添加失败" });
return;
}
setKnowledgeURL("");
setKnowledgeURLTitle("");
setMessage({ type: "success", text: "URL 已加入知识库,正在抓取与向量化" });
await loadKnowledgeDocs();
} catch (err) {
setMessage({ type: "error", text: "URL 添加失败,请稍后重试" });
} finally {
setChecking(null);
}
};
const refreshKnowledgeDoc = async (docId: number) => {
setChecking(`knowledge-refresh-${docId}`);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/rag/knowledge-base/docs/${docId}/refresh`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
setMessage({ type: "error", text: payload?.detail || "刷新失败" });
return;
}
setMessage({ type: "success", text: "已触发刷新,正在处理" });
await loadKnowledgeDocs();
} catch (err) {
setMessage({ type: "error", text: "刷新失败,请稍后重试" });
} finally {
setChecking(null);
}
};
const deleteKnowledgeDoc = async (docId: number) => {
if (!confirm("确认删除该知识库条目及其向量数据?")) return;
setChecking(`knowledge-delete-${docId}`);
try {
const token = getAuthToken();
const response = await fetch(`${apiBaseURL}/rag/knowledge-base/docs/${docId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
setMessage({ type: "error", text: payload?.detail || "删除失败" });
return;
}
setMessage({ type: "success", text: "已删除知识库条目" });
await loadKnowledgeDocs();
} catch (err) {
setMessage({ type: "error", text: "删除失败,请稍后重试" });
} finally {
setChecking(null);
}
};
// ==================== Initial Load ====================
// Scroll to section when tab changes to configure
useEffect(() => {
if (activeTab === "configure" && scrollToSection) {
// Small delay to ensure the tab content is rendered
setTimeout(() => {
const element = document.getElementById(`config-${scrollToSection}`);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
setScrollToSection(null);
}, 100);
}
}, [activeTab, scrollToSection]);
const handleConfigureClick = (sourceId: string) => {
setScrollToSection(sourceId);
setActiveTab("configure");
};
useEffect(() => {
setStatus({
feishu: { configured: false },
wecom: { configured: false },
github: { configured: false, installations: [], recent_deliveries: [] },
browser: { installed: false },
hn_monitor: { active: false }
});
loadStatus();
}, []);
useEffect(() => {
const requestedTab = searchParams.get("tab");
if (requestedTab && ["overview", "configure", "knowledge", "advanced", "prompts"].includes(requestedTab)) {
setActiveTab(requestedTab);
}
}, [searchParams]);
useEffect(() => {
if (activeTab !== "knowledge") return;
loadKnowledgeDocs();
}, [activeTab]);
useEffect(() => {
if (activeTab !== "knowledge") return;
if (!knowledgeDocs.some((item) => item.status === "processing")) return;
const timer = setInterval(() => {
loadKnowledgeDocs();
}, 3000);
return () => clearInterval(timer);
}, [activeTab, knowledgeDocs]);
const renderActiveTab = () => {
switch (activeTab) {
case "overview":
return (
<Suspense fallback={<SectionLoadingShell blocks={2} compact />}>
<OverviewTab integrationSources={integrationSources} onConfigureClick={handleConfigureClick} />
</Suspense>
);
case "configure":
return (
<Suspense fallback={<SectionLoadingShell blocks={2} compact />}>
<ConfigureTab
status={status}
checking={checking}
setChecking={setChecking}
integrationSources={integrationSources}
feishuConfig={feishuConfig}
setFeishuConfig={setFeishuConfig}
wecomConfig={wecomConfig}
setWecomConfig={setWecomConfig}
githubConfig={githubConfig}
monitorSettings={monitorSettings}
setMonitorSettings={setMonitorSettings}
showFeishuGuide={showFeishuGuide}
setShowFeishuGuide={setShowFeishuGuide}
showWecomGuide={showWecomGuide}
setShowWecomGuide={setShowWecomGuide}
editingFeishu={editingFeishu}
setEditingFeishu={setEditingFeishu}
editingWecom={editingWecom}
setEditingWecom={setEditingWecom}
wecomCopyTip={wecomCopyTip}
setMessage={setMessage}
loadStatus={loadStatus}
saveFeishuConfig={saveFeishuConfig}
testFeishuConnection={testFeishuConnection}
deleteFeishuConfig={deleteFeishuConfig}
saveWecomConfig={saveWecomConfig}
testWecomConnection={testWecomConnection}
deleteWecomConfig={deleteWecomConfig}
startHNMonitor={startHNMonitor}
handleCopyWebhook={handleCopyWebhook}
getWecomWebhookUrl={getWecomWebhookUrl}
/>
</Suspense>
);
case "knowledge":
return (
<Suspense fallback={<SectionLoadingShell blocks={2} compact />}>
<KnowledgeTab
checking={checking}
knowledgeDocs={knowledgeDocs}
knowledgeFile={knowledgeFile}
knowledgeURL={knowledgeURL}
knowledgeURLTitle={knowledgeURLTitle}
onKnowledgeFileChange={setKnowledgeFile}
onKnowledgeURLChange={setKnowledgeURL}
onKnowledgeURLTitleChange={setKnowledgeURLTitle}
onUploadKnowledgeFile={uploadKnowledgeFile}
onAddKnowledgeURL={addKnowledgeURL}
onRefreshKnowledgeDoc={refreshKnowledgeDoc}
onDeleteKnowledgeDoc={deleteKnowledgeDoc}
/>
</Suspense>
);
case "advanced":
return (
<Suspense fallback={<SectionLoadingShell blocks={2} compact />}>
<AdvancedTab />
</Suspense>
);
case "prompts":
return (
<TabsContent value="prompts" className="mt-6">
<Suspense fallback={<SectionLoadingShell blocks={2} compact />}>
<PromptRoleSettings />
</Suspense>
</TabsContent>
);
default:
return null;
}
};
// ==================== Render ====================
return (
<div className="min-h-screen bg-[#f7f9fd] p-3 dark:bg-slate-950 sm:p-4 md:p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-6 rounded-3xl border border-blue-100 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700 dark:bg-blue-900/35 dark:text-blue-200">
<Sparkles className="h-3.5 w-3.5" />
团队级 AI 运营
</div>
<h1 className="text-3xl font-bold tracking-tight text-slate-950 dark:text-slate-100">
AI 运营中心
</h1>
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-300">
管理团队 AI 策略、供应商连接、路由诊断和语义治理;这里不是项目级配置页。
</p>
</div>
<div className="grid gap-2 text-xs text-slate-500 sm:grid-cols-3 lg:w-[420px]">
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 dark:border-slate-800 dark:bg-slate-950">
<div className="font-semibold text-slate-900 dark:text-slate-100">策略</div>
<div className="mt-1">速度 / 质量 / 成本</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 dark:border-slate-800 dark:bg-slate-950">
<div className="font-semibold text-slate-900 dark:text-slate-100">连接</div>
<div className="mt-1">供应商 / 工具</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 dark:border-slate-800 dark:bg-slate-950">
<div className="font-semibold text-slate-900 dark:text-slate-100">治理</div>
<div className="mt-1">诊断 / 审计</div>
</div>
</div>
</div>
</div>
<div className="mb-6 flex items-start gap-3 rounded-2xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-900 dark:border-blue-900 dark:bg-blue-950/50 dark:text-blue-100">
<ShieldCheck className="mt-0.5 h-4 w-4 shrink-0" />
<p>
这些配置是团队级配置,会影响当前团队空间内所有项目;项目内只展示 AI 处理模式和可信状态,不展示底层模型参数。
</p>
</div>
{/* AI Suggestion Card */}
{suggestion && (
<Card className="mb-6 border-violet-200 dark:border-violet-800 bg-violet-50 dark:bg-violet-900/20">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<Sparkles className="w-5 h-5 text-violet-500 mt-0.5" />
<div>
<h3 className="font-semibold text-violet-900 dark:text-violet-100">AI 运营建议</h3>
<p className="text-sm text-violet-700 dark:text-violet-300 mt-1">{suggestion.reasoning}</p>
<p className="text-xs text-violet-600 dark:text-violet-400 mt-2">预期影响: {suggestion.estimated_impact}</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Message */}
{message && (
<div className={`mb-4 p-3 rounded-lg flex items-center gap-2 ${
message.type === "success"
? "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300"
: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300"
}`}>
{message.type === "success" ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<AlertCircle className="w-4 h-4" />
)}
{message.text}
</div>
)}
<Suspense fallback={<SectionLoadingShell className="mb-6 mt-0" blocks={3} compact />}>
<GovernanceToolsCard />
</Suspense>
{/* Main Content */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex w-full min-w-0 flex-nowrap gap-2 overflow-x-auto rounded-lg p-1 [-webkit-overflow-scrolling:touch]">
<TabsTrigger value="overview" className="shrink-0 whitespace-nowrap">运行总览</TabsTrigger>
<TabsTrigger value="configure" className="shrink-0 whitespace-nowrap">连接配置</TabsTrigger>
<TabsTrigger value="knowledge" className="shrink-0 whitespace-nowrap">知识治理</TabsTrigger>
<TabsTrigger value="advanced" className="shrink-0 whitespace-nowrap">高级设置</TabsTrigger>
<TabsTrigger value="prompts" className="shrink-0 whitespace-nowrap">Prompt 设置</TabsTrigger>
</TabsList>
{renderActiveTab()}
</Tabs>
</div>
</div>
);
}