WIP memory editor

This commit is contained in:
2024-11-23 21:36:50 +01:00
parent dbc0068a82
commit 2c820f6ead
15 changed files with 467 additions and 37 deletions

View File

@@ -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

View File

@@ -5,7 +5,9 @@ export const Tabs = {
CONTEXT: 'CONTEXT',
RESPONSE: 'RESPONSE',
INPUT: 'INPUT',
OUTPUT: 'OUTPUT'
OUTPUT: 'OUTPUT',
MEMORY: 'MEMORY'
};
export const Header = ({

View 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>
);
};

View 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;

View 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>
);