From 6e78a0f0bd46abb3f68526bda238e6f44d48aa0c Mon Sep 17 00:00:00 2001 From: Niels Geens Date: Wed, 6 Nov 2024 16:12:50 +0100 Subject: [PATCH] Major UI update --- claude.sh | 47 +- sia/__main__.py | 6 +- web/src/components/App.jsx | 459 +++++------------- web/src/components/FloatingButton.jsx | 14 + web/src/components/Header.jsx | 48 ++ web/src/components/Sidebar.jsx | 55 +++ web/src/components/editors/BaseEditor.jsx | 35 ++ web/src/components/editors/ContentEditor.jsx | 35 ++ web/src/components/editors/ContextEditor.jsx | 15 + .../components/editors/DiffEditorWrapper.jsx | 60 +++ web/src/components/editors/InputEditor.jsx | 10 + web/src/components/editors/OutputEditor.jsx | 10 + web/src/components/editors/ResponseEditor.jsx | 17 + web/src/components/editors/StandardEditor.jsx | 35 ++ web/src/constants.jsx | 34 ++ web/src/hooks/useWebSocket.jsx | 72 +++ 16 files changed, 594 insertions(+), 358 deletions(-) create mode 100644 web/src/components/FloatingButton.jsx create mode 100644 web/src/components/Header.jsx create mode 100644 web/src/components/Sidebar.jsx create mode 100644 web/src/components/editors/BaseEditor.jsx create mode 100644 web/src/components/editors/ContentEditor.jsx create mode 100644 web/src/components/editors/ContextEditor.jsx create mode 100644 web/src/components/editors/DiffEditorWrapper.jsx create mode 100644 web/src/components/editors/InputEditor.jsx create mode 100644 web/src/components/editors/OutputEditor.jsx create mode 100644 web/src/components/editors/ResponseEditor.jsx create mode 100644 web/src/components/editors/StandardEditor.jsx create mode 100644 web/src/constants.jsx create mode 100644 web/src/hooks/useWebSocket.jsx diff --git a/claude.sh b/claude.sh index b50a24b..596211f 100755 --- a/claude.sh +++ b/claude.sh @@ -1,16 +1,41 @@ #!/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 ]" >&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 -> claude.txt +output_file="$original_dir/claude.txt" +> "$output_file" # Generate and add directory tree -echo "Directory Tree:" > claude.txt -echo "=============" >> claude.txt +echo "Directory Tree:" > "$output_file" +echo "=============" >> "$output_file" # 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 "=============" >> claude.txt +echo -e "\nFile Contents:" >> "$output_file" +echo "=============" >> "$output_file" # 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 @@ -22,7 +47,6 @@ echo "=============" >> claude.txt if [ "$file" = "claude.txt" ]; then continue fi - # Skip non-existent files if [ ! -f "$file" ]; then continue @@ -34,9 +58,12 @@ echo "=============" >> claude.txt continue fi - echo -e "\n=== File: $file ===" >> claude.txt - echo -e "------------------------" >> claude.txt - cat "$file" >> claude.txt + echo -e "\n=== File: $file ===" >> "$output_file" + echo -e "------------------------" >> "$output_file" + cat "$file" >> "$output_file" done +# Return to original directory +cd "$original_dir" || exit 1 + echo "Concatenation complete. Output written to claude.txt" \ No newline at end of file diff --git a/sia/__main__.py b/sia/__main__.py index 9210aa5..f25cb9c 100644 --- a/sia/__main__.py +++ b/sia/__main__.py @@ -70,7 +70,8 @@ class Main: self._app = web.Application() self._init_routes() - async def app(self): + @property + def app(self): return self._app def _init_routes(self): @@ -114,6 +115,5 @@ class Main: if __name__ == "__main__": main = Main() - app = asyncio.run(main.app()) print("Web server started at http://localhost:8080") - web.run_app(app, port=8080) \ No newline at end of file + web.run_app(main.app, port=8080) \ No newline at end of file diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index d59118c..04e17eb 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -1,372 +1,141 @@ -import React, { useState, useEffect, useRef } from 'react'; -import Editor from '@monaco-editor/react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import React, { useState, useEffect } from 'react'; +import { Header } from '@/components/Header'; +import { Sidebar } from '@/components/Sidebar'; +import { FloatingButton } from '@/components/FloatingButton'; +import { ContentEditor } from '@/components/editors/ContentEditor'; +import { StandardEditor } from '@/components/editors/StandardEditor'; +import { useWebSocket } from '@/hooks/useWebSocket'; +import { AgentState, Tabs, ClientMessageType, ServerMessageType } from '@/constants'; -const WebSocketState = { - CONNECTING: 'CONNECTING', - CONNECTED: 'CONNECTED', - DISCONNECTED: 'DISCONNECTED', -}; -const AgentState = { - 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 = () => { +const App = () => { // Editor content state - const [context, setContext] = useState(''); + const [originalContext, setOriginalContext] = useState(''); + const [modifiedContext, setModifiedContext] = useState(''); const [originalResponse, setOriginalResponse] = useState(''); const [modifiedResponse, setModifiedResponse] = useState(''); + const [validationError, setValidationError] = useState(null); const [input, setInput] = useState(''); const [output, setOutput] = useState(''); - const [validationError, setValidationError] = useState(null); - + // 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 [wsState, setWsState] = useState(WebSocketState.DISCONNECTED); - // WebSocket ref - const wsRef = useRef(null); + const { wsState, sendMessage, setMessageHandler } = useWebSocket('ws://localhost:8080/ws'); - // 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(() => { + setMessageHandler(ServerMessageType.STATE_CHANGE, (message) => { + setAgentState(message.state); + }); + + setMessageHandler(ServerMessageType.CONTEXT_UPDATE, (message) => { + setOriginalContext(message.context); + setModifiedContext(message.context); + }); + + setMessageHandler(ServerMessageType.RESPONSE_UPDATE, (message) => { + setOriginalResponse(message.response); + setModifiedResponse(message.response); + setValidationError(message.validation_error); + }); + + setMessageHandler(ServerMessageType.OUTPUT_UPDATE, (message) => { + 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(() => { - connectWebSocket(); - 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); + switch (agentState) { + case AgentState.CONTEXT_APPROVAL: + setActiveTab(Tabs.CONTEXT); break; - case MessageType.CONTEXT_UPDATE: - setContext(message.context); - break; - case MessageType.RESPONSE_UPDATE: - setOriginalResponse(message.response); - setModifiedResponse(message.response); - setValidationError(message.validation_error); - break; - case MessageType.OUTPUT_UPDATE: - setOutput(prev => prev + message.data); + case AgentState.RESPONSE_APPROVAL: + setActiveTab(Tabs.RESPONSE); break; } - }; - - 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 = ` - - `; - - 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); - }; + }, [agentState]); return ( -
-
- {/* Header */} -
-

