summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/document/nodes')
-rw-r--r--frontend/src/components/document/nodes/ContractBlock.css123
-rw-r--r--frontend/src/components/document/nodes/ContractBlockComponent.tsx117
-rw-r--r--frontend/src/components/document/nodes/ContractBlockNode.tsx106
-rw-r--r--frontend/src/components/document/nodes/ContractLogFeed.css346
-rw-r--r--frontend/src/components/document/nodes/ContractLogFeed.tsx225
-rw-r--r--frontend/src/components/document/nodes/StepLogFeed.tsx4
-rw-r--r--frontend/src/components/document/nodes/StepsDiagram.css683
-rw-r--r--frontend/src/components/document/nodes/StepsDiagramComponent.tsx37
-rw-r--r--frontend/src/components/document/nodes/StepsDiagramNode.tsx91
9 files changed, 2 insertions, 1730 deletions
diff --git a/frontend/src/components/document/nodes/ContractBlock.css b/frontend/src/components/document/nodes/ContractBlock.css
deleted file mode 100644
index 80edb74..0000000
--- a/frontend/src/components/document/nodes/ContractBlock.css
+++ /dev/null
@@ -1,123 +0,0 @@
-/* ============================================
- Contract Block - Inline contract reference
- ============================================ */
-
-.contract-block-wrapper {
- margin: 1rem 0;
- user-select: none;
-}
-
-.contract-block {
- background: #fafbff;
- border: 1px solid #e2e5ef;
- border-radius: 8px;
- padding: 0.65rem 0.85rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- font-size: 13px;
- color: #374151;
- transition: box-shadow 0.2s ease, border-color 0.2s ease;
- animation: contractBlockAppear 0.25s ease-out both;
-}
-
-@keyframes contractBlockAppear {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.contract-block:hover {
- border-color: #c7cce0;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.contract-block--error {
- border-color: #fecaca;
- background: #fef2f2;
-}
-
-/* Header */
-.contract-block-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.contract-block-icon {
- font-size: 1rem;
- flex-shrink: 0;
-}
-
-.contract-block-name {
- font-weight: 600;
- font-size: 0.88rem;
- color: #1f2937;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.contract-block-phase-badge {
- font-size: 0.68rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.03em;
- padding: 0.1rem 0.4rem;
- border-radius: 8px;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.contract-block-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- flex-shrink: 0;
-}
-
-/* Meta */
-.contract-block-meta {
- margin-top: 0.3rem;
- padding-left: 1.5rem;
-}
-
-.contract-block-type {
- font-size: 0.75rem;
- color: #9ca3af;
- font-style: italic;
-}
-
-.contract-block-error-msg {
- margin-top: 0.25rem;
- font-size: 0.78rem;
- color: #dc2626;
- padding-left: 1.5rem;
-}
-
-/* Loading state */
-.contract-block-loading {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.4rem 0;
- color: #9ca3af;
- font-size: 0.82rem;
-}
-
-.contract-block-spinner {
- width: 14px;
- height: 14px;
- border: 2px solid #e5e7eb;
- border-top-color: #6b7280;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
diff --git a/frontend/src/components/document/nodes/ContractBlockComponent.tsx b/frontend/src/components/document/nodes/ContractBlockComponent.tsx
deleted file mode 100644
index 0d9a25a..0000000
--- a/frontend/src/components/document/nodes/ContractBlockComponent.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import './ContractBlock.css';
-
-interface ContractBlockComponentProps {
- contractId: string;
- contractName: string;
-}
-
-interface ContractInfo {
- id: string;
- name: string;
- status: string;
- phase: string;
- contract_type: string;
-}
-
-const PHASE_COLORS: Record<string, string> = {
- planning: '#3b82f6',
- execution: '#f59e0b',
- review: '#8b5cf6',
- completed: '#10b981',
- failed: '#ef4444',
-};
-
-const STATUS_COLORS: Record<string, string> = {
- active: '#10b981',
- running: '#10b981',
- idle: '#f59e0b',
- paused: '#f59e0b',
- completed: '#10b981',
- failed: '#ef4444',
- archived: '#6b7280',
-};
-
-export function ContractBlockComponent({ contractId, contractName }: ContractBlockComponentProps) {
- const [contract, setContract] = useState<ContractInfo | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
-
- useEffect(() => {
- let cancelled = false;
-
- async function fetchContract() {
- try {
- const response = await fetch(`/api/v1/contracts/${contractId}`);
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const data = await response.json();
- if (!cancelled) {
- setContract(data.contract || data);
- setError(null);
- }
- } catch (err) {
- if (!cancelled) {
- setError(err instanceof Error ? err.message : 'Failed to load');
- }
- } finally {
- if (!cancelled) setLoading(false);
- }
- }
-
- fetchContract();
- return () => { cancelled = true; };
- }, [contractId]);
-
- if (loading) {
- return (
- <div className="contract-block" contentEditable={false}>
- <div className="contract-block-loading">
- <div className="contract-block-spinner" />
- <span>Loading contract...</span>
- </div>
- </div>
- );
- }
-
- if (error) {
- return (
- <div className="contract-block contract-block--error" contentEditable={false}>
- <div className="contract-block-header">
- <span className="contract-block-icon">&#x1F4E6;</span>
- <span className="contract-block-name">{contractName}</span>
- </div>
- <div className="contract-block-error-msg">Unable to load: {error}</div>
- </div>
- );
- }
-
- const phase = contract?.phase?.toLowerCase() || 'unknown';
- const status = contract?.status?.toLowerCase() || 'unknown';
- const phaseColor = PHASE_COLORS[phase] || '#6b7280';
- const statusColor = STATUS_COLORS[status] || '#6b7280';
-
- return (
- <div className="contract-block" contentEditable={false}>
- <div className="contract-block-header">
- <span className="contract-block-icon">&#x1F4E6;</span>
- <span className="contract-block-name">{contract?.name || contractName}</span>
- <span
- className="contract-block-phase-badge"
- style={{ backgroundColor: phaseColor + '20', color: phaseColor }}
- >
- {phase}
- </span>
- <span
- className="contract-block-status-dot"
- style={{ backgroundColor: statusColor }}
- title={status}
- />
- </div>
- {contract?.contract_type && (
- <div className="contract-block-meta">
- <span className="contract-block-type">{contract.contract_type}</span>
- </div>
- )}
- </div>
- );
-}
diff --git a/frontend/src/components/document/nodes/ContractBlockNode.tsx b/frontend/src/components/document/nodes/ContractBlockNode.tsx
deleted file mode 100644
index 86e4c9d..0000000
--- a/frontend/src/components/document/nodes/ContractBlockNode.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- DecoratorNode,
- DOMExportOutput,
- LexicalNode,
- NodeKey,
- SerializedLexicalNode,
- Spread,
-} from 'lexical';
-import React from 'react';
-import { ContractBlockComponent } from './ContractBlockComponent';
-
-export type SerializedContractBlockNode = Spread<
- {
- contractId: string;
- contractName: string;
- },
- SerializedLexicalNode
->;
-
-export class ContractBlockNode extends DecoratorNode<JSX.Element> {
- __contractId: string;
- __contractName: string;
-
- static getType(): string {
- return 'contract-block';
- }
-
- static clone(node: ContractBlockNode): ContractBlockNode {
- return new ContractBlockNode(node.__contractId, node.__contractName, node.__key);
- }
-
- constructor(contractId: string, contractName: string, key?: NodeKey) {
- super(key);
- this.__contractId = contractId;
- this.__contractName = contractName;
- }
-
- createDOM(): HTMLElement {
- const div = document.createElement('div');
- div.className = 'contract-block-wrapper';
- return div;
- }
-
- updateDOM(): boolean {
- return false;
- }
-
- decorate(): JSX.Element {
- return (
- <ContractBlockComponent
- contractId={this.__contractId}
- contractName={this.__contractName}
- />
- );
- }
-
- exportJSON(): SerializedContractBlockNode {
- return {
- ...super.exportJSON(),
- type: 'contract-block',
- contractId: this.__contractId,
- contractName: this.__contractName,
- version: 1,
- };
- }
-
- static importJSON(serializedNode: SerializedContractBlockNode): ContractBlockNode {
- return $createContractBlockNode(
- serializedNode.contractId,
- serializedNode.contractName
- );
- }
-
- isInline(): boolean {
- return false;
- }
-
- canInsertTextBefore(): boolean {
- return false;
- }
-
- canInsertTextAfter(): boolean {
- return false;
- }
-
- exportDOM(): DOMExportOutput {
- const element = document.createElement('div');
- element.className = 'contract-block-wrapper';
- element.setAttribute('data-contract-id', this.__contractId);
- element.textContent = `[Contract: ${this.__contractName}]`;
- return { element };
- }
-}
-
-export function $createContractBlockNode(
- contractId: string,
- contractName: string
-): ContractBlockNode {
- return new ContractBlockNode(contractId, contractName);
-}
-
-export function $isContractBlockNode(
- node: LexicalNode | null | undefined,
-): node is ContractBlockNode {
- return node instanceof ContractBlockNode;
-}
diff --git a/frontend/src/components/document/nodes/ContractLogFeed.css b/frontend/src/components/document/nodes/ContractLogFeed.css
deleted file mode 100644
index b5dd15d..0000000
--- a/frontend/src/components/document/nodes/ContractLogFeed.css
+++ /dev/null
@@ -1,346 +0,0 @@
-/* ============================================
- Contract Log Feed
- ============================================ */
-
-.contract-log-feed {
- display: flex;
- flex-direction: column;
- background: #1a1d23;
- border: 1px solid #2d3039;
- border-radius: 8px;
- overflow: hidden;
- margin-top: 0.5rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- font-size: 13px;
- max-height: 420px;
- animation: logFeedSlideIn 0.25s ease-out;
-}
-
-@keyframes logFeedSlideIn {
- from {
- opacity: 0;
- transform: translateY(-6px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* ---- Header ---- */
-.contract-log-feed-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.5rem 0.75rem;
- background: #22252b;
- border-bottom: 1px solid #2d3039;
-}
-
-.contract-log-feed-title {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.contract-log-feed-name {
- font-weight: 600;
- font-size: 0.82rem;
- color: #e5e7eb;
-}
-
-.contract-log-feed-status {
- font-size: 0.65rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- padding: 0.1rem 0.4rem;
- border-radius: 8px;
-}
-
-.contract-log-feed-status--running,
-.contract-log-feed-status--starting {
- background: rgba(245, 158, 11, 0.2);
- color: #fbbf24;
- animation: statusPulse 2s ease-in-out infinite;
-}
-
-.contract-log-feed-status--completed {
- background: rgba(16, 185, 129, 0.2);
- color: #34d399;
-}
-
-.contract-log-feed-status--failed {
- background: rgba(239, 68, 68, 0.2);
- color: #f87171;
-}
-
-.contract-log-feed-status--pending,
-.contract-log-feed-status--ready {
- background: rgba(107, 114, 128, 0.2);
- color: #9ca3af;
-}
-
-.contract-log-feed-close {
- background: none;
- border: none;
- color: #6b7280;
- font-size: 1.1rem;
- cursor: pointer;
- padding: 0 0.25rem;
- line-height: 1;
- border-radius: 3px;
- transition: color 0.15s, background 0.15s;
-}
-
-.contract-log-feed-close:hover {
- color: #e5e7eb;
- background: rgba(255, 255, 255, 0.08);
-}
-
-/* ---- Log Content ---- */
-.contract-log-feed-content {
- flex: 1;
- overflow-y: auto;
- padding: 0.5rem 0.75rem;
- min-height: 80px;
- max-height: 240px;
- scrollbar-width: thin;
- scrollbar-color: #3a3f4b transparent;
-}
-
-.contract-log-feed-content::-webkit-scrollbar {
- width: 5px;
-}
-
-.contract-log-feed-content::-webkit-scrollbar-thumb {
- background: #3a3f4b;
- border-radius: 3px;
-}
-
-.contract-log-feed-empty {
- color: #6b7280;
- font-size: 0.82rem;
- font-style: italic;
- text-align: center;
- padding: 1.5rem 0;
-}
-
-/* ---- Log Entry ---- */
-.contract-log-entry {
- display: flex;
- gap: 0.5rem;
- padding: 0.2rem 0;
- line-height: 1.5;
- animation: entryFadeIn 0.2s ease-out;
-}
-
-@keyframes entryFadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.contract-log-entry-time {
- flex-shrink: 0;
- font-size: 0.7rem;
- color: #4b5563;
- font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
- line-height: 1.65;
-}
-
-.contract-log-entry-text {
- color: #d1d5db;
- white-space: pre-wrap;
- word-break: break-word;
- font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
- font-size: 0.78rem;
-}
-
-.contract-log-entry--user .contract-log-entry-text {
- color: #93c5fd;
-}
-
-.contract-log-entry--user::before {
- content: '>';
- color: #3b82f6;
- font-weight: 700;
- font-family: monospace;
- flex-shrink: 0;
- line-height: 1.5;
-}
-
-.contract-log-entry--system .contract-log-entry-text {
- color: #fbbf24;
- font-style: italic;
-}
-
-/* ---- Error ---- */
-.contract-log-feed-error {
- padding: 0.4rem 0.75rem;
- background: rgba(239, 68, 68, 0.12);
- border-top: 1px solid rgba(239, 68, 68, 0.25);
- color: #f87171;
- font-size: 0.78rem;
-}
-
-/* ---- Interaction Bar ---- */
-.contract-interaction-bar {
- border-top: 1px solid #2d3039;
- padding: 0.5rem 0.75rem;
- background: #22252b;
-}
-
-.contract-interaction-bar--disabled {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0.6rem 0.75rem;
-}
-
-.contract-interaction-disabled-text {
- color: #6b7280;
- font-size: 0.78rem;
- font-style: italic;
-}
-
-.contract-interaction-message-row {
- display: flex;
- align-items: flex-end;
- gap: 0.4rem;
- position: relative;
-}
-
-.contract-message-input {
- flex: 1;
- background: #1a1d23;
- border: 1px solid #3a3f4b;
- border-radius: 6px;
- color: #e5e7eb;
- padding: 0.4rem 0.6rem;
- font-size: 0.82rem;
- font-family: inherit;
- resize: none;
- min-height: 32px;
- max-height: 80px;
- line-height: 1.4;
- outline: none;
- transition: border-color 0.15s;
-}
-
-.contract-message-input::placeholder {
- color: #4b5563;
- font-size: 0.78rem;
-}
-
-.contract-message-input:focus {
- border-color: #3b82f6;
-}
-
-.contract-message-input:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.contract-send-btn {
- flex-shrink: 0;
- background: #3b82f6;
- color: #fff;
- border: none;
- border-radius: 6px;
- padding: 0.4rem 0.85rem;
- font-size: 0.8rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.15s, opacity 0.15s;
- min-height: 32px;
-}
-
-.contract-send-btn:hover:not(:disabled) {
- background: #2563eb;
-}
-
-.contract-send-btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.contract-sent-indicator {
- position: absolute;
- right: 0;
- top: -1.4rem;
- font-size: 0.7rem;
- color: #34d399;
- font-weight: 500;
- animation: sentFlash 1.5s ease-out forwards;
-}
-
-@keyframes sentFlash {
- 0% {
- opacity: 1;
- transform: translateY(0);
- }
- 70% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- transform: translateY(-4px);
- }
-}
-
-/* ---- Actions Row ---- */
-.contract-interaction-actions-row {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-top: 0.4rem;
-}
-
-.contract-interrupt-btn {
- background: transparent;
- color: #ef4444;
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: 6px;
- padding: 0.3rem 0.7rem;
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.15s, border-color 0.15s, color 0.15s;
-}
-
-.contract-interrupt-btn:hover:not(:disabled) {
- background: rgba(239, 68, 68, 0.1);
- border-color: rgba(239, 68, 68, 0.5);
-}
-
-.contract-interrupt-btn--confirm {
- background: rgba(239, 68, 68, 0.15);
- border-color: #ef4444;
- color: #f87171;
- animation: confirmPulse 0.8s ease-in-out infinite;
-}
-
-@keyframes confirmPulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.7; }
-}
-
-.contract-interrupt-btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-/* ---- Responsive ---- */
-@media (max-width: 640px) {
- .contract-log-feed {
- max-height: 360px;
- }
-
- .contract-log-feed-content {
- max-height: 180px;
- }
-
- .contract-message-input::placeholder {
- font-size: 0.72rem;
- }
-}
diff --git a/frontend/src/components/document/nodes/ContractLogFeed.tsx b/frontend/src/components/document/nodes/ContractLogFeed.tsx
deleted file mode 100644
index 79af91c..0000000
--- a/frontend/src/components/document/nodes/ContractLogFeed.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import React, { useState, useRef, useEffect, useCallback } from 'react';
-import {
- sendContractMessage,
- interruptContract,
- getContractOutput,
-} from '../../../services/directiveApi';
-import './ContractLogFeed.css';
-
-interface ContractLogFeedProps {
- taskId: string;
- contractName: string;
- status: string;
- onClose?: () => void;
-}
-
-interface LogEntry {
- id: string;
- text: string;
- type: 'output' | 'user' | 'system';
- timestamp: Date;
-}
-
-const INTERACTIVE_STATUSES = ['running', 'starting'];
-
-export function ContractLogFeed({ taskId, contractName, status, onClose }: ContractLogFeedProps) {
- const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
- const [message, setMessage] = useState('');
- const [sending, setSending] = useState(false);
- const [sentIndicator, setSentIndicator] = useState(false);
- const [interruptConfirm, setInterruptConfirm] = useState(false);
- const [interrupting, setInterrupting] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const logEndRef = useRef<HTMLDivElement>(null);
- const textareaRef = useRef<HTMLTextAreaElement>(null);
- const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
- const lastOutputRef = useRef<string>('');
- const entryIdRef = useRef(0);
-
- const isInteractive = INTERACTIVE_STATUSES.includes(status.toLowerCase());
-
- const addLogEntry = useCallback((text: string, type: LogEntry['type']) => {
- entryIdRef.current += 1;
- setLogEntries(prev => [
- ...prev,
- { id: `entry-${entryIdRef.current}`, text, type, timestamp: new Date() },
- ]);
- }, []);
-
- // Poll for contract output
- const fetchOutput = useCallback(async () => {
- if (!taskId) return;
- try {
- const data = await getContractOutput(taskId);
- const output = data.output || '';
- if (output && output !== lastOutputRef.current) {
- // Find new content
- const newContent = output.startsWith(lastOutputRef.current)
- ? output.slice(lastOutputRef.current.length).trim()
- : output.trim();
- lastOutputRef.current = output;
- if (newContent) {
- addLogEntry(newContent, 'output');
- }
- }
- } catch {
- // Silently ignore fetch errors for output polling
- }
- }, [taskId, addLogEntry]);
-
- useEffect(() => {
- fetchOutput();
- pollRef.current = setInterval(fetchOutput, 3000);
- return () => {
- if (pollRef.current) clearInterval(pollRef.current);
- };
- }, [fetchOutput]);
-
- // Auto-scroll to bottom on new entries
- useEffect(() => {
- logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [logEntries]);
-
- // Reset interrupt confirm after timeout
- useEffect(() => {
- if (!interruptConfirm) return;
- const timer = setTimeout(() => setInterruptConfirm(false), 3000);
- return () => clearTimeout(timer);
- }, [interruptConfirm]);
-
- const handleSendMessage = async () => {
- const trimmed = message.trim();
- if (!trimmed || sending || !isInteractive) return;
-
- setSending(true);
- setError(null);
- try {
- await sendContractMessage(taskId, trimmed);
- addLogEntry(trimmed, 'user');
- setMessage('');
- setSentIndicator(true);
- setTimeout(() => setSentIndicator(false), 1500);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to send message');
- } finally {
- setSending(false);
- }
- };
-
- const handleInterrupt = async () => {
- if (!interruptConfirm) {
- setInterruptConfirm(true);
- return;
- }
-
- setInterrupting(true);
- setError(null);
- setInterruptConfirm(false);
- try {
- await interruptContract(taskId);
- addLogEntry('Contract interrupted by user', 'system');
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to interrupt contract');
- } finally {
- setInterrupting(false);
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- handleSendMessage();
- }
- };
-
- const statusLower = status.toLowerCase();
-
- return (
- <div className="contract-log-feed">
- <div className="contract-log-feed-header">
- <div className="contract-log-feed-title">
- <span className="contract-log-feed-name">{contractName}</span>
- <span className={`contract-log-feed-status contract-log-feed-status--${statusLower}`}>
- {status}
- </span>
- </div>
- {onClose && (
- <button className="contract-log-feed-close" onClick={onClose} title="Close">
- &times;
- </button>
- )}
- </div>
-
- <div className="contract-log-feed-content">
- {logEntries.length === 0 && (
- <div className="contract-log-feed-empty">
- {isInteractive ? 'Waiting for output...' : 'No output available.'}
- </div>
- )}
- {logEntries.map(entry => (
- <div key={entry.id} className={`contract-log-entry contract-log-entry--${entry.type}`}>
- <span className="contract-log-entry-time">
- {entry.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
- </span>
- <span className="contract-log-entry-text">{entry.text}</span>
- </div>
- ))}
- <div ref={logEndRef} />
- </div>
-
- {error && (
- <div className="contract-log-feed-error">{error}</div>
- )}
-
- {isInteractive && (
- <div className="contract-interaction-bar">
- <div className="contract-interaction-message-row">
- <textarea
- ref={textareaRef}
- className="contract-message-input"
- value={message}
- onChange={e => setMessage(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Send a message to the contract... (Enter to send, Shift+Enter for newline)"
- rows={1}
- disabled={sending}
- />
- <button
- className="contract-send-btn"
- onClick={handleSendMessage}
- disabled={sending || !message.trim()}
- title="Send message"
- >
- {sending ? 'Sending...' : 'Send'}
- </button>
- {sentIndicator && (
- <span className="contract-sent-indicator">Sent</span>
- )}
- </div>
- <div className="contract-interaction-actions-row">
- <button
- className={`contract-interrupt-btn ${interruptConfirm ? 'contract-interrupt-btn--confirm' : ''}`}
- onClick={handleInterrupt}
- disabled={interrupting}
- title={interruptConfirm ? 'Click again to confirm interrupt' : 'Interrupt contract'}
- >
- {interrupting
- ? 'Interrupting...'
- : interruptConfirm
- ? 'Click again to confirm interrupt'
- : 'Interrupt'}
- </button>
- </div>
- </div>
- )}
-
- {!isInteractive && statusLower !== 'pending' && statusLower !== 'ready' && (
- <div className="contract-interaction-bar contract-interaction-bar--disabled">
- <span className="contract-interaction-disabled-text">
- Contract is {status.toLowerCase()} - interaction unavailable
- </span>
- </div>
- )}
- </div>
- );
-}
diff --git a/frontend/src/components/document/nodes/StepLogFeed.tsx b/frontend/src/components/document/nodes/StepLogFeed.tsx
index 0357de8..2f2f553 100644
--- a/frontend/src/components/document/nodes/StepLogFeed.tsx
+++ b/frontend/src/components/document/nodes/StepLogFeed.tsx
@@ -211,7 +211,7 @@ export function StepLogFeed({ taskId, stepName, stepStatus, onCollapse }: StepLo
<button
className="step-log-feed-interrupt-btn"
onClick={handleInterrupt}
- title="Interrupt this task"
+ title="Interrupt this contract"
>
&#x23F9; Interrupt
</button>
@@ -256,7 +256,7 @@ export function StepLogFeed({ taskId, stepName, stepStatus, onCollapse }: StepLo
ref={inputRef}
type="text"
className="step-log-feed-input-field"
- placeholder="Send a message to this task..."
+ placeholder="Send a message to this contract..."
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
diff --git a/frontend/src/components/document/nodes/StepsDiagram.css b/frontend/src/components/document/nodes/StepsDiagram.css
deleted file mode 100644
index 9856c6d..0000000
--- a/frontend/src/components/document/nodes/StepsDiagram.css
+++ /dev/null
@@ -1,683 +0,0 @@
-/* ============================================
- Steps Diagram Block
- ============================================ */
-
-.steps-diagram-block {
- margin: 1.5rem 0;
- user-select: none;
-}
-
-.steps-diagram {
- background: #f8f9fc;
- border: 1px solid #e2e5ef;
- border-radius: 10px;
- padding: 1rem 1.25rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- font-size: 14px;
- color: #374151;
- transition: max-height 0.3s ease;
-}
-
-.steps-diagram--has-expanded {
- /* Allow more vertical space when a step is expanded */
- max-height: none;
-}
-
-/* ---- Header ---- */
-.steps-diagram-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 1rem;
- padding-bottom: 0.6rem;
- border-bottom: 1px solid #e5e7eb;
-}
-
-.steps-diagram-header-left {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.steps-diagram-header-title {
- font-weight: 600;
- font-size: 0.9rem;
- color: #1f2937;
- letter-spacing: 0.01em;
-}
-
-.steps-diagram-header-count {
- font-size: 0.78rem;
- color: #6b7280;
- background: #e5e7eb;
- border-radius: 10px;
- padding: 0.15rem 0.55rem;
-}
-
-.steps-diagram-header-author {
- font-size: 0.72rem;
- color: #9ca3af;
- font-style: italic;
-}
-
-/* ---- DAG Layout ---- */
-.steps-diagram-dag {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0;
-}
-
-.steps-diagram-group {
- display: flex;
- flex-wrap: wrap;
- gap: 0.6rem;
- justify-content: center;
- width: 100%;
-}
-
-/* ---- Arrow between groups ---- */
-.steps-diagram-arrow {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 0.15rem 0;
-}
-
-.steps-diagram-arrow-line {
- width: 2px;
- height: 16px;
- background: #cbd5e1;
-}
-
-.steps-diagram-arrow-head {
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-top: 6px solid #cbd5e1;
-}
-
-/* ---- Step Card Wrapper ---- */
-.steps-diagram-card-wrapper {
- flex: 1 1 180px;
- max-width: 280px;
-}
-
-/* ---- Step Card ---- */
-.steps-diagram-card {
- background: #ffffff;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- padding: 0.65rem 0.8rem;
- transition: box-shadow 0.2s ease, border-color 0.2s ease, max-width 0.3s ease;
- animation: stepCardAppear 0.35s ease-out both;
-}
-
-.steps-diagram-card--expanded {
- flex: 1 1 100%;
- max-width: 100%;
- border-color: #93c5fd;
- box-shadow: 0 2px 12px rgba(59, 130, 246, 0.1);
-}
-
-@keyframes stepCardAppear {
- from {
- opacity: 0;
- transform: translateY(8px) scale(0.97);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
-.steps-diagram-card:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
-}
-
-.steps-diagram-card--expandable {
- cursor: pointer;
-}
-
-.steps-diagram-card--expandable:hover {
- border-color: #c7cbd5;
-}
-
-.steps-diagram-card--expanded {
- border-radius: 8px 8px 0 0;
- border-bottom-color: transparent;
-}
-
-.steps-diagram-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.5rem;
- margin-bottom: 0.3rem;
-}
-
-.steps-diagram-card-header--clickable {
- cursor: pointer;
- user-select: none;
-}
-
-.steps-diagram-card-header--clickable:hover .steps-diagram-card-name {
- color: #2563eb;
-}
-
-.steps-diagram-card-header-right {
- display: flex;
- align-items: center;
- gap: 0.4rem;
- flex-shrink: 0;
-}
-
-.steps-diagram-card-name {
- font-weight: 600;
- font-size: 0.85rem;
- color: #1f2937;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- transition: color 0.15s;
-}
-
-.steps-diagram-card-desc {
- font-size: 0.78rem;
- color: #6b7280;
- margin: 0.2rem 0 0.4rem 0;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.steps-diagram-card-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 0.72rem;
- color: #9ca3af;
-}
-
-.steps-diagram-card-index {
- font-weight: 500;
-}
-
-.steps-diagram-card-contract-ref {
- font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, monospace;
- font-size: 0.68rem;
- color: #6b7280;
- background: #f3f4f6;
- padding: 0.08rem 0.35rem;
- border-radius: 4px;
- cursor: default;
-}
-
-.steps-diagram-card-progress {
- color: #d97706;
- font-style: italic;
-}
-
-.steps-diagram-card-time {
- color: #6b7280;
-}
-
-/* ---- Expand Icon ---- */
-.steps-diagram-expand-icon {
- font-size: 0.6rem;
- color: #9ca3af;
- transition: transform 0.2s ease, color 0.15s;
- display: inline-block;
-}
-
-.steps-diagram-expand-icon.expanded {
- transform: rotate(90deg);
- color: #3b82f6;
-}
-
-.steps-diagram-card-header--clickable:hover .steps-diagram-expand-icon {
- color: #3b82f6;
-}
-
-/* ---- Status Badge ---- */
-.steps-diagram-status-badge {
- font-size: 0.68rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.03em;
- padding: 0.12rem 0.45rem;
- border-radius: 9px;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.steps-diagram-status-badge--pending {
- background: #f3f4f6;
- color: #6b7280;
-}
-
-.steps-diagram-status-badge--ready {
- background: #dbeafe;
- color: #2563eb;
-}
-
-.steps-diagram-status-badge--running {
- background: #fef3c7;
- color: #d97706;
- animation: statusPulse 2s ease-in-out infinite;
-}
-
-.steps-diagram-status-badge--completed {
- background: #d1fae5;
- color: #059669;
-}
-
-.steps-diagram-status-badge--failed {
- background: #fee2e2;
- color: #dc2626;
-}
-
-.steps-diagram-status-badge--skipped {
- background: repeating-linear-gradient(
- 45deg,
- #f3f4f6,
- #f3f4f6 4px,
- #e5e7eb 4px,
- #e5e7eb 8px
- );
- color: #9ca3af;
-}
-
-/* ---- Status-specific Card Borders ---- */
-.steps-diagram-card--pending {
- border-left: 3px solid #d1d5db;
-}
-
-.steps-diagram-card--ready {
- border-left: 3px solid #3b82f6;
-}
-
-.steps-diagram-card--running {
- border-left: 3px solid #f59e0b;
- animation: cardGlow 2s ease-in-out infinite;
-}
-
-.steps-diagram-card--completed {
- border-left: 3px solid #10b981;
-}
-
-.steps-diagram-card--failed {
- border-left: 3px solid #ef4444;
-}
-
-.steps-diagram-card--skipped {
- border-left: 3px solid #d1d5db;
- opacity: 0.7;
-}
-
-/* ---- Expanded Card ---- */
-.steps-diagram-card--expanded {
- flex: 1 1 100%;
- max-width: 100%;
-}
-
-.steps-diagram-card-expand {
- flex-shrink: 0;
- font-size: 0.7rem;
- color: #9ca3af;
- margin-left: 0.25rem;
-}
-
-/* ---- Animations ---- */
-@keyframes statusPulse {
- 0%, 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.65;
- }
-}
-
-@keyframes cardGlow {
- 0%, 100% {
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
- }
- 50% {
- box-shadow: 0 0 8px 2px rgba(245, 158, 11, 0.15);
- }
-}
-
-/* ---- Loading State ---- */
-.steps-diagram-loading {
- display: flex;
- align-items: center;
- gap: 0.6rem;
- padding: 1rem 0;
- color: #9ca3af;
- font-size: 0.85rem;
-}
-
-.steps-diagram-spinner {
- width: 16px;
- height: 16px;
- border: 2px solid #e5e7eb;
- border-top-color: #6b7280;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* ---- Planning State ---- */
-.steps-diagram-planning {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 1.25rem 0;
- color: #6b7280;
- font-size: 0.85rem;
- font-style: italic;
-}
-
-.steps-diagram-planning-dots {
- display: flex;
- gap: 4px;
-}
-
-.steps-diagram-planning-dots span {
- width: 6px;
- height: 6px;
- background: #9ca3af;
- border-radius: 50%;
- animation: dotBounce 1.4s ease-in-out infinite;
-}
-
-.steps-diagram-planning-dots span:nth-child(2) {
- animation-delay: 0.2s;
-}
-
-.steps-diagram-planning-dots span:nth-child(3) {
- animation-delay: 0.4s;
-}
-
-@keyframes dotBounce {
- 0%, 80%, 100% {
- transform: scale(0.6);
- opacity: 0.4;
- }
- 40% {
- transform: scale(1);
- opacity: 1;
- }
-}
-
-/* ---- Empty / Error ---- */
-.steps-diagram-empty {
- padding: 1rem 0;
- color: #9ca3af;
- font-size: 0.85rem;
- text-align: center;
-}
-
-.steps-diagram-error {
- padding: 0.75rem;
- background: #fef2f2;
- border: 1px solid #fecaca;
- border-radius: 6px;
- color: #dc2626;
- font-size: 0.82rem;
-}
-
-/* ============================================
- Step Log Feed (Expandable)
- ============================================ */
-
-.step-log-feed {
- margin-top: 0.5rem;
- border-top: 1px solid #e5e7eb;
- padding-top: 0.5rem;
- animation: logFeedSlideIn 0.25s ease-out both;
-}
-
-@keyframes logFeedSlideIn {
- from {
- opacity: 0;
- max-height: 0;
- }
- to {
- opacity: 1;
- max-height: 500px;
- }
-}
-
-.step-log-feed-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-bottom: 0.4rem;
- margin-bottom: 0.4rem;
- border-bottom: 1px solid #f3f4f6;
-}
-
-.step-log-feed-header-left {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.step-log-feed-header-right {
- display: flex;
- align-items: center;
- gap: 0.35rem;
-}
-
-.step-log-feed-title {
- font-size: 0.75rem;
- font-weight: 600;
- color: #4b5563;
-}
-
-.step-log-feed-status {
- font-size: 0.65rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- padding: 0.1rem 0.4rem;
- border-radius: 8px;
-}
-
-.step-log-feed-status.connected {
- background: #d1fae5;
- color: #059669;
-}
-
-.step-log-feed-status.disconnected {
- background: #f3f4f6;
- color: #9ca3af;
-}
-
-.step-log-feed-interrupt-btn {
- background: #fef2f2;
- border: 1px solid #fecaca;
- color: #dc2626;
- font-size: 0.72rem;
- font-weight: 600;
- padding: 0.2rem 0.5rem;
- border-radius: 5px;
- cursor: pointer;
- transition: background 0.15s, border-color 0.15s;
-}
-
-.step-log-feed-interrupt-btn:hover {
- background: #fee2e2;
- border-color: #f87171;
-}
-
-.step-log-feed-collapse-btn {
- background: none;
- border: 1px solid #e5e7eb;
- color: #6b7280;
- font-size: 0.75rem;
- width: 22px;
- height: 22px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.15s;
- padding: 0;
-}
-
-.step-log-feed-collapse-btn:hover {
- background: #f3f4f6;
- color: #1f2937;
- border-color: #d1d5db;
-}
-
-/* Log content area */
-.step-log-feed-content {
- max-height: 280px;
- overflow-y: auto;
- background: #1a1b26;
- border-radius: 6px;
- padding: 0.5rem;
- font-family: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
- font-size: 0.75rem;
- line-height: 1.5;
-}
-
-.step-log-feed-empty {
- color: #6b7280;
- font-style: italic;
- padding: 1rem;
- text-align: center;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-}
-
-.step-log-feed-error {
- color: #f87171;
- padding: 0.25rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- font-size: 0.78rem;
-}
-
-/* Log entries */
-.step-log-entry {
- display: flex;
- gap: 0.5rem;
- padding: 0.1rem 0.25rem;
- border-radius: 2px;
-}
-
-.step-log-entry:hover {
- background: rgba(255, 255, 255, 0.03);
-}
-
-.step-log-entry-time {
- color: #565f89;
- white-space: nowrap;
- flex-shrink: 0;
- min-width: 5.5em;
-}
-
-.step-log-entry-content {
- color: #a9b1d6;
- word-break: break-word;
- white-space: pre-wrap;
-}
-
-.step-log-entry--stderr .step-log-entry-content {
- color: #f7768e;
-}
-
-.step-log-entry--system .step-log-entry-content {
- color: #7aa2f7;
- font-style: italic;
-}
-
-.step-log-entry--user .step-log-entry-content {
- color: #9ece6a;
-}
-
-.step-log-entry--user::before {
- content: '> ';
- color: #9ece6a;
-}
-
-/* Message input */
-.step-log-feed-input {
- display: flex;
- gap: 0.35rem;
- margin-top: 0.4rem;
-}
-
-.step-log-feed-input-field {
- flex: 1;
- background: #f9fafb;
- border: 1px solid #e5e7eb;
- border-radius: 5px;
- padding: 0.35rem 0.6rem;
- font-size: 0.78rem;
- color: #1f2937;
- outline: none;
- transition: border-color 0.15s;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-}
-
-.step-log-feed-input-field:focus {
- border-color: #93c5fd;
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
-}
-
-.step-log-feed-input-field:disabled {
- opacity: 0.5;
-}
-
-.step-log-feed-send-btn {
- background: #3b82f6;
- border: none;
- color: #ffffff;
- font-size: 0.82rem;
- padding: 0.35rem 0.65rem;
- border-radius: 5px;
- cursor: pointer;
- transition: background 0.15s;
- white-space: nowrap;
-}
-
-.step-log-feed-send-btn:hover:not(:disabled) {
- background: #2563eb;
-}
-
-.step-log-feed-send-btn:disabled {
- background: #93c5fd;
- cursor: not-allowed;
-}
-
-/* ---- Responsive ---- */
-@media (max-width: 640px) {
- .steps-diagram {
- padding: 0.75rem;
- }
-
- .steps-diagram-card-wrapper {
- flex: 1 1 100%;
- max-width: 100%;
- }
-
- .steps-diagram-card-wrapper {
- flex: 1 1 100%;
- max-width: 100%;
- }
-
- .step-log-feed-content {
- max-height: 200px;
- }
-}
diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
index 53f860e..ac1cb83 100644
--- a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
+++ b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
@@ -70,32 +70,7 @@ function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProp
<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 */}
@@ -120,18 +95,6 @@ export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDi
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);
diff --git a/frontend/src/components/document/nodes/StepsDiagramNode.tsx b/frontend/src/components/document/nodes/StepsDiagramNode.tsx
deleted file mode 100644
index 8b37f52..0000000
--- a/frontend/src/components/document/nodes/StepsDiagramNode.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- DecoratorNode,
- DOMExportOutput,
- LexicalNode,
- NodeKey,
- SerializedLexicalNode,
- Spread,
-} from 'lexical';
-import React from 'react';
-import { StepsDiagramComponent } from './StepsDiagramComponent';
-
-export type SerializedStepsDiagramNode = Spread<
- {
- directiveId: string;
- },
- SerializedLexicalNode
->;
-
-export class StepsDiagramNode extends DecoratorNode<JSX.Element> {
- __directiveId: string;
-
- static getType(): string {
- return 'steps-diagram';
- }
-
- static clone(node: StepsDiagramNode): StepsDiagramNode {
- return new StepsDiagramNode(node.__directiveId, node.__key);
- }
-
- constructor(directiveId: string, key?: NodeKey) {
- super(key);
- this.__directiveId = directiveId;
- }
-
- createDOM(): HTMLElement {
- const div = document.createElement('div');
- div.className = 'steps-diagram-block';
- return div;
- }
-
- updateDOM(): boolean {
- return false;
- }
-
- decorate(): JSX.Element {
- return <StepsDiagramComponent directiveId={this.__directiveId} />;
- }
-
- exportJSON(): SerializedStepsDiagramNode {
- return {
- ...super.exportJSON(),
- type: 'steps-diagram',
- directiveId: this.__directiveId,
- version: 1,
- };
- }
-
- static importJSON(serializedNode: SerializedStepsDiagramNode): StepsDiagramNode {
- return $createStepsDiagramNode(serializedNode.directiveId);
- }
-
- isInline(): boolean {
- return false;
- }
-
- canInsertTextBefore(): boolean {
- return false;
- }
-
- canInsertTextAfter(): boolean {
- return false;
- }
-
- exportDOM(): DOMExportOutput {
- const element = document.createElement('div');
- element.className = 'steps-diagram-block';
- element.setAttribute('data-directive-id', this.__directiveId);
- element.textContent = '[Steps Diagram]';
- return { element };
- }
-}
-
-export function $createStepsDiagramNode(directiveId: string): StepsDiagramNode {
- return new StepsDiagramNode(directiveId);
-}
-
-export function $isStepsDiagramNode(
- node: LexicalNode | null | undefined,
-): node is StepsDiagramNode {
- return node instanceof StepsDiagramNode;
-}