summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-28 17:35:08 +0100
committerGitHub <noreply@github.com>2026-04-28 17:35:08 +0100
commitd513f93c84ae985738e0f696fcb72fa1153046ef (patch)
treed169fa48ce93f1e204a80b60ca9295772bc2fa63 /frontend/src/components/document/nodes/StepsDiagramComponent.tsx
parent5aa3fafb4acfa89c7d04e84abf7861607733e8ce (diff)
downloadsoryu-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.tsx142
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' : ''}`}>
+ &#x25B6;
+ </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>