Basic web interface

This commit is contained in:
2024-11-02 12:33:24 +01:00
parent 092a05c3aa
commit 1974769eb4
21 changed files with 679 additions and 98 deletions

View File

@@ -1,15 +1,5 @@
#!/bin/bash
# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Not in a git repository. Creating temporary one to use gitignore rules..."
# Initialize temporary git repo if not in one
git init >/dev/null 2>&1
temp_git=true
else
temp_git=false
fi
# Clear/create output file
> claude.txt
@@ -32,6 +22,11 @@ echo "=============" >> claude.txt
if [ "$file" = "claude.txt" ]; then
continue
fi
# Skip non-existent files
if [ ! -f "$file" ]; then
continue
fi
# Skip binary files
if file "$file" | grep -q "binary"; then
@@ -44,10 +39,4 @@ echo "=============" >> claude.txt
cat "$file" >> claude.txt
done
# Clean up temporary git repo if we created one
if [ "$temp_git" = true ]; then
rm -rf .git
echo "Cleaned up temporary git repository"
fi
echo "Concatenation complete. Output written to claude.txt"

View File

@@ -1,4 +1,4 @@
node_modules
dist
coverage
.gitignore
.git
*.log

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React App</title>
<title>SIA Web Interface</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,10 +0,0 @@
export default {
testEnvironment: 'jsdom',
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
},
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
}

View File

@@ -1,7 +1,7 @@
{
"name": "my-react-app",
"name": "sia-web",
"private": true,
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,8 +11,17 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-tabs": "^1.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@babel/preset-env": "^7.23.9",
@@ -30,4 +39,4 @@
"tailwindcss": "^3.4.1",
"vite": "^5.1.0"
}
}
}

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,25 +1,337 @@
import React from 'react'
import { add } from '../utils/calculator.js' // Note the .js extension
import React, { useState, useEffect, useRef } from 'react';
import Editor from '@monaco-editor/react';
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card.jsx';
import { Button } from '../components/ui/button.jsx';
import { ScrollArea } from '../components/ui/scroll-area.jsx';
import { Alert, AlertDescription } from '../components/ui/alert.jsx';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs.jsx';
import { Textarea } from '../components/ui/textarea.jsx';
import { Input } from '../components/ui/input.jsx';
function App() {
const [result, setResult] = React.useState(0)
const WebSocketState = {
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
};
const handleCalculate = () => {
setResult(add(5, 3))
}
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 SIAInterface = () => {
// Editor content state
const [context, setContext] = useState('');
const [originalResponse, setOriginalResponse] = useState('');
const [modifiedResponse, setModifiedResponse] = useState('');
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validationError, setValidationError] = useState(null);
// UI state
const [activeTab, setActiveTab] = useState('input');
const [agentState, setAgentState] = useState(AgentState.UPDATE);
const [wsState, setWsState] = useState(WebSocketState.DISCONNECTED);
// WebSocket ref
const wsRef = useRef(null);
// Monaco editor options
const editorOptions = {
minimap: { enabled: false },
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false,
fontSize: 14,
automaticLayout: true,
};
useEffect(() => {
// Connect to WebSocket
connectWebSocket();
// Cleanup on unmount
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);
// Attempt to reconnect after delay
setTimeout(connectWebSocket, 2000);
};
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.data);
break;
case MessageType.CONTEXT_UPDATE:
setContext(message.data);
break;
case MessageType.RESPONSE_UPDATE:
setOriginalResponse(message.data);
setModifiedResponse(message.data);
setValidationError(message.validation_error || null);
break;
case MessageType.OUTPUT_UPDATE:
setOutput(prev => prev + message.data);
break;
}
};
const sendWebSocketMessage = (type, data) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type, data }));
}
};
const handleSendInput = (withStdin = false) => {
if (!input.trim()) return;
// Send input to backend
sendWebSocketMessage('SEND_INPUT', input);
setInput('');
if (withStdin) {
// Prepare read_stdin response
const readStdinXml = `<read_stdin>
<!-- Content will be populated with stdin -->
</read_stdin>`;
setOriginalResponse(readStdinXml);
setModifiedResponse(readStdinXml);
setActiveTab('modified');
}
};
const handleApproveContext = () => {
sendWebSocketMessage('APPROVE_CONTEXT');
};
const handleApproveResponse = (modified = false) => {
const response = modified ? modifiedResponse : originalResponse;
sendWebSocketMessage('APPROVE_RESPONSE', response);
};
const handleResetModified = () => {
setModifiedResponse(originalResponse);
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">React App</h1>
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={handleCalculate}
>
Calculate 5 + 3
</button>
<p className="mt-4">Result: {result}</p>
</div>
)
}
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">SIA Control Interface</h1>
<div className="flex items-center space-x-4">
<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>
export default App
{/* 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}
className="w-full"
>
Approve Context
</Button>
</div>
</CardContent>
</Card>
{/* Input/Response Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="input">Input</TabsTrigger>
<TabsTrigger value="original">Original Response</TabsTrigger>
<TabsTrigger value="modified">Modified Response</TabsTrigger>
</TabsList>
<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">
<Button
onClick={() => handleSendInput(false)}
className="flex-1"
>
Send Input
</Button>
<Button
onClick={() => handleSendInput(true)}
variant="secondary"
className="flex-1"
>
Send & Read Stdin
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="original">
<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
onClick={() => handleApproveResponse(false)}
disabled={agentState !== AgentState.RESPONSE_APPROVAL || 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">
<Button
onClick={handleResetModified}
variant="secondary"
className="flex-1"
>
Reset to Original
</Button>
<Button
onClick={() => handleApproveResponse(true)}
disabled={agentState !== AgentState.RESPONSE_APPROVAL || validationError}
className="flex-1"
>
Approve Modified Response
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Output Display */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Output</CardTitle>
<Button
onClick={() => setOutput('')}
variant="outline"
size="sm"
>
Clear Output
</Button>
</CardHeader>
<CardContent>
<ScrollArea className="h-32">
<div className="font-mono whitespace-pre-wrap">
{output}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</div>
);
};
export default SIAInterface;

