Wip web interface
This commit is contained in:
@@ -180,4 +180,5 @@ class AutoApprover:
|
|||||||
return
|
return
|
||||||
if (self._response_enabled and
|
if (self._response_enabled and
|
||||||
self.agent.llms[self._llm_name] == LlmState.IDLE):
|
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()
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class Api:
|
|||||||
"""Approve current buffer content"""
|
"""Approve current buffer content"""
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
try:
|
try:
|
||||||
|
response = data.get("response")
|
||||||
|
self._agent.response_buffer.set_text(response)
|
||||||
self._agent.approve_response()
|
self._agent.approve_response()
|
||||||
return web.Response(status=200)
|
return web.Response(status=200)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -147,10 +149,10 @@ class Api:
|
|||||||
"""Get all LLMs and their current states."""
|
"""Get all LLMs and their current states."""
|
||||||
states = self._agent.llms
|
states = self._agent.llms
|
||||||
return web.Response(
|
return web.Response(
|
||||||
text=json.dumps({
|
text=json.dumps(
|
||||||
name: state.name
|
[{"name": name, "state": state.name}
|
||||||
for name, state in states.items()
|
for name, state in states.items()]
|
||||||
}),
|
),
|
||||||
content_type="application/json"
|
content_type="application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,10 +236,12 @@ class Api:
|
|||||||
"""Get complete working memory state."""
|
"""Get complete working memory state."""
|
||||||
entries = self._working_memory.get_entries()
|
entries = self._working_memory.get_entries()
|
||||||
return web.Response(
|
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"
|
content_type="application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _create_entry(self, request: web.Request) -> web.Response:
|
async def _create_entry(self, request: web.Request) -> web.Response:
|
||||||
"""Create a new entry in working memory."""
|
"""Create a new entry in working memory."""
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ResponseWebSocket:
|
|||||||
|
|
||||||
async def _handle_buffer_change(self, buffer):
|
async def _handle_buffer_change(self, buffer):
|
||||||
await self._broadcast_message({
|
await self._broadcast_message({
|
||||||
"buffer": buffer,
|
"response": buffer,
|
||||||
})
|
})
|
||||||
|
|
||||||
async def _broadcast_message(self, message):
|
async def _broadcast_message(self, message):
|
||||||
@@ -32,7 +32,7 @@ class ResponseWebSocket:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await ws.send_json({
|
await ws.send_json({
|
||||||
"buffer": self._agent.response_buffer.get_text(),
|
"response": self._agent.response_buffer.get_text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Websockets:
|
|||||||
|
|
||||||
app.router.add_get("/ws/auto_approver", self._auto_approver_ws.handle_connection)
|
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/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/memory", self._memory_ws.handle_connection)
|
||||||
app.router.add_get("/ws/response", self._response_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)
|
app.router.add_get("/ws/stdout", self._stdout_ws.handle_connection)
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ class WebAgent(BaseAgent):
|
|||||||
|
|
||||||
def approve_response(self) -> None:
|
def approve_response(self) -> None:
|
||||||
"""Process approved response from specified LLM"""
|
"""Process approved response from specified LLM"""
|
||||||
|
if self.llms.get(llm_name) != LlmState.IDLE:
|
||||||
|
return
|
||||||
timestamp = datetime.now(timezone.utc)
|
timestamp = datetime.now(timezone.utc)
|
||||||
self._iteration_logger.log_iteration(timestamp, self._context, self._response_buffer.get_text())
|
self._iteration_logger.log_iteration(timestamp, self._context, self._response_buffer.get_text())
|
||||||
parse_result = self._parser.parse(timestamp, self._response_buffer.get_text())
|
parse_result = self._parser.parse(timestamp, self._response_buffer.get_text())
|
||||||
|
|||||||
@@ -1,373 +1,349 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ContentEditor } from '@/components/editors/ContentEditor';
|
import { ContentEditor } from '@/components/editors/ContentEditor';
|
||||||
import { FloatingButton } from '@/components/FloatingButton';
|
import { FloatingButton } from '@/components/FloatingButton';
|
||||||
import { Header, Tabs } from '@/components/Header';
|
import { Header, Tabs } from '@/components/Header';
|
||||||
import { MemoryEditor } from '@/components/editors/MemoryEditor';
|
import { MemoryEditor } from '@/components/editors/MemoryEditor';
|
||||||
import { Sidebar } from '@/components/Sidebar';
|
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';
|
||||||
|
|
||||||
export const LlmState = {
|
export const LlmState = {
|
||||||
NO_OUTPUT: 'NO_OUTPUT',
|
IDLE: 'IDLE',
|
||||||
INFERENCE: 'INFERENCE',
|
INFERENCE: 'INFERENCE'
|
||||||
OUTPUT: 'OUTPUT'
|
};
|
||||||
};
|
|
||||||
|
const App = () => {
|
||||||
const App = () => {
|
// Editor content state
|
||||||
// Editor content state
|
const [generatedContext, setGeneratedContext] = useState('');
|
||||||
const [generatedContext, setGeneratedContext] = useState('');
|
const [modifiedContext, setModifiedContext] = useState('');
|
||||||
const [modifiedContext, setModifiedContext] = useState('');
|
const [contextDirty, setContextDirty] = useState(false);
|
||||||
const [contextDirty, setContextDirty] = useState(false);
|
const [generatedResponse, setGeneratedResponse] = useState('');
|
||||||
const [generatedResponses, setGeneratedResponses] = useState({});
|
const [modifiedResponse, setModifiedResponse] = useState('');
|
||||||
const [modifiedResponses, setModifiedResponses] = useState({});
|
const [input, setInput] = useState('');
|
||||||
const [input, setInput] = useState('');
|
const [output, setOutput] = useState('');
|
||||||
const [output, setOutput] = useState('');
|
|
||||||
|
// LLM state
|
||||||
// LLM state
|
const [llms, setLlms] = useState({});
|
||||||
const [llms, setLlms] = useState({});
|
const [activeLlm, setActiveLlm] = useState(null);
|
||||||
const [activeLlm, setActiveLlm] = useState(null);
|
|
||||||
|
// Auto approver state
|
||||||
// Auto approver state
|
const [autoApproverConfig, setAutoApproverConfig] = useState({
|
||||||
const [autoApproverConfig, setAutoApproverConfig] = useState({
|
context_enabled: false,
|
||||||
context_enabled: false,
|
response_enabled: false,
|
||||||
response_enabled: false,
|
context_timeout: 5.0,
|
||||||
context_timeout: 5.0,
|
response_timeout: 10.0,
|
||||||
response_timeout: 10.0,
|
llm_name: null
|
||||||
llm_name: null
|
});
|
||||||
});
|
|
||||||
|
// UI state
|
||||||
// UI state
|
const [activeTab, setActiveTab] = useState(Tabs.CONTEXT);
|
||||||
const [activeTab, setActiveTab] = useState(Tabs.CONTEXT);
|
const [showDiff, setShowDiff] = useState(false);
|
||||||
const [showDiff, setShowDiff] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [autoScroll, setAutoScroll] = useState(false);
|
||||||
const [autoScroll, setAutoScroll] = useState(false);
|
|
||||||
|
// WebSocket connections
|
||||||
// WebSocket connections
|
const wsRoot = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
|
||||||
const wsRoot = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
|
const llmWs = useWebSocket(`${wsRoot}/llms`);
|
||||||
const llmWs = useWebSocket(`${wsRoot}/llm`);
|
const contextWs = useWebSocket(`${wsRoot}/context`);
|
||||||
const contextWs = useWebSocket(`${wsRoot}/context`);
|
const responseWs = useWebSocket(`${wsRoot}/response`);
|
||||||
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 memoryWs = useWebSocket(`${wsRoot}/memory`);
|
const [entries, setEntries] = useState([]);
|
||||||
const [entries, setEntries] = useState([]);
|
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
||||||
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
|
||||||
|
// Handle llm state changes
|
||||||
// Handle llm state changes
|
useEffect(() => {
|
||||||
useEffect(() => {
|
llmWs.addMessageHandler((data) => {
|
||||||
llmWs.addMessageHandler((data) => {
|
setLlms(prevLlms => ({
|
||||||
setLlms(prevLlms => ({
|
...prevLlms,
|
||||||
...prevLlms,
|
[data.llm]: data.state
|
||||||
[data.llm]: data.state
|
}));
|
||||||
}));
|
});
|
||||||
if (data.state === LlmState.NO_OUTPUT) {
|
}, []);
|
||||||
setGeneratedResponses(prev => {
|
|
||||||
const updated = { ...prev };
|
// Handle context changes
|
||||||
delete updated[data.llm];
|
useEffect(() => {
|
||||||
return updated;
|
contextWs.addMessageHandler((data) => {
|
||||||
});
|
setContextDirty(false);
|
||||||
setModifiedResponses(prev => {
|
setModifiedContext(data.context);
|
||||||
const updated = { ...prev };
|
if (data.generated) {
|
||||||
delete updated[data.llm];
|
setGeneratedContext(data.context);
|
||||||
return updated;
|
}
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
});
|
|
||||||
}, []);
|
// Handle response changes
|
||||||
|
useEffect(() => {
|
||||||
// Handle context changes
|
responseWs.addMessageHandler((data) => {
|
||||||
useEffect(() => {
|
setModifiedResponse(data.response);
|
||||||
contextWs.addMessageHandler((data) => {
|
setGeneratedResponse(data.response);
|
||||||
setContextDirty(false);
|
});
|
||||||
setModifiedContext(data.context);
|
}, []);
|
||||||
if (data.generated) {
|
|
||||||
setGeneratedContext(data.context);
|
// Handle stdout changes
|
||||||
}
|
useEffect(() => {
|
||||||
});
|
stdoutWs.addMessageHandler((data) => {
|
||||||
}, []);
|
setOutput(data.output);
|
||||||
|
});
|
||||||
// Handle incoming tokens
|
}, []);
|
||||||
useEffect(() => {
|
|
||||||
tokenWs.addMessageHandler((data) => {
|
// Handle auto approver config updates
|
||||||
setGeneratedResponses(prev => ({
|
useEffect(() => {
|
||||||
...prev,
|
autoApproverWs.addMessageHandler((data) => {
|
||||||
[data.llm]: (prev[data.llm] || '') + data.token
|
setAutoApproverConfig(data.config);
|
||||||
}));
|
});
|
||||||
setModifiedResponses(prev => ({
|
}, []);
|
||||||
...prev,
|
|
||||||
[data.llm]: (prev[data.llm] || '') + data.token
|
// Handle memory state changes
|
||||||
}));
|
useEffect(() => {
|
||||||
});
|
memoryWs.addMessageHandler((data) => {
|
||||||
}, []);
|
if (data.type === 'memory_state') {
|
||||||
|
setEntries(data.entries);
|
||||||
// Handle stdout changes
|
}
|
||||||
useEffect(() => {
|
});
|
||||||
stdoutWs.addMessageHandler((data) => {
|
}, []);
|
||||||
setOutput(data.output);
|
|
||||||
});
|
// Initialize active llm
|
||||||
}, []);
|
useEffect(() => {
|
||||||
|
if (activeLlm === null && Object.keys(llms).length > 0) {
|
||||||
// Handle auto approver config updates
|
setActiveLlm(Object.keys(llms)[0]);
|
||||||
useEffect(() => {
|
}
|
||||||
autoApproverWs.addMessageHandler((data) => {
|
}, [llms, activeLlm]);
|
||||||
setAutoApproverConfig(data.config);
|
|
||||||
});
|
const handleNextLlm = () => {
|
||||||
}, []);
|
const llmNames = Object.keys(llms);
|
||||||
|
const currentIndex = llmNames.indexOf(activeLlm);
|
||||||
// Handle memory state changes
|
const nextIndex = (currentIndex + 1) % llmNames.length;
|
||||||
useEffect(() => {
|
setActiveLlm(llmNames[nextIndex]);
|
||||||
memoryWs.addMessageHandler((data) => {
|
};
|
||||||
if (data.type === 'memory_state') {
|
|
||||||
setEntries(data.entries);
|
const handleApprove = () => {
|
||||||
}
|
fetch('/api/response/approve', {
|
||||||
});
|
method: 'POST',
|
||||||
}, []);
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ response: modifiedResponse })
|
||||||
// Initialize active llm
|
});
|
||||||
useEffect(() => {
|
};
|
||||||
if (activeLlm === null && Object.keys(llms).length > 0) {
|
|
||||||
setActiveLlm(Object.keys(llms)[0]);
|
const handleInference = () => {
|
||||||
}
|
fetch('/api/context', {
|
||||||
}, [llms, activeLlm]);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
const handleNextLlm = () => {
|
body: JSON.stringify({ context: modifiedContext })
|
||||||
const llmNames = Object.keys(llms);
|
});
|
||||||
const currentIndex = llmNames.indexOf(activeLlm);
|
|
||||||
const nextIndex = (currentIndex + 1) % llmNames.length;
|
fetch('/api/response', {
|
||||||
setActiveLlm(llmNames[nextIndex]);
|
method: 'POST',
|
||||||
};
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ response: modifiedResponse })
|
||||||
const handleApprove = () => {
|
});
|
||||||
const state = llms[activeLlm];
|
|
||||||
if (state === LlmState.NO_OUTPUT) {
|
fetch(`/api/inference/${activeLlm}`, {
|
||||||
if (contextDirty) {
|
method: 'POST',
|
||||||
fetch('/api/context', {
|
});
|
||||||
method: 'POST',
|
};
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ context: modifiedContext })
|
const handleStop = () => {
|
||||||
});
|
// We still specify which LLM to stop inference for
|
||||||
}
|
fetch(`/api/inference/${activeLlm}/stop`, {
|
||||||
fetch(`/api/inference/${activeLlm}`, {
|
method: 'POST'
|
||||||
method: 'POST',
|
});
|
||||||
});
|
};
|
||||||
} else if (state === LlmState.OUTPUT) {
|
|
||||||
fetch(`/api/approve/${activeLlm}`, {
|
const handleSendInput = () => {
|
||||||
method: 'POST',
|
if (input.trim()) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
fetch('/api/input', {
|
||||||
body: JSON.stringify({ response: modifiedResponses[activeLlm] })
|
method: 'POST',
|
||||||
});
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
body: JSON.stringify({ input: input })
|
||||||
};
|
});
|
||||||
|
setInput('');
|
||||||
const handleStop = () => {
|
}
|
||||||
fetch(`/api/inference/${activeLlm}/stop`, {
|
};
|
||||||
method: 'POST'
|
|
||||||
});
|
const handleClearOutput = () => {
|
||||||
};
|
fetch('/api/clear', { method: 'POST' });
|
||||||
|
};
|
||||||
const handleSendInput = () => {
|
|
||||||
if (input.trim()) {
|
const handleContextEdit = (context) => {
|
||||||
fetch('/api/input', {
|
setContextDirty(true);
|
||||||
method: 'POST',
|
// When context is edited, we need to reset all LLM states to IDLE
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// since responses are no longer valid
|
||||||
body: JSON.stringify({ input: input })
|
const resetLlms = {};
|
||||||
});
|
for (const llm of Object.keys(llms)) {
|
||||||
setInput('');
|
resetLlms[llm] = LlmState.IDLE;
|
||||||
}
|
}
|
||||||
};
|
setLlms(resetLlms);
|
||||||
|
setModifiedContext(context);
|
||||||
const handleClearOutput = () => {
|
|
||||||
fetch('/api/clear', { method: 'POST' });
|
// Reset response buffers
|
||||||
};
|
setGeneratedResponse('');
|
||||||
|
setModifiedResponse('');
|
||||||
const handleContextEdit = (context) => {
|
};
|
||||||
setContextDirty(true);
|
|
||||||
setGeneratedResponses({});
|
const handleResponseEdit = (response) => {
|
||||||
setModifiedResponses({});
|
setModifiedResponse(response);
|
||||||
const resetLlms = {};
|
};
|
||||||
for (const llm of Object.keys(llms)) {
|
|
||||||
resetLlms[llm] = LlmState.NO_OUTPUT;
|
const handleAutoApproverConfigChange = async (config) => {
|
||||||
}
|
try {
|
||||||
setLlms(resetLlms);
|
const response = await fetch('/api/auto_approver/config', {
|
||||||
setModifiedContext(context);
|
method: 'POST',
|
||||||
};
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config)
|
||||||
const handleAutoApproverConfigChange = async (config) => {
|
});
|
||||||
try {
|
if (!response.ok) {
|
||||||
const response = await fetch('/api/auto_approver/config', {
|
throw new Error('Failed to update auto approver config');
|
||||||
method: 'POST',
|
}
|
||||||
headers: { 'Content-Type': 'application/json' },
|
} catch (error) {
|
||||||
body: JSON.stringify(config)
|
console.error('Error updating auto approver config:', error);
|
||||||
});
|
}
|
||||||
if (!response.ok) {
|
};
|
||||||
throw new Error('Failed to update auto approver config');
|
|
||||||
}
|
const handleCreateEntry = async (entry) => {
|
||||||
} catch (error) {
|
const response = await fetch('/api/memory/entry', {
|
||||||
console.error('Error updating auto approver config:', error);
|
method: 'POST',
|
||||||
}
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
body: JSON.stringify(entry)
|
||||||
|
});
|
||||||
const handleCreateEntry = async (entry) => {
|
if (!response.ok) throw new Error('Failed to create entry');
|
||||||
const response = await fetch('/api/memory/entry', {
|
};
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const handleSaveEntry = async (id, entry) => {
|
||||||
body: JSON.stringify(entry)
|
const response = await fetch(`/api/memory/entry/${id}`, {
|
||||||
});
|
method: 'PUT',
|
||||||
if (!response.ok) throw new Error('Failed to create entry');
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
body: JSON.stringify(entry)
|
||||||
|
});
|
||||||
const handleSaveEntry = async (id, entry) => {
|
if (!response.ok) throw new Error('Failed to update entry');
|
||||||
const response = await fetch(`/api/memory/entry/${id}`, {
|
};
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const handleDeleteEntry = async (id) => {
|
||||||
body: JSON.stringify(entry)
|
const response = await fetch(`/api/memory/entry/${id}`, {
|
||||||
});
|
method: 'DELETE'
|
||||||
if (!response.ok) throw new Error('Failed to update entry');
|
});
|
||||||
};
|
if (!response.ok) throw new Error('Failed to delete entry');
|
||||||
|
};
|
||||||
const handleDeleteEntry = async (id) => {
|
|
||||||
const response = await fetch(`/api/memory/entry/${id}`, {
|
const handleResetEntry = async (id) => {
|
||||||
method: 'DELETE'
|
const response = await fetch(`/api/memory/entry/${id}/reset`, {
|
||||||
});
|
method: 'POST'
|
||||||
if (!response.ok) throw new Error('Failed to delete entry');
|
});
|
||||||
};
|
if (!response.ok) throw new Error('Failed to reset entry');
|
||||||
|
};
|
||||||
const handleResetEntry = async (id) => {
|
|
||||||
const response = await fetch(`/api/memory/entry/${id}/reset`, {
|
const handleUpdateEntry = async (id) => {
|
||||||
method: 'POST'
|
const response = await fetch(`/api/memory/entry/${id}/update`, {
|
||||||
});
|
method: 'POST'
|
||||||
if (!response.ok) throw new Error('Failed to reset entry');
|
});
|
||||||
};
|
if (!response.ok) throw new Error('Failed to update entry');
|
||||||
|
};
|
||||||
const handleUpdateEntry = async (id) => {
|
|
||||||
const response = await fetch(`/api/memory/entry/${id}/update`, {
|
// Determine active LLM's current state for UI feedback
|
||||||
method: 'POST'
|
const activeLlmState = activeLlm ? llms[activeLlm] : null;
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to update entry');
|
// Track overall WebSocket connection state
|
||||||
};
|
useEffect(() => {
|
||||||
|
if (llmWs.wsState === WebSocketState.CONNECTED &&
|
||||||
// Update active tab on state change
|
contextWs.wsState === WebSocketState.CONNECTED &&
|
||||||
useEffect(() => {
|
responseWs.wsState === WebSocketState.CONNECTED &&
|
||||||
if (activeTab === Tabs.MEMORY) return;
|
stdoutWs.wsState === WebSocketState.CONNECTED &&
|
||||||
const state = llms[activeLlm];
|
autoApproverWs.wsState === WebSocketState.CONNECTED &&
|
||||||
switch (state) {
|
memoryWs.wsState === WebSocketState.CONNECTED) {
|
||||||
case null:
|
setWsState(WebSocketState.CONNECTED);
|
||||||
break;
|
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
|
||||||
case LlmState.NO_OUTPUT:
|
contextWs.wsState === WebSocketState.CONNECTING ||
|
||||||
setActiveTab(Tabs.CONTEXT);
|
responseWs.wsState === WebSocketState.CONNECTING ||
|
||||||
setShowDiff(false);
|
stdoutWs.wsState === WebSocketState.CONNECTING ||
|
||||||
setShowSidebar(false);
|
autoApproverWs.wsState === WebSocketState.CONNECTING ||
|
||||||
break;
|
memoryWs.wsState === WebSocketState.CONNECTING) {
|
||||||
default:
|
setWsState(WebSocketState.CONNECTING);
|
||||||
setActiveTab(Tabs.RESPONSE);
|
} else {
|
||||||
setShowDiff(false);
|
setWsState(WebSocketState.DISCONNECTED);
|
||||||
setShowSidebar(false);
|
}
|
||||||
break;
|
}, [llmWs.wsState, contextWs.wsState, responseWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
|
||||||
}
|
|
||||||
}, [llms[activeLlm]]);
|
return (
|
||||||
|
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
|
||||||
useEffect(() => {
|
<Header
|
||||||
if (llmWs.wsState === WebSocketState.CONNECTED &&
|
wsState={wsState}
|
||||||
contextWs.wsState === WebSocketState.CONNECTED &&
|
agentState={activeLlmState}
|
||||||
tokenWs.wsState === WebSocketState.CONNECTED &&
|
activeTab={activeTab}
|
||||||
stdoutWs.wsState === WebSocketState.CONNECTED &&
|
setActiveTab={setActiveTab}
|
||||||
autoApproverWs.wsState === WebSocketState.CONNECTED &&
|
/>
|
||||||
memoryWs.wsState === WebSocketState.CONNECTED) {
|
|
||||||
setWsState(WebSocketState.CONNECTED);
|
<div className="flex flex-1 overflow-hidden">
|
||||||
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
|
<main className="flex-1 p-4 overflow-auto">
|
||||||
contextWs.wsState === WebSocketState.CONNECTING ||
|
{activeTab === Tabs.CONTEXT && (
|
||||||
tokenWs.wsState === WebSocketState.CONNECTING ||
|
<ContentEditor
|
||||||
stdoutWs.wsState === WebSocketState.CONNECTING ||
|
showDiff={showDiff}
|
||||||
autoApproverWs.wsState === WebSocketState.CONNECTING ||
|
originalContent={generatedContext}
|
||||||
memoryWs.wsState === WebSocketState.CONNECTING) {
|
modifiedContent={modifiedContext}
|
||||||
setWsState(WebSocketState.CONNECTING);
|
onChange={handleContextEdit}
|
||||||
} else {
|
autoScroll={autoScroll}
|
||||||
setWsState(WebSocketState.DISCONNECTED);
|
/>
|
||||||
}
|
)}
|
||||||
}, [llmWs.wsState, contextWs.wsState, tokenWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
|
{activeTab === Tabs.RESPONSE && (
|
||||||
|
<ContentEditor
|
||||||
return (
|
showDiff={showDiff}
|
||||||
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
|
originalContent={generatedResponse}
|
||||||
<Header
|
modifiedContent={modifiedResponse}
|
||||||
wsState={wsState}
|
onChange={handleResponseEdit}
|
||||||
agentState={llms[activeLlm]}
|
/>
|
||||||
activeTab={activeTab}
|
)}
|
||||||
setActiveTab={setActiveTab}
|
{activeTab === Tabs.INPUT && (
|
||||||
/>
|
<StandardEditor
|
||||||
|
content={input}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
onChange={value => setInput(value)}
|
||||||
<main className="flex-1 p-4 overflow-auto">
|
language="plaintext"
|
||||||
{activeTab === Tabs.CONTEXT && (
|
/>
|
||||||
<ContentEditor
|
)}
|
||||||
showDiff={showDiff}
|
{activeTab === Tabs.OUTPUT && (
|
||||||
originalContent={generatedContext}
|
<StandardEditor
|
||||||
modifiedContent={modifiedContext}
|
content={output}
|
||||||
onChange={handleContextEdit}
|
readOnly={true}
|
||||||
autoScroll={autoScroll}
|
language="plaintext"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === Tabs.RESPONSE && (
|
{activeTab === Tabs.MEMORY && (
|
||||||
<ContentEditor
|
<MemoryEditor
|
||||||
showDiff={showDiff}
|
entries={entries}
|
||||||
originalContent={generatedResponses[activeLlm] || ''}
|
onCreateEntry={handleCreateEntry}
|
||||||
modifiedContent={modifiedResponses[activeLlm] || ''}
|
onSaveEntry={handleSaveEntry}
|
||||||
onChange={value => setModifiedResponses(prev => ({
|
onDeleteEntry={handleDeleteEntry}
|
||||||
...prev,
|
onResetEntry={handleResetEntry}
|
||||||
[activeLlm]: value
|
onUpdateEntry={handleUpdateEntry}
|
||||||
}))}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</main>
|
||||||
{activeTab === Tabs.INPUT && (
|
|
||||||
<StandardEditor
|
<Sidebar
|
||||||
content={input}
|
showSidebar={showSidebar}
|
||||||
onChange={value => setInput(value)}
|
llms={llms}
|
||||||
language="plaintext"
|
activeLlm={activeLlm}
|
||||||
/>
|
llmState={activeLlmState}
|
||||||
)}
|
showDiff={showDiff}
|
||||||
{activeTab === Tabs.OUTPUT && (
|
autoScroll={autoScroll}
|
||||||
<StandardEditor
|
onAutoScrollChange={setAutoScroll}
|
||||||
content={output}
|
input={input}
|
||||||
readOnly={true}
|
output={output}
|
||||||
language="plaintext"
|
autoApproverConfig={autoApproverConfig}
|
||||||
/>
|
onApprove={handleApprove}
|
||||||
)}
|
onInference={handleInference}
|
||||||
{activeTab === Tabs.MEMORY && (
|
onStop={handleStop}
|
||||||
<MemoryEditor
|
onToggleDiff={() => setShowDiff(!showDiff)}
|
||||||
entries={entries}
|
onSendInput={handleSendInput}
|
||||||
onCreateEntry={handleCreateEntry}
|
onClearOutput={handleClearOutput}
|
||||||
onSaveEntry={handleSaveEntry}
|
onLlmChange={setActiveLlm}
|
||||||
onDeleteEntry={handleDeleteEntry}
|
onNextLlm={handleNextLlm}
|
||||||
onResetEntry={handleResetEntry}
|
onAutoApproverConfigChange={handleAutoApproverConfigChange}
|
||||||
onUpdateEntry={handleUpdateEntry}
|
/>
|
||||||
/>
|
|
||||||
)}
|
<FloatingButton onClick={() => setShowSidebar(!showSidebar)} />
|
||||||
</main>
|
</div>
|
||||||
|
</div>
|
||||||
<Sidebar
|
);
|
||||||
showSidebar={showSidebar}
|
};
|
||||||
llms={llms}
|
|
||||||
activeLlm={activeLlm}
|
export default App;
|
||||||
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;
|
|
||||||
@@ -1,57 +1,58 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { WebSocketState } from '../hooks/useWebSocket';
|
import { WebSocketState } from '../hooks/useWebSocket';
|
||||||
|
|
||||||
export const Tabs = {
|
export const Tabs = {
|
||||||
CONTEXT: 'CONTEXT',
|
CONTEXT: 'CONTEXT',
|
||||||
RESPONSE: 'RESPONSE',
|
RESPONSE: 'RESPONSE',
|
||||||
INPUT: 'INPUT',
|
INPUT: 'INPUT',
|
||||||
OUTPUT: 'OUTPUT',
|
OUTPUT: 'OUTPUT',
|
||||||
MEMORY: 'MEMORY'
|
MEMORY: 'MEMORY'
|
||||||
|
};
|
||||||
};
|
|
||||||
|
export const Header = ({
|
||||||
export const Header = ({
|
wsState,
|
||||||
wsState,
|
agentState,
|
||||||
agentState,
|
activeTab,
|
||||||
activeTab,
|
setActiveTab,
|
||||||
setActiveTab,
|
}) => (
|
||||||
}) => (
|
<header className="bg-white shadow-sm">
|
||||||
<header className="bg-white shadow-sm">
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex items-center space-x-4">
|
<h1 className="text-xl font-semibold">SIA Control Interface</h1>
|
||||||
<h1 className="text-xl font-semibold">SIA Control Interface</h1>
|
</div>
|
||||||
</div>
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className={`px-3 py-1 rounded-full text-sm ${
|
||||||
<div className={`px-3 py-1 rounded-full text-sm ${
|
wsState === WebSocketState.CONNECTED
|
||||||
wsState === WebSocketState.CONNECTED
|
? 'bg-green-100 text-green-800'
|
||||||
? 'bg-green-100 text-green-800'
|
: wsState === WebSocketState.CONNECTING
|
||||||
: wsState === WebSocketState.CONNECTING
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
: 'bg-red-100 text-red-800'
|
||||||
: 'bg-red-100 text-red-800'
|
}`}>
|
||||||
}`}>
|
{wsState}
|
||||||
{wsState}
|
</div>
|
||||||
</div>
|
{agentState && (
|
||||||
<div className="px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm">
|
<div className="px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm">
|
||||||
{agentState}
|
{agentState}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t">
|
</div>
|
||||||
<div className="flex px-4">
|
<div className="border-t">
|
||||||
{Object.values(Tabs).map((tab) => (
|
<div className="flex px-4">
|
||||||
<button
|
{Object.values(Tabs).map((tab) => (
|
||||||
key={tab}
|
<button
|
||||||
onClick={() => setActiveTab(tab)}
|
key={tab}
|
||||||
className={`px-4 py-2 font-medium text-sm border-b-2 ${
|
onClick={() => setActiveTab(tab)}
|
||||||
activeTab === tab
|
className={`px-4 py-2 font-medium text-sm border-b-2 ${
|
||||||
? 'border-blue-500 text-blue-600'
|
activeTab === tab
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
? 'border-blue-500 text-blue-600'
|
||||||
}`}
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
>
|
}`}
|
||||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
>
|
||||||
</button>
|
{tab.charAt(0).toUpperCase() + tab.slice(1).toLowerCase()}
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
@@ -1,161 +1,177 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { LlmState } from '@/components/App';
|
import { LlmState } from '@/components/App';
|
||||||
|
|
||||||
export const Sidebar = ({
|
export const Sidebar = ({
|
||||||
showSidebar,
|
showSidebar,
|
||||||
llms,
|
llms,
|
||||||
activeLlm,
|
activeLlm,
|
||||||
llmState,
|
llmState,
|
||||||
showDiff,
|
showDiff,
|
||||||
autoScroll,
|
autoScroll,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
autoApproverConfig,
|
autoApproverConfig,
|
||||||
onApprove,
|
onApprove,
|
||||||
onStop,
|
onInference,
|
||||||
onToggleDiff,
|
onStop,
|
||||||
onSendInput,
|
onToggleDiff,
|
||||||
onClearOutput,
|
onSendInput,
|
||||||
onLlmChange,
|
onClearOutput,
|
||||||
onNextLlm,
|
onLlmChange,
|
||||||
onAutoScrollChange,
|
onNextLlm,
|
||||||
onAutoApproverConfigChange,
|
onAutoScrollChange,
|
||||||
}) => (
|
onAutoApproverConfigChange,
|
||||||
<aside className={`
|
}) => (
|
||||||
fixed top-0 right-0 h-screen w-64 bg-white shadow-lg
|
<aside className={`
|
||||||
transition-transform duration-200 ease-in-out z-50
|
fixed top-0 right-0 h-screen w-64 bg-white shadow-lg
|
||||||
md:sticky md:h-[calc(100vh-4rem)]
|
transition-transform duration-200 ease-in-out z-50
|
||||||
${showSidebar ? 'translate-x-0' : 'translate-x-full md:translate-x-0'}
|
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}>
|
<div className="p-4 space-y-4 overflow-y-auto h-full">
|
||||||
<SelectTrigger>
|
<Select value={activeLlm} onValueChange={onLlmChange}>
|
||||||
<SelectValue placeholder="Select LLM" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select LLM" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{Object.keys(llms).map(llm => (
|
<SelectContent>
|
||||||
<SelectItem key={llm} value={llm}>
|
{Object.keys(llms).map(llm => (
|
||||||
{llm} ({llms[llm]})
|
<SelectItem key={llm} value={llm}>
|
||||||
</SelectItem>
|
{llm} ({llms[llm]})
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<Button variant="outline" onClick={onNextLlm} className="w-full">
|
|
||||||
Next LLM
|
<Button variant="outline" onClick={onNextLlm} className="w-full">
|
||||||
</Button>
|
Next LLM
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={onToggleDiff} className="w-full">
|
|
||||||
{showDiff ? 'Hide Diff' : 'Show Diff'}
|
<Button variant="outline" onClick={onToggleDiff} className="w-full">
|
||||||
</Button>
|
{showDiff ? 'Hide Diff' : 'Show Diff'}
|
||||||
|
</Button>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label>Auto-Scroll</label>
|
<div className="flex items-center justify-between">
|
||||||
<Switch
|
<label>Auto-Scroll</label>
|
||||||
checked={autoScroll}
|
<Switch
|
||||||
onCheckedChange={onAutoScrollChange}
|
checked={autoScroll}
|
||||||
/>
|
onCheckedChange={onAutoScrollChange}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
|
||||||
onClick={llmState === LlmState.INFERENCE ? onStop : onApprove}
|
{llmState === LlmState.INFERENCE ? (
|
||||||
className="w-full"
|
<Button
|
||||||
>
|
onClick={onStop}
|
||||||
{
|
className="w-full"
|
||||||
llmState === LlmState.NO_OUTPUT ? 'Inference' :
|
>
|
||||||
llmState === LlmState.INFERENCE ? 'Stop' :
|
Stop
|
||||||
'Approve'
|
</Button>
|
||||||
}
|
) : (
|
||||||
</Button>
|
<Button
|
||||||
|
onClick={onInference}
|
||||||
<Button onClick={onSendInput} disabled={!input.trim()} className="w-full">
|
className="w-full"
|
||||||
Send Input
|
>
|
||||||
</Button>
|
Start Inference
|
||||||
|
</Button>
|
||||||
<Button onClick={onClearOutput} disabled={!output.trim()} className="w-full">
|
)}
|
||||||
Clear Output
|
|
||||||
</Button>
|
<Button
|
||||||
|
onClick={onApprove}
|
||||||
{/* Auto Approver Config */}
|
className="w-full"
|
||||||
<div className="border-t pt-4">
|
>
|
||||||
<h3 className="font-medium mb-2">Auto Approver</h3>
|
Approve Response
|
||||||
|
</Button>
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
<Button onClick={onSendInput} disabled={!input.trim()} className="w-full">
|
||||||
<label>Context Auto-Approve</label>
|
Send Input
|
||||||
<Switch
|
</Button>
|
||||||
checked={autoApproverConfig.context_enabled}
|
|
||||||
onCheckedChange={(enabled) => onAutoApproverConfigChange({
|
<Button onClick={onClearOutput} disabled={!output.trim()} className="w-full">
|
||||||
...autoApproverConfig,
|
Clear Output
|
||||||
context_enabled: enabled
|
</Button>
|
||||||
})}
|
|
||||||
/>
|
{/* Auto Approver Config */}
|
||||||
</div>
|
<div className="border-t pt-4">
|
||||||
|
<h3 className="font-medium mb-2">Auto Approver</h3>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label>Response Auto-Approve</label>
|
<div className="space-y-2">
|
||||||
<Switch
|
<div className="flex items-center justify-between">
|
||||||
checked={autoApproverConfig.response_enabled}
|
<label>Context Auto-Approve</label>
|
||||||
onCheckedChange={(enabled) => onAutoApproverConfigChange({
|
<Switch
|
||||||
...autoApproverConfig,
|
checked={autoApproverConfig.context_enabled}
|
||||||
response_enabled: enabled
|
onCheckedChange={(enabled) => onAutoApproverConfigChange({
|
||||||
})}
|
...autoApproverConfig,
|
||||||
/>
|
context_enabled: enabled
|
||||||
</div>
|
})}
|
||||||
|
/>
|
||||||
<div className="space-y-1">
|
</div>
|
||||||
<label className="text-sm">Context Timeout (s)</label>
|
|
||||||
<Input
|
<div className="flex items-center justify-between">
|
||||||
type="number"
|
<label>Response Auto-Approve</label>
|
||||||
min="0"
|
<Switch
|
||||||
step="0.1"
|
checked={autoApproverConfig.response_enabled}
|
||||||
value={autoApproverConfig.context_timeout}
|
onCheckedChange={(enabled) => onAutoApproverConfigChange({
|
||||||
onChange={(e) => onAutoApproverConfigChange({
|
...autoApproverConfig,
|
||||||
...autoApproverConfig,
|
response_enabled: enabled
|
||||||
context_timeout: parseFloat(e.target.value)
|
})}
|
||||||
})}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<label className="text-sm">Context Timeout (s)</label>
|
||||||
<label className="text-sm">Response Timeout (s)</label>
|
<Input
|
||||||
<Input
|
type="number"
|
||||||
type="number"
|
min="0"
|
||||||
min="0"
|
step="0.1"
|
||||||
step="0.1"
|
value={autoApproverConfig.context_timeout}
|
||||||
value={autoApproverConfig.response_timeout}
|
onChange={(e) => onAutoApproverConfigChange({
|
||||||
onChange={(e) => onAutoApproverConfigChange({
|
...autoApproverConfig,
|
||||||
...autoApproverConfig,
|
context_timeout: parseFloat(e.target.value)
|
||||||
response_timeout: parseFloat(e.target.value)
|
})}
|
||||||
})}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<label className="text-sm">Response Timeout (s)</label>
|
||||||
<label className="text-sm">Auto-Approve LLM</label>
|
<Input
|
||||||
<Select
|
type="number"
|
||||||
value={autoApproverConfig.llm_name}
|
min="0"
|
||||||
onValueChange={(llm) => onAutoApproverConfigChange({
|
step="0.1"
|
||||||
...autoApproverConfig,
|
value={autoApproverConfig.response_timeout}
|
||||||
llm_name: llm
|
onChange={(e) => onAutoApproverConfigChange({
|
||||||
})}
|
...autoApproverConfig,
|
||||||
>
|
response_timeout: parseFloat(e.target.value)
|
||||||
<SelectTrigger>
|
})}
|
||||||
<SelectValue placeholder="Select LLM" />
|
/>
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent>
|
|
||||||
{Object.keys(llms).map(llm => (
|
<div className="space-y-1">
|
||||||
<SelectItem key={llm} value={llm}>{llm}</SelectItem>
|
<label className="text-sm">LLM for Inference</label>
|
||||||
))}
|
<Select
|
||||||
</SelectContent>
|
value={autoApproverConfig.llm_name}
|
||||||
</Select>
|
onValueChange={(llm) => onAutoApproverConfigChange({
|
||||||
</div>
|
...autoApproverConfig,
|
||||||
</div>
|
llm_name: llm
|
||||||
</div>
|
})}
|
||||||
</div>
|
>
|
||||||
</aside>
|
<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>
|
||||||
|
);
|
||||||
@@ -1,37 +1,37 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DiffEditorWrapper } from './DiffEditorWrapper';
|
import { DiffEditorWrapper } from './DiffEditorWrapper';
|
||||||
import { StandardEditor } from './StandardEditor';
|
import { StandardEditor } from './StandardEditor';
|
||||||
|
|
||||||
export const ContentEditor = ({
|
export const ContentEditor = ({
|
||||||
showDiff,
|
showDiff,
|
||||||
originalContent,
|
originalContent,
|
||||||
modifiedContent,
|
modifiedContent,
|
||||||
onChange,
|
onChange,
|
||||||
validationError = null,
|
validationError = null,
|
||||||
language = 'xml',
|
language = 'xml',
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (showDiff) {
|
if (showDiff) {
|
||||||
return (
|
return (
|
||||||
<DiffEditorWrapper
|
<DiffEditorWrapper
|
||||||
originalContent={originalContent}
|
originalContent={originalContent}
|
||||||
modifiedContent={modifiedContent}
|
modifiedContent={modifiedContent}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
}}
|
}}
|
||||||
language={language}
|
language={language}
|
||||||
validationError={validationError}
|
validationError={validationError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StandardEditor
|
<StandardEditor
|
||||||
content={modifiedContent}
|
content={modifiedContent}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
language={language}
|
language={language}
|
||||||
validationError={validationError}
|
validationError={validationError}
|
||||||
autoScroll={autoScroll}
|
autoScroll={autoScroll}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DiffEditor } from '@monaco-editor/react';
|
import { DiffEditor } from '@monaco-editor/react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
||||||
export const DiffEditorWrapper = ({
|
export const DiffEditorWrapper = ({
|
||||||
originalContent,
|
originalContent,
|
||||||
modifiedContent,
|
modifiedContent,
|
||||||
onChange,
|
onChange,
|
||||||
language = 'xml',
|
language = 'xml',
|
||||||
validationError = null,
|
validationError = null,
|
||||||
}) => {
|
}) => {
|
||||||
const handleMount = (editor, monaco) => {
|
const handleMount = (editor, monaco) => {
|
||||||
const modified = editor.getModifiedEditor();
|
const modified = editor.getModifiedEditor();
|
||||||
modified.onDidChangeModelContent(() => {
|
modified.onDidChangeModelContent(() => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(modified.getValue());
|
onChange(modified.getValue());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Card className="h-[calc(100vh-8rem)]">
|
<Card className="h-[calc(100vh-8rem)]">
|
||||||
<CardContent className="p-0 h-full">
|
<CardContent className="p-0 h-full">
|
||||||
{validationError && (
|
{validationError && (
|
||||||
<Alert variant="destructive" className="m-4">
|
<Alert variant="destructive" className="m-4">
|
||||||
<AlertDescription>{validationError}</AlertDescription>
|
<AlertDescription>{validationError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<DiffEditor
|
<DiffEditor
|
||||||
height="100%"
|
height="100%"
|
||||||
original={originalContent}
|
original={originalContent}
|
||||||
modified={modifiedContent}
|
modified={modifiedContent}
|
||||||
language={language}
|
language={language}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
options={{
|
options={{
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
originalEditable: false,
|
originalEditable: false,
|
||||||
modifiedEditable: true,
|
modifiedEditable: true,
|
||||||
minimap: { enabled: true },
|
minimap: { enabled: true },
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
renderSideBySide: true,
|
renderSideBySide: true,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
diffAlgorithm: 'advanced',
|
diffAlgorithm: 'advanced',
|
||||||
diffWordWrap: 'off',
|
diffWordWrap: 'off',
|
||||||
originalEditor: {
|
originalEditor: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
},
|
},
|
||||||
modifiedEditor: {
|
modifiedEditor: {
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -1,217 +1,217 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BackgroundEntryEditor } from './BackgroundEntryEditor';
|
import { BackgroundEntryEditor } from './BackgroundEntryEditor';
|
||||||
import { ParseErrorEntryEditor } from './ParseErrorEntryEditor';
|
import { ParseErrorEntryEditor } from './ParseErrorEntryEditor';
|
||||||
import { ReadEntryEditor } from './ReadEntryEditor';
|
import { ReadEntryEditor } from './ReadEntryEditor';
|
||||||
import { ReasoningEntryEditor } from './ReasoningEntryEditor';
|
import { ReasoningEntryEditor } from './ReasoningEntryEditor';
|
||||||
import { RepeatEntryEditor } from './RepeatEntryEditor';
|
import { RepeatEntryEditor } from './RepeatEntryEditor';
|
||||||
import { SingleEntryEditor } from './SingleEntryEditor';
|
import { SingleEntryEditor } from './SingleEntryEditor';
|
||||||
import { WriteEntryEditor } from './WriteEntryEditor';
|
import { WriteEntryEditor } from './WriteEntryEditor';
|
||||||
|
|
||||||
const EntryTypes = {
|
const EntryTypes = {
|
||||||
BACKGROUND: 'background',
|
BACKGROUND: 'background',
|
||||||
PARSE_ERROR: 'parse_error',
|
PARSE_ERROR: 'parse_error',
|
||||||
READ_STDIN: 'read_stdin',
|
READ_STDIN: 'read_stdin',
|
||||||
REASONING: 'reasoning',
|
REASONING: 'reasoning',
|
||||||
REPEAT: 'repeat',
|
REPEAT: 'repeat',
|
||||||
SINGLE: 'single',
|
SINGLE: 'single',
|
||||||
WRITE: 'write'
|
WRITE: 'write'
|
||||||
};
|
};
|
||||||
|
|
||||||
const EntryEditors = {
|
const EntryEditors = {
|
||||||
[EntryTypes.BACKGROUND]: BackgroundEntryEditor,
|
[EntryTypes.BACKGROUND]: BackgroundEntryEditor,
|
||||||
[EntryTypes.PARSE_ERROR]: ParseErrorEntryEditor,
|
[EntryTypes.PARSE_ERROR]: ParseErrorEntryEditor,
|
||||||
[EntryTypes.READ_STDIN]: ReadEntryEditor,
|
[EntryTypes.READ_STDIN]: ReadEntryEditor,
|
||||||
[EntryTypes.REASONING]: ReasoningEntryEditor,
|
[EntryTypes.REASONING]: ReasoningEntryEditor,
|
||||||
[EntryTypes.REPEAT]: RepeatEntryEditor,
|
[EntryTypes.REPEAT]: RepeatEntryEditor,
|
||||||
[EntryTypes.SINGLE]: SingleEntryEditor,
|
[EntryTypes.SINGLE]: SingleEntryEditor,
|
||||||
[EntryTypes.WRITE]: WriteEntryEditor
|
[EntryTypes.WRITE]: WriteEntryEditor
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MemoryEditor = ({
|
export const MemoryEditor = ({
|
||||||
entries = [],
|
entries = [],
|
||||||
onCreateEntry,
|
onCreateEntry,
|
||||||
onSaveEntry,
|
onSaveEntry,
|
||||||
onDeleteEntry,
|
onDeleteEntry,
|
||||||
onResetEntry,
|
onResetEntry,
|
||||||
onUpdateEntry
|
onUpdateEntry
|
||||||
}) => {
|
}) => {
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
const [selectedType, setSelectedType] = useState(EntryTypes.SINGLE);
|
const [selectedType, setSelectedType] = useState(EntryTypes.SINGLE);
|
||||||
|
|
||||||
const handleCreate = (_id, entry) => {
|
const handleCreate = (_id, entry) => {
|
||||||
onCreateEntry(entry);
|
onCreateEntry(entry);
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadIteration = async (event) => {
|
const handleLoadIteration = async (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
const response = await fetch('/api/memory/load_iteration', {
|
const response = await fetch('/api/memory/load_iteration', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content: content })
|
body: JSON.stringify({ content: content })
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to load iteration');
|
if (!response.ok) throw new Error('Failed to load iteration');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DOMException) {
|
if (error instanceof DOMException) {
|
||||||
alert('Failed to read the file. Check permissions. chown -R $USER:$USER iteration');
|
alert('Failed to read the file. Check permissions. chown -R $USER:$USER iteration');
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadIterationClick = () => {
|
const handleLoadIterationClick = () => {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.xml';
|
input.accept = '.xml';
|
||||||
input.onchange = handleLoadIteration;
|
input.onchange = handleLoadIteration;
|
||||||
input.click();
|
input.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateId = () => {
|
const generateId = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
|
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
|
||||||
return utc.getFullYear() +
|
return utc.getFullYear() +
|
||||||
String(utc.getMonth() + 1).padStart(2, '0') +
|
String(utc.getMonth() + 1).padStart(2, '0') +
|
||||||
String(utc.getDate()).padStart(2, '0') + '_' +
|
String(utc.getDate()).padStart(2, '0') + '_' +
|
||||||
String(utc.getHours()).padStart(2, '0') +
|
String(utc.getHours()).padStart(2, '0') +
|
||||||
String(utc.getMinutes()).padStart(2, '0') +
|
String(utc.getMinutes()).padStart(2, '0') +
|
||||||
String(utc.getSeconds()).padStart(2, '0') + '_' +
|
String(utc.getSeconds()).padStart(2, '0') + '_' +
|
||||||
String(utc.getMilliseconds()).padStart(3, '0');
|
String(utc.getMilliseconds()).padStart(3, '0');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialEntry = (type) => {
|
const getInitialEntry = (type) => {
|
||||||
const baseEntry = {
|
const baseEntry = {
|
||||||
type: type,
|
type: type,
|
||||||
id: generateId()
|
id: generateId()
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case EntryTypes.BACKGROUND:
|
case EntryTypes.BACKGROUND:
|
||||||
case EntryTypes.REPEAT:
|
case EntryTypes.REPEAT:
|
||||||
case EntryTypes.SINGLE:
|
case EntryTypes.SINGLE:
|
||||||
return {
|
return {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
script: '',
|
script: '',
|
||||||
timeout: null,
|
timeout: null,
|
||||||
limit: null
|
limit: null
|
||||||
};
|
};
|
||||||
case EntryTypes.PARSE_ERROR:
|
case EntryTypes.PARSE_ERROR:
|
||||||
return {
|
return {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
content: '',
|
content: '',
|
||||||
error: ''
|
error: ''
|
||||||
};
|
};
|
||||||
case EntryTypes.READ_STDIN:
|
case EntryTypes.READ_STDIN:
|
||||||
return {
|
return {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
content: '',
|
content: '',
|
||||||
read: false
|
read: false
|
||||||
};
|
};
|
||||||
case EntryTypes.REASONING:
|
case EntryTypes.REASONING:
|
||||||
return {
|
return {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
content: ''
|
content: ''
|
||||||
};
|
};
|
||||||
case EntryTypes.WRITE:
|
case EntryTypes.WRITE:
|
||||||
return {
|
return {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
content: '',
|
content: '',
|
||||||
written: false
|
written: false
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return baseEntry;
|
return baseEntry;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCreateDialog = () => {
|
const renderCreateDialog = () => {
|
||||||
const EditorComponent = EntryEditors[selectedType];
|
const EditorComponent = EntryEditors[selectedType];
|
||||||
const entry = getInitialEntry(selectedType);
|
const entry = getInitialEntry(selectedType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[100]">
|
<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="bg-white p-4 rounded-lg max-w-2xl w-full m-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-lg font-medium">Create New Entry</h2>
|
<h2 className="text-lg font-medium">Create New Entry</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateDialog(false)}
|
onClick={() => setShowCreateDialog(false)}
|
||||||
className="text-gray-500 hover:text-gray-700"
|
className="text-gray-500 hover:text-gray-700"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700">Entry Type</label>
|
<label className="block text-sm font-medium text-gray-700">Entry Type</label>
|
||||||
<select
|
<select
|
||||||
value={selectedType}
|
value={selectedType}
|
||||||
onChange={(e) => setSelectedType(e.target.value)}
|
onChange={(e) => setSelectedType(e.target.value)}
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
|
||||||
>
|
>
|
||||||
{Object.entries(EntryTypes).map(([key, value]) => (
|
{Object.entries(EntryTypes).map(([key, value]) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value}>
|
||||||
{key.toLowerCase().replace('_', ' ')}
|
{key.toLowerCase().replace('_', ' ')}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<EditorComponent
|
<EditorComponent
|
||||||
entry={entry}
|
entry={entry}
|
||||||
isNew={true}
|
isNew={true}
|
||||||
onSave={handleCreate}
|
onSave={handleCreate}
|
||||||
onCancel={() => setShowCreateDialog(false)}
|
onCancel={() => setShowCreateDialog(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderEntry = (entry) => {
|
const renderEntry = (entry) => {
|
||||||
const EditorComponent = EntryEditors[entry.type];
|
const EditorComponent = EntryEditors[entry.type];
|
||||||
if (!EditorComponent) {
|
if (!EditorComponent) {
|
||||||
console.warn(`Unknown entry type: ${entry.type}`);
|
console.warn(`Unknown entry type: ${entry.type}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorComponent
|
<EditorComponent
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
onSave={onSaveEntry}
|
onSave={onSaveEntry}
|
||||||
onDelete={onDeleteEntry}
|
onDelete={onDeleteEntry}
|
||||||
onReset={onResetEntry}
|
onReset={onResetEntry}
|
||||||
onUpdate={onUpdateEntry}
|
onUpdate={onUpdateEntry}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex justify-end mb-4 gap-2">
|
<div className="flex justify-end mb-4 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleLoadIterationClick}
|
onClick={handleLoadIterationClick}
|
||||||
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
|
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
|
||||||
>
|
>
|
||||||
Load Iteration
|
Load Iteration
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateDialog(true)}
|
onClick={() => setShowCreateDialog(true)}
|
||||||
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
|
className="px-4 py-2 text-sm font-medium bg-white hover:bg-gray-100 border border-gray-200 rounded-md"
|
||||||
>
|
>
|
||||||
Add Entry
|
Add Entry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{entries.map(renderEntry)}
|
{entries.map(renderEntry)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreateDialog && renderCreateDialog()}
|
{showCreateDialog && renderCreateDialog()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MemoryEditor;
|
export default MemoryEditor;
|
||||||
@@ -1,61 +1,61 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
||||||
export const StandardEditor = ({
|
export const StandardEditor = ({
|
||||||
content,
|
content,
|
||||||
onChange,
|
onChange,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
language = 'xml',
|
language = 'xml',
|
||||||
validationError = null,
|
validationError = null,
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
}) => {
|
}) => {
|
||||||
const editorRef = React.useRef(null);
|
const editorRef = React.useRef(null);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (editorRef.current && autoScroll) {
|
if (editorRef.current && autoScroll) {
|
||||||
const model = editorRef.current.getModel();
|
const model = editorRef.current.getModel();
|
||||||
const lineCount = model.getLineCount();
|
const lineCount = model.getLineCount();
|
||||||
editorRef.current.revealLine(lineCount, monaco.editor.ScrollType.Smooth);
|
editorRef.current.revealLine(lineCount, monaco.editor.ScrollType.Smooth);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditorDidMount = (editor) => {
|
const handleEditorDidMount = (editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [content, autoScroll]);
|
}, [content, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-[calc(100vh-8rem)]">
|
<Card className="h-[calc(100vh-8rem)]">
|
||||||
<CardContent className="p-0 h-full">
|
<CardContent className="p-0 h-full">
|
||||||
{validationError && (
|
{validationError && (
|
||||||
<Alert variant="destructive" className="m-4">
|
<Alert variant="destructive" className="m-4">
|
||||||
<AlertDescription>{validationError}</AlertDescription>
|
<AlertDescription>{validationError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
language={language}
|
language={language}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
readOnly,
|
readOnly,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,68 +1,68 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
export const WebSocketState = {
|
export const WebSocketState = {
|
||||||
CONNECTING: 'CONNECTING',
|
CONNECTING: 'CONNECTING',
|
||||||
CONNECTED: 'CONNECTED',
|
CONNECTED: 'CONNECTED',
|
||||||
DISCONNECTED: 'DISCONNECTED',
|
DISCONNECTED: 'DISCONNECTED',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWebSocket = (url) => {
|
export const useWebSocket = (url) => {
|
||||||
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
|
||||||
const wsRef = useRef(null);
|
const wsRef = useRef(null);
|
||||||
const handlersRef = useRef([]);
|
const handlersRef = useRef([]);
|
||||||
|
|
||||||
const connect = useCallback(() => {
|
const connect = useCallback(() => {
|
||||||
setWsState(WebSocketState.CONNECTING);
|
setWsState(WebSocketState.CONNECTING);
|
||||||
const ws = new WebSocket(url);
|
const ws = new WebSocket(url);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
setWsState(WebSocketState.CONNECTED);
|
setWsState(WebSocketState.CONNECTED);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setWsState(WebSocketState.DISCONNECTED);
|
setWsState(WebSocketState.DISCONNECTED);
|
||||||
setTimeout(connect, 2000);
|
setTimeout(connect, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
handlersRef.current.forEach(handler => handler(message));
|
handlersRef.current.forEach(handler => handler(message));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling WebSocket message:', error);
|
console.error('Error handling WebSocket message:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return ws;
|
return ws;
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ws = connect();
|
const ws = connect();
|
||||||
return () => {
|
return () => {
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
};
|
};
|
||||||
}, [connect]);
|
}, [connect]);
|
||||||
|
|
||||||
const sendMessage = useCallback((data) => {
|
const sendMessage = useCallback((data) => {
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
wsRef.current.send(JSON.stringify(data));
|
wsRef.current.send(JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
console.error('WebSocket not connected, state:', wsRef.current?.readyState);
|
console.error('WebSocket not connected, state:', wsRef.current?.readyState);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addMessageHandler = useCallback((handler) => {
|
const addMessageHandler = useCallback((handler) => {
|
||||||
handlersRef.current.push(handler);
|
handlersRef.current.push(handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wsState,
|
wsState,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
addMessageHandler
|
addMessageHandler
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user