Wip web interface

This commit is contained in:
Niels Geens
2025-04-17 22:09:15 +02:00
parent 813c6a3f8f
commit 023f6bba9a
13 changed files with 1040 additions and 1040 deletions

View File

@@ -180,4 +180,5 @@ class AutoApprover:
return
if (self._response_enabled and
self.agent.llms[self._llm_name] == LlmState.IDLE):
self.agent.approve_response(self._llm_name, self.agent.get_output(self._llm_name))
response = self.agent.response_buffer.get_text()
self.agent.approve_response()

View File

@@ -94,6 +94,8 @@ class Api:
"""Approve current buffer content"""
data = await request.json()
try:
response = data.get("response")
self._agent.response_buffer.set_text(response)
self._agent.approve_response()
return web.Response(status=200)
except ValueError as e:
@@ -147,10 +149,10 @@ class Api:
"""Get all LLMs and their current states."""
states = self._agent.llms
return web.Response(
text=json.dumps({
name: state.name
for name, state in states.items()
}),
text=json.dumps(
[{"name": name, "state": state.name}
for name, state in states.items()]
),
content_type="application/json"
)
@@ -234,10 +236,12 @@ class Api:
"""Get complete working memory state."""
entries = self._working_memory.get_entries()
return web.Response(
text=json.dumps([e.serialize() for e in entries]),
text=json.dumps({
"type": "memory_state",
"entries": [e.serialize() for e in entries]
}),
content_type="application/json"
)
async def _create_entry(self, request: web.Request) -> web.Response:
"""Create a new entry in working memory."""
data = await request.json()

View File

@@ -12,7 +12,7 @@ class ResponseWebSocket:
async def _handle_buffer_change(self, buffer):
await self._broadcast_message({
"buffer": buffer,
"response": buffer,
})
async def _broadcast_message(self, message):
@@ -32,7 +32,7 @@ class ResponseWebSocket:
try:
await ws.send_json({
"buffer": self._agent.response_buffer.get_text(),
"response": self._agent.response_buffer.get_text(),
})
async for msg in ws:

View File

@@ -22,7 +22,7 @@ class Websockets:
app.router.add_get("/ws/auto_approver", self._auto_approver_ws.handle_connection)
app.router.add_get("/ws/context", self._context_ws.handle_connection)
app.router.add_get("/ws/llm", self._llm_ws.handle_connection)
app.router.add_get("/ws/llms", self._llm_ws.handle_connection)
app.router.add_get("/ws/memory", self._memory_ws.handle_connection)
app.router.add_get("/ws/response", self._response_ws.handle_connection)
app.router.add_get("/ws/stdout", self._stdout_ws.handle_connection)

View File

@@ -132,6 +132,8 @@ class WebAgent(BaseAgent):
def approve_response(self) -> None:
"""Process approved response from specified LLM"""
if self.llms.get(llm_name) != LlmState.IDLE:
return
timestamp = datetime.now(timezone.utc)
self._iteration_logger.log_iteration(timestamp, self._context, self._response_buffer.get_text())
parse_result = self._parser.parse(timestamp, self._response_buffer.get_text())

View File

