Major UI update

This commit is contained in:
Niels Geens
2024-11-06 16:12:50 +01:00
parent 70ed16f8ab
commit 6e78a0f0bd
16 changed files with 594 additions and 358 deletions

View File

@@ -1,16 +1,41 @@
#!/bin/bash #!/bin/bash
# Parse command line arguments
target_dir="." # Default to current directory
while [[ $# -gt 0 ]]; do
case $1 in
-d|--directory)
if [ -n "$2" ] && [ -d "$2" ]; then
target_dir="$2"
shift 2
else
echo "Error: Directory '$2' does not exist or is not specified" >&2
exit 1
fi
;;
*)
echo "Usage: $0 [-d|--directory <path>]" >&2
exit 1
;;
esac
done
# Change to target directory while storing original directory
original_dir=$(pwd)
cd "$target_dir" || exit 1
# Clear/create output file # Clear/create output file
> claude.txt output_file="$original_dir/claude.txt"
> "$output_file"
# Generate and add directory tree # Generate and add directory tree
echo "Directory Tree:" > claude.txt echo "Directory Tree:" > "$output_file"
echo "=============" >> claude.txt echo "=============" >> "$output_file"
# Use tree with gitignore patterns # Use tree with gitignore patterns
tree -I "$(git check-ignore * .*)" >> claude.txt tree -I "$(git check-ignore * .*)" >> "$output_file"
echo -e "\nFile Contents:" >> claude.txt echo -e "\nFile Contents:" >> "$output_file"
echo "=============" >> claude.txt echo "=============" >> "$output_file"
# Use git ls-files to get tracked files and untracked files that aren't ignored # Use git ls-files to get tracked files and untracked files that aren't ignored
# The --exclude-standard flag makes git ls-files respect .gitignore # The --exclude-standard flag makes git ls-files respect .gitignore
@@ -22,7 +47,6 @@ echo "=============" >> claude.txt
if [ "$file" = "claude.txt" ]; then if [ "$file" = "claude.txt" ]; then
continue continue
fi fi
# Skip non-existent files # Skip non-existent files
if [ ! -f "$file" ]; then if [ ! -f "$file" ]; then
continue continue
@@ -34,9 +58,12 @@ echo "=============" >> claude.txt
continue continue
fi fi
echo -e "\n=== File: $file ===" >> claude.txt echo -e "\n=== File: $file ===" >> "$output_file"
echo -e "------------------------" >> claude.txt echo -e "------------------------" >> "$output_file"
cat "$file" >> claude.txt cat "$file" >> "$output_file"
done done
# Return to original directory
cd "$original_dir" || exit 1
echo "Concatenation complete. Output written to claude.txt" echo "Concatenation complete. Output written to claude.txt"

View File

@@ -70,7 +70,8 @@ class Main:
self._app = web.Application() self._app = web.Application()
self._init_routes() self._init_routes()
async def app(self): @property
def app(self):
return self._app return self._app
def _init_routes(self): def _init_routes(self):
@@ -114,6 +115,5 @@ class Main:
if __name__ == "__main__": if __name__ == "__main__":
main = Main() main = Main()
app = asyncio.run(main.app())
print("Web server started at http://localhost:8080") print("Web server started at http://localhost:8080")
web.run_app(app, port=8080) web.run_app(main.app, port=8080)

View File

