Generated by Spark: ?

This commit is contained in:
Anderson 2026-01-17 02:52:12 +00:00 committed by GitHub
parent fbab43a6f9
commit fb17d0e075
13 changed files with 1059 additions and 6 deletions

125
PRD.md Normal file
View file

@ -0,0 +1,125 @@
# Planning Guide
AeThex Studio is a browser-based Roblox Lua script editor with integrated AI assistance for rapid prototyping and learning.
**Experience Qualities**:
1. **Professional** - Should feel like a legitimate development tool with clean typography and purposeful spacing
2. **Intelligent** - AI assistance should feel seamlessly integrated, not bolted on
3. **Empowering** - Users should feel confident writing and understanding Lua code
**Complexity Level**: Light Application (multiple features with basic state)
This is a focused code editor with AI chat, script templates, and syntax validation - not a full-featured IDE with file systems, debugging, or deployment pipelines.
## Essential Features
### Monaco Code Editor
- **Functionality**: Full-featured Lua code editor with syntax highlighting, autocomplete, and line numbers
- **Purpose**: Provide a professional coding environment for Roblox scripting
- **Trigger**: Loads on app start with default template
- **Progression**: User opens app → sees editor with starter code → begins typing → receives syntax highlighting and autocomplete
- **Success criteria**: Code is editable, syntax-highlighted, and changes persist between sessions
### AI Chat Assistant
- **Functionality**: Conversational AI panel that helps explain code, debug issues, and suggest improvements
- **Purpose**: Lower the learning curve for new Roblox developers
- **Trigger**: User clicks chat button or types a question in the AI panel
- **Progression**: User asks question → AI analyzes current code → provides contextual answer with code examples
- **Success criteria**: AI responses are relevant, helpful, and include code snippets when appropriate
### Script Templates
- **Functionality**: Pre-built Roblox script templates (basic part manipulation, player join events, GUI interactions, etc.)
- **Purpose**: Jump-start development and teach common patterns
- **Trigger**: User clicks "Templates" button
- **Progression**: User browses templates → clicks template → code loads into editor → user modifies for their needs
- **Success criteria**: Templates load instantly and represent common Roblox use cases
### Code Validation
- **Functionality**: Real-time syntax checking and error highlighting
- **Purpose**: Catch errors before testing in Roblox Studio
- **Trigger**: Automatic as user types
- **Progression**: User types invalid Lua → error highlights appear → user hovers for details → fixes error
- **Success criteria**: Common Lua syntax errors are caught and clearly displayed
### Export/Copy Code
- **Functionality**: One-click copy of current code to clipboard
- **Purpose**: Easy transfer to Roblox Studio
- **Trigger**: User clicks copy button
- **Progression**: User finishes editing → clicks copy → receives confirmation → pastes into Roblox Studio
- **Success criteria**: Code copies to clipboard with proper formatting
## Edge Case Handling
- **Empty Editor**: Show helpful placeholder text with getting started tips
- **AI Errors**: Display user-friendly error messages if AI service is unavailable
- **Large Scripts**: Monaco handles large files, but warn if script exceeds 5000 lines (unusual for Roblox scripts)
- **Invalid Lua**: Show errors inline without blocking the user from continuing to type
- **Lost Work**: Auto-save code every 30 seconds to prevent data loss
## Design Direction
The design should evoke a sense of technical sophistication meets creative playfulness - this is a tool for building games, not enterprise software. Think dark themes with neon accents, geometric patterns, and smooth transitions that feel modern and game-inspired.
## Color Selection
A dark, code-focused theme with vibrant accent colors inspired by Roblox's playful brand and gaming aesthetics.
- **Primary Color**: oklch(0.45 0.20 265) - Deep electric blue that represents technical precision and digital creativity
- **Secondary Colors**:
- Background: oklch(0.15 0.02 265) - Near-black with subtle blue tint
- Surface: oklch(0.20 0.03 265) - Slightly elevated dark panels
- **Accent Color**: oklch(0.75 0.20 150) - Vibrant cyan-green for CTAs, highlights, and active states
- **Foreground/Background Pairings**:
- Primary (Deep Blue): White text (oklch(0.98 0 0)) - Ratio 7.2:1 ✓
- Accent (Cyan-Green): Dark text (oklch(0.15 0.02 265)) - Ratio 8.1:1 ✓
- Background (Near-Black): Light text (oklch(0.85 0.03 265)) - Ratio 12.3:1 ✓
- Surface (Dark Panel): Light text (oklch(0.85 0.03 265)) - Ratio 10.5:1 ✓
## Font Selection
Typography should feel technical yet approachable - monospace for code, clean sans-serif for UI.
- **Typographic Hierarchy**:
- H1 (App Title): Space Grotesk Bold/32px/tight tracking (-0.02em)
- H2 (Section Headers): Space Grotesk Semibold/20px/normal tracking
- Body (UI Text): Inter Medium/14px/relaxed line-height (1.6)
- Code (Editor): JetBrains Mono Regular/14px/monospace
- Small (Hints): Inter Regular/12px/muted color
## Animations
Animations should feel snappy and technical - quick state transitions with subtle easing that reinforces the digital tool aesthetic. Use framer-motion for panel slides and modal appearances, but keep transitions under 200ms to avoid feeling sluggish. Add micro-interactions on buttons (scale on press) and smooth scrolling in the chat panel.
## Component Selection
- **Components**:
- `Button` (primary actions like "Run", "Copy", "Ask AI") with custom variants for destructive and accent states
- `Card` for template previews and code snippets
- `Separator` for dividing editor from chat panel
- `ScrollArea` for chat messages and template list
- `Tabs` for switching between templates and settings
- `Dialog` for first-time user onboarding
- `Badge` for Lua syntax error indicators
- `Tooltip` for icon button explanations
- Custom Monaco Editor wrapper component
- Custom AI chat component with message bubbles
- **Customizations**:
- Monaco editor needs dark theme configuration
- Chat bubbles with distinct styling for user vs AI messages
- Template cards with code preview using syntax-highlighted pre blocks
- Resizable panels using react-resizable-panels
- **States**:
- Buttons: Default (solid accent), Hover (brightened +10% lightness), Active (scale 0.97), Disabled (50% opacity)
- Editor: Default (dark background), Focused (subtle glow border)
- Chat input: Default (muted border), Focused (accent border with glow)
- **Icon Selection**:
- Code (editor): `<Code />` from Phosphor
- Chat: `<ChatCircle />` from Phosphor
- Copy: `<Copy />` from Phosphor
- Templates: `<FileCode />` from Phosphor
- AI sparkle: `<Sparkle />` from Phosphor
- Play/Run: `<Play />` from Phosphor
- **Spacing**:
- Container padding: p-4 (16px)
- Component gaps: gap-4 (16px) for major sections, gap-2 (8px) for grouped items
- Button padding: px-6 py-2.5
- Card padding: p-6
- **Mobile**:
- Stack editor and chat vertically on mobile
- Make chat panel collapsible on small screens
- Reduce font sizes: H1 to 24px, body to 13px
- Full-width buttons with bottom sheet for templates
- Hide Monaco minimap on screens < 768px