@@ -1,373 +1,349 @@
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 = {
NO_OUTPUT: 'NO_OUTPUT',
INFERENCE: 'INFERENCE',
OUTPUT: 'OUTPUT'
};
const App = () => {
// Editor content state
const [generatedContext, setGeneratedContext] = useState('');
const [modifiedContext, setModifiedContext] = useState('');
const [contextDirty, setContextDirty] = useState(false);
const [generatedResponses, setGeneratedResponses] = useState({});
const [modifiedResponses, setModifiedResponses] = 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}/llm`);
const contextWs = useWebSocket(`${wsRoot}/context`);
const tokenWs = useWebSocket(`${wsRoot}/token`);
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
}));
if (data.state === LlmState.NO_OUTPUT) {
setGeneratedResponses(prev => {
const updated = { ...prev };
delete updated[data.llm];
return updated;
});
setModifiedResponses(prev => {
const updated = { ...prev };
delete updated[data.llm];
return updated;
});
}
});
}, []);
// Handle context changes
useEffect(() => {
contextWs.addMessageHandler((data) => {
setContextDirty(false);
setModifiedContext(data.context);
if (data.generated) {
setGeneratedContext(data.context);
}
});
}, []);
// Handle incoming tokens
useEffect(() => {
tokenWs.addMessageHandler((data) => {
setGeneratedResponses(prev => ({
...prev,
[data.llm]: (prev[data.llm] || '') + data.token
}));
setModifiedResponses(prev => ({
...prev,
[data.llm]: (prev[data.llm] || '') + data.token
}));
});
}, []);
// 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 = () => {
const state = llms[activeLlm];
if (state === LlmState.NO_OUTPUT) {
if (contextDirty) {
fetch('/api/context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context: modifiedContext })
});
}
fetch(`/api/inference/${activeLlm}`, {
method: 'POST',
});
} else if (state === LlmState.OUTPUT) {
fetch(`/api/approve/${activeLlm}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponses[activeLlm] })
});
}
};
const handleStop = () => {
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);
setGeneratedResponses({});
setModifiedResponses({});
const resetLlms = {};
for (const llm of Object.keys(llms)) {
resetLlms[llm] = LlmState.NO_OUTPUT;
}
setLlms(resetLlms);
setModifiedContext(context);
};
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');
};
// Update active tab on state change
useEffect(() => {
if (activeTab === Tabs.MEMORY) return;
const state = llms[activeLlm];
switch (state) {
case null:
break;
case LlmState.NO_OUTPUT:
setActiveTab(Tabs.CONTEXT);
setShowDiff(false);
setShowSidebar(false);
break;
default:
setActiveTab(Tabs.RESPONSE);
setShowDiff(false);
setShowSidebar(false);
break;
}
}, [llms[activeLlm]]);
useEffect(() => {
if (llmWs.wsState === WebSocketState.CONNECTED &&
contextWs.wsState === WebSocketState.CONNECTED &&
tokenWs.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 ||
tokenWs.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, tokenWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
return (
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
<Header
wsState={wsState}
agentState={llms[activeLlm]}
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={generatedResponses[activeLlm] || ''}
modifiedContent={modifiedResponses[activeLlm] || ''}
onChange={value => setModifiedResponses(prev => ({
...prev,
[activeLlm]: value
}))}
/>
)}
{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={llms[activeLlm]}
showDiff={showDiff}
autoScroll={autoScroll}
onAutoScrollChange={(enabled) => setAutoScroll(enabled)}
input={input}
output={output}
autoApproverConfig={autoApproverConfig}
onApprove={handleApprove}
onStop={handleStop}
onToggleDiff={() => setShowDiff(!showDiff)}
onSendInput={handleSendInput}
onClearOutput={handleClearOutput}
onLlmChange={setActiveLlm}
onNextLlm={handleNextLlm}
onAutoApproverConfigChange={handleAutoApproverConfigChange}
/>
<FloatingButton onClick={() => setShowSidebar(!showSidebar)} />
</div>
</div>
);
};
export default App;
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 [contextDirty, setContextDirty] = useState(false);
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) => {
setContextDirty(false);
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/context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context: modifiedContext })
});
fetch('/api/response', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponse })
});
fetch(`/api/inference/${activeLlm}`, {
method: 'POST',
});
};
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);
// Reset response buffers
setGeneratedResponse('');
setModifiedResponse('');
};
const handleResponseEdit = (response) => {
setModifiedResponse(response);
};
const handleAutoApproverConfigChange = async (config) => {
try {
const response = await fetch('/api/auto_approver/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) {
throw new Error('Failed to update auto approver config');
}
} catch (error) {
console.error('Error updating auto approver config:', error);
}
};
const handleCreateEntry = async (entry) => {
const response = await fetch('/api/memory/entry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
if (!response.ok) throw new Error('Failed to create entry');
};
const handleSaveEntry = async (id, entry) => {
const response = await fetch(`/api/memory/entry/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
if (!response.ok) throw new Error('Failed to update entry');
};
const handleDeleteEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete entry');
};
const handleResetEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}/reset`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to reset entry');
};
const handleUpdateEntry = async (id) => {
const response = await fetch(`/api/memory/entry/${id}/update`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to update entry');
};
// Determine active LLM's current state for UI feedback
const activeLlmState = activeLlm ? llms[activeLlm] : null;
// Track overall WebSocket connection state
useEffect(() => {
if (llmWs.wsState === WebSocketState.CONNECTED &&
contextWs.wsState === WebSocketState.CONNECTED &&
responseWs.wsState === WebSocketState.CONNECTED &&
stdoutWs.wsState === WebSocketState.CONNECTED &&
autoApproverWs.wsState === WebSocketState.CONNECTED &&
memoryWs.wsState === WebSocketState.CONNECTED) {
setWsState(WebSocketState.CONNECTED);
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
contextWs.wsState === WebSocketState.CONNECTING ||
responseWs.wsState === WebSocketState.CONNECTING ||
stdoutWs.wsState === WebSocketState.CONNECTING ||
autoApproverWs.wsState === WebSocketState.CONNECTING ||
memoryWs.wsState === WebSocketState.CONNECTING) {
setWsState(WebSocketState.CONNECTING);
} else {
setWsState(WebSocketState.DISCONNECTED);
}
}, [llmWs.wsState, contextWs.wsState, responseWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
return (
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
<Header
wsState={wsState}
agentState={activeLlmState}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
<div className="flex flex-1 overflow-hidden">
<main className="flex-1 p-4 overflow-auto">
{activeTab === Tabs.CONTEXT && (
<ContentEditor
showDiff={showDiff}
originalContent={generatedContext}
modifiedContent={modifiedContext}
onChange={handleContextEdit}
autoScroll={autoScroll}
/>
)}
{activeTab === Tabs.RESPONSE && (
<ContentEditor
showDiff={showDiff}
originalContent={generatedResponse}
modifiedContent={modifiedResponse}
onChange={handleResponseEdit}
/>
)}
{activeTab === Tabs.INPUT && (
<StandardEditor
content={input}
onChange={value => setInput(value)}
language="plaintext"
/>
)}
{activeTab === Tabs.OUTPUT && (
<StandardEditor
content={output}
readOnly={true}
language="plaintext"
/>
)}
{activeTab === Tabs.MEMORY && (
<MemoryEditor
entries={entries}
onCreateEntry={handleCreateEntry}
onSaveEntry={handleSaveEntry}
onDeleteEntry={handleDeleteEntry}
onResetEntry={handleResetEntry}
onUpdateEntry={handleUpdateEntry}
/>
)}
</main>
<Sidebar
showSidebar={showSidebar}
llms={llms}
activeLlm={activeLlm}
llmState={activeLlmState}
showDiff={showDiff}
autoScroll={autoScroll}
onAutoScrollChange={setAutoScroll}
input={input}
output={output}
autoApproverConfig={autoApproverConfig}
onApprove={handleApprove}
onInference={handleInference}
onStop={handleStop}
onToggleDiff={() => setShowDiff(!showDiff)}
onSendInput={handleSendInput}
onClearOutput={handleClearOutput}
onLlmChange={setActiveLlm}
onNextLlm={handleNextLlm}
onAutoApproverConfigChange={handleAutoApproverConfigChange}
/>
<FloatingButton onClick={() => setShowSidebar(!showSidebar)} />
</div>
</div>
);
};
export default App;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,68 +1,68 @@
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
};
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
};
};