Enhance App and Header Components with Published Status

- Add publishedMatcher and publishedRuleCount props to the App component for better runtime status tracking.
- Update Header component to display the current matching algorithm and count of enabled rules in the status menu.
- Refactor LlmConfiguration and RuleConfiguration components by removing unused props and improving layout for clarity.

These changes improve the visibility of the application's runtime status and streamline the configuration components.
This commit is contained in:
Xin Wang
2026-07-18 13:36:05 +08:00
parent 311f3388bd
commit 6c482c275a
4 changed files with 74 additions and 53 deletions

View File

@@ -220,6 +220,8 @@ export default function App() {
activeTab={activeTab}
setActiveTab={setActiveTab}
publishedVersion={publishedVersion}
publishedMatcher={transferMatcher(publishedRuleSets)}
publishedRuleCount={activeRules.filter(rule => rule.enabled).length}
isDirty={isDirty}
versionHistory={versionHistory}
versionHistoryLoading={versionHistoryLoading}

View File

@@ -11,6 +11,8 @@ interface HeaderProps {
activeTab: 'rules' | 'sandbox';
setActiveTab: (tab: 'rules' | 'sandbox') => void;
publishedVersion: number;
publishedMatcher: 'ac-keyword' | 'llm';
publishedRuleCount: number;
isDirty: boolean;
versionHistory: RuleVersionSummary[];
versionHistoryLoading: boolean;
@@ -34,6 +36,8 @@ export default function Header({
activeTab,
setActiveTab,
publishedVersion,
publishedMatcher,
publishedRuleCount,
isDirty,
versionHistory,
versionHistoryLoading,
@@ -41,18 +45,26 @@ export default function Header({
onLoadVersionHistory,
onRestoreVersion,
}: HeaderProps) {
const [statusMenuOpen, setStatusMenuOpen] = useState(false);
const [versionMenuOpen, setVersionMenuOpen] = useState(false);
const statusMenuRef = useRef<HTMLDivElement>(null);
const versionMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!versionMenuOpen) return;
if (!statusMenuOpen && !versionMenuOpen) return;
const handlePointerDown = (event: MouseEvent) => {
if (!statusMenuRef.current?.contains(event.target as Node)) {
setStatusMenuOpen(false);
}
if (!versionMenuRef.current?.contains(event.target as Node)) {
setVersionMenuOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setVersionMenuOpen(false);
if (event.key === 'Escape') {
setStatusMenuOpen(false);
setVersionMenuOpen(false);
}
};
document.addEventListener('mousedown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
@@ -60,11 +72,17 @@ export default function Header({
document.removeEventListener('mousedown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [versionMenuOpen]);
}, [statusMenuOpen, versionMenuOpen]);
const toggleStatusMenu = () => {
setStatusMenuOpen(current => !current);
setVersionMenuOpen(false);
};
const toggleVersionMenu = () => {
const nextOpen = !versionMenuOpen;
setVersionMenuOpen(nextOpen);
setStatusMenuOpen(false);
if (nextOpen) onLoadVersionHistory();
};
@@ -114,24 +132,55 @@ export default function Header({
</button>
</nav>
{/* Right Section: Version Badge & Env Perceive */}
{/* Right Section: Published runtime status and version history */}
<div className="flex items-center space-x-2">
<div className={`flex items-center space-x-1.5 px-2.5 py-1 border rounded-full transition-colors ${
isDirty
? 'bg-amber-50 border-amber-100 text-amber-800'
: 'bg-emerald-50 border-emerald-100 text-emerald-800'
}`}>
<span className="relative flex h-1.5 w-1.5">
<span className={`absolute inline-flex h-full w-full rounded-full opacity-75 ${
isDirty ? 'animate-ping bg-amber-400' : 'animate-ping bg-emerald-400'
}`}></span>
<span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${
isDirty ? 'bg-amber-500' : 'bg-emerald-500'
}`}></span>
</span>
<span className="font-sans text-[10px] font-bold tracking-tight">
线
</span>
<div ref={statusMenuRef} className="relative">
<button
type="button"
onClick={toggleStatusMenu}
aria-haspopup="dialog"
aria-expanded={statusMenuOpen}
className="inline-flex items-center gap-1.5 rounded-full border border-emerald-100 bg-emerald-50 px-2.5 py-1 text-emerald-800 transition hover:border-emerald-200 hover:bg-emerald-100/70"
>
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
<span className="font-sans text-[10px] font-bold tracking-tight">线</span>
<ChevronDown className={`h-3 w-3 transition-transform ${statusMenuOpen ? 'rotate-180' : ''}`} />
</button>
{statusMenuOpen && (
<div
role="dialog"
aria-label="当前运行状态"
className="absolute right-0 top-full z-50 mt-2 w-64 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-xl"
>
<div className="border-b border-slate-100 px-4 py-3">
<p className="text-xs font-bold text-slate-900"></p>
<p className="mt-1 text-[10px] text-slate-400">线</p>
</div>
<dl className="space-y-3 px-4 py-3 text-[10px]">
<div className="flex items-center justify-between gap-4">
<dt className="text-slate-400"></dt>
<dd className="inline-flex items-center gap-1.5 font-bold text-emerald-700">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
线
</dd>
</div>
<div className="flex items-center justify-between gap-4">
<dt className="text-slate-400"></dt>
<dd className="font-bold text-slate-800">
{publishedMatcher === 'llm' ? '大模型事件匹配' : '关键词匹配'}
</dd>
</div>
<div className="flex items-center justify-between gap-4">
<dt className="text-slate-400"></dt>
<dd className="font-mono font-bold text-slate-800">{publishedRuleCount} </dd>
</div>
</dl>
</div>
)}
</div>
{isDirty && (
<span className="hidden rounded bg-amber-100/70 border border-amber-200 px-2 py-0.5 text-[10px] font-bold text-amber-800 tracking-wider sm:inline">

View File

@@ -10,7 +10,6 @@ import type { LlmConfig } from '../types';
interface LlmConfigurationProps {
config: LlmConfig;
enabledRuleCount: number;
onChange: (config: LlmConfig) => void;
addToast: (message: string, type: 'success' | 'warning' | 'info') => void;
}
@@ -35,7 +34,6 @@ const DEFAULT_PROMPT = `你是实时通话事件识别器。根据规则库和
export default function LlmConfiguration({
config,
enabledRuleCount,
onChange,
addToast,
}: LlmConfigurationProps) {
@@ -99,7 +97,7 @@ export default function LlmConfiguration({
return (
<div className="space-y-5">
<section className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs">
<div className="flex flex-col gap-4 border-b border-slate-200 px-6 py-5 xl:flex-row xl:items-center xl:justify-between">
<div className="border-b border-slate-200 px-6 py-5">
<div className="flex min-w-0 items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-950 text-white">
<Sparkles className="h-4.5 w-4.5" />
@@ -116,12 +114,6 @@ export default function LlmConfiguration({
</p>
</div>
</div>
<div className="grid gap-2 sm:grid-cols-3">
<Stat label="当前模型" value={config.model || '未配置'} />
<Stat label="启用规则" value={`${enabledRuleCount}`} />
<Stat label="输出协议" value='["E001"]' />
</div>
</div>
<div className="space-y-5 p-6">
@@ -267,15 +259,6 @@ export default function LlmConfiguration({
);
}
function Stat({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0 rounded-md border border-slate-200 bg-slate-50 px-3 py-2">
<p className="text-[9px] font-bold uppercase tracking-widest text-slate-400">{label}</p>
<p className="mt-0.5 max-w-36 truncate font-mono text-[11px] font-bold text-slate-800">{value}</p>
</div>
);
}
function Field({
label,
hint,

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import {
BookOpen,
@@ -75,7 +75,6 @@ export default function RuleConfiguration({
addToast,
}: RuleConfigurationProps) {
const [activeSection, setActiveSection] = useState<ConfigurationSection>('rules');
const enabledRuleCount = useMemo(() => rules.filter(rule => rule.enabled).length, [rules]);
const copy = SECTION_COPY[activeSection];
const selectMatcher = (nextMatcher: 'ac-keyword' | 'llm') => {
@@ -99,7 +98,7 @@ export default function RuleConfiguration({
return (
<div className="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start">
<aside className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs lg:sticky lg:top-20">
<aside className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs lg:sticky lg:top-20 lg:h-[calc(100vh-6.5rem)] lg:min-h-[420px]">
<nav className="grid grid-cols-3 gap-1 p-2 lg:grid-cols-1" aria-label="规则配置导航">
{NAV_ITEMS.map(item => {
const Icon = item.icon;
@@ -139,17 +138,6 @@ export default function RuleConfiguration({
);
})}
</nav>
<div className="hidden border-t border-slate-200 bg-slate-50/70 p-4 lg:block">
<p className="text-[9px] font-bold uppercase tracking-widest text-slate-400"></p>
<div className="mt-2 flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${matcher === 'llm' ? 'bg-violet-500' : 'bg-blue-500'}`} />
<span className="text-[11px] font-bold text-slate-800">
{matcher === 'llm' ? '大模型事件匹配' : '关键词匹配'}
</span>
</div>
<p className="mt-1 text-[9px] leading-4 text-slate-400">{enabledRuleCount} </p>
</div>
</aside>
<section className="min-w-0">
@@ -211,7 +199,6 @@ export default function RuleConfiguration({
{activeSection === 'llm' && (
<LlmConfiguration
config={llmConfig}
enabledRuleCount={enabledRuleCount}
onChange={setLlmConfig}
addToast={addToast}
/>