/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import Header from './components/Header'; import RuleManagement from './components/RuleManagement'; import SandboxSimulation from './components/SandboxSimulation'; import ToastContainer, { Toast } from './components/ToastContainer'; import { ApiError, fetchCurrentRules, fetchRuleVersions, restoreRuleVersion, saveAndActivateRules, } from './api'; import { LlmConfig, Rule, RuleSet, RuleVersionSummary } from './types'; const TRANSFER_SET_ID = 'transfer'; const DEFAULT_LLM_CONFIG: LlmConfig = { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1-mini', timeoutMs: 3000, maxOutputTokens: 64, prompt: '', }; function emptyTransferSet(): RuleSet { return { id: TRANSFER_SET_ID, name: '潜在转接', enabled: true, matcher: 'ac-keyword', rules: [], }; } function transferRules(ruleSets: RuleSet[]): Rule[] { return ruleSets.find(ruleSet => ruleSet.id === TRANSFER_SET_ID)?.rules ?? []; } function replaceTransferRules(ruleSets: RuleSet[], rules: Rule[]): RuleSet[] { if (!ruleSets.some(ruleSet => ruleSet.id === TRANSFER_SET_ID)) { return [{ ...emptyTransferSet(), rules }, ...ruleSets]; } return ruleSets.map(ruleSet => ruleSet.id === TRANSFER_SET_ID ? { ...ruleSet, rules } : ruleSet ); } function transferMatcher(ruleSets: RuleSet[]): 'ac-keyword' | 'llm' { return ruleSets.find(ruleSet => ruleSet.id === TRANSFER_SET_ID)?.matcher === 'llm' ? 'llm' : 'ac-keyword'; } function replaceTransferMatcher( ruleSets: RuleSet[], matcher: 'ac-keyword' | 'llm', ): RuleSet[] { if (!ruleSets.some(ruleSet => ruleSet.id === TRANSFER_SET_ID)) { return [{ ...emptyTransferSet(), matcher }, ...ruleSets]; } return ruleSets.map(ruleSet => ruleSet.id === TRANSFER_SET_ID ? { ...ruleSet, matcher } : ruleSet ); } export default function App() { // Global Active Tab: 'rules' | 'sandbox' const [activeTab, setActiveTab] = useState<'rules' | 'sandbox'>('rules'); const [ruleSets, setRuleSets] = useState([]); const [publishedRuleSets, setPublishedRuleSets] = useState([]); const [llmConfig, setLlmConfig] = useState(DEFAULT_LLM_CONFIG); const [publishedLlmConfig, setPublishedLlmConfig] = useState(DEFAULT_LLM_CONFIG); const [publishedVersion, setPublishedVersion] = useState(0); const [loadingRules, setLoadingRules] = useState(true); const [savingRules, setSavingRules] = useState(false); const [versionHistory, setVersionHistory] = useState([]); const [versionHistoryLoading, setVersionHistoryLoading] = useState(false); const [restoringVersion, setRestoringVersion] = useState(null); // Floating notifications const [toasts, setToasts] = useState([]); // 1. Toast helper const addToast = (message: string, type: 'success' | 'warning' | 'info') => { const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; setToasts((prev) => [...prev, { id, message, type }]); // Auto remove after 4.5 seconds setTimeout(() => { removeToast(id); }, 4500); }; const removeToast = (id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }; // Load the active server-side snapshot. The backend is the source of truth. useEffect(() => { let active = true; void fetchCurrentRules() .then(data => { if (!active) return; setRuleSets(data.ruleSets); setPublishedRuleSets(data.ruleSets); setLlmConfig(data.llmConfig); setPublishedLlmConfig(data.llmConfig); setPublishedVersion(data.version); }) .catch(error => { if (!active) return; const message = error instanceof ApiError ? error.message : '无法连接规则服务'; addToast(`加载规则失败:${message}`, 'warning'); }) .finally(() => { if (active) setLoadingRules(false); }); return () => { active = false; }; }, []); const handleUpdateRules = (updatedRules: Rule[]) => { setRuleSets(current => replaceTransferRules(current, updatedRules)); }; const handleUpdateMatcher = (matcher: 'ac-keyword' | 'llm') => { setRuleSets(current => replaceTransferMatcher(current, matcher)); }; const isDirty = useMemo(() => { return JSON.stringify(ruleSets) !== JSON.stringify(publishedRuleSets) || JSON.stringify(llmConfig) !== JSON.stringify(publishedLlmConfig); }, [llmConfig, publishedLlmConfig, publishedRuleSets, ruleSets]); const handlePublish = async () => { if (!isDirty || savingRules || restoringVersion !== null) return; setSavingRules(true); try { const result = await saveAndActivateRules(publishedVersion, llmConfig, ruleSets); setRuleSets(result.ruleSets); setPublishedRuleSets(result.ruleSets); setLlmConfig(result.llmConfig); setPublishedLlmConfig(result.llmConfig); setPublishedVersion(result.version); addToast(`保存成功,监控规则 V${result.version} 已发布生效`, 'success'); result.warnings.forEach(warning => addToast(warning, 'warning')); } catch (error) { if (error instanceof ApiError && error.body.code === 'RULE_VERSION_CONFLICT') { addToast(`规则版本冲突,服务端当前为 V${error.body.currentVersion ?? '?' },请刷新页面后重试`, 'warning'); } else { const message = error instanceof ApiError ? error.message : '未知错误'; addToast(`发布失败:${message}`, 'warning'); } } finally { setSavingRules(false); } }; const loadVersionHistory = async () => { if (versionHistoryLoading) return; setVersionHistoryLoading(true); try { setVersionHistory(await fetchRuleVersions(20)); } catch (error) { const message = error instanceof ApiError ? error.message : '未知错误'; addToast(`加载历史版本失败:${message}`, 'warning'); } finally { setVersionHistoryLoading(false); } }; const handleRestoreVersion = async (sourceVersion: number) => { if (restoringVersion !== null || savingRules || sourceVersion === publishedVersion) return; const draftWarning = isDirty ? '\n当前未发布的页面修改将被回溯结果覆盖。' : ''; const confirmed = window.confirm( `确定回溯到 V${sourceVersion}?${draftWarning}\n系统不会删除历史版本,而是基于 V${sourceVersion} 创建并发布一个新版本。` ); if (!confirmed) return; setRestoringVersion(sourceVersion); try { const result = await restoreRuleVersion(sourceVersion, publishedVersion); setRuleSets(result.ruleSets); setPublishedRuleSets(result.ruleSets); setLlmConfig(result.llmConfig); setPublishedLlmConfig(result.llmConfig); setPublishedVersion(result.version); addToast(`已基于 V${sourceVersion} 创建并生效 V${result.version}`, 'success'); result.warnings.forEach(warning => addToast(warning, 'warning')); void loadVersionHistory(); } catch (error) { if (error instanceof ApiError && error.body.code === 'RULE_VERSION_CONFLICT') { addToast(`回溯失败:服务端当前已是 V${error.body.currentVersion ?? '?'},请重新打开版本菜单`, 'warning'); void loadVersionHistory(); } else { const message = error instanceof ApiError ? error.message : '未知错误'; addToast(`回溯失败:${message}`, 'warning'); } } finally { setRestoringVersion(null); } }; const rules = transferRules(ruleSets); const activeRules = transferRules(publishedRuleSets); return (
{/* Dynamic Header */}
void loadVersionHistory()} onRestoreVersion={(version) => void handleRestoreVersion(version)} /> {/* Main Container Area */}
{activeTab === 'rules' ? ( ) : ( )}
{/* Persistent Toast Notifications */}
); }