import { useState, useEffect } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Sparkles } from "lucide-react"; import { apiBaseURL, getAuthToken } from "@/lib/api"; import { Card, CardContent, } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAuthStore } from "@/stores/auth"; import { QAHistoryPanel } from "@/components/ai-steward/QAHistoryPanel"; import { parseUserRoles } from "@/api/users"; import { aiStewardApi, type LLMEngine, type LLMEngineUpsertRequest, } from "@/api/aiSteward"; import { DEFAULT_LLM_ENGINE_SETTINGS, ENGINE_PROVIDER_FALLBACK_PRESETS, } from "./constants"; import type { Blocker, CognitiveConflict, DailyBriefing, FeedbackCenterSourceGroup, FeedbackCenterSection, FeedbackCenterTrendWindowDays, EngineDataState, EngineForm, EngineMessage, EngineProviderPresets, ProjectFromTextResponse, AIStewardTab, } from "./types"; import FeatureOverviewCards from "./components/FeatureOverviewCards"; import BlockersTab from "./sections/BlockersTab"; import BriefingTab from "./sections/BriefingTab"; import ConflictsTab from "./sections/ConflictsTab"; import CreateProjectTab from "./sections/CreateProjectTab"; import EngineTab from "./sections/EngineTab"; import FeedbackCenterTab from "./sections/FeedbackCenterTab"; import OverviewTab from "./sections/OverviewTab"; import SignalsTab from "./sections/SignalsTab"; const AIS_STEWARD_TABS: AIStewardTab[] = [ "overview", "create", "signals", "conflicts", "blockers", "briefing", "qa", "feedback-center", "engine", ]; const FEEDBACK_SECTIONS: FeedbackCenterSection[] = ["overview", "metrics", "trend", "task", "qa", "signal"]; function normalizeTab(value: string | null): AIStewardTab { if (value && AIS_STEWARD_TABS.includes(value as AIStewardTab)) { return value as AIStewardTab; } return "overview"; } function normalizeSection(value: string | null): FeedbackCenterSection { if (value && FEEDBACK_SECTIONS.includes(value as FeedbackCenterSection)) { return value as FeedbackCenterSection; } return "overview"; } function normalizeTrendWindow(value: string | null): FeedbackCenterTrendWindowDays { return value === "30" ? 30 : 7; } function normalizeTrendSourceGroup(value: string | null): FeedbackCenterSourceGroup { if (value === "task" || value === "qa" || value === "signal") { return value; } return "all"; } export default function AIStewardPage() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const user = useAuthStore((s) => s.user); const canViewFeedbackCenter = parseUserRoles(user?.role).some((role) => role === "ADMIN" || role === "BOSS"); // Project creation state const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [projectResult, setProjectResult] = useState(null); const [error, setError] = useState(null); // AI Steward features state const [activeTab, setActiveTab] = useState(() => normalizeTab(searchParams.get("tab"))); const [feedbackSection, setFeedbackSection] = useState(() => normalizeSection(searchParams.get("section")) ); const [trendWindowDays, setTrendWindowDays] = useState(() => normalizeTrendWindow(searchParams.get("window")) ); const [trendSourceGroup, setTrendSourceGroup] = useState(() => normalizeTrendSourceGroup(searchParams.get("scope")) ); const [conflicts, setConflicts] = useState([]); const [blockers, setBlockers] = useState([]); const [briefing, setBriefing] = useState(null); const [briefingError, setBriefingError] = useState(null); const [featureLoading, setFeatureLoading] = useState(null); const [engineData, setEngineData] = useState({ active_engine_id: null, engines: [], settings: DEFAULT_LLM_ENGINE_SETTINGS, }); const [engineProviderPresets, setEngineProviderPresets] = useState( ENGINE_PROVIDER_FALLBACK_PRESETS ); const [engineForm, setEngineForm] = useState({ name: "", provider: "glm", api_key: "", model: ENGINE_PROVIDER_FALLBACK_PRESETS.glm.model, base_url: ENGINE_PROVIDER_FALLBACK_PRESETS.glm.base_url, }); const [engineEditingId, setEngineEditingId] = useState(null); const [engineMessage, setEngineMessage] = useState(null); // ==================== API Calls ==================== const loadConflicts = async () => { setFeatureLoading("conflicts"); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/cognitive-conflicts`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); setConflicts(data); } } catch (err) { console.error("Failed to load conflicts:", err); } finally { setFeatureLoading(null); } }; const loadBlockers = async () => { setFeatureLoading("blockers"); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/blockers/active`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); setBlockers(data.blockers || []); } } catch (err) { console.error("Failed to load blockers:", err); } finally { setFeatureLoading(null); } }; const loadBriefing = async () => { setFeatureLoading("briefing"); setBriefingError(null); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/daily-briefing`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const data = await response.json(); setBriefing(data); } else { const payload = await response.json().catch(() => null); const detail = typeof payload?.detail === "string" ? payload.detail : typeof payload?.message === "string" ? payload.message : ""; setBriefing(null); setBriefingError(detail || `获取简报失败 (${response.status})`); } } catch (err) { console.error("Failed to load briefing:", err); setBriefing(null); setBriefingError(err instanceof Error ? err.message : "获取简报失败"); } finally { setFeatureLoading(null); } }; const detectConflicts = async (projectId: number = 1) => { setFeatureLoading("detect-conflicts"); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/conflicts/detect`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ project_id: projectId, detect_type: "all" }), }); if (response.ok) { await loadConflicts(); return true; } } catch (err) { console.error("Failed to detect conflicts:", err); } finally { setFeatureLoading(null); } return false; }; const detectTaskBlockers = async () => { setFeatureLoading("detect-blockers"); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/blockers/detect-tasks`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { await loadBlockers(); return true; } } catch (err) { console.error("Failed to detect blockers:", err); } finally { setFeatureLoading(null); } return false; }; const startHNMonitor = async () => { setFeatureLoading("hn-monitor"); try { const token = getAuthToken(); const response = await fetch(`${apiBaseURL}/ai-steward/signal/start-hn-monitor?interval_minutes=30`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { return true; } } catch (err) { console.error("Failed to start HN monitor:", err); } finally { setFeatureLoading(null); } return false; }; const loadEngineConfigs = async () => { setFeatureLoading("engine-list"); try { const data = await aiStewardApi.listLlmEngines(); setEngineData({ active_engine_id: data?.active_engine_id ?? null, engines: data?.engines ?? [], settings: data?.settings ?? DEFAULT_LLM_ENGINE_SETTINGS, }); } catch (err) { console.error("Failed to load engine configs:", err); } finally { setFeatureLoading(null); } }; const loadEngineProviderPresets = async () => { try { const data = await aiStewardApi.getLlmProviderPresets(); const providers = data?.providers ?? {}; const nextPresets: EngineProviderPresets = { deepseek: { model: providers.deepseek?.model || ENGINE_PROVIDER_FALLBACK_PRESETS.deepseek.model, base_url: providers.deepseek?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.deepseek.base_url, }, glm: { model: providers.glm?.model || data?.low || ENGINE_PROVIDER_FALLBACK_PRESETS.glm.model, base_url: providers.glm?.base_url || data?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.glm.base_url, }, openai: { model: providers.openai?.model || data?.high || ENGINE_PROVIDER_FALLBACK_PRESETS.openai.model, base_url: providers.openai?.base_url || data?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.openai.base_url, }, ollama: { model: providers.ollama?.model || ENGINE_PROVIDER_FALLBACK_PRESETS.ollama.model, base_url: providers.ollama?.base_url || ENGINE_PROVIDER_FALLBACK_PRESETS.ollama.base_url, }, }; setEngineProviderPresets(nextPresets); setEngineForm((prev) => { if (engineEditingId) return prev; const preset = nextPresets[prev.provider]; return { ...prev, model: preset.model, base_url: preset.base_url, }; }); } catch (err) { console.error("Failed to load provider presets:", err); } }; const resetEngineForm = () => { setEngineEditingId(null); setEngineForm({ name: "", provider: "glm", api_key: "", model: engineProviderPresets.glm.model, base_url: engineProviderPresets.glm.base_url, }); }; const saveEngineConfig = async () => { setFeatureLoading("engine-save"); setEngineMessage(null); try { const isEdit = Boolean(engineEditingId); const body: LLMEngineUpsertRequest = { name: engineForm.name || `${engineForm.provider.toUpperCase()} 引擎`, provider: engineForm.provider, model: engineForm.model || undefined, base_url: engineForm.base_url || undefined, }; if (engineForm.api_key.trim()) { body.api_key = engineForm.api_key.trim(); } if (!isEdit) { body.set_active = true; } const data = isEdit ? await aiStewardApi.updateLlmEngine(String(engineEditingId), body) : await aiStewardApi.createLlmEngine(body); setEngineData({ active_engine_id: data?.active_engine_id ?? null, engines: data?.engines ?? [], settings: data?.settings ?? DEFAULT_LLM_ENGINE_SETTINGS, }); setEngineMessage({ type: "success", text: isEdit ? "引擎配置已更新" : "引擎配置已新增" }); resetEngineForm(); } catch (err) { setEngineMessage({ type: "error", text: err instanceof Error ? err.message : "网络错误,保存失败" }); } finally { setFeatureLoading(null); } }; const activateEngine = async (engineId: string) => { setFeatureLoading(`engine-activate-${engineId}`); setEngineMessage(null); try { const data = await aiStewardApi.activateLlmEngine(engineId); setEngineData({ active_engine_id: data?.active_engine_id ?? null, engines: data?.engines ?? [], settings: data?.settings ?? DEFAULT_LLM_ENGINE_SETTINGS, }); setEngineMessage({ type: "success", text: "已切换激活引擎" }); } catch (err) { setEngineMessage({ type: "error", text: err instanceof Error ? err.message : "网络错误,切换失败" }); } finally { setFeatureLoading(null); } }; const deleteEngine = async (engineId: string) => { if (!confirm("确定删除该引擎配置吗?")) return; setFeatureLoading(`engine-delete-${engineId}`); setEngineMessage(null); try { const data = await aiStewardApi.deleteLlmEngine(engineId); setEngineData({ active_engine_id: data?.active_engine_id ?? null, engines: data?.engines ?? [], settings: data?.settings ?? DEFAULT_LLM_ENGINE_SETTINGS, }); if (engineEditingId === engineId) { resetEngineForm(); } setEngineMessage({ type: "success", text: "引擎配置已删除" }); } catch (err) { setEngineMessage({ type: "error", text: err instanceof Error ? err.message : "网络错误,删除失败" }); } finally { setFeatureLoading(null); } }; const testEngineConnection = async (engineId?: string) => { const key = engineId ? `engine-test-${engineId}` : "engine-test-active"; setFeatureLoading(key); setEngineMessage(null); try { const data = engineId ? await aiStewardApi.testLlmEngine(engineId) : await aiStewardApi.testActiveLlmEngine(); const name = data?.name || "未知引擎"; const latency = typeof data?.latency_ms === "number" ? `${data.latency_ms}ms` : "-"; const preview = String(data?.response_preview || "").slice(0, 60); setEngineMessage({ type: "success", text: `连通成功:${name}(延迟 ${latency})${preview ? `,响应:${preview}` : ""}`, }); } catch (err) { setEngineMessage({ type: "error", text: err instanceof Error ? err.message : "网络错误,连通测试失败" }); } finally { setFeatureLoading(null); } }; const updateLocalKbQaSetting = async (checked: boolean) => { setFeatureLoading("engine-settings"); setEngineMessage(null); try { const data = await aiStewardApi.updateLlmEngineSettings({ enable_local_kb_qa: checked }); setEngineData({ active_engine_id: data?.active_engine_id ?? null, engines: data?.engines ?? [], settings: data?.settings ?? DEFAULT_LLM_ENGINE_SETTINGS, }); setEngineMessage({ type: "success", text: checked ? "已启用本地 LLM,仅知识库问答可使用" : "已禁用本地 LLM", }); } catch (err) { setEngineMessage({ type: "error", text: err instanceof Error ? err.message : "更新开关失败" }); } finally { setFeatureLoading(null); } }; const startEditEngine = (engine: LLMEngine) => { setEngineEditingId(engine.id); setEngineForm({ name: engine.name || "", provider: engine.provider, api_key: "", model: engine.model || "", base_url: engine.base_url || "", }); setEngineMessage(null); }; // ==================== Project Creation ==================== const handleProjectSubmit = async () => { if (!input.trim()) { setError("请输入项目描述"); return; } setLoading(true); setError(null); setProjectResult(null); try { const token = getAuthToken(); if (!token) { navigate("/login"); return; } const response = await fetch(`${apiBaseURL}/ai-steward/project-from-text`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ input_text: input }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || "项目创建失败"); } const data: ProjectFromTextResponse = await response.json(); setProjectResult(data); } catch (err) { setError(err instanceof Error ? err.message : "未知错误"); } finally { setLoading(false); } }; const syncUrlParams = ( tab: AIStewardTab, section: FeedbackCenterSection, nextWindowDays: FeedbackCenterTrendWindowDays = trendWindowDays, nextSourceGroup: FeedbackCenterSourceGroup = trendSourceGroup ) => { const next = new URLSearchParams(searchParams); next.set("tab", tab); next.set("section", section); next.set("window", String(nextWindowDays)); next.set("scope", nextSourceGroup); setSearchParams(next, { replace: true }); }; const handleTabChange = (value: string) => { const nextTab = normalizeTab(value); if (nextTab === "feedback-center" && !canViewFeedbackCenter) { setActiveTab("overview"); syncUrlParams("overview", "overview"); return; } setActiveTab(nextTab); syncUrlParams(nextTab, feedbackSection); }; const handleFeedbackSectionChange = (nextSection: FeedbackCenterSection) => { setFeedbackSection(nextSection); syncUrlParams(activeTab, nextSection); }; const handleTrendWindowDaysChange = (nextWindowDays: FeedbackCenterTrendWindowDays) => { setTrendWindowDays(nextWindowDays); syncUrlParams(activeTab, feedbackSection, nextWindowDays, trendSourceGroup); }; const handleTrendSourceGroupChange = (nextSourceGroup: FeedbackCenterSourceGroup) => { setTrendSourceGroup(nextSourceGroup); syncUrlParams(activeTab, feedbackSection, trendWindowDays, nextSourceGroup); }; // ==================== Initial Load ==================== useEffect(() => { const urlTab = normalizeTab(searchParams.get("tab")); const urlSection = normalizeSection(searchParams.get("section")); const safeTab = urlTab === "feedback-center" && !canViewFeedbackCenter ? "overview" : urlTab; if (safeTab !== activeTab) { setActiveTab(safeTab); } if (urlSection !== feedbackSection) { setFeedbackSection(urlSection); } const urlWindow = normalizeTrendWindow(searchParams.get("window")); if (urlWindow !== trendWindowDays) { setTrendWindowDays(urlWindow); } const urlSourceGroup = normalizeTrendSourceGroup(searchParams.get("scope")); if (urlSourceGroup !== trendSourceGroup) { setTrendSourceGroup(urlSourceGroup); } }, [searchParams, activeTab, feedbackSection, trendWindowDays, trendSourceGroup, canViewFeedbackCenter]); useEffect(() => { if (activeTab === "conflicts") loadConflicts(); if (activeTab === "blockers") loadBlockers(); if (activeTab === "briefing") loadBriefing(); if (activeTab === "engine") { loadEngineProviderPresets(); loadEngineConfigs(); } }, [activeTab]); // ==================== Render ==================== return (
{/* Header */}

AI 管家

主动式 AI 管家 - 7x24 小时自动守护项目

handleTabChange(tab)} /> {/* Main Content */} 总览 零配置创建 信号捕获 认知冲突 阻塞检测 每日简报 问答记录 {canViewFeedbackCenter && ( 反馈中心 )} 引擎配置 setProjectResult(null)} /> void startHNMonitor()} onOpenFeishuConfig={() => navigate("/feishu-config")} /> void detectConflicts()} /> void detectTaskBlockers()} /> void loadBriefing()} /> {/* QA Records Tab */} {canViewFeedbackCenter && ( )} void testEngineConnection()} onRefreshEngines={() => void loadEngineConfigs()} onUpdateLocalKbQaSetting={(checked) => void updateLocalKbQaSetting(checked)} onActivateEngine={(engineId) => void activateEngine(engineId)} onTestEngine={(engineId) => void testEngineConnection(engineId)} onStartEditEngine={startEditEngine} onDeleteEngine={(engineId) => void deleteEngine(engineId)} onSaveEngineConfig={() => void saveEngineConfig()} onResetEngineForm={resetEngineForm} />
); }