790 lines
27 KiB
TypeScript
790 lines
27 KiB
TypeScript
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>
|
||
);
|
||
}
|