New web interface, move llm engine to separate process

This commit is contained in:
2025-05-20 09:43:17 +02:00
parent 895a533e01
commit d4a4902b94
137 changed files with 4850 additions and 3503 deletions

View File

@@ -1,4 +0,0 @@
node_modules
dist
.git
*.log

5
web/.gitignore vendored
View File

@@ -1,5 +0,0 @@
.DS_Store
coverage
dist
node_modules
package-lock.json

View File

@@ -1,12 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SIA Web Interface</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -1,43 +1,27 @@
{
"name": "sia-web",
"private": true,
"name": "sia-web-interface",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "jest",
"test:watch": "jest --watch"
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"monaco-editor": "^0.52.0",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.9",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.1.0"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.0",
"vite": "^6.3.5"
}
}
}

77
web/src/App.tsx Normal file
View File

@@ -0,0 +1,77 @@
import React, { useState } from 'react';
import useScreenSize from './hooks/useScreenSize';
import { MobileControls, Ribbon, MobileTaskbar } from './components/Layout';
import { ResponseEditor } from './components/Response';
import { MemoryEditor } from './components/Memory';
import { ChatWindow } from './components/Chat';
import { WebSocketProvider } from './contexts/WebSocketContext';
import { MonacoProvider } from './contexts/MonacoContext';
const App: React.FC = () => {
const [mobileView, setMobileView] = useState<'controls' | 'memory' | 'response' | 'chat'>('controls');
const screenSize = useScreenSize();
return (
<WebSocketProvider>
<MonacoProvider>
<div className="h-screen bg-gray-100 flex flex-col">
<Ribbon />
<div className="flex-1 flex flex-col md:flex-row overflow-hidden">
<div className={`
${screenSize === 'mobile' ? 'flex-1' : 'hidden'}
${mobileView !== 'controls' && screenSize === 'mobile' ? 'hidden' : ''}
overflow-auto pb-16
`}>
<MobileControls />
</div>
<div className={`
${screenSize === 'mobile' ? 'flex-1' : 'hidden'}
${mobileView !== 'memory' && screenSize === 'mobile' ? 'hidden' : ''}
overflow-auto pb-16
`}>
<MemoryEditor className="h-full" />
</div>
<div className={`
${screenSize === 'mobile' ? 'flex-1' : 'hidden'}
${mobileView !== 'response' && screenSize === 'mobile' ? 'hidden' : ''}
overflow-auto pb-16
`}>
<ResponseEditor className="h-full" />
</div>
<div className={`
${screenSize === 'mobile' ? 'flex-1' : 'hidden'}
${mobileView !== 'chat' && screenSize === 'mobile' ? 'hidden' : ''}
overflow-auto pb-16
`}>
<ChatWindow className="h-full" />
</div>
<div className={`${screenSize === 'tablet' ? 'flex-1' : 'hidden'} flex flex-col overflow-hidden`}>
<div className="h-1/2 border-b overflow-auto">
<MemoryEditor className="h-full" />
</div>
<div className="h-1/2 overflow-hidden">
<ResponseEditor className="h-full" />
</div>
</div>
<div className={`${screenSize === 'desktop' ? 'w-1/2' : 'hidden'} border-r overflow-auto`}>
<MemoryEditor className="h-full" />
</div>
<div className={`${screenSize === 'desktop' ? 'w-1/2' : 'hidden'} overflow-hidden`}>
<ResponseEditor className="h-full" />
</div>
</div>
{screenSize === 'mobile' && <MobileTaskbar mobileView={mobileView} setMobileView={setMobileView} />}
{screenSize !== 'mobile' && <ChatWindow />}
</div>
</MonacoProvider>
</WebSocketProvider>
);
};
export default App;

View File

@@ -1,336 +0,0 @@
import React, { useState, useEffect } from 'react';
import { ContentEditor } from '@/components/editors/ContentEditor';
import { FloatingButton } from '@/components/FloatingButton';
import { Header, Tabs } from '@/components/Header';
import { MemoryEditor } from '@/components/editors/MemoryEditor';
import { Sidebar } from '@/components/Sidebar';
import { StandardEditor } from '@/components/editors/StandardEditor';
import { useWebSocket, WebSocketState } from '@/hooks/useWebSocket';
export const LlmState = {
IDLE: 'IDLE',
INFERENCE: 'INFERENCE'
};
const App = () => {
// Editor content state
const [generatedContext, setGeneratedContext] = useState('');
const [modifiedContext, setModifiedContext] = useState('');
const [generatedResponse, setGeneratedResponse] = useState('');
const [modifiedResponse, setModifiedResponse] = useState('');
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
// LLM state
const [llms, setLlms] = useState({});
const [activeLlm, setActiveLlm] = useState(null);
// Auto approver state
const [autoApproverConfig, setAutoApproverConfig] = useState({
context_enabled: false,
response_enabled: false,
context_timeout: 5.0,
response_timeout: 10.0,
llm_name: null
});
// UI state
const [activeTab, setActiveTab] = useState(Tabs.CONTEXT);
const [showDiff, setShowDiff] = useState(false);
const [showSidebar, setShowSidebar] = useState(false);
const [autoScroll, setAutoScroll] = useState(false);
// WebSocket connections
const wsRoot = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
const llmWs = useWebSocket(`${wsRoot}/llms`);
const contextWs = useWebSocket(`${wsRoot}/context`);
const responseWs = useWebSocket(`${wsRoot}/response`);
const stdoutWs = useWebSocket(`${wsRoot}/stdout`);
const autoApproverWs = useWebSocket(`${wsRoot}/auto_approver`);
const memoryWs = useWebSocket(`${wsRoot}/memory`);
const [entries, setEntries] = useState([]);
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
// Handle llm state changes
useEffect(() => {
llmWs.addMessageHandler((data) => {
setLlms(prevLlms => ({
...prevLlms,
[data.llm]: data.state
}));
});
}, []);
// Handle context changes
useEffect(() => {
contextWs.addMessageHandler((data) => {
setModifiedContext(data.context);
if (data.generated) {
setGeneratedContext(data.context);
}
});
}, []);
// Handle response changes
useEffect(() => {
responseWs.addMessageHandler((data) => {
setModifiedResponse(data.response);
setGeneratedResponse(data.response);
});
}, []);
// Handle stdout changes
useEffect(() => {
stdoutWs.addMessageHandler((data) => {
setOutput(data.output);
});
}, []);
// Handle auto approver config updates
useEffect(() => {
autoApproverWs.addMessageHandler((data) => {
setAutoApproverConfig(data.config);
});
}, []);
// Handle memory state changes
useEffect(() => {
memoryWs.addMessageHandler((data) => {
if (data.type === 'memory_state') {
setEntries(data.entries);
}
});
}, []);
// Initialize active llm
useEffect(() => {
if (activeLlm === null && Object.keys(llms).length > 0) {
setActiveLlm(Object.keys(llms)[0]);
}
}, [llms, activeLlm]);
const handleNextLlm = () => {
const llmNames = Object.keys(llms);
const currentIndex = llmNames.indexOf(activeLlm);
const nextIndex = (currentIndex + 1) % llmNames.length;
setActiveLlm(llmNames[nextIndex]);
};
const handleApprove = () => {
fetch('/api/response/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponse })
});
};
const handleInference = () => {
fetch(`/api/inference/${activeLlm}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
response: modifiedResponse,
context: modifiedContext,
})
});
};
const handleStop = () => {
// We still specify which LLM to stop inference for
fetch(`/api/inference/${activeLlm}/stop`, {
method: 'POST'
});
};
const handleSendInput = () => {
if (input.trim()) {
fetch('/api/input', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: input })
});
setInput('');
}
};
const handleClearOutput = () => {
fetch('/api/clear', { method: 'POST' });
};
const handleContextEdit = (context) => {
setContextDirty(true);
// When context is edited, we need to reset all LLM states to IDLE
// since responses are no longer valid
const resetLlms = {};
for (const llm of Object.keys(llms)) {
resetLlms[llm] = LlmState.IDLE;
}
setLlms(resetLlms);
setModifiedContext(context);
};
const handleResponseEdit = (response) => {
setModifiedResponse(response);
};
const handleAutoApproverConfigChange = async (config) => {
try {
const response = await fetch('/api/auto_approver/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) {
throw new Error('Failed to update auto approver config');
}
} catch (error) {
console.error('Error updating auto approver config:', error);
}
};
const handleCreateEntry = async (entry) => {
const response = await fetch('/api/memory/entry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
if (!response.ok) throw new Error('Failed to create entry');
};
const handleSaveEntry = async (id, entry) => {
const response = await fetch(`/api/memory/entry/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
if (!response.ok) throw new Error('Failed to update entry');
};
const handleDeleteEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete entry');
};
const handleResetEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}/reset`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to reset entry');
};
const handleUpdateEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}/update`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to update entry');
};
// Determine active LLM's current state for UI feedback
const activeLlmState = activeLlm ? llms[activeLlm] : null;
// Track overall WebSocket connection state
useEffect(() => {
if (llmWs.wsState === WebSocketState.CONNECTED &&
contextWs.wsState === WebSocketState.CONNECTED &&
responseWs.wsState === WebSocketState.CONNECTED &&
stdoutWs.wsState === WebSocketState.CONNECTED &&
autoApproverWs.wsState === WebSocketState.CONNECTED &&
memoryWs.wsState === WebSocketState.CONNECTED) {
setWsState(WebSocketState.CONNECTED);
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
contextWs.wsState === WebSocketState.CONNECTING ||
responseWs.wsState === WebSocketState.CONNECTING ||
stdoutWs.wsState === WebSocketState.CONNECTING ||
autoApproverWs.wsState === WebSocketState.CONNECTING ||
memoryWs.wsState === WebSocketState.CONNECTING) {
setWsState(WebSocketState.CONNECTING);
} else {
setWsState(WebSocketState.DISCONNECTED);
}
}, [llmWs.wsState, contextWs.wsState, responseWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
return (
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
<Header
wsState={wsState}
agentState={activeLlmState}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
<div className="flex flex-1 overflow-hidden">
<main className="flex-1 p-4 overflow-auto">
{activeTab === Tabs.CONTEXT && (
<ContentEditor
showDiff={showDiff}
originalContent={generatedContext}
modifiedContent={modifiedContext}
onChange={handleContextEdit}
autoScroll={autoScroll}
/>
)}
{activeTab === Tabs.RESPONSE && (
<ContentEditor
showDiff={showDiff}
originalContent={generatedResponse}
modifiedContent={modifiedResponse}
onChange={handleResponseEdit}
/>
)}
{activeTab === Tabs.INPUT && (
<StandardEditor
content={input}
onChange={value => setInput(value)}
language="plaintext"
/>
)}
{activeTab === Tabs.OUTPUT && (
<StandardEditor
content={output}
readOnly={true}
language="plaintext"
/>
)}
{activeTab === Tabs.MEMORY && (
<MemoryEditor
entries={entries}
onCreateEntry={handleCreateEntry}
onSaveEntry={handleSaveEntry}
onDeleteEntry={handleDeleteEntry}
onResetEntry={handleResetEntry}
onUpdateEntry={handleUpdateEntry}
/>
)}
</main>
<Sidebar
showSidebar={showSidebar}
llms={llms}
activeLlm={activeLlm}
llmState={activeLlmState}
showDiff={showDiff}
autoScroll={autoScroll}
onAutoScrollChange={setAutoScroll}
input={input}
output={output}
autoApproverConfig={autoApproverConfig}
onApprove={handleApprove}
onInference={handleInference}
onStop={handleStop}
onToggleDiff={() => setShowDiff(!showDiff)}
onSendInput={handleSendInput}
onClearOutput={handleClearOutput}
onLlmChange={setActiveLlm}
onNextLlm={handleNextLlm}
onAutoApproverConfigChange={handleAutoApproverConfigChange}
/>
<FloatingButton onClick={() => setShowSidebar(!showSidebar)} />
</div>
</div>
);
};
export default App;

View File

@@ -0,0 +1,62 @@
import React, { useState, useRef, KeyboardEvent } from 'react';
import { Send } from 'lucide-react';
interface ChatInputProps {
onSendMessage: (message: string) => void;
disabled?: boolean;
}
const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage, disabled = false }) => {
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const handleSend = () => {
if (message.trim() && !disabled) {
onSendMessage(message.trim());
setMessage('');
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 0);
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
};
return (
<div className="border-t p-3 bg-white">
<div className="flex gap-2">
<input
ref={inputRef}
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
className="flex-1 px-3 py-2 border rounded-lg text-sm"
disabled={disabled}
autoFocus
/>
<button
onClick={handleSend}
className={`px-3 py-2 rounded-lg ${
disabled || !message.trim()
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-600 text-white'
}`}
disabled={disabled || !message.trim()}
>
<Send className="w-4 h-4" />
</button>
</div>
</div>
);
};
export default ChatInput;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Trash, CheckCheck } from 'lucide-react';
import { ChatMessage as ChatMessageType } from '../../types';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
type ChatMessageProps = ChatMessageType & {
onDelete: (id: string) => void;
};
const ChatMessage: React.FC<ChatMessageProps> = ({
id,
content,
message_type,
onDelete
}) => {
const { lastReadTimestamp } = useWebSocketContext();
const isUser = message_type === 'user';
const isRead = lastReadTimestamp !== null && id <= lastReadTimestamp;
return (
<div className={`flex mb-3 ${isUser ? 'justify-end' : 'justify-start'}`}>
<div className={`min-w-[200px] max-w-xs sm:max-w-sm px-3 py-2 rounded-lg ${
isUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-900'
}`}>
<div className={`flex items-center justify-between text-xs mb-1 ${
isUser ? 'text-blue-100' : 'text-gray-500'
}`}>
<span>{id}</span>
<div className="flex items-center gap-1">
{isUser && (
<span className="flex items-center" title={isRead ? "Read" : "Unread"}>
<CheckCheck className={`w-3 h-3 ${isRead ? 'opacity-100' : 'opacity-30'}`} />
</span>
)}
<button
onClick={() => onDelete(id)}
className={`hover:opacity-70 ${isUser ? 'text-blue-100' : 'text-gray-500'}`}
>
<Trash className="w-3 h-3" />
</button>
</div>
</div>
<p className="text-sm whitespace-pre-wrap break-words">{content}</p>
</div>
</div>
);
};
export default ChatMessage;

