Basic web interface
This commit is contained in:
21
claude.sh
21
claude.sh
@@ -1,15 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/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
|
# Clear/create output file
|
||||||
> claude.txt
|
> claude.txt
|
||||||
|
|
||||||
@@ -33,6 +23,11 @@ echo "=============" >> claude.txt
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Skip non-existent files
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Skip binary files
|
# Skip binary files
|
||||||
if file "$file" | grep -q "binary"; then
|
if file "$file" | grep -q "binary"; then
|
||||||
echo "Skipping binary file: $file"
|
echo "Skipping binary file: $file"
|
||||||
@@ -44,10 +39,4 @@ echo "=============" >> claude.txt
|
|||||||
cat "$file" >> claude.txt
|
cat "$file" >> claude.txt
|
||||||
done
|
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"
|
echo "Concatenation complete. Output written to claude.txt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
coverage
|
.git
|
||||||
.gitignore
|
*.log
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>React App</title>
|
<title>SIA Web Interface</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "my-react-app",
|
"name": "sia-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,8 +11,17 @@
|
|||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwind-merge": "^2.2.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.23.9",
|
"@babel/preset-env": "^7.23.9",
|
||||||
|
|||||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,25 +1,337 @@
|
|||||||
import React from 'react'
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { add } from '../utils/calculator.js' // Note the .js extension
|
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 WebSocketState = {
|
||||||
const [result, setResult] = React.useState(0)
|
CONNECTING: 'CONNECTING',
|
||||||
|
CONNECTED: 'CONNECTED',
|
||||||
|
DISCONNECTED: 'DISCONNECTED',
|
||||||
|
};
|
||||||
|
|
||||||
const handleCalculate = () => {
|
const AgentState = {
|
||||||
setResult(add(5, 3))
|
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 (
|
return (
|
||||||
<div className="p-4">
|
<div className="min-h-screen bg-gray-100 p-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">React App</h1>
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
<button
|
{/* Header */}
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
<div className="flex items-center justify-between">
|
||||||
onClick={handleCalculate}
|
<h1 className="text-3xl font-bold">SIA Control Interface</h1>
|
||||||
>
|
<div className="flex items-center space-x-4">
|
||||||
Calculate 5 + 3
|
<div className={`px-4 py-2 rounded-full ${
|
||||||
</button>
|
wsState === WebSocketState.CONNECTED
|
||||||
<p className="mt-4">Result: {result}</p>
|
? '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>
|
</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;
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
29
web/src/components/ui/alert.jsx
Normal file
29
web/src/components/ui/alert.jsx
Normal 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 }
|
||||||
36
web/src/components/ui/button.jsx
Normal file
36
web/src/components/ui/button.jsx
Normal 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 }
|
||||||
35
web/src/components/ui/card.jsx
Normal file
35
web/src/components/ui/card.jsx
Normal 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 }
|
||||||
15
web/src/components/ui/input.jsx
Normal file
15
web/src/components/ui/input.jsx
Normal 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 }
|
||||||
29
web/src/components/ui/scroll-area.jsx
Normal file
29
web/src/components/ui/scroll-area.jsx
Normal 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 }
|
||||||
33
web/src/components/ui/tabs.jsx
Normal file
33
web/src/components/ui/tabs.jsx
Normal 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 }
|
||||||
14
web/src/components/ui/textarea.jsx
Normal file
14
web/src/components/ui/textarea.jsx
Normal 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
46
web/src/index.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './components/App'
|
import App from './components/App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
);
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
67
web/tailwind.config.js
Normal 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")],
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'components': path.resolve(__dirname, './src/components')
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.jsx']
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
assetsDir: 'assets',
|
assetsDir: 'assets',
|
||||||
base: '/'
|
base: '/'
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
Reference in New Issue
Block a user