@@ -1,372 +1,141 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import Editor from '@monaco-editor/react'; import { Header } from '@/components/Header';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Sidebar } from '@/components/Sidebar';
import { Button } from '@/components/ui/button'; import { FloatingButton } from '@/components/FloatingButton';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ContentEditor } from '@/components/editors/ContentEditor';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { StandardEditor } from '@/components/editors/StandardEditor';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useWebSocket } from '@/hooks/useWebSocket';
import { AgentState, Tabs, ClientMessageType, ServerMessageType } from '@/constants';
const WebSocketState = {
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
};
const AgentState = { const App = () => {
UPDATE: 'UPDATE',
CONTEXT_APPROVAL: 'CONTEXT_APPROVAL',
INFERENCE: 'INFERENCE',
RESPONSE_APPROVAL: 'RESPONSE_APPROVAL',
};
const MessageType = {
STATE_CHANGE: 'STATE_CHANGE',
CONTEXT_UPDATE: 'CONTEXT_UPDATE',
RESPONSE_UPDATE: 'RESPONSE_UPDATE',
OUTPUT_UPDATE: 'OUTPUT_UPDATE',
VALIDATION_ERROR: 'VALIDATION_ERROR',
};
const ClientMessageType = {
APPROVE_CONTEXT: 'APPROVE_CONTEXT',
APPROVE_RESPONSE: 'APPROVE_RESPONSE',
MODIFY_RESPONSE: 'MODIFY_RESPONSE',
SEND_INPUT: 'SEND_INPUT',
};
const SIAInterface = () => {
// Editor content state // Editor content state
const [context, setContext] = useState(''); const [originalContext, setOriginalContext] = useState('');
const [modifiedContext, setModifiedContext] = useState('');
const [originalResponse, setOriginalResponse] = useState(''); const [originalResponse, setOriginalResponse] = useState('');
const [modifiedResponse, setModifiedResponse] = useState(''); const [modifiedResponse, setModifiedResponse] = useState('');
const [validationError, setValidationError] = useState(null);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [output, setOutput] = useState(''); const [output, setOutput] = useState('');
const [validationError, setValidationError] = useState(null);
// UI state // UI state
const [activeTab, setActiveTab] = useState('original'); const [activeTab, setActiveTab] = useState(Tabs.CONTEXT);
const [showDiff, setShowDiff] = useState(false);
const [showSidebar, setShowSidebar] = useState(false);
const [agentState, setAgentState] = useState(AgentState.CONTEXT_APPROVAL); const [agentState, setAgentState] = useState(AgentState.CONTEXT_APPROVAL);
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
// WebSocket ref const { wsState, sendMessage, setMessageHandler } = useWebSocket('ws://localhost:8080/ws');
const wsRef = useRef(null);
// Monaco editor options
const editorOptions = {
minimap: { enabled: false },
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false,
fontSize: 14,
automaticLayout: true,
};
// Set up WebSocket message handlers
useEffect(() => { useEffect(() => {
connectWebSocket(); setMessageHandler(ServerMessageType.STATE_CHANGE, (message) => {
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
const connectWebSocket = () => {
setWsState(WebSocketState.CONNECTING);
const ws = new WebSocket('ws://localhost:8080/ws');
wsRef.current = ws;
ws.onopen = () => {
setWsState(WebSocketState.CONNECTED);
};
ws.onclose = () => {
setWsState(WebSocketState.DISCONNECTED);
setTimeout(connectWebSocket, 2000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
};
const handleWebSocketMessage = (message) => {
switch (message.type) {
case MessageType.STATE_CHANGE:
setAgentState(message.state); setAgentState(message.state);
break; });
case MessageType.CONTEXT_UPDATE:
setContext(message.context); setMessageHandler(ServerMessageType.CONTEXT_UPDATE, (message) => {
break; setOriginalContext(message.context);
case MessageType.RESPONSE_UPDATE: setModifiedContext(message.context);
});
setMessageHandler(ServerMessageType.RESPONSE_UPDATE, (message) => {
setOriginalResponse(message.response); setOriginalResponse(message.response);
setModifiedResponse(message.response); setModifiedResponse(message.response);
setValidationError(message.validation_error); setValidationError(message.validation_error);
break; });
case MessageType.OUTPUT_UPDATE:
setMessageHandler(ServerMessageType.OUTPUT_UPDATE, (message) => {
setOutput(prev => prev + message.data); setOutput(prev => prev + message.data);
});
}, [setMessageHandler]);
const handleApprove = () => {
if (agentState === AgentState.CONTEXT_APPROVAL) {
sendMessage(ClientMessageType.APPROVE_CONTEXT);
} else if (agentState === AgentState.RESPONSE_APPROVAL) {
sendMessage(ClientMessageType.APPROVE_RESPONSE, modifiedResponse);
}
};
const handleSendInput = () => {
if (input.trim()) {
sendMessage(ClientMessageType.SEND_INPUT, input);
setInput('');
}
};
useEffect(() => {
switch (agentState) {
case AgentState.CONTEXT_APPROVAL:
setActiveTab(Tabs.CONTEXT);
break;
case AgentState.RESPONSE_APPROVAL:
setActiveTab(Tabs.RESPONSE);
break; break;
} }
}; }, [agentState]);
const sendWebSocketMessage = (type, data = null) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const message = {
type: type, // Ensure exact string match
data: data || undefined // Only include if not null
};
console.log('Sending message:', JSON.stringify(message)); // Log exact message being sent
wsRef.current.send(JSON.stringify(message));
} else {
console.error('WebSocket not connected, state:', wsRef.current?.readyState);
}
};
const handleSendInput = (withStdin = false) => {
if (!input.trim()) return;
sendWebSocketMessage(ClientMessageType.SEND_INPUT, input);
setInput('');
if (withStdin) {
const readStdinXml = `<read_stdin>
<!-- Content will be populated with stdin -->
</read_stdin>`;
setOriginalResponse(readStdinXml);
setModifiedResponse(readStdinXml);
setActiveTab('modified');
}
};
const handleApproveContext = () => {
console.log('Current state:', agentState);
console.log('WebSocket state:', wsRef.current?.readyState);
if (agentState === AgentState.CONTEXT_APPROVAL &&
wsRef.current?.readyState === WebSocket.OPEN) {
console.log('Sending approve context message');
sendWebSocketMessage(ClientMessageType.APPROVE_CONTEXT);
} else {
console.warn(
'Cannot approve context.',
'Agent state:', agentState,
'WebSocket state:', wsRef.current?.readyState
);
}
};
const handleApproveResponse = (modified = false) => {
if (agentState === AgentState.RESPONSE_APPROVAL) {
const response = modified ? modifiedResponse : originalResponse;
sendWebSocketMessage(ClientMessageType.APPROVE_RESPONSE, response);
}
};
const handleResetModified = () => {
setModifiedResponse(originalResponse);
};
return ( return (
<div className="min-h-screen bg-gray-100 p-8"> <div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
<div className="max-w-7xl mx-auto space-y-6"> <Header
{/* Header */} wsState={wsState}
<div className="flex items-center justify-between"> agentState={agentState}
<h1 className="text-3xl font-bold">SIA Control Interface</h1> activeTab={activeTab}
<div className="flex items-center space-x-4"> setActiveTab={setActiveTab}
<div className={`px-4 py-2 rounded-full ${
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-4 py-2 rounded-full bg-blue-100 text-blue-800">
{agentState}
</div>
</div>
</div>
{/* Context */}
<Card>
<CardHeader>
<CardTitle>Context</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64">
<Editor
height="100%"
defaultLanguage="xml"
value={context}
options={{ ...editorOptions, readOnly: true }}
/> />
</div>
<div className="mt-4">
<Button
onClick={handleApproveContext}
disabled={agentState !== AgentState.CONTEXT_APPROVAL || wsState !== WebSocketState.CONNECTED}
className="w-full"
>
Approve Context
</Button>
</div>
</CardContent>
</Card>
{/* Input/Response Tabs */} <div className="flex flex-1 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab}> <main className="flex-1 p-4 overflow-auto">
<TabsList className="grid w-full grid-cols-3"> {activeTab === Tabs.CONTEXT && (
<TabsTrigger value="original">Original Response</TabsTrigger> <ContentEditor
<TabsTrigger value="modified">Modified Response</TabsTrigger> showDiff={showDiff}
<TabsTrigger value="input">Input</TabsTrigger> originalContent={originalContext}
</TabsList> modifiedContent={modifiedContext}
onChange={value => setModifiedContext(value)}
<TabsContent value="original"> validationError={null}
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<div className="h-[200px]">
<Editor
height="100%"
defaultLanguage="xml"
value={originalResponse}
options={{ ...editorOptions, readOnly: true }}
/> />
</div>
{validationError && (
<Alert variant="destructive">
<AlertDescription>{validationError}</AlertDescription>
</Alert>
)} )}
<Button {activeTab === Tabs.RESPONSE && (
onClick={() => handleApproveResponse(false)} <ContentEditor
disabled={ showDiff={showDiff}
agentState !== AgentState.RESPONSE_APPROVAL || originalContent={originalResponse}
validationError || modifiedContent={modifiedResponse}
wsState !== WebSocketState.CONNECTED onChange={value => setModifiedResponse(value)}
} validationError={validationError}
className="w-full"
>
Approve Original Response
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="modified">
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<div className="h-[200px]">
<Editor
height="100%"
defaultLanguage="xml"
value={modifiedResponse}
onChange={setModifiedResponse}
options={editorOptions}
/> />
</div>
{validationError && (
<Alert variant="destructive">
<AlertDescription>{validationError}</AlertDescription>
</Alert>
)} )}
<div className="flex space-x-2"> {activeTab === Tabs.INPUT && (
<Button <StandardEditor
onClick={handleResetModified} content={input}
variant="secondary" onChange={value => setInput(value)}
className="flex-1" language="plaintext"
>
Reset to Original
</Button>
<Button
onClick={() => handleApproveResponse(true)}
disabled={
agentState !== AgentState.RESPONSE_APPROVAL ||
validationError ||
wsState !== WebSocketState.CONNECTED
}
className="flex-1"
>
Approve Modified Response
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="input">
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<div className="h-[200px]">
<Editor
height="100%"
defaultLanguage="plaintext"
value={input}
onChange={setInput}
options={editorOptions}
/> />
</div> )}
<div className="flex space-x-2"> {activeTab === Tabs.OUTPUT && (
<Button <StandardEditor
onClick={() => handleSendInput(false)} content={output}
disabled={wsState !== WebSocketState.CONNECTED} readOnly={true}
className="flex-1" language="plaintext"
> />
Send Input )}
</Button> </main>
<Button
onClick={() => handleSendInput(true)}
variant="secondary"
disabled={wsState !== WebSocketState.CONNECTED}
className="flex-1"
>
Send & Read Stdin
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Output Display */} <Sidebar
<Card> showSidebar={showSidebar}
<CardHeader className="flex flex-row items-center justify-between"> activeTab={activeTab}
<CardTitle>Output</CardTitle> agentState={agentState}
<Button validationError={validationError}
onClick={() => setOutput('')} showDiff={showDiff}
variant="outline" input={input}
size="sm" onApprove={handleApprove}
> onToggleDiff={() => setShowDiff(!showDiff)}
Clear Output onSendInput={handleSendInput}
</Button> onClearOutput={() => setOutput('')}
</CardHeader> />
<CardContent>
<ScrollArea className="h-32"> <FloatingButton onClick={() => setShowSidebar(!showSidebar)} />
<div className="font-mono whitespace-pre-wrap">
{output}
</div>
</ScrollArea>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );
}; };
export default SIAInterface; export default App;

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Menu } from 'lucide-react';
export const FloatingButton = ({ onClick }) => (
<Button
variant="secondary"
size="lg"
className="fixed bottom-4 right-4 rounded-full shadow-lg md:hidden z-[60]"
onClick={onClick}
>
<Menu className="h-6 w-6" />
</Button>
);

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { WebSocketState, AgentState, Tabs } from '../constants';
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>
);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { AgentState, Tabs } from '../constants';
export const Sidebar = ({
showSidebar,
agentState,
showDiff,
input,
onApprove,
onToggleDiff,
onSendInput,
onClearOutput,
}) => (
<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)] md:top-16
${showSidebar ? 'translate-x-0' : 'translate-x-full md:translate-x-0'}
`}>
<div className="p-4 space-y-4 md:pt-0">
<Button
variant="outline"
onClick={onToggleDiff}
className="w-full"
>
{showDiff ? 'Hide Diff' : 'Show Diff'}
</Button>
<Button
onClick={onApprove}
disabled={(agentState !== AgentState.CONTEXT_APPROVAL) && (agentState !== AgentState.RESPONSE_APPROVAL)}
className="w-full"
>
Approve
</Button>
<Button
onClick={onSendInput}
disabled={!input.trim()}
className="w-full"
>
Send Input
</Button>
<Button
onClick={onClearOutput}
variant="outline"
className="w-full"
>
Clear Output
</Button>
</div>
</aside>
);

View File

@@ -0,0 +1,35 @@
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 BaseEditor = ({
content,
onChange,
readOnly = false,
language = 'xml',
validationError = null,
}) => (
<Card className="h-full">
<CardContent className="p-0 h-full">
{validationError && (
<Alert variant="destructive" className="m-4">
<AlertDescription>{validationError}</AlertDescription>
</Alert>
)}
<Editor
height="100%"
defaultLanguage={language}
value={content}
onChange={onChange}
options={{
minimap: { enabled: false },
lineNumbers: 'on',
readOnly,
fontSize: 14,
automaticLayout: true,
}}
/>
</CardContent>
</Card>
);

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { DiffEditorWrapper } from './DiffEditorWrapper';
import { StandardEditor } from './StandardEditor';
export const ContentEditor = ({
showDiff,
originalContent,
modifiedContent,
onChange,
validationError = null,
language = 'xml',
}) => {
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}
/>
);
};

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { BaseEditor } from './BaseEditor';
export const ContextEditor = ({
originalContent,
modifiedContent,
showOriginal,
onChange,
}) => (
<BaseEditor
content={showOriginal ? originalContent : modifiedContent}
onChange={onChange}
readOnly={showOriginal}
/>
);

View File

@@ -0,0 +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>
)
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { BaseEditor } from './BaseEditor';
export const InputEditor = ({ content, onChange }) => (
<BaseEditor
content={content}
onChange={onChange}
language="plaintext"
/>
);

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { BaseEditor } from './BaseEditor';
export const OutputEditor = ({ content }) => (
<BaseEditor
content={content}
readOnly={true}
language="plaintext"
/>
);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { BaseEditor } from './BaseEditor';
export const ResponseEditor = ({
originalContent,
modifiedContent,
showOriginal,
onChange,
validationError,
}) => (
<BaseEditor
content={showOriginal ? originalContent : modifiedContent}
onChange={onChange}
readOnly={showOriginal}
validationError={validationError}
/>
);

View File

@@ -0,0 +1,35 @@
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,
}) => (
<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}
options={{
minimap: { enabled: false },
lineNumbers: 'on',
readOnly,
fontSize: 14,
automaticLayout: true,
}}
/>
</CardContent>
</Card>
);

