New web interface, move llm engine to separate process
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
*.log
|
||||
5
web/.gitignore
vendored
5
web/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
.DS_Store
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
@@ -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>
|
||||
@@ -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
77
web/src/App.tsx
Normal 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;
|
||||
@@ -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;
|
||||
62
web/src/components/Chat/ChatInput.tsx
Normal file
62
web/src/components/Chat/ChatInput.tsx
Normal 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;
|
||||
49
web/src/components/Chat/ChatMessage.tsx
Normal file
49
web/src/components/Chat/ChatMessage.tsx
Normal 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;
|
||||
172
web/src/components/Chat/ChatWindow.tsx
Normal file
172
web/src/components/Chat/ChatWindow.tsx
Normal 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;
|
||||
4
web/src/components/Chat/index.ts
Normal file
4
web/src/components/Chat/index.ts
Normal 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';
|
||||
67
web/src/components/Controls/AgentSection.tsx
Normal file
67
web/src/components/Controls/AgentSection.tsx
Normal 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;
|
||||
102
web/src/components/Controls/AutoApproverSection.tsx
Normal file
102
web/src/components/Controls/AutoApproverSection.tsx
Normal 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;
|
||||
91
web/src/components/Controls/LLMSection.tsx
Normal file
91
web/src/components/Controls/LLMSection.tsx
Normal 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;
|
||||
126
web/src/components/Controls/MemorySection.tsx
Normal file
126
web/src/components/Controls/MemorySection.tsx
Normal 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;
|
||||
4
web/src/components/Controls/index.ts
Normal file
4
web/src/components/Controls/index.ts
Normal 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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
26
web/src/components/Layout/MobileControls.tsx
Normal file
26
web/src/components/Layout/MobileControls.tsx
Normal 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;
|
||||
53
web/src/components/Layout/MobileTaskbar.tsx
Normal file
53
web/src/components/Layout/MobileTaskbar.tsx
Normal 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;
|
||||
30
web/src/components/Layout/Ribbon.tsx
Normal file
30
web/src/components/Layout/Ribbon.tsx
Normal 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;
|
||||
3
web/src/components/Layout/index.ts
Normal file
3
web/src/components/Layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as MobileControls } from './MobileControls';
|
||||
export { default as Ribbon } from './Ribbon';
|
||||
export { default as MobileTaskbar } from './MobileTaskbar';
|
||||
269
web/src/components/Memory/MemoryEditor.tsx
Normal file
269
web/src/components/Memory/MemoryEditor.tsx
Normal 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;
|
||||
158
web/src/components/Memory/entries/BackgroundEntry.tsx
Normal file
158
web/src/components/Memory/entries/BackgroundEntry.tsx
Normal 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;
|
||||
157
web/src/components/Memory/entries/BaseEntry.tsx
Normal file
157
web/src/components/Memory/entries/BaseEntry.tsx
Normal 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;
|
||||
87
web/src/components/Memory/entries/IOEntry.tsx
Normal file
87
web/src/components/Memory/entries/IOEntry.tsx
Normal 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;
|
||||
75
web/src/components/Memory/entries/ParseErrorEntry.tsx
Normal file
75
web/src/components/Memory/entries/ParseErrorEntry.tsx
Normal 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;
|
||||
78
web/src/components/Memory/entries/ReadEntry.tsx
Normal file
78
web/src/components/Memory/entries/ReadEntry.tsx
Normal 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;
|
||||
53
web/src/components/Memory/entries/ReasoningEntry.tsx
Normal file
53
web/src/components/Memory/entries/ReasoningEntry.tsx
Normal 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;
|
||||
215
web/src/components/Memory/entries/RepeatEntry.tsx
Normal file
215
web/src/components/Memory/entries/RepeatEntry.tsx
Normal 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;
|
||||
127
web/src/components/Memory/entries/ScriptEntry.tsx
Normal file
127
web/src/components/Memory/entries/ScriptEntry.tsx
Normal 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;
|
||||
228
web/src/components/Memory/entries/SingleEntry.tsx
Normal file
228
web/src/components/Memory/entries/SingleEntry.tsx
Normal 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;
|
||||
78
web/src/components/Memory/entries/WriteEntry.tsx
Normal file
78
web/src/components/Memory/entries/WriteEntry.tsx
Normal 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;
|
||||
8
web/src/components/Memory/entries/index.ts
Normal file
8
web/src/components/Memory/entries/index.ts
Normal 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';
|
||||
1
web/src/components/Memory/index.ts
Normal file
1
web/src/components/Memory/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as MemoryEditor } from './MemoryEditor';
|
||||
77
web/src/components/Response/ResponseEditor.tsx
Normal file
77
web/src/components/Response/ResponseEditor.tsx
Normal 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;
|
||||
1
web/src/components/Response/index.ts
Normal file
1
web/src/components/Response/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ResponseEditor } from './ResponseEditor';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
58
web/src/components/shared/Button.tsx
Normal file
58
web/src/components/shared/Button.tsx
Normal 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;
|
||||
1
web/src/components/shared/index.ts
Normal file
1
web/src/components/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Button } from './Button';
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
41
web/src/contexts/MonacoContext.tsx
Normal file
41
web/src/contexts/MonacoContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
243
web/src/contexts/WebSocketContext.tsx
Normal file
243
web/src/contexts/WebSocketContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
web/src/hooks/useScreenSize.ts
Normal file
24
web/src/hooks/useScreenSize.ts
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
254
web/src/services/api.ts
Normal 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
54
web/src/types/entries.ts
Normal 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
49
web/src/types/index.ts
Normal 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;
|
||||
}
|
||||
@@ -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
25
web/tsconfig.json
Normal 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
10
web/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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
31
web/vite.config.ts
Normal 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'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user