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