View File

@@ -1,15 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react'
import App from './App'
describe('App', () => {
it('renders without crashing', () => {
render(<App />)
expect(screen.getByText(/React App/i)).toBeInTheDocument()
})
it('calculates correctly when button is clicked', () => {
render(<App />)
fireEvent.click(screen.getByText(/Calculate/i))
expect(screen.getByText(/Result: 8/i)).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,29 @@
import * as React from "react"
const Alert = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
const variants = {
default: "bg-background text-foreground",
destructive: "bg-destructive text-destructive-foreground"
}
return (
<div
ref={ref}
role="alert"
className={`relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground ${variants[variant]} ${className}`}
{...props}
/>
)
})
Alert.displayName = "Alert"
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`text-sm [&_p]:leading-relaxed ${className}`}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertDescription }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
const Button = React.forwardRef(({
className = "",
variant = "default",
size = "default",
disabled = false,
...props
}, ref) => {
const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
const variants = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
}
const sizes = {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
ref={ref}
disabled={disabled}
{...props}
/>
)
})
Button.displayName = "Button"
export { Button }

View File

@@ -0,0 +1,35 @@
import * as React from "react"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`rounded-lg border bg-card text-card-foreground shadow-sm ${className}`}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={`flex flex-col space-y-1.5 p-6 ${className}`}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={`text-2xl font-semibold leading-none tracking-tight ${className}`}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={`p-6 pt-0 ${className}`} {...props} />
))
CardContent.displayName = "CardContent"
export { Card, CardHeader, CardTitle, CardContent }

View File

@@ -0,0 +1,15 @@
import * as React from "react"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={`relative overflow-hidden ${className}`}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.ScrollAreaScrollbar
className="flex touch-none select-none transition-colors"
orientation="vertical"
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
<ScrollAreaPrimitive.ScrollAreaScrollbar
className="flex h-2.5 touch-none select-none transition-colors"
orientation="horizontal"
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
export { ScrollArea }

View File

@@ -0,0 +1,33 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={`inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground ${className}`}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={`inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm ${className}`}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={`mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${className}`}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,14 @@
import * as React from "react"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
<textarea
className={`flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

46
web/src/index.css Normal file
View File

@@ -0,0 +1,46 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,9 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './components/App'
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './components/App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
);

View File

@@ -1,7 +0,0 @@
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export const multiply = (a, b) => a * b
export const divide = (a, b) => {
if (b === 0) throw new Error('Division by zero')
return a / b
}

View File

@@ -1,16 +0,0 @@
// calculator.test.js
import { add, subtract, multiply, divide } from './calculator.js' // Note the .js extension
describe('Calculator', () => {
describe('add', () => {
it('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5)
})
})
describe('divide', () => {
it('throws error on division by zero', () => {
expect(() => divide(1, 0)).toThrow('Division by zero')
})
})
})

67
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,67 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./src/**/*.{js,jsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@@ -1,11 +1,19 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'components': path.resolve(__dirname, './src/components')
},
extensions: ['.js', '.jsx']
},
build: {
outDir: 'dist',
assetsDir: 'assets',
base: '/'
}
})
});