diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 17:35:08 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 17:35:08 +0100 |
| commit | d513f93c84ae985738e0f696fcb72fa1153046ef (patch) | |
| tree | d169fa48ce93f1e204a80b60ca9295772bc2fa63 /frontend/src/components/document/nodes/StepsDiagramComponent.tsx | |
| parent | 5aa3fafb4acfa89c7d04e84abf7861607733e8ce (diff) | |
| download | soryu-d513f93c84ae985738e0f696fcb72fa1153046ef.tar.gz soryu-d513f93c84ae985738e0f696fcb72fa1153046ef.zip | |
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
Diffstat (limited to 'frontend/src/components/document/nodes/StepsDiagramComponent.tsx')
| -rw-r--r-- | frontend/src/components/document/nodes/StepsDiagramComponent.tsx | 142 |
1 files changed, 119 insertions, 23 deletions
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<string, string> = { - 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 ( - <div className={`steps-diagram-card steps-diagram-card--${status}`}> - <div className="steps-diagram-card-header"> + <div className={`steps-diagram-card steps-diagram-card--${status} ${isExpanded ? 'steps-diagram-card--expanded' : ''}`}> + <div + className={`steps-diagram-card-header ${canExpand ? 'steps-diagram-card-header--clickable' : ''}`} + onClick={canExpand ? onToggleExpand : undefined} + role={canExpand ? 'button' : undefined} + tabIndex={canExpand ? 0 : undefined} + onKeyDown={canExpand ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined} + > <span className="steps-diagram-card-name">{step.name}</span> - <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}> - {STATUS_LABELS[status] || status} - </span> + <div className="steps-diagram-card-header-right"> + <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}> + {STATUS_LABELS[status] || status} + </span> + {canExpand && ( + <span className={`steps-diagram-expand-icon ${isExpanded ? 'expanded' : ''}`}> + ▶ + </span> + )} + </div> </div> - {step.description && ( + {step.description && !isExpanded && ( <p className="steps-diagram-card-desc">{step.description}</p> )} <div className="steps-diagram-card-footer"> @@ -46,20 +70,68 @@ function StepCard({ step }: { step: DirectiveStep }) { <span className="steps-diagram-card-time"> Completed {formatTime(step.completedAt)} </span> + {hasTask && ( + <button + className={`steps-diagram-card-expand-btn ${expanded ? 'steps-diagram-card-expand-btn--open' : ''}`} + onClick={() => setExpanded((v) => !v)} + title={expanded ? 'Collapse log feed' : 'Expand log feed'} + > + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> + <polyline points="6 9 12 15 18 9" /> + </svg> + </button> + )} + </div> + {step.description && ( + <p className="steps-diagram-card-desc">{step.description}</p> )} + <div className="steps-diagram-card-footer"> + <span className="steps-diagram-card-index">#{step.orderIndex}</span> + {status === 'running' && ( + <span className="steps-diagram-card-progress">In progress...</span> + )} + {status === 'completed' && step.completedAt && ( + <span className="steps-diagram-card-time"> + Completed {formatTime(step.completedAt)} + </span> + )} + </div> </div> + + {/* Expandable log feed */} + {isExpanded && hasTask && ( + <StepLogFeed + taskId={step.taskId || step.contractId} + stepName={step.name} + stepStatus={status} + onCollapse={onCollapse} + /> + )} </div> ); } -export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProps) { +export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) { const [steps, setSteps] = useState<DirectiveStep[]>([]); const [directiveStatus, setDirectiveStatus] = useState<string>(''); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); + const [expandedStepId, setExpandedStepId] = useState<string | null>(null); const intervalRef = useRef<ReturnType<typeof setInterval> | 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<number, DirectiveStep[]> = new Map(); @@ -106,12 +196,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp return ( <div className="steps-diagram" contentEditable={false}> <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Steps</span> + <span className="steps-diagram-header-title">Contract Steps</span> <span className="steps-diagram-header-author">Authored by Makima</span> </div> <div className="steps-diagram-loading"> <div className="steps-diagram-spinner" /> - <span>Loading steps...</span> + <span>Loading contracts...</span> </div> </div> ); @@ -121,22 +211,22 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp return ( <div className="steps-diagram" contentEditable={false}> <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Steps</span> + <span className="steps-diagram-header-title">Contract Steps</span> <span className="steps-diagram-header-author">Authored by Makima</span> </div> - <div className="steps-diagram-error">Failed to load steps: {error}</div> + <div className="steps-diagram-error">Failed to load contracts: {error}</div> </div> ); } return ( - <div className="steps-diagram" contentEditable={false}> + <div className={`steps-diagram ${expandedStepId ? 'steps-diagram--has-expanded' : ''}`} contentEditable={false}> <div className="steps-diagram-header"> <div className="steps-diagram-header-left"> - <span className="steps-diagram-header-title">Steps</span> + <span className="steps-diagram-header-title">Contract Steps</span> {totalCount > 0 && ( <span className="steps-diagram-header-count"> - {completedCount}/{totalCount} completed + {completedCount}/{totalCount} fulfilled </span> )} </div> @@ -148,12 +238,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp <div className="steps-diagram-planning-dots"> <span /><span /><span /> </div> - <span>Makima is building the plan...</span> + <span>Makima is drafting contracts...</span> </div> )} {totalCount === 0 && !isBuilding && ( - <div className="steps-diagram-empty">No steps defined yet.</div> + <div className="steps-diagram-empty">No contract steps defined yet.</div> )} {totalCount > 0 && ( @@ -168,7 +258,13 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp )} <div className="steps-diagram-group"> {groupSteps.map((step) => ( - <StepCard key={step.id} step={step} /> + <StepCard + key={step.id} + step={step} + isExpanded={expandedStepId === step.id} + onToggleExpand={() => toggleExpand(step.id)} + onCollapse={collapseExpanded} + /> ))} </div> </React.Fragment> |
