WIP memory editor
This commit is contained in:
@@ -15,14 +15,9 @@ class Entry(ABC):
|
|||||||
Args:
|
Args:
|
||||||
id: Unique identifier for this entry
|
id: Unique identifier for this entry
|
||||||
"""
|
"""
|
||||||
self._id = id
|
self.id = id
|
||||||
self._change_handlers: List[Callable[['Entry'], None]] = []
|
self._change_handlers: List[Callable[['Entry'], None]] = []
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> str:
|
|
||||||
"""Get entry's unique identifier."""
|
|
||||||
return self._id
|
|
||||||
|
|
||||||
def add_change_handler(self, handler: Callable[['Entry'], None]) -> None:
|
def add_change_handler(self, handler: Callable[['Entry'], None]) -> None:
|
||||||
"""Add a callback for entry changes."""
|
"""Add a callback for entry changes."""
|
||||||
if handler not in self._change_handlers:
|
if handler not in self._change_handlers:
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class BackgroundEntry(Entry):
|
|||||||
Returns:
|
Returns:
|
||||||
ET.Element: XML element containing the entry's data
|
ET.Element: XML element containing the entry's data
|
||||||
"""
|
"""
|
||||||
element = ET.Element("background", {"id": self._id})
|
element = ET.Element("background", {"id": self.id})
|
||||||
if self._process is not None:
|
if self._process is not None:
|
||||||
element.set("pid", str(self._process.pid))
|
element.set("pid", str(self._process.pid))
|
||||||
elif self.exit_code is not None:
|
elif self.exit_code is not None:
|
||||||
@@ -154,7 +154,7 @@ class BackgroundEntry(Entry):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "background",
|
"type": "background",
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"script": self.script,
|
"script": self.script,
|
||||||
"stdout": self.stdout,
|
"stdout": self.stdout,
|
||||||
"stderr": self.stderr,
|
"stderr": self.stderr,
|
||||||
|
|||||||
@@ -116,5 +116,4 @@ class EntryFactory:
|
|||||||
if "content" in data:
|
if "content" in data:
|
||||||
entry.content = data["content"]
|
entry.content = data["content"]
|
||||||
if "written" in data:
|
if "written" in data:
|
||||||
entry.written = bool(data["written"])
|
entry.written = bool(data["written"])
|
||||||
entry.notify_change()
|
|
||||||
@@ -35,7 +35,7 @@ class ParseErrorEntry(Entry):
|
|||||||
Returns:
|
Returns:
|
||||||
ET.Element: XML element containing the entry's data
|
ET.Element: XML element containing the entry's data
|
||||||
"""
|
"""
|
||||||
element = ET.Element("parse_error", {"id": self._id})
|
element = ET.Element("parse_error", {"id": self.id})
|
||||||
error_elem = ET.SubElement(element, "error")
|
error_elem = ET.SubElement(element, "error")
|
||||||
error_elem.text = self.error
|
error_elem.text = self.error
|
||||||
content_elem = ET.SubElement(element, "content")
|
content_elem = ET.SubElement(element, "content")
|
||||||
@@ -49,7 +49,7 @@ class ParseErrorEntry(Entry):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "parse_error",
|
"type": "parse_error",
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"content": self.content,
|
"content": self.content,
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ class ReadEntry(Entry):
|
|||||||
io_buffer: Buffer to use for IO operations
|
io_buffer: Buffer to use for IO operations
|
||||||
"""
|
"""
|
||||||
super().__init__(id)
|
super().__init__(id)
|
||||||
self.content: str = "",
|
self.content: str = ""
|
||||||
self.read = False
|
self.read = False
|
||||||
self._io_buffer = io_buffer
|
self._io_buffer = io_buffer
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class ReadEntry(Entry):
|
|||||||
Returns:
|
Returns:
|
||||||
ET.Element: XML element containing the entry's data
|
ET.Element: XML element containing the entry's data
|
||||||
"""
|
"""
|
||||||
element = ET.Element("read_stdin", {"id": self._id})
|
element = ET.Element("read_stdin", {"id": self.id})
|
||||||
if self.read:
|
if self.read:
|
||||||
element.text = self.content
|
element.text = self.content
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ class ReadEntry(Entry):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "read_stdin",
|
"type": "read_stdin",
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"content": self.content,
|
"content": self.content,
|
||||||
"read": self.read,
|
"read": self.read,
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ class ReasoningEntry(Entry):
|
|||||||
Returns:
|
Returns:
|
||||||
ET.Element: XML element containing the entry's data
|
ET.Element: XML element containing the entry's data
|
||||||
"""
|
"""
|
||||||
element = ET.Element("reasoning", {"id": self._id})
|
element = ET.Element("reasoning", {"id": self.id})
|
||||||
element.text = self.content
|
element.text = self.content
|
||||||
|
|
||||||
return element
|
return element
|
||||||
@@ -45,6 +45,6 @@ class ReasoningEntry(Entry):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "reasoning",
|
"type": "reasoning",
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"content": self.content,
|
"content": self.content,
|
||||||
}
|
}
|
||||||
@@ -32,9 +32,9 @@ class RepeatEntry(Entry):
|
|||||||
self.script = script
|
self.script = script
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.stdout: str = "",
|
self.stdout: str = ""
|
||||||
self.stderr: str = "",
|
self.stderr: str = ""
|
||||||
self.exit_code: Optional[int] = None,
|
self.exit_code: Optional[int] = None
|
||||||
self.timed_out: bool = False
|
self.timed_out: bool = False
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ class SingleEntry(Entry):
|
|||||||
self.script = script
|
self.script = script
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.stdout: str = "",
|
self.stdout: str = ""
|
||||||
self.stderr: str = "",
|
self.stderr: str = ""
|
||||||
self.exit_code: Optional[int] = None
|
self.exit_code: Optional[int] = None
|
||||||
self.executed: bool = False,
|
self.executed: bool = False
|
||||||
self.timed_out: bool = False
|
self.timed_out: bool = False
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -111,4 +111,6 @@ class SingleEntry(Entry):
|
|||||||
"stdout": self.stdout,
|
"stdout": self.stdout,
|
||||||
"stderr": self.stderr,
|
"stderr": self.stderr,
|
||||||
"exit_code": self.exit_code,
|
"exit_code": self.exit_code,
|
||||||
|
"executed": self.executed,
|
||||||
|
"timed_out": self.timed_out
|
||||||
}
|
}
|
||||||
@@ -54,6 +54,6 @@ class WriteEntry(Entry):
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"type": "write",
|
"type": "write",
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"content": self.content,
|
"content": self.content,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ class Api:
|
|||||||
self._app.router.add_post("/api/auto_approver/llm", self._set_llm_name)
|
self._app.router.add_post("/api/auto_approver/llm", self._set_llm_name)
|
||||||
self._app.router.add_get("/api/memory", self._get_memory)
|
self._app.router.add_get("/api/memory", self._get_memory)
|
||||||
self._app.router.add_post("/api/memory/entry", self._create_entry)
|
self._app.router.add_post("/api/memory/entry", self._create_entry)
|
||||||
self._app.router.add_put("/api/memory/entry/{id}", self._update_entry)
|
self._app.router.add_put("/api/memory/entry/{id}", self._save_entry)
|
||||||
self._app.router.add_delete("/api/memory/entry/{id}", self._delete_entry)
|
self._app.router.add_delete("/api/memory/entry/{id}", self._delete_entry)
|
||||||
self._app.router.add_post("/api/memory/entry/{id}/reset", self._reset_entry)
|
self._app.router.add_post("/api/memory/entry/{id}/reset", self._reset_entry)
|
||||||
|
self._app.router.add_post("/api/memory/entry/{id}/update", self._update_entry)
|
||||||
|
|
||||||
async def _run_inference(self, request: web.Request) -> web.Response:
|
async def _run_inference(self, request: web.Request) -> web.Response:
|
||||||
"""Start inference on specified LLM."""
|
"""Start inference on specified LLM."""
|
||||||
@@ -212,7 +213,7 @@ class Api:
|
|||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
return web.Response(status=400, text=str(e))
|
return web.Response(status=400, text=str(e))
|
||||||
|
|
||||||
async def _update_entry(self, request: web.Request) -> web.Response:
|
async def _save_entry(self, request: web.Request) -> web.Response:
|
||||||
"""Update properties of an existing entry."""
|
"""Update properties of an existing entry."""
|
||||||
entry_id = request.match_info["id"]
|
entry_id = request.match_info["id"]
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
@@ -221,6 +222,7 @@ class Api:
|
|||||||
return web.Response(status=404, text="Entry not found")
|
return web.Response(status=404, text="Entry not found")
|
||||||
try:
|
try:
|
||||||
EntryFactory.update_entry(entry, data)
|
EntryFactory.update_entry(entry, data)
|
||||||
|
entry.notify_change()
|
||||||
return web.Response(status=200)
|
return web.Response(status=200)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return web.Response(status=400, text=str(e))
|
return web.Response(status=400, text=str(e))
|
||||||
@@ -238,4 +240,18 @@ class Api:
|
|||||||
if not entry:
|
if not entry:
|
||||||
return web.Response(status=404, text="Entry not found")
|
return web.Response(status=404, text="Entry not found")
|
||||||
entry.reset()
|
entry.reset()
|
||||||
return web.Response(status=200)
|
entry.notify_change()
|
||||||
|
return web.Response(status=200)
|
||||||
|
|
||||||
|
async def _update_entry(self, request: web.Request) -> web.Response:
|
||||||
|
"""Update an entry's state."""
|
||||||
|
entry_id = request.match_info["id"]
|
||||||
|
entry = self._working_memory.get_entry(entry_id)
|
||||||
|
if not entry:
|
||||||
|
return web.Response(status=404, text="Entry not found")
|
||||||
|
try:
|
||||||
|
entry.update()
|
||||||
|
entry.notify_change()
|
||||||
|
return web.Response(status=200)
|
||||||
|
except ValueError as e:
|
||||||
|
return web.Response(status=400, text=str(e))
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { 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 { StandardEditor } from '@/components/editors/StandardEditor';
|
||||||
import { useWebSocket, WebSocketState } from '@/hooks/useWebSocket';
|
import { useWebSocket, WebSocketState } from '@/hooks/useWebSocket';
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ const App = () => {
|
|||||||
const tokenWs = useWebSocket(`${wsRoot}/token`);
|
const tokenWs = useWebSocket(`${wsRoot}/token`);
|
||||||
const stdoutWs = useWebSocket(`${wsRoot}/stdout`);
|
const stdoutWs = useWebSocket(`${wsRoot}/stdout`);
|
||||||
const autoApproverWs = useWebSocket(`${wsRoot}/auto_approver`);
|
const autoApproverWs = useWebSocket(`${wsRoot}/auto_approver`);
|
||||||
|
const memoryWs = useWebSocket(`${wsRoot}/memory`);
|
||||||
|
const [entries, setEntries] = useState([]);
|
||||||
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
||||||
|
|
||||||
// Handle llm state changes
|
// 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
|
// Initialize active llm
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeLlm === null && Object.keys(llms).length > 0) {
|
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(() => {
|
useEffect(() => {
|
||||||
|
if (activeTab === Tabs.MEMORY) return;
|
||||||
const state = llms[activeLlm];
|
const state = llms[activeLlm];
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case LlmState.NO_OUTPUT:
|
case LlmState.NO_OUTPUT:
|
||||||
@@ -210,18 +263,20 @@ const App = () => {
|
|||||||
contextWs.wsState === WebSocketState.CONNECTED &&
|
contextWs.wsState === WebSocketState.CONNECTED &&
|
||||||
tokenWs.wsState === WebSocketState.CONNECTED &&
|
tokenWs.wsState === WebSocketState.CONNECTED &&
|
||||||
stdoutWs.wsState === WebSocketState.CONNECTED &&
|
stdoutWs.wsState === WebSocketState.CONNECTED &&
|
||||||
autoApproverWs.wsState === WebSocketState.CONNECTED) {
|
autoApproverWs.wsState === WebSocketState.CONNECTED &&
|
||||||
|
memoryWs.wsState === WebSocketState.CONNECTED) {
|
||||||
setWsState(WebSocketState.CONNECTED);
|
setWsState(WebSocketState.CONNECTED);
|
||||||
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
|
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
|
||||||
contextWs.wsState === WebSocketState.CONNECTING ||
|
contextWs.wsState === WebSocketState.CONNECTING ||
|
||||||
tokenWs.wsState === WebSocketState.CONNECTING ||
|
tokenWs.wsState === WebSocketState.CONNECTING ||
|
||||||
stdoutWs.wsState === WebSocketState.CONNECTING ||
|
stdoutWs.wsState === WebSocketState.CONNECTING ||
|
||||||
autoApproverWs.wsState === WebSocketState.CONNECTING) {
|
autoApproverWs.wsState === WebSocketState.CONNECTING ||
|
||||||
|
memoryWs.wsState === WebSocketState.CONNECTING) {
|
||||||
setWsState(WebSocketState.CONNECTING);
|
setWsState(WebSocketState.CONNECTING);
|
||||||
} else {
|
} else {
|
||||||
setWsState(WebSocketState.DISCONNECTED);
|
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 (
|
return (
|
||||||
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
|
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
|
||||||
@@ -268,6 +323,16 @@ const App = () => {
|
|||||||
language="plaintext"
|
language="plaintext"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeTab === Tabs.MEMORY && (
|
||||||
|
<MemoryEditor
|
||||||
|
entries={entries}
|
||||||
|
onCreateEntry={handleCreateEntry}
|
||||||
|
onSaveEntry={handleSaveEntry}
|
||||||
|
onDeleteEntry={handleDeleteEntry}
|
||||||
|
onResetEntry={handleResetEntry}
|
||||||
|
onUpdateEntry={handleUpdateEntry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ export const Tabs = {
|
|||||||
CONTEXT: 'CONTEXT',
|
CONTEXT: 'CONTEXT',
|
||||||
RESPONSE: 'RESPONSE',
|
RESPONSE: 'RESPONSE',
|
||||||
INPUT: 'INPUT',
|
INPUT: 'INPUT',
|
||||||
OUTPUT: 'OUTPUT'
|
OUTPUT: 'OUTPUT',
|
||||||
|
MEMORY: 'MEMORY'
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = ({
|
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