View File

@@ -0,0 +1,172 @@
import React, { useState, useRef, useEffect } from 'react';
import { MessageCircle, X, GripHorizontal } from 'lucide-react';
import ChatMessage from './ChatMessage';
import ChatInput from './ChatInput';
import useScreenSize from '../../hooks/useScreenSize';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
import { api } from '../../services/api';
interface ChatWindowProps {
className?: string;
}
const ChatWindow: React.FC<ChatWindowProps> = ({ className = '' }) => {
const screenSize = useScreenSize();
const [chatOpen, setChatOpen] = useState(false);
const [chatHeight, setChatHeight] = useState(400);
const [chatWidth, setChatWidth] = useState(360);
const [isDragging, setIsDragging] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const startHeightRef = useRef<number>(0);
const startWidthRef = useRef<number>(0);
const startPosRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 });
const { messages = [], isConnected } = useWebSocketContext();
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleResizeStart = (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
startHeightRef.current = chatHeight;
startWidthRef.current = chatWidth;
startPosRef.current = { x: e.clientX, y: e.clientY };
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const deltaY = startPosRef.current.y - e.clientY;
const deltaX = startPosRef.current.x - e.clientX;
const newHeight = Math.max(300, startHeightRef.current + deltaY);
const newWidth = Math.max(300, startWidthRef.current + deltaX);
setChatHeight(newHeight);
setChatWidth(newWidth);
};
const handleMouseUp = () => {
setIsDragging(false);
};
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging]);
const handleSendMessage = async (content: string) => {
try {
setIsLoading(true);
await api.addChatMessage({ message: content });
} catch (error) {
console.error('Failed to send message:', error);
} finally {
setIsLoading(false);
}
};
const handleDeleteMessage = async (id: string) => {
try {
await api.deleteChatMessage(id);
} catch (error) {
console.error('Failed to delete message:', error);
}
};
if (screenSize === 'mobile') {
return (
<div className={`h-full flex flex-col bg-white ${className}`}>
<div className="px-4 py-2 border-b bg-gray-50 flex justify-between items-center">
<h3 className="font-semibold text-sm">Chat</h3>
</div>
<div className="flex-1 overflow-y-auto p-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-400">
<p>No messages yet</p>
</div>
) : (
messages.map((message) => (
<ChatMessage
key={message.id}
{...message}
onDelete={handleDeleteMessage}
/>
))
)}
<div ref={messagesEndRef} />
</div>
<ChatInput onSendMessage={handleSendMessage} disabled={isLoading || !isConnected} />
</div>
);
}
return (
<>
{!chatOpen && (
<button
onClick={() => setChatOpen(true)}
className="fixed bottom-4 right-4 w-14 h-14 bg-blue-500 text-white rounded-full shadow-lg hover:bg-blue-600 flex items-center justify-center z-40"
aria-label="Open chat"
>
<MessageCircle className="w-6 h-6" />
</button>
)}
{chatOpen && (
<div
className="fixed bottom-0 right-4 bg-white shadow-2xl rounded-t-lg flex flex-col z-40"
style={{ height: `${chatHeight}px`, width: `${chatWidth}px` }}
>
<div
className="flex items-center justify-between p-3 border-b bg-gray-50 rounded-t-lg cursor-move"
onMouseDown={handleResizeStart}
>
<div className="flex items-center gap-2">
<GripHorizontal className="w-4 h-4 text-gray-400" />
<h3 className="font-semibold text-sm">Chat</h3>
</div>
<button
onClick={() => setChatOpen(false)}
className="text-gray-500 hover:text-gray-700 p-1"
aria-label="Close chat"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-400">
<p>No messages yet</p>
</div>
) : (
messages.map((message) => (
<ChatMessage
key={message.id}
{...message}
onDelete={handleDeleteMessage}
/>
))
)}
<div ref={messagesEndRef} />
</div>
<ChatInput onSendMessage={handleSendMessage} disabled={isLoading || !isConnected} />
</div>
)}
</>
);
};
export default ChatWindow;

View File

@@ -0,0 +1,4 @@
// src/components/Chat/index.ts
export { default as ChatWindow } from './ChatWindow';
export { default as ChatMessage } from './ChatMessage';
export { default as ChatInput } from './ChatInput';

View File

@@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
interface AgentSectionProps {
className?: string;
}
const AgentSection: React.FC<AgentSectionProps> = ({ className = '' }) => {
const { state, isConnected, llms, setActiveLLM } = useWebSocketContext();
const [isLoading, setIsLoading] = useState(false);
const handleLLMChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const newLLM = event.target.value;
try {
setIsLoading(true);
await setActiveLLM(newLLM);
} catch (error) {
console.error('Failed to set active LLM:', error);
} finally {
setIsLoading(false);
}
};
const stateColors = {
'IDLE': 'bg-green-100 text-green-800',
'INFERENCE': 'bg-yellow-100 text-yellow-800',
'PROCESSING_RESPONSE': 'bg-blue-100 text-blue-800'
};
return (
<div className={className}>
<h4 className="text-xs font-semibold text-gray-700 mb-1">Agent</h4>
<div className="grid grid-cols-2 gap-x-2 gap-y-1 items-center">
<span className="text-xs text-gray-600">Connection:</span>
<span className={`text-xs px-2 py-0.5 rounded-full text-center inline-block min-w-[85px] ${
isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{isConnected ? 'Connected' : 'Disconnected'}
</span>
<span className="text-xs text-gray-600">State:</span>
<span className={`text-xs px-2 py-0.5 rounded-full text-center inline-block min-w-[85px] ${stateColors[state.agentState]}`}>
{state.agentState}
</span>
<span className="text-xs text-gray-600">Engine:</span>
<select
value={state.activeLLM}
onChange={handleLLMChange}
disabled={isLoading || !isConnected || llms.length === 0}
className="w-full min-w-[85px] px-1 py-0.5 border rounded text-xs disabled:opacity-50 disabled:cursor-not-allowed"
>
{llms.length === 0 && (
<option value="">Loading...</option>
)}
{llms.map(llm => (
<option key={llm.name} value={llm.name}>
{llm.name}
</option>
))}
</select>
</div>
</div>
);
};
export default AgentSection;

View File

@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
interface AutoApproverSectionProps {
className?: string;
}
const AutoApproverSection: React.FC<AutoApproverSectionProps> = ({ className = '' }) => {
const { autoApprover, setAutoApproverConfig } = useWebSocketContext();
const [isLoading, setIsLoading] = useState(false);
const handleCheckboxChange = async (field: 'context_enabled' | 'response_enabled') => {
try {
setIsLoading(true);
await setAutoApproverConfig({ [field]: !autoApprover[field] });
} catch (error) {
console.error(`Failed to update ${field}:`, error);
} finally {
setIsLoading(false);
}
};
const handleTimeoutChange = async (field: 'context_timeout' | 'response_timeout', value: string) => {
const numValue = parseFloat(value);
if (!isNaN(numValue) && numValue >= 0) {
try {
setIsLoading(true);
await setAutoApproverConfig({ [field]: numValue });
} catch (error) {
console.error(`Failed to update ${field}:`, error);
} finally {
setIsLoading(false);
}
}
};
return (
<div className={className}>
<h4 className="text-xs font-semibold text-gray-700 mb-1">Auto Approver</h4>
<div className="space-y-1">
<div className="flex gap-2">
<label className="flex items-center gap-1 text-xs cursor-pointer">
<input
type="checkbox"
className="w-3 h-3"
checked={autoApprover.context_enabled}
onChange={() => handleCheckboxChange('context_enabled')}
disabled={isLoading}
/>
Context
</label>
<label className="flex items-center gap-1 text-xs cursor-pointer">
<input
type="checkbox"
className="w-3 h-3"
checked={autoApprover.response_enabled}
onChange={() => handleCheckboxChange('response_enabled')}
disabled={isLoading}
/>
Response
</label>
</div>
<div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600">Context:</span>
<div className="flex items-center">
<input
type="number"
value={autoApprover.context_timeout}
onChange={(e) => handleTimeoutChange('context_timeout', e.target.value)}
step="0.1"
min="0"
className="w-28 sm:w-16 px-1 py-0.5 border rounded text-xs text-right"
disabled={isLoading || autoApprover.context_enabled}
/>
<span className="text-xs text-gray-500 ml-1">s</span>
</div>
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-xs text-gray-600">Response:</span>
<div className="flex items-center">
<input
type="number"
value={autoApprover.response_timeout}
onChange={(e) => handleTimeoutChange('response_timeout', e.target.value)}
step="0.1"
min="0"
className="w-28 sm:w-16 px-1 py-0.5 border rounded text-xs text-right"
disabled={isLoading || autoApprover.response_enabled}
/>
<span className="text-xs text-gray-500 ml-1">s</span>
</div>
</div>
</div>
</div>
</div>
);
};
export default AutoApproverSection;

View File

@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { Play, StopCircle, Check } from 'lucide-react';
import { Button } from '../shared';
import { api } from '../../services/api';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
interface LLMSectionProps {
className?: string;
}
const LLMSection: React.FC<LLMSectionProps> = ({ className = '' }) => {
const { state } = useWebSocketContext();
const [isInferring, setIsInferring] = useState(false);
const [isStopping, setIsStopping] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const isInferenceBlocked = state.agentState === 'INFERENCE' || state.agentState === 'PROCESSING_RESPONSE';
const handleInfer = async () => {
try {
setIsInferring(true);
await api.startInference();
} catch (error) {
console.error('Failed to start inference:', error);
} finally {
setIsInferring(false);
}
};
const handleStop = async () => {
try {
setIsStopping(true);
await api.stopInference();
} catch (error) {
console.error('Failed to stop inference:', error);
} finally {
setIsStopping(false);
}
};
const handleApprove = async () => {
try {
setIsApproving(true);
await api.approveResponse();
} catch (error) {
console.error('Failed to approve response:', error);
} finally {
setIsApproving(false);
}
};
return (
<div className={className}>
<h4 className="text-xs font-semibold text-gray-700 mb-1">LLM</h4>
<div className="space-y-1">
<Button
icon={Play}
variant="primary"
size="sm"
className="w-full"
onClick={handleInfer}
disabled={isInferring || isInferenceBlocked}
>
Infer
</Button>
<Button
icon={StopCircle}
variant="danger"
size="sm"
className="w-full"
onClick={handleStop}
disabled={isStopping || state.agentState !== 'INFERENCE'}
>
Stop
</Button>
<Button
icon={Check}
variant="success"
size="sm"
className="w-full"
onClick={handleApprove}
disabled={isApproving || state.agentState === 'PROCESSING_RESPONSE'}
>
Approve
</Button>
</div>
</div>
);
};
export default LLMSection;

View File

@@ -0,0 +1,126 @@
import React, { useState, useRef } from 'react';
import { FileText, Trash, Save } from 'lucide-react';
import { Button } from '../shared';
import { api } from '../../services/api';
interface MemorySectionProps {
className?: string;
}
const MemorySection: React.FC<MemorySectionProps> = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleLoad = async () => {
fileInputRef.current?.click();
};
const handleFileLoad = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
setIsLoading(true);
const content = await file.text();
await api.loadIteration(content);
console.log('Iteration loaded successfully');
} catch (error) {
console.error('Failed to load iteration:', error);
alert('Failed to load iteration file');
} finally {
setIsLoading(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleClear = async () => {
if (!window.confirm('Are you sure you want to clear all memory entries?')) {
return;
}
try {
setIsLoading(true);
const memoryState = await api.getMemory();
const entries = memoryState.entries || [];
for (const entry of entries) {
await api.deleteMemoryEntry(entry.id);
}
console.log('Memory cleared successfully');
} catch (error) {
console.error('Failed to clear memory:', error);
alert('Failed to clear memory');
} finally {
setIsLoading(false);
}
};
const handleUpdate = async () => {
try {
setIsLoading(true);
const memoryState = await api.getMemory();
const entries = memoryState.entries || [];
for (const entry of entries) {
await api.updateMemoryEntry(entry.id);
}
console.log('Memory updated successfully');
} catch (error) {
console.error('Failed to update memory:', error);
alert('Failed to update memory');
} finally {
setIsLoading(false);
}
};
return (
<div className={className}>
<h4 className="text-xs font-semibold text-gray-700 mb-1">Memory</h4>
<div className="space-y-1">
<input
ref={fileInputRef}
type="file"
accept=".xml"
onChange={handleFileLoad}
style={{ display: 'none' }}
/>
<Button
icon={FileText}
variant="secondary"
size="sm"
className="w-full"
onClick={handleLoad}
disabled={isLoading}
>
Load
</Button>
<Button
icon={Trash}
variant="secondary"
size="sm"
className="w-full"
onClick={handleClear}
disabled={isLoading}
>
Clear
</Button>
<Button
icon={Save}
variant="secondary"
size="sm"
className="w-full"
onClick={handleUpdate}
disabled={isLoading}
>
Update
</Button>
</div>
</div>
);
};
export default MemorySection;

View File

@@ -0,0 +1,4 @@
export { default as AgentSection } from './AgentSection';
export { default as AutoApproverSection } from './AutoApproverSection';
export { default as LLMSection } from './LLMSection';
export { default as MemorySection } from './MemorySection';

View File

@@ -1,14 +0,0 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Menu } from 'lucide-react';
export const FloatingButton = ({ onClick }) => (
<Button
variant="secondary"
size="lg"
className="fixed bottom-4 right-4 rounded-full shadow-lg md:hidden z-[60]"
onClick={onClick}
>
<Menu className="h-6 w-6" />
</Button>
);

View File

@@ -1,58 +0,0 @@
import React from 'react';
import { WebSocketState } from '../hooks/useWebSocket';
export const Tabs = {
CONTEXT: 'CONTEXT',
RESPONSE: 'RESPONSE',
INPUT: 'INPUT',
OUTPUT: 'OUTPUT',
MEMORY: 'MEMORY'
};
export const Header = ({
wsState,
agentState,
activeTab,
setActiveTab,
}) => (
<header className="bg-white shadow-sm">
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center space-x-4">
<h1 className="text-xl font-semibold">SIA Control Interface</h1>
</div>
<div className="flex items-center space-x-2">
<div className={`px-3 py-1 rounded-full text-sm ${
wsState === WebSocketState.CONNECTED
? 'bg-green-100 text-green-800'
: wsState === WebSocketState.CONNECTING
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{wsState}
</div>
{agentState && (
<div className="px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm">
{agentState}
</div>
)}
</div>
</div>
<div className="border-t">
<div className="flex px-4">
{Object.values(Tabs).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 font-medium text-sm border-b-2 ${
activeTab === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1).toLowerCase()}
</button>
))}
</div>
</div>
</header>
);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { AgentSection, LLMSection, AutoApproverSection, MemorySection } from '../Controls';
const MobileControls: React.FC = () => {
return (
<div className="p-4 space-y-4 h-full overflow-y-auto bg-white">
<h3 className="text-lg font-semibold mb-4">Agent Controls</h3>
<AgentSection className="pb-4 border-b" />
<div className="pt-4">
<LLMSection className="pb-4 border-b" />
</div>
<div className="pt-4">
<AutoApproverSection className="pb-4 border-b" />
</div>
<div className="pt-4">
<MemorySection />
</div>
</div>
);
};
export default MobileControls;

