diff --git a/PRD.md b/PRD.md
new file mode 100644
index 0000000..3d4acbb
--- /dev/null
+++ b/PRD.md
@@ -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): `` from Phosphor
+ - Chat: ` ` from Phosphor
+ - Copy: ` ` from Phosphor
+ - Templates: ` ` from Phosphor
+ - AI sparkle: ` ` from Phosphor
+ - Play/Run: ` ` 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
diff --git a/index.html b/index.html
index f62002d..a829f30 100644
--- a/index.html
+++ b/index.html
@@ -4,9 +4,10 @@
-
+ AeThex Studio - Roblox Lua Editor
+
diff --git a/package-lock.json b/package-lock.json
index c92f18e..e5a1a68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 165aec8..6fac6a5 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spark.meta.json b/spark.meta.json
index 706a311..a277bc5 100644
--- a/spark.meta.json
+++ b/spark.meta.json
@@ -1,3 +1,4 @@
{
- "templateVersion": 1
-}
+ "templateVersion": 1,
+ "dbType": "kv"
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 98ef973..0831574 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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
+ 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 (
+
+
setShowTemplates(true)} />
+
+
+ {isMobile ? (
+
+
+ Editor
+ AI Assistant
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ {showTemplates && (
+ setShowTemplates(false)}
+ />
+ )}
+
+
+
+
+ );
}
-export default App
\ No newline at end of file
+export default App;
\ No newline at end of file
diff --git a/src/components/AIChat.tsx b/src/components/AIChat.tsx
new file mode 100644
index 0000000..49ed590
--- /dev/null
+++ b/src/components/AIChat.tsx
@@ -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([
+ {
+ 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 (
+
+
+
+
AI Assistant
+
+
+
+
+ {messages.map((message, index) => (
+
+ ))}
+ {isLoading && (
+
+ )}
+
+
+
+
+
+
+ Press Enter to send, Shift+Enter for new line
+
+
+
+ );
+}
diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx
new file mode 100644
index 0000000..676668b
--- /dev/null
+++ b/src/components/CodeEditor.tsx
@@ -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 (
+
+ = 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 },
+ }}
+ />
+
+ );
+}
diff --git a/src/components/TemplatesDrawer.tsx b/src/components/TemplatesDrawer.tsx
new file mode 100644
index 0000000..f05eb23
--- /dev/null
+++ b/src/components/TemplatesDrawer.tsx
@@ -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 (
+
+
+
+
+
Script Templates
+
+ Choose a template to get started quickly
+
+
+
+
+
+
+
+
+
+ Beginner
+ Gameplay
+ UI
+ Tools
+
+
+ {Object.entries(categories).map(([category, items]) => (
+
+
+
+ {items.map((template) => (
+
handleTemplateClick(template)}
+ >
+
+
{template.name}
+
+ {template.category}
+
+
+
+ {template.description}
+
+
+ {template.code.split('\n').slice(0, 3).join('\n')}...
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx
new file mode 100644
index 0000000..c52ebf6
--- /dev/null
+++ b/src/components/Toolbar.tsx
@@ -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 (
+ <>
+
+
+ AeThex
+
+
+
+
+
+
+
+
+
+ Templates
+
+
+ Browse script templates
+
+
+
+
+
+
+ Copy
+
+
+ Copy code to clipboard
+
+
+
+
+
+
+ Export
+
+
+ Download as .lua file
+
+
+
+
+ setShowInfo(true)}>
+
+
+
+ About AeThex Studio
+
+
+
+
+
+
+
+ About AeThex Studio
+
+
+ AeThex Studio is a browser-based Roblox Lua editor with AI assistance,
+ designed to help you write better scripts faster.
+
+
+
+
Features:
+
+ Monaco editor with Lua syntax highlighting
+ AI assistant for code help and debugging
+ Ready-made script templates
+ Auto-save (code persists between sessions)
+ Export to .lua files
+
+
+
+
Tips:
+
+ Your code is automatically saved as you type
+ Ask the AI assistant about any Roblox API or scripting concepts
+ Use templates as starting points for common tasks
+ Copy your finished scripts to paste into Roblox Studio
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/WelcomeDialog.tsx b/src/components/WelcomeDialog.tsx
new file mode 100644
index 0000000..f81cc5f
--- /dev/null
+++ b/src/components/WelcomeDialog.tsx
@@ -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 (
+
+
+
+
+ Welcome to AeThex Studio
+
+
+ Your AI-powered Roblox Lua editor in the browser
+
+
+
+
+
+
+
+
+
+
Code Editor
+
+ Professional Monaco editor with Lua syntax highlighting and autocomplete
+
+
+
+
+
+
+
+
+
+
AI Assistant
+
+ Ask questions about your code and get instant help with Roblox scripting
+
+
+
+
+
+
+
+
+
+
Templates
+
+ Jump-start your projects with ready-made script templates
+
+
+
+
+
+
+
+ Get Started
+
+
+
+
+ );
+}
diff --git a/src/index.css b/src/index.css
index 860d6f2..6596ff3 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1 +1,101 @@
-/* This is where custom CSS goes */
\ No newline at end of file
+@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;
+}
\ No newline at end of file
diff --git a/src/lib/templates.ts b/src/lib/templates.ts
new file mode 100644
index 0000000..7440160
--- /dev/null
+++ b/src/lib/templates.ts
@@ -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)`,
+ },
+];