WIP memory editor
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Header, Tabs } from '@/components/Header';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import { FloatingButton } from '@/components/FloatingButton';
|
||||
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';
|
||||
|
||||
@@ -48,6 +49,8 @@ const App = () => {
|
||||
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
|
||||
@@ -111,6 +114,15 @@ const App = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 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) {
|
||||
@@ -189,7 +201,48 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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 LlmState.NO_OUTPUT:
|
||||
@@ -210,18 +263,20 @@ const App = () => {
|
||||
contextWs.wsState === WebSocketState.CONNECTED &&
|
||||
tokenWs.wsState === WebSocketState.CONNECTED &&
|
||||
stdoutWs.wsState === WebSocketState.CONNECTED &&
|
||||
autoApproverWs.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) {
|
||||
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]);
|
||||
}, [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">
|
||||
@@ -268,6 +323,16 @@ const App = () => {
|
||||
language="plaintext"
|
||||
/>
|
||||
)}
|
||||
{activeTab === Tabs.MEMORY && (
|
||||
<MemoryEditor
|
||||
entries={entries}
|
||||
onCreateEntry={handleCreateEntry}
|
||||
onSaveEntry={handleSaveEntry}
|
||||
onDeleteEntry={handleDeleteEntry}
|
||||
onResetEntry={handleResetEntry}
|
||||
onUpdateEntry={handleUpdateEntry}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Sidebar
|
||||
|
||||
@@ -5,7 +5,9 @@ export const Tabs = {
|
||||
CONTEXT: 'CONTEXT',
|
||||
RESPONSE: 'RESPONSE',
|
||||
INPUT: 'INPUT',
|
||||
OUTPUT: 'OUTPUT'
|
||||
OUTPUT: 'OUTPUT',
|
||||
MEMORY: 'MEMORY'
|
||||
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
|
||||
117
web/src/components/editors/BaseEntryEditor.jsx
Normal file
117
web/src/components/editors/BaseEntryEditor.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
96
web/src/components/editors/MemoryEditor.jsx
Normal file
96
web/src/components/editors/MemoryEditor.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState } from 'react';
|
||||
import { SingleEntryEditor } from './SingleEntryEditor';
|
||||
|
||||
const EntryTypes = {
|
||||
SINGLE: 'single'
|
||||
};
|
||||
|
||||
export const MemoryEditor = ({
|
||||
entries = [],
|
||||
onCreateEntry,
|
||||
onSaveEntry,
|
||||
onDeleteEntry,
|
||||
onResetEntry,
|
||||
onUpdateEntry
|
||||
}) => {
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const handleCreate = (_id, entry) => {
|
||||
onCreateEntry(entry);
|
||||
setShowCreateDialog(false);
|
||||
};
|
||||
|
||||
const renderCreateDialog = () => {
|
||||
const now = new Date();
|
||||
const id = now.getFullYear() +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
String(now.getDate()).padStart(2, '0') + '_' +
|
||||
String(now.getHours()).padStart(2, '0') +
|
||||
String(now.getMinutes()).padStart(2, '0') +
|
||||
String(now.getSeconds()).padStart(2, '0') + '_' +
|
||||
String(now.getMilliseconds()).padStart(3, '0');
|
||||
|
||||
const entry = {
|
||||
type: EntryTypes.SINGLE,
|
||||
id: id,
|
||||
script: '',
|
||||
timeout: null,
|
||||
limit: null,
|
||||
executed: false
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<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>
|
||||
<SingleEntryEditor
|
||||
entry={entry}
|
||||
isNew={true}
|
||||
isEditing={true}
|
||||
onSave={handleCreate}
|
||||
onCancel={() => setShowCreateDialog(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Add Entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{entries.map(entry => (
|
||||
<SingleEntryEditor
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
isEditing={true}
|
||||
onSave={onSaveEntry}
|
||||
onDelete={onDeleteEntry}
|
||||
onReset={onResetEntry}
|
||||
onUpdate={onUpdateEntry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showCreateDialog && renderCreateDialog()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoryEditor;
|
||||
138
web/src/components/editors/SingleEntryEditor.jsx
Normal file
138
web/src/components/editors/SingleEntryEditor.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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>
|
||||
<label className="block text-sm font-medium text-gray-700">Timeout (seconds)</label>
|
||||
<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"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Memory Limit (bytes)</label>
|
||||
<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"
|
||||
/>
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-gray-700">Exit Code</label>
|
||||
<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"
|
||||
/>
|
||||
</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>
|
||||
<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 ?? 'None'}</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>
|
||||
);
|
||||
Reference in New Issue
Block a user