680 lines
24 KiB
TypeScript
680 lines
24 KiB
TypeScript
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<ProjectFromTextResponse | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// AI Steward features state
|
||
const [activeTab, setActiveTab] = useState<AIStewardTab>(() => normalizeTab(searchParams.get("tab")));
|
||
const [feedbackSection, setFeedbackSection] = useState<FeedbackCenterSection>(() =>
|
||
normalizeSection(searchParams.get("section"))
|
||
);
|
||
const [trendWindowDays, setTrendWindowDays] = useState<FeedbackCenterTrendWindowDays>(() =>
|
||
normalizeTrendWindow(searchParams.get("window"))
|
||
);
|
||
const [trendSourceGroup, setTrendSourceGroup] = useState<FeedbackCenterSourceGroup>(() =>
|
||
normalizeTrendSourceGroup(searchParams.get("scope"))
|
||
);
|
||
const [conflicts, setConflicts] = useState<CognitiveConflict[]>([]);
|
||
const [blockers, setBlockers] = useState<Blocker[]>([]);
|
||
const [briefing, setBriefing] = useState<DailyBriefing | null>(null);
|
||
const [briefingError, setBriefingError] = useState<string | null>(null);
|
||
const [featureLoading, setFeatureLoading] = useState<string | null>(null);
|
||
const [engineData, setEngineData] = useState<EngineDataState>({
|
||
active_engine_id: null,
|
||
engines: [],
|
||
settings: DEFAULT_LLM_ENGINE_SETTINGS,
|
||
});
|
||
const [engineProviderPresets, setEngineProviderPresets] = useState<EngineProviderPresets>(
|
||
ENGINE_PROVIDER_FALLBACK_PRESETS
|
||
);
|
||
const [engineForm, setEngineForm] = useState<EngineForm>({
|
||
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<string | null>(null);
|
||
const [engineMessage, setEngineMessage] = useState<EngineMessage | null>(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 (
|
||
<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-7xl mx-auto">
|
||
{/* Header */}
|
||
<div className="text-center mb-8">
|
||
<div className="flex items-center justify-center gap-3 mb-4">
|
||
<Sparkles className="w-10 h-10 text-violet-500" />
|
||
<h1 className="text-4xl font-bold text-slate-900 dark:text-slate-100">
|
||
AI 管家
|
||
</h1>
|
||
</div>
|
||
<p className="text-slate-600 dark:text-slate-400 text-lg">
|
||
主动式 AI 管家 - 7x24 小时自动守护项目
|
||
</p>
|
||
</div>
|
||
|
||
<FeatureOverviewCards onSelectTab={(tab) => handleTabChange(tab)} />
|
||
|
||
{/* Main Content */}
|
||
<Card>
|
||
<CardContent className="pt-6">
|
||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||
<TabsList className="flex w-full min-w-0 flex-nowrap gap-1 overflow-x-auto rounded-lg p-1 [-webkit-overflow-scrolling:touch]">
|
||
<TabsTrigger value="overview" className="shrink-0 whitespace-nowrap">总览</TabsTrigger>
|
||
<TabsTrigger value="create" className="shrink-0 whitespace-nowrap">零配置创建</TabsTrigger>
|
||
<TabsTrigger value="signals" className="shrink-0 whitespace-nowrap">信号捕获</TabsTrigger>
|
||
<TabsTrigger value="conflicts" className="shrink-0 whitespace-nowrap">认知冲突</TabsTrigger>
|
||
<TabsTrigger value="blockers" className="shrink-0 whitespace-nowrap">阻塞检测</TabsTrigger>
|
||
<TabsTrigger value="briefing" className="shrink-0 whitespace-nowrap">每日简报</TabsTrigger>
|
||
<TabsTrigger value="qa" className="shrink-0 whitespace-nowrap">问答记录</TabsTrigger>
|
||
{canViewFeedbackCenter && (
|
||
<TabsTrigger value="feedback-center" className="shrink-0 whitespace-nowrap">反馈中心</TabsTrigger>
|
||
)}
|
||
<TabsTrigger value="engine" className="shrink-0 whitespace-nowrap">引擎配置</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<OverviewTab />
|
||
|
||
<CreateProjectTab
|
||
input={input}
|
||
loading={loading}
|
||
error={error}
|
||
projectResult={projectResult}
|
||
onInputChange={setInput}
|
||
onSubmit={handleProjectSubmit}
|
||
onResetProject={() => setProjectResult(null)}
|
||
/>
|
||
|
||
<SignalsTab
|
||
featureLoading={featureLoading}
|
||
onStartHNMonitor={() => void startHNMonitor()}
|
||
onOpenFeishuConfig={() => navigate("/feishu-config")}
|
||
/>
|
||
|
||
<ConflictsTab
|
||
featureLoading={featureLoading}
|
||
conflicts={conflicts}
|
||
onDetectConflicts={() => void detectConflicts()}
|
||
/>
|
||
|
||
<BlockersTab
|
||
featureLoading={featureLoading}
|
||
blockers={blockers}
|
||
onDetectTaskBlockers={() => void detectTaskBlockers()}
|
||
/>
|
||
|
||
<BriefingTab
|
||
featureLoading={featureLoading}
|
||
briefing={briefing}
|
||
briefingError={briefingError}
|
||
username={user?.username}
|
||
onRefreshBriefing={() => void loadBriefing()}
|
||
/>
|
||
|
||
{/* QA Records Tab */}
|
||
<TabsContent value="qa" className="mt-6">
|
||
<QAHistoryPanel />
|
||
</TabsContent>
|
||
|
||
{canViewFeedbackCenter && (
|
||
<FeedbackCenterTab
|
||
section={feedbackSection}
|
||
projectId={Number(searchParams.get("project_id") || "0") || null}
|
||
onSectionChange={handleFeedbackSectionChange}
|
||
trendWindowDays={trendWindowDays}
|
||
trendSourceGroup={trendSourceGroup}
|
||
onTrendWindowDaysChange={handleTrendWindowDaysChange}
|
||
onTrendSourceGroupChange={handleTrendSourceGroupChange}
|
||
/>
|
||
)}
|
||
|
||
<EngineTab
|
||
featureLoading={featureLoading}
|
||
engineData={engineData}
|
||
engineMessage={engineMessage}
|
||
engineForm={engineForm}
|
||
engineEditingId={engineEditingId}
|
||
engineProviderPresets={engineProviderPresets}
|
||
setEngineForm={setEngineForm}
|
||
onTestActiveEngine={() => 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}
|
||
/>
|
||
</Tabs>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|