diff options
Diffstat (limited to 'frontend/src/components/document/nodes')
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">📦</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">📦</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"> - × - </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" > ⏹ 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; -} |