34
web/src/constants.jsx Normal file
View File

@@ -0,0 +1,34 @@
export const WebSocketState = {
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
};
export const AgentState = {
UPDATE: 'UPDATE',
CONTEXT_APPROVAL: 'CONTEXT_APPROVAL',
INFERENCE: 'INFERENCE',
RESPONSE_APPROVAL: 'RESPONSE_APPROVAL',
};
export const Tabs = {
CONTEXT: 'context',
RESPONSE: 'response',
INPUT: 'input',
OUTPUT: 'output'
};
export const ClientMessageType = {
MODIFY_CONTEXT: 'MODIFY_CONTEXT',
APPROVE_CONTEXT: 'APPROVE_CONTEXT',
MODIFY_RESPONSE: 'MODIFY_RESPONSE',
APPROVE_RESPONSE: 'APPROVE_RESPONSE',
SEND_INPUT: 'SEND_INPUT',
};
export const ServerMessageType = {
STATE_CHANGE: 'STATE_CHANGE',
CONTEXT_UPDATE: 'CONTEXT_UPDATE',
RESPONSE_UPDATE: 'RESPONSE_UPDATE',
OUTPUT_UPDATE: 'OUTPUT_UPDATE',
};

View File

@@ -0,0 +1,72 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { WebSocketState, MessageType } from '../constants';
export const useWebSocket = (url) => {
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
const wsRef = useRef(null);
const messageHandlersRef = 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);
const handler = messageHandlersRef.current[message.type];
if (handler) {
handler(message);
} else {
console.warn('No handler for message type:', message.type);
}
} 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((type, data = null) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const message = {
type,
data: data || undefined
};
wsRef.current.send(JSON.stringify(message));
} else {
console.error('WebSocket not connected, state:', wsRef.current?.readyState);
}
}, []);
const setMessageHandler = useCallback((type, handler) => {
messageHandlersRef.current[type] = handler;
}, []);
return {
wsState,
sendMessage,
setMessageHandler
};
};