summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/nodes/ContractLogFeed.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/document/nodes/ContractLogFeed.tsx')
-rw-r--r--frontend/src/components/document/nodes/ContractLogFeed.tsx225
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">
- &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>
- );
-}