diff --git a/src/frontend/src/api.ts b/src/frontend/src/api.ts index b3e8903..c0253aa 100644 --- a/src/frontend/src/api.ts +++ b/src/frontend/src/api.ts @@ -1,125 +1,580 @@ -// API 客户端 +/** + * API 层 — 对接 dashboard/server.py + * 生产环境从同源 (port 7891) 请求,开发环境可通过 VITE_API_URL 指定 + */ -import type { - Task, - Project, - Observation, - Event, - DaemonStatus, -} from './types'; +const API_BASE = import.meta.env.VITE_API_URL || ''; -const API_BASE = '/api'; +// ── 通用请求 ── -async function fetchJSON(url: string, options?: RequestInit): Promise { - const resp = await fetch(`${API_BASE}${url}`, { +async function fetchJ(url: string): Promise { + const res = await fetch(url, { cache: 'no-store' }); + if (!res.ok) throw new Error(String(res.status)); + return res.json(); +} + +async function postJ(url: string, data: unknown): Promise { + const res = await fetch(url, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, - ...options, - }); - if (!resp.ok) { - throw new Error(`API error: ${resp.status} ${resp.statusText}`); - } - return resp.json(); -} - -// ---- Projects ---- - -export async function listProjects(): Promise { - const data = await fetchJSON('/projects'); - if (Array.isArray(data)) return data as Project[]; - if (data && typeof data === 'object') { - const obj = data as Record; - const projectsObj = ('projects' in obj ? obj.projects : obj) as Record; - return Object.values(projectsObj); - } - return []; -} - -export async function getProject(projectId: string): Promise { - return fetchJSON(`/projects/${projectId}`); -} - -export async function createProject(data: Partial): Promise { - return fetchJSON('/projects', { - method: 'POST', body: JSON.stringify(data), }); + return res.json(); } -// ---- Tasks ---- +// ── API 接口 ── -export async function listTasks(projectId: string): Promise { - return fetchJSON(`/projects/${projectId}/tasks`); +export const api = { + // 核心数据 + liveStatus: () => fetchJ(`${API_BASE}/api/live-status`), + agentConfig: () => fetchJ(`${API_BASE}/api/agent-config`), + modelChangeLog: () => fetchJ(`${API_BASE}/api/model-change-log`).catch(() => []), + officialsStats: () => fetchJ(`${API_BASE}/api/officials-stats`), + morningBrief: () => fetchJ(`${API_BASE}/api/morning-brief`), + morningConfig: () => fetchJ(`${API_BASE}/api/morning-config`), + agentsStatus: () => fetchJ(`${API_BASE}/api/agents-status`), + + // 任务实时动态 + taskActivity: (id: string) => + fetchJ(`${API_BASE}/api/task-activity/${encodeURIComponent(id)}`), + schedulerState: (id: string) => + fetchJ(`${API_BASE}/api/scheduler-state/${encodeURIComponent(id)}`), + + // 技能内容 + skillContent: (agentId: string, skillName: string) => + fetchJ( + `${API_BASE}/api/skill-content/${encodeURIComponent(agentId)}/${encodeURIComponent(skillName)}` + ), + + // 操作类 + setModel: (agentId: string, model: string) => + postJ(`${API_BASE}/api/set-model`, { agentId, model }), + setDispatchChannel: (channel: string) => + postJ(`${API_BASE}/api/set-dispatch-channel`, { channel }), + agentWake: (agentId: string) => + postJ(`${API_BASE}/api/agent-wake`, { agentId }), + taskAction: (taskId: string, action: string, reason: string) => + postJ(`${API_BASE}/api/task-action`, { taskId, action, reason }), + reviewAction: (taskId: string, action: string, comment: string) => + postJ(`${API_BASE}/api/review-action`, { taskId, action, comment }), + advanceState: (taskId: string, comment: string) => + postJ(`${API_BASE}/api/advance-state`, { taskId, comment }), + archiveTask: (taskId: string, archived: boolean) => + postJ(`${API_BASE}/api/archive-task`, { taskId, archived }), + archiveAllDone: () => + postJ(`${API_BASE}/api/archive-task`, { archiveAllDone: true }), + schedulerScan: (thresholdSec = 180) => + postJ( + `${API_BASE}/api/scheduler-scan`, + { thresholdSec } + ), + schedulerRetry: (taskId: string, reason: string) => + postJ(`${API_BASE}/api/scheduler-retry`, { taskId, reason }), + schedulerEscalate: (taskId: string, reason: string) => + postJ(`${API_BASE}/api/scheduler-escalate`, { taskId, reason }), + schedulerRollback: (taskId: string, reason: string) => + postJ(`${API_BASE}/api/scheduler-rollback`, { taskId, reason }), + refreshMorning: () => + postJ(`${API_BASE}/api/morning-brief/refresh`, {}), + saveMorningConfig: (config: SubConfig) => + postJ(`${API_BASE}/api/morning-config`, config), + addSkill: (agentId: string, skillName: string, description: string, trigger: string) => + postJ(`${API_BASE}/api/add-skill`, { agentId, skillName, description, trigger }), + + // 远程 Skills 管理 + addRemoteSkill: (agentId: string, skillName: string, sourceUrl: string, description?: string) => + postJ( + `${API_BASE}/api/add-remote-skill`, { agentId, skillName, sourceUrl, description: description || '' } + ), + remoteSkillsList: () => + fetchJ(`${API_BASE}/api/remote-skills-list`), + updateRemoteSkill: (agentId: string, skillName: string) => + postJ(`${API_BASE}/api/update-remote-skill`, { agentId, skillName }), + removeRemoteSkill: (agentId: string, skillName: string) => + postJ(`${API_BASE}/api/remove-remote-skill`, { agentId, skillName }), + + createTask: (data: CreateTaskPayload) => + postJ(`${API_BASE}/api/tasks`, data), + + // ── M3: 成果物 ── + artifacts: (taskId: string) => + fetchJ(`${API_BASE}/api/tasks/${encodeURIComponent(taskId)}/artifacts`), + artifactPreview: (taskId: string, path: string) => + fetchJ(`${API_BASE}/api/tasks/${encodeURIComponent(taskId)}/artifacts/preview?path=${encodeURIComponent(path)}`), + artifactDownloadUrl: (taskId: string, path: string) => + `${API_BASE}/api/tasks/${encodeURIComponent(taskId)}/artifacts/download?path=${encodeURIComponent(path)}`, + + // ── M3: 人工干预 ── + humanInput: (taskId: string) => + fetchJ(`${API_BASE}/api/tasks/${encodeURIComponent(taskId)}/human-input`), + humanInputRespond: (taskId: string, data: HumanInputRespondPayload) => + postJ(`${API_BASE}/api/tasks/${encodeURIComponent(taskId)}/human-input/respond`, data), + + // ── 朝堂议政 ── + courtDiscussStart: (topic: string, officials: string[], taskId?: string) => + postJ(`${API_BASE}/api/court-discuss/start`, { topic, officials, taskId }), + courtDiscussAdvance: (sessionId: string, userMessage?: string, decree?: string) => + postJ(`${API_BASE}/api/court-discuss/advance`, { sessionId, userMessage, decree }), + courtDiscussConclude: (sessionId: string) => + postJ(`${API_BASE}/api/court-discuss/conclude`, { sessionId }), + courtDiscussDestroy: (sessionId: string) => + postJ(`${API_BASE}/api/court-discuss/destroy`, { sessionId }), + courtDiscussFate: () => + fetchJ<{ ok: boolean; event: string }>(`${API_BASE}/api/court-discuss/fate`), +}; + +// ── Types ── + +export interface ActionResult { + ok: boolean; + message?: string; + error?: string; } -export async function getTask(projectId: string, taskId: string): Promise { - return fetchJSON(`/projects/${projectId}/tasks/${taskId}`); +export interface FlowEntry { + at: string; + from: string; + to: string; + remark: string; } -export async function createTask( - projectId: string, - data: Partial -): Promise { - return fetchJSON(`/projects/${projectId}/tasks`, { - method: 'POST', - body: JSON.stringify(data), - }); +export interface TodoItem { + id: string | number; + title: string; + status: 'not-started' | 'in-progress' | 'completed'; + detail?: string; } -export async function updateTask( - projectId: string, - taskId: string, - data: Partial -): Promise { - return fetchJSON(`/projects/${projectId}/tasks/${taskId}`, { - method: 'PATCH', - body: JSON.stringify(data), - }); +export interface Heartbeat { + status: 'active' | 'warn' | 'stalled' | 'unknown' | 'idle'; + label: string; } -// ---- Observations ---- - -export async function listObservations( - projectId: string, - taskId: string -): Promise { - return fetchJSON(`/projects/${projectId}/tasks/${taskId}/observations`); +export interface Task { + id: string; + title: string; + state: string; + org: string; + now: string; + eta: string; + block: string; + ac: string; + output: string; + heartbeat: Heartbeat; + flow_log: FlowEntry[]; + todos: TodoItem[]; + review_round: number; + archived: boolean; + archivedAt?: string; + updatedAt?: string; + updated_at?: string; // moziplus backend returns snake_case + // moziplus fields + status?: string; // moziplus task status: created/planning/executing/completed/failed/cancelled/paused + plan_status?: string; // moziplus plan status: pending/ready/challenging/approved/rejected + nodes?: TaskNode[]; // moziplus DAG nodes + latest_event?: TaskEvent; // moziplus latest event + sourceMeta?: Record; + activity?: ActivityEntry[]; + _prev_state?: string; } -// ---- Events ---- - -export async function listEvents( - projectId: string, - taskId: string -): Promise { - return fetchJSON(`/projects/${projectId}/tasks/${taskId}/events`); +export interface TaskNode { + node_id: string; + name: string; + agent_id: string; + status: string; + depends_on?: string[]; + output_summary?: string; + output?: string; + // Guard 字段 + guard_status?: 'not_checked' | 'entry_passed' | 'entry_failed' | 'exit_passed' | 'exit_failed' | 'exit_warned'; + guard_checks?: GuardCheck[]; + rollback_count?: number; } -// ---- Daemon ---- - -export async function getDaemonStatus(): Promise { - return fetchJSON('/daemon/status'); +export interface GuardCheck { + name: string; + passed: boolean; + message: string; + severity: 'error' | 'warn' | 'info'; } -export async function triggerTick(): Promise<{ tick: number }> { - return fetchJSON('/daemon/tick', { method: 'POST' }); +export interface TaskEvent { + event_type: string; + reason: string; + timestamp: string; } -// ---- SSE ---- +export interface SyncStatus { + ok: boolean; + [key: string]: unknown; +} -export function createEventSource( - onEvent: (data: unknown) => void, - onError?: () => void -): EventSource { - const es = new EventSource(`${API_BASE}/events`); - es.onmessage = (e) => { - try { - onEvent(JSON.parse(e.data)); - } catch { - onEvent(e.data); - } +export interface LiveStatus { + tasks: Task[]; + syncStatus: SyncStatus; +} + +export interface AgentInfo { + id: string; + label: string; + emoji: string; + role: string; + model: string; + skills: SkillInfo[]; +} + +export interface SkillInfo { + name: string; + description: string; + path: string; +} + +export interface KnownModel { + id: string; + label: string; + provider: string; +} + +export interface AgentConfig { + agents: AgentInfo[]; + knownModels?: KnownModel[]; + dispatchChannel?: string; +} + +export interface ChangeLogEntry { + at: string; + agentId: string; + oldModel: string; + newModel: string; + rolledBack?: boolean; +} + +export interface OfficialInfo { + id: string; + label: string; + emoji: string; + role: string; + rank: string; + model: string; + model_short: string; + tokens_in: number; + tokens_out: number; + cache_read: number; + cache_write: number; + cost_cny: number; + cost_usd: number; + sessions: number; + messages: number; + tasks_done: number; + tasks_active: number; + tasks_failed?: number; // moziplus: 失败任务数 + memorySize?: number; // MEMORY.md 字数 + memoryUpdatedAt?: string; // MEMORY.md 最后修改时间 + flow_participations: number; + merit_score: number; + merit_rank: number; + last_active: string; + heartbeat: Heartbeat; + participated_edicts: { id: string; title: string; state: string; status?: string }[]; +} + +export interface OfficialsData { + officials: OfficialInfo[]; + totals: { tasks_done: number; cost_cny: number }; + top_official: string; +} + +export interface AgentStatusInfo { + id: string; + label: string; + emoji: string; + role: string; + status: 'running' | 'idle' | 'offline' | 'unconfigured'; + statusLabel: string; + lastActive?: string; +} + +export interface GatewayStatus { + alive: boolean; + probe: boolean; + status: string; +} + +export interface AgentsStatusData { + ok: boolean; + gateway: GatewayStatus; + agents: AgentStatusInfo[]; + checkedAt: string; +} + +export interface MorningNewsItem { + title: string; + summary?: string; + desc?: string; + link: string; + source: string; + image?: string; + pub_date?: string; +} + +export interface MorningBrief { + date?: string; + generated_at?: string; + categories: Record; +} + +export interface SubCategoryConfig { + name: string; + enabled: boolean; +} + +export interface CustomFeed { + name: string; + url: string; + category: string; +} + +export interface SubConfig { + categories: SubCategoryConfig[]; + keywords: string[]; + custom_feeds: CustomFeed[]; + feishu_webhook: string; +} + +export interface ActivityEntry { + kind: string; + at?: number | string; + text?: string; + thinking?: string; + agent?: string; + from?: string; + to?: string; + remark?: string; + tools?: { name: string; input_preview?: string }[]; + tool?: string; + output?: string; + exitCode?: number | null; + items?: TodoItem[]; + diff?: { + changed?: { id: string; from: string; to: string }[]; + added?: { id: string; title: string }[]; + removed?: { id: string; title: string }[]; }; - es.onerror = () => { - onError?.(); - }; - return es; +} + +export interface PhaseDuration { + phase: string; + durationSec: number; + durationText: string; + ongoing?: boolean; +} + +export interface TodosSummary { + total: number; + completed: number; + inProgress: number; + notStarted: number; + percent: number; +} + +export interface ResourceSummary { + totalTokens?: number; + totalCost?: number; + totalElapsedSec?: number; +} + +export interface TimelineEntry { + time: string; + phase: string; + node_id: string; + node_name: string; + agent: string; + action: string; + verdict: string; + detail: string; +} + +export interface TaskActivityData { + ok: boolean; + message?: string; + error?: string; + activity?: ActivityEntry[]; + timeline?: TimelineEntry[]; + relatedAgents?: string[]; + agentLabel?: string; + lastActive?: string; + phaseDurations?: PhaseDuration[]; + totalDuration?: string; + todosSummary?: TodosSummary; + resourceSummary?: ResourceSummary; +} + +export interface SchedulerInfo { + retryCount?: number; + escalationLevel?: number; + lastDispatchStatus?: string; + stallThresholdSec?: number; + enabled?: boolean; + lastProgressAt?: string; + lastDispatchAt?: string; + lastDispatchAgent?: string; + autoRollback?: boolean; +} + +export interface SchedulerStateData { + ok: boolean; + error?: string; + scheduler?: SchedulerInfo; + stalledSec?: number; +} + +export interface SkillContentResult { + ok: boolean; + name?: string; + agent?: string; + content?: string; + path?: string; + error?: string; +} + +export interface ScanAction { + taskId: string; + action: string; + to?: string; + toState?: string; + stalledSec?: number; +} + +export interface CreateTaskPayload { + title: string; + org?: string; + targetDept?: string; + priority?: string; + templateId?: string; + params?: Record; + // moziplus fields + requirement?: string; + project_root?: string; + project_type?: string; +} + +export interface RemoteSkillItem { + skillName: string; + agentId: string; + sourceUrl: string; + description: string; + localPath: string; + addedAt: string; + lastUpdated: string; + status: 'valid' | 'not-found' | string; +} + +export interface RemoteSkillsListResult { + ok: boolean; + remoteSkills?: RemoteSkillItem[]; + count?: number; + listedAt?: string; + error?: string; +} + +// ── 朝堂议政 ── + +export interface CourtDiscussResult { + ok: boolean; + session_id?: string; + topic?: string; + round?: number; + new_messages?: Array<{ + official_id: string; + name: string; + content: string; + emotion?: string; + action?: string; + }>; + scene_note?: string; + total_messages?: number; + error?: string; +} + +// ── M3: 成果物 ── + +export interface ArtifactEntry { + name: string; + path: string; + size: number; + type: string; // document | code | data | config | other + render_as: string; // markdown | code | table | binary + previewable: boolean; +} + +export interface ArtifactNode { + node_id: string; + name: string; + agent_id: string; + status: string; + artifacts: ArtifactEntry[]; +} + +export interface ArtifactsResult { + ok: boolean; + deliverable: ArtifactEntry | null; + nodes: ArtifactNode[]; +} + +export interface ArtifactPreviewResult { + ok: boolean; + previewable: boolean; + name: string; + type: string; + render_as: string; + size: number; + content: string | null; + message?: string; +} + +// ── M3: 人工干预 ── + +export interface CheckpointInfo { + node_id: string; + node_name: string; + agent_id: string; + checkpoint: HumanInputCheckpoint; + triggered_at: string; +} + +export interface HumanInputCheckpoint { + type: 'verify' | 'decision' | 'action'; + title: string; + description?: string; + urgency?: string; + options?: Array<{ + id: string; + label: string; + description?: string; + pros?: string[]; + cons?: string[]; + recommended?: boolean; + }>; + verificationSteps?: string[]; + autoCheckResults?: Array<{ label: string; passed: boolean }>; + actionSteps?: Array<{ id: string; description: string; command?: string }>; + verificationCommand?: string; + artifacts?: Array<{ name: string; path: string }>; +} + +export interface HumanInputResult { + ok: boolean; + active: boolean; + checkpoints: CheckpointInfo[]; +} + +export interface HumanInputRespondPayload { + node_id?: string; + action?: string; + reason?: string; + selected_option?: string; + completed_steps?: string[]; + verification_note?: string; + free_text?: string; } diff --git a/src/frontend/src/components/ArtifactList.tsx b/src/frontend/src/components/ArtifactList.tsx new file mode 100644 index 0000000..07850cc --- /dev/null +++ b/src/frontend/src/components/ArtifactList.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from 'react'; +import { api } from '../api'; +import type { ArtifactsResult, ArtifactEntry } from '../api'; + +const TYPE_ICONS: Record = { + document: '📄', code: '💻', data: '📊', config: '⚙️', other: '📁', +}; + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; +} + +export default function ArtifactList({ taskId, isCompleted }: { taskId: string; isCompleted: boolean }) { + const [data, setData] = useState(null); + const [open, setOpen] = useState(false); + + useEffect(() => { + api.artifacts(taskId).then(setData).catch(() => {}); + }, [taskId]); + + if (!data) return null; + + const hasDeliverable = !!data.deliverable; + const nodeCount = data.nodes.length; + + if (!hasDeliverable && nodeCount === 0) return null; + + return ( +
+ {/* 最终交付物 */} + {hasDeliverable && ( + + )} + + {/* 节点产出折叠 */} + {nodeCount > 0 && ( +
+
setOpen(!open)} style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', userSelect: 'none' }}> + {open ? '▼' : '▶'} + 📁 {nodeCount}个节点产出 +
+ {open && ( +
+ {data.nodes.map((node) => ( +
+ {node.status === 'done' ? '✅' : '🔄'}{' '} + {node.name} + {node.artifacts.map((a) => ( + + {TYPE_ICONS[a.type]} {a.name} ({formatSize(a.size)}) + + ))} +
+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/src/frontend/src/components/ArtifactPanel.tsx b/src/frontend/src/components/ArtifactPanel.tsx new file mode 100644 index 0000000..4fbabc6 --- /dev/null +++ b/src/frontend/src/components/ArtifactPanel.tsx @@ -0,0 +1,185 @@ +import { useState, useEffect } from 'react'; +import { api } from '../api'; +import type { ArtifactsResult, ArtifactEntry, ArtifactPreviewResult } from '../api'; +import { useStore } from '../store'; + +const TYPE_ICONS: Record = { + document: '📄', + code: '💻', + data: '📊', + config: '⚙️', + other: '📁', +}; + +const AGENT_EMOJI: Record = { + 'pangtong-fujunshi': '🐦', + 'simayi-challenger': '🦅', + 'jiangwei-infra': '🔧', + 'guanyu-dev': '⚔️', + 'zhangfei-dev': '💪', + 'zhaoyun-data': '📊', +}; + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; +} + +// ── 预览模态框 ── + +function ArtifactPreviewModal({ + taskId, + artifact, + onClose, +}: { + taskId: string; + artifact: ArtifactEntry; + onClose: () => void; +}) { + const [preview, setPreview] = useState(null); + const [loading, setLoading] = useState(true); + const toast = useStore((s) => s.toast); + + useEffect(() => { + if (!artifact.previewable) { + setLoading(false); + return; + } + api.artifactPreview(taskId, artifact.path) + .then(setPreview) + .catch(() => toast('预览加载失败', 'err')) + .finally(() => setLoading(false)); + }, [taskId, artifact.path]); + + return ( +
+
e.stopPropagation()} style={{ position: 'relative', maxWidth: 700 }}> + +
+ {TYPE_ICONS[artifact.type] || '📁'} + {artifact.name} + {formatSize(artifact.size)} + ⬇ 下载 +
+
+ {loading &&
加载中...
} + {!loading && preview && !preview.previewable && ( +
+
📁
+
{preview.message || '文件过大或不可预览,请下载查看'}
+
+ )} + {!loading && preview?.content && ( + preview.render_as === 'markdown' ? ( +
+ ) : preview.render_as === 'table' ? ( +
{preview.content}
+ ) : ( +
{preview.content}
+ ) + )} +
+
+
+ ); +} + +// ── 简易 Markdown 渲染 ── + +function simpleMarkdown(text: string): string { + return text + .replace(/&/g, '&').replace(//g, '>') + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/\n/g, '
'); +} + +// ── 主面板 ── + +export default function ArtifactPanel({ taskId, isCompleted }: { taskId: string; isCompleted: boolean }) { + const [data, setData] = useState(null); + const [previewTarget, setPreviewTarget] = useState(null); + const toast = useStore((s) => s.toast); + + useEffect(() => { + api.artifacts(taskId) + .then(setData) + .catch(() => {}); + }, [taskId]); + + if (!data) return
加载中...
; + + return ( +
+ {/* 最终交付物 */} +
+
+ 📋 + 最终交付物 +
+ {data.deliverable ? ( +
+ {TYPE_ICONS[data.deliverable.type]} +
+
{data.deliverable.name}
+
{formatSize(data.deliverable.size)}
+
+
+ + 下载 +
+
+ ) : ( +
+ {isCompleted ? '无交付物' : '最终交付物将在任务完成后生成'} +
+ )} +
+ + {/* 节点产出 */} +
📁 节点产出
+ {data.nodes.length === 0 ? ( +
暂无节点产出
+ ) : data.nodes.map((node) => ( +
+
+ {node.status === 'done' ? '✅' : node.status === 'waiting_human' ? '🛐' : '🔄'} + {node.name} + {AGENT_EMOJI[node.agent_id] || ''} {node.agent_id} + + {node.status} + +
+
+ {node.artifacts.map((a) => ( +
+ {TYPE_ICONS[a.type] || '📁'} + {a.name} + {formatSize(a.size)} +
+ {a.previewable && ( + + )} + 下载 +
+
+ ))} +
+
+ ))} + + {/* 预览模态框 */} + {previewTarget && ( + setPreviewTarget(null)} /> + )} +
+ ); +} diff --git a/src/frontend/src/components/CheckpointPanel.tsx b/src/frontend/src/components/CheckpointPanel.tsx new file mode 100644 index 0000000..bd67c0b --- /dev/null +++ b/src/frontend/src/components/CheckpointPanel.tsx @@ -0,0 +1,350 @@ +import { useState } from 'react'; +import { api } from '../api'; +import { useStore } from '../store'; +import type { CheckpointInfo, HumanInputCheckpoint } from '../api'; + +const AGENT_EMOJI: Record = { + 'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧', + 'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊', +}; + +// ── Decision Checkpoint ── + +function DecisionCheckpoint({ + taskId, + info, + onDone, +}: { + taskId: string; + info: CheckpointInfo; + onDone: () => void; +}) { + const cp = info.checkpoint; + const [selected, setSelected] = useState(null); + const [note, setNote] = useState(''); + const [loading, setLoading] = useState(false); + const toast = useStore((s) => s.toast); + + const handleSubmit = async () => { + if (!selected) return; + setLoading(true); + try { + const r = await api.humanInputRespond(taskId, { + node_id: info.node_id, + action: 'decide', + selected_option: selected, + reason: note, + }); + if (r.ok) { toast('✅ 已御批', 'ok'); onDone(); } + else toast(r.error || '操作失败', 'err'); + } catch { toast('服务器连接失败', 'err'); } + setLoading(false); + }; + + return ( +
+
+ {cp.description} +
+ {/* 关联产出物 */} + {cp.artifacts?.length ? ( +
+
📦 关联产出物
+
+ {cp.artifacts.map((a, i) => ( + + 📄 {a.name} ↗ + + ))} +
+
+ ) : null} + {/* 方案 */} +
📋 备选方案
+
+ {cp.options?.map((opt) => ( +
setSelected(opt.id)} + style={{ + flex: '1 1 180px', padding: 14, borderRadius: 10, cursor: 'pointer', + border: `2px solid ${selected === opt.id ? 'var(--acc)' : 'var(--line)'}`, + background: selected === opt.id ? 'rgba(201,162,39,0.08)' : 'var(--panel)', + transition: 'all 0.15s', + }}> + {opt.recommended && ( +
+ )} +
{opt.label}
+ {opt.description &&
{opt.description}
} + {opt.pros?.map((p, i) =>
👍 {p}
)} + {opt.cons?.map((c, i) =>
👎 {c}
)} +
+ ))} +
+ {/* 兜底自由输入 */} + {(!cp.options || cp.options.length === 0) && ( +
+
无预定义方案,请直接输入决策:
+