View File

@@ -0,0 +1,53 @@
import React from 'react';
interface MobileTaskbarProps {
mobileView: 'controls' | 'memory' | 'response' | 'chat';
setMobileView: (view: 'controls' | 'memory' | 'response' | 'chat') => void;
}
const MobileTaskbar: React.FC<MobileTaskbarProps> = ({ mobileView, setMobileView }) => {
return (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t z-10">
<div className="flex">
<button
onClick={() => setMobileView('controls')}
className={`flex-1 flex flex-col items-center justify-center py-2 ${
mobileView === 'controls' ? 'text-blue-600' : 'text-gray-600'
}`}
>
<span className="text-lg"></span>
<span className="text-xs mt-1">Controls</span>
</button>
<button
onClick={() => setMobileView('memory')}
className={`flex-1 flex flex-col items-center justify-center py-2 ${
mobileView === 'memory' ? 'text-blue-600' : 'text-gray-600'
}`}
>
<span className="text-lg">🧠</span>
<span className="text-xs mt-1">Memory</span>
</button>
<button
onClick={() => setMobileView('response')}
className={`flex-1 flex flex-col items-center justify-center py-2 ${
mobileView === 'response' ? 'text-blue-600' : 'text-gray-600'
}`}
>
<span className="text-lg">📄</span>
<span className="text-xs mt-1">Response</span>
</button>
<button
onClick={() => setMobileView('chat')}
className={`flex-1 flex flex-col items-center justify-center py-2 ${
mobileView === 'chat' ? 'text-blue-600' : 'text-gray-600'
}`}
>
<span className="text-lg">💬</span>
<span className="text-xs mt-1">Chat</span>
</button>
</div>
</div>
);
};
export default MobileTaskbar;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import useScreenSize from '../../hooks/useScreenSize';
import { AgentSection, LLMSection, AutoApproverSection, MemorySection } from '../Controls';
const Ribbon: React.FC = () => {
const screenSize = useScreenSize();
if (screenSize === 'mobile') return null;
return (
<div className="bg-white border-b">
<div className="flex items-stretch">
<div className="px-3 py-2 border-r">
<AgentSection />
</div>
<div className="px-3 py-2 border-r">
<LLMSection />
</div>
<div className="px-3 py-2 border-r">
<AutoApproverSection />
</div>
<div className="px-3 py-2">
<MemorySection />
</div>
</div>
</div>
);
};
export default Ribbon;

View File

@@ -0,0 +1,3 @@
export { default as MobileControls } from './MobileControls';
export { default as Ribbon } from './Ribbon';
export { default as MobileTaskbar } from './MobileTaskbar';

View File

