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

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

View File

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

View File

@@ -117,4 +117,3 @@ class EntryFactory:
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
entry.notify_change()
return web.Response(status=200) 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))

View File

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

View File

@@ -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 = ({

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