PMFlow/frontend/src/pages/AIConfigPage.tsx

1714 lines
72 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import {
Settings as SettingsIcon, Loader2, CheckCircle2, AlertCircle, AlertTriangle,
MessageSquare, Activity, Github, Globe,
ChevronRight, Info, Sparkles, Building2, Upload, Link2, RefreshCw, Trash2,
Files, LayoutTemplate, History, ShieldCheck, Settings2
} from "lucide-react";
import { apiBaseURL, getAuthToken } from "@/lib/api";
import {
Card, CardContent, CardDescription, CardHeader, CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import PromptRoleSettings from "@/components/ai-steward/PromptRoleSettings";
import {
FEISHU_EVENT_MODE_OPTIONS,
type FeishuEventMode,
getFeishuEventModeLabel,
} from "@/lib/feishuEventMode";
// ==================== Types ====================
interface IntegrationSource {
id: string;
name: string;
description: string;
icon: any;
status: "available" | "configured" | "needs_setup" | "unavailable";
priority: "high" | "medium" | "low";
estimated_benefit: string;
setup_time: string;
auto_detected: boolean;
config_fields?: ConfigField[];
setup_guide?: string[];
}
interface ConfigField {
key: string;
label: string;
type: "text" | "password" | "url" | "toggle";
placeholder?: string;
required: boolean;
description?: string;
}
interface AIConfigSuggestion {
recommended_sources: string[];
reasoning: string;
estimated_impact: string;
}
interface ConfigStatus {
feishu: {
configured: boolean;
app_id?: string;
event_mode?: FeishuEventMode;
};
wecom: {
configured: boolean;
corp_id?: string;
agent_id?: string;
token_preview?: string;
encoding_aes_key_preview?: string;
};
github: { configured: boolean; repos?: string[] };
browser: { installed: boolean };
hn_monitor: { active: boolean; last_run?: string };
}
interface KnowledgeDoc {
id: number;
title: string;
file_type: string;
source_type: string;
file_url?: string | null;
status: "processing" | "complete" | "failed";
created_at?: string | null;
last_fetched_at?: string | null;
error_message?: string | null;
}
export default function AIConfigPage() {
const navigate = useNavigate();
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({
app_id: "",
app_secret: "",
event_mode: "long_connection" as FeishuEventMode
});
// WeChat Work config form
const [wecomConfig, setWecomConfig] = useState({
corp_id: "",
agent_id: "",
app_secret: "",
token: "",
encoding_aes_key: ""
});
// GitHub config form
const [githubConfig, setGithubConfig] = useState({
repo_url: "",
webhook_url: "",
access_token: ""
});
// Monitor settings
const [monitorSettings, setMonitorSettings] = useState({
hn_enabled: false,
hn_interval: 30,
browser_enabled: false
});
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | 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 [editingGithub, setEditingGithub] = useState(false);
const governanceTools = [
{
id: "brief-center",
name: "增量任务包中心",
description: "生成与查看任务包卡片",
to: "/ai-config/task-brief-center",
icon: Files,
},
{
id: "brief-template",
name: "任务包模板管理",
description: "统一维护模板与规范",
to: "/ai-config/task-brief-templates",
icon: LayoutTemplate,
},
{
id: "brief-runs",
name: "任务包生成记录",
description: "查看生成历史与状态",
to: "/ai-config/task-brief-runs",
icon: History,
},
{
id: "contract-board",
name: "契约同步看板",
description: "检查契约文档同步状态",
to: "/ai-config/contract-sync-board",
icon: ShieldCheck,
},
{
id: "model-routing",
name: "模型路由配置",
description: "动态切换函数模型路由",
to: "/ai-config/model-routing-config",
icon: Settings2,
},
];
// ==================== Integration Sources ====================
const integrationSources: IntegrationSource[] = [
{
id: "feishu",
name: "飞书群聊",
description: "自动捕获群聊中的需求讨论,AI 分析价值",
icon: MessageSquare,
status: status?.feishu?.configured ? "configured" : "needs_setup",
priority: "high",
estimated_benefit: "每天自动捕获 5-10 个需求信号",
setup_time: "5 分钟",
auto_detected: true,
config_fields: [
{ key: "app_id", label: "应用编号", type: "text", placeholder: "cli_xxxxxxxxxxxxx", required: true },
{ key: "app_secret", label: "应用密钥", type: "password", placeholder: "应用密钥", required: true },
],
setup_guide: [
"访问 https://open.feishu.cn/ 创建自建应用",
"在「凭证与基础信息」获取应用编号和应用密钥",
"飞书接入已统一为长连接模式,无需再配置回调地址",
"添加权限:im:message, im:message:group_at_msg, im:chat",
"订阅事件:接收消息 v1 (message.receive_v1)",
"保存后后端会自动重启飞书长连接"
]
},
{
id: "wecom",
name: "企业微信",
description: "自动捕获企业微信群消息,AI 分析需求价值",
icon: Building2,
status: status?.wecom?.configured ? "configured" : "needs_setup",
priority: "high",
estimated_benefit: "每天自动捕获 5-10 个需求信号",
setup_time: "5 分钟",
auto_detected: true,
config_fields: [
{ key: "corp_id", label: "企业编号", type: "text", placeholder: "wwxxxxxxxxxxxxxx", required: true },
{ key: "agent_id", label: "应用编号", type: "text", placeholder: "1000002", required: true },
{ key: "app_secret", label: "应用密钥", type: "password", placeholder: "应用密钥", required: true },
{ key: "token", label: "验证令牌", type: "text", placeholder: "回调验证令牌", required: true },
{ key: "encoding_aes_key", label: "消息加密密钥", type: "text", placeholder: "消息加密密钥(43位)", required: true }
],
setup_guide: [
"访问 https://work.weixin.qq.com/ 创建应用",
"在「应用管理」获取应用编号和应用密钥",
"在「我的企业」获取企业编号",
"在「应用管理」-「接收消息」设置验证令牌和消息加密密钥",
"添加权限:通讯录权限、消息权限",
"点击保存后,复制回调地址到企业微信配置"
]
},
{
id: "hn",
name: "技术资讯监控(HN)",
description: "定时抓取技术前沿,分析行业趋势",
icon: Activity,
status: status?.hn_monitor?.active ? "configured" : "available",
priority: "medium",
estimated_benefit: "每周捕获 2-3 个高价值技术趋势",
setup_time: "1 分钟",
auto_detected: true,
config_fields: [
{ key: "hn_interval", label: "抓取间隔(分钟)", type: "text", placeholder: "30", required: false }
]
},
{
id: "github",
name: "代码仓监控",
description: "跟踪竞品仓库、上游依赖更新",
icon: Github,
status: status?.github?.configured ? "configured" : "available",
priority: "medium",
estimated_benefit: "及时获知竞品动态和依赖更新",
setup_time: "10 分钟",
auto_detected: true,
config_fields: [
{ key: "repo_url", label: "仓库地址", type: "url", placeholder: "https://github.com/user/repo", required: true },
{ key: "access_token", label: "访问令牌", type: "password", placeholder: "ghp_xxxxxxxxxxxx", required: false }
]
},
{
id: "browser",
name: "浏览器扩展",
description: "监控访问的技术文档、竞品页面",
icon: Globe,
status: status?.browser?.installed ? "configured" : "needs_setup",
priority: "low",
estimated_benefit: "自动捕获浏览时的有价值内容",
setup_time: "2 分钟",
auto_detected: false,
config_fields: [],
setup_guide: [
"从 Chrome 应用商店安装 PMFlow 扩展",
"登录您的 PMFlow 账号",
"扩展会自动监控技术文档和竞品页面"
]
}
];
// ==================== 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);
}
} 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 },
browser: { installed: false },
hn_monitor: { active: false }
});
loadStatus();
}, []);
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]);
// ==================== Helper Components ====================
const StatusBadge = ({ status }: { status: IntegrationSource["status"] }) => {
const config = {
configured: { label: "已配置", variant: "default" as const, icon: CheckCircle2, color: "text-green-600" },
available: { label: "可用", variant: "secondary" as const, icon: Sparkles, color: "text-blue-600" },
needs_setup: { label: "待配置", variant: "outline" as const, icon: AlertTriangle, color: "text-orange-600" },
unavailable: { label: "不可用", variant: "secondary" as const, icon: AlertCircle, color: "text-slate-400" }
};
const { label, variant, icon: Icon, color } = config[status];
return (
<Badge variant={variant} className="gap-1">
<Icon className={`w-3 h-3 ${color}`} />
{label}
</Badge>
);
};
const PriorityBadge = ({ priority }: { priority: IntegrationSource["priority"] }) => {
const config = {
high: { label: "高优先级", className: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300" },
medium: { label: "中优先级", className: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" },
low: { label: "低优先级", className: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300" }
};
const { label, className } = config[priority];
return <span className={`px-2 py-0.5 rounded text-xs ${className}`}>{label}</span>;
};
// ==================== Render ====================
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-3 dark:from-slate-950 dark:to-slate-900 sm:p-4 md:p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<div className="flex items-center justify-center gap-3 mb-4">
<SettingsIcon className="w-10 h-10 text-violet-500" />
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">
AI 智能配置中心
</h1>
</div>
<p className="text-slate-600 dark:text-slate-400">
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>
)}
<Card className="mb-6 border-slate-200/80 bg-white/80 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/70">
<CardHeader>
<CardTitle className="text-lg">配置治理工具</CardTitle>
<CardDescription>新增治理页面统一收敛到智能配置中心入口,避免侧栏信息过载。</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
{governanceTools.map(({ id, name, description, to, icon: Icon }) => (
<Link
key={id}
to={to}
className="group rounded-2xl border border-slate-200 bg-white p-3 transition-all hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:hover:border-blue-800"
>
<div className="mb-2 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
<Icon className="h-4 w-4" />
</div>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">{name}</p>
<p className="mt-1 text-xs leading-relaxed text-slate-500 dark:text-slate-400">{description}</p>
</Link>
))}
</div>
</CardContent>
</Card>
{/* 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>
{/* Overview Tab */}
<TabsContent value="overview" className="mt-6">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{integrationSources.map((source) => {
const Icon = source.icon;
return (
<Card key={source.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
source.status === "configured" ? "bg-green-100 dark:bg-green-900/30" :
source.status === "needs_setup" ? "bg-orange-100 dark:bg-orange-900/30" :
"bg-slate-100 dark:bg-slate-800"
}`}>
<Icon className={`w-5 h-5 ${
source.status === "configured" ? "text-green-600 dark:text-green-400" :
source.status === "needs_setup" ? "text-orange-600 dark:text-orange-400" :
"text-slate-600 dark:text-slate-400"
}`} />
</div>
<div>
<CardTitle className="text-lg">{source.name}</CardTitle>
<CardDescription className="text-sm">{source.description}</CardDescription>
</div>
</div>
<StatusBadge status={source.status} />
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500">优先级</span>
<PriorityBadge priority={source.priority} />
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500">预计收益</span>
<span className="text-xs text-slate-700 dark:text-slate-300">{source.estimated_benefit}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500">配置耗时</span>
<span className="text-xs text-slate-700 dark:text-slate-300">{source.setup_time}</span>
</div>
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={() => handleConfigureClick(source.id)}
>
{source.status === "configured" ? "查看配置" : "开始配置"}
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</CardContent>
</Card>
);
})}
</div>
</TabsContent>
{/* Quick Configure Tab */}
<TabsContent value="configure" className="mt-6">
<div className="space-y-6">
{/* Feishu Config */}
<Card id="config-feishu" className={status?.feishu?.configured ? "border-green-200 dark:border-green-800" : ""}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-500" />
<CardTitle>飞书群聊集成</CardTitle>
{status?.feishu?.configured && (
<Badge className="ml-2" variant="default">已配置</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowFeishuGuide(!showFeishuGuide)}
>
{showFeishuGuide ? "收起指南" : "配置指南"}
<ChevronRight className={`w-4 h-4 ml-1 transition-transform ${showFeishuGuide ? "rotate-90" : ""}`} />
</Button>
</div>
<CardDescription>
AI 自动从飞书群聊捕获需求信号,无需手动录入
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Setup Guide */}
{showFeishuGuide && (
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-3 flex items-center gap-2">
<Info className="w-4 h-4" />
飞书配置指南(6 步完成)
</h4>
<div className="space-y-2">
{integrationSources.find(s => s.id === "feishu")?.setup_guide?.map((step, index) => (
<div key={index} className="flex items-start gap-2">
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-blue-500 text-white text-xs flex items-center justify-center">
{index + 1}
</div>
<span className="text-sm text-blue-800 dark:text-blue-200">{step}</span>
</div>
))}
</div>
<div className="mt-3 p-2 bg-amber-50 dark:bg-amber-900/20 rounded text-xs text-amber-800 dark:text-amber-200">
⚠️ 配置完成后,请点击下方"测试连接"按钮验证
</div>
</div>
)}
{status?.feishu?.configured && !editingFeishu ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<CheckCircle2 className="w-4 h-4" />
<span className="font-medium">飞书已配置</span>
{status.feishu.app_id && (
<span className="text-xs ml-2 text-green-600 dark:text-green-400">
(应用编号: {status.feishu.app_id})
</span>
)}
</div>
<p className="text-xs text-green-700/80 dark:text-green-300/80 mt-2">
当前接收模式:{getFeishuEventModeLabel(status.feishu.event_mode || "long_connection")}
</p>
<div className="mt-3 flex gap-2">
<Button onClick={testFeishuConnection} disabled={checking === "feishu-test"} size="sm">
{checking === "feishu-test" ? <Loader2 className="w-4 h-4 animate-spin" /> : "测试连接"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setEditingFeishu(true);
void loadStatus();
}}
>
修改配置
</Button>
<Button
variant="outline"
size="sm"
onClick={deleteFeishuConfig}
disabled={checking === "feishu-delete"}
className="text-red-600 hover:text-red-700"
>
{checking === "feishu-delete" ? <Loader2 className="w-4 h-4 animate-spin" /> : "删除配置"}
</Button>
</div>
</div>
) : (
<div className="space-y-3">
{status?.feishu?.configured && (
<div className="p-2 rounded bg-amber-50 dark:bg-amber-900/20 text-xs text-amber-800 dark:text-amber-200">
正在修改已有配置:已自动回填当前配置,可直接核对并修改后保存。
</div>
)}
<div>
<Label className="mb-2 block">事件接收方式 *</Label>
<p className="text-xs text-slate-500 mb-2">
飞书接入已统一为 <code>long_connection</code>
</p>
<div className="grid grid-cols-1 gap-2">
{FEISHU_EVENT_MODE_OPTIONS.map((option) => (
<label key={option.value} className="flex items-center gap-2 rounded border px-3 py-2 text-sm cursor-pointer">
<input
type="radio"
name="feishu_event_mode"
value={option.value}
checked={feishuConfig.event_mode === option.value}
onChange={() => setFeishuConfig({ ...feishuConfig, event_mode: option.value })}
/>
<span>{option.label}</span>
<code className="text-[11px] text-slate-500">({option.value})</code>
</label>
))}
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<Label htmlFor="feishu_app_id">应用编号 *</Label>
<Input
id="feishu_app_id"
placeholder="cli_xxxxxxxxxxxxx"
value={feishuConfig.app_id}
onChange={(e) => setFeishuConfig({ ...feishuConfig, app_id: e.target.value })}
/>
</div>
<div>
<Label htmlFor="feishu_app_secret">应用密钥 *</Label>
<Input
id="feishu_app_secret"
type="password"
placeholder="应用密钥"
value={feishuConfig.app_secret}
onChange={(e) => setFeishuConfig({ ...feishuConfig, app_secret: e.target.value })}
/>
</div>
</div>
<div className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
已切换到飞书长连接模式。无需配置 Webhook、Encrypt Key 或 Verification Token。
</div>
<div className="flex gap-2">
<Button
onClick={saveFeishuConfig}
disabled={checking === "feishu" || !feishuConfig.app_id || !feishuConfig.app_secret}
className="w-full"
>
{checking === "feishu" ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
status?.feishu?.configured ? "更新配置" : "保存配置"
)}
</Button>
{status?.feishu?.configured && (
<Button
variant="outline"
onClick={() => setEditingFeishu(false)}
disabled={checking === "feishu"}
>
取消
</Button>
)}
</div>
</div>
)}
<div className="p-3 bg-slate-100 dark:bg-slate-800 rounded-lg text-xs text-slate-600 dark:text-slate-300">
当前选择的是「长连接」模式,无需配置 Webhook 请求地址。
</div>
</CardContent>
</Card>
{/* WeChat Work Config */}
<Card id="config-wecom" className={status?.wecom?.configured ? "border-green-200 dark:border-green-800" : ""}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-emerald-500" />
<CardTitle>企业微信集成</CardTitle>
{status?.wecom?.configured && (
<Badge className="ml-2" variant="default">已配置</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowWecomGuide(!showWecomGuide)}
>
{showWecomGuide ? "收起指南" : "配置指南"}
<ChevronRight className={`w-4 h-4 ml-1 transition-transform ${showWecomGuide ? "rotate-90" : ""}`} />
</Button>
</div>
<CardDescription>
AI 自动从企业微信群聊捕获需求信号,无需手动录入
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Setup Guide */}
{showWecomGuide && (
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg border border-emerald-200 dark:border-emerald-800">
<h4 className="font-semibold text-emerald-900 dark:text-emerald-100 mb-3 flex items-center gap-2">
<Info className="w-4 h-4" />
企业微信配置指南(6 步完成)
</h4>
<div className="space-y-2">
{integrationSources.find(s => s.id === "wecom")?.setup_guide?.map((step, index) => (
<div key={index} className="flex items-start gap-2">
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-emerald-500 text-white text-xs flex items-center justify-center">
{index + 1}
</div>
<span className="text-sm text-emerald-800 dark:text-emerald-200">{step}</span>
</div>
))}
</div>
<div className="mt-3 p-2 bg-amber-50 dark:bg-amber-900/20 rounded text-xs text-amber-800 dark:text-amber-200">
⚠️ 配置完成后,请点击下方"测试连接"按钮验证
</div>
</div>
)}
{status?.wecom?.configured && !editingWecom ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<CheckCircle2 className="w-4 h-4" />
<span className="font-medium">企业微信已配置</span>
{status.wecom.corp_id && (
<span className="text-xs ml-2 text-green-600 dark:text-green-400">
(企业编号: {status.wecom.corp_id})
</span>
)}
</div>
{status.wecom.agent_id && (
<p className="text-xs text-green-700/80 dark:text-green-300/80 mt-2">
应用编号:{status.wecom.agent_id}
</p>
)}
<p className="text-xs text-green-700/80 dark:text-green-300/80 mt-1">
Token: {status.wecom.token_preview || "未设置"},EncodingAESKey: {status.wecom.encoding_aes_key_preview || "未设置"}
</p>
<div className="mt-3 flex gap-2">
<Button onClick={testWecomConnection} disabled={checking === "wecom-test"} size="sm">
{checking === "wecom-test" ? <Loader2 className="w-4 h-4 animate-spin" /> : "测试连接"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setEditingWecom(true);
void loadStatus();
}}
>
修改配置
</Button>
<Button
variant="outline"
size="sm"
onClick={deleteWecomConfig}
disabled={checking === "wecom-delete"}
className="text-red-600 hover:text-red-700"
>
{checking === "wecom-delete" ? <Loader2 className="w-4 h-4 animate-spin" /> : "删除配置"}
</Button>
</div>
</div>
) : (
<div className="space-y-3">
{status?.wecom?.configured && (
<div className="p-2 rounded bg-amber-50 dark:bg-amber-900/20 text-xs text-amber-800 dark:text-amber-200">
正在修改已有配置:已自动回填当前配置,可直接核对并修改后保存。
</div>
)}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<Label htmlFor="wecom_corp_id">企业编号 *</Label>
<Input
id="wecom_corp_id"
placeholder="wwxxxxxxxxxxxxxx"
value={wecomConfig.corp_id}
onChange={(e) => setWecomConfig({ ...wecomConfig, corp_id: e.target.value })}
/>
</div>
<div>
<Label htmlFor="wecom_agent_id">应用编号 *</Label>
<Input
id="wecom_agent_id"
placeholder="1000002"
value={wecomConfig.agent_id}
onChange={(e) => setWecomConfig({ ...wecomConfig, agent_id: e.target.value })}
/>
</div>
</div>
<div>
<Label htmlFor="wecom_app_secret">应用密钥 *</Label>
<Input
id="wecom_app_secret"
type="password"
placeholder="应用密钥"
value={wecomConfig.app_secret}
onChange={(e) => setWecomConfig({ ...wecomConfig, app_secret: e.target.value })}
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<Label htmlFor="wecom_token">验证令牌 *</Label>
<Input
id="wecom_token"
placeholder="回调验证令牌"
value={wecomConfig.token}
onChange={(e) => setWecomConfig({ ...wecomConfig, token: e.target.value })}
/>
</div>
<div>
<Label htmlFor="wecom_encoding_aes_key">消息加密密钥 *</Label>
<Input
id="wecom_encoding_aes_key"
placeholder="消息加密密钥(43位)"
value={wecomConfig.encoding_aes_key}
onChange={(e) => setWecomConfig({ ...wecomConfig, encoding_aes_key: e.target.value })}
/>
</div>
</div>
<div className="flex gap-2">
<Button
onClick={saveWecomConfig}
disabled={checking === "wecom" || !wecomConfig.corp_id || !wecomConfig.agent_id || !wecomConfig.app_secret}
className="w-full"
>
{checking === "wecom" ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
status?.wecom?.configured ? "更新配置" : "保存配置"
)}
</Button>
{status?.wecom?.configured && (
<Button
variant="outline"
onClick={() => {
setEditingWecom(false);
void loadStatus();
}}
disabled={checking === "wecom"}
>
取消
</Button>
)}
</div>
</div>
)}
{/* 回调地址 */}
<div className="p-3 bg-slate-100 dark:bg-slate-800 rounded-lg">
<div className="flex items-center justify-between mb-2">
<Label className="text-sm">回调地址</Label>
<div className="flex items-center gap-2">
{wecomCopyTip && <span className="text-xs text-green-600">{wecomCopyTip}</span>}
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyWebhook(getWecomWebhookUrl())}
>
复制
</Button>
</div>
</div>
<code className="text-xs break-all block">{getWecomWebhookUrl()}</code>
<p className="text-xs text-slate-500 mt-2">
复制此回调地址到企业微信管理后台 - 应用管理 - 接收消息 - 请求地址
</p>
</div>
</CardContent>
</Card>
{/* HN Monitor */}
<Card id="config-hn">
<CardHeader>
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-purple-500" />
<CardTitle>技术资讯监控(HN)</CardTitle>
{status?.hn_monitor?.active && (
<Badge className="ml-auto" variant="default">运行中</Badge>
)}
</div>
<CardDescription>
定时抓取 HN 首页,AI 分析技术趋势
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>启用监控</Label>
<p className="text-xs text-slate-500">每 {monitorSettings.hn_interval} 分钟抓取一次</p>
</div>
<Switch
checked={monitorSettings.hn_enabled}
onCheckedChange={(checked) => {
if (checked) startHNMonitor();
setMonitorSettings({ ...monitorSettings, hn_enabled: checked });
}}
/>
</div>
{!monitorSettings.hn_enabled && (
<Button
onClick={startHNMonitor}
disabled={checking === "hn"}
className="w-full"
>
{checking === "hn" ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : "启动监控"}
</Button>
)}
</CardContent>
</Card>
{/* GitHub */}
<Card id="config-github" className={status?.github?.configured ? "border-green-200 dark:border-green-800" : ""}>
<CardHeader>
<div className="flex items-center gap-2">
<Github className="w-5 h-5 text-slate-700 dark:text-slate-300" />
<CardTitle>代码仓监控</CardTitle>
{status?.github?.configured && (
<Badge className="ml-2" variant="default">已配置</Badge>
)}
</div>
<CardDescription>
跟踪竞品仓库更新、上游依赖变更
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{status?.github?.configured && !editingGithub ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<CheckCircle2 className="w-4 h-4" />
<span className="font-medium">代码仓监控已配置</span>
{status.github.repos && (
<span className="text-xs ml-2 text-green-600 dark:text-green-400">
({status.github.repos.length} 个仓库)
</span>
)}
</div>
<div className="mt-3 flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setGithubConfig((prev) => ({
...prev,
repo_url: status?.github?.repos?.join(",") || prev.repo_url,
}));
setEditingGithub(true);
}}
>
修改配置
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setGithubConfig({ repo_url: "", webhook_url: "", access_token: "" });
setStatus(prev => ({ ...prev!, github: { configured: false } }));
setEditingGithub(false);
}}
className="text-red-600 hover:text-red-700"
>
删除配置
</Button>
</div>
</div>
) : (
<div className="space-y-3">
<div>
<Label htmlFor="github_repo_url">仓库地址 *</Label>
<Input
id="github_repo_url"
placeholder="https://github.com/user/repo"
value={githubConfig.repo_url}
onChange={(e) => setGithubConfig({ ...githubConfig, repo_url: e.target.value })}
/>
<p className="text-xs text-slate-500 mt-1">支持输入多个仓库,用逗号分隔</p>
</div>
<div>
<Label htmlFor="github_webhook_url">回调地址</Label>
<Input
id="github_webhook_url"
placeholder="https://your-domain.com/api/github/webhook"
value={githubConfig.webhook_url}
onChange={(e) => setGithubConfig({ ...githubConfig, webhook_url: e.target.value })}
/>
<p className="text-xs text-slate-500 mt-1">仓库侧配置的回调地址</p>
</div>
<div>
<Label htmlFor="github_token">访问令牌</Label>
<Input
id="github_token"
type="password"
placeholder="ghp_xxxxxxxxxxxx"
value={githubConfig.access_token}
onChange={(e) => setGithubConfig({ ...githubConfig, access_token: e.target.value })}
/>
<p className="text-xs text-slate-500 mt-1">用于获取仓库信息的个人访问令牌</p>
</div>
<Button
onClick={() => {
setMessage({ type: "success", text: "代码仓监控配置已保存" });
setStatus(prev => ({ ...prev!, github: { configured: true, repos: githubConfig.repo_url.split(",") } }));
setEditingGithub(false);
}}
disabled={!githubConfig.repo_url}
className="w-full"
>
{status?.github?.configured ? "更新配置" : "保存配置"}
</Button>
{status?.github?.configured && (
<Button
variant="outline"
onClick={() => setEditingGithub(false)}
className="w-full"
>
取消
</Button>
)}
</div>
)}
</CardContent>
</Card>
{/* Browser Extension */}
<Card id="config-browser">
<CardHeader>
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-green-500" />
<CardTitle>浏览器扩展</CardTitle>
<Badge className="ml-auto" variant="secondary">敬请期待</Badge>
</div>
<CardDescription>
自动监控访问的技术文档、竞品页面
</CardDescription>
</CardHeader>
<CardContent>
<div className="p-4 bg-slate-100 dark:bg-slate-800 rounded-lg text-center">
<Globe className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-sm text-slate-600 dark:text-slate-400">
浏览器扩展正在开发中
</p>
<p className="text-xs text-slate-500 mt-1">
支持功能:自动捕获页面内容、智能分析价值
</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Knowledge Base Tab */}
<TabsContent value="knowledge" className="mt-6">
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-violet-500" />
文件上传
</CardTitle>
<CardDescription>支持 PDF / Word / Markdown / TXT,上传后自动分块与向量化。</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input
type="file"
accept=".pdf,.doc,.docx,.md,.markdown,.txt"
onChange={(e) => setKnowledgeFile(e.target.files?.[0] || null)}
/>
<div className="flex items-center justify-between">
<div className="text-xs text-slate-500">
{knowledgeFile ? `已选择: ${knowledgeFile.name}` : "未选择文件"}
</div>
<Button onClick={uploadKnowledgeFile} disabled={!knowledgeFile || checking === "knowledge-upload"}>
{checking === "knowledge-upload" ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
上传中…
</>
) : (
"上传并处理"
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Link2 className="w-5 h-5 text-blue-500" />
URL 知识源
</CardTitle>
<CardDescription>添加公开文档 URL,系统自动抓取并向量化,可手动刷新。</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input
placeholder="URL 标题(可选)"
value={knowledgeURLTitle}
onChange={(e) => setKnowledgeURLTitle(e.target.value)}
/>
<Input
placeholder="https://example.com/docs"
value={knowledgeURL}
onChange={(e) => setKnowledgeURL(e.target.value)}
/>
<div className="flex justify-end">
<Button onClick={addKnowledgeURL} disabled={!knowledgeURL.trim() || checking === "knowledge-url"}>
{checking === "knowledge-url" ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
添加中…
</>
) : (
"Add URL"
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>知识库列表</CardTitle>
<CardDescription>处理中状态会每 3 秒自动刷新。</CardDescription>
</CardHeader>
<CardContent>
{knowledgeDocs.length === 0 ? (
<div className="text-sm text-slate-500">暂无知识库条目</div>
) : (
<div className="space-y-3">
{knowledgeDocs.map((doc) => (
<div key={doc.id} className="p-3 rounded-lg border border-slate-200 dark:border-slate-800">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="font-medium truncate">{doc.title}</div>
<div className="text-xs text-slate-500">
类型: {doc.file_type} · 来源: {doc.source_type}
</div>
<div className="text-xs text-slate-500">
上传时间: {doc.created_at ? new Date(doc.created_at).toLocaleString() : "-"}
{doc.last_fetched_at ? ` · 最近抓取: ${new Date(doc.last_fetched_at).toLocaleString()}` : ""}
</div>
{doc.file_url && (
<div className="text-xs text-blue-600 truncate">{doc.file_url}</div>
)}
{doc.error_message && (
<div className="text-xs text-red-600">{doc.error_message}</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge variant={doc.status === "complete" ? "default" : doc.status === "failed" ? "destructive" : "secondary"}>
{doc.status === "processing" ? "processing" : doc.status === "complete" ? "complete" : "failed"}
</Badge>
{doc.source_type === "url" && (
<Button
size="sm"
variant="outline"
onClick={() => refreshKnowledgeDoc(doc.id)}
disabled={checking === `knowledge-refresh-${doc.id}`}
>
{checking === `knowledge-refresh-${doc.id}` ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
</Button>
)}
<Button
size="sm"
variant="destructive"
onClick={() => deleteKnowledgeDoc(doc.id)}
disabled={checking === `knowledge-delete-${doc.id}`}
>
{checking === `knowledge-delete-${doc.id}` ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Trash2 className="w-3 h-3" />
)}
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
{/* Advanced Tab */}
<TabsContent value="advanced" className="mt-6">
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>自动检测规则</CardTitle>
<CardDescription>
AI 自动判断信号价值的阈值设置
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<Label>信号捕获阈值</Label>
<span className="text-sm text-slate-500">影响分 ≥ 0.7</span>
</div>
<p className="text-xs text-slate-500">
低于此阈值的信号将被忽略,减少噪音
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>冲突检测灵敏度</Label>
<span className="text-sm text-slate-500">中等</span>
</div>
<p className="text-xs text-slate-500">
控制认知冲突检测的严格程度
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>简报推送时间</Label>
<span className="text-sm text-slate-500">每天 9:00</span>
</div>
<p className="text-xs text-slate-500">
个性化每日简报的推送时间
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>数据同步设置</CardTitle>
<CardDescription>
控制数据同步频率和范围
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>实时同步</Label>
<p className="text-xs text-slate-500">重要事件立即推送</p>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<Label>历史数据导入</Label>
<p className="text-xs text-slate-500">导入过去 30 天的聊天记录</p>
</div>
<Switch />
</div>
<div className="flex items-center justify-between">
<div>
<Label>自动关联项目</Label>
<p className="text-xs text-slate-500">新信号自动关联到活跃项目</p>
</div>
<Switch defaultChecked />
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Prompt Profiles Tab */}
<TabsContent value="prompts" className="mt-6">
<PromptRoleSettings />
</TabsContent>
</Tabs>
</div>
</div>
);
}