View file

@ -4,9 +4,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<title>AeThex Studio - Roblox Lua Editor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="/src/main.css" rel="stylesheet" />
</head>

72
package-lock.json generated
View file

@ -11,6 +11,7 @@
"@github/spark": ">=0.43.1 <1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.3",
"@monaco-editor/react": "^4.7.0",
"@octokit/core": "^6.1.4",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",
@ -1076,6 +1077,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"license": "MIT",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4404,6 +4428,14 @@
"@types/node": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
@ -5992,6 +6024,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -7953,6 +7995,30 @@
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/monaco-editor/node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
@ -9198,6 +9264,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",

View file

@ -15,6 +15,7 @@
"@github/spark": ">=0.43.1 <1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.3",
"@monaco-editor/react": "^4.7.0",
"@octokit/core": "^6.1.4",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",

View file

@ -1,3 +1,4 @@
{
"templateVersion": 1
"templateVersion": 1,
"dbType": "kv"
}

View file

@ -1,5 +1,70 @@
import { useState } from 'react';
import { Toaster } from '@/components/ui/sonner';
import { CodeEditor } from '@/components/CodeEditor';
import { AIChat } from '@/components/AIChat';
import { Toolbar } from '@/components/Toolbar';
import { TemplatesDrawer } from '@/components/TemplatesDrawer';
import { WelcomeDialog } from '@/components/WelcomeDialog';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { useKV } from '@github/spark/hooks';
import { useIsMobile } from '@/hooks/use-mobile';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
function App() {
return <div></div>
const [currentCode, setCurrentCode] = useState('');
const [showTemplates, setShowTemplates] = useState(false);
const [code, setCode] = useKV('aethex-current-code', '');
const isMobile = useIsMobile();
const handleTemplateSelect = (templateCode: string) => {
setCode(templateCode);
setCurrentCode(templateCode);
};
return (
<div className="h-screen flex flex-col bg-background text-foreground">
<Toolbar code={currentCode} onTemplatesClick={() => setShowTemplates(true)} />
<div className="flex-1 overflow-hidden">
{isMobile ? (
<Tabs defaultValue="editor" className="h-full flex flex-col">
<TabsList className="w-full rounded-none border-b border-border">
<TabsTrigger value="editor" className="flex-1">Editor</TabsTrigger>
<TabsTrigger value="ai" className="flex-1">AI Assistant</TabsTrigger>
</TabsList>
<TabsContent value="editor" className="flex-1 m-0">
<CodeEditor onCodeChange={setCurrentCode} />
</TabsContent>
<TabsContent value="ai" className="flex-1 m-0">
<AIChat currentCode={currentCode} />
</TabsContent>
</Tabs>
) : (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={60} minSize={30}>
<CodeEditor onCodeChange={setCurrentCode} />
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={40} minSize={25}>
<AIChat currentCode={currentCode} />
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
{showTemplates && (
<TemplatesDrawer
onSelectTemplate={handleTemplateSelect}
onClose={() => setShowTemplates(false)}
/>
)}
<WelcomeDialog />
<Toaster position="bottom-right" theme="dark" />
</div>
);
}
export default App
export default App;

127
src/components/AIChat.tsx Normal file
View file

@ -0,0 +1,127 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Textarea } from '@/components/ui/textarea';
import { Sparkle, PaperPlaneRight } from '@phosphor-icons/react';
import { toast } from 'sonner';
interface Message {
role: 'user' | 'assistant';
content: string;
}
interface AIChatProps {
currentCode: string;
}
export function AIChat({ currentCode }: AIChatProps) {
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content: 'Hi! I\'m your AI assistant for Roblox Lua development. Ask me anything about your code, Roblox scripting, or game development!',
},
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
setInput('');
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);
setIsLoading(true);
try {
const promptText = `You are an expert Roblox Lua developer helping a user with their code. The user is working on this code:
\`\`\`lua
${currentCode}
\`\`\`
User question: ${userMessage}
Provide helpful, concise answers. Include code examples when relevant. Keep responses friendly and encouraging.`;
const response = await window.spark.llm(promptText, 'gpt-4o-mini');
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
} catch (error) {
console.error('AI Error:', error);
toast.error('Failed to get AI response. Please try again.');
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error. Please try asking again.' }]);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="flex flex-col h-full bg-card border-l border-border">
<div className="flex items-center gap-2 px-4 py-3 border-b border-border bg-card/50">
<Sparkle className="text-accent" weight="fill" />
<h2 className="font-semibold text-lg">AI Assistant</h2>
</div>
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] rounded-lg px-4 py-2.5 ${
message.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted text-foreground'
}`}
>
<p className="text-sm whitespace-pre-wrap leading-relaxed">{message.content}</p>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-muted rounded-lg px-4 py-2.5">
<div className="flex gap-1">
<div className="w-2 h-2 bg-accent rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-2 h-2 bg-accent rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-accent rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
</div>
</ScrollArea>
<div className="p-4 border-t border-border bg-card/50">
<div className="flex gap-2">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask about your code..."
className="resize-none min-h-[60px] bg-background"
disabled={isLoading}
/>
<Button
onClick={handleSend}
disabled={!input.trim() || isLoading}
className="bg-accent text-accent-foreground hover:bg-accent/90 btn-accent-hover self-end"
>
<PaperPlaneRight weight="fill" />
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,63 @@
import Editor from '@monaco-editor/react';
import { useKV } from '@github/spark/hooks';
import { useEffect } from 'react';
interface CodeEditorProps {
onCodeChange?: (code: string) => void;
}
export function CodeEditor({ onCodeChange }: CodeEditorProps) {
const [code, setCode] = useKV('aethex-current-code', `-- Welcome to AeThex Studio!
-- Write your Roblox Lua code here
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game!")
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
end)
`);
useEffect(() => {
if (onCodeChange && code) {
onCodeChange(code);
}
}, [code, onCodeChange]);
const handleEditorChange = (value: string | undefined) => {
setCode(value || '');
};
return (
<div className="h-full w-full">
<Editor
height="100%"
defaultLanguage="lua"
theme="vs-dark"
value={code}
onChange={handleEditorChange}
options={{
minimap: { enabled: window.innerWidth >= 768 },
fontSize: 14,
lineNumbers: 'on',
automaticLayout: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
cursorBlinking: 'smooth',
smoothScrolling: true,
padding: { top: 16, bottom: 16 },
}}
/>
</div>
);
}

View file

@ -0,0 +1,82 @@
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { X } from '@phosphor-icons/react';
import { templates, type ScriptTemplate } from '@/lib/templates';
interface TemplatesDrawerProps {
onSelectTemplate: (code: string) => void;
onClose: () => void;
}
export function TemplatesDrawer({ onSelectTemplate, onClose }: TemplatesDrawerProps) {
const categories = {
beginner: templates.filter(t => t.category === 'beginner'),
gameplay: templates.filter(t => t.category === 'gameplay'),
ui: templates.filter(t => t.category === 'ui'),
tools: templates.filter(t => t.category === 'tools'),
};
const handleTemplateClick = (template: ScriptTemplate) => {
onSelectTemplate(template.code);
onClose();
};
return (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-4xl max-h-[80vh] flex flex-col bg-card border-border">
<div className="flex items-center justify-between p-4 border-b border-border">
<div>
<h2 className="text-2xl font-bold">Script Templates</h2>
<p className="text-sm text-muted-foreground mt-1">
Choose a template to get started quickly
</p>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<X />
</Button>
</div>
<Tabs defaultValue="beginner" className="flex-1 flex flex-col">
<TabsList className="mx-4 mt-4">
<TabsTrigger value="beginner">Beginner</TabsTrigger>
<TabsTrigger value="gameplay">Gameplay</TabsTrigger>
<TabsTrigger value="ui">UI</TabsTrigger>
<TabsTrigger value="tools">Tools</TabsTrigger>
</TabsList>
{Object.entries(categories).map(([category, items]) => (
<TabsContent key={category} value={category} className="flex-1 m-0">
<ScrollArea className="h-[calc(80vh-180px)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4">
{items.map((template) => (
<Card
key={template.id}
className="p-4 cursor-pointer hover:border-accent transition-colors"
onClick={() => handleTemplateClick(template)}
>
<div className="flex items-start justify-between mb-2">
<h3 className="font-semibold">{template.name}</h3>
<Badge variant="secondary" className="text-xs">
{template.category}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
{template.description}
</p>
<pre className="text-xs bg-background p-2 rounded overflow-x-auto">
<code>{template.code.split('\n').slice(0, 3).join('\n')}...</code>
</pre>
</Card>
))}
</div>
</ScrollArea>
</TabsContent>
))}
</Tabs>
</Card>
</div>
);
}

130
src/components/Toolbar.tsx Normal file
View file

@ -0,0 +1,130 @@
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Copy, FileCode, Download, Info } from '@phosphor-icons/react';
import { toast } from 'sonner';
import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
interface ToolbarProps {
code: string;
onTemplatesClick: () => void;
}
export function Toolbar({ code, onTemplatesClick }: ToolbarProps) {
const [showInfo, setShowInfo] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
toast.success('Code copied to clipboard!');
} catch (error) {
toast.error('Failed to copy code');
}
};
const handleExport = () => {
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'script.lua';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Script exported!');
};
return (
<>
<div className="flex items-center gap-2 px-4 py-3 bg-card border-b border-border">
<h1 className="text-2xl font-bold tracking-tight">
Ae<span className="text-accent">Thex</span>
</h1>
<div className="flex-1" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={onTemplatesClick}>
<FileCode />
<span className="ml-2 hidden sm:inline">Templates</span>
</Button>
</TooltipTrigger>
<TooltipContent>Browse script templates</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={handleCopy}>
<Copy />
<span className="ml-2 hidden sm:inline">Copy</span>
</Button>
</TooltipTrigger>
<TooltipContent>Copy code to clipboard</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleExport}
className="hidden sm:flex"
>
<Download />
<span className="ml-2">Export</span>
</Button>
</TooltipTrigger>
<TooltipContent>Download as .lua file</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={() => setShowInfo(true)}>
<Info />
</Button>
</TooltipTrigger>
<TooltipContent>About AeThex Studio</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Dialog open={showInfo} onOpenChange={setShowInfo}>
<DialogContent>
<DialogHeader>
<DialogTitle>About AeThex Studio</DialogTitle>
<DialogDescription className="space-y-3 pt-2">
<p>
AeThex Studio is a browser-based Roblox Lua editor with AI assistance,
designed to help you write better scripts faster.
</p>
<div className="space-y-2 text-sm">
<div>
<strong className="text-foreground">Features:</strong>
<ul className="list-disc list-inside mt-1 space-y-1 text-muted-foreground">
<li>Monaco editor with Lua syntax highlighting</li>
<li>AI assistant for code help and debugging</li>
<li>Ready-made script templates</li>
<li>Auto-save (code persists between sessions)</li>
<li>Export to .lua files</li>
</ul>
</div>
<div>
<strong className="text-foreground">Tips:</strong>
<ul className="list-disc list-inside mt-1 space-y-1 text-muted-foreground">
<li>Your code is automatically saved as you type</li>
<li>Ask the AI assistant about any Roblox API or scripting concepts</li>
<li>Use templates as starting points for common tasks</li>
<li>Copy your finished scripts to paste into Roblox Studio</li>
</ul>
</div>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -0,0 +1,86 @@
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Sparkle, Code, FileCode } from '@phosphor-icons/react';
import { useKV } from '@github/spark/hooks';
export function WelcomeDialog() {
const [hasSeenWelcome, setHasSeenWelcome] = useKV('aethex-welcome-seen', 'false');
const [open, setOpen] = useState(false);
useEffect(() => {
if (hasSeenWelcome !== 'true') {
setOpen(true);
}
}, [hasSeenWelcome]);
const handleClose = () => {
setHasSeenWelcome('true');
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-2xl">
Welcome to <span className="text-accent">AeThex Studio</span>
</DialogTitle>
<DialogDescription className="text-base leading-relaxed pt-2">
Your AI-powered Roblox Lua editor in the browser
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex gap-3">
<div className="mt-1">
<Code className="text-accent" size={24} weight="duotone" />
</div>
<div>
<h3 className="font-semibold mb-1">Code Editor</h3>
<p className="text-sm text-muted-foreground">
Professional Monaco editor with Lua syntax highlighting and autocomplete
</p>
</div>
</div>
<div className="flex gap-3">
<div className="mt-1">
<Sparkle className="text-accent" size={24} weight="duotone" />
</div>
<div>
<h3 className="font-semibold mb-1">AI Assistant</h3>
<p className="text-sm text-muted-foreground">
Ask questions about your code and get instant help with Roblox scripting
</p>
</div>
</div>
<div className="flex gap-3">
<div className="mt-1">
<FileCode className="text-accent" size={24} weight="duotone" />
</div>
<div>
<h3 className="font-semibold mb-1">Templates</h3>
<p className="text-sm text-muted-foreground">
Jump-start your projects with ready-made script templates
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button onClick={handleClose} className="bg-accent text-accent-foreground hover:bg-accent/90 btn-accent-hover">
Get Started
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -1 +1,101 @@
/* This is where custom CSS goes */
@import 'tailwindcss';
@import "tw-animate-css";
@layer base {
* {
@apply border-border;
}
body {
font-family: 'Inter', sans-serif;
background:
radial-gradient(circle at 20% 50%, oklch(0.20 0.08 265 / 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, oklch(0.20 0.08 150 / 0.2) 0%, transparent 50%),
oklch(0.15 0.02 265);
background-attachment: fixed;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Space Grotesk', sans-serif;
}
code, pre {
font-family: 'JetBrains Mono', monospace;
}
}
@layer components {
.btn-accent-hover {
transition: all 0.2s ease;
}
.btn-accent-hover:hover {
transform: scale(1.02);
filter: brightness(1.1);
}
.btn-accent-hover:active {
transform: scale(0.98);
}
}
:root {
--background: oklch(0.15 0.02 265);
--foreground: oklch(0.85 0.03 265);
--card: oklch(0.20 0.03 265);
--card-foreground: oklch(0.85 0.03 265);
--popover: oklch(0.20 0.03 265);
--popover-foreground: oklch(0.85 0.03 265);
--primary: oklch(0.45 0.20 265);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.25 0.04 265);
--secondary-foreground: oklch(0.85 0.03 265);
--muted: oklch(0.22 0.03 265);
--muted-foreground: oklch(0.55 0.03 265);
--accent: oklch(0.75 0.20 150);
--accent-foreground: oklch(0.15 0.02 265);
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.30 0.04 265);
--input: oklch(0.30 0.04 265);
--ring: oklch(0.75 0.20 150);
--radius: 0.5rem;
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) * 1.5);
--radius-xl: calc(var(--radius) * 2);
--radius-2xl: calc(var(--radius) * 3);
--radius-full: 9999px;
}

200
src/lib/templates.ts Normal file
View file

@ -0,0 +1,200 @@
export interface ScriptTemplate {
id: string;
name: string;
description: string;
code: string;
category: 'beginner' | 'gameplay' | 'ui' | 'tools';
}
export const templates: ScriptTemplate[] = [
{
id: 'hello-world',
name: 'Hello World',
description: 'Basic print statement to test scripts',
category: 'beginner',
code: `-- Hello World Script
print("Hello from Roblox!")
local message = "Welcome to scripting!"
print(message)`,
},
{
id: 'player-join',
name: 'Player Join Handler',
description: 'Detect when players join and leave the game',
category: 'beginner',
code: `local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game!")
-- Create leaderstats folder
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
-- Add coins value
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
end)
Players.PlayerRemoving:Connect(function(player)
print(player.Name .. " left the game!")
end)`,
},
{
id: 'part-touch',
name: 'Part Touch Detector',
description: 'Detect when a player touches a part',
category: 'gameplay',
code: `local part = script.Parent
part.Touched:Connect(function(hit)
local humanoid = hit.Parent:FindFirstChild("Humanoid")
if humanoid then
local player = game.Players:GetPlayerFromCharacter(hit.Parent)
if player then
print(player.Name .. " touched the part!")
-- Do something here
end
end
end)`,
},
{
id: 'teleport-part',
name: 'Teleport Part',
description: 'Teleport players when they touch a part',
category: 'gameplay',
code: `local part = script.Parent
local destination = Vector3.new(0, 10, 0) -- Change this to your destination
part.Touched:Connect(function(hit)
local humanoid = hit.Parent:FindFirstChild("Humanoid")
if humanoid then
local rootPart = hit.Parent:FindFirstChild("HumanoidRootPart")
if rootPart then
rootPart.CFrame = CFrame.new(destination)
print("Teleported " .. hit.Parent.Name)
end
end
end)`,
},
{
id: 'gui-button',
name: 'GUI Button Click',
description: 'Handle button clicks in a GUI',
category: 'ui',
code: `local button = script.Parent
button.MouseButton1Click:Connect(function()
print("Button clicked!")
-- Add button feedback
button.BackgroundColor3 = Color3.fromRGB(0, 255, 0)
wait(0.1)
button.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
end)`,
},
{
id: 'give-tool',
name: 'Give Tool to Player',
description: 'Give a tool to players when they join',
category: 'tools',
code: `local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local toolName = "YourToolName" -- Change this to your tool name
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
wait(1) -- Wait for character to load
local tool = ReplicatedStorage:FindFirstChild(toolName)
if tool then
local toolClone = tool:Clone()
toolClone.Parent = player.Backpack
print("Gave " .. toolName .. " to " .. player.Name)
end
end)
end)`,
},
{
id: 'tween-part',
name: 'Tween Part Animation',
description: 'Smoothly animate a part using TweenService',
category: 'gameplay',
code: `local TweenService = game:GetService("TweenService")
local part = script.Parent
local tweenInfo = TweenInfo.new(
2, -- Duration
Enum.EasingStyle.Quad,
Enum.EasingDirection.InOut,
-1, -- Repeat count (-1 = infinite)
true, -- Reverse
0 -- Delay
)
local goal = {
Position = part.Position + Vector3.new(0, 5, 0)
}
local tween = TweenService:Create(part, tweenInfo, goal)
tween:Play()`,
},
{
id: 'datastore',
name: 'DataStore Save/Load',
description: 'Save and load player data using DataStore',
category: 'gameplay',
code: `local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")
Players.PlayerAdded:Connect(function(player)
local userId = "Player_" .. player.UserId
local data
local success, errorMsg = pcall(function()
data = playerDataStore:GetAsync(userId)
end)
if success then
if data then
print("Loaded data for " .. player.Name)
-- Use the data here
else
print("New player: " .. player.Name)
end
else
warn("Error loading data: " .. errorMsg)
end
end)
Players.PlayerRemoving:Connect(function(player)
local userId = "Player_" .. player.UserId
local data = {
coins = 100, -- Replace with actual data
level = 5
}
local success, errorMsg = pcall(function()
playerDataStore:SetAsync(userId, data)
end)
if success then
print("Saved data for " .. player.Name)
else
warn("Error saving data: " .. errorMsg)
end
end)`,
},
];