auto-sync: 2026-05-17 11:26:08
This commit is contained in:
+195
-82
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user