Generated by Spark: ?
This commit is contained in:
parent
fbab43a6f9
commit
fb17d0e075
13 changed files with 1059 additions and 6 deletions
125
PRD.md
Normal file
125
PRD.md
Normal 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
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title></title>
|
<title>AeThex Studio - Roblox Lua Editor</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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" />
|
<link href="/src/main.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
72
package-lock.json
generated
72
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
||||||
"@github/spark": ">=0.43.1 <1",
|
"@github/spark": ">=0.43.1 <1",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@octokit/core": "^6.1.4",
|
"@octokit/core": "^6.1.4",
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/colors": "^3.0.0",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
|
|
@ -1076,6 +1077,29 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -4404,6 +4428,14 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.48.0",
|
"version": "8.48.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||||
|
|
@ -5992,6 +6024,16 @@
|
||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -7953,6 +7995,30 @@
|
||||||
"node": "*"
|
"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": {
|
"node_modules/motion-dom": {
|
||||||
"version": "12.23.23",
|
"version": "12.23.23",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||||
|
|
@ -9198,6 +9264,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@github/spark": ">=0.43.1 <1",
|
"@github/spark": ">=0.43.1 <1",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@octokit/core": "^6.1.4",
|
"@octokit/core": "^6.1.4",
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/colors": "^3.0.0",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"templateVersion": 1
|
"templateVersion": 1,
|
||||||
}
|
"dbType": "kv"
|
||||||
|
}
|
||||||
69
src/App.tsx
69
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() {
|
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
127
src/components/AIChat.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/components/CodeEditor.tsx
Normal file
63
src/components/CodeEditor.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/components/TemplatesDrawer.tsx
Normal file
82
src/components/TemplatesDrawer.tsx
Normal 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
130
src/components/Toolbar.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/components/WelcomeDialog.tsx
Normal file
86
src/components/WelcomeDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/index.css
102
src/index.css
|
|
@ -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
200
src/lib/templates.ts
Normal 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)`,
|
||||||
|
},
|
||||||
|
];
|
||||||
Loading…
Reference in a new issue