Basic web interface
This commit is contained in:
21
claude.sh
21
claude.sh
@@ -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"
|
||||
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.gitignore
|
||||
.git
|
||||
*.log
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
"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
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 { 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;
|
||||
@@ -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 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>
|
||||
)
|
||||
);
|
||||
@@ -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 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: '/'
|
||||
}
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user