auto-sync: 2026-05-17 11:26:08

This commit is contained in:
cfdaily
2026-05-17 11:26:08 +08:00
parent d9b2e860ff
commit 3c5d2db008
+195 -82
View File
@@ -1,94 +1,207 @@
// App 主入口
import React, { useState, useCallback } from 'react';
import { TaskBoard } from './pages/TaskBoard';
import { Monitor } from './pages/Monitor';
import { Config } from './pages/Config';
import { Briefing } from './pages/Briefing';
import { TaskModal } from './components/TaskModal';
import { useProjects } from './hooks/useApi';
import type { Task } from './types';
type Page = 'tasks' | 'monitor' | 'config' | 'briefing';
import { useEffect, useState } from 'react';
import { useStore, TAB_DEFS, startPolling, stopPolling, isEdict, isArchived } from './store';
import { api } from './api';
import EdictBoard from './components/EdictBoard';
import MonitorPanel from './components/MonitorPanel';
import OfficialPanel from './components/OfficialPanel';
import ModelConfig from './components/ModelConfig';
import SkillsConfig from './components/SkillsConfig';
import SessionsPanel from './components/SessionsPanel';
import MemorialPanel from './components/MemorialPanel';
import TemplatePanel from './components/TemplatePanel';
import TaskModal from './components/TaskModal';
import Toaster from './components/Toaster';
import CourtCeremony from './components/CourtCeremony';
import CourtDiscussion from './components/CourtDiscussion';
import UsagePanel from './components/UsagePanel';
import SettingsPanel from './components/SettingsPanel';
export default function App() {
const [page, setPage] = useState<Page>('tasks');
const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const { projects } = useProjects();
const [showCreateModal, setShowCreateModal] = useState(false);
const activeTab = useStore((s) => s.activeTab);
const setActiveTab = useStore((s) => s.setActiveTab);
const liveStatus = useStore((s) => s.liveStatus);
const countdown = useStore((s) => s.countdown);
const loadAll = useStore((s) => s.loadAll);
const toast = useStore((s) => s.toast);
const handleSelectTask = useCallback((task: Task) => {
setSelectedTask(task);
useEffect(() => {
startPolling();
return () => stopPolling();
}, []);
const tasks = liveStatus?.tasks || [];
const moziTasks = tasks.filter(isEdict);
const activeTasks = moziTasks.filter((t) => !isArchived(t));
const syncOk = !!liveStatus;
const tabBadge = (key: string): string => {
if (key === 'tasks') return String(activeTasks.length);
if (key === 'sessions') return String(tasks.filter((t) => !isEdict(t)).length);
if (key === 'archives') return String(moziTasks.filter((t) => ['completed', 'cancelled'].includes(t.state)).length);
if (key === 'monitor') {
const executing = tasks.filter((t) => isEdict(t) && t.state === 'executing').length;
return executing + '活跃';
}
return '';
};
// Tab → Component mapping (moziplus keys → Edict components)
const panels: Record<string, React.ReactNode> = {
tasks: <EdictBoard />,
court: <CourtDiscussion />,
monitor: <MonitorPanel />,
agents: <OfficialPanel />,
models: <ModelConfig />,
skills: <SkillsConfig />,
sessions: <SessionsPanel />,
archives: <MemorialPanel />,
templates: <TemplatePanel />,
usage: <UsagePanel />,
settings: <SettingsPanel />,
};
return (
<div className="app">
{/* Sidebar */}
<nav className="sidebar">
<div className="sidebar-title">+ v2.0</div>
<div className="sidebar-nav">
<div
className={`nav-item ${page === 'tasks' ? 'active' : ''}`}
onClick={() => setPage('tasks')}
>
📋
</div>
<div
className={`nav-item ${page === 'monitor' ? 'active' : ''}`}
onClick={() => setPage('monitor')}
>
📊
</div>
<div
className={`nav-item ${page === 'briefing' ? 'active' : ''}`}
onClick={() => setPage('briefing')}
>
🤖 AI Briefing
</div>
<div
className={`nav-item ${page === 'config' ? 'active' : ''}`}
onClick={() => setPage('config')}
>
</div>
{/* Project selector */}
{page === 'tasks' && (
<div style={{ marginTop: 16, padding: '0 4px' }}>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 6, textTransform: 'uppercase' }}>
</div>
{projects.map(p => (
<div
key={p.id}
className={`nav-item ${selectedProject === p.id ? 'active' : ''}`}
onClick={() => setSelectedProject(p.id)}
>
{p.name}
</div>
))}
{projects.length === 0 && (
<div style={{ fontSize: 12, color: 'var(--muted)', padding: '4px 12px' }}>
</div>
)}
</div>
)}
<div className="wrap">
{/* Header */}
<div className="hdr">
<div>
<div className="logo"> · </div>
<div className="sub-text">MoziPlus Dashboard</div>
</div>
</nav>
<div className="hdr-r">
<span className={`chip ${syncOk ? 'ok' : syncOk === false ? 'err' : ''}`}>
{syncOk ? '✅ 同步正常' : syncOk === false ? '❌ 服务器未启动' : '⏳ 连接中…'}
</span>
<span className="chip">{activeTasks.length} </span>
<button className="btn-refresh" onClick={() => loadAll()}>
</button>
<button
className="btn btn-action"
style={{ fontSize: 12, padding: '4px 14px', marginLeft: 6 }}
onClick={() => setShowCreateModal(true)}
>
📜
</button>
<span style={{ fontSize: 11, color: 'var(--muted)' }}> {countdown}s</span>
</div>
</div>
{/* Main content */}
<main className="main">
{page === 'tasks' && (
<TaskBoard projectId={selectedProject} onSelectTask={handleSelectTask} />
)}
{page === 'monitor' && <Monitor />}
{page === 'briefing' && <Briefing />}
{page === 'config' && <Config />}
</main>
{/* Tabs */}
<div className="tabs">
{TAB_DEFS.map((t) => (
<div
key={t.key}
className={`tab ${activeTab === t.key ? 'active' : ''}`}
onClick={() => setActiveTab(t.key)}
>
{t.icon} {t.label}
{tabBadge(t.key) && <span className="tbadge">{tabBadge(t.key)}</span>}
</div>
))}
</div>
{/* Task detail modal */}
<TaskModal task={selectedTask} onClose={() => setSelectedTask(null)} />
{/* Panel */}
{panels[activeTab] || null}
{/* Create Task Modal */}
{showCreateModal && <CreateTaskModal
onClose={() => setShowCreateModal(false)}
onSubmit={async (data) => {
try {
const r = await api.createTask({...data, title: data.title || ''});
const rAny = r as unknown as Record<string, unknown>;
if (r.ok || r.taskId || rAny.id) {
const tid = r.taskId || String(rAny.id || '');
toast(`📜 ${tid} 军令已创建`, 'ok');
setShowCreateModal(false);
loadAll();
} else {
toast(r.error || '创建失败', 'err');
}
} catch (e: any) {
toast(e.message || '⚠️ 创建失败', 'err');
}
}}
/>}
{/* Overlays */}
<TaskModal />
<Toaster />
<CourtCeremony />
</div>
);
}
/* ── Create Task Modal ── */
function CreateTaskModal({ onClose, onSubmit }: {
onClose: () => void;
onSubmit: (data: { requirement: string; project_root: string; project_type: string; title?: string }) => Promise<void>;
}) {
const [requirement, setRequirement] = useState('');
const [title, setTitle] = useState('');
const [projectType, setProjectType] = useState('general');
const [loading, setLoading] = useState(false);
const handleSubmit = () => {
if (!requirement.trim()) return;
setLoading(true);
onSubmit({
requirement: requirement.trim(),
title: title.trim() || undefined,
project_root: '/tmp',
project_type: projectType,
}).finally(() => setLoading(false));
};
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
onClick={(e) => e.target === e.currentTarget && onClose()}>
<div style={{ background: 'var(--bg)', borderRadius: 12, padding: 24, width: 480, maxWidth: '90vw', boxShadow: '0 8px 32px rgba(0,0,0,0.3)', border: '1px solid var(--line)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<span style={{ fontSize: 16, fontWeight: 700 }}>📜 </span>
<button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--muted)', cursor: 'pointer', fontSize: 18 }}></button>
</div>
{/* Title */}
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 4, display: 'block' }}></label>
<input value={title} onChange={(e) => setTitle(e.target.value)}
placeholder="简要概括任务..."
style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--line)', background: 'var(--panel)', color: 'var(--fg)', fontSize: 13 }} />
</div>
{/* Requirement */}
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 4, display: 'block' }}> <span style={{ color: '#ff5270' }}>*</span></label>
<textarea value={requirement} onChange={(e) => setRequirement(e.target.value)} rows={4}
placeholder="详细描述任务需求..."
style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--line)', background: 'var(--panel)', color: 'var(--fg)', fontSize: 13, resize: 'vertical' }} />
</div>
{/* Project Type */}
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 4, display: 'block' }}></label>
<select value={projectType} onChange={(e) => setProjectType(e.target.value)}
style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--line)', background: 'var(--panel)', color: 'var(--fg)', fontSize: 13 }}>
<option value="general"></option>
<option value="quant"></option>
<option value="data"></option>
<option value="infra"></option>
</select>
</div>
{/* Actions */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button onClick={onClose} disabled={loading}
style={{ padding: '6px 16px', borderRadius: 6, border: '1px solid var(--line)', background: 'var(--panel)', color: 'var(--fg)', cursor: 'pointer', fontSize: 13 }}></button>
<button onClick={handleSubmit} disabled={loading || !requirement.trim()}
style={{ padding: '6px 16px', borderRadius: 6, border: 'none', background: requirement.trim() ? '#4a90d9' : '#555', color: '#fff', cursor: requirement.trim() ? 'pointer' : 'not-allowed', fontSize: 13, fontWeight: 600 }}>
{loading ? '创建中...' : '发布军令'}
</button>
</div>
</div>
</div>
);
}