SIA Control Interface

-
-
- {wsState} -
-
- {agentState} -
-
-
+
+
- {/* Context */} - - - Context - - -
- -
-
- -
-
-
+
+
+ {activeTab === Tabs.CONTEXT && ( + setModifiedContext(value)} + validationError={null} + /> + )} + {activeTab === Tabs.RESPONSE && ( + setModifiedResponse(value)} + validationError={validationError} + /> + )} + {activeTab === Tabs.INPUT && ( + setInput(value)} + language="plaintext" + /> + )} + {activeTab === Tabs.OUTPUT && ( + + )} +
- {/* Input/Response Tabs */} - - - Original Response - Modified Response - Input - + setShowDiff(!showDiff)} + onSendInput={handleSendInput} + onClearOutput={() => setOutput('')} + /> - - - -
-
- -
- {validationError && ( - - {validationError} - - )} - -
-
-
-
- - - - -
-
- -
- {validationError && ( - - {validationError} - - )} -
- - -
-
-
-
-
- - - - - -
-
- -
-
- - -
-
-
-
-
-
- - {/* Output Display */} - - - Output - - - - -
- {output} -
-
-
-
+ setShowSidebar(!showSidebar)} />
); }; -export default SIAInterface; +export default App; \ No newline at end of file diff --git a/web/src/components/FloatingButton.jsx b/web/src/components/FloatingButton.jsx new file mode 100644 index 0000000..41a2d69 --- /dev/null +++ b/web/src/components/FloatingButton.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Menu } from 'lucide-react'; + +export const FloatingButton = ({ onClick }) => ( + +); \ No newline at end of file diff --git a/web/src/components/Header.jsx b/web/src/components/Header.jsx new file mode 100644 index 0000000..904217b --- /dev/null +++ b/web/src/components/Header.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { WebSocketState, AgentState, Tabs } from '../constants'; + +export const Header = ({ + wsState, + agentState, + activeTab, + setActiveTab, +}) => ( +
+
+
+

SIA Control Interface

+
+
+
+ {wsState} +
+
+ {agentState} +
+
+
+
+
+ {Object.values(Tabs).map((tab) => ( + + ))} +
+
+
+); \ No newline at end of file diff --git a/web/src/components/Sidebar.jsx b/web/src/components/Sidebar.jsx new file mode 100644 index 0000000..7562427 --- /dev/null +++ b/web/src/components/Sidebar.jsx @@ -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, +}) => ( + +); \ No newline at end of file diff --git a/web/src/components/editors/BaseEditor.jsx b/web/src/components/editors/BaseEditor.jsx new file mode 100644 index 0000000..96cfe02 --- /dev/null +++ b/web/src/components/editors/BaseEditor.jsx @@ -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, +}) => ( + + + {validationError && ( + + {validationError} + + )} + + + +); \ No newline at end of file diff --git a/web/src/components/editors/ContentEditor.jsx b/web/src/components/editors/ContentEditor.jsx new file mode 100644 index 0000000..abbc428 --- /dev/null +++ b/web/src/components/editors/ContentEditor.jsx @@ -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 ( + { + onChange(value); + }} + language={language} + validationError={validationError} + /> + ); + } + + return ( + + ); +}; diff --git a/web/src/components/editors/ContextEditor.jsx b/web/src/components/editors/ContextEditor.jsx new file mode 100644 index 0000000..61067bc --- /dev/null +++ b/web/src/components/editors/ContextEditor.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { BaseEditor } from './BaseEditor'; + +export const ContextEditor = ({ + originalContent, + modifiedContent, + showOriginal, + onChange, +}) => ( + +); \ No newline at end of file diff --git a/web/src/components/editors/DiffEditorWrapper.jsx b/web/src/components/editors/DiffEditorWrapper.jsx new file mode 100644 index 0000000..28dec68 --- /dev/null +++ b/web/src/components/editors/DiffEditorWrapper.jsx @@ -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 ( + + + {validationError && ( + + {validationError} + + )} + + + + ) +}; \ No newline at end of file diff --git a/web/src/components/editors/InputEditor.jsx b/web/src/components/editors/InputEditor.jsx new file mode 100644 index 0000000..c7784af --- /dev/null +++ b/web/src/components/editors/InputEditor.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseEditor } from './BaseEditor'; + +export const InputEditor = ({ content, onChange }) => ( + +); \ No newline at end of file diff --git a/web/src/components/editors/OutputEditor.jsx b/web/src/components/editors/OutputEditor.jsx new file mode 100644 index 0000000..7820598 --- /dev/null +++ b/web/src/components/editors/OutputEditor.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseEditor } from './BaseEditor'; + +export const OutputEditor = ({ content }) => ( + +); \ No newline at end of file diff --git a/web/src/components/editors/ResponseEditor.jsx b/web/src/components/editors/ResponseEditor.jsx new file mode 100644 index 0000000..22fc370 --- /dev/null +++ b/web/src/components/editors/ResponseEditor.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { BaseEditor } from './BaseEditor'; + +export const ResponseEditor = ({ + originalContent, + modifiedContent, + showOriginal, + onChange, + validationError, +}) => ( + +); \ No newline at end of file diff --git a/web/src/components/editors/StandardEditor.jsx b/web/src/components/editors/StandardEditor.jsx new file mode 100644 index 0000000..9879215 --- /dev/null +++ b/web/src/components/editors/StandardEditor.jsx @@ -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, +}) => ( + + + {validationError && ( + + {validationError} + + )} + + + +); \ No newline at end of file diff --git a/web/src/constants.jsx b/web/src/constants.jsx new file mode 100644 index 0000000..e240a9d --- /dev/null +++ b/web/src/constants.jsx @@ -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', +}; \ No newline at end of file diff --git a/web/src/hooks/useWebSocket.jsx b/web/src/hooks/useWebSocket.jsx new file mode 100644 index 0000000..d73d35d --- /dev/null +++ b/web/src/hooks/useWebSocket.jsx @@ -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 + }; +}; \ No newline at end of file