@@ -0,0 +1,269 @@
import React, { useState } from 'react';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
import { api, EntryTypes, getInitialEntryData } from '../../services/api';
import {
ReasoningEntry,
SingleEntry,
RepeatEntry,
ReadEntry,
WriteEntry,
ParseErrorEntry,
BackgroundEntry
} from './entries';
import {
EntryData,
ReasoningEntryData,
ScriptEntryData,
IOEntryData,
ParseErrorEntryData,
BackgroundEntryData
} from '../../types/entries';
import { Plus } from 'lucide-react';
interface MemoryEditorProps {
className?: string;
}
const MemoryEditor: React.FC<MemoryEditorProps> = ({ className = '' }) => {
const { memory } = useWebSocketContext();
const [isLoading, setIsLoading] = useState(false);
const [showNewEntryMenu, setShowNewEntryMenu] = useState(false);
const handleDelete = async (id: string) => {
if (!window.confirm('Are you sure you want to delete this entry?')) {
return;
}
try {
setIsLoading(true);
await api.deleteMemoryEntry(id);
} catch (error) {
console.error('Failed to delete entry:', error);
alert('Failed to delete entry');
} finally {
setIsLoading(false);
}
};
const handleUpdate = async (id: string) => {
try {
setIsLoading(true);
await api.updateMemoryEntry(id);
} catch (error) {
console.error('Failed to update entry:', error);
alert('Failed to update entry');
} finally {
setIsLoading(false);
}
};
const handleReset = async (id: string) => {
try {
setIsLoading(true);
await api.resetMemoryEntry(id);
} catch (error) {
console.error('Failed to reset entry:', error);
alert('Failed to reset entry');
} finally {
setIsLoading(false);
}
};
const handleSaveEntry = async <T extends EntryData>(id: string, data: T) => {
try {
setIsLoading(true);
await api.saveMemoryEntry(id, data);
} catch (error) {
console.error('Failed to save entry:', error);
alert('Failed to save entry');
} finally {
setIsLoading(false);
}
};
const handleCreateEntry = async (type: string) => {
try {
setIsLoading(true);
setShowNewEntryMenu(false);
const entryType = type as typeof EntryTypes[keyof typeof EntryTypes];
const newEntryData = getInitialEntryData(entryType);
await api.createMemoryEntry(newEntryData);
} catch (error) {
console.error('Failed to create entry:', error);
alert('Failed to create entry');
} finally {
setIsLoading(false);
}
};
const renderEntry = (entry: EntryData) => {
switch (entry.type) {
case EntryTypes.REASONING:
return (
<ReasoningEntry
key={entry.id}
entry={entry as ReasoningEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.SINGLE:
return (
<SingleEntry
key={entry.id}
entry={entry as ScriptEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.REPEAT:
return (
<RepeatEntry
key={entry.id}
entry={entry as ScriptEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.READ_STDIN:
return (
<ReadEntry
key={entry.id}
entry={entry as IOEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.WRITE:
return (
<WriteEntry
key={entry.id}
entry={entry as IOEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.PARSE_ERROR:
return (
<ParseErrorEntry
key={entry.id}
entry={entry as ParseErrorEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
case EntryTypes.BACKGROUND:
return (
<BackgroundEntry
key={entry.id}
entry={entry as BackgroundEntryData}
onDelete={handleDelete}
onUpdate={handleUpdate}
onReset={handleReset}
onSave={handleSaveEntry}
/>
);
default:
return null;
}
};
const renderNewEntryMenu = () => (
<div className="absolute right-4 top-12 bg-white shadow-lg rounded-lg border p-2 z-10">
<div className="text-sm font-medium mb-2 pb-1 border-b">Create New Entry</div>
<div className="space-y-1">
<button
onClick={() => handleCreateEntry(EntryTypes.REASONING)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Reasoning
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.SINGLE)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Single Script
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.REPEAT)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Repeat Script
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.READ_STDIN)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Read Input
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.WRITE)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Write Output
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.BACKGROUND)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Background Process
</button>
<button
onClick={() => handleCreateEntry(EntryTypes.PARSE_ERROR)}
className="w-full text-left px-3 py-1 text-sm hover:bg-blue-50 rounded"
>
Parse Error
</button>
</div>
</div>
);
return (
<div className={`h-full flex flex-col bg-white ${className}`}>
<div className="px-4 py-2 border-b bg-gray-50 flex justify-between items-center relative">
<h3 className="font-semibold text-sm">Memory / Context</h3>
<button
onClick={() => setShowNewEntryMenu(!showNewEntryMenu)}
className="p-0 text-blue-600 hover:bg-blue-50 rounded"
title="Create New Entry"
disabled={isLoading}
>
<Plus className="w-4 h-4" />
</button>
{showNewEntryMenu && renderNewEntryMenu()}
</div>
<div className="flex-1 overflow-y-auto p-4">
{isLoading && (
<div className="fixed top-0 left-0 right-0 bg-blue-500 h-1 z-50">
<div className="animate-pulse bg-blue-300 h-full w-24"></div>
</div>
)}
{memory.length === 0 ? (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">No memory entries</p>
</div>
) : (
memory.map((entry) => renderEntry(entry))
)}
</div>
</div>
);
};
export default MemoryEditor;

View File

@@ -0,0 +1,158 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { BackgroundEntryData } from '../../../types/entries';
interface BackgroundEntryProps {
entry: BackgroundEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: BackgroundEntryData) => Promise<void>;
}
const BackgroundEntry: React.FC<BackgroundEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: BackgroundEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap overflow-x-auto">{data.script || ''}</pre>
</div>
{data.pid !== null && data.pid !== undefined && (
<div>
<span className="text-sm font-medium text-gray-700">Process ID:</span>
<span className="ml-2 text-sm text-gray-900">{data.pid}</span>
</div>
)}
{data.stdout && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded overflow-x-auto max-h-40">{data.stdout}</pre>
</div>
)}
{data.stderr && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-red-50 p-2 rounded overflow-x-auto max-h-40">{data.stderr}</pre>
</div>
)}
{data.exit_code !== null && data.exit_code !== undefined && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{data.exit_code}</span>
</div>
)}
</div>
);
const renderEditForm = (
data: BackgroundEntryData,
onChange: (data: BackgroundEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Script</label>
<textarea
value={data.script || ''}
onChange={e => onChange({ ...data, script: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.pid !== null && data.pid !== undefined}
onChange={e => onChange({
...data,
pid: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Process ID</label>
</div>
<input
type="number"
value={data.pid !== null && data.pid !== undefined ? data.pid : ''}
onChange={e => onChange({ ...data, pid: e.target.value ? parseInt(e.target.value) : null })}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
min="0"
disabled={isLoading || data.pid === null || data.pid === undefined}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Output</label>
<textarea
value={data.stdout || ''}
onChange={e => onChange({ ...data, stdout: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Error</label>
<textarea
value={data.stderr || ''}
onChange={e => onChange({ ...data, stderr: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.exit_code !== null && data.exit_code !== undefined}
onChange={e => onChange({
...data,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={data.exit_code !== null && data.exit_code !== undefined ? data.exit_code : ''}
onChange={e => onChange({ ...data, exit_code: e.target.value ? parseInt(e.target.value) : null })}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
disabled={isLoading || data.exit_code === null || data.exit_code === undefined}
/>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default BackgroundEntry;

View File

@@ -0,0 +1,157 @@
import React, { useState } from 'react';
import { Edit2, Trash, RefreshCw, RotateCcw, X, Check } from 'lucide-react';
import { EntryData } from '../../../types/entries';
export interface BaseEntryProps<T extends EntryData> {
entry: T;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: T) => Promise<void>;
renderContent: (entry: T) => React.ReactNode;
renderEditForm: (
data: T,
onChange: (data: T) => void,
isLoading: boolean
) => React.ReactNode;
}
export function BaseEntry<T extends EntryData>({
entry,
onDelete,
onUpdate,
onReset,
onSave,
renderContent,
renderEditForm
}: BaseEntryProps<T>) {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<T>(entry);
const [isLoading, setIsLoading] = useState(false);
const [originalId, setOriginalId] = useState<string>(entry.id);
const getTypeColor = (type: string) => {
const colors: Record<string, string> = {
'reasoning': 'bg-blue-100 text-blue-800',
'repeat': 'bg-green-100 text-green-800',
'read_stdin': 'bg-purple-100 text-purple-800',
'single': 'bg-yellow-100 text-yellow-800',
'write_stdout': 'bg-pink-100 text-pink-800',
'parse_error': 'bg-red-100 text-red-800',
'background': 'bg-gray-100 text-gray-800'
};
return colors[type] || 'bg-gray-100 text-gray-800';
};
const handleEditClick = () => {
setEditData(entry);
setOriginalId(entry.id);
setIsEditing(true);
};
const handleCancel = () => {
setIsEditing(false);
};
const handleSave = async () => {
try {
setIsLoading(true);
await onSave(originalId, editData);
setIsEditing(false);
} catch (error) {
console.error('Failed to save entry:', error);
} finally {
setIsLoading(false);
}
};
return (
<div className="border rounded-lg p-3 mb-3 bg-white hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
<div>
<span className={`inline-block px-2 py-0.5 text-xs rounded ${getTypeColor(entry.type)}`}>
{entry.type}
</span>
<span className="ml-2 text-xs text-gray-500">{entry.id}</span>
</div>
{isEditing ? (
<div className="flex gap-1">
<button
onClick={handleCancel}
className="p-1 text-gray-600 hover:bg-gray-50 rounded"
title="Cancel"
disabled={isLoading}
>
<X className="w-3 h-3" />
</button>
<button
onClick={handleSave}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Save"
disabled={isLoading}
>
<Check className="w-3 h-3" />
</button>
</div>
) : (
<div className="flex gap-1">
<button
onClick={() => onReset(entry.id)}
className="p-1 text-orange-600 hover:bg-orange-50 rounded"
title="Reset"
>
<RotateCcw className="w-3 h-3" />
</button>
<button
onClick={() => onUpdate(entry.id)}
className="p-1 text-gray-600 hover:bg-gray-50 rounded"
title="Update"
>
<RefreshCw className="w-3 h-3" />
</button>
<button
onClick={handleEditClick}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Edit"
>
<Edit2 className="w-3 h-3" />
</button>
<button
onClick={() => onDelete(entry.id)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Delete"
>
<Trash className="w-3 h-3" />
</button>
</div>
)}
</div>
{isEditing ? (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">ID (Timestamp)</label>
<input
type="text"
value={editData.id}
onChange={(e) => setEditData({...editData, id: e.target.value})}
className="w-full p-2 border rounded text-sm"
disabled={isLoading}
/>
</div>
{renderEditForm(
editData,
(newData) => setEditData(newData),
isLoading
)}
</div>
) : (
renderContent(entry)
)}
</div>
);
}
export default BaseEntry;

View File

@@ -0,0 +1,87 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { IOEntryData } from '../../../types/entries';
interface IOEntryProps {
entry: IOEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: IOEntryData) => Promise<void>;
}
const IOEntry: React.FC<IOEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: IOEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{data.content || ''}</pre>
</div>
<div>
<span className="text-sm text-gray-700">
{data.type === 'read_stdin'
? (data.read ? '✓ Read' : '✗ Not Read')
: (data.written ? '✓ Written' : '✗ Not Written')}
</span>
</div>
</div>
);
const renderEditForm = (
data: IOEntryData,
onChange: (data: IOEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
value={data.content || ''}
onChange={e => onChange({ ...data, content: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={data.type === 'read_stdin' ? (data.read || false) : (data.written || false)}
onChange={e => onChange({
...data,
...(data.type === 'read_stdin'
? { read: e.target.checked }
: { written: e.target.checked })
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">
{data.type === 'read_stdin' ? 'Read' : 'Written'}
</span>
</label>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default IOEntry;

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { ParseErrorEntryData } from '../../../types/entries';
interface ParseErrorEntryProps {
entry: ParseErrorEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: ParseErrorEntryData) => Promise<void>;
}
const ParseErrorEntry: React.FC<ParseErrorEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: ParseErrorEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{data.content || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-red-700">Error:</span>
<pre className="mt-1 text-sm text-red-900 bg-red-50 p-2 rounded whitespace-pre-wrap">{data.error || ''}</pre>
</div>
</div>
);
const renderEditForm = (
data: ParseErrorEntryData,
onChange: (data: ParseErrorEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
value={data.content || ''}
onChange={e => onChange({ ...data, content: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-red-700 mb-1">Error</label>
<textarea
value={data.error || ''}
onChange={e => onChange({ ...data, error: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default ParseErrorEntry;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { IOEntryData } from '../../../types/entries';
interface ReadEntryProps {
entry: IOEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: IOEntryData) => Promise<void>;
}
const ReadEntry: React.FC<ReadEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: IOEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{data.content || ''}</pre>
</div>
<div>
<span className="text-sm text-gray-700">
{data.read ? '✓ Read' : '✗ Not Read'}
</span>
</div>
</div>
);
const renderEditForm = (
data: IOEntryData,
onChange: (data: IOEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
value={data.content || ''}
onChange={e => onChange({ ...data, content: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={data.read || false}
onChange={e => onChange({ ...data, read: e.target.checked })}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">Read</span>
</label>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default ReadEntry;

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { ReasoningEntryData } from '../../../types/entries';
interface ReasoningEntryProps {
entry: ReasoningEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: ReasoningEntryData) => Promise<void>;
}
const ReasoningEntry: React.FC<ReasoningEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: ReasoningEntryData) => (
<p className="text-sm text-gray-700 whitespace-pre-wrap">{data.content}</p>
);
const renderEditForm = (
data: ReasoningEntryData,
onChange: (data: ReasoningEntryData) => void,
isLoading: boolean
) => (
<div className="mt-2">
<textarea
value={data.content}
onChange={(e) => onChange({ ...data, content: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={6}
disabled={isLoading}
/>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default ReasoningEntry;

View File

@@ -0,0 +1,215 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { ScriptEntryData } from '../../../types/entries';
interface RepeatEntryProps {
entry: ScriptEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: ScriptEntryData) => Promise<void>;
}
const RepeatEntry: React.FC<RepeatEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: ScriptEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap overflow-x-auto">{data.script || ''}</pre>
</div>
{data.timeout !== undefined && data.timeout !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Timeout:</span>
<span className="ml-2 text-sm text-gray-900">{data.timeout} seconds</span>
</div>
)}
{data.limit !== undefined && data.limit !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Memory Limit:</span>
<span className="ml-2 text-sm text-gray-900">{data.limit} bytes</span>
</div>
)}
{data.stdout && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded overflow-x-auto max-h-40">{data.stdout}</pre>
</div>
)}
{data.stderr && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-red-50 p-2 rounded overflow-x-auto max-h-40">{data.stderr}</pre>
</div>
)}
{data.exit_code !== null && data.exit_code !== undefined && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{data.exit_code}</span>
</div>
)}
<div>
<span className="text-sm text-gray-700">
{data.timed_out ? '⚠ Timed Out' : '✓ Within Timeout'}
</span>
</div>
</div>
);
const renderEditForm = (
data: ScriptEntryData,
onChange: (data: ScriptEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Script</label>
<textarea
value={data.script || ''}
onChange={e => onChange({ ...data, script: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.timeout !== null && data.timeout !== undefined}
onChange={e => onChange({
...data,
timeout: e.target.checked ? 1.0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Timeout (seconds)</label>
</div>
<input
type="number"
value={data.timeout !== null && data.timeout !== undefined ? data.timeout : ''}
onChange={e => onChange({
...data,
timeout: e.target.value ? parseFloat(e.target.value) : null
})}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
step="0.1"
min="0"
disabled={isLoading || data.timeout === null || data.timeout === undefined}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.limit !== null && data.limit !== undefined}
onChange={e => onChange({
...data,
limit: e.target.checked ? 1024 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Memory Limit (bytes)</label>
</div>
<input
type="number"
value={data.limit !== null && data.limit !== undefined ? data.limit : ''}
onChange={e => onChange({
...data,
limit: e.target.value ? parseInt(e.target.value) : null
})}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
min="0"
disabled={isLoading || data.limit === null || data.limit === undefined}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Output</label>
<textarea
value={data.stdout || ''}
onChange={e => onChange({ ...data, stdout: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Error</label>
<textarea
value={data.stderr || ''}
onChange={e => onChange({ ...data, stderr: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.exit_code !== null && data.exit_code !== undefined}
onChange={e => onChange({
...data,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={data.exit_code !== null && data.exit_code !== undefined ? data.exit_code : ''}
onChange={e => onChange({ ...data, exit_code: e.target.value ? parseInt(e.target.value) : null })}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
disabled={isLoading || data.exit_code === null || data.exit_code === undefined}
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={data.timed_out || false}
onChange={e => onChange({ ...data, timed_out: e.target.checked })}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">Timed Out</span>
</label>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default RepeatEntry;

View File

@@ -0,0 +1,127 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { ScriptEntryData } from '../../../types/entries';
interface ScriptEntryProps {
entry: ScriptEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: ScriptEntryData) => Promise<void>;
}
const ScriptEntry: React.FC<ScriptEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: ScriptEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap overflow-x-auto">{data.script || ''}</pre>
</div>
{data.stdout && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded overflow-x-auto max-h-40">{data.stdout}</pre>
</div>
)}
{data.stderr && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-red-50 p-2 rounded overflow-x-auto max-h-40">{data.stderr}</pre>
</div>
)}
{data.exit_code !== null && data.exit_code !== undefined && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{data.exit_code}</span>
</div>
)}
</div>
);
const renderEditForm = (
data: ScriptEntryData,
onChange: (data: ScriptEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Script</label>
<textarea
value={data.script || ''}
onChange={e => onChange({ ...data, script: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Output</label>
<textarea
value={data.stdout || ''}
onChange={e => onChange({ ...data, stdout: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Error</label>
<textarea
value={data.stderr || ''}
onChange={e => onChange({ ...data, stderr: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.exit_code !== null && data.exit_code !== undefined}
onChange={e => onChange({
...data,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={data.exit_code !== null && data.exit_code !== undefined ? data.exit_code : ''}
onChange={e => onChange({ ...data, exit_code: e.target.value ? parseInt(e.target.value) : null })}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
disabled={isLoading || data.exit_code === null || data.exit_code === undefined}
/>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default ScriptEntry;

View File

@@ -0,0 +1,228 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { ScriptEntryData } from '../../../types/entries';
interface SingleEntryProps {
entry: ScriptEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: ScriptEntryData) => Promise<void>;
}
const SingleEntry: React.FC<SingleEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: ScriptEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap overflow-x-auto">{data.script || ''}</pre>
</div>
{data.timeout !== undefined && data.timeout !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Timeout:</span>
<span className="ml-2 text-sm text-gray-900">{data.timeout} seconds</span>
</div>
)}
{data.limit !== undefined && data.limit !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Memory Limit:</span>
<span className="ml-2 text-sm text-gray-900">{data.limit} bytes</span>
</div>
)}
{data.stdout && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded overflow-x-auto max-h-40">{data.stdout}</pre>
</div>
)}
{data.stderr && (
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-red-50 p-2 rounded overflow-x-auto max-h-40">{data.stderr}</pre>
</div>
)}
{data.exit_code !== null && data.exit_code !== undefined && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{data.exit_code}</span>
</div>
)}
<div className="flex space-x-4">
<span className="text-sm text-gray-700">
{data.executed ? '✓ Executed' : '✗ Not Executed'}
</span>
<span className="text-sm text-gray-700">
{data.timed_out ? '⚠ Timed Out' : '✓ Within Timeout'}
</span>
</div>
</div>
);
const renderEditForm = (
data: ScriptEntryData,
onChange: (data: ScriptEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Script</label>
<textarea
value={data.script || ''}
onChange={e => onChange({ ...data, script: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={4}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.timeout !== null && data.timeout !== undefined}
onChange={e => onChange({
...data,
timeout: e.target.checked ? 1.0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Timeout (seconds)</label>
</div>
<input
type="number"
value={data.timeout !== null && data.timeout !== undefined ? data.timeout : ''}
onChange={e => onChange({
...data,
timeout: e.target.value ? parseFloat(e.target.value) : null
})}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
step="0.1"
min="0"
disabled={isLoading || data.timeout === null || data.timeout === undefined}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.limit !== null && data.limit !== undefined}
onChange={e => onChange({
...data,
limit: e.target.checked ? 1024 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Memory Limit (bytes)</label>
</div>
<input
type="number"
value={data.limit !== null && data.limit !== undefined ? data.limit : ''}
onChange={e => onChange({
...data,
limit: e.target.value ? parseInt(e.target.value) : null
})}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
min="0"
disabled={isLoading || data.limit === null || data.limit === undefined}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Output</label>
<textarea
value={data.stdout || ''}
onChange={e => onChange({ ...data, stdout: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standard Error</label>
<textarea
value={data.stderr || ''}
onChange={e => onChange({ ...data, stderr: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={3}
disabled={isLoading}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={data.exit_code !== null && data.exit_code !== undefined}
onChange={e => onChange({
...data,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
disabled={isLoading}
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={data.exit_code !== null && data.exit_code !== undefined ? data.exit_code : ''}
onChange={e => onChange({ ...data, exit_code: e.target.value ? parseInt(e.target.value) : null })}
className="w-full p-2 border rounded text-sm disabled:bg-gray-100"
disabled={isLoading || data.exit_code === null || data.exit_code === undefined}
/>
</div>
<div className="flex space-x-4">
<label className="flex items-center">
<input
type="checkbox"
checked={data.executed || false}
onChange={e => onChange({ ...data, executed: e.target.checked })}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">Executed</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={data.timed_out || false}
onChange={e => onChange({ ...data, timed_out: e.target.checked })}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">Timed Out</span>
</label>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default SingleEntry;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { BaseEntry } from './BaseEntry';
import { IOEntryData } from '../../../types/entries';
interface WriteEntryProps {
entry: IOEntryData;
onDelete: (id: string) => void;
onUpdate: (id: string) => void;
onReset: (id: string) => void;
onSave: (id: string, data: IOEntryData) => Promise<void>;
}
const WriteEntry: React.FC<WriteEntryProps> = ({
entry,
onDelete,
onUpdate,
onReset,
onSave
}) => {
const renderContent = (data: IOEntryData) => (
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{data.content || ''}</pre>
</div>
<div>
<span className="text-sm text-gray-700">
{data.written ? '✓ Written' : '✗ Not Written'}
</span>
</div>
</div>
);
const renderEditForm = (
data: IOEntryData,
onChange: (data: IOEntryData) => void,
isLoading: boolean
) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
value={data.content || ''}
onChange={e => onChange({ ...data, content: e.target.value })}
className="w-full p-2 border rounded font-mono text-sm resize-vertical"
rows={6}
disabled={isLoading}
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={data.written || false}
onChange={e => onChange({ ...data, written: e.target.checked })}
className="rounded border-gray-300"
disabled={isLoading}
/>
<span className="ml-2 text-sm text-gray-700">Written</span>
</label>
</div>
</div>
);
return (
<BaseEntry
entry={entry}
onDelete={onDelete}
onUpdate={onUpdate}
onReset={onReset}
onSave={onSave}
renderContent={renderContent}
renderEditForm={renderEditForm}
/>
);
};
export default WriteEntry;

View File

@@ -0,0 +1,8 @@
export { default as BaseEntry } from './BaseEntry';
export { default as BackgroundEntry } from './BackgroundEntry';
export { default as ParseErrorEntry } from './ParseErrorEntry';
export { default as ReadEntry } from './ReadEntry';
export { default as ReasoningEntry } from './ReasoningEntry';
export { default as RepeatEntry } from './RepeatEntry';
export { default as SingleEntry } from './SingleEntry';
export { default as WriteEntry } from './WriteEntry';

View File

@@ -0,0 +1 @@
export { default as MemoryEditor } from './MemoryEditor';

View File

@@ -0,0 +1,77 @@
import React, { useState, useRef } from 'react';
import Editor from '@monaco-editor/react';
import { useWebSocketContext } from '../../contexts/WebSocketContext';
import { useMonaco } from '../../contexts/MonacoContext';
interface ResponseEditorProps {
className?: string;
}
const ResponseEditor: React.FC<ResponseEditorProps> = ({ className = '' }) => {
const { response, setResponse } = useWebSocketContext();
const { isMonacoLoading } = useMonaco();
const editorRef = useRef<any>(null);
const lastUserEditTimeRef = useRef<number>(0);
const inactivityTimerRef = useRef<number | null>(null);
const [localResponse, setLocalResponse] = useState<string | null>(null);
const displayValue = localResponse !== null ? localResponse : response;
const handleEditorDidMount = (editor: any) => {
editorRef.current = editor;
};
const handleEditorChange = (value: string | undefined) => {
if (value !== undefined) {
setLocalResponse(value);
lastUserEditTimeRef.current = Date.now();
if (inactivityTimerRef.current !== null) {
clearTimeout(inactivityTimerRef.current);
}
inactivityTimerRef.current = window.setTimeout(() => {
setResponse(value);
setLocalResponse(null);
inactivityTimerRef.current = null;
}, 1000);
}
};
if (isMonacoLoading) {
return (
<div className={`h-full flex items-center justify-center bg-white ${className}`}>
<div className="text-gray-500">Loading editor...</div>
</div>
);
}
return (
<div className={`h-full flex flex-col bg-white ${className}`}>
<div className="px-4 py-2 border-b bg-gray-50">
<h3 className="font-semibold text-sm">Response</h3>
</div>
<div className="flex-1 relative">
<Editor
height="100%"
defaultLanguage="xml"
value={displayValue}
onChange={handleEditorChange}
onMount={handleEditorDidMount}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontFamily: 'monospace',
fontSize: 14,
lineNumbers: 'on',
folding: true,
wordWrap: 'on'
}}
/>
</div>
</div>
);
};
export default ResponseEditor;

View File

@@ -0,0 +1 @@
export { default as ResponseEditor } from './ResponseEditor';

View File

@@ -1,177 +0,0 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { LlmState } from '@/components/App';
export const Sidebar = ({
showSidebar,
llms,
activeLlm,
llmState,
showDiff,
autoScroll,
input,
output,
autoApproverConfig,
onApprove,
onInference,
onStop,
onToggleDiff,
onSendInput,
onClearOutput,
onLlmChange,
onNextLlm,
onAutoScrollChange,
onAutoApproverConfigChange,
}) => (
<aside className={`
fixed top-0 right-0 h-screen w-64 bg-white shadow-lg
transition-transform duration-200 ease-in-out z-50
md:sticky md:h-[calc(100vh-4rem)]
${showSidebar ? 'translate-x-0' : 'translate-x-full md:translate-x-0'}
`}>
<div className="p-4 space-y-4 overflow-y-auto h-full">
<Select value={activeLlm} onValueChange={onLlmChange}>
<SelectTrigger>
<SelectValue placeholder="Select LLM" />
</SelectTrigger>
<SelectContent>
{Object.keys(llms).map(llm => (
<SelectItem key={llm} value={llm}>
{llm} ({llms[llm]})
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={onNextLlm} className="w-full">
Next LLM
</Button>
<Button variant="outline" onClick={onToggleDiff} className="w-full">
{showDiff ? 'Hide Diff' : 'Show Diff'}
</Button>
<div className="flex items-center justify-between">
<label>Auto-Scroll</label>
<Switch
checked={autoScroll}
onCheckedChange={onAutoScrollChange}
/>
</div>
{llmState === LlmState.INFERENCE ? (
<Button
onClick={onStop}
className="w-full"
>
Stop
</Button>
) : (
<Button
onClick={onInference}
className="w-full"
>
Start Inference
</Button>
)}
<Button
onClick={onApprove}
className="w-full"
>
Approve Response
</Button>
<Button onClick={onSendInput} disabled={!input.trim()} className="w-full">
Send Input
</Button>
<Button onClick={onClearOutput} disabled={!output.trim()} className="w-full">
Clear Output
</Button>
{/* Auto Approver Config */}
<div className="border-t pt-4">
<h3 className="font-medium mb-2">Auto Approver</h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label>Context Auto-Approve</label>
<Switch
checked={autoApproverConfig.context_enabled}
onCheckedChange={(enabled) => onAutoApproverConfigChange({
...autoApproverConfig,
context_enabled: enabled
})}
/>
</div>
<div className="flex items-center justify-between">
<label>Response Auto-Approve</label>
<Switch
checked={autoApproverConfig.response_enabled}
onCheckedChange={(enabled) => onAutoApproverConfigChange({
...autoApproverConfig,
response_enabled: enabled
})}
/>
</div>
<div className="space-y-1">
<label className="text-sm">Context Timeout (s)</label>
<Input
type="number"
min="0"
step="0.1"
value={autoApproverConfig.context_timeout}
onChange={(e) => onAutoApproverConfigChange({
...autoApproverConfig,
context_timeout: parseFloat(e.target.value)
})}
/>
</div>
<div className="space-y-1">
<label className="text-sm">Response Timeout (s)</label>
<Input
type="number"
min="0"
step="0.1"
value={autoApproverConfig.response_timeout}
onChange={(e) => onAutoApproverConfigChange({
...autoApproverConfig,
response_timeout: parseFloat(e.target.value)
})}
/>
</div>
<div className="space-y-1">
<label className="text-sm">LLM for Inference</label>
<Select
value={autoApproverConfig.llm_name}
onValueChange={(llm) => onAutoApproverConfigChange({
...autoApproverConfig,
llm_name: llm
})}
>
<SelectTrigger>
<SelectValue placeholder="Select LLM" />
</SelectTrigger>
<SelectContent>
{Object.keys(llms).map(llm => (
<SelectItem key={llm} value={llm}>{llm}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-gray-500 mt-1">
This selects which model will generate content during auto-approval.
</p>
</div>
</div>
</div>
</div>
</aside>
);

View File

@@ -1,124 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Script</label>
<textarea
value={formData.script || ''}
onChange={e => setFormData({...formData, script: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
</>
);
const EditForm = ({ formData, setFormData }) => (
<>
<CreateForm formData={formData} setFormData={setFormData} />
<div>
<label className="block text-sm font-medium text-gray-700">Standard Output</label>
<textarea
value={formData.stdout || ''}
onChange={e => setFormData({...formData, stdout: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Standard Error</label>
<textarea
value={formData.stderr || ''}
onChange={e => setFormData({...formData, stderr: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.exit_code !== null}
onChange={e => setFormData({
...formData,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={formData.exit_code ?? ''}
onChange={e => setFormData({...formData, exit_code: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.exit_code === null}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.pid !== null}
onChange={e => setFormData({
...formData,
pid: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Process ID</label>
</div>
<input
type="number"
value={formData.pid ?? ''}
onChange={e => setFormData({...formData, pid: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.pid === null}
/>
</div>
</>
);
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.script || ''}</pre>
</div>
{formData.pid !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Process ID:</span>
<span className="ml-2 text-sm text-gray-900">{formData.pid}</span>
</div>
)}
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stdout || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stderr || ''}</pre>
</div>
{formData.exit_code !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{formData.exit_code}</span>
</div>
)}
</>
);
export const BackgroundEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,117 +0,0 @@
import React, { useEffect, useState } from 'react';
export const BaseEntryEditor = ({
entry,
isNew = false,
onSave,
onCancel,
onDelete,
onReset,
onUpdate,
children
}) => {
const [isEditing, setIsEditing] = useState(isNew);
const [formData, setFormData] = useState(entry);
useEffect(() => {
if (!isEditing) {
setFormData(entry);
}
}, [entry]);
const handleSave = () => {
const id = isNew ? formData.id : entry.id;
onSave(id, formData);
setIsEditing(false);
};
const handleEdit = () => {
setIsEditing(true);
};
const handleCancel = () => {
setFormData(entry);
setIsEditing(false);
if (onCancel) onCancel();
};
return (
<div className="border rounded-lg shadow-sm bg-white p-4 mb-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">{formData.type}</h2>
{isEditing ? (
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">ID (Timestamp)</label>
<input
value={formData.id}
onChange={e => setFormData({...formData, id: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
{/* Child components inject their specific form fields here */}
{children({ formData, setFormData, isEditing, isNew })}
<div className="flex justify-end space-x-2">
<button
type="button"
onClick={handleCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
type="button"
onClick={handleSave}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
>
{isNew ? 'Create' : 'Save'}
</button>
</div>
</form>
) : (
<div className="space-y-4">
<div>
<span className="text-sm font-medium text-gray-700">ID:</span>
<span className="ml-2 text-sm text-gray-900">{entry.id}</span>
</div>
{/* Child components inject their specific read-only fields here */}
{children({ formData, setFormData, isEditing, isNew })}
<div className="flex justify-end space-x-2">
{onReset && (
<button
onClick={() => onReset(entry.id)}
className="px-4 py-2 text-sm font-medium text-green-600 bg-white border border-green-600 rounded-md hover:bg-green-50"
>
Reset
</button>
)}
{onUpdate && (
<button
onClick={() => onUpdate(entry.id)}
className="px-4 py-2 text-sm font-medium text-green-600 bg-white border border-green-600 rounded-md hover:bg-green-50"
>
Update
</button>
)}
<button
onClick={handleEdit}
className="px-4 py-2 text-sm font-medium text-blue-600 bg-white border border-blue-300 rounded-md hover:bg-blue-50"
>
Edit
</button>
<button
onClick={() => onDelete(entry.id)}
className="px-4 py-2 text-sm font-medium text-red-600 bg-white border border-red-300 rounded-md hover:bg-red-50"
>
Delete
</button>
</div>
</div>
)}
</div>
);
};

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { DiffEditorWrapper } from './DiffEditorWrapper';
import { StandardEditor } from './StandardEditor';
export const ContentEditor = ({
showDiff,
originalContent,
modifiedContent,
onChange,
validationError = null,
language = 'xml',
autoScroll = false,
}) => {
if (showDiff) {
return (
<DiffEditorWrapper
originalContent={originalContent}
modifiedContent={modifiedContent}
onChange={value => {
onChange(value);
}}
language={language}
validationError={validationError}
/>
);
}
return (
<StandardEditor
content={modifiedContent}
onChange={onChange}
language={language}
validationError={validationError}
autoScroll={autoScroll}
/>
);
};

View File

@@ -1,60 +0,0 @@
import React from 'react';
import { DiffEditor } from '@monaco-editor/react';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
export const DiffEditorWrapper = ({
originalContent,
modifiedContent,
onChange,
language = 'xml',
validationError = null,
}) => {
const handleMount = (editor, monaco) => {
const modified = editor.getModifiedEditor();
modified.onDidChangeModelContent(() => {
if (onChange) {
onChange(modified.getValue());
}
});
};
return (
<Card className="h-[calc(100vh-8rem)]">
<CardContent className="p-0 h-full">
{validationError && (
<Alert variant="destructive" className="m-4">
<AlertDescription>{validationError}</AlertDescription>
</Alert>
)}
<DiffEditor
height="100%"
original={originalContent}
modified={modifiedContent}
language={language}
onChange={onChange}
onMount={handleMount}
options={{
readOnly: false,
originalEditable: false,
modifiedEditable: true,
minimap: { enabled: true },
lineNumbers: 'on',
fontSize: 14,
renderSideBySide: true,
automaticLayout: true,
diffAlgorithm: 'advanced',
diffWordWrap: 'off',
originalEditor: {
readOnly: true,
wordWrap: 'on',
},
modifiedEditor: {
readOnly: false,
wordWrap: 'on',
}
}}
/>
</CardContent>
</Card>
)
};

View File

@@ -1,217 +0,0 @@
import React, { useState } from 'react';
import { BackgroundEntryEditor } from './BackgroundEntryEditor';
import { ParseErrorEntryEditor } from './ParseErrorEntryEditor';
import { ReadEntryEditor } from './ReadEntryEditor';
import { ReasoningEntryEditor } from './ReasoningEntryEditor';
import { RepeatEntryEditor } from './RepeatEntryEditor';
import { SingleEntryEditor } from './SingleEntryEditor';
import { WriteEntryEditor } from './WriteEntryEditor';
const EntryTypes = {
BACKGROUND: 'background',
PARSE_ERROR: 'parse_error',
READ_STDIN: 'read_stdin',
REASONING: 'reasoning',
REPEAT: 'repeat',
SINGLE: 'single',
WRITE: 'write'
};
const EntryEditors = {
[EntryTypes.BACKGROUND]: BackgroundEntryEditor,
[EntryTypes.PARSE_ERROR]: ParseErrorEntryEditor,
[EntryTypes.READ_STDIN]: ReadEntryEditor,
[EntryTypes.REASONING]: ReasoningEntryEditor,
[EntryTypes.REPEAT]: RepeatEntryEditor,
[EntryTypes.SINGLE]: SingleEntryEditor,
[EntryTypes.WRITE]: WriteEntryEditor
};
export const MemoryEditor = ({
entries = [],
onCreateEntry,
onSaveEntry,
onDeleteEntry,
onResetEntry,
onUpdateEntry
}) => {
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [selectedType, setSelectedType] = useState(EntryTypes.SINGLE);
const handleCreate = (_id, entry) => {
onCreateEntry(entry);
setShowCreateDialog(false);
};
const handleLoadIteration = async (event) => {
const file = event.target.files[0];
if (file) {
try {
const content = await file.text();
const response = await fetch('/api/memory/load_iteration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content })
});
if (!response.ok) throw new Error('Failed to load iteration');
} catch (error) {
if (error instanceof DOMException) {
alert('Failed to read the file. Check permissions. chown -R $USER:$USER iteration');
} else {
throw error;
}
}
}
};
const handleLoadIterationClick = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xml';
input.onchange = handleLoadIteration;
input.click();
};
const generateId = () => {
const now = new Date();
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
return utc.getFullYear() +
String(utc.getMonth() + 1).padStart(2, '0') +
String(utc.getDate()).padStart(2, '0') + '_' +
String(utc.getHours()).padStart(2, '0') +
String(utc.getMinutes()).padStart(2, '0') +
String(utc.getSeconds()).padStart(2, '0') + '_' +
String(utc.getMilliseconds()).padStart(3, '0');
};
const getInitialEntry = (type) => {
const baseEntry = {
type: type,
id: generateId()
};
switch (type) {
case EntryTypes.BACKGROUND:
case EntryTypes.REPEAT:
case EntryTypes.SINGLE:
return {
...baseEntry,
script: '',
timeout: null,
limit: null
};
case EntryTypes.PARSE_ERROR:
return {
...baseEntry,
content: '',
error: ''
};
case EntryTypes.READ_STDIN:
return {
...baseEntry,
content: '',
read: false
};
case EntryTypes.REASONING:
return {
...baseEntry,
content: ''
};
case EntryTypes.WRITE:
return {
...baseEntry,
content: '',
written: false
};
default:
return baseEntry;
}
};
const renderCreateDialog = () => {
const EditorComponent = EntryEditors[selectedType];
const entry = getInitialEntry(selectedType);
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[100]">
<div className="bg-white p-4 rounded-lg max-w-2xl w-full m-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium">Create New Entry</h2>
<button
onClick={() => setShowCreateDialog(false)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Entry Type</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
>
{Object.entries(EntryTypes).map(([key, value]) => (
<option key={value} value={value}>
{key.toLowerCase().replace('_', ' ')}
</option>
))}
</select>
</div>
<EditorComponent
entry={entry}
isNew={true}
onSave={handleCreate}
onCancel={() => setShowCreateDialog(false)}
/>
</div>
</div>
);
};
const renderEntry = (entry) => {
const EditorComponent = EntryEditors[entry.type];
if (!EditorComponent) {
console.warn(`Unknown entry type: ${entry.type}`);
return null;
}
return (
<EditorComponent
key={entry.id}
entry={entry}
onSave={onSaveEntry}
onDelete={onDeleteEntry}
onReset={onResetEntry}
onUpdate={onUpdateEntry}
/>
);
};
return (
<div className="p-4">
<div className="flex justify-end mb-4 gap-2">
<button
onClick={handleLoadIterationClick}
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
>
Load Iteration
</button>
<button
onClick={() => setShowCreateDialog(true)}
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
>
Add Entry
</button>
</div>
<div className="space-y-4">
{entries.map(renderEntry)}
</div>
{showCreateDialog && renderCreateDialog()}
</div>
);
};
export default MemoryEditor;

View File

@@ -1,53 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Content</label>
<textarea
value={formData.content || ''}
onChange={e => setFormData({...formData, content: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Error</label>
<textarea
value={formData.error || ''}
onChange={e => setFormData({...formData, error: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
</>
);
const EditForm = CreateForm;
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.content || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-red-700">Error:</span>
<pre className="mt-1 text-sm text-red-900 bg-red-50 p-2 rounded">{formData.error || ''}</pre>
</div>
</>
);
export const ParseErrorEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,54 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Content</label>
<textarea
value={formData.content || ''}
onChange={e => setFormData({...formData, content: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={formData.read || false}
onChange={e => setFormData({...formData, read: e.target.checked})}
className="rounded border-gray-300"
/>
<span className="ml-2 text-sm text-gray-700">Read</span>
</div>
</>
);
const EditForm = CreateForm;
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.content || ''}</pre>
</div>
<div>
<span className="text-sm text-gray-700">
{formData.read ? '✓ Read' : '✗ Not Read'}
</span>
</div>
</>
);
export const ReadEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,40 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Content</label>
<textarea
value={formData.content || ''}
onChange={e => setFormData({...formData, content: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={6}
/>
</div>
</>
);
const EditForm = CreateForm;
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Reasoning:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{formData.content || ''}</pre>
</div>
</>
);
export const ReasoningEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,164 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Script</label>
<textarea
value={formData.script || ''}
onChange={e => setFormData({...formData, script: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.timeout !== null}
onChange={e => setFormData({
...formData,
timeout: e.target.checked ? 1 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Timeout (seconds)</label>
</div>
<input
type="number"
value={formData.timeout || ''}
onChange={e => setFormData({...formData, timeout: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
step="0.1"
disabled={formData.timeout === null}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.limit !== null}
onChange={e => setFormData({
...formData,
limit: e.target.checked ? 1024 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Memory Limit (bytes)</label>
</div>
<input
type="number"
value={formData.limit || ''}
onChange={e => setFormData({...formData, limit: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.limit === null}
/>
</div>
</>
);
const EditForm = ({ formData, setFormData }) => (
<>
<CreateForm formData={formData} setFormData={setFormData} />
<div>
<label className="block text-sm font-medium text-gray-700">Standard Output</label>
<textarea
value={formData.stdout || ''}
onChange={e => setFormData({...formData, stdout: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Standard Error</label>
<textarea
value={formData.stderr || ''}
onChange={e => setFormData({...formData, stderr: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.exit_code !== null}
onChange={e => setFormData({
...formData,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={formData.exit_code ?? ''}
onChange={e => setFormData({...formData, exit_code: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.exit_code === null}
/>
</div>
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.timed_out || false}
onChange={e => setFormData({...formData, timed_out: e.target.checked})}
className="rounded border-gray-300"
/>
<span className="ml-2 text-sm text-gray-700">Timed Out</span>
</label>
</div>
</>
);
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.script || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Timeout:</span>
<span className="ml-2 text-sm text-gray-900">{formData.timeout || 'None'}</span>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Memory Limit:</span>
<span className="ml-2 text-sm text-gray-900">{formData.limit || 'None'}</span>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stdout || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stderr || ''}</pre>
</div>
{formData.exit_code !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{formData.exit_code}</span>
</div>
)}
<div>
<span className="text-sm text-gray-700">
{formData.timed_out ? '⚠ Timed Out' : '✓ Within Timeout'}
</span>
</div>
</>
);
export const RepeatEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,176 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Script</label>
<textarea
value={formData.script || ''}
onChange={e => setFormData({...formData, script: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.timeout !== null}
onChange={e => setFormData({
...formData,
timeout: e.target.checked ? 1 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Timeout (seconds)</label>
</div>
<input
type="number"
value={formData.timeout || ''}
onChange={e => setFormData({...formData, timeout: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
step="0.1"
disabled={formData.timeout === null}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.limit !== null}
onChange={e => setFormData({
...formData,
limit: e.target.checked ? 1024 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Custom Memory Limit (bytes)</label>
</div>
<input
type="number"
value={formData.limit || ''}
onChange={e => setFormData({...formData, limit: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.limit === null}
/>
</div>
</>
);
const EditForm = ({ formData, setFormData }) => (
<>
<CreateForm formData={formData} setFormData={setFormData} />
<div>
<label className="block text-sm font-medium text-gray-700">Standard Output</label>
<textarea
value={formData.stdout || ''}
onChange={e => setFormData({...formData, stdout: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Standard Error</label>
<textarea
value={formData.stderr || ''}
onChange={e => setFormData({...formData, stderr: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={4}
/>
</div>
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={formData.exit_code !== null}
onChange={e => setFormData({
...formData,
exit_code: e.target.checked ? 0 : null
})}
className="rounded border-gray-300"
/>
<label className="ml-2 text-sm font-medium text-gray-700">Exit Code</label>
</div>
<input
type="number"
value={formData.exit_code ?? ''}
onChange={e => setFormData({...formData, exit_code: e.target.value ? Number(e.target.value) : null})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm disabled:bg-gray-100"
disabled={formData.exit_code === null}
/>
</div>
<div className="flex space-x-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.executed || false}
onChange={e => setFormData({...formData, executed: e.target.checked})}
className="rounded border-gray-300"
/>
<span className="ml-2 text-sm text-gray-700">Executed</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.timed_out || false}
onChange={e => setFormData({...formData, timed_out: e.target.checked})}
className="rounded border-gray-300"
/>
<span className="ml-2 text-sm text-gray-700">Timed Out</span>
</label>
</div>
</>
);
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Script:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.script || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Timeout:</span>
<span className="ml-2 text-sm text-gray-900">{formData.timeout || 'None'}</span>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Memory Limit:</span>
<span className="ml-2 text-sm text-gray-900">{formData.limit || 'None'}</span>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Standard Output:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stdout || ''}</pre>
</div>
<div>
<span className="text-sm font-medium text-gray-700">Standard Error:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded">{formData.stderr || ''}</pre>
</div>
{formData.exit_code !== null && (
<div>
<span className="text-sm font-medium text-gray-700">Exit Code:</span>
<span className="ml-2 text-sm text-gray-900">{formData.exit_code}</span>
</div>
)}
<div className="flex space-x-4">
<span className="text-sm text-gray-700">
{formData.executed ? '✓ Executed' : '✗ Not Executed'}
</span>
<span className="text-sm text-gray-700">
{formData.timed_out ? '⚠ Timed Out' : '✓ Within Timeout'}
</span>
</div>
</>
);
export const SingleEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -1,61 +0,0 @@
import React from 'react';
import Editor from '@monaco-editor/react';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
export const StandardEditor = ({
content,
onChange,
readOnly = false,
language = 'xml',
validationError = null,
autoScroll = false,
}) => {
const editorRef = React.useRef(null);
const scrollToBottom = () => {
if (editorRef.current && autoScroll) {
const model = editorRef.current.getModel();
const lineCount = model.getLineCount();
editorRef.current.revealLine(lineCount, monaco.editor.ScrollType.Smooth);
}
};
const handleEditorDidMount = (editor) => {
editorRef.current = editor;
setTimeout(() => {
scrollToBottom();
}, 10);
};
React.useEffect(() => {
scrollToBottom();
}, [content, autoScroll]);
return (
<Card className="h-[calc(100vh-8rem)]">
<CardContent className="p-0 h-full">
{validationError && (
<Alert variant="destructive" className="m-4">
<AlertDescription>{validationError}</AlertDescription>
</Alert>
)}
<Editor
height="100%"
language={language}
value={content}
onChange={onChange}
onMount={handleEditorDidMount}
options={{
minimap: { enabled: false },
lineNumbers: 'on',
readOnly,
fontSize: 14,
automaticLayout: true,
wordWrap: 'on',
}}
/>
</CardContent>
</Card>
);
};

View File

@@ -1,54 +0,0 @@
import { BaseEntryEditor } from './BaseEntryEditor';
const CreateForm = ({ formData, setFormData }) => (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Content</label>
<textarea
value={formData.content || ''}
onChange={e => setFormData({...formData, content: e.target.value})}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
rows={6}
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={formData.written || false}
onChange={e => setFormData({...formData, written: e.target.checked})}
className="rounded border-gray-300"
/>
<span className="ml-2 text-sm text-gray-700">Written</span>
</div>
</>
);
const EditForm = CreateForm;
const ViewMode = ({ formData }) => (
<>
<div>
<span className="text-sm font-medium text-gray-700">Content:</span>
<pre className="mt-1 text-sm text-gray-900 bg-gray-50 p-2 rounded whitespace-pre-wrap">{formData.content || ''}</pre>
</div>
<div>
<span className="text-sm text-gray-700">
{formData.written ? '✓ Written' : '✗ Not Written'}
</span>
</div>
</>
);
export const WriteEntryEditor = (props) => (
<BaseEntryEditor {...props}>
{({ formData, setFormData, isEditing, isNew }) => {
if (isNew) {
return <CreateForm formData={formData} setFormData={setFormData} />;
}
if (isEditing) {
return <EditForm formData={formData} setFormData={setFormData} />;
}
return <ViewMode formData={formData} />;
}}
</BaseEntryEditor>
);

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
interface ButtonProps {
icon?: LucideIcon;
children: React.ReactNode;
variant?: 'primary' | 'danger' | 'success' | 'secondary';
size?: 'sm' | 'md';
onClick?: () => void;
className?: string;
iconOnly?: boolean;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
icon: Icon,
children,
variant = 'primary',
size = 'md',
onClick,
className = '',
iconOnly = false,
disabled = false
}) => {
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
danger: 'bg-red-600 text-white hover:bg-red-700',
success: 'bg-green-600 text-white hover:bg-green-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-700'
};
const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : '';
const sizeClasses = {
sm: 'px-2 py-1 text-xs',
md: 'px-3 py-2 text-sm'
};
const iconSizeClasses = {
sm: 'w-3 h-3',
md: 'w-4 h-4'
};
return (
<button
onClick={onClick}
disabled={disabled}
className={`flex items-center justify-center rounded transition-colors ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClasses} ${className}`}
>
{Icon && (
<Icon className={`${iconSizeClasses[size]} ${!iconOnly ? 'mr-1' : ''} flex-shrink-0`} />
)}
{!iconOnly && <span className="flex-1 text-center">{children}</span>}
</button>
);
};
export default Button;

View File

@@ -0,0 +1 @@
export { default as Button } from './Button';

View File

@@ -1,29 +0,0 @@
import * as React from "react"
const Alert = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
const variants = {
default: "bg-background text-foreground",
destructive: "bg-destructive text-destructive-foreground"
}
return (
<div
ref={ref}
role="alert"
className={`relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground ${variants[variant]} ${className}`}
{...props}
/>
)
})
Alert.displayName = "Alert"
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`text-sm [&_p]:leading-relaxed ${className}`}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertDescription }

View File

@@ -1,36 +0,0 @@
import * as React from "react"
const Button = React.forwardRef(({
className = "",
variant = "default",
size = "default",
disabled = false,
...props
}, ref) => {
const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
const variants = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
}
const sizes = {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
ref={ref}
disabled={disabled}
{...props}
/>
)
})
Button.displayName = "Button"
export { Button }

View File

@@ -1,35 +0,0 @@
import * as React from "react"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`rounded-lg border bg-card text-card-foreground shadow-sm ${className}`}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`flex flex-col space-y-1.5 p-6 ${className}`}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={`text-2xl font-semibold leading-none tracking-tight ${className}`}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={`p-6 pt-0 ${className}`} {...props} />
))
CardContent.displayName = "CardContent"
export { Card, CardHeader, CardTitle, CardContent }

View File

@@ -1,15 +0,0 @@
import * as React from "react"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }

View File

@@ -1,29 +0,0 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={`relative overflow-hidden ${className}`}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.ScrollAreaScrollbar
className="flex touch-none select-none transition-colors"
orientation="vertical"
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
<ScrollAreaPrimitive.ScrollAreaScrollbar
className="flex h-2.5 touch-none select-none transition-colors"
orientation="horizontal"
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
export { ScrollArea }

View File

@@ -1,52 +0,0 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown } from "lucide-react"
const Select = SelectPrimitive.Root
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = "SelectTrigger"
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className="relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-background text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
position={position}
{...props}>
<SelectPrimitive.Viewport className="p-1">
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = "SelectContent"
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = "SelectItem"
const SelectValue = SelectPrimitive.Value
export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue }

View File

@@ -1,21 +0,0 @@
import React from 'react';
export const Switch = ({ checked, onCheckedChange, disabled }) => (
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
disabled={disabled}
/>
<div className={`
w-11 h-6 bg-gray-200 rounded-full peer
peer-checked:after:translate-x-full peer-checked:bg-blue-600
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
after:bg-white after:rounded-full after:h-5 after:w-5
after:transition-all
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}></div>
</label>
);

View File

@@ -1,33 +0,0 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={`inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground ${className}`}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={`inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm ${className}`}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={`mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${className}`}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,14 +0,0 @@
import * as React from "react"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
<textarea
className={`flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -0,0 +1,41 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import * as monaco from 'monaco-editor';
interface MonacoContextType {
monaco: typeof monaco | null;
isMonacoLoading: boolean;
}
const MonacoContext = createContext<MonacoContextType>({
monaco: null,
isMonacoLoading: true
});
export const useMonaco = () => useContext(MonacoContext);
export const MonacoProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
const [monacoInstance, setMonacoInstance] = useState<typeof monaco | null>(null);
const [isMonacoLoading, setIsMonacoLoading] = useState(true);
useEffect(() => {
if (!monacoInstance) {
import('monaco-editor').then(monaco => {
if (!monaco.languages.getLanguages().some(lang => lang.id === 'xml')) {
monaco.languages.register({ id: 'xml' });
}
setMonacoInstance(monaco);
setIsMonacoLoading(false);
}).catch(error => {
console.error('Failed to load Monaco editor:', error);
setIsMonacoLoading(false);
});
}
}, [monacoInstance]);
return (
<MonacoContext.Provider value={{ monaco: monacoInstance, isMonacoLoading }}>
{children}
</MonacoContext.Provider>
);
};

View File

@@ -0,0 +1,243 @@
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import { AutoApproverConfig, ChatMessage } from '../types';
interface WebSocketContextType {
state: {
agentState: 'IDLE' | 'INFERENCE' | 'PROCESSING_RESPONSE';
activeLLM: string;
};
response: string;
setResponse: (response: string) => void;
memory: any[];
messages: ChatMessage[];
lastReadTimestamp: string | null;
autoApprover: AutoApproverConfig;
setAutoApproverConfig: (config: Partial<AutoApproverConfig>) => Promise<void>;
isConnected: boolean;
llms: {name: string}[];
setActiveLLM: (llm: string) => Promise<void>;
connectionStatus: Record<string, string>;
}
const defaultState: WebSocketContextType = {
state: {
agentState: 'IDLE',
activeLLM: '',
},
response: '',
setResponse: () => {},
memory: [],
messages: [],
lastReadTimestamp: null,
autoApprover: {
context_enabled: false,
response_enabled: false,
context_timeout: 1.0,
response_timeout: 1.0
},
setAutoApproverConfig: async () => {},
isConnected: false,
llms: [],
setActiveLLM: async () => {},
connectionStatus: {},
};
const WebSocketContext = createContext<WebSocketContextType>(defaultState);
export const useWebSocketContext = () => useContext(WebSocketContext);
export const WebSocketProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
const [isConnected, setIsConnected] = useState(false);
const [state, setState] = useState(defaultState.state);
const [response, setResponseState] = useState('');
const [memory, setMemory] = useState<any[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [lastReadTimestamp, setLastReadTimestamp] = useState<string | null>(null);
const [autoApprover, setAutoApprover] = useState<AutoApproverConfig>(defaultState.autoApprover);
const [llms, setLLMs] = useState<{name: string}[]>([]);
const [connectionStatus, setConnectionStatus] = useState<Record<string, string>>({});
const sockets = useRef<{[key: string]: WebSocket | null}>({});
const setResponse = async (newResponse: string) => {
setResponseState(newResponse);
await fetch('/api/response', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: newResponse })
});
};
const setAutoApproverConfig = async (updates: Partial<AutoApproverConfig>) => {
const newConfig = { ...autoApprover, ...updates };
await fetch('/api/auto_approver', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig)
});
setAutoApprover(newConfig);
};
const setActiveLLM = async (llm: string) => {
await fetch('/api/llms/active', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active_llm: llm })
});
setState(prev => ({ ...prev, activeLLM: llm }));
};
useEffect(() => {
const loadInitialData = async () => {
const llmsResponse = await fetch('/api/llms');
if (llmsResponse.ok) {
const llmsData = await llmsResponse.json();
setLLMs(llmsData);
}
};
loadInitialData();
}, []);
useEffect(() => {
const endpoints = ['ws/state', 'ws/response', 'ws/memory', 'ws/auto_approver', 'ws/chat'];
const newConnectionStatus: Record<string, string> = {};
const createWebSocket = (endpoint: string) => {
if (
sockets.current[endpoint] &&
(sockets.current[endpoint]?.readyState === WebSocket.CONNECTING ||
sockets.current[endpoint]?.readyState === WebSocket.OPEN)
) {
return;
}
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const fullUrl = `${protocol}//${host}/${endpoint}`;
console.log(`Connecting to WebSocket: ${fullUrl}`);
newConnectionStatus[endpoint] = 'connecting';
const socket = new WebSocket(fullUrl);
sockets.current[endpoint] = socket;
socket.onopen = () => {
console.log(`WebSocket connected: ${endpoint}`);
newConnectionStatus[endpoint] = 'connected';
setConnectionStatus({...newConnectionStatus});
setIsConnected(true);
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (endpoint === 'ws/state') {
setState({
agentState: data.state,
activeLLM: data.active_llm
});
} else if (endpoint === 'ws/response') {
if (data.response !== undefined) {
setResponseState(data.response);
}
} else if (endpoint === 'ws/memory') {
if (data.entries) {
setMemory(data.entries);
}
} else if (endpoint === 'ws/auto_approver') {
if (data.config) {
setAutoApprover(data.config);
}
} else if (endpoint === 'ws/chat') {
if (data.messages) {
setMessages(prevMessages => {
const updatedIds = data.messages.map((m: ChatMessage) => m.id);
const filteredMessages = prevMessages.filter(m => !updatedIds.includes(m.id));
return [...filteredMessages, ...data.messages];
});
}
if (data.deleted_message_ids) {
setMessages(prevMessages =>
prevMessages.filter(m => !data.deleted_message_ids.includes(m.id))
);
}
if (data.last_read_timestamp) {
setLastReadTimestamp(data.last_read_timestamp);
}
}
} catch (error) {
console.error(`Failed to parse message from ${endpoint}:`, error);
}
};
socket.onclose = (event) => {
console.log(`WebSocket closed: ${endpoint} (code: ${event.code})`);
newConnectionStatus[endpoint] = 'disconnected';
setConnectionStatus({...newConnectionStatus});
sockets.current[endpoint] = null;
if (event.code === 1000 || event.code === 1001) {
return;
}
setTimeout(() => createWebSocket(endpoint), 3000);
};
socket.onerror = (error) => {
console.error(`WebSocket error: ${endpoint}`, error);
newConnectionStatus[endpoint] = 'error';
setConnectionStatus({...newConnectionStatus});
};
} catch (error) {
console.error(`Failed to create WebSocket: ${endpoint}`, error);
newConnectionStatus[endpoint] = 'error';
setConnectionStatus({...newConnectionStatus});
setTimeout(() => createWebSocket(endpoint), 3000);
}
};
endpoints.forEach(endpoint => {
createWebSocket(endpoint);
});
return () => {
Object.entries(sockets.current).forEach(([endpoint, socket]) => {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
console.log(`Closing WebSocket: ${endpoint}`);
socket.close(1000, 'Component unmounting');
}
});
sockets.current = {};
};
}, []);
const value = {
state,
response,
setResponse,
memory,
messages,
lastReadTimestamp,
autoApprover,
setAutoApproverConfig,
isConnected,
llms,
setActiveLLM,
connectionStatus
};
return (
<WebSocketContext.Provider value={value}>
{children}
</WebSocketContext.Provider>
);
};

View File

@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
const useScreenSize = () => {
const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
useEffect(() => {
const handleResize = () => {
const width = window.innerWidth;
setScreenSize(
width < 640 ? 'mobile' :
width < 768 ? 'tablet' :
'desktop'
);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return screenSize;
};
export default useScreenSize;

View File

@@ -1,68 +0,0 @@
import { useState, useEffect, useRef, useCallback } from 'react';
export const WebSocketState = {
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
};
export const useWebSocket = (url) => {
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
const wsRef = useRef(null);
const handlersRef = useRef([]);
const connect = useCallback(() => {
setWsState(WebSocketState.CONNECTING);
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
setWsState(WebSocketState.CONNECTED);
};
ws.onclose = () => {
setWsState(WebSocketState.DISCONNECTED);
setTimeout(connect, 2000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handlersRef.current.forEach(handler => handler(message));
} catch (error) {
console.error('Error handling WebSocket message:', error);
}
};
return ws;
}, [url]);
useEffect(() => {
const ws = connect();
return () => {
if (ws) ws.close();
};
}, [connect]);
const sendMessage = useCallback((data) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(data));
} else {
console.error('WebSocket not connected, state:', wsRef.current?.readyState);
}
}, []);
const addMessageHandler = useCallback((handler) => {
handlersRef.current.push(handler);
}, []);
return {
wsState,
sendMessage,
addMessageHandler
};
};

View File

@@ -1,46 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@tailwind utilities;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './components/App';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>

254
web/src/services/api.ts Normal file
View File

@@ -0,0 +1,254 @@
import { AutoApproverConfig } from '../types';
import {
EntryData,
ReasoningEntryData,
ScriptEntryData,
IOEntryData,
ParseErrorEntryData,
} from '../types/entries';
const API_BASE = '/api';
const generateId = (): string => {
const now = new Date();
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
return utc.getFullYear() +
String(utc.getMonth() + 1).padStart(2, '0') +
String(utc.getDate()).padStart(2, '0') + '_' +
String(utc.getHours()).padStart(2, '0') +
String(utc.getMinutes()).padStart(2, '0') +
String(utc.getSeconds()).padStart(2, '0') + '_' +
String(utc.getMilliseconds()).padStart(3, '0');
};
export const EntryTypes = {
BACKGROUND: 'background',
PARSE_ERROR: 'parse_error',
READ_STDIN: 'read_stdin',
REASONING: 'reasoning',
REPEAT: 'repeat',
SINGLE: 'single',
WRITE: 'write'
} as const;
export type EntryTypeValue = typeof EntryTypes[keyof typeof EntryTypes];
export const getInitialEntryData = (type: EntryTypeValue): Partial<EntryData> => {
const baseEntry = {
type,
id: generateId()
};
switch (type) {
case EntryTypes.BACKGROUND:
case EntryTypes.REPEAT:
case EntryTypes.SINGLE:
return {
...baseEntry,
script: '',
} as Partial<ScriptEntryData>;
case EntryTypes.PARSE_ERROR:
return {
...baseEntry,
content: '',
error: ''
} as Partial<ParseErrorEntryData>;
case EntryTypes.READ_STDIN:
return {
...baseEntry,
} as Partial<IOEntryData>;
case EntryTypes.REASONING:
return {
...baseEntry,
content: ''
} as Partial<ReasoningEntryData>;
case EntryTypes.WRITE:
return {
...baseEntry,
content: '',
} as Partial<IOEntryData>;
default:
return baseEntry;
}
};
interface LLMResponse {
name: string;
}
interface ActiveLLMResponse {
active_llm: string;
}
interface ResponseData {
response: string;
}
interface MemoryResponse {
entries: EntryData[];
}
export const api = {
// LLMs
getLLMs: async (): Promise<LLMResponse[]> => {
const response = await fetch(`${API_BASE}/llms`);
if (!response.ok) throw new Error('Failed to get LLMs');
return response.json();
},
getActiveLLM: async (): Promise<ActiveLLMResponse> => {
const response = await fetch(`${API_BASE}/llms/active`);
if (!response.ok) throw new Error('Failed to get active LLM');
return response.json();
},
setActiveLLM: async (llm: string): Promise<void> => {
const response = await fetch(`${API_BASE}/llms/active`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active_llm: llm })
});
if (!response.ok) throw new Error('Failed to set active LLM');
},
startInference: async (): Promise<Response> => {
const response = await fetch(`${API_BASE}/inference`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to start inference');
return response;
},
stopInference: async (): Promise<Response> => {
const response = await fetch(`${API_BASE}/inference/stop`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to stop inference');
return response;
},
getResponse: async (): Promise<ResponseData> => {
const response = await fetch(`${API_BASE}/response`);
if (!response.ok) throw new Error('Failed to get response');
return response.json();
},
setResponse: async (responseText: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: responseText })
});
if (!response.ok) throw new Error('Failed to set response');
return response;
},
approveResponse: async (): Promise<Response> => {
const response = await fetch(`${API_BASE}/response/approve`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to approve response');
return response;
},
setAutoApproverConfig: async (config: Partial<AutoApproverConfig>): Promise<Response> => {
const response = await fetch(`${API_BASE}/auto_approver`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) throw new Error('Failed to set auto approver config');
return response;
},
getMemory: async (): Promise<MemoryResponse> => {
const response = await fetch(`${API_BASE}/memory`);
if (!response.ok) throw new Error('Failed to get memory');
return response.json();
},
createMemoryEntry: async (entryData: Partial<EntryData>): Promise<EntryData> => {
const data = {
...getInitialEntryData(entryData.type as EntryTypeValue),
...entryData
};
const response = await fetch(`${API_BASE}/memory/entry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const errorText = await response.text();
console.error('Server response:', errorText);
throw new Error('Failed to create entry');
}
return response.json();
},
loadIteration: async (content: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/memory/load_iteration`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
if (!response.ok) throw new Error('Failed to load iteration');
return response;
},
deleteMemoryEntry: async (id: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/memory/entry/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`Failed to delete entry ${id}`);
return response;
},
updateMemoryEntry: async (id: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/memory/entry/${id}/update`, {
method: 'POST'
});
if (!response.ok) throw new Error(`Failed to update entry ${id}`);
return response;
},
saveMemoryEntry: async <T extends EntryData>(id: string, data: T): Promise<Response> => {
const response = await fetch(`${API_BASE}/memory/entry/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`Failed to save entry ${id}`);
return response;
},
resetMemoryEntry: async (id: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/memory/entry/${id}/reset`, {
method: 'POST'
});
if (!response.ok) throw new Error(`Failed to reset entry ${id}`);
return response;
},
addChatMessage: async (data: { message: string }): Promise<Response> => {
const response = await fetch(`${API_BASE}/chat/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to add chat message');
return response;
},
deleteChatMessage: async (id: string): Promise<Response> => {
const response = await fetch(`${API_BASE}/chat/message/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`Failed to delete chat message ${id}`);
return response;
},
clearChat: async (): Promise<Response> => {
const response = await fetch(`${API_BASE}/chat`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to clear chat');
return response;
}
};

54
web/src/types/entries.ts Normal file
View File

@@ -0,0 +1,54 @@
// src/types/entries.ts
import { EntryTypes } from '../services/api';
export interface BaseEntryData {
id: string;
type: string;
timestamp?: number;
}
export interface ReasoningEntryData extends BaseEntryData {
type: typeof EntryTypes.REASONING;
content: string;
}
export interface ScriptEntryData extends BaseEntryData {
type: typeof EntryTypes.SINGLE | typeof EntryTypes.REPEAT;
script: string;
stdout?: string;
stderr?: string;
exit_code?: number | null;
timeout?: number | null;
limit?: number | null;
timed_out?: boolean;
executed?: boolean;
}
export interface IOEntryData extends BaseEntryData {
type: typeof EntryTypes.READ_STDIN | typeof EntryTypes.WRITE;
content: string;
read?: boolean;
written?: boolean;
}
export interface ParseErrorEntryData extends BaseEntryData {
type: typeof EntryTypes.PARSE_ERROR;
content: string;
error: string;
}
export interface BackgroundEntryData extends BaseEntryData {
type: typeof EntryTypes.BACKGROUND;
script: string;
stdout?: string;
stderr?: string;
exit_code?: number | null;
pid?: number | null;
}
export type EntryData =
| ReasoningEntryData
| ScriptEntryData
| IOEntryData
| ParseErrorEntryData
| BackgroundEntryData;

49
web/src/types/index.ts Normal file
View File

@@ -0,0 +1,49 @@
// src/types/index.ts
export interface AgentState {
connected: boolean;
state: 'IDLE' | 'INFERENCE' | 'PROCESSING_RESPONSE';
activeLLM: string;
}
export interface LLM {
name: string;
}
// Memory types
export interface MemoryEntry {
id: string;
type: EntryType;
timestamp: number;
}
export type EntryType =
| 'background'
| 'parse_error'
| 'read_stdin'
| 'reasoning'
| 'repeat'
| 'single'
| 'write';
// Chat types
export interface ChatMessage {
id: string;
content: string;
message_type: 'user' | 'sia'; // Changed from messageType to message_type
timestamp: number;
read?: boolean;
}
// WebSocket message types
export interface StateWebSocketMessage {
state: AgentState['state'];
active_llm: string;
}
// Auto Approver types
export interface AutoApproverConfig {
context_enabled: boolean;
response_enabled: boolean;
context_timeout: number;
response_timeout: number;
}

View File

@@ -1,69 +1,11 @@
import tailwindcssAnimate from "tailwindcss-animate";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./src/**/*.{js,jsx}"],
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
extend: {},
},
plugins: [tailwindcssAnimate],
plugins: [],
}

25
web/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
web/tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'components': path.resolve(__dirname, './src/components')
},
extensions: ['.js', '.jsx']
},
build: {
outDir: 'dist',
assetsDir: 'assets',
base: '/'
}
});

31
web/vite.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': 'http://localhost:8080',
'/ws': {
target: 'ws://localhost:8080',
ws: true
}
}
},
optimizeDeps: {
include: ['monaco-editor/esm/vs/language/json/json.worker'],
},
build: {
rollupOptions: {
output: {
manualChunks: {
jsonWorker: ['monaco-editor/esm/vs/language/json/json.worker'],
cssWorker: ['monaco-editor/esm/vs/language/css/css.worker'],
htmlWorker: ['monaco-editor/esm/vs/language/html/html.worker'],
editorWorker: ['monaco-editor/esm/vs/editor/editor.worker'],
}
}
}
}
})