diff options
Diffstat (limited to 'frontend/src/components/document/nodes/ContractLogFeed.tsx')
| -rw-r--r-- | frontend/src/components/document/nodes/ContractLogFeed.tsx | 225 |
1 files changed, 0 insertions, 225 deletions
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> - ); -} |
