From d513f93c84ae985738e0f696fcb72fa1153046ef Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 28 Apr 2026 17:35:08 +0100 Subject: feat: document UI with contract blocks, expandable logs, and interaction controls (#97) * feat: soryu-co/soryu - makima: Rename tasks to contracts in directive API and types * feat: soryu-co/soryu - makima: Add contract interaction panel with comment and interrupt * feat: soryu-co/soryu - makima: Build expandable contract log feed in StepsDiagram * feat: soryu-co/soryu - makima: Rename tasks to contracts throughout document UI and add contract block support * feat: soryu-co/soryu - makima: Add comment and interrupt controls to expanded step log feed * feat: soryu-co/soryu - makima: Audit and fix Document UI feature flag visibility and missing implementations * feat: soryu-co/soryu - makima: Add expandable step rows with live log feed in StepsDiagram * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Integrate all document UI components and final polish --- .../document/nodes/StepsDiagramComponent.tsx | 142 +++++++++++++++++---- 1 file changed, 119 insertions(+), 23 deletions(-) (limited to 'frontend/src/components/document/nodes/StepsDiagramComponent.tsx') diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx index 606c0ab..53f860e 100644 --- a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx +++ b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx @@ -1,18 +1,20 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi'; +import { StepLogFeed } from './StepLogFeed'; import './StepsDiagram.css'; interface StepsDiagramComponentProps { directiveId: string; + onExpandContract?: (step: DirectiveStep) => void; } type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped'; const STATUS_LABELS: Record = { - pending: 'Pending', + pending: 'Queued', ready: 'Ready', - running: 'Running', - completed: 'Done', + running: 'Executing', + completed: 'Fulfilled', failed: 'Failed', skipped: 'Skipped', }; @@ -23,18 +25,40 @@ function formatTime(dateStr: string): string { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } -function StepCard({ step }: { step: DirectiveStep }) { +interface StepCardProps { + step: DirectiveStep; + isExpanded: boolean; + onToggleExpand: () => void; + onCollapse: () => void; +} + +function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProps) { const status = (step.status || 'pending').toLowerCase() as StepStatus; + const hasTask = !!step.taskId || !!step.contractId; + const canExpand = hasTask && ['running', 'completed', 'failed'].includes(status); return ( -
-
+
+
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined} + > {step.name} - - {STATUS_LABELS[status] || status} - +
+ + {STATUS_LABELS[status] || status} + + {canExpand && ( + + ▶ + + )} +
- {step.description && ( + {step.description && !isExpanded && (

{step.description}

)}
@@ -46,20 +70,68 @@ function StepCard({ step }: { step: DirectiveStep }) { Completed {formatTime(step.completedAt)} + {hasTask && ( + + )} +
+ {step.description && ( +

{step.description}

)} +
+ #{step.orderIndex} + {status === 'running' && ( + In progress... + )} + {status === 'completed' && step.completedAt && ( + + Completed {formatTime(step.completedAt)} + + )} +
+ + {/* Expandable log feed */} + {isExpanded && hasTask && ( + + )}
); } -export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProps) { +export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) { const [steps, setSteps] = useState([]); const [directiveStatus, setDirectiveStatus] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [expandedStepId, setExpandedStepId] = useState(null); const intervalRef = useRef | null>(null); const prevStepCountRef = useRef(0); + const toggleStep = useCallback((stepId: string) => { + setExpandedSteps((prev) => { + const next = new Set(prev); + if (next.has(stepId)) { + next.delete(stepId); + } else { + next.add(stepId); + } + return next; + }); + }, []); + const fetchSteps = useCallback(async () => { try { const data: DirectiveWithSteps = await getDirective(directiveId); @@ -67,7 +139,7 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp setDirectiveStatus(data.status || ''); setError(null); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load steps'); + setError(err instanceof Error ? err.message : 'Failed to load contracts'); } finally { setLoading(false); } @@ -86,11 +158,29 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp prevStepCountRef.current = steps.length; }, [steps.length]); + // Keyboard shortcut: Escape to collapse expanded step + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape' && expandedStepId) { + setExpandedStepId(null); + } + }; + document.addEventListener('keydown', handler); + return () => document.removeEventListener('keydown', handler); + }, [expandedStepId]); + + const toggleExpand = useCallback((stepId: string) => { + setExpandedStepId(prev => prev === stepId ? null : stepId); + }, []); + + const collapseExpanded = useCallback(() => { + setExpandedStepId(null); + }, []); + const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length; const totalCount = steps.length; const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase()); const isBuilding = isActive && steps.length === 0; - const isAddingSteps = isActive && steps.length > 0 && steps.length > prevStepCountRef.current; // Group steps by orderIndex const groupedSteps: Map = new Map(); @@ -106,12 +196,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp return (
- Steps + Contract Steps Authored by Makima
- Loading steps... + Loading contracts...
); @@ -121,22 +211,22 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp return (
- Steps + Contract Steps Authored by Makima
-
Failed to load steps: {error}
+
Failed to load contracts: {error}
); } return ( -
+
- Steps + Contract Steps {totalCount > 0 && ( - {completedCount}/{totalCount} completed + {completedCount}/{totalCount} fulfilled )}
@@ -148,12 +238,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
- Makima is building the plan... + Makima is drafting contracts...
)} {totalCount === 0 && !isBuilding && ( -
No steps defined yet.
+
No contract steps defined yet.
)} {totalCount > 0 && ( @@ -168,7 +258,13 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp )}
{groupSteps.map((step) => ( - + toggleExpand(step.id)} + onCollapse={collapseExpanded} + /> ))}
-- cgit v1.2.3