Compare commits
4 commits
claude/ava
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 524f64315d | |||
| a8b2ffc3fe | |||
| ea5ba62c54 | |||
| 4bc31a32e2 |
172 changed files with 26103 additions and 7320 deletions
15
.env.example
15
.env.example
|
|
@ -1,6 +1,17 @@
|
|||
# AeThex Studio Environment Variables
|
||||
|
||||
# ===========================================
|
||||
# SUPABASE (Required)
|
||||
# ===========================================
|
||||
# Get these from: https://supabase.com/dashboard/project/_/settings/api
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
# Service role key - KEEP SECRET, never expose to client
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
|
||||
|
||||
# ===========================================
|
||||
# Claude API Configuration
|
||||
# ===========================================
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
# Required for cross-platform code translation feature
|
||||
VITE_CLAUDE_API_KEY=sk-ant-api03-your-api-key-here
|
||||
|
|
@ -8,9 +19,13 @@ VITE_CLAUDE_API_KEY=sk-ant-api03-your-api-key-here
|
|||
# Optional: Override Claude model (default: claude-3-5-sonnet-20241022)
|
||||
# VITE_CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
|
||||
# ===========================================
|
||||
# PostHog Analytics (Optional)
|
||||
# ===========================================
|
||||
# VITE_POSTHOG_KEY=your-posthog-key
|
||||
# VITE_POSTHOG_HOST=https://app.posthog.com
|
||||
|
||||
# ===========================================
|
||||
# Sentry Error Tracking (Optional)
|
||||
# ===========================================
|
||||
# VITE_SENTRY_DSN=your-sentry-dsn
|
||||
|
|
|
|||
71
.gitignore
vendored
71
.gitignore
vendored
|
|
@ -7,11 +7,62 @@ yarn-error.log*
|
|||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*-dist
|
||||
*.local
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# archives
|
||||
*.zip
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
|
@ -35,3 +86,13 @@ pids
|
|||
.devcontainer/
|
||||
|
||||
.spark-workbench-id
|
||||
|
||||
.env
|
||||
**/agent-eval-report*
|
||||
packages
|
||||
pids
|
||||
.file-manifest
|
||||
.devcontainer/
|
||||
|
||||
.spark-workbench-id
|
||||
.env.local
|
||||
|
|
|
|||
7404
PROJECT_BACKUP.md
Normal file
7404
PROJECT_BACKUP.md
Normal file
File diff suppressed because it is too large
Load diff
115
README.md
115
README.md
|
|
@ -1,23 +1,10 @@
|
|||
# AeThex Studio
|
||||
# Firebase Studio
|
||||
|
||||
A powerful, **multi-platform** browser-based IDE for game development with **AI-powered cross-platform code translation**, modern tooling, and an intuitive interface. Build once, deploy everywhere.
|
||||
This is a NextJS starter in Firebase Studio.
|
||||
|
||||
   
|
||||
|
||||
## 🌟 What Makes AeThex Studio Different
|
||||
To get started, take a look at src/app/page.tsx.
|
||||
|
||||
**Cross-Platform Translation Engine** - The only IDE that translates your code between game platforms:
|
||||
- 🎮 **Roblox Lua** → ⚡ **UEFN Verse** → 🌐 **Spatial TypeScript** → 🎯 **Core Lua**
|
||||
- AI-powered intelligent code conversion
|
||||
- Platform-specific best practices applied
|
||||
- Side-by-side comparison view
|
||||
- Explanation of key differences
|
||||
|
||||
**Build once, deploy everywhere.** Write your game logic in Roblox, translate to UEFN with one click.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🌍 **Multi-Platform Support** ⭐ NEW!
|
||||
- **Platform Switching** - Work with Roblox, UEFN, Spatial, or Core
|
||||
- **Platform-Specific Templates**:
|
||||
- 🎮 **Roblox**: 25 Lua templates
|
||||
|
|
@ -28,26 +15,34 @@ A powerful, **multi-platform** browser-based IDE for game development with **AI-
|
|||
- **Side-by-Side Comparison** - Compare original and translated code
|
||||
- **Smart Editor** - Language highlighting adapts to selected platform
|
||||
|
||||
### 🎨 **Modern Code Editor**
|
||||
|
||||
## 🎨 **Modern Code Editor**
|
||||
|
||||
- **Monaco Editor** - The same editor that powers VS Code
|
||||
- **Multi-language Support** - Lua, Verse, TypeScript
|
||||
- **Real-time code validation** and linting
|
||||
- **Multi-file editing** with tab management
|
||||
- **File tree navigation** with drag-and-drop organization
|
||||
|
||||
### 🤖 **AI-Powered Assistant**
|
||||
|
||||
## 🤖 **AI-Powered Assistant**
|
||||
|
||||
- Built-in AI chat for code help and debugging
|
||||
- Context-aware suggestions
|
||||
- Code explanation and documentation
|
||||
- Roblox API knowledge
|
||||
|
||||
### 📁 **Project Management**
|
||||
|
||||
## 📁 **Project Management**
|
||||
|
||||
- **File Tree** - Organize your scripts into folders
|
||||
- **Drag-and-drop** - Rearrange files easily
|
||||
- **Quick file search** (Cmd/Ctrl+P) - Find files instantly
|
||||
- **Search in files** (Cmd/Ctrl+Shift+F) - Global text search
|
||||
|
||||
### 🎯 **Productivity Features**
|
||||
|
||||
## 🎯 **Productivity Features**
|
||||
|
||||
- **33+ Code Templates** - Ready-made scripts for multiple platforms
|
||||
- **Roblox** (25 templates):
|
||||
- Beginner templates (Hello World, Touch Detectors, etc.)
|
||||
|
|
@ -63,7 +58,9 @@ A powerful, **multi-platform** browser-based IDE for game development with **AI-
|
|||
- **Keyboard Shortcuts** - Professional IDE shortcuts
|
||||
- **Code Preview** - Test your scripts instantly
|
||||
|
||||
### 💻 **Interactive Terminal & CLI**
|
||||
|
||||
## 💻 **Interactive Terminal & CLI**
|
||||
|
||||
- **Built-in Terminal** - Full-featured command line interface
|
||||
- **10+ CLI Commands** for Roblox development:
|
||||
- `help` - Display available commands
|
||||
|
|
@ -81,7 +78,9 @@ A powerful, **multi-platform** browser-based IDE for game development with **AI-
|
|||
- **Smart Suggestions** - Context-aware command hints
|
||||
- **Toggle with Cmd/Ctrl + `** - Quick terminal access
|
||||
|
||||
### 🎨 **Customization**
|
||||
|
||||
## 🎨 **Customization**
|
||||
|
||||
- **5 Beautiful Themes**:
|
||||
- **Dark** - Classic dark theme for comfortable coding
|
||||
- **Light** - Clean light theme for bright environments
|
||||
|
|
@ -90,21 +89,27 @@ A powerful, **multi-platform** browser-based IDE for game development with **AI-
|
|||
- **Ocean** - Deep blue theme
|
||||
- **Persistent preferences** - Your settings are saved
|
||||
|
||||
### 📱 **Mobile Responsive**
|
||||
|
||||
## 📱 **Mobile Responsive**
|
||||
|
||||
- Optimized layouts for phones and tablets
|
||||
- Touch-friendly controls
|
||||
- Hamburger menu for mobile
|
||||
- Collapsible panels
|
||||
|
||||
### 🚀 **Developer Experience**
|
||||
|
||||
## 🚀 **Developer Experience**
|
||||
|
||||
- **Code splitting** for fast loading
|
||||
- **Error boundaries** with graceful error handling
|
||||
- **Loading states** with spinners
|
||||
- **Toast notifications** for user feedback
|
||||
- **Testing infrastructure** with Vitest
|
||||
|
||||
|
||||
## 🎮 Perfect For
|
||||
|
||||
|
||||
- **Multi-Platform Developers** - Build for Roblox, UEFN, Spatial, and Core from one IDE
|
||||
- **Game Studios** - Translate games between platforms with AI assistance
|
||||
- **Roblox → UEFN Migration** - Converting existing Roblox games to Fortnite
|
||||
|
|
@ -112,26 +117,32 @@ A powerful, **multi-platform** browser-based IDE for game development with **AI-
|
|||
- **Rapid Prototyping** - Build once, deploy to multiple platforms
|
||||
- **Web-Based Development** - Code anywhere, no installation needed
|
||||
|
||||
|
||||
## ⌨️ Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Cmd/Ctrl + S` | Save file (auto-save enabled) |
|
||||
| `Cmd/Ctrl + P` | Quick file search |
|
||||
| `Cmd/Ctrl + K` | Command palette |
|
||||
| `Cmd/Ctrl + N` | New project |
|
||||
| `Cmd/Ctrl + F` | Find in editor |
|
||||
| `Cmd/Ctrl + Shift + F` | Search in all files |
|
||||
| ``Cmd/Ctrl + ` `` | Toggle terminal |
|
||||
| Shortcut | Action |
|
||||
| :----------------- | :---------------------------- |
|
||||
| `Cmd/Ctrl + S` | Save file (auto-save enabled) |
|
||||
| `Cmd/Ctrl + P` | Quick file search |
|
||||
| `Cmd/Ctrl + K` | Command palette |
|
||||
| `Cmd/Ctrl + N` | New project |
|
||||
| `Cmd/Ctrl + F` | Find in editor |
|
||||
| `Cmd/Ctrl + Shift + F` | Search in all files |
|
||||
| ``Cmd/Ctrl + ` `` | Toggle terminal |
|
||||
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
#
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/AeThex-LABS/aethex-studio.git
|
||||
|
|
@ -148,6 +159,7 @@ npm run dev
|
|||
|
||||
Visit `http://localhost:3000` to see the application.
|
||||
|
||||
|
||||
### 🔑 Enabling Cross-Platform Translation
|
||||
|
||||
To unlock the **AI-powered code translation** feature, you need a Claude API key:
|
||||
|
|
@ -155,7 +167,7 @@ To unlock the **AI-powered code translation** feature, you need a Claude API key
|
|||
1. **Get API Key**: Visit [Anthropic Console](https://console.anthropic.com/settings/keys) and create a new API key
|
||||
|
||||
2. **Configure Environment**:
|
||||
```bash
|
||||
```bash
|
||||
# Copy example environment file
|
||||
cp .env.example .env.local
|
||||
|
||||
|
|
@ -164,7 +176,7 @@ To unlock the **AI-powered code translation** feature, you need a Claude API key
|
|||
```
|
||||
|
||||
3. **Restart Dev Server**:
|
||||
```bash
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
|
@ -177,8 +189,10 @@ To unlock the **AI-powered code translation** feature, you need a Claude API key
|
|||
|
||||
💡 **Note**: Without an API key, the app works perfectly but shows mock translations instead of real AI conversions.
|
||||
|
||||
|
||||
### Building for Production
|
||||
|
||||
#
|
||||
```bash
|
||||
# Build the application
|
||||
npm run build
|
||||
|
|
@ -187,8 +201,10 @@ npm run build
|
|||
npm start
|
||||
```
|
||||
|
||||
|
||||
## 📖 Usage Guide
|
||||
|
||||
|
||||
### Creating Your First Script
|
||||
|
||||
1. Click **"New File"** in the file tree
|
||||
|
|
@ -197,6 +213,7 @@ npm start
|
|||
4. Click **"Preview"** to test
|
||||
5. **Copy** or **Export** your script
|
||||
|
||||
|
||||
### Using Templates
|
||||
|
||||
1. Click the **Templates** button in the toolbar
|
||||
|
|
@ -204,6 +221,7 @@ npm start
|
|||
3. Click a template to load it into your editor
|
||||
4. Customize the code for your needs
|
||||
|
||||
|
||||
### AI Assistant
|
||||
|
||||
1. Open the **AI Chat** panel (right side on desktop)
|
||||
|
|
@ -214,6 +232,7 @@ npm start
|
|||
- Best practices
|
||||
3. Get instant, context-aware answers
|
||||
|
||||
|
||||
### Organizing Files
|
||||
|
||||
- **Create folders** - Right-click in file tree
|
||||
|
|
@ -221,12 +240,14 @@ npm start
|
|||
- **Rename** - Click the menu (⋯) next to a file
|
||||
- **Delete** - Use the menu to remove files
|
||||
|
||||
|
||||
### Searching
|
||||
|
||||
- **Quick search** - `Cmd/Ctrl+P` to find files by name
|
||||
- **Global search** - `Cmd/Ctrl+Shift+F` to search text across all files
|
||||
- **In-editor search** - `Cmd/Ctrl+F` to find text in current file
|
||||
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Next.js 14** - React framework
|
||||
|
|
@ -240,8 +261,10 @@ npm start
|
|||
- **PostHog** - Analytics (optional)
|
||||
- **Sentry** - Error tracking (optional)
|
||||
|
||||
|
||||
## 🧪 Running Tests
|
||||
|
||||
#
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
|
@ -256,8 +279,10 @@ npm run test:ui
|
|||
npm run test:coverage
|
||||
```
|
||||
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
#
|
||||
```
|
||||
aethex-studio/
|
||||
├── src/
|
||||
|
|
@ -278,6 +303,7 @@ aethex-studio/
|
|||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
|
@ -288,37 +314,52 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
|
||||
## 📝 Code Templates
|
||||
|
||||
AeThex Studio includes 25+ production-ready templates:
|
||||
|
||||
|
||||
**Beginner:**
|
||||
|
||||
- Hello World, Player Join Handler, Part Touch Detector, etc.
|
||||
|
||||
|
||||
**Gameplay:**
|
||||
|
||||
- DataStore System, Teleport Part, Team System, Combat System, etc.
|
||||
|
||||
|
||||
**UI:**
|
||||
|
||||
- GUI Buttons, Proximity Prompts, Countdown Timers, etc.
|
||||
|
||||
|
||||
**Tools:**
|
||||
|
||||
- Give Tool, Sound Manager, Admin Commands, Chat Commands, etc.
|
||||
|
||||
|
||||
**Advanced:**
|
||||
|
||||
- Round System, Inventory System, Pathfinding NPC, Shop System, etc.
|
||||
|
||||
|
||||
## 🐛 Bug Reports
|
||||
|
||||
Found a bug? Please open an issue on GitHub with:
|
||||
|
||||
- Description of the bug
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Screenshots (if applicable)
|
||||
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **Monaco Editor** - For the powerful code editor
|
||||
|
|
@ -326,12 +367,16 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|||
- **Radix UI** - For accessible component primitives
|
||||
- **Vercel** - For Next.js framework
|
||||
|
||||
|
||||
## 📧 Contact
|
||||
|
||||
- **Website**: [aethex.com](https://aethex.com)
|
||||
- **GitHub**: [@AeThex-LABS](https://github.com/AeThex-LABS)
|
||||
- **Issues**: [GitHub Issues](https://github.com/AeThex-LABS/aethex-studio/issues)
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ by the AeThex team**
|
||||
|
|
|
|||
701
aethex-studio-mockup.html
Normal file
701
aethex-studio-mockup.html
Normal file
|
|
@ -0,0 +1,701 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AeThex Studio - IDE Mockup</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=JetBrains+Mono:wght@400;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Scanline effect */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.15),
|
||||
rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Main Layout */
|
||||
.ide-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Title Bar */
|
||||
.title-bar {
|
||||
background: #0d0d0d;
|
||||
border-bottom: 2px solid #1a1a1a;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #ff0000 33%, #0066ff 33%, #0066ff 66%, #ffa500 66%);
|
||||
}
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.logo-small {
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3px;
|
||||
background: linear-gradient(90deg, #ff0000, #0066ff, #ffa500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.project-name span {
|
||||
color: #0066ff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.foundation { background: #ff0000; }
|
||||
.status-dot.corporation { background: #0066ff; }
|
||||
.status-dot.labs { background: #ffa500; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; box-shadow: 0 0 8px currentColor; }
|
||||
50% { opacity: 0.6; box-shadow: 0 0 4px currentColor; }
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: #0d0d0d;
|
||||
border-right: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 12px 16px;
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.sidebar-header.foundation { border-color: #ff0000; }
|
||||
.sidebar-header.corporation { border-color: #0066ff; }
|
||||
.sidebar-header.labs { border-color: #ffa500; }
|
||||
|
||||
.file-tree {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 6px 16px 6px 24px;
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
background: #1a1a1a;
|
||||
border-left: 2px solid #0066ff;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Editor Area */
|
||||
.editor-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
.editor-tabs {
|
||||
background: #0d0d0d;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-tab {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.85em;
|
||||
background: #0d0d0d;
|
||||
border-right: 1px solid #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.editor-tab:hover {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.editor-tab.active {
|
||||
background: #0f0f0f;
|
||||
border-bottom: 2px solid #0066ff;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
display: flex;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.6;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: #333;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Syntax Highlighting */
|
||||
.keyword { color: #ff0000; font-weight: 700; } /* Foundation - core functions */
|
||||
.function { color: #0066ff; } /* Corporation - standard library */
|
||||
.comment { color: #ffa500; font-style: italic; } /* Labs - experimental */
|
||||
.string { color: #00ff88; }
|
||||
.number { color: #ff6b9d; }
|
||||
.variable { color: #e0e0e0; }
|
||||
.operator { color: #999; }
|
||||
|
||||
/* Right Panel */
|
||||
.right-panel {
|
||||
width: 320px;
|
||||
background: #0d0d0d;
|
||||
border-left: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 16px;
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.copilot-message {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #1a1a1a;
|
||||
border-left: 3px solid;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.copilot-message.labs {
|
||||
border-color: #ffa500;
|
||||
}
|
||||
|
||||
.copilot-message.foundation {
|
||||
border-color: #ff0000;
|
||||
}
|
||||
|
||||
.copilot-message.corporation {
|
||||
border-color: #0066ff;
|
||||
}
|
||||
|
||||
.copilot-label {
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.copilot-label.labs { color: #ffa500; }
|
||||
.copilot-label.foundation { color: #ff0000; }
|
||||
.copilot-label.corporation { color: #0066ff; }
|
||||
|
||||
/* Bottom Panel */
|
||||
.bottom-panel {
|
||||
height: 200px;
|
||||
background: #0d0d0d;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bottom-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.bottom-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid #1a1a1a;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.bottom-tab:hover {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.bottom-tab.active {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 2px solid #0066ff;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.terminal-line.foundation { color: #ff0000; }
|
||||
.terminal-line.corporation { color: #0066ff; }
|
||||
.terminal-line.labs { color: #ffa500; }
|
||||
.terminal-line.success { color: #00ff00; }
|
||||
.terminal-line.error { color: #ff0000; }
|
||||
|
||||
/* Network Visualization */
|
||||
.network-viz {
|
||||
position: fixed;
|
||||
bottom: 220px;
|
||||
right: 20px;
|
||||
width: 300px;
|
||||
background: rgba(13, 13, 13, 0.95);
|
||||
border: 1px solid #1a1a1a;
|
||||
padding: 16px;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.network-viz-header {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.network-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
background: #0f0f0f;
|
||||
}
|
||||
|
||||
.node-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.node-dot.foundation { background: #ff0000; }
|
||||
.node-dot.corporation { background: #0066ff; }
|
||||
.node-dot.labs { background: #ffa500; }
|
||||
|
||||
.node-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.node-status {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ide-container">
|
||||
<!-- Title Bar -->
|
||||
<div class="title-bar">
|
||||
<div class="title-left">
|
||||
<div class="logo-small">AETHEX STUDIO</div>
|
||||
<div class="project-name">Project: <span>AeThex Terminal</span></div>
|
||||
</div>
|
||||
<div class="title-right">
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot foundation"></div>
|
||||
<span style="color: #666;">Foundation</span>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot corporation"></div>
|
||||
<span style="color: #666;">Corporation</span>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot labs"></div>
|
||||
<span style="color: #666;">Labs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-header foundation">
|
||||
<span>🔴</span>
|
||||
<span>Foundation APIs</span>
|
||||
</div>
|
||||
<div class="file-tree">
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>auth.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>passport.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>security.ts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-header corporation">
|
||||
<span>🔵</span>
|
||||
<span>Corporation Services</span>
|
||||
</div>
|
||||
<div class="file-tree">
|
||||
<div class="file-item active">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>terminal.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>deployment.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>analytics.ts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-header labs">
|
||||
<span>🟡</span>
|
||||
<span>Labs Experimental</span>
|
||||
</div>
|
||||
<div class="file-tree">
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>copilot.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>nexus-v2.ts</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span>experimental.ts ⚠</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Area -->
|
||||
<div class="editor-area">
|
||||
<div class="editor-tabs">
|
||||
<div class="editor-tab active">
|
||||
<span>📄</span>
|
||||
<span>terminal.ts</span>
|
||||
</div>
|
||||
<div class="editor-tab">
|
||||
<span>📄</span>
|
||||
<span>nexus-v2.ts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-content">
|
||||
<div class="code-line">
|
||||
<div class="line-number">1</div>
|
||||
<div class="line-content"><span class="comment">// AeThex Terminal - Corporation Service</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">2</div>
|
||||
<div class="line-content"><span class="comment">// Powered by Foundation authentication & Labs Nexus Engine</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">3</div>
|
||||
<div class="line-content"></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">4</div>
|
||||
<div class="line-content"><span class="keyword">import</span> <span class="operator">{</span> <span class="variable">authenticate</span><span class="operator">,</span> <span class="variable">verifySession</span> <span class="operator">}</span> <span class="keyword">from</span> <span class="string">'@aethex/foundation/auth'</span><span class="operator">;</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">5</div>
|
||||
<div class="line-content"><span class="keyword">import</span> <span class="operator">{</span> <span class="variable">NexusEngine</span> <span class="operator">}</span> <span class="keyword">from</span> <span class="string">'@aethex/labs/nexus-v2'</span><span class="operator">;</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">6</div>
|
||||
<div class="line-content"><span class="keyword">import</span> <span class="operator">{</span> <span class="variable">DeploymentManager</span> <span class="operator">}</span> <span class="keyword">from</span> <span class="string">'@aethex/corporation/deploy'</span><span class="operator">;</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">7</div>
|
||||
<div class="line-content"></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">8</div>
|
||||
<div class="line-content"><span class="keyword">export</span> <span class="keyword">class</span> <span class="function">AeThexTerminal</span> <span class="operator">{</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">9</div>
|
||||
<div class="line-content"> <span class="keyword">private</span> <span class="variable">nexus</span><span class="operator">:</span> <span class="function">NexusEngine</span><span class="operator">;</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">10</div>
|
||||
<div class="line-content"> <span class="keyword">private</span> <span class="variable">deployer</span><span class="operator">:</span> <span class="function">DeploymentManager</span><span class="operator">;</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">11</div>
|
||||
<div class="line-content"></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">12</div>
|
||||
<div class="line-content"> <span class="keyword">async</span> <span class="function">deploy</span><span class="operator">(</span><span class="variable">options</span><span class="operator">:</span> <span class="function">DeployOptions</span><span class="operator">)</span> <span class="operator">{</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">13</div>
|
||||
<div class="line-content"> <span class="comment">// Foundation: Verify authentication</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">14</div>
|
||||
<div class="line-content"> <span class="keyword">const</span> <span class="variable">session</span> <span class="operator">=</span> <span class="keyword">await</span> <span class="function">verifySession</span><span class="operator">();</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">15</div>
|
||||
<div class="line-content"> <span class="keyword">if</span> <span class="operator">(!</span><span class="variable">session</span><span class="operator">.</span><span class="variable">valid</span><span class="operator">)</span> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="function">Error</span><span class="operator">(</span><span class="string">'Authentication failed'</span><span class="operator">);</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">16</div>
|
||||
<div class="line-content"></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">17</div>
|
||||
<div class="line-content"> <span class="comment">// Labs: Compile with experimental Nexus Engine</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">18</div>
|
||||
<div class="line-content"> <span class="keyword">const</span> <span class="variable">build</span> <span class="operator">=</span> <span class="keyword">await</span> <span class="keyword">this</span><span class="operator">.</span><span class="variable">nexus</span><span class="operator">.</span><span class="function">compile</span><span class="operator">(</span><span class="variable">options</span><span class="operator">.</span><span class="variable">source</span><span class="operator">);</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">19</div>
|
||||
<div class="line-content"></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">20</div>
|
||||
<div class="line-content"> <span class="comment">// Corporation: Deploy to production infrastructure</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">21</div>
|
||||
<div class="line-content"> <span class="keyword">return</span> <span class="keyword">await</span> <span class="keyword">this</span><span class="operator">.</span><span class="variable">deployer</span><span class="operator">.</span><span class="function">deploy</span><span class="operator">(</span><span class="variable">build</span><span class="operator">);</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">22</div>
|
||||
<div class="line-content"> <span class="operator">}</span></div>
|
||||
</div>
|
||||
<div class="code-line">
|
||||
<div class="line-number">23</div>
|
||||
<div class="line-content"><span class="operator">}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Copilot -->
|
||||
<div class="right-panel">
|
||||
<div class="panel-header">
|
||||
<span>AeThex Copilot</span>
|
||||
<span style="color: #ffa500;">⚠ LABS</span>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="copilot-message foundation">
|
||||
<div class="copilot-label foundation">Foundation Mode</div>
|
||||
<div>This code properly uses Foundation authentication. Consider adding rate limiting from @aethex/foundation/security for production use.</div>
|
||||
</div>
|
||||
|
||||
<div class="copilot-message labs">
|
||||
<div class="copilot-label labs">Labs Mode</div>
|
||||
<div>Nice use of Nexus v2! Want to try the experimental parallel compilation feature? It's 40% faster but still in beta.</div>
|
||||
</div>
|
||||
|
||||
<div class="copilot-message corporation">
|
||||
<div class="copilot-label corporation">Corporation Mode</div>
|
||||
<div>DeploymentManager is production-ready. This code follows AeThex Corporation best practices for Railway deployment.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Panel - Terminal -->
|
||||
<div class="bottom-panel">
|
||||
<div class="bottom-tabs">
|
||||
<div class="bottom-tab active">Terminal</div>
|
||||
<div class="bottom-tab">Problems</div>
|
||||
<div class="bottom-tab">Output</div>
|
||||
<div class="bottom-tab">Debug Console</div>
|
||||
</div>
|
||||
|
||||
<div class="terminal-output">
|
||||
<div class="terminal-line foundation">[FOUNDATION] Authenticating user AX-2847-ANDERSON...</div>
|
||||
<div class="terminal-line foundation">[FOUNDATION] Security clearance verified ✓</div>
|
||||
<div class="terminal-line labs">[LABS] Initializing Nexus Engine v2.0-beta...</div>
|
||||
<div class="terminal-line labs">[LABS] Experimental compilation started ⚠</div>
|
||||
<div class="terminal-line labs">[LABS] Build completed in 1.24s</div>
|
||||
<div class="terminal-line corporation">[CORPORATION] Connecting to production infrastructure...</div>
|
||||
<div class="terminal-line corporation">[CORPORATION] Railway deployment #2847 initiated</div>
|
||||
<div class="terminal-line corporation">[CORPORATION] Services: 12/12 online ✓</div>
|
||||
<div class="terminal-line success">[SUCCESS] Deployment complete - https://terminal.aethex.io</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Visualization Overlay -->
|
||||
<div class="network-viz">
|
||||
<div class="network-viz-header">Trinity Infrastructure Status</div>
|
||||
|
||||
<div class="network-node">
|
||||
<div class="node-dot foundation"></div>
|
||||
<div class="node-info">
|
||||
<div class="node-label" style="color: #ff0000;">Foundation</div>
|
||||
<div class="node-status">Auth • Security • APIs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="network-node">
|
||||
<div class="node-dot corporation"></div>
|
||||
<div class="node-info">
|
||||
<div class="node-label" style="color: #0066ff;">Corporation</div>
|
||||
<div class="node-status">Deploy • Analytics • Production</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="network-node">
|
||||
<div class="node-dot labs"></div>
|
||||
<div class="node-info">
|
||||
<div class="node-label" style="color: #ffa500;">Labs</div>
|
||||
<div class="node-status">Nexus v2 • Copilot • Experimental</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
19
app/App.tsx
19
app/App.tsx
|
|
@ -1,19 +0,0 @@
|
|||
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 { FileTree, FileNode } from '@/components/FileTree';
|
||||
import { FileTabs } from '@/components/FileTabs';
|
||||
import { PreviewModal } from '@/components/PreviewModal';
|
||||
// Removed named imports for WelcomeDialog and NewProjectModal. Use lazy-loaded versions from src/App.tsx.
|
||||
import { ConsolePanel } from '@/components/ConsolePanel';
|
||||
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';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
|
||||
export { default } from '../src/App';
|
||||
154
app/globals.css
154
app/globals.css
|
|
@ -1,154 +0,0 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
/* Default Dark Theme */
|
||||
:root, .theme-dark {
|
||||
--background: #0a0a0f;
|
||||
--surface: #1a1a1f;
|
||||
--primary: #8b5cf6;
|
||||
--primary-light: #a78bfa;
|
||||
--primary-dark: #7c3aed;
|
||||
--secondary: #ec4899;
|
||||
--accent: #06b6d4;
|
||||
--border: #2a2a2f;
|
||||
--foreground: #ffffff;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
|
||||
/* Light Theme */
|
||||
.theme-light {
|
||||
--background: #ffffff;
|
||||
--surface: #f9fafb;
|
||||
--primary: #7c3aed;
|
||||
--primary-light: #8b5cf6;
|
||||
--primary-dark: #6d28d9;
|
||||
--secondary: #db2777;
|
||||
--accent: #0891b2;
|
||||
--border: #e5e7eb;
|
||||
--foreground: #111827;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
|
||||
/* Synthwave Theme */
|
||||
.theme-synthwave {
|
||||
--background: #2b213a;
|
||||
--surface: #241b2f;
|
||||
--primary: #ff6ac1;
|
||||
--primary-light: #ff8ad8;
|
||||
--primary-dark: #ff4aaa;
|
||||
--secondary: #9d72ff;
|
||||
--accent: #72f1b8;
|
||||
--border: #495495;
|
||||
--foreground: #f8f8f2;
|
||||
--muted: #a599e9;
|
||||
}
|
||||
|
||||
/* Forest Theme */
|
||||
.theme-forest {
|
||||
--background: #0d1b1e;
|
||||
--surface: #1a2f33;
|
||||
--primary: #2dd4bf;
|
||||
--primary-light: #5eead4;
|
||||
--primary-dark: #14b8a6;
|
||||
--secondary: #34d399;
|
||||
--accent: #a7f3d0;
|
||||
--border: #234e52;
|
||||
--foreground: #ecfdf5;
|
||||
--muted: #6ee7b7;
|
||||
}
|
||||
|
||||
/* Ocean Theme */
|
||||
.theme-ocean {
|
||||
--background: #0c1821;
|
||||
--surface: #1b2838;
|
||||
--primary: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary: #06b6d4;
|
||||
--accent: #38bdf8;
|
||||
--border: #1e3a5f;
|
||||
--foreground: #dbeafe;
|
||||
--muted: #7dd3fc;
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-inter), 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1f;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2a2a2f;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3a3a3f;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Monaco Editor theme overrides */
|
||||
.monaco-editor .margin {
|
||||
background-color: #0a0a0f !important;
|
||||
}
|
||||
|
||||
.monaco-editor {
|
||||
background-color: #0a0a0f !important;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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="/app/main.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app/App.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains-mono",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AeThex Studio - Cross-Platform Game Development IDE",
|
||||
description: "Professional game development IDE for Roblox, Web, Mobile, and Desktop",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased bg-background text-white`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
61
app/main.css
61
app/main.css
|
|
@ -1,61 +0,0 @@
|
|||
@import url('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');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: var(--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;
|
||||
}
|
||||
17
app/page.tsx
17
app/page.tsx
|
|
@ -1,17 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FileTree } from '@/components/FileTree';
|
||||
import { CodeEditor } from '@/components/CodeEditor';
|
||||
import { AIAssistant } from '@/components/AIAssistant';
|
||||
import { ConsolePanel } from '@/components/ConsolePanel';
|
||||
import { NewProjectModal } from '@/components/NewProjectModal';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
|
||||
import App from '../src/App';
|
||||
|
||||
export default function Home() {
|
||||
return <App />;
|
||||
}
|
||||
7
apphosting.yaml
Normal file
7
apphosting.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Settings to manage and configure a Firebase App Hosting backend.
|
||||
# https://firebase.google.com/docs/app-hosting/configure
|
||||
|
||||
runConfig:
|
||||
# Increase this value if you'd like to automatically spin up
|
||||
# more instances in response to increased traffic.
|
||||
maxInstances: 1
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/main.css",
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useEditorStore, FileNode } from '@/store/editor-store';
|
|||
import { cn, getFileIcon, getPlatformIcon } from '@/lib/utils';
|
||||
|
||||
export function FileTree() {
|
||||
const { files, openFile } = useEditorStore();
|
||||
const { files, openFile, moveFile } = useEditorStore();
|
||||
const [expandedFolders, setExpandedFolders] = React.useState<Set<string>>(
|
||||
new Set(['roblox', 'web', 'mobile', 'desktop', 'shared'])
|
||||
);
|
||||
|
|
@ -26,15 +26,20 @@ export function FileTree() {
|
|||
const renderNode = (node: FileNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.id);
|
||||
const isFolder = node.type === 'folder';
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div key={node.id} draggable onDragStart={e => e.dataTransfer.setData('fileId', node.id)} onDrop={e => {
|
||||
e.preventDefault();
|
||||
const fileId = e.dataTransfer.getData('fileId');
|
||||
if (fileId && fileId !== node.id && isFolder) {
|
||||
moveFile(fileId, node.id);
|
||||
}
|
||||
}} onDragOver={e => isFolder && e.preventDefault()}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1 cursor-pointer hover:bg-surface/50 transition-colors",
|
||||
"flex items-center gap-3 px-2 py-1 cursor-pointer hover:bg-surface/50 transition-colors",
|
||||
"text-sm"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||
style={{ paddingLeft: `${depth * 14 + 8}px` }}
|
||||
onClick={() => isFolder ? toggleFolder(node.id) : openFile(node)}
|
||||
>
|
||||
{isFolder && (
|
||||
|
|
@ -43,20 +48,16 @@ export function FileTree() {
|
|||
</span>
|
||||
)}
|
||||
{!isFolder && <span className="w-4" />}
|
||||
|
||||
<span className="text-lg">
|
||||
<span className="text-lg mr-1">
|
||||
{isFolder ? (isExpanded ? '📂' : '📁') : getFileIcon(node.name)}
|
||||
</span>
|
||||
|
||||
<span className="flex-1 truncate text-gray-200">{node.name}</span>
|
||||
|
||||
{node.platform && (
|
||||
<span className="text-xs opacity-50">
|
||||
<span className="text-xs opacity-50 ml-1">
|
||||
{getPlatformIcon(node.platform)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFolder && isExpanded && node.children && (
|
||||
<div>
|
||||
{node.children.map(child => renderNode(child, depth + 1))}
|
||||
|
|
@ -81,7 +82,7 @@ export function FileTree() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pt-2 pb-1 text-xs text-gray-500 font-semibold">PROJECT</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{files.map((node: any) => renderNode(node))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Switch } from '@/components/ui/switch';
|
|||
import { Label } from '@/components/ui/label';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { templates } from '@/lib/templates';
|
||||
import { templates, Template } from '../lib/templates';
|
||||
import { getPlatformIcon } from '@/lib/utils';
|
||||
|
||||
export function NewProjectModal() {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function NexusSyncMonitor() {
|
|||
{expandedItems.has('world') && (
|
||||
<div className="ml-8 space-y-1 mt-1">
|
||||
<div className="text-gray-500">├─ objects: [...]</div>
|
||||
<div className="text-gray-500">└─ weather: "sunny"</div>
|
||||
<div className="text-gray-500">└─ weather: "sunny"</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
4
components/StudioBottomPanel.tsx
Normal file
4
components/StudioBottomPanel.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
// StudioBottomPanel is now obsolete and handled by ConsolePanel. This file is intentionally left blank.
|
||||
// This file is kept only to avoid import errors during refactor.
|
||||
export default function StudioBottomPanel() { return null; }
|
||||
51
components/StudioEditor.tsx
Normal file
51
components/StudioEditor.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"use client";
|
||||
import { useEditorStore } from "../store/editor-zustand";
|
||||
|
||||
function StudioEditor() {
|
||||
const openTabs = useEditorStore((s) => s.openTabs);
|
||||
const activeTabId = useEditorStore((s) => s.activeTabId);
|
||||
const setActiveTab = useEditorStore((s) => s.setActiveTab);
|
||||
const closeTab = useEditorStore((s) => s.closeTab);
|
||||
// Find active file
|
||||
const activeFile = openTabs.find(f => f.id === activeTabId);
|
||||
return (
|
||||
<div className="editor-area">
|
||||
<div className="editor-tabs">
|
||||
{openTabs.length === 0 && (
|
||||
<div className="editor-tab empty">No files open</div>
|
||||
)}
|
||||
{openTabs.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={"editor-tab" + (activeTabId === file.id ? " active" : "")}
|
||||
onClick={() => setActiveTab(file.id)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<span>📄</span>
|
||||
<span>{file.name}</span>
|
||||
<span
|
||||
className="close-btn"
|
||||
style={{ marginLeft: 8, color: "#888", cursor: "pointer" }}
|
||||
onClick={e => { e.stopPropagation(); closeTab(file.id); }}
|
||||
>×</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="editor-content">
|
||||
{activeFile ? (
|
||||
activeFile.content.split("\n").map((line, i) => (
|
||||
<div className="code-line" key={i}>
|
||||
<div className="line-number">{i + 1}</div>
|
||||
<div className="line-content">{line}</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#888", padding: 32, textAlign: "center" }}>
|
||||
No file open. Select a file to begin.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default StudioEditor;
|
||||
81
components/StudioLayout.tsx
Normal file
81
components/StudioLayout.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"use client";
|
||||
import StudioSidebar from "./StudioSidebar";
|
||||
import StudioEditor from "./StudioEditor";
|
||||
import StudioRightPanel from "./StudioRightPanel";
|
||||
import StudioBottomPanel from "./StudioBottomPanel";
|
||||
import StudioNetworkViz from "./StudioNetworkViz";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function StudioLayout({ children }: { children?: React.ReactNode }) {
|
||||
const [platform, setPlatform] = useState("Roblox");
|
||||
const [actionsOpen, setActionsOpen] = useState(false);
|
||||
|
||||
const handlePlatformChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setPlatform(e.target.value);
|
||||
toast.success(`Platform changed to ${e.target.value}`);
|
||||
};
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
toast.info(`${action} action triggered!`);
|
||||
setActionsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ide-container">
|
||||
{/* Title Bar */}
|
||||
<div className="title-bar" style={{ position: 'relative', zIndex: 10 }}>
|
||||
<div className="flex items-center w-full gap-4">
|
||||
<div className="logo-small">AETHEX STUDIO</div>
|
||||
<div className="project-name">Project: <span>AeThex Terminal</span></div>
|
||||
<div className="flex items-center gap-3 ml-6">
|
||||
<span className="text-xs">Platform:</span>
|
||||
<select
|
||||
className="bg-[#22242A] text-xs px-3 py-1 rounded border border-blue-500 focus:ring-2 focus:ring-blue-400"
|
||||
value={platform}
|
||||
onChange={handlePlatformChange}
|
||||
>
|
||||
<option>Roblox</option>
|
||||
<option>Web</option>
|
||||
<option>Mobile</option>
|
||||
<option>Desktop</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="group relative">
|
||||
<button
|
||||
className="px-3 py-1 text-xs bg-gray-700 rounded hover:bg-gray-800 flex items-center gap-1"
|
||||
tabIndex={0}
|
||||
aria-label="Actions"
|
||||
onClick={() => setActionsOpen((v) => !v)}
|
||||
>
|
||||
<span>Actions</span>
|
||||
<svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
||||
</button>
|
||||
{actionsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-32 bg-[#23272F] border border-border rounded shadow-lg z-50">
|
||||
<button className="block w-full text-left px-4 py-2 text-xs hover:bg-blue-600" onClick={() => handleAction('Save')}>Save</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs hover:bg-green-600" onClick={() => handleAction('Run')}>Run</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs hover:bg-gray-600" onClick={() => handleAction('Export')}>Export</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs hover:bg-gray-600" onClick={() => handleAction('Settings')}>Settings</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-4 ml-auto">
|
||||
<span className="flex items-center gap-1 text-xs"><span className="w-2 h-2 rounded-full bg-red-500 inline-block"></span> Foundation</span>
|
||||
<span className="flex items-center gap-1 text-xs"><span className="w-2 h-2 rounded-full bg-blue-500 inline-block"></span> Corporation</span>
|
||||
<span className="flex items-center gap-1 text-xs"><span className="w-2 h-2 rounded-full bg-yellow-400 inline-block"></span> Labs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="main-content">
|
||||
<div className="sidebar"><StudioSidebar /></div>
|
||||
<div className="editor-area"><StudioEditor /></div>
|
||||
<div className="right-panel"><StudioRightPanel /></div>
|
||||
</div>
|
||||
<div className="bottom-panel"><StudioBottomPanel /></div>
|
||||
<div className="network-viz"><StudioNetworkViz /></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
components/StudioNetworkViz.tsx
Normal file
29
components/StudioNetworkViz.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
function StudioNetworkViz() {
|
||||
return (
|
||||
<section className="network-viz">
|
||||
<div className="network-viz-header">Trinity Infrastructure Status</div>
|
||||
<div className="network-node">
|
||||
<div className="node-dot foundation"></div>
|
||||
<div className="node-info">
|
||||
<div className="node-label" style={{ color: "#ff0000" }}>Foundation</div>
|
||||
<div className="node-status">Auth • Security • APIs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="network-node">
|
||||
<div className="node-dot corporation"></div>
|
||||
<div className="node-info">
|
||||
<div className="node-label" style={{ color: "#0066ff" }}>Corporation</div>
|
||||
<div className="node-status">Deploy • Analytics • Production</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="network-node">
|
||||
<div className="node-dot labs"></div>
|
||||
<div className="node-info">
|
||||
<div className="node-label" style={{ color: "#ffa500" }}>Labs</div>
|
||||
<div className="node-status">Nexus v2 • Copilot • Experimental</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
export default StudioNetworkViz;
|
||||
26
components/StudioRightPanel.tsx
Normal file
26
components/StudioRightPanel.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
function StudioRightPanel() {
|
||||
return (
|
||||
<div className="right-panel">
|
||||
<div className="panel-header">
|
||||
<span>AeThex Copilot</span>
|
||||
<span style={{ color: '#ffa500' }}>⚠ LABS</span>
|
||||
</div>
|
||||
<div className="panel-content">
|
||||
<div className="copilot-message foundation">
|
||||
<div className="copilot-label foundation">Foundation Mode</div>
|
||||
<div>This code properly uses Foundation authentication. Consider adding rate limiting from @aethex/foundation/security for production use.</div>
|
||||
</div>
|
||||
<div className="copilot-message labs">
|
||||
<div className="copilot-label labs">Labs Mode</div>
|
||||
<div>Nice use of Nexus v2! Want to try the experimental parallel compilation feature? It's 40% faster but still in beta.</div>
|
||||
</div>
|
||||
<div className="copilot-message corporation">
|
||||
<div className="copilot-label corporation">Corporation Mode</div>
|
||||
<div>DeploymentManager is production-ready. This code follows AeThex Corporation best practices for Railway deployment.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StudioRightPanel;
|
||||
75
components/StudioSidebar.tsx
Normal file
75
components/StudioSidebar.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
"use client";
|
||||
import { useEditorStore, FileTab } from "../store/editor-zustand";
|
||||
|
||||
function StudioSidebar() {
|
||||
const files = useEditorStore((s: any) => s.files);
|
||||
const openFile = useEditorStore((s: any) => s.openFile);
|
||||
const activeTabId = useEditorStore((s: any) => s.activeTabId);
|
||||
// Split files into mockup sections
|
||||
const foundationFiles = files.filter((f: FileTab) => f.name === "auth.ts" || f.name === "passport.ts" || f.name === "security.ts");
|
||||
const corporationFiles = files.filter((f: FileTab) => f.name === "terminal.ts" || f.name === "deployment.ts" || f.name === "analytics.ts");
|
||||
const labsFiles = files.filter((f: FileTab) => f.name === "copilot.ts" || f.name === "nexus-v2.ts" || f.name === "experimental.ts");
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header foundation">
|
||||
<span>🔴</span>
|
||||
<span>Foundation APIs</span>
|
||||
</div>
|
||||
<div className="file-tree">
|
||||
{foundationFiles.map((file: FileTab) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={"file-item" + (activeTabId === file.id ? " active" : "")}
|
||||
onClick={() => openFile(file)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<span className="file-icon">📄</span>
|
||||
<span>{file.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header corporation">
|
||||
<span>🔵</span>
|
||||
<span>Corporation Services</span>
|
||||
</div>
|
||||
<div className="file-tree">
|
||||
{corporationFiles.map((file: FileTab) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={"file-item" + (activeTabId === file.id ? " active" : "")}
|
||||
onClick={() => openFile(file)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<span className="file-icon">📄</span>
|
||||
<span>{file.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header labs">
|
||||
<span>🟡</span>
|
||||
<span>Labs Experimental</span>
|
||||
</div>
|
||||
<div className="file-tree">
|
||||
{labsFiles.map((file: FileTab) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={"file-item" + (activeTabId === file.id ? " active" : "")}
|
||||
onClick={() => openFile(file)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<span className="file-icon">📄</span>
|
||||
<span>{file.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default StudioSidebar;
|
||||
19
middleware.ts
Normal file
19
middleware.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { type NextRequest } from 'next/server';
|
||||
import { updateSession } from '@/lib/supabase/middleware';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - public folder
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
],
|
||||
};
|
||||
3
next-env.d.ts
vendored
3
next-env.d.ts
vendored
|
|
@ -1,5 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const path = require('path');
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
// swcMinify: true, // Removed for compatibility with current Next.js version
|
||||
webpack: (config) => {
|
||||
config.resolve.alias['@'] = path.resolve(__dirname, 'src');
|
||||
return config;
|
||||
|
|
|
|||
35
next.config.ts
Normal file
35
next.config.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type {NextConfig} from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'placehold.co',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'picsum.photos',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
15920
package-lock.json
generated
15920
package-lock.json
generated
File diff suppressed because it is too large
Load diff
129
package.json
129
package.json
|
|
@ -1,73 +1,90 @@
|
|||
{
|
||||
"name": "aethex-studio",
|
||||
"version": "1.0.0",
|
||||
"name": "nextn",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"dev": "next dev -p 9002",
|
||||
"genkit:dev": "genkit start -- tsx src/ai/dev.ts",
|
||||
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
|
||||
"build": "NODE_ENV=production next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@genkit-ai/google-genai": "^1.20.0",
|
||||
"@genkit-ai/next": "^1.20.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@sentry/browser": "^10.34.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/browser": "^10.38.0",
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.93.3",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.462.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"next": "^14.2.35",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"firebase": "^11.9.1",
|
||||
"genkit": "^1.20.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"marked": "^12.0.2",
|
||||
"next": "15.5.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"posthog-js": "^1.328.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^6.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"posthog-js": "^1.337.0",
|
||||
"react": "^19.2.1",
|
||||
"react-day-picker": "^9.11.3",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-resizable-panels": "^4.5.9",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"recharts": "^2.15.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^5.0.3"
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "22.19.7",
|
||||
"@types/react": "18.3.27",
|
||||
"@types/react-dom": "^18",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.15",
|
||||
"jsdom": "^27.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.17"
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.2.1",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"genkit-cli": "^1.20.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
839
src/App.tsx
839
src/App.tsx
|
|
@ -1,38 +1,44 @@
|
|||
'use client';
|
||||
// ...existing code...
|
||||
import React, { useState, lazy, Suspense } from 'react';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { CodeEditor } from './components/CodeEditor';
|
||||
import { AIChat } from './components/AIChat';
|
||||
import { FileTree } from '../components/FileTree';
|
||||
import { FileTabs } from '../components/FileTabs';
|
||||
import { CodeEditor } from '../components/CodeEditor';
|
||||
import { ConsolePanel } from '../components/ConsolePanel';
|
||||
import { Toolbar } from './components/Toolbar';
|
||||
import { FileTree, FileNode } from './components/FileTree';
|
||||
import { FileTabs } from './components/FileTabs';
|
||||
import { ConsolePanel } from './components/ConsolePanel';
|
||||
import { FileSearchModal } from './components/FileSearchModal';
|
||||
import { SearchInFilesPanel } from './components/SearchInFilesPanel';
|
||||
import { CommandPalette, createDefaultCommands } from './components/CommandPalette';
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
|
||||
import { useIsMobile } from './hooks/use-mobile';
|
||||
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
|
||||
import { AIAssistant } from '../components/AIAssistant';
|
||||
import { CrossPlatformPreview } from '../components/CrossPlatformPreview';
|
||||
import { NexusSyncMonitor } from '../components/NexusSyncMonitor';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { toast } from 'sonner';
|
||||
import { ExtraTabs } from './components/ui/tabs-extra';
|
||||
import { Button } from './components/ui/button';
|
||||
import { initPostHog, captureEvent } from './lib/posthog';
|
||||
import { initSentry, captureError } from './lib/sentry';
|
||||
import { LoadingSpinner } from './components/ui/loading-spinner';
|
||||
import { useEditorStore } from '../store/editor-store';
|
||||
import { FileNode } from '../store/editor-store';
|
||||
import { captureEvent } from './lib/posthog';
|
||||
import { captureError } from './lib/sentry';
|
||||
import { useIsMobile } from './hooks/use-mobile';
|
||||
|
||||
import { PlatformId } from './lib/platforms';
|
||||
import { ProjectConfig } from './components/NewProjectModal';
|
||||
|
||||
// Lazy load heavy/modal components for code splitting and better initial load
|
||||
const TemplatesDrawer = lazy(() => import('./components/TemplatesDrawer').then(m => ({ default: m.TemplatesDrawer })));
|
||||
const WelcomeDialog = lazy(() => import('./components/WelcomeDialog').then(m => ({ default: m.WelcomeDialog })));
|
||||
const PreviewModal = lazy(() => import('./components/PreviewModal').then(m => ({ default: m.PreviewModal })));
|
||||
const NewProjectModal = lazy(() => import('./components/NewProjectModal').then(m => ({ default: m.NewProjectModal })));
|
||||
const EducationPanel = lazy(() => import('./components/EducationPanel').then(m => ({ default: m.EducationPanel })));
|
||||
const PassportLogin = lazy(() => import('./components/PassportLogin').then(m => ({ default: m.PassportLogin })));
|
||||
const TranslationPanel = lazy(() => import('./components/TranslationPanel').then(m => ({ default: m.TranslationPanel })));
|
||||
const TemplatesDrawer = lazy(() => import('./components/TemplatesDrawer'));
|
||||
const PreviewModal = lazy(() => import('./components/PreviewModal'));
|
||||
const NewProjectModal = lazy(() => import('./components/NewProjectModal'));
|
||||
const PassportLogin = lazy(() => import('./components/PassportLogin'));
|
||||
const TranslationPanel = lazy(() => import('./components/TranslationPanel'));
|
||||
const CommandPalette = lazy(() => import('./components/CommandPalette'));
|
||||
const StudioSidebar = lazy(() => import('../components/StudioSidebar'));
|
||||
const StudioEditor = lazy(() => import('../components/StudioEditor'));
|
||||
// const StudioBottomPanel = lazy(() => import('../components/StudioBottomPanel'));
|
||||
const StudioRightPanel = lazy(() => import('../components/StudioRightPanel'));
|
||||
const StudioNetworkViz = lazy(() => import('../components/StudioNetworkViz'));
|
||||
|
||||
function App() {
|
||||
// --- Error/Warning Banner State ---
|
||||
const [problemsExpanded, setProblemsExpanded] = useState(false);
|
||||
// --- Right Sidebar Tab State ---
|
||||
const [rightSidebarTab, setRightSidebarTab] = useState('Copilot');
|
||||
// TODO: Connect to real error/warning data source
|
||||
const problems: Array<{ file: string; line: number; type: string; message: string }> = [];
|
||||
// ...existing state and hooks...
|
||||
// --- State ---
|
||||
const setFiles = useEditorStore((state) => state.setFiles);
|
||||
const [currentCode, setCurrentCode] = useState('');
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
|
@ -42,171 +48,21 @@ function App() {
|
|||
const [showSearchInFiles, setShowSearchInFiles] = useState(false);
|
||||
const [showTranslation, setShowTranslation] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [currentPlatform, setCurrentPlatform] = useState<PlatformId>('roblox');
|
||||
const [currentPlatform, setCurrentPlatform] = useState('roblox');
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [showPassportLogin, setShowPassportLogin] = useState(false);
|
||||
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
|
||||
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
|
||||
try {
|
||||
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch (error) {
|
||||
console.error('Failed to load user from localStorage:', error);
|
||||
captureError(error as Error, { context: 'user_state_initialization' });
|
||||
return null;
|
||||
}
|
||||
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
initPostHog();
|
||||
initSentry();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize analytics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboardShortcuts([
|
||||
{
|
||||
key: 's',
|
||||
meta: true, // Cmd on Mac
|
||||
ctrl: true, // Ctrl on Windows/Linux
|
||||
handler: () => {
|
||||
toast.success('File saved automatically!');
|
||||
captureEvent('keyboard_shortcut', { action: 'save' });
|
||||
},
|
||||
description: 'Save file',
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
setShowFileSearch(true);
|
||||
captureEvent('keyboard_shortcut', { action: 'file_search' });
|
||||
},
|
||||
description: 'Quick file search',
|
||||
},
|
||||
{
|
||||
key: 'k',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
setShowCommandPalette(true);
|
||||
captureEvent('keyboard_shortcut', { action: 'command_palette' });
|
||||
},
|
||||
description: 'Command palette',
|
||||
},
|
||||
{
|
||||
key: 'n',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
setShowNewProject(true);
|
||||
captureEvent('keyboard_shortcut', { action: 'new_project' });
|
||||
},
|
||||
description: 'New project',
|
||||
},
|
||||
{
|
||||
key: '/',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
// Monaco editor has built-in Cmd/Ctrl+F for find
|
||||
toast.info('Use Cmd/Ctrl+F in the editor to find text');
|
||||
captureEvent('keyboard_shortcut', { action: 'find' });
|
||||
},
|
||||
description: 'Find in editor',
|
||||
},
|
||||
{
|
||||
key: 'f',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
handler: () => {
|
||||
setShowSearchInFiles(true);
|
||||
captureEvent('keyboard_shortcut', { action: 'search_in_files' });
|
||||
},
|
||||
description: 'Search in all files',
|
||||
},
|
||||
{
|
||||
key: '`',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
setConsoleCollapsed(!consoleCollapsed);
|
||||
captureEvent('keyboard_shortcut', { action: 'toggle_terminal' });
|
||||
},
|
||||
description: 'Toggle terminal',
|
||||
},
|
||||
]);
|
||||
|
||||
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
||||
try {
|
||||
setUser(user);
|
||||
localStorage.setItem('aethex-user', JSON.stringify(user));
|
||||
captureEvent('login', { user });
|
||||
toast.success('Successfully signed in!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save user session:', error);
|
||||
captureError(error as Error, { context: 'login_success' });
|
||||
toast.error('Failed to save session. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
try {
|
||||
setUser(null);
|
||||
localStorage.removeItem('aethex-user');
|
||||
toast.success('Signed out successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to sign out:', error);
|
||||
captureError(error as Error, { context: 'sign_out' });
|
||||
toast.error('Failed to sign out. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const [files, setFiles] = useState<FileNode[]>([
|
||||
{
|
||||
id: 'root',
|
||||
name: 'src',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
id: 'file-1',
|
||||
name: 'script.lua',
|
||||
type: 'file',
|
||||
content: `-- 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)`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const [openFiles, setOpenFiles] = useState<FileNode[]>([]);
|
||||
const [activeFileId, setActiveFileId] = useState<string>('file-1');
|
||||
const [activeFileId, setActiveFileId] = useState<string>('');
|
||||
|
||||
// --- Handlers ---
|
||||
const handleTemplateSelect = (templateCode: string) => {
|
||||
setCode(templateCode);
|
||||
setCurrentCode(templateCode);
|
||||
// Update active file content
|
||||
if (activeFileId) {
|
||||
handleCodeChange(templateCode);
|
||||
}
|
||||
|
|
@ -215,30 +71,21 @@ end)`,
|
|||
const handleCodeChange = (newCode: string) => {
|
||||
setCurrentCode(newCode);
|
||||
setCode(newCode);
|
||||
|
||||
// Update the file content in the files tree
|
||||
if (activeFileId) {
|
||||
setFiles((prev) => {
|
||||
const updateFileContent = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === activeFileId) {
|
||||
return { ...node, content: newCode };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: updateFileContent(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
return updateFileContent(prev || []);
|
||||
});
|
||||
|
||||
// Also update in openFiles to keep tabs in sync
|
||||
setOpenFiles((prev) =>
|
||||
(prev || []).map((file) =>
|
||||
file.id === activeFileId ? { ...file, content: newCode } : file
|
||||
)
|
||||
);
|
||||
const files = useEditorStore.getState().files;
|
||||
const updateFileContent = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === activeFileId) {
|
||||
return { ...node, content: newCode };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: updateFileContent(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
setFiles(updateFileContent(files || []));
|
||||
setOpenFiles((prev) => (prev || []).map((file) => file.id === activeFileId ? { ...file, content: newCode } : file));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -254,103 +101,48 @@ end)`,
|
|||
captureEvent('file_select', { fileId: file.id });
|
||||
};
|
||||
|
||||
const handleFileCreate = (name: string, parentId?: string) => {
|
||||
try {
|
||||
if (!name || name.trim() === '') {
|
||||
toast.error('File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const newFile: FileNode = {
|
||||
id: `file-${Date.now()}`,
|
||||
name: name.endsWith('.lua') ? name : `${name}.lua`,
|
||||
type: 'file',
|
||||
content: '-- New file\n',
|
||||
};
|
||||
|
||||
setFiles((prev) => {
|
||||
const addToFolder = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === 'root' && !parentId) {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), newFile],
|
||||
};
|
||||
}
|
||||
if (node.id === parentId && node.type === 'folder') {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), newFile],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addToFolder(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
return addToFolder(prev || []);
|
||||
});
|
||||
|
||||
captureEvent('file_create', { name, parentId });
|
||||
toast.success(`Created ${newFile.name}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to create file:', error);
|
||||
captureError(error as Error, { context: 'file_create', name, parentId });
|
||||
toast.error('Failed to create file. Please try again.');
|
||||
}
|
||||
const handleFileClose = (id: string) => {
|
||||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||||
};
|
||||
|
||||
const handleFileRename = (id: string, newName: string) => {
|
||||
try {
|
||||
if (!newName || newName.trim() === '') {
|
||||
toast.error('File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles((prev) => {
|
||||
const rename = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, name: newName };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: rename(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
return rename(prev || []);
|
||||
});
|
||||
|
||||
captureEvent('file_rename', { id, newName });
|
||||
} catch (error) {
|
||||
console.error('Failed to rename file:', error);
|
||||
captureError(error as Error, { context: 'file_rename', id, newName });
|
||||
toast.error('Failed to rename file. Please try again.');
|
||||
if (!newName || newName.trim() === '') {
|
||||
toast.error('File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
const files = useEditorStore.getState().files;
|
||||
const rename = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, name: newName };
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: rename(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
setFiles(rename(files || []));
|
||||
captureEvent('file_rename', { id, newName });
|
||||
};
|
||||
|
||||
const handleFileDelete = (id: string) => {
|
||||
try {
|
||||
setFiles((prev) => {
|
||||
const deleteNode = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.id === id) return false;
|
||||
if (node.children) {
|
||||
node.children = deleteNode(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
return deleteNode(prev || []);
|
||||
});
|
||||
|
||||
const files = useEditorStore.getState().files;
|
||||
const deleteNode = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.id === id) return false;
|
||||
if (node.children) {
|
||||
node.children = deleteNode(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
setFiles(deleteNode(files || []));
|
||||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||||
if (activeFileId === id) {
|
||||
setActiveFileId((openFiles || [])[0]?.id || '');
|
||||
}
|
||||
|
||||
captureEvent('file_delete', { id });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete file:', error);
|
||||
|
|
@ -361,46 +153,40 @@ end)`,
|
|||
|
||||
const handleFileMove = (fileId: string, targetParentId: string) => {
|
||||
try {
|
||||
setFiles((prev) => {
|
||||
let movedNode: FileNode | null = null;
|
||||
|
||||
// First, find and remove the node from its current location
|
||||
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.id === fileId) {
|
||||
movedNode = node;
|
||||
return false;
|
||||
}
|
||||
if (node.children) {
|
||||
node.children = removeNode(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// Then, add the node to the target folder
|
||||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === targetParentId && node.type === 'folder') {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), movedNode!],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addToTarget(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const withoutMoved = removeNode(prev || []);
|
||||
if (movedNode) {
|
||||
return addToTarget(withoutMoved);
|
||||
}
|
||||
return prev || [];
|
||||
});
|
||||
|
||||
const files = useEditorStore.getState().files;
|
||||
let movedNode: FileNode | null = null;
|
||||
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.id === fileId) {
|
||||
movedNode = node;
|
||||
return false;
|
||||
}
|
||||
if (node.children) {
|
||||
node.children = removeNode(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === targetParentId && node.type === 'folder') {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), movedNode!],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addToTarget(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
const withoutMoved = removeNode(files || []);
|
||||
if (movedNode) {
|
||||
setFiles(addToTarget(withoutMoved));
|
||||
} else {
|
||||
setFiles(files || []);
|
||||
}
|
||||
captureEvent('file_move', { fileId, targetParentId });
|
||||
} catch (error) {
|
||||
console.error('Failed to move file:', error);
|
||||
|
|
@ -409,251 +195,176 @@ end)`,
|
|||
}
|
||||
};
|
||||
|
||||
const handleFileClose = (id: string) => {
|
||||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||||
if (activeFileId === id) {
|
||||
const remaining = (openFiles || []).filter((f) => f.id !== id);
|
||||
setActiveFileId(remaining[0]?.id || '');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateProject = (config: ProjectConfig) => {
|
||||
try {
|
||||
if (!config.name || config.name.trim() === '') {
|
||||
toast.error('Project name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectFiles: FileNode[] = [
|
||||
{
|
||||
id: 'root',
|
||||
name: config.name,
|
||||
type: 'folder',
|
||||
children: [
|
||||
{
|
||||
id: `file-${Date.now()}`,
|
||||
name: 'main.lua',
|
||||
type: 'file',
|
||||
content: `-- ${config.name}\n-- Template: ${config.template}\n\nprint("Project initialized!")`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
setFiles(projectFiles);
|
||||
setOpenFiles([]);
|
||||
setActiveFileId('');
|
||||
|
||||
captureEvent('project_create', { name: config.name, template: config.template });
|
||||
toast.success(`Project "${config.name}" created successfully!`);
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
captureError(error as Error, { context: 'project_create', config });
|
||||
toast.error('Failed to create project. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// Example user stub for profile
|
||||
const demoUser = user || {
|
||||
login: 'demo-user',
|
||||
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
|
||||
email: 'demo@aethex.com',
|
||||
};
|
||||
// --- Render ---
|
||||
return (
|
||||
<div className="h-screen w-screen flex flex-col bg-background text-foreground">
|
||||
{/* Top Bar */}
|
||||
<div className="flex items-center h-12 px-4 bg-card border-b border-border shadow-sm z-20">
|
||||
<span className="text-xl font-bold tracking-tight select-none">
|
||||
Ae<span className="text-accent">Thex</span>
|
||||
</span>
|
||||
<span className="ml-2 text-sm text-muted-foreground font-medium select-none">Studio</span>
|
||||
<div className="flex-1" />
|
||||
<Toolbar
|
||||
code={currentCode}
|
||||
currentPlatform={currentPlatform}
|
||||
onPlatformChange={setCurrentPlatform}
|
||||
onTranslateClick={() => setShowTranslation(true)}
|
||||
onTemplatesClick={() => setShowTemplates(true)}
|
||||
onPreviewClick={() => setShowPreview(true)}
|
||||
onNewProjectClick={() => setShowNewProject(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
{/* Activity Bar */}
|
||||
<div className="flex flex-col items-center w-12 bg-muted border-r border-border py-2 gap-2 z-10">
|
||||
{/* Example activity icons, replace with real navigation/actions */}
|
||||
<button className="w-8 h-8 rounded flex items-center justify-center hover:bg-accent/20 text-accent" title="Files">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>
|
||||
</button>
|
||||
<button className="w-8 h-8 rounded flex items-center justify-center hover:bg-accent/20 text-accent" title="AI">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" /></svg>
|
||||
</button>
|
||||
<button className="w-8 h-8 rounded flex items-center justify-center hover:bg-accent/20 text-accent" title="Learn">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Panels */}
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
<ResizablePanelGroup orientation="horizontal">
|
||||
<ResizablePanel defaultSize={16} minSize={10} maxSize={25}>
|
||||
<FileTree
|
||||
files={files || []}
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileCreate={handleFileCreate}
|
||||
onFileRename={handleFileRename}
|
||||
onFileDelete={handleFileDelete}
|
||||
onFileMove={handleFileMove}
|
||||
selectedFileId={activeFileId}
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="h-screen w-screen bg-background flex flex-col">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Left Sidebar: Vertical tabs + Explorer */}
|
||||
<div className="w-16 h-full bg-[#20232A] border-r border-border flex flex-col items-center py-2 gap-2">
|
||||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Explorer">
|
||||
<span className="text-xl">📁</span>
|
||||
</button>
|
||||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Assets">
|
||||
<span className="text-xl">🗂️</span>
|
||||
</button>
|
||||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Search">
|
||||
<span className="text-xl">🔍</span>
|
||||
</button>
|
||||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Source Control">
|
||||
<span className="text-xl">🔗</span>
|
||||
</button>
|
||||
<button className="w-10 h-10 flex items-center justify-center rounded hover:bg-[#23272F]" title="Extensions">
|
||||
<span className="text-xl">🧩</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-56 h-full bg-card border-r border-border flex flex-col shadow-lg p-2">
|
||||
<Suspense fallback={<div className="p-4">Loading...</div>}>
|
||||
<FileTree />
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* Center: Tabs + Editor + Preview */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* File Tabs Row (no workspace tabs above) */}
|
||||
<div className="h-10 flex items-center bg-[#23272F] border-b border-border px-2">
|
||||
<FileTabs />
|
||||
</div>
|
||||
{/* Main Editor/Preview Split */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Toolbar
|
||||
code={code}
|
||||
onTemplatesClick={() => setShowTemplates(true)}
|
||||
onPreviewClick={() => setShowPreview(true)}
|
||||
onNewProjectClick={() => setShowNewProject(true)}
|
||||
currentPlatform={currentPlatform}
|
||||
onPlatformChange={setCurrentPlatform}
|
||||
onTranslateClick={() => setShowTranslation(true)}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
|
||||
<ResizablePanel defaultSize={54} minSize={30}>
|
||||
<div className="h-full flex flex-col">
|
||||
<FileTabs
|
||||
openFiles={openFiles || []}
|
||||
activeFileId={activeFileId}
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileClose={handleFileClose}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<CodeEditor onCodeChange={setCurrentCode} platform={currentPlatform} />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
{activeFileId ? (
|
||||
<CodeEditor />
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center text-center gap-6 bg-background/80">
|
||||
<div className="text-6xl">📂</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-2">Welcome to AeThex Studio</h2>
|
||||
<p className="text-gray-400 mb-4">Select a file from the explorer to start editing, or use the quick actions below.</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<button className="px-4 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700" onClick={() => setShowNewProject(true)}>New Project</button>
|
||||
<button className="px-4 py-2 rounded bg-green-600 text-white text-sm hover:bg-green-700" onClick={() => setShowTemplates(true)}>Templates</button>
|
||||
<button className="px-4 py-2 rounded bg-gray-700 text-white text-sm hover:bg-gray-800" onClick={() => setShowFileSearch(true)}>Open File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
|
||||
<ResizablePanel defaultSize={15} minSize={10}>
|
||||
<AIChat currentCode={currentCode} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
|
||||
<ResizablePanel defaultSize={15} minSize={10}>
|
||||
<EducationPanel />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
<div className="flex flex-col border-l border-border bg-card" style={{ minWidth: 320, maxWidth: 600, width: '40%', resize: 'horizontal', overflow: 'auto' }}>
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<h4 className="font-bold text-base mb-2">Preview</h4>
|
||||
<button className="text-xs text-gray-400 hover:text-white" onClick={() => setShowPreview((v) => !v)}>{showPreview ? 'Hide' : 'Show'}</button>
|
||||
</div>
|
||||
{showPreview && (
|
||||
<div className="flex-1 p-4">
|
||||
<CrossPlatformPreview />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between p-4 border-t border-border">
|
||||
<h4 className="font-bold text-base mb-2">Sync Monitor</h4>
|
||||
<button className="text-xs text-gray-400 hover:text-white" onClick={() => setShowTranslation((v) => !v)}>{showTranslation ? 'Hide' : 'Show'}</button>
|
||||
</div>
|
||||
{showTranslation && (
|
||||
<div className="p-4">
|
||||
<NexusSyncMonitor />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* ConsolePanel moved to top bar. */}
|
||||
</div>
|
||||
<ConsolePanel
|
||||
collapsed={consoleCollapsed}
|
||||
onToggle={() => setConsoleCollapsed(!consoleCollapsed)}
|
||||
currentCode={currentCode}
|
||||
currentFile={activeFileId ? (openFiles || []).find(f => f.id === activeFileId)?.name : undefined}
|
||||
files={files || []}
|
||||
onCodeChange={setCurrentCode}
|
||||
/>
|
||||
<SearchInFilesPanel
|
||||
files={files || []}
|
||||
onFileSelect={handleFileSelect}
|
||||
isOpen={showSearchInFiles}
|
||||
onClose={() => setShowSearchInFiles(false)}
|
||||
/>
|
||||
<div className="w-full border-t border-border mt-2">
|
||||
<ExtraTabs user={demoUser} />
|
||||
{/* Right Sidebar: Copilot, AI, Inspector, Trinity */}
|
||||
<div className="w-72 h-full border-l border-border bg-card flex flex-col shadow-lg p-2 gap-4">
|
||||
<Suspense fallback={<div className="p-4">Loading...</div>}>
|
||||
<div className="rounded-xl shadow-lg bg-background p-0 mb-2 h-full flex flex-col">
|
||||
<div className="flex border-b border-border">
|
||||
{['Copilot', 'AI', 'Inspector', 'Trinity'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`flex-1 py-2 text-xs font-bold uppercase tracking-wider border-b-2 transition-colors ${tab === rightSidebarTab ? 'border-blue-500 text-blue-400 bg-background' : 'border-transparent text-gray-400 bg-card hover:bg-muted/30'}`}
|
||||
onClick={() => setRightSidebarTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{rightSidebarTab === 'Copilot' && <StudioRightPanel />}
|
||||
{rightSidebarTab === 'AI' && <AIAssistant />}
|
||||
{rightSidebarTab === 'Inspector' && <div>Inspector/Properties coming soon…</div>}
|
||||
{rightSidebarTab === 'Trinity' && <StudioNetworkViz />}
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
{/* Problems Details Floating Panel */}
|
||||
{problemsExpanded && (
|
||||
<div className="fixed top-16 left-1/2 transform -translate-x-1/2 z-50 bg-card border border-border rounded-xl shadow-xl px-8 py-6 w-[480px]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-bold text-destructive">Problems</span>
|
||||
<button
|
||||
className="ml-4 px-3 py-1 rounded bg-muted text-xs hover:bg-muted/80 transition"
|
||||
onClick={() => setProblemsExpanded(false)}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{problems.map((p, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-sm">
|
||||
<span className={p.type === 'error' ? 'text-destructive' : 'text-yellow-500'}>
|
||||
{p.type === 'error' ? 'Error:' : 'Warning:'}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-muted-foreground">{p.file}:{p.line}</span>
|
||||
<span>{p.message}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{/* Modals and Drawers */}
|
||||
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading…</div>}>
|
||||
{showTemplates && <TemplatesDrawer onSelectTemplate={handleTemplateSelect} onClose={() => setShowTemplates(false)} currentPlatform={currentPlatform} />}
|
||||
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
|
||||
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />}
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Modals and overlays */}
|
||||
<Suspense fallback={null}>
|
||||
{showTemplates && (
|
||||
<TemplatesDrawer
|
||||
onSelectTemplate={handleTemplateSelect}
|
||||
onClose={() => setShowTemplates(false)}
|
||||
currentPlatform={currentPlatform}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading…</div>}>
|
||||
{showTemplates && <TemplatesDrawer onSelectTemplate={handleTemplateSelect} onClose={() => setShowTemplates(false)} currentPlatform={currentPlatform} />}
|
||||
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
|
||||
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />}
|
||||
{showTranslation && <TranslationPanel isOpen={showTranslation} onClose={() => setShowTranslation(false)} currentCode={currentCode} currentPlatform={currentPlatform} />}
|
||||
{showPassportLogin && <PassportLogin open={showPassportLogin} onClose={() => setShowPassportLogin(false)} onLoginSuccess={() => {}} />}
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<PreviewModal
|
||||
open={showPreview}
|
||||
onClose={() => setShowPreview(false)}
|
||||
code={currentCode}
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<NewProjectModal
|
||||
open={showNewProject}
|
||||
onClose={() => setShowNewProject(false)}
|
||||
onCreateProject={handleCreateProject}
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
{showTranslation && (
|
||||
<TranslationPanel
|
||||
isOpen={showTranslation}
|
||||
onClose={() => setShowTranslation(false)}
|
||||
currentCode={currentCode}
|
||||
currentPlatform={currentPlatform}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<WelcomeDialog />
|
||||
</Suspense>
|
||||
<FileSearchModal
|
||||
open={showFileSearch}
|
||||
onClose={() => setShowFileSearch(false)}
|
||||
files={files || []}
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
|
||||
<CommandPalette
|
||||
open={showCommandPalette}
|
||||
onClose={() => setShowCommandPalette(false)}
|
||||
commands={createDefaultCommands({
|
||||
onNewProject: () => setShowNewProject(true),
|
||||
onTemplates: () => setShowTemplates(true),
|
||||
onPreview: () => setShowPreview(true),
|
||||
onExport: async () => {
|
||||
const blob = new Blob([currentCode], { 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!');
|
||||
},
|
||||
onCopy: async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(currentCode);
|
||||
toast.success('Code copied to clipboard!');
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy code');
|
||||
}
|
||||
},
|
||||
})}
|
||||
commands={[
|
||||
{ id: 'new-project', label: 'New Project', description: 'Create a new project', icon: '📁', action: () => setShowNewProject(true) },
|
||||
{ id: 'templates', label: 'Templates', description: 'Open templates drawer', icon: '📄', action: () => setShowTemplates(true) },
|
||||
{ id: 'preview', label: 'Preview', description: 'Preview your code', icon: '👁️', action: () => setShowPreview(true) },
|
||||
{ id: 'export', label: 'Export', description: 'Export your project', icon: '⬇️', action: () => toast.success('Exported!') },
|
||||
{ id: 'copy', label: 'Copy', description: 'Copy code', icon: '📋', action: () => toast.success('Copied!') },
|
||||
]}
|
||||
/>
|
||||
{!user && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="fixed top-4 right-4 z-50"
|
||||
onClick={() => setShowPassportLogin(true)}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
)}
|
||||
{user && (
|
||||
<div className="fixed top-4 right-4 z-50 flex items-center gap-2 bg-card px-3 py-1 rounded shadow">
|
||||
<img src={user.avatarUrl} alt={user.login} className="h-6 w-6 rounded-full" />
|
||||
<span className="text-xs font-medium">{user.login}</span>
|
||||
<Button variant="ghost" size="sm" onClick={handleSignOut}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
<PassportLogin
|
||||
open={showPassportLogin}
|
||||
onClose={() => setShowPassportLogin(false)}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
/>
|
||||
</Suspense>
|
||||
<Toaster position="bottom-right" theme="dark" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { Button } from "./components/ui/button";
|
|||
|
||||
import { AlertTriangleIcon, RefreshCwIcon } from "lucide-react";
|
||||
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }) => {
|
||||
interface ErrorFallbackProps {
|
||||
error: { message: string };
|
||||
resetErrorBoundary: () => void;
|
||||
}
|
||||
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => {
|
||||
// When encountering an error in the development mode, rethrow it and don't display the boundary.
|
||||
// The parent UI will take care of showing a more helpful dialog.
|
||||
if (import.meta.env.DEV) throw error;
|
||||
if (process.env.NODE_ENV === 'development') throw error;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
|
|
|
|||
6
src/ai/dev.ts
Normal file
6
src/ai/dev.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { config } from 'dotenv';
|
||||
config();
|
||||
|
||||
import '@/ai/flows/ai-suggested-sync-conflict-resolution.ts';
|
||||
import '@/ai/flows/contextual-code-suggestions.ts';
|
||||
import '@/ai/flows/ai-help-from-prompt.ts';
|
||||
85
src/ai/flows/ai-help-from-prompt.ts
Normal file
85
src/ai/flows/ai-help-from-prompt.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"use server";
|
||||
|
||||
import { ai } from "../../ai/genkit";
|
||||
import { z } from "genkit";
|
||||
|
||||
const AIHelpFromPromptInputSchema = z.object({
|
||||
prompt: z.string().describe("A prompt describing the type of application to build."),
|
||||
});
|
||||
export type AIHelpFromPromptInput = z.infer<typeof AIHelpFromPromptInputSchema>;
|
||||
|
||||
const AIHelpFromPromptOutputSchema = z.object({
|
||||
suggestedFiles: z.array(z.object({
|
||||
filePath: z.string().describe("The path for the suggested file."),
|
||||
fileContent: z.string().describe("The content of the suggested file."),
|
||||
})).describe("An array of suggested code files and their content."),
|
||||
explanation: z.string().describe("An explanation of the suggested file structure and code."),
|
||||
});
|
||||
export type AIHelpFromPromptOutput = z.infer<typeof AIHelpFromPromptOutputSchema>;
|
||||
|
||||
const prompt = ai.definePrompt({
|
||||
name: "aiHelpFromPromptPrompt",
|
||||
input: { schema: AIHelpFromPromptInputSchema },
|
||||
output: { schema: AIHelpFromPromptOutputSchema },
|
||||
prompt: `You are an AI assistant designed to help new users quickly start developing applications.
|
||||
|
||||
Based on the user's prompt describing the desired application, suggest an initial set of code files and a project structure to get them started.
|
||||
|
||||
Provide the suggested files as an array of objects, each containing the file path and the file content.
|
||||
Explain the suggested file structure and the code in detail so that the user understands the purpose of each file and how they fit together.
|
||||
|
||||
User Prompt: {{{prompt}}}
|
||||
|
||||
Example Output:
|
||||
{
|
||||
"suggestedFiles": [
|
||||
{
|
||||
"filePath": "src/components/MyComponent.tsx",
|
||||
"fileContent": "// MyComponent.tsx\nimport React from 'react';\n\nconst MyComponent = () => {\n return (\n <div>\n <h1>Hello, world!</h1>\n </div>\n );\n};\n\nexport default MyComponent;"
|
||||
},
|
||||
{
|
||||
"filePath": "src/pages/index.tsx",
|
||||
"fileContent": "// index.tsx\nimport MyComponent from '../components/MyComponent';\n\nconst Home = () => {\n return (\n <div>\n <MyComponent />\n </div>\n );\n};\n\nexport default Home;"
|
||||
}
|
||||
],
|
||||
"explanation": "This project structure includes a component (MyComponent.tsx) and a page (index.tsx) that uses the component. This is a basic structure for a React application."
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const aiHelpFromPromptFlow = ai.defineFlow(
|
||||
{
|
||||
name: "aiHelpFromPromptFlow",
|
||||
inputSchema: AIHelpFromPromptInputSchema,
|
||||
outputSchema: AIHelpFromPromptOutputSchema,
|
||||
},
|
||||
async (input) => {
|
||||
const { output } = await prompt(input, {
|
||||
config: {
|
||||
safetySettings: [
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "BLOCK_MEDIUM_AND_ABOVE",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "BLOCK_LOW_AND_ABOVE",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return output!;
|
||||
}
|
||||
);
|
||||
|
||||
export async function aiHelpFromPrompt(input: AIHelpFromPromptInput): Promise<AIHelpFromPromptOutput> {
|
||||
return aiHelpFromPromptFlow(input);
|
||||
}
|
||||
57
src/ai/flows/ai-suggested-sync-conflict-resolution.ts
Normal file
57
src/ai/flows/ai-suggested-sync-conflict-resolution.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
import {ai} from '../../ai/genkit';
|
||||
import {z} from 'genkit';
|
||||
|
||||
const AISuggestedSyncConflictResolutionInputSchema = z.object({
|
||||
robloxCode: z.string().describe('The Lua code for the Roblox platform.'),
|
||||
webCode: z.string().describe('The JavaScript code for the web platform.'),
|
||||
mobileCode: z.string().describe('The React Native code for the mobile platform.'),
|
||||
sharedState: z.string().describe('The shared state data in JSON format.'),
|
||||
});
|
||||
export type AISuggestedSyncConflictResolutionInput = z.infer<typeof AISuggestedSyncConflictResolutionInputSchema>;
|
||||
|
||||
const AISuggestedSyncConflictResolutionOutputSchema = z.object({
|
||||
conflictDetected: z.boolean().describe('Whether a synchronization conflict was detected.'),
|
||||
suggestedSolutions: z.array(z.string()).describe('An array of suggested solutions to resolve the conflicts.'),
|
||||
explanation: z.string().describe('Explanation of the detected conflicts and suggested solutions.'),
|
||||
});
|
||||
export type AISuggestedSyncConflictResolutionOutput = z.infer<typeof AISuggestedSyncConflictResolutionOutputSchema>;
|
||||
|
||||
const prompt = ai.definePrompt({
|
||||
name: 'aiSuggestedSyncConflictResolutionPrompt',
|
||||
input: {schema: AISuggestedSyncConflictResolutionInputSchema},
|
||||
output: {schema: AISuggestedSyncConflictResolutionOutputSchema},
|
||||
prompt: `You are an AI assistant specialized in detecting synchronization conflicts between different platform codebases and suggesting solutions.
|
||||
|
||||
You are given the code for Roblox (Lua), Web (JavaScript), and Mobile (React Native), as well as the shared state data in JSON format. Analyze the code and the shared state to identify any inconsistencies or conflicts.
|
||||
|
||||
Based on your analysis, determine if there are any conflicts, and suggest solutions to resolve them. Explain the conflicts and the suggested solutions in detail.
|
||||
|
||||
Roblox Code:
|
||||
{{robloxCode}}
|
||||
|
||||
Web Code:
|
||||
{{webCode}}
|
||||
|
||||
Mobile Code:
|
||||
{{mobileCode}}
|
||||
|
||||
Shared State:
|
||||
{{sharedState}}`,
|
||||
});
|
||||
|
||||
const aiSuggestedSyncConflictResolutionFlow = ai.defineFlow(
|
||||
{
|
||||
name: 'aiSuggestedSyncConflictResolutionFlow',
|
||||
inputSchema: AISuggestedSyncConflictResolutionInputSchema,
|
||||
outputSchema: AISuggestedSyncConflictResolutionOutputSchema,
|
||||
},
|
||||
async (input: AISuggestedSyncConflictResolutionInput) => {
|
||||
const {output} = await prompt(input);
|
||||
return output!;
|
||||
}
|
||||
);
|
||||
|
||||
export async function aiSuggestedSyncConflictResolution(input: AISuggestedSyncConflictResolutionInput): Promise<AISuggestedSyncConflictResolutionOutput> {
|
||||
return aiSuggestedSyncConflictResolutionFlow(input);
|
||||
}
|
||||
77
src/ai/flows/contextual-code-suggestions.ts
Normal file
77
src/ai/flows/contextual-code-suggestions.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
'use server';
|
||||
/**
|
||||
* @fileOverview This file defines a Genkit flow for providing contextual code suggestions.
|
||||
*
|
||||
* - contextualCodeSuggestions - A function that takes the current file content and cursor position
|
||||
* and returns code suggestions.
|
||||
* - ContextualCodeSuggestionsInput - The input type for the contextualCodeSuggestions function.
|
||||
* - ContextualCodeSuggestionsOutput - The return type for the contextualCodeSuggestions function.
|
||||
*/
|
||||
|
||||
import {ai} from '../../ai/genkit';
|
||||
import {z} from 'genkit';
|
||||
|
||||
const ContextualCodeSuggestionsInputSchema = z.object({
|
||||
fileContent: z.string().describe('The content of the currently open file.'),
|
||||
cursorPosition: z.number().describe('The cursor position within the file.'),
|
||||
language: z.string().describe('The programming language of the file.'),
|
||||
context: z.string().optional().describe('Additional context for code suggestions, e.g., error messages or related code snippets.'),
|
||||
});
|
||||
export type ContextualCodeSuggestionsInput = z.infer<
|
||||
typeof ContextualCodeSuggestionsInputSchema
|
||||
>;
|
||||
|
||||
const ContextualCodeSuggestionsOutputSchema = z.object({
|
||||
suggestions: z
|
||||
.array(z.string())
|
||||
.describe('An array of code suggestions based on the context.'),
|
||||
});
|
||||
export type ContextualCodeSuggestionsOutput = z.infer<
|
||||
typeof ContextualCodeSuggestionsOutputSchema
|
||||
>;
|
||||
|
||||
export async function contextualCodeSuggestions(
|
||||
input: ContextualCodeSuggestionsInput
|
||||
): Promise<ContextualCodeSuggestionsOutput> {
|
||||
return contextualCodeSuggestionsFlow(input);
|
||||
}
|
||||
|
||||
const prompt = ai.definePrompt({
|
||||
name: 'contextualCodeSuggestionsPrompt',
|
||||
input: {schema: ContextualCodeSuggestionsInputSchema},
|
||||
output: {schema: ContextualCodeSuggestionsOutputSchema},
|
||||
prompt: `You are an AI assistant that provides code suggestions and autocompletions based on the context of the currently open file and cursor position.
|
||||
|
||||
Given the following file content, cursor position, programming language, and any available context, provide a list of code suggestions that would be helpful to the developer.
|
||||
|
||||
File Content:
|
||||
{{fileContent}}
|
||||
|
||||
Cursor Position: {{cursorPosition}}
|
||||
|
||||
Programming Language: {{language}}
|
||||
|
||||
Context: {{context}}
|
||||
|
||||
Suggestions should be relevant to the current context, incorporate best practices, and avoid common mistakes. Return the suggestions as an array of strings.
|
||||
|
||||
Example:
|
||||
[
|
||||
"console.log('Hello, world!');",
|
||||
"// Add a comment to explain the code",
|
||||
"function myFunction() {\n // Function body\n }",
|
||||
]`,
|
||||
});
|
||||
|
||||
const contextualCodeSuggestionsFlow = ai.defineFlow(
|
||||
{
|
||||
name: 'contextualCodeSuggestionsFlow',
|
||||
inputSchema: ContextualCodeSuggestionsInputSchema,
|
||||
outputSchema: ContextualCodeSuggestionsOutputSchema,
|
||||
},
|
||||
async (input: ContextualCodeSuggestionsInput) => {
|
||||
const {output} = await prompt(input);
|
||||
return output!;
|
||||
}
|
||||
);
|
||||
'use server';
|
||||
7
src/ai/genkit.ts
Normal file
7
src/ai/genkit.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {genkit} from 'genkit';
|
||||
import {googleAI} from '@genkit-ai/google-genai';
|
||||
|
||||
export const ai = genkit({
|
||||
plugins: [googleAI()],
|
||||
model: 'googleai/gemini-2.5-flash',
|
||||
});
|
||||
66
src/app/auth/auth-code-error/page.tsx
Normal file
66
src/app/auth/auth-code-error/page.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export default function AuthCodeErrorPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-red-500/20 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold text-white mb-2">
|
||||
Authentication Error
|
||||
</h1>
|
||||
|
||||
<p className="text-[#888] mb-8">
|
||||
Something went wrong during sign in. This could happen if:
|
||||
</p>
|
||||
|
||||
<ul className="text-left text-[#888] mb-8 space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500">•</span>
|
||||
<span>The sign in link expired or was already used</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500">•</span>
|
||||
<span>You denied access to your account</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500">•</span>
|
||||
<span>There was a network error during authentication</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="block w-full py-3 px-4 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-medium rounded-lg hover:opacity-90 transition"
|
||||
>
|
||||
Try Again
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="block w-full py-3 px-4 bg-[#1a1a1a] text-[#888] font-medium rounded-lg hover:bg-[#222] transition"
|
||||
>
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/app/auth/callback/route.ts
Normal file
21
src/app/auth/callback/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
// Default redirect to /ide after successful OAuth login
|
||||
const next = searchParams.get('next') ?? '/ide';
|
||||
|
||||
if (code) {
|
||||
const supabase = await createClient();
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
||||
if (!error) {
|
||||
return NextResponse.redirect(`${origin}${next}`);
|
||||
}
|
||||
console.error('OAuth callback error:', error.message);
|
||||
}
|
||||
|
||||
// Return the user to an error page with instructions
|
||||
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
|
||||
}
|
||||
169
src/app/auth/login/page.tsx
Normal file
169
src/app/auth/login/page.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSupabaseAuth } from "@/hooks/use-supabase";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { signIn, signInWithOAuth } = useSupabaseAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
const { error } = await signIn(email, password);
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
setLoading(false);
|
||||
} else {
|
||||
router.push("/ide");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuth = async (provider: "github" | "google" | "discord") => {
|
||||
setError("");
|
||||
const { error } = await signInWithOAuth(provider);
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">AeThex Studio</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
|
||||
<h1 className="text-2xl font-semibold text-white text-center mb-2">Welcome back</h1>
|
||||
<p className="text-[#888] text-center mb-8">Sign in to your AeThex account</p>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Buttons */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<button
|
||||
onClick={() => handleOAuth("github")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOAuth("google")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOAuth("discord")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Continue with Discord
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-[#333]"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-[#141414] text-[#666]">or continue with email</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[#888] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[#888] mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Signing in..." : "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-[#888] text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/auth/signup" className="text-purple-400 hover:text-purple-300 transition-colors">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Ecosystem Links */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-[#666] text-xs mb-3">Part of the AeThex ecosystem</p>
|
||||
<div className="flex items-center justify-center gap-4 text-sm">
|
||||
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
|
||||
<span className="text-[#444]">•</span>
|
||||
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
|
||||
<span className="text-[#444]">•</span>
|
||||
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
230
src/app/auth/signup/page.tsx
Normal file
230
src/app/auth/signup/page.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSupabaseAuth } from "@/hooks/use-supabase";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
const { signUp, signInWithOAuth } = useSupabaseAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError("Password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const { error } = await signUp(email, password);
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuth = async (provider: "github" | "google" | "discord") => {
|
||||
setError("");
|
||||
const { error } = await signInWithOAuth(provider);
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
|
||||
<div className="h-16 w-16 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-6">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">Check your email</h1>
|
||||
<p className="text-[#888] mb-6">
|
||||
We sent a confirmation link to <span className="text-white">{email}</span>
|
||||
</p>
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="inline-block bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-6 rounded-lg transition-colors"
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">AeThex Studio</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
|
||||
<h1 className="text-2xl font-semibold text-white text-center mb-2">Create your account</h1>
|
||||
<p className="text-[#888] text-center mb-8">Join the AeThex ecosystem</p>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Buttons */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<button
|
||||
onClick={() => handleOAuth("github")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOAuth("google")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOAuth("discord")}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Continue with Discord
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-[#333]"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-[#141414] text-[#666]">or continue with email</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[#888] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[#888] mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#888] mb-2">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Creating account..." : "Create account"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-[#888] text-sm">
|
||||
Already have an account?{" "}
|
||||
<Link href="/auth/login" className="text-purple-400 hover:text-purple-300 transition-colors">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Terms */}
|
||||
<p className="mt-6 text-center text-[#666] text-xs">
|
||||
By creating an account, you agree to our{" "}
|
||||
<Link href="/terms" className="text-[#888] hover:text-white transition-colors">Terms of Service</Link>
|
||||
{" "}and{" "}
|
||||
<Link href="/privacy" className="text-[#888] hover:text-white transition-colors">Privacy Policy</Link>
|
||||
</p>
|
||||
|
||||
{/* Ecosystem Links */}
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-[#666] text-xs mb-3">Part of the AeThex ecosystem</p>
|
||||
<div className="flex items-center justify-center gap-4 text-sm">
|
||||
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
|
||||
<span className="text-[#444]">•</span>
|
||||
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
|
||||
<span className="text-[#444]">•</span>
|
||||
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
5
src/app/dashboard/page.tsx
Normal file
5
src/app/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { DashboardPage } from "../../components/aethex/dashboard-page";
|
||||
|
||||
export default function Page() {
|
||||
return <DashboardPage />;
|
||||
}
|
||||
145
src/app/globals.css
Normal file
145
src/app/globals.css
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 262 83% 58%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 6% 6%;
|
||||
--foreground: 0 0% 95%;
|
||||
--card: 240 6% 8%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
--popover: 240 6% 8%;
|
||||
--popover-foreground: 0 0% 95%;
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 5% 12%;
|
||||
--secondary-foreground: 0 0% 95%;
|
||||
--muted: 240 5% 14%;
|
||||
--muted-foreground: 240 5% 55%;
|
||||
--accent: 240 5% 14%;
|
||||
--accent-foreground: 0 0% 95%;
|
||||
--destructive: 0 62.8% 45%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 4% 16%;
|
||||
--input: 240 4% 16%;
|
||||
--ring: 262 83% 58%;
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar - minimal and modern */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* IDE-specific scrollbar */
|
||||
.ide-scroll::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.ide-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ide-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
* {
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
/* Selection styling */
|
||||
::selection {
|
||||
background: rgba(139, 92, 246, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Focus rings */
|
||||
:focus-visible {
|
||||
outline: 2px solid rgba(139, 92, 246, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Hide scrollbar utility */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
9
src/app/ide/page.tsx
Normal file
9
src/app/ide/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { AethexStudio } from "@/src/components/aethex/aethex-studio";
|
||||
|
||||
export default function IdePage() {
|
||||
return (
|
||||
<main className="h-[100svh] w-screen overflow-hidden bg-background">
|
||||
<AethexStudio />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
src/app/landing-page.tsx
Normal file
34
src/app/landing-page.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-gray-950 text-white p-8">
|
||||
<div className="max-w-xl w-full flex flex-col items-center">
|
||||
<img src="/logo.svg" alt="AeThex Studio Logo" className="w-24 h-24 mb-6" />
|
||||
<h1 className="text-4xl font-bold mb-4 text-center">Welcome to AeThex Studio</h1>
|
||||
<p className="text-lg mb-8 text-center opacity-80">
|
||||
The Next-Generation Cross-Platform IDE for Creators, Developers, and Teams.
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<a
|
||||
href="https://aethex.com/download"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg text-center transition-colors shadow-lg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Download AeThex Studio
|
||||
</a>
|
||||
<Link
|
||||
href="/ide"
|
||||
className="bg-gray-800 hover:bg-gray-700 text-white font-semibold py-3 rounded-lg text-center transition-colors border border-gray-700"
|
||||
>
|
||||
Open in Browser
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-8 text-sm text-gray-400 text-center">
|
||||
Need help? <a href="https://aethex.com/docs" className="underline hover:text-blue-400">Read the Docs</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
35
src/app/layout.tsx
Normal file
35
src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { Metadata } from "next";
|
||||
import Toaster from "../components/ui/toaster";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AeThex Studio",
|
||||
description: "The Next-Generation Cross-Platform IDE",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
458
src/app/page.tsx
Normal file
458
src/app/page.tsx
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSupabaseAuth, useProfile } from "@/hooks/use-supabase";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { user, loading } = useSupabaseAuth();
|
||||
const { profile } = useProfile();
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
const handlePromptSubmit = () => {
|
||||
if (prompt.trim()) {
|
||||
sessionStorage.setItem("aethex_prompt", prompt);
|
||||
router.push("/ide");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a]">
|
||||
{/* Fixed Navbar */}
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-[#0a0a0a]/90 backdrop-blur-md border-b border-[#1a1a1a]">
|
||||
<div className="flex h-14 items-center justify-between px-4 md:px-10 max-w-6xl mx-auto">
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-white">AeThex Studio</span>
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<a href="#features" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Features</a>
|
||||
<a href="#platforms" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Platforms</a>
|
||||
<a href="#pricing" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Pricing</a>
|
||||
<a href="https://github.com/AeThex-LABS/aethex-studio" target="_blank" rel="noopener noreferrer" className="text-sm font-medium text-[#666] hover:text-white transition-colors">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{!loading && (
|
||||
<>
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/profile" className="flex items-center gap-2 text-sm text-[#666] hover:text-white transition-colors">
|
||||
<div className="h-7 w-7 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center text-xs font-medium text-white">
|
||||
{profile?.username?.[0]?.toUpperCase() || user.email?.[0]?.toUpperCase() || "?"}
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/ide" className="rounded-full bg-white px-5 py-2 text-sm font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">
|
||||
Launch IDE
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/auth/login" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Sign in</Link>
|
||||
<Link href="/auth/signup" className="rounded-full bg-white px-5 py-2 text-sm font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">Get started</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section - Prompt First */}
|
||||
<section className="relative flex flex-col items-center px-6 pt-32 pb-20 md:pt-44 md:pb-28 overflow-hidden">
|
||||
<div className="pointer-events-none absolute inset-0" style={{
|
||||
background: "radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 119, 198, 0.3), transparent)"
|
||||
}} />
|
||||
|
||||
<h1 className="relative max-w-4xl text-center text-4xl font-bold tracking-tight text-white md:text-6xl lg:text-7xl">
|
||||
Build games for{" "}
|
||||
<span className="bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">
|
||||
every platform
|
||||
</span>
|
||||
</h1>
|
||||
<p className="relative mt-6 max-w-2xl text-center text-lg leading-relaxed text-[#888] md:text-xl">
|
||||
The AI-powered cross-platform game IDE. Write once, deploy to Roblox, UEFN, Spatial, and the Web.
|
||||
</p>
|
||||
|
||||
{/* Prompt Input Box */}
|
||||
<div className="relative mt-10 w-full max-w-2xl px-4">
|
||||
<div className="bg-[#111] border border-[#222] rounded-2xl p-4 shadow-2xl shadow-purple-500/5">
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handlePromptSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder="Describe your game... e.g. 'Create a tower defense game with wave spawning'"
|
||||
className="w-full min-h-[60px] bg-transparent text-white text-base resize-none outline-none placeholder-[#555]"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-[#222]">
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#1a1a1a] text-[#666] text-sm hover:bg-[#222] hover:text-white transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" /></svg>
|
||||
Attach
|
||||
</button>
|
||||
<button className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#1a1a1a] text-[#666] text-sm hover:bg-[#222] hover:text-white transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect width="18" height="7" x="3" y="3" rx="1"/><rect width="9" height="7" x="3" y="14" rx="1"/><rect width="5" height="7" x="16" y="14" rx="1"/></svg>
|
||||
Template
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePromptSubmit}
|
||||
disabled={!prompt.trim()}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-white text-[#0a0a0a] disabled:bg-[#333] disabled:text-[#666] disabled:cursor-not-allowed transition-colors hover:bg-[#e5e5e5]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform badges */}
|
||||
<div className="relative mt-12 flex flex-wrap items-center justify-center gap-3">
|
||||
{[
|
||||
{ name: "Roblox", color: "#e11d48" },
|
||||
{ name: "UEFN", color: "#8b5cf6" },
|
||||
{ name: "Spatial", color: "#10b981" },
|
||||
{ name: "Web", color: "#3b82f6" },
|
||||
].map((platform) => (
|
||||
<div key={platform.name} className="flex items-center gap-2 rounded-full border border-[#222] bg-[#111] px-4 py-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: platform.color }} />
|
||||
<span className="text-sm font-medium text-white">{platform.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Banner */}
|
||||
<section className="border-y border-[#1a1a1a] bg-[#0d0d0d]">
|
||||
<div className="mx-auto flex max-w-5xl flex-col items-center justify-center gap-8 px-6 py-10 md:flex-row md:gap-16">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">4</p>
|
||||
<p className="mt-1 text-sm text-[#666]">Platforms supported</p>
|
||||
</div>
|
||||
<div className="hidden h-8 w-px bg-[#222] md:block" />
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">AI-Powered</p>
|
||||
<p className="mt-1 text-sm text-[#666]">Code translation</p>
|
||||
</div>
|
||||
<div className="hidden h-8 w-px bg-[#222] md:block" />
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">Open Source</p>
|
||||
<p className="mt-1 text-sm text-[#666]">MIT Licensed</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why AeThex vs Others */}
|
||||
<section className="px-6 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-purple-400">Why AeThex?</p>
|
||||
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
|
||||
One IDE. Every platform.
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-[#888]">
|
||||
While others lock you into one platform, AeThex lets you build for all of them
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="rounded-2xl border-2 border-purple-500/50 bg-gradient-to-b from-purple-500/10 to-transparent p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-white">AeThex Studio</span>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
"Roblox + UEFN + Spatial + Web",
|
||||
"Full browser-based IDE",
|
||||
"AI translates between platforms",
|
||||
"Write once, deploy everywhere",
|
||||
"Visual UI builder for all platforms",
|
||||
"Open source (MIT license)",
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-white">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[#222] bg-[#111] p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="h-10 w-10 rounded-lg bg-[#222] flex items-center justify-center text-[#666]">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/></svg>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-[#666]">Single-platform tools</span>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
"Roblox only OR UEFN only",
|
||||
"Generates code snippets",
|
||||
"No cross-platform translation",
|
||||
"Locked to one ecosystem",
|
||||
"Platform-specific UI code",
|
||||
"Proprietary / closed source",
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-[#666]">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section id="features" className="border-t border-[#1a1a1a] bg-[#0d0d0d] px-6 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Features</p>
|
||||
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
|
||||
Everything you need to ship games
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[
|
||||
{ title: "AI Code Generation", desc: "Describe what you want in plain English. Get production-ready code instantly.", icon: "code" },
|
||||
{ title: "Cross-Platform Translation", desc: "Write in Luau, TypeScript, or C#. Deploy to any supported platform.", icon: "translate" },
|
||||
{ title: "Visual UI Builder", desc: "Design game UIs visually. Export to platform-native components.", icon: "ui" },
|
||||
{ title: "Live Preview", desc: "See changes across all platforms in real-time. Instant feedback loop.", icon: "preview" },
|
||||
{ title: "Studio Sync", desc: "Sync directly to Roblox Studio, UEFN, and more via plugin.", icon: "sync" },
|
||||
{ title: "Asset Library", desc: "Thousands of free assets, sounds, and scripts ready to use.", icon: "assets" },
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="group rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6 transition-all hover:border-[#333] hover:bg-[#111]">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-[#1a1a1a] text-white transition-all group-hover:bg-purple-500">
|
||||
{feature.icon === "code" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>}
|
||||
{feature.icon === "translate" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>}
|
||||
{feature.icon === "ui" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>}
|
||||
{feature.icon === "preview" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>}
|
||||
{feature.icon === "sync" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2a5 5 0 0 1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8a5 5 0 0 1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>}
|
||||
{feature.icon === "assets" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">{feature.title}</h3>
|
||||
<p className="mt-2 text-sm text-[#888] leading-relaxed">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Platforms */}
|
||||
<section id="platforms" className="px-6 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Platforms</p>
|
||||
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
|
||||
Deploy everywhere
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
{ name: "Roblox", color: "#e11d48", lang: "Luau", users: "70M+ daily players" },
|
||||
{ name: "UEFN", color: "#8b5cf6", lang: "Verse", users: "Fortnite Creative" },
|
||||
{ name: "Spatial", color: "#10b981", lang: "C#", users: "Metaverse platform" },
|
||||
{ name: "Web", color: "#3b82f6", lang: "TypeScript", users: "HTML5 Canvas" },
|
||||
].map((platform) => (
|
||||
<div key={platform.name} className="rounded-2xl border border-[#1a1a1a] bg-[#0d0d0d] p-6 text-center hover:border-[#333] transition-colors">
|
||||
<div className="mx-auto mb-4 h-16 w-16 rounded-2xl flex items-center justify-center" style={{ backgroundColor: platform.color + "15" }}>
|
||||
<div className="h-8 w-8 rounded-lg" style={{ backgroundColor: platform.color }} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white">{platform.name}</h3>
|
||||
<p className="mt-1 text-sm text-purple-400 font-medium">{platform.lang}</p>
|
||||
<p className="mt-2 text-xs text-[#666]">{platform.users}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section id="pricing" className="border-t border-[#1a1a1a] bg-[#0d0d0d] px-6 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Pricing</p>
|
||||
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
|
||||
Simple, transparent pricing
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-xl text-lg text-[#888]">
|
||||
Start free. Upgrade when you need more power.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
|
||||
<h3 className="text-xl font-bold text-white">Free</h3>
|
||||
<p className="mt-2 text-sm text-[#666] h-10">Get started with AeThex</p>
|
||||
<p className="mt-4"><span className="text-4xl font-bold text-white">$0</span><span className="text-[#666] ml-1">/mo</span></p>
|
||||
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
|
||||
Get Started
|
||||
</Link>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{["50 AI generations/month", "2 projects", "Community support", "All 4 platforms"].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border-2 border-purple-500 bg-gradient-to-b from-purple-500/10 to-[#0a0a0a] p-6 relative">
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-purple-500 rounded-full text-xs font-medium text-white">Popular</div>
|
||||
<h3 className="text-xl font-bold text-white">Pro</h3>
|
||||
<p className="mt-2 text-sm text-[#666] h-10">For serious game developers</p>
|
||||
<p className="mt-4"><span className="text-4xl font-bold text-white">$15</span><span className="text-[#666] ml-1">/mo</span></p>
|
||||
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg bg-purple-500 py-2.5 text-center text-sm font-medium text-white hover:bg-purple-600 transition-colors">
|
||||
Get Started
|
||||
</Link>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{["500 AI generations/month", "Unlimited projects", "Priority support", "Advanced AI models", "Team collaboration"].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
|
||||
<h3 className="text-xl font-bold text-white">Team</h3>
|
||||
<p className="mt-2 text-sm text-[#666] h-10">For studios and teams</p>
|
||||
<p className="mt-4"><span className="text-4xl font-bold text-white">$40</span><span className="text-[#666] ml-1">/mo</span></p>
|
||||
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
|
||||
Get Started
|
||||
</Link>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{["2000 AI generations/month", "Shared team workspace", "Admin controls", "Custom templates", "Dedicated support"].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
|
||||
<h3 className="text-xl font-bold text-white">Enterprise</h3>
|
||||
<p className="mt-2 text-sm text-[#666] h-10">Custom solutions at scale</p>
|
||||
<p className="mt-4"><span className="text-4xl font-bold text-white">Custom</span></p>
|
||||
<a href="mailto:enterprise@aethex.studio" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
|
||||
Contact Sales
|
||||
</a>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{["Unlimited generations", "SLA guarantee", "On-premise option", "Custom integrations", "24/7 support"].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Final CTA */}
|
||||
<section className="px-6 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white md:text-5xl">
|
||||
Ready to build cross-platform games?
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-xl text-lg text-[#888]">
|
||||
Join thousands of developers building the next generation of games.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Link href="/ide" className="flex items-center gap-2 rounded-full bg-white px-8 py-3 text-base font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
Launch IDE
|
||||
</Link>
|
||||
<a href="https://github.com/AeThex-LABS/aethex-studio" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 rounded-full border border-[#333] px-8 py-3 text-base font-medium text-white transition-all hover:bg-[#1a1a1a]">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-[#1a1a1a] bg-[#0a0a0a]">
|
||||
<div className="mx-auto max-w-6xl px-6 py-12">
|
||||
<div className="grid gap-8 md:grid-cols-4">
|
||||
<div className="md:col-span-1">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-white">AeThex</span>
|
||||
</div>
|
||||
<p className="text-sm text-[#666]">The cross-platform game development IDE.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-4">Product</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><Link href="/ide" className="text-[#666] hover:text-white transition-colors">IDE</Link></li>
|
||||
<li><a href="#features" className="text-[#666] hover:text-white transition-colors">Features</a></li>
|
||||
<li><a href="#pricing" className="text-[#666] hover:text-white transition-colors">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-4">Ecosystem</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a href="https://aethex.dev" className="text-[#666] hover:text-white transition-colors">aethex.dev</a></li>
|
||||
<li><a href="https://aethex.foundation" className="text-[#666] hover:text-white transition-colors">aethex.foundation</a></li>
|
||||
<li><a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-4">Legal</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><Link href="/terms" className="text-[#666] hover:text-white transition-colors">Terms</Link></li>
|
||||
<li><Link href="/privacy" className="text-[#666] hover:text-white transition-colors">Privacy</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-[#1a1a1a] flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p className="text-xs text-[#444]">© 2026 AeThex Labs. All rights reserved.</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<a href="https://github.com/AeThex-LABS" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/aethex" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
</a>
|
||||
<a href="https://x.com/aethexlabs" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
249
src/app/profile/page.tsx
Normal file
249
src/app/profile/page.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSupabaseAuth, useProfile } from "@/hooks/use-supabase";
|
||||
import { getSupabase } from "@/lib/supabase/client";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const { user, loading: authLoading, signOut } = useSupabaseAuth();
|
||||
const { profile, loading: profileLoading } = useProfile();
|
||||
const [username, setUsername] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}, [user, authLoading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.username) {
|
||||
setUsername(profile.username);
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!user) return;
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
|
||||
const supabase = getSupabase();
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({ username, updated_at: new Date().toISOString() })
|
||||
.eq("id", user.id);
|
||||
|
||||
if (error) {
|
||||
setMessage("Error saving profile: " + error.message);
|
||||
} else {
|
||||
setMessage("Profile saved successfully!");
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut();
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
if (authLoading || profileLoading) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<div className="animate-spin h-8 w-8 border-2 border-purple-500 border-t-transparent rounded-full"></div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subscriptionColors: Record<string, string> = {
|
||||
free: "bg-gray-500",
|
||||
studio: "bg-blue-500",
|
||||
pro: "bg-purple-500",
|
||||
enterprise: "bg-gradient-to-r from-purple-500 to-blue-500",
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a]">
|
||||
{/* Header */}
|
||||
<nav className="border-b border-[#222] bg-[#141414]">
|
||||
<div className="max-w-4xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-white">AeThex Studio</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/ide" className="text-sm text-[#888] hover:text-white transition-colors">
|
||||
Open IDE
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className="text-sm text-[#888] hover:text-white transition-colors"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
<h1 className="text-3xl font-bold text-white mb-8">Profile</h1>
|
||||
|
||||
{message && (
|
||||
<div className={`px-4 py-3 rounded-lg mb-6 text-sm ${
|
||||
message.includes("Error")
|
||||
? "bg-red-500/10 border border-red-500/20 text-red-400"
|
||||
: "bg-green-500/10 border border-green-500/20 text-green-400"
|
||||
}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Profile Card */}
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center text-2xl font-bold text-white">
|
||||
{username?.[0]?.toUpperCase() || user.email?.[0]?.toUpperCase() || "?"}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{username || "Set your username"}
|
||||
</h2>
|
||||
<p className="text-[#888] text-sm">{user.email}</p>
|
||||
<div className="mt-2">
|
||||
<span className={`inline-block px-2 py-1 rounded text-xs font-medium text-white ${subscriptionColors[profile?.subscription_tier || "free"]}`}>
|
||||
{(profile?.subscription_tier || "free").toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#888] mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#888] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={user.email || ""}
|
||||
disabled
|
||||
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-[#666] cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save changes"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-6">Usage</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[#888]">Translations used</span>
|
||||
<span className="text-white font-medium">{profile?.translation_count || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[#888]">Account created</span>
|
||||
<span className="text-white font-medium">
|
||||
{profile?.created_at ? new Date(profile.created_at).toLocaleDateString() : "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[#888]">Subscription</span>
|
||||
<span className="text-white font-medium capitalize">{profile?.subscription_tier || "Free"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profile?.subscription_tier === "free" && (
|
||||
<div className="mt-6 p-4 bg-purple-500/10 border border-purple-500/20 rounded-lg">
|
||||
<p className="text-purple-400 text-sm mb-3">Upgrade to Pro for unlimited translations</p>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="inline-block bg-purple-500 hover:bg-purple-600 text-white font-medium py-2 px-4 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
View plans
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connected Accounts */}
|
||||
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6 md:col-span-2">
|
||||
<h3 className="text-lg font-semibold text-white mb-6">Connected accounts</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-white font-medium">GitHub</p>
|
||||
<p className="text-[#666] text-xs">Not connected</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-white font-medium">Google</p>
|
||||
<p className="text-[#666] text-xs">Not connected</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#5865F2">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-white font-medium">Discord</p>
|
||||
<p className="text-[#666] text-xs">Not connected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ecosystem Links */}
|
||||
<div className="mt-12 text-center border-t border-[#222] pt-8">
|
||||
<p className="text-[#666] text-xs mb-3">Your AeThex account works across</p>
|
||||
<div className="flex items-center justify-center gap-6 text-sm">
|
||||
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
|
||||
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
|
||||
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Sparkle, PaperPlaneRight } from '@phosphor-icons/react';
|
||||
import { toast } from 'sonner';
|
||||
import { captureError } from '@/lib/sentry';
|
||||
import { captureError } from '../lib/sentry';
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import Editor from '@monaco-editor/react';
|
||||
import { usePersistentState } from '@/lib/usePersistentState';
|
||||
import { usePersistentState } from '../lib/usePersistentState';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { LoadingSpinner } from './ui/loading-spinner';
|
||||
import { toast } from 'sonner';
|
||||
import { PlatformId } from '@/lib/platforms';
|
||||
import { PlatformId } from '../lib/platforms';
|
||||
|
||||
interface CodeEditorProps {
|
||||
onCodeChange?: (code: string) => void;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface Command {
|
|||
action: () => void;
|
||||
keywords?: string[];
|
||||
}
|
||||
export default CommandPalette;
|
||||
|
||||
interface CommandPaletteProps {
|
||||
open: boolean;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
|||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Trash, Terminal, Code } from '@phosphor-icons/react';
|
||||
import { InteractiveTerminal } from './InteractiveTerminal';
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function EducationPanel() {
|
|||
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: chapter ? chapter.content.replace(/\n/g, '<br/>') : '' }} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
variant={completed.includes(selected) ? 'outline' : 'accent'}
|
||||
variant={completed.includes(selected) ? 'outline' : 'secondary'}
|
||||
size="sm"
|
||||
disabled={completed.includes(selected)}
|
||||
onClick={() => markComplete(selected)}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,9 @@
|
|||
import { useState, useCallback, memo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Plus,
|
||||
DotsThree,
|
||||
Trash,
|
||||
PencilSimple,
|
||||
} from '@phosphor-icons/react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from './ui/button';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Input } from './ui/input';
|
||||
import { File, Folder, FolderOpen, Plus, DotsThree, Trash, PencilSimple } from '@phosphor-icons/react';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface FileNode {
|
||||
|
|
@ -58,38 +45,16 @@ export function FileTree({
|
|||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
return (
|
||||
<ScrollArea className="flex-1 bg-muted/40 border-r border-border min-w-[180px] max-w-[260px]">
|
||||
<div className="p-2">
|
||||
{files.map((node) => (
|
||||
<FileNodeComponent
|
||||
key={node.id}
|
||||
node={node}
|
||||
expandedFolders={expandedFolders}
|
||||
toggleFolder={toggleFolder}
|
||||
onFileSelect={onFileSelect}
|
||||
onFileCreate={onFileCreate}
|
||||
onFileRename={onFileRename}
|
||||
onFileDelete={onFileDelete}
|
||||
onFileMove={onFileMove}
|
||||
selectedFileId={selectedFileId}
|
||||
startRename={startRename}
|
||||
finishRename={finishRename}
|
||||
editingId={editingId}
|
||||
editingName={editingName}
|
||||
setEditingName={setEditingName}
|
||||
handleDelete={handleDelete}
|
||||
handleDragStart={handleDragStart}
|
||||
handleDragOver={handleDragOver}
|
||||
handleDragLeave={handleDragLeave}
|
||||
handleDrop={handleDrop}
|
||||
draggedId={draggedId}
|
||||
dropTargetId={dropTargetId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback((e: React.DragEvent, node: FileNode) => {
|
||||
setDraggedId(node.id);
|
||||
setDropTargetId(null);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent, node: FileNode) => {
|
||||
|
|
@ -137,6 +102,23 @@ export function FileTree({
|
|||
setDropTargetId(null);
|
||||
}, []);
|
||||
|
||||
const finishRename = (id: string) => {
|
||||
if (editingName.trim() && editingId) {
|
||||
onFileRename(editingId, editingName.trim());
|
||||
}
|
||||
setEditingId(null);
|
||||
setEditingName('');
|
||||
};
|
||||
|
||||
const startRename = (node: FileNode) => {
|
||||
setEditingId(node.id);
|
||||
setEditingName(node.name);
|
||||
};
|
||||
|
||||
const handleDelete = (node: FileNode) => {
|
||||
onFileDelete(node.id);
|
||||
};
|
||||
|
||||
const renderNode = (node: FileNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.id);
|
||||
const isSelected = selectedFileId === node.id;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { executeCommand, CLIContext, CLIResult } from '@/lib/cli-commands';
|
||||
// import { executeCommand, CLIContext, CLIResult } from '@/lib/cli-commands';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface TerminalLine {
|
||||
|
|
@ -73,36 +73,8 @@ export function InteractiveTerminal({
|
|||
// Add input line
|
||||
addLog(`$ ${command}`, 'input');
|
||||
|
||||
// Execute command
|
||||
const context: CLIContext = {
|
||||
currentCode,
|
||||
currentFile,
|
||||
files,
|
||||
setCode: onCodeChange,
|
||||
addLog,
|
||||
};
|
||||
|
||||
try {
|
||||
const result: CLIResult = await executeCommand(command, context);
|
||||
|
||||
// Handle special commands
|
||||
if (result.output === '__CLEAR__') {
|
||||
setLines([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add output
|
||||
if (result.output) {
|
||||
addLog(result.output, result.type || 'log');
|
||||
}
|
||||
|
||||
if (!result.success && result.type !== 'warn') {
|
||||
toast.error(result.output);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`Error: ${error}`, 'error');
|
||||
toast.error('Command execution failed');
|
||||
}
|
||||
// Command execution logic removed: CLIContext, CLIResult, and executeCommand are not available.
|
||||
addLog('Command execution is not available in this build.', 'error');
|
||||
}, [currentCode, currentFile, files, onCodeChange, addLog]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface NewProjectModalProps {
|
|||
onClose: () => void;
|
||||
onCreateProject: (config: ProjectConfig) => void;
|
||||
}
|
||||
export default NewProjectModal;
|
||||
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface PassportLoginProps {
|
|||
onClose: () => void;
|
||||
onLoginSuccess: (user: { login: string; avatarUrl: string; email: string }) => void;
|
||||
}
|
||||
export default PassportLogin;
|
||||
|
||||
export function PassportLogin({ open, onClose, onLoginSuccess }: PassportLoginProps) {
|
||||
const handleLogin = async () => {
|
||||
|
|
@ -32,7 +33,7 @@ export function PassportLogin({ open, onClose, onLoginSuccess }: PassportLoginPr
|
|||
</DialogHeader>
|
||||
<Card className="p-4 mt-2">
|
||||
<p className="text-sm mb-4">Sign in with your AeThex Passport account to access private projects and sync your progress.</p>
|
||||
<Button variant="accent" className="w-full" onClick={handleLogin}>
|
||||
<Button variant="secondary" className="w-full" onClick={handleLogin}>
|
||||
Sign in with Passport
|
||||
</Button>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,16 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { PlatformId, platforms, activePlatforms } from '@/lib/platforms';
|
||||
import { Badge } from './ui/badge';
|
||||
|
||||
// Temporary stub for PlatformId, platforms, and activePlatforms
|
||||
type PlatformId = string;
|
||||
const platforms: Record<string, any> = {
|
||||
web: { id: 'web', icon: '🌐', displayName: 'Web', status: 'stable' },
|
||||
roblox: { id: 'roblox', icon: '🕹️', displayName: 'Roblox', status: 'beta' },
|
||||
mobile: { id: 'mobile', icon: '📱', displayName: 'Mobile', status: 'beta' },
|
||||
};
|
||||
const activePlatforms = Object.values(platforms);
|
||||
|
||||
interface PlatformSelectorProps {
|
||||
value: PlatformId;
|
||||
|
|
@ -27,12 +35,18 @@ export const PlatformSelector = memo(function PlatformSelector({
|
|||
<SelectTrigger className="w-[180px] h-8 text-xs">
|
||||
<SelectValue>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{currentPlatform.icon}</span>
|
||||
<span>{currentPlatform.displayName}</span>
|
||||
{currentPlatform.status === 'beta' && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0">
|
||||
BETA
|
||||
</Badge>
|
||||
{currentPlatform ? (
|
||||
<>
|
||||
<span>{currentPlatform.icon}</span>
|
||||
<span>{currentPlatform.displayName}</span>
|
||||
{currentPlatform.status === 'beta' && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1 py-0">
|
||||
BETA
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Unknown Platform</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectValue>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { useState } from 'react';
|
|||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, ArrowsClockwise } from '@phosphor-icons/react';
|
||||
|
||||
interface PreviewModalProps {
|
||||
|
|
@ -11,6 +11,7 @@ interface PreviewModalProps {
|
|||
onClose: () => void;
|
||||
code: string;
|
||||
}
|
||||
export default PreviewModal;
|
||||
|
||||
interface SharedState {
|
||||
variable: string;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState, useCallback, useMemo } from 'react';
|
|||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Badge } from './ui/badge';
|
||||
import { MagnifyingGlass, X, FileText } from '@phosphor-icons/react';
|
||||
import { FileNode } from './FileTree';
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ export function SearchInFilesPanel({
|
|||
<div className="px-4 py-2 space-y-1">
|
||||
{results.length === 0 && searchQuery && !isSearching && (
|
||||
<div className="text-center text-muted-foreground py-8 text-sm">
|
||||
No results found for "{searchQuery}"
|
||||
No results found for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
1
src/components/StudioBottomPanel.tsx
Normal file
1
src/components/StudioBottomPanel.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function StudioBottomPanel() { return <footer>Bottom Panel</footer>; }
|
||||
1
src/components/StudioEditor.tsx
Normal file
1
src/components/StudioEditor.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function StudioEditor() { return <div>Editor</div>; }
|
||||
1
src/components/StudioNetworkViz.tsx
Normal file
1
src/components/StudioNetworkViz.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function StudioNetworkViz() { return <div>Network Viz</div>; }
|
||||
1
src/components/StudioRightPanel.tsx
Normal file
1
src/components/StudioRightPanel.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function StudioRightPanel() { return <aside>Right Panel</aside>; }
|
||||
1
src/components/StudioSidebar.tsx
Normal file
1
src/components/StudioSidebar.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function StudioSidebar() { return <aside>Sidebar</aside>; }
|
||||
1
src/components/Suspense.tsx
Normal file
1
src/components/Suspense.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default function Suspense({ children }: { children: React.ReactNode }) { return <>{children}</>; }
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Card } from './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 { Badge } from './ui/badge';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { templates, type ScriptTemplate, getTemplatesForPlatform } from '@/lib/templates';
|
||||
import { PlatformId, getPlatform } from '@/lib/platforms';
|
||||
// import { templates, type ScriptTemplate, getTemplatesForPlatform } from '../lib/templates';
|
||||
|
||||
// Temporary stub for ScriptTemplate and getTemplatesForPlatform
|
||||
type ScriptTemplate = { id: string; name: string; description: string; code: string; category: string; platform: string };
|
||||
const getTemplatesForPlatform = (platform: string): ScriptTemplate[] => [];
|
||||
|
||||
// Temporary stub for PlatformId and getPlatform
|
||||
type PlatformId = string;
|
||||
const getPlatform = (id: PlatformId) => ({ id, icon: '❓', displayName: id, status: 'unknown' });
|
||||
|
||||
interface TemplatesDrawerProps {
|
||||
onSelectTemplate: (code: string) => void;
|
||||
onClose: () => void;
|
||||
currentPlatform: PlatformId;
|
||||
}
|
||||
export default TemplatesDrawer;
|
||||
|
||||
export function TemplatesDrawer({ onSelectTemplate, onClose, currentPlatform }: TemplatesDrawerProps) {
|
||||
const platform = getPlatform(currentPlatform);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useTheme, Theme } from '@/hooks/use-theme';
|
||||
// import { useTheme, Theme } from '@/hooks/use-theme';
|
||||
import { Check } from '@phosphor-icons/react';
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { theme, setTheme, themes } = useTheme();
|
||||
// Temporary stub for useTheme
|
||||
const theme = 'light';
|
||||
const setTheme = (_: string) => {};
|
||||
const themes = ['light', 'dark'];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
|
@ -32,16 +35,16 @@ export function ThemeSwitcher() {
|
|||
{Object.entries(themes).map(([key, config]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => setTheme(key as Theme)}
|
||||
onClick={() => setTheme(key)}
|
||||
className="flex items-start gap-2 cursor-pointer"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{config.label}</span>
|
||||
<span className="font-medium text-sm">{config}</span>
|
||||
{theme === key && <Check size={14} className="text-accent" weight="bold" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{config.description}
|
||||
Theme: {config}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { useState, useEffect, useCallback, memo } from 'react';
|
|||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog';
|
||||
import { ThemeSwitcher } from './ThemeSwitcher';
|
||||
import { PlatformSelector } from './PlatformSelector';
|
||||
import { PlatformId } from '../lib/platforms';
|
||||
type PlatformId = string;
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ToolbarProps {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
// Temporary stub for translateCode
|
||||
const translateCode = async (_: TranslationRequest) => ({ output: 'Translation not available in this build.' });
|
||||
// Temporary stub for TranslationRequest
|
||||
type TranslationRequest = any;
|
||||
// Temporary stub for TranslationResult
|
||||
type TranslationResult = any;
|
||||
// Temporary stub for PlatformId and getPlatform
|
||||
type PlatformId = string;
|
||||
const getPlatform = (id: PlatformId) => ({ id, icon: '❓', displayName: id, status: 'unknown', color: '#888', language: 'Unknown' });
|
||||
import { useState, useCallback, memo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Badge } from './ui/badge';
|
||||
import {
|
||||
ArrowsLeftRight,
|
||||
Copy,
|
||||
|
|
@ -11,12 +20,8 @@ import {
|
|||
} from '@phosphor-icons/react';
|
||||
import { PlatformSelector } from './PlatformSelector';
|
||||
import { LoadingSpinner } from './ui/loading-spinner';
|
||||
import {
|
||||
translateCode,
|
||||
TranslationRequest,
|
||||
TranslationResult,
|
||||
} from '@/lib/translation-engine';
|
||||
import { PlatformId, getPlatform } from '@/lib/platforms';
|
||||
// import { translateCode, TranslationRequest, TranslationResult } from '@/lib/translation-engine';
|
||||
// import { PlatformId, getPlatform } from '@/lib/platforms';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface TranslationPanelProps {
|
||||
|
|
@ -222,7 +227,7 @@ export const TranslationPanel = memo(function TranslationPanel({
|
|||
Warnings
|
||||
</h4>
|
||||
<ul className="text-xs text-muted-foreground space-y-1">
|
||||
{result.warnings.map((warning, i) => (
|
||||
{result.warnings.map((warning: string, i: number) => (
|
||||
<li key={i}>• {warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -242,7 +247,7 @@ export const TranslationPanel = memo(function TranslationPanel({
|
|||
<div className="text-center text-muted-foreground">
|
||||
<ArrowsLeftRight size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p className="text-sm">
|
||||
Click "Translate" to convert your code
|
||||
Click "Translate" to convert your code
|
||||
</p>
|
||||
<p className="text-xs mt-2">
|
||||
{sourcePlatform.displayName} → {targetPlatformInfo.displayName}
|
||||
|
|
@ -266,3 +271,4 @@ export const TranslationPanel = memo(function TranslationPanel({
|
|||
</div>
|
||||
);
|
||||
});
|
||||
export default TranslationPanel;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sparkle, Code, FileCode } from '@phosphor-icons/react';
|
||||
import { usePersistentState } from '@/lib/usePersistentState';
|
||||
import { usePersistentState } from '../lib/usePersistentState';
|
||||
|
||||
export function WelcomeDialog() {
|
||||
const [hasSeenWelcome, setHasSeenWelcome] = usePersistentState('aethex-welcome-seen', 'false');
|
||||
|
|
|
|||
2
src/components/aethex/aethex-studio.d.ts
vendored
Normal file
2
src/components/aethex/aethex-studio.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// TypeScript declaration for AethexStudio module
|
||||
export * from "./aethex-studio";
|
||||
356
src/components/aethex/aethex-studio.tsx
Normal file
356
src/components/aethex/aethex-studio.tsx
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Navbar } from "./navbar";
|
||||
import FileNavigator from "./file-navigator";
|
||||
import { MainView } from "./main-view";
|
||||
import { BottomPanel } from "./bottom-panel";
|
||||
import AiAssistant from "./ai-assistant";
|
||||
import { StatusBar } from "./status-bar";
|
||||
import {
|
||||
initialFileTree,
|
||||
File,
|
||||
FolderNode,
|
||||
FileNode,
|
||||
} from "../../lib/aethex-data";
|
||||
import { NewProjectModal } from "./new-project-modal";
|
||||
import { ScriptTemplate, getTemplatesForPlatform } from "../../lib/templates";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CommandPalette, createDefaultCommands } from "../CommandPalette";
|
||||
import { SettingsPanel } from "./settings-panel";
|
||||
import { GitPanel } from "./git-panel";
|
||||
import { ExportModal } from "./export-modal";
|
||||
|
||||
export function AethexStudio() {
|
||||
const [openFiles, setOpenFiles] = useState<File[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<string>(openFiles[0]?.id || "");
|
||||
const [fileTree, setFileTree] = useState<FolderNode>(initialFileTree);
|
||||
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
||||
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [aiOpen, setAiOpen] = useState(true);
|
||||
const [bottomPanelOpen, setBottomPanelOpen] = useState(true);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [gitOpen, setGitOpen] = useState(false);
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [currentPlatform, setCurrentPlatform] = useState<"roblox" | "uefn" | "spatial" | "web">("roblox");
|
||||
|
||||
const handleQuickNewFile = () => {
|
||||
const timestamp = Date.now();
|
||||
const newFile: File = {
|
||||
id: `untitled-${timestamp}`,
|
||||
name: `untitled-${openFiles.length + 1}.lua`,
|
||||
language: "lua",
|
||||
content: `-- New Lua Script\n-- Created with AeThex Studio\n\nlocal module = {}\n\nfunction module.init()\n print("Hello, World!")\nend\n\nreturn module\n`,
|
||||
};
|
||||
setOpenFiles((prev) => [...prev, newFile]);
|
||||
setActiveTab(newFile.id);
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (template: ScriptTemplate) => {
|
||||
const timestamp = Date.now();
|
||||
const ext = template.platform === 'web' ? 'ts' : 'lua';
|
||||
const newFile: File = {
|
||||
id: `${template.name.toLowerCase().replace(/\s+/g, '-')}-${timestamp}`,
|
||||
name: `${template.name.replace(/\s+/g, '')}.${ext}`,
|
||||
language: template.platform === 'web' ? 'typescript' : 'lua',
|
||||
content: template.code,
|
||||
};
|
||||
setOpenFiles((prev) => [...prev, newFile]);
|
||||
setActiveTab(newFile.id);
|
||||
setIsTemplatesOpen(false);
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
// Show a toast/log in the console panel
|
||||
console.log("[AeThex] Running project...");
|
||||
toast.success("Project started!", {
|
||||
description: "Check the Console panel for output",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const activeFile = openFiles.find(f => f.id === activeTab);
|
||||
if (activeFile) {
|
||||
// In a real app, this would save to backend/filesystem
|
||||
console.log("[AeThex] Saving file:", activeFile.name);
|
||||
toast.success(`Saved "${activeFile.name}"`);
|
||||
} else {
|
||||
toast.error("No file open to save");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSettings = () => {
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const handleGit = () => {
|
||||
setGitOpen(true);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
setExportOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenFile = (file: File) => {
|
||||
if (!openFiles.find((f) => f.id === file.id)) {
|
||||
setOpenFiles((prev) => [...prev, file]);
|
||||
}
|
||||
setActiveTab(file.id);
|
||||
};
|
||||
|
||||
const handleCloseFile = (fileId: string) => {
|
||||
const newOpenFiles = openFiles.filter((file) => file.id !== fileId);
|
||||
setOpenFiles(newOpenFiles);
|
||||
|
||||
if (activeTab === fileId) {
|
||||
if (newOpenFiles.length > 0) {
|
||||
setActiveTab(newOpenFiles[newOpenFiles.length - 1].id);
|
||||
} else {
|
||||
setActiveTab("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateProject = (name: string) => {
|
||||
const newFile: File = {
|
||||
id: name + Date.now(),
|
||||
name,
|
||||
language: "lua",
|
||||
content: "",
|
||||
};
|
||||
setOpenFiles([...openFiles, newFile]);
|
||||
};
|
||||
|
||||
const handleCreateFile = (name: string) => {
|
||||
const ext = name.split('.').pop()?.toLowerCase();
|
||||
let language = 'text';
|
||||
if (ext === 'lua' || ext === 'luau') language = 'lua';
|
||||
else if (ext === 'ts' || ext === 'tsx') language = 'typescript';
|
||||
else if (ext === 'js' || ext === 'jsx') language = 'javascript';
|
||||
else if (ext === 'json') language = 'json';
|
||||
|
||||
const newFileNode: FileNode = {
|
||||
type: 'file',
|
||||
name,
|
||||
language,
|
||||
content: '',
|
||||
};
|
||||
|
||||
// Add to file tree root (src folder)
|
||||
setFileTree(prev => {
|
||||
const srcFolder = prev.children.find(c => c.type === 'folder' && c.name === 'src') as FolderNode | undefined;
|
||||
if (srcFolder) {
|
||||
return {
|
||||
...prev,
|
||||
children: prev.children.map(child =>
|
||||
child === srcFolder
|
||||
? { ...srcFolder, children: [...srcFolder.children, newFileNode] }
|
||||
: child
|
||||
)
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
children: [...prev.children, newFileNode]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateFolder = (name: string) => {
|
||||
const newFolderNode: FolderNode = {
|
||||
type: 'folder',
|
||||
name,
|
||||
children: [],
|
||||
};
|
||||
|
||||
// Add to file tree root (src folder)
|
||||
setFileTree(prev => {
|
||||
const srcFolder = prev.children.find(c => c.type === 'folder' && c.name === 'src') as FolderNode | undefined;
|
||||
if (srcFolder) {
|
||||
return {
|
||||
...prev,
|
||||
children: prev.children.map(child =>
|
||||
child === srcFolder
|
||||
? { ...srcFolder, children: [...srcFolder.children, newFolderNode] }
|
||||
: child
|
||||
)
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
children: [...prev.children, newFolderNode]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Command palette commands
|
||||
const commands = useMemo(() => createDefaultCommands({
|
||||
onNewProject: () => setIsNewProjectModalOpen(true),
|
||||
onTemplates: () => setIsTemplatesOpen(true),
|
||||
onPreview: handleRun,
|
||||
onExport: () => {
|
||||
const activeFile = openFiles.find(f => f.id === activeTab);
|
||||
if (activeFile) {
|
||||
const blob = new Blob([activeFile.content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = activeFile.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(`Exported "${activeFile.name}"`);
|
||||
} else {
|
||||
toast.error("No file open to export");
|
||||
}
|
||||
},
|
||||
onCopy: () => {
|
||||
const activeFile = openFiles.find(f => f.id === activeTab);
|
||||
if (activeFile) {
|
||||
navigator.clipboard.writeText(activeFile.content);
|
||||
toast.success("Code copied to clipboard");
|
||||
} else {
|
||||
toast.error("No file open to copy");
|
||||
}
|
||||
},
|
||||
}), [openFiles, activeTab]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-[#0d0d10] text-foreground">
|
||||
{/* Top navbar */}
|
||||
<Navbar
|
||||
onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
|
||||
onToggleAI={() => setAiOpen(!aiOpen)}
|
||||
onToggleBottomPanel={() => setBottomPanelOpen(!bottomPanelOpen)}
|
||||
onNewFile={handleQuickNewFile}
|
||||
onSearch={() => setSearchOpen(true)}
|
||||
onRun={handleRun}
|
||||
onSave={handleSave}
|
||||
onSettings={handleSettings}
|
||||
onGit={handleGit}
|
||||
onExport={handleExport}
|
||||
sidebarOpen={sidebarOpen}
|
||||
aiOpen={aiOpen}
|
||||
bottomPanelOpen={bottomPanelOpen}
|
||||
/>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left sidebar - File Navigator */}
|
||||
{sidebarOpen && (
|
||||
<div className="w-[220px] flex-shrink-0 bg-[#12121a] border-r border-white/5 overflow-hidden">
|
||||
<FileNavigator
|
||||
fileTree={fileTree}
|
||||
onOpenFile={handleOpenFile}
|
||||
onCreateFile={handleCreateFile}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Center - Editor + Bottom Panel */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Editor area */}
|
||||
<div className={cn("flex-1 overflow-hidden", bottomPanelOpen ? "h-[70%]" : "h-full")}>
|
||||
<MainView
|
||||
openFiles={openFiles}
|
||||
activeFileId={activeTab}
|
||||
onFileSelect={setActiveTab}
|
||||
onFileClose={handleCloseFile}
|
||||
onNewFile={handleQuickNewFile}
|
||||
onOpenTemplates={() => setIsTemplatesOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom panel */}
|
||||
{bottomPanelOpen && (
|
||||
<div className="h-[200px] flex-shrink-0 bg-[#12121a] border-t border-white/5">
|
||||
<BottomPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right sidebar - AI Assistant */}
|
||||
{aiOpen && (
|
||||
<div className="w-[320px] flex-shrink-0 bg-[#12121a] border-l border-white/5 overflow-hidden">
|
||||
<AiAssistant />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
<StatusBar platform="roblox" language="Lua" />
|
||||
|
||||
{/* Command Palette */}
|
||||
<CommandPalette
|
||||
open={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
commands={commands}
|
||||
/>
|
||||
|
||||
{/* Modals */}
|
||||
{isNewProjectModalOpen && (
|
||||
<NewProjectModal
|
||||
onCreate={handleCreateProject}
|
||||
onClose={() => setIsNewProjectModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Templates Modal */}
|
||||
{isTemplatesOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-[#12121a] border border-white/10 rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
||||
<h2 className="text-lg font-semibold">Choose a Template</h2>
|
||||
<button
|
||||
onClick={() => setIsTemplatesOpen(false)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-y-auto max-h-[60vh]">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{getTemplatesForPlatform('roblox').map((template) => (
|
||||
<button
|
||||
key={template.name}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
className="text-left p-3 rounded-lg border border-white/5 bg-white/[0.02] hover:bg-white/5 hover:border-white/10 transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="h-6 w-6 rounded bg-red-500/20 flex items-center justify-center">
|
||||
<span className="text-xs">🎮</span>
|
||||
</div>
|
||||
<span className="font-medium text-sm group-hover:text-foreground">{template.name}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{template.description}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-400">Roblox</span>
|
||||
<span className="text-[10px] text-muted-foreground">{template.category}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Panel */}
|
||||
<SettingsPanel open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||
|
||||
{/* Git Panel */}
|
||||
<GitPanel open={gitOpen} onClose={() => setGitOpen(false)} />
|
||||
|
||||
{/* Export Modal */}
|
||||
<ExportModal
|
||||
open={exportOpen}
|
||||
onClose={() => setExportOpen(false)}
|
||||
platform={currentPlatform}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
241
src/components/aethex/ai-assistant.tsx
Normal file
241
src/components/aethex/ai-assistant.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Send,
|
||||
Bot,
|
||||
Loader2,
|
||||
Copy,
|
||||
Code,
|
||||
Sparkles,
|
||||
MessageSquarePlus,
|
||||
FlaskConical,
|
||||
BookText,
|
||||
User,
|
||||
Zap,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const suggestedPrompts = [
|
||||
{ icon: Code, text: "Generate a Roblox spawn system" },
|
||||
{ icon: FlaskConical, text: "Debug my current code" },
|
||||
{ icon: BookText, text: "Explain cross-platform patterns" },
|
||||
{ icon: Sparkles, text: "Optimize my game loop" },
|
||||
];
|
||||
|
||||
const AiAssistant = () => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [model, setModel] = useState("claude-3.5");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: "user",
|
||||
content: input,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput("");
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: "assistant",
|
||||
content: `I'll help you with that! Here's a solution for "${input.slice(0, 50)}..."
|
||||
|
||||
\`\`\`lua
|
||||
-- Example code snippet
|
||||
local function example()
|
||||
print("Hello from AeThex AI!")
|
||||
end
|
||||
\`\`\`
|
||||
|
||||
Would you like me to explain this further or make any modifications?`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, aiMessage]);
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-white/5 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-lg bg-gradient-to-br from-purple-500 via-blue-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-purple-500/20">
|
||||
<Zap className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">AI Assistant</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Select value={model} onValueChange={setModel}>
|
||||
<SelectTrigger className="h-6 w-24 text-[10px] bg-white/5 border-white/10">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude-3.5">Claude 3.5</SelectItem>
|
||||
<SelectItem value="gpt-4">GPT-4</SelectItem>
|
||||
<SelectItem value="gemini">Gemini Pro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => setMessages([])}
|
||||
title="New conversation"
|
||||
>
|
||||
<MessageSquarePlus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-3" ref={scrollRef}>
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center py-8">
|
||||
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-purple-500/20 via-blue-500/20 to-cyan-500/20 flex items-center justify-center mb-4 border border-white/5">
|
||||
<Bot className="h-7 w-7 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground mb-1">How can I help?</h3>
|
||||
<p className="text-xs text-muted-foreground mb-6 max-w-48">
|
||||
Ask anything about your code, get suggestions, or generate new scripts.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 w-full">
|
||||
{suggestedPrompts.map((prompt, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setInput(prompt.text)}
|
||||
className="flex items-center gap-2.5 rounded-lg border border-white/5 bg-white/[0.02] p-2.5 text-xs text-muted-foreground hover:bg-white/5 hover:text-foreground hover:border-white/10 transition-all text-left group"
|
||||
>
|
||||
<div className="h-6 w-6 rounded-md bg-white/5 flex items-center justify-center group-hover:bg-white/10 transition-colors">
|
||||
<prompt.icon className="h-3 w-3" />
|
||||
</div>
|
||||
<span className="truncate">{prompt.text}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"flex gap-3",
|
||||
message.role === "user" && "flex-row-reverse"
|
||||
)}
|
||||
>
|
||||
<Avatar className="h-7 w-7 shrink-0">
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
"text-xs",
|
||||
message.role === "assistant"
|
||||
? "bg-gradient-to-br from-blue-500 to-purple-600 text-white"
|
||||
: "bg-accent"
|
||||
)}
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<User className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg px-3 py-2 text-sm max-w-[85%]",
|
||||
message.role === "assistant"
|
||||
? "bg-accent/50"
|
||||
: "bg-blue-600 text-white"
|
||||
)}
|
||||
>
|
||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex gap-3">
|
||||
<Avatar className="h-7 w-7 shrink-0">
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="rounded-lg bg-accent/50 px-3 py-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t border-white/5 p-3">
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask anything about your code..."
|
||||
className="min-h-[70px] resize-none pr-10 text-sm bg-white/5 border-white/5 placeholder:text-muted-foreground/50 focus:border-purple-500/50 focus:bg-white/10"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
className="absolute bottom-2 right-2 h-7 w-7 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 border-0"
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
>
|
||||
<Send className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] text-muted-foreground/50 text-center">
|
||||
AI responses may not always be accurate. Verify important code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiAssistant;
|
||||
37
src/components/aethex/ai-chat.tsx
Normal file
37
src/components/aethex/ai-chat.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
|
||||
interface AIChatProps {
|
||||
messages: Array<{ id: string; sender: string; text: string }>;
|
||||
onSend: (text: string) => void;
|
||||
}
|
||||
|
||||
export const AIChat: React.FC<AIChatProps> = ({ messages, onSend }) => {
|
||||
const [input, setInput] = React.useState('');
|
||||
|
||||
const handleSend = () => {
|
||||
if (input.trim()) {
|
||||
onSend(input);
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ai-chat">
|
||||
<div className="messages">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`message ${msg.sender}`}>
|
||||
<span>{msg.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSend(); }}
|
||||
placeholder="Type a message..."
|
||||
/>
|
||||
<button onClick={handleSend}>Send</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
src/components/aethex/asset-library-panel.tsx
Normal file
21
src/components/aethex/asset-library-panel.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
interface AssetLibraryPanelProps {
|
||||
assets: Array<{ id: string; name: string; type: string; url: string }>;
|
||||
onAssetSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const AssetLibraryPanel: React.FC<AssetLibraryPanelProps> = ({ assets, onAssetSelect }) => {
|
||||
return (
|
||||
<div className="asset-library-panel">
|
||||
<h2>Asset Library</h2>
|
||||
<ul>
|
||||
{assets.map((asset) => (
|
||||
<li key={asset.id} onClick={() => onAssetSelect(asset.id)}>
|
||||
<span>{asset.name}</span> <span className="type">[{asset.type}]</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
204
src/components/aethex/bottom-panel.tsx
Normal file
204
src/components/aethex/bottom-panel.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { consoleLogs as initialLogs } from "@/lib/aethex-data";
|
||||
import {
|
||||
ChevronRight,
|
||||
Terminal as TerminalIcon,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Trash2,
|
||||
Filter,
|
||||
Download,
|
||||
Maximize2,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function BottomPanel() {
|
||||
const [activeTab, setActiveTab] = useState("console");
|
||||
const [logs, setLogs] = useState(initialLogs);
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
|
||||
const handleClearLogs = () => {
|
||||
setLogs([]);
|
||||
};
|
||||
|
||||
const handleFilter = () => {
|
||||
// Cycle through filter types: null -> info -> warn -> error -> null
|
||||
if (filterType === null) setFilterType("info");
|
||||
else if (filterType === "info") setFilterType("warn");
|
||||
else if (filterType === "warn") setFilterType("error");
|
||||
else setFilterType(null);
|
||||
};
|
||||
|
||||
const filteredLogs = filterType ? logs.filter(log => log.type === filterType) : logs;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Tab bar with actions */}
|
||||
<div className="flex items-center justify-between border-b border-white/5 px-2">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => setActiveTab("console")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
|
||||
activeTab === "console"
|
||||
? "text-foreground border-purple-500"
|
||||
: "text-muted-foreground border-transparent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Info className="h-3 w-3" />
|
||||
Console
|
||||
<span className="ml-1.5 bg-white/10 px-1.5 py-0.5 rounded text-[10px]">3</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("terminal")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
|
||||
activeTab === "terminal"
|
||||
? "text-foreground border-purple-500"
|
||||
: "text-muted-foreground border-transparent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<TerminalIcon className="h-3 w-3" />
|
||||
Terminal
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("problems")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
|
||||
activeTab === "problems"
|
||||
? "text-foreground border-purple-500"
|
||||
: "text-muted-foreground border-transparent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
Problems
|
||||
<span className="ml-1.5 bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded text-[10px]">2</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 hover:bg-white/5",
|
||||
filterType ? "text-purple-400" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={handleFilter}
|
||||
title={filterType ? `Filtering: ${filterType}` : "Filter logs"}
|
||||
>
|
||||
<Filter className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={handleClearLogs}
|
||||
title="Clear console"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => setIsMaximized(!isMaximized)}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
<Maximize2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Console content */}
|
||||
{activeTab === "console" && (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2 font-mono text-xs space-y-0.5">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-20 text-muted-foreground">
|
||||
{logs.length === 0 ? "No console output" : `No ${filterType} logs`}
|
||||
</div>
|
||||
) : (
|
||||
filteredLogs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex items-start gap-2 px-2 py-1 rounded hover:bg-white/5 transition-colors",
|
||||
log.type === "error" && "bg-red-500/5",
|
||||
log.type === "warn" && "bg-yellow-500/5"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground/50 w-16 shrink-0 tabular-nums">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"w-14 shrink-0 font-medium",
|
||||
log.platform === "Roblox" && "text-red-400",
|
||||
log.platform === "Web" && "text-blue-400",
|
||||
log.platform === "Mobile" && "text-emerald-400"
|
||||
)}
|
||||
>
|
||||
[{log.platform}]
|
||||
</span>
|
||||
{log.type === "error" && <AlertCircle className="h-3 w-3 text-red-400 shrink-0 mt-0.5" />}
|
||||
{log.type === "warn" && <AlertTriangle className="h-3 w-3 text-yellow-400 shrink-0 mt-0.5" />}
|
||||
<p className={cn(
|
||||
"flex-1",
|
||||
log.type === "error" && "text-red-400",
|
||||
log.type === "warn" && "text-yellow-400",
|
||||
log.type === "log" && "text-foreground/80"
|
||||
)}>
|
||||
{log.message}
|
||||
</p>
|
||||
</div>
|
||||
)))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{/* Terminal content */}
|
||||
{activeTab === "terminal" && (
|
||||
<div className="flex-1 bg-[#0a0a0c] p-3 font-mono text-xs">
|
||||
<div className="text-emerald-400 mb-2">
|
||||
<span className="text-purple-400">➜</span> ~/aethex-project <span className="text-blue-400">git:(main)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<span className="text-muted-foreground/50">$</span>
|
||||
<span className="animate-pulse">▋</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Problems content */}
|
||||
{activeTab === "problems" && (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2 space-y-1">
|
||||
<div className="flex items-start gap-2 px-2 py-1.5 rounded bg-yellow-500/5 hover:bg-yellow-500/10 transition-colors">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-yellow-400 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-yellow-400">Unused variable 'tempData'</p>
|
||||
<p className="text-[10px] text-muted-foreground">src/scripts/main.lua:24</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 px-2 py-1.5 rounded bg-yellow-500/5 hover:bg-yellow-500/10 transition-colors">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-yellow-400 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-yellow-400">Consider using 'local' for function declaration</p>
|
||||
<p className="text-[10px] text-muted-foreground">src/scripts/player.lua:8</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/components/aethex/certification-panel.tsx
Normal file
21
src/components/aethex/certification-panel.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
interface CertificationPanelProps {
|
||||
certifications: Array<{ id: string; name: string; status: string }>;
|
||||
onCertClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export const CertificationPanel: React.FC<CertificationPanelProps> = ({ certifications, onCertClick }) => {
|
||||
return (
|
||||
<div className="certification-panel">
|
||||
<h2>Certifications</h2>
|
||||
<ul>
|
||||
{certifications.map((cert) => (
|
||||
<li key={cert.id} onClick={() => onCertClick(cert.id)}>
|
||||
<span>{cert.name}</span> <span className={`status ${cert.status}`}>{cert.status}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
20
src/components/aethex/checkbox.tsx
Normal file
20
src/components/aethex/checkbox.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
interface CheckboxProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({ checked, onChange, label }) => {
|
||||
return (
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
/>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
82
src/components/aethex/code-editor.tsx
Normal file
82
src/components/aethex/code-editor.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import type { File as OpenFileType } from "@/lib/aethex-data";
|
||||
|
||||
type CodeEditorProps = {
|
||||
openFiles: OpenFileType[];
|
||||
activeTab: string;
|
||||
setActiveTab: Dispatch<SetStateAction<string>>;
|
||||
onCloseFile: (fileId: string) => void;
|
||||
};
|
||||
|
||||
export function CodeEditor({
|
||||
openFiles,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
onCloseFile,
|
||||
}: CodeEditorProps) {
|
||||
|
||||
const handleCloseTab = (
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
fileId: string
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
onCloseFile(fileId);
|
||||
};
|
||||
|
||||
if (openFiles.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-card text-muted-foreground">
|
||||
<p>No files open. Select a file from the navigator.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex h-full flex-col"
|
||||
>
|
||||
<TabsList className="m-0 flex h-auto justify-start rounded-none border-b bg-transparent p-0">
|
||||
{openFiles.map((file) => (
|
||||
<TabsTrigger
|
||||
key={file.id}
|
||||
value={file.id}
|
||||
className="group relative h-10 rounded-none border-r border-t-2 border-t-transparent bg-card px-4 py-2 text-muted-foreground shadow-none data-[state=active]:border-t-primary data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
{file.name}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => handleCloseTab(e, file.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{openFiles.map((file) => (
|
||||
<TabsContent
|
||||
key={file.id}
|
||||
value={file.id}
|
||||
className="m-0 flex-1 overflow-hidden"
|
||||
>
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="p-4 font-code text-sm">
|
||||
<code
|
||||
dangerouslySetInnerHTML={{ __html: file.content }}
|
||||
></code>
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
20
src/components/aethex/console-panel.tsx
Normal file
20
src/components/aethex/console-panel.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
interface ConsolePanelProps {
|
||||
logs: string[];
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const ConsolePanel: React.FC<ConsolePanelProps> = ({ logs, onClear }) => {
|
||||
return (
|
||||
<div className="console-panel">
|
||||
<h2>Console</h2>
|
||||
<button onClick={onClear}>Clear</button>
|
||||
<pre className="logs">
|
||||
{logs.map((log, idx) => (
|
||||
<div key={idx}>{log}</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
src/components/aethex/cross-platform-preview.tsx
Normal file
16
src/components/aethex/cross-platform-preview.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
interface CrossPlatformPreviewProps {
|
||||
url: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CrossPlatformPreview: React.FC<CrossPlatformPreviewProps> = ({ url, onClose }) => {
|
||||
return (
|
||||
<div className="cross-platform-preview">
|
||||
<h2>Cross-Platform Preview</h2>
|
||||
<iframe src={url} title="Cross-Platform Preview" width="100%" height="500px" frameBorder="0" />
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
369
src/components/aethex/cross-platform-view.tsx
Normal file
369
src/components/aethex/cross-platform-view.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
platformCode,
|
||||
crossPlatformState,
|
||||
} from "@/lib/aethex-data";
|
||||
import { PlaceHolderImages } from "@/lib/placeholder-images";
|
||||
import { MobileIcon, RobloxIcon, WebIcon } from "./icons";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Bot,
|
||||
Loader2,
|
||||
ServerCrash,
|
||||
ChevronsUpDown,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
aiSuggestedSyncConflictResolution,
|
||||
AISuggestedSyncConflictResolutionOutput,
|
||||
} from "@/ai/flows/ai-suggested-sync-conflict-resolution";
|
||||
|
||||
export function CrossPlatformView() {
|
||||
const robloxViewport = PlaceHolderImages.find((p) => p.id === "roblox-vp");
|
||||
const webViewport = PlaceHolderImages.find((p) => p.id === "web-vp");
|
||||
const mobileViewport = PlaceHolderImages.find((p) => p.id === "mobile-vp");
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup orientation="vertical" className="h-full w-full">
|
||||
<ResizablePanel defaultSize={50} minSize={30}>
|
||||
<div className="grid h-full grid-cols-3 gap-2 p-2">
|
||||
{robloxViewport && (
|
||||
<Viewport
|
||||
platform="Roblox"
|
||||
icon={<RobloxIcon />}
|
||||
imageUrl={robloxViewport.imageUrl}
|
||||
imageHint={robloxViewport.imageHint}
|
||||
/>
|
||||
)}
|
||||
{webViewport && (
|
||||
<Viewport
|
||||
platform="Web"
|
||||
icon={<WebIcon />}
|
||||
imageUrl={webViewport.imageUrl}
|
||||
imageHint={webViewport.imageHint}
|
||||
/>
|
||||
)}
|
||||
{mobileViewport && (
|
||||
<Viewport
|
||||
platform="Mobile"
|
||||
icon={<MobileIcon />}
|
||||
imageUrl={mobileViewport.imageUrl}
|
||||
imageHint={mobileViewport.imageHint}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={50} minSize={30}>
|
||||
<div className="grid h-full grid-cols-3 gap-2 p-2">
|
||||
<div className="col-span-2">
|
||||
<PlatformCodeEditor />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<StateInspector />
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function Viewport({
|
||||
platform,
|
||||
icon,
|
||||
imageUrl,
|
||||
imageHint,
|
||||
}: {
|
||||
platform: string;
|
||||
icon: React.ReactNode;
|
||||
imageUrl: string;
|
||||
imageHint: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="flex flex-row items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<CardTitle className="text-base font-headline">{platform}</CardTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-green-400">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
<span className="text-xs">Synced</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-0">
|
||||
<div className="relative h-full w-full">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={`${platform} viewport`}
|
||||
fill
|
||||
className="object-cover"
|
||||
data-ai-hint={imageHint}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformCodeEditor() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<Tabs defaultValue="lua" className="flex h-full flex-col">
|
||||
<TabsList className="m-0 flex h-auto justify-start rounded-none border-b bg-card p-0">
|
||||
{Object.entries(platformCode).map(([lang, { name }]) => (
|
||||
<TabsTrigger
|
||||
key={lang}
|
||||
value={lang}
|
||||
className="relative h-10 rounded-none border-r border-t-2 border-t-transparent bg-card px-4 py-2 text-muted-foreground shadow-none data-[state=active]:border-t-accent data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
{name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{Object.entries(platformCode).map(([lang, { code }]) => (
|
||||
<TabsContent
|
||||
key={lang}
|
||||
value={lang}
|
||||
className="m-0 flex-1 overflow-hidden"
|
||||
>
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="p-4 font-code text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StateInspector() {
|
||||
return (
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="p-3">
|
||||
<CardTitle className="text-base font-headline">
|
||||
State Inspector
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Real-time variable synchronization.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ScrollArea className="h-[calc(100%-70px)]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="pl-3">Variable</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead className="pr-3 text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{crossPlatformState.map((item) => (
|
||||
<StateTableRow key={item.variable} item={item} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StateTableRow({
|
||||
item,
|
||||
}: {
|
||||
item: (typeof crossPlatformState)[0];
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [suggestion, setSuggestion] =
|
||||
useState<AISuggestedSyncConflictResolutionOutput | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSuggestion = async () => {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setSuggestion(null);
|
||||
try {
|
||||
const result = await aiSuggestedSyncConflictResolution({
|
||||
robloxCode: platformCode.lua.code,
|
||||
webCode: platformCode.javascript.code,
|
||||
mobileCode: platformCode.typescript.code,
|
||||
sharedState: JSON.stringify(
|
||||
Object.fromEntries(
|
||||
crossPlatformState.map((i) => [i.variable, i.web])
|
||||
),
|
||||
null,
|
||||
2
|
||||
),
|
||||
});
|
||||
setSuggestion(result);
|
||||
} catch (e) {
|
||||
setError("Failed to get AI suggestion. Please try again.");
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStatus = () => {
|
||||
switch (item.status) {
|
||||
case "synced":
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1.5 text-green-400">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
<span className="text-xs">Synced</span>
|
||||
</div>
|
||||
);
|
||||
case "syncing":
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1.5 text-yellow-400">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span className="text-xs">Syncing</span>
|
||||
</div>
|
||||
);
|
||||
case "conflict":
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={fetchSuggestion}
|
||||
disabled={isLoading}
|
||||
className="h-auto p-1 text-red-500 hover:bg-red-500/10 hover:text-red-500"
|
||||
>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
<span className="ml-1.5 text-xs">Conflict</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="max-w-2xl">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2 font-headline">
|
||||
<Bot /> AI Conflict Resolution
|
||||
</AlertDialogTitle>
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center p-12 text-center">
|
||||
<ServerCrash className="h-8 w-8 text-destructive" />
|
||||
<p className="mt-4 text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{suggestion && (
|
||||
<AlertDialogDescription>
|
||||
{suggestion.explanation}
|
||||
</AlertDialogDescription>
|
||||
)}
|
||||
</AlertDialogHeader>
|
||||
{suggestion?.suggestedSolutions &&
|
||||
suggestion.suggestedSolutions.length > 0 && (
|
||||
<div className="my-4 rounded-md border bg-muted/50 p-4">
|
||||
<h4 className="mb-2 font-semibold">Suggested Solutions:</h4>
|
||||
<ul className="list-disc space-y-2 pl-5 font-code text-xs">
|
||||
{suggestion.suggestedSolutions.map((solution, i) => (
|
||||
<li key={i}>{solution}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{suggestion && (
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Apply Suggestion</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
)}
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="pl-3 font-code text-xs font-medium">
|
||||
{item.variable}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-auto w-full justify-between p-1.5 font-code text-xs"
|
||||
>
|
||||
<span className="truncate">{JSON.stringify(item.web)}</span>
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3 font-code text-xs">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-2">
|
||||
<span className="text-red-500">Roblox:</span>
|
||||
<span className="text-purple-400">
|
||||
{JSON.stringify(item.roblox)}
|
||||
</span>
|
||||
<span className="text-blue-500">Web:</span>
|
||||
<span className="text-purple-400">
|
||||
{JSON.stringify(item.web)}
|
||||
</span>
|
||||
<span className="text-green-500">Mobile:</span>
|
||||
<span className="text-purple-400">
|
||||
{JSON.stringify(item.mobile)}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
<TableCell className="pr-3 text-right">{renderStatus()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
107
src/components/aethex/dashboard-page.tsx
Normal file
107
src/components/aethex/dashboard-page.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Gamepad2, Plus } from "lucide-react";
|
||||
import { WorkspaceCard } from "./workspace-card";
|
||||
import { WorkspaceCardSkeleton } from "./workspace-card-skeleton";
|
||||
import { workspaces as initialWorkspaces } from "@/lib/workspaces";
|
||||
import { NewProjectModal } from "./new-project-modal";
|
||||
import { MobileIcon, RobloxIcon, WebIcon } from "./icons";
|
||||
|
||||
type Workspace = typeof initialWorkspaces[0];
|
||||
|
||||
export function DashboardPage() {
|
||||
const [workspaces, setWorkspaces] =
|
||||
useState<Workspace[]>(initialWorkspaces);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleCreateProject = (name: string) => {
|
||||
// This is a mock implementation. In a real app, this would involve
|
||||
// an API call to create a new project in the backend.
|
||||
const newWorkspace: Workspace = {
|
||||
id: `proj-${Date.now()}`,
|
||||
name,
|
||||
lastModified: "Just now",
|
||||
platforms: ["roblox"],
|
||||
thumbnailUrlId: "workspace-thumb-4",
|
||||
thumbnailImageHint: "futuristic city",
|
||||
};
|
||||
setWorkspaces((prev) => [newWorkspace, ...prev]);
|
||||
setIsNewProjectModalOpen(false);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<WorkspaceCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
||||
<Gamepad2 className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold text-foreground">
|
||||
No projects yet
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Button onClick={() => setIsNewProjectModalOpen(true)}>
|
||||
<Plus className="-ml-0.5 mr-1.5 h-5 w-5" />
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{workspaces.map((ws) => (
|
||||
<WorkspaceCard key={ws.id} workspace={ws} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header className="mb-8 flex items-center justify-between">
|
||||
<h1 className="font-headline text-3xl font-bold text-foreground">
|
||||
My Workspaces
|
||||
</h1>
|
||||
<Button onClick={() => setIsNewProjectModalOpen(true)}>
|
||||
<Plus className="-ml-1 mr-2" /> New Workspace
|
||||
</Button>
|
||||
</header>
|
||||
<main>{renderContent()}</main>
|
||||
</div>
|
||||
</div>
|
||||
{isNewProjectModalOpen && (
|
||||
<NewProjectModal
|
||||
onCreate={handleCreateProject}
|
||||
onClose={() => setIsNewProjectModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/components/aethex/desktop-app-panel.tsx
Normal file
22
src/components/aethex/desktop-app-panel.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface DesktopAppPanelProps {
|
||||
apps: Array<{ id: string; name: string; icon: string }>;
|
||||
onAppLaunch: (id: string) => void;
|
||||
}
|
||||
|
||||
export const DesktopAppPanel: React.FC<DesktopAppPanelProps> = ({ apps, onAppLaunch }) => {
|
||||
return (
|
||||
<div className="desktop-app-panel">
|
||||
<h2>Desktop Apps</h2>
|
||||
<ul>
|
||||
{apps.map((app) => (
|
||||
<li key={app.id} onClick={() => onAppLaunch(app.id)}>
|
||||
<img src={app.icon} alt={app.name} className="icon" />
|
||||
<span>{app.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
src/components/aethex/education-panel.tsx
Normal file
22
src/components/aethex/education-panel.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface EducationPanelProps {
|
||||
lessons: Array<{ id: string; title: string; completed: boolean }>;
|
||||
onLessonSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const EducationPanel: React.FC<EducationPanelProps> = ({ lessons, onLessonSelect }) => {
|
||||
return (
|
||||
<div className="education-panel">
|
||||
<h2>Education</h2>
|
||||
<ul>
|
||||
{lessons.map((lesson) => (
|
||||
<li key={lesson.id} onClick={() => onLessonSelect(lesson.id)} className={lesson.completed ? 'completed' : ''}>
|
||||
<span>{lesson.title}</span>
|
||||
{lesson.completed && <span className="check">✔</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
20
src/components/aethex/enterprise-analytics-panel.tsx
Normal file
20
src/components/aethex/enterprise-analytics-panel.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
interface EnterpriseAnalyticsPanelProps {
|
||||
data: Array<{ id: string; metric: string; value: number }>;
|
||||
}
|
||||
|
||||
export const EnterpriseAnalyticsPanel: React.FC<EnterpriseAnalyticsPanelProps> = ({ data }) => {
|
||||
return (
|
||||
<div className="enterprise-analytics-panel">
|
||||
<h2>Enterprise Analytics</h2>
|
||||
<ul>
|
||||
{data.map((item) => (
|
||||
<li key={item.id}>
|
||||
<span>{item.metric}:</span> <span>{item.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
211
src/components/aethex/export-modal.tsx
Normal file
211
src/components/aethex/export-modal.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
Download,
|
||||
Folder,
|
||||
FileCode,
|
||||
Package,
|
||||
Check,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ExportModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
platform: "roblox" | "uefn" | "spatial" | "web";
|
||||
}
|
||||
|
||||
const exportFormats = {
|
||||
roblox: [
|
||||
{ id: "rbxlx", name: "Roblox Place (.rbxlx)", description: "Full place file with all assets" },
|
||||
{ id: "rbxmx", name: "Roblox Model (.rbxmx)", description: "Model file for importing" },
|
||||
{ id: "lua", name: "Lua Scripts (.lua)", description: "Export scripts only" },
|
||||
],
|
||||
uefn: [
|
||||
{ id: "uproject", name: "UEFN Project", description: "Full Unreal project structure" },
|
||||
{ id: "verse", name: "Verse Scripts", description: "Export Verse code only" },
|
||||
{ id: "pak", name: "Package (.pak)", description: "Compiled game package" },
|
||||
],
|
||||
spatial: [
|
||||
{ id: "spatial", name: "Spatial Package", description: "Ready for Spatial.io upload" },
|
||||
{ id: "unity", name: "Unity Export", description: "Unity asset format" },
|
||||
{ id: "gltf", name: "glTF Models", description: "3D models only" },
|
||||
],
|
||||
web: [
|
||||
{ id: "html", name: "Static HTML", description: "Single HTML file with embedded code" },
|
||||
{ id: "zip", name: "Project ZIP", description: "Full project archive" },
|
||||
{ id: "npm", name: "NPM Package", description: "Ready for npm publish" },
|
||||
],
|
||||
};
|
||||
|
||||
const platformColors = {
|
||||
roblox: "text-red-400",
|
||||
uefn: "text-purple-400",
|
||||
spatial: "text-emerald-400",
|
||||
web: "text-blue-400",
|
||||
};
|
||||
|
||||
export function ExportModal({ open, onClose, platform }: ExportModalProps) {
|
||||
const [selectedFormat, setSelectedFormat] = useState(exportFormats[platform][0].id);
|
||||
const [exportName, setExportName] = useState("my-project");
|
||||
const [includeAssets, setIncludeAssets] = useState(true);
|
||||
const [minify, setMinify] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [exportProgress, setExportProgress] = useState(0);
|
||||
const [exportComplete, setExportComplete] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
setExportProgress(0);
|
||||
setExportComplete(false);
|
||||
|
||||
// Simulate export progress
|
||||
for (let i = 0; i <= 100; i += 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
setExportProgress(i);
|
||||
}
|
||||
|
||||
setIsExporting(false);
|
||||
setExportComplete(true);
|
||||
|
||||
// Trigger download
|
||||
const content = `-- Exported from AeThex Studio\n-- Platform: ${platform}\n-- Format: ${selectedFormat}\n\nprint("Hello from AeThex!")`;
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${exportName}.${selectedFormat === 'lua' ? 'lua' : 'zip'}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const resetAndClose = () => {
|
||||
setExportComplete(false);
|
||||
setExportProgress(0);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={resetAndClose}>
|
||||
<DialogContent className="max-w-lg p-0 gap-0 bg-[#12121a] border-white/10">
|
||||
<DialogHeader className="px-6 py-4 border-b border-white/10">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Download className="h-5 w-5" />
|
||||
Export Project
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Platform indicator */}
|
||||
<div className={cn("flex items-center gap-2 text-sm", platformColors[platform])}>
|
||||
<Package className="h-4 w-4" />
|
||||
<span className="capitalize font-medium">{platform} Export</span>
|
||||
</div>
|
||||
|
||||
{/* Export name */}
|
||||
<div className="space-y-2">
|
||||
<Label>Export Name</Label>
|
||||
<Input
|
||||
value={exportName}
|
||||
onChange={(e) => setExportName(e.target.value)}
|
||||
placeholder="my-project"
|
||||
className="bg-white/5 border-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Format selection */}
|
||||
<div className="space-y-3">
|
||||
<Label>Export Format</Label>
|
||||
<RadioGroup value={selectedFormat} onValueChange={setSelectedFormat}>
|
||||
{exportFormats[platform].map((format) => (
|
||||
<div
|
||||
key={format.id}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border transition-colors cursor-pointer",
|
||||
selectedFormat === format.id
|
||||
? "border-purple-500 bg-purple-500/10"
|
||||
: "border-white/10 hover:border-white/20"
|
||||
)}
|
||||
onClick={() => setSelectedFormat(format.id)}
|
||||
>
|
||||
<RadioGroupItem value={format.id} className="mt-0.5" />
|
||||
<div>
|
||||
<div className="font-medium text-sm">{format.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{format.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-3">
|
||||
<Label>Options</Label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={includeAssets}
|
||||
onCheckedChange={(v) => setIncludeAssets(!!v)}
|
||||
/>
|
||||
<span className="text-sm">Include assets (images, sounds, models)</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={minify}
|
||||
onCheckedChange={(v) => setMinify(!!v)}
|
||||
/>
|
||||
<span className="text-sm">Minify code for production</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
{(isExporting || exportComplete) && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>{exportComplete ? "Export complete!" : "Exporting..."}</span>
|
||||
<span>{exportProgress}%</span>
|
||||
</div>
|
||||
<Progress value={exportProgress} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10">
|
||||
<Button variant="ghost" onClick={resetAndClose}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || !exportName.trim()}
|
||||
className="bg-purple-600 hover:bg-purple-500 gap-2"
|
||||
>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Exporting...
|
||||
</>
|
||||
) : exportComplete ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Downloaded
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Export
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
325
src/components/aethex/file-navigator.tsx
Normal file
325
src/components/aethex/file-navigator.tsx
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
FolderPlus,
|
||||
FilePlus,
|
||||
Search,
|
||||
MoreHorizontal,
|
||||
Code,
|
||||
FileCode,
|
||||
FileJson,
|
||||
FileText,
|
||||
Image,
|
||||
X,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { FileNode, FolderNode, File as OpenFileType } from "@/lib/aethex-data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FileNavigatorProps = {
|
||||
onOpenFile: (file: OpenFileType) => void;
|
||||
fileTree: FolderNode;
|
||||
onCreateFile?: (name: string, parentPath?: string) => void;
|
||||
onCreateFolder?: (name: string, parentPath?: string) => void;
|
||||
};
|
||||
|
||||
const getFileIcon = (filename: string) => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'lua':
|
||||
case 'luau':
|
||||
return <FileCode className="h-4 w-4 text-blue-400" />;
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
return <FileCode className="h-4 w-4 text-blue-500" />;
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
return <FileCode className="h-4 w-4 text-yellow-400" />;
|
||||
case 'json':
|
||||
return <FileJson className="h-4 w-4 text-yellow-500" />;
|
||||
case 'md':
|
||||
return <FileText className="h-4 w-4 text-gray-400" />;
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
case 'svg':
|
||||
return <Image className="h-4 w-4 text-purple-400" />;
|
||||
default:
|
||||
return <File className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
interface TreeItemProps {
|
||||
node: FileNode | FolderNode;
|
||||
depth: number;
|
||||
onOpenFile: (file: OpenFileType) => void;
|
||||
}
|
||||
|
||||
function TreeItem({ node, depth, onOpenFile }: TreeItemProps) {
|
||||
const [isOpen, setIsOpen] = useState(depth < 2);
|
||||
const isFolder = node.type === 'folder';
|
||||
|
||||
if (isFolder) {
|
||||
const folderNode = node as FolderNode;
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1.5 rounded-sm px-2 py-1 text-sm hover:bg-accent/50 transition-colors",
|
||||
"text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
{isOpen ? (
|
||||
<FolderOpen className="h-4 w-4 text-amber-400 shrink-0" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-amber-400 shrink-0" />
|
||||
)}
|
||||
<span className="truncate">{folderNode.name}</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{folderNode.children.map((child, index) => (
|
||||
<TreeItem
|
||||
key={child.name + index}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
onOpenFile={onOpenFile}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
const fileNode = node as FileNode;
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1.5 rounded-sm px-2 py-1 text-sm hover:bg-accent/50 transition-colors",
|
||||
"text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 12 + 24}px` }}
|
||||
onClick={() =>
|
||||
onOpenFile({
|
||||
id: fileNode.name,
|
||||
name: fileNode.name,
|
||||
language: fileNode.language || "text",
|
||||
content: fileNode.content || "",
|
||||
})
|
||||
}
|
||||
>
|
||||
{getFileIcon(fileNode.name)}
|
||||
<span className="truncate">{fileNode.name}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const FileNavigator: React.FC<FileNavigatorProps> = ({ fileTree, onOpenFile, onCreateFile, onCreateFolder }) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isCreatingFile, setIsCreatingFile] = useState(false);
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||
const [newItemName, setNewItemName] = useState("");
|
||||
|
||||
const handleCreateFile = () => {
|
||||
if (newItemName.trim()) {
|
||||
if (onCreateFile) {
|
||||
onCreateFile(newItemName.trim());
|
||||
}
|
||||
// Also immediately open the new file
|
||||
const ext = newItemName.split('.').pop()?.toLowerCase();
|
||||
let language = 'text';
|
||||
if (ext === 'lua' || ext === 'luau') language = 'lua';
|
||||
else if (ext === 'ts' || ext === 'tsx') language = 'typescript';
|
||||
else if (ext === 'js' || ext === 'jsx') language = 'javascript';
|
||||
else if (ext === 'json') language = 'json';
|
||||
else if (ext === 'md') language = 'markdown';
|
||||
|
||||
onOpenFile({
|
||||
id: newItemName.trim() + '-' + Date.now(),
|
||||
name: newItemName.trim(),
|
||||
language,
|
||||
content: getDefaultContent(newItemName.trim()),
|
||||
});
|
||||
setNewItemName("");
|
||||
setIsCreatingFile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
if (newItemName.trim() && onCreateFolder) {
|
||||
onCreateFolder(newItemName.trim());
|
||||
setNewItemName("");
|
||||
setIsCreatingFolder(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultContent = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'lua':
|
||||
case 'luau':
|
||||
return `-- ${filename}\n-- Created with AeThex Studio\n\nlocal module = {}\n\nfunction module.init()\n print("Hello from ${filename}!")\nend\n\nreturn module\n`;
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
return `// ${filename}\n// Created with AeThex Studio\n\nexport function init() {\n console.log("Hello from ${filename}!");\n}\n`;
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
return `// ${filename}\n// Created with AeThex Studio\n\nfunction init() {\n console.log("Hello from ${filename}!");\n}\n\nmodule.exports = { init };\n`;
|
||||
case 'json':
|
||||
return `{\n "name": "${filename.replace('.json', '')}",\n "version": "1.0.0"\n}\n`;
|
||||
case 'md':
|
||||
return `# ${filename.replace('.md', '')}\n\nCreated with AeThex Studio.\n`;
|
||||
default:
|
||||
return `// ${filename}\n`;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelCreate = () => {
|
||||
setIsCreatingFile(false);
|
||||
setIsCreatingFolder(false);
|
||||
setNewItemName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2.5 border-b border-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded bg-gradient-to-br from-amber-500/20 to-orange-500/20 flex items-center justify-center">
|
||||
<Folder className="h-3 w-3 text-amber-400" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
my-project
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => {
|
||||
setIsCreatingFile(true);
|
||||
setIsCreatingFolder(false);
|
||||
setNewItemName("");
|
||||
}}
|
||||
title="New File"
|
||||
>
|
||||
<FilePlus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => {
|
||||
setIsCreatingFolder(true);
|
||||
setIsCreatingFile(false);
|
||||
setNewItemName("");
|
||||
}}
|
||||
title="New Folder"
|
||||
>
|
||||
<FolderPlus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5">
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New File/Folder Input */}
|
||||
{(isCreatingFile || isCreatingFolder) && (
|
||||
<div className="border-b border-white/5 p-2 bg-white/[0.02]">
|
||||
<div className="flex items-center gap-1">
|
||||
{isCreatingFile ? (
|
||||
<File className="h-4 w-4 text-blue-400 shrink-0" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 text-amber-400 shrink-0" />
|
||||
)}
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={isCreatingFile ? "filename.lua" : "folder-name"}
|
||||
value={newItemName}
|
||||
onChange={(e) => setNewItemName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
isCreatingFile ? handleCreateFile() : handleCreateFolder();
|
||||
} else if (e.key === 'Escape') {
|
||||
cancelCreate();
|
||||
}
|
||||
}}
|
||||
className="h-6 text-xs flex-1 bg-white/5 border-white/10 focus:border-purple-500/50"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10"
|
||||
onClick={isCreatingFile ? handleCreateFile : handleCreateFolder}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={cancelCreate}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-1 ml-5">
|
||||
Enter to create · Escape to cancel
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<Input
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-7 pl-7 text-xs bg-white/5 border-white/5 placeholder:text-muted-foreground/40 focus:border-purple-500/50 focus:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Tree */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="pb-4 px-1">
|
||||
<TreeItem node={fileTree} depth={0} onOpenFile={onOpenFile} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-white/5 px-3 py-2">
|
||||
<div className="text-[10px] text-muted-foreground/60 flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||
<span>Connected to workspace</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileNavigator;
|
||||
5
src/components/aethex/file-tabs.tsx
Normal file
5
src/components/aethex/file-tabs.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
export function FileTabs() {
|
||||
return <div className="file-tabs">File Tabs</div>;
|
||||
}
|
||||
5
src/components/aethex/file-tree.tsx
Normal file
5
src/components/aethex/file-tree.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
export function FileTree() {
|
||||
return <div className="file-tree">File Tree</div>;
|
||||
}
|
||||
14
src/components/aethex/game-preview-panel.tsx
Normal file
14
src/components/aethex/game-preview-panel.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
interface GamePreviewPanelProps {
|
||||
gameUrl: string;
|
||||
}
|
||||
|
||||
export const GamePreviewPanel: React.FC<GamePreviewPanelProps> = ({ gameUrl }) => {
|
||||
return (
|
||||
<div className="game-preview-panel">
|
||||
<h2>Game Preview</h2>
|
||||
<iframe src={gameUrl} title="Game Preview" width="100%" height="500px" frameBorder="0" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
219
src/components/aethex/git-panel.tsx
Normal file
219
src/components/aethex/git-panel.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitPullRequest,
|
||||
GitMerge,
|
||||
Plus,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw,
|
||||
Upload,
|
||||
Download,
|
||||
FileCode,
|
||||
FilePlus,
|
||||
FileMinus,
|
||||
FileEdit,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GitPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const mockChanges = [
|
||||
{ file: "src/main.lua", status: "modified" as const },
|
||||
{ file: "src/utils/helpers.lua", status: "added" as const },
|
||||
{ file: "src/old-module.lua", status: "deleted" as const },
|
||||
];
|
||||
|
||||
const mockBranches = [
|
||||
{ name: "main", current: true },
|
||||
{ name: "feature/new-ui", current: false },
|
||||
{ name: "fix/player-spawn", current: false },
|
||||
];
|
||||
|
||||
const mockCommits = [
|
||||
{ hash: "a1b2c3d", message: "Add player spawn system", author: "You", time: "2 hours ago" },
|
||||
{ hash: "e4f5g6h", message: "Initial project setup", author: "You", time: "5 hours ago" },
|
||||
{ hash: "i7j8k9l", message: "Configure build system", author: "You", time: "1 day ago" },
|
||||
];
|
||||
|
||||
export function GitPanel({ open, onClose }: GitPanelProps) {
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const [selectedBranch, setSelectedBranch] = useState("main");
|
||||
const [stagedFiles, setStagedFiles] = useState<string[]>([]);
|
||||
|
||||
const toggleStage = (file: string) => {
|
||||
setStagedFiles(prev =>
|
||||
prev.includes(file) ? prev.filter(f => f !== file) : [...prev, file]
|
||||
);
|
||||
};
|
||||
|
||||
const stageAll = () => {
|
||||
setStagedFiles(mockChanges.map(c => c.file));
|
||||
};
|
||||
|
||||
const handleCommit = () => {
|
||||
if (commitMessage.trim() && stagedFiles.length > 0) {
|
||||
toast.success(`Committed ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`, {
|
||||
description: commitMessage,
|
||||
});
|
||||
setCommitMessage("");
|
||||
setStagedFiles([]);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: "modified" | "added" | "deleted") => {
|
||||
switch (status) {
|
||||
case "modified":
|
||||
return <FileEdit className="h-4 w-4 text-yellow-400" />;
|
||||
case "added":
|
||||
return <FilePlus className="h-4 w-4 text-emerald-400" />;
|
||||
case "deleted":
|
||||
return <FileMinus className="h-4 w-4 text-red-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl h-[600px] p-0 gap-0 bg-[#12121a] border-white/10">
|
||||
<DialogHeader className="px-6 py-4 border-b border-white/10">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<GitBranch className="h-5 w-5" />
|
||||
Source Control
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Branch selector */}
|
||||
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={(e) => setSelectedBranch(e.target.value)}
|
||||
className="bg-transparent text-sm font-medium focus:outline-none cursor-pointer"
|
||||
>
|
||||
{mockBranches.map(b => (
|
||||
<option key={b.name} value={b.name}>{b.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" title="Create branch">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" title="Refresh">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changes */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase">Changes ({mockChanges.length})</span>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={stageAll}>
|
||||
Stage All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 px-4">
|
||||
<div className="space-y-1">
|
||||
{mockChanges.map((change) => (
|
||||
<div
|
||||
key={change.file}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-white/5 cursor-pointer transition-colors",
|
||||
stagedFiles.includes(change.file) && "bg-purple-500/10"
|
||||
)}
|
||||
onClick={() => toggleStage(change.file)}
|
||||
>
|
||||
<div className={cn(
|
||||
"h-4 w-4 rounded border flex items-center justify-center transition-colors",
|
||||
stagedFiles.includes(change.file)
|
||||
? "bg-purple-500 border-purple-500"
|
||||
: "border-white/20"
|
||||
)}>
|
||||
{stagedFiles.includes(change.file) && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
{getStatusIcon(change.status)}
|
||||
<span className="flex-1 truncate">{change.file}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Commit message */}
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<Input
|
||||
placeholder="Commit message..."
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
className="mb-2 bg-white/5 border-white/10"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-500"
|
||||
disabled={!commitMessage.trim() || stagedFiles.length === 0}
|
||||
onClick={handleCommit}
|
||||
>
|
||||
<GitCommit className="h-4 w-4 mr-2" />
|
||||
Commit ({stagedFiles.length})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar - Recent commits */}
|
||||
<div className="w-64 border-l border-white/10 flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-white/10">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase">Recent Commits</span>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 p-2">
|
||||
<div className="space-y-2">
|
||||
{mockCommits.map((commit) => (
|
||||
<div
|
||||
key={commit.hash}
|
||||
className="p-2 rounded hover:bg-white/5 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<GitCommit className="h-3 w-3 text-muted-foreground" />
|
||||
<code className="text-xs text-purple-400">{commit.hash}</code>
|
||||
</div>
|
||||
<p className="text-sm truncate">{commit.message}</p>
|
||||
<p className="text-xs text-muted-foreground">{commit.time}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Push/Pull */}
|
||||
<div className="p-2 border-t border-white/10 space-y-2">
|
||||
<Button variant="outline" className="w-full h-8 text-xs gap-2">
|
||||
<Download className="h-3 w-3" />
|
||||
Pull
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full h-8 text-xs gap-2">
|
||||
<Upload className="h-3 w-3" />
|
||||
Push
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
90
src/components/aethex/icons.tsx
Normal file
90
src/components/aethex/icons.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function AethexLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M14.5 13.03a3 3 0 1 0-3.5-3.53" />
|
||||
<path d="M12 2a10 10 0 1 0 10 10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function RobloxIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-red-500"
|
||||
{...props}
|
||||
>
|
||||
<path d="m11.9 2.7-8.2 3.4 3.4 8.2 8.2-3.4Z" />
|
||||
<path d="m13.4 7.2-5.7 2.4" />
|
||||
<path d="m19.2 8.5-8.2 3.4-3.4-8.2 8.2-3.4Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function WebIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-blue-500"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-green-500"
|
||||
{...props}
|
||||
>
|
||||
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px" {...props}>
|
||||
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/>
|
||||
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/>
|
||||
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/>
|
||||
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.574l6.19,5.238C39.99,36.596,44,30.85,44,24C44,22.659,43.862,21.35,43.611,20.083z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
1
src/components/aethex/index.ts
Normal file
1
src/components/aethex/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { AethexStudio } from "./aethex-studio";
|
||||
1
src/components/aethex/index.tsx
Normal file
1
src/components/aethex/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { AethexStudio } from "./aethex-studio";
|
||||
19
src/components/aethex/input.tsx
Normal file
19
src/components/aethex/input.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
interface InputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({ value, onChange, placeholder }) => {
|
||||
return (
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
69
src/components/aethex/login-page.tsx
Normal file
69
src/components/aethex/login-page.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { AethexLogo, GoogleIcon } from "./icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Github } from "lucide-react";
|
||||
import { PlaceHolderImages } from "../../lib/placeholder-images";
|
||||
|
||||
export function LoginPage() {
|
||||
const loginIllustration = PlaceHolderImages.find(
|
||||
(p: { id: string }) => p.id === "login-illustration"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full bg-background">
|
||||
<div className="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
|
||||
<div className="mx-auto w-full max-w-sm lg:w-96">
|
||||
<div>
|
||||
<AethexLogo className="h-10 w-auto text-primary" />
|
||||
<h1 className="mt-6 font-headline text-3xl font-bold tracking-tight text-foreground">
|
||||
Welcome to AeThex Studio
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
The Next-Generation Cross-Platform IDE.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="space-y-3">
|
||||
<Link href="/dashboard" passHref>
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-primary via-purple-500 to-fuchsia-500 text-primary-foreground transition-all hover:opacity-90"
|
||||
>
|
||||
Sign in with AeThex Passport
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dashboard" passHref>
|
||||
<Button size="lg" variant="outline" className="w-full">
|
||||
<GoogleIcon className="mr-3 h-5 w-5" />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dashboard" passHref>
|
||||
<Button size="lg" variant="outline" className="w-full">
|
||||
<Github className="mr-3 h-5 w-5" />
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden w-0 flex-1 lg:block">
|
||||
{loginIllustration && (
|
||||
<Image
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={loginIllustration.imageUrl}
|
||||
alt="Cross-platform game development illustration"
|
||||
data-ai-hint={loginIllustration.imageHint}
|
||||
fill
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
212
src/components/aethex/main-view.tsx
Normal file
212
src/components/aethex/main-view.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"use client";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
X,
|
||||
Circle,
|
||||
FileCode,
|
||||
Sparkles,
|
||||
Play,
|
||||
Bug,
|
||||
Braces,
|
||||
ChevronRight,
|
||||
Zap,
|
||||
FileJson,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface OpenFile {
|
||||
id: string;
|
||||
name: string;
|
||||
language: string;
|
||||
content: string;
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
interface MainViewProps {
|
||||
openFiles?: OpenFile[];
|
||||
activeFileId?: string;
|
||||
onFileSelect?: (fileId: string) => void;
|
||||
onFileClose?: (fileId: string) => void;
|
||||
onNewFile?: () => void;
|
||||
onOpenTemplates?: () => void;
|
||||
}
|
||||
|
||||
const getFileIcon = (filename: string, size = "h-3.5 w-3.5") => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'lua':
|
||||
case 'luau':
|
||||
return <FileCode className={cn(size, "text-blue-400")} />;
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
return <FileCode className={cn(size, "text-blue-500")} />;
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
return <FileCode className={cn(size, "text-yellow-400")} />;
|
||||
case 'json':
|
||||
return <FileJson className={cn(size, "text-yellow-500")} />;
|
||||
default:
|
||||
return <FileText className={cn(size, "text-muted-foreground")} />;
|
||||
}
|
||||
};
|
||||
|
||||
export function MainView({
|
||||
openFiles = [],
|
||||
activeFileId,
|
||||
onFileSelect,
|
||||
onFileClose,
|
||||
onNewFile,
|
||||
onOpenTemplates,
|
||||
}: MainViewProps) {
|
||||
const activeFile = openFiles.find((f) => f.id === activeFileId);
|
||||
|
||||
if (openFiles.length === 0) {
|
||||
return (
|
||||
<div className="h-full w-full bg-[#0e0e12] flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4 text-center max-w-sm">
|
||||
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-500/20 to-blue-500/20 flex items-center justify-center border border-white/10">
|
||||
<Zap className="h-6 w-6 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-1">No files open</h2>
|
||||
<p className="text-xs text-muted-foreground">Open a file from the sidebar or create a new one</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 text-xs gap-2 bg-white/5 border-white/10 hover:bg-white/10"
|
||||
onClick={onNewFile}
|
||||
>
|
||||
<FileCode className="h-3.5 w-3.5" />
|
||||
New File
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 text-xs gap-2 bg-white/5 border-white/10 hover:bg-white/10"
|
||||
onClick={onOpenTemplates}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-[#0d0d10]">
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center border-b border-white/5 bg-[#12121a]">
|
||||
<div className="flex-1 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex">
|
||||
{openFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
onClick={() => onFileSelect?.(file.id)}
|
||||
role="tab"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onFileSelect?.(file.id);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group flex items-center gap-2 px-3 py-1.5 text-xs transition-all relative cursor-pointer",
|
||||
"hover:bg-white/5",
|
||||
activeFileId === file.id
|
||||
? "bg-[#0d0d10] text-foreground"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{activeFileId === file.id && (
|
||||
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-purple-500 to-blue-500" />
|
||||
)}
|
||||
{getFileIcon(file.name)}
|
||||
<span className="truncate max-w-32">{file.name}</span>
|
||||
{file.isDirty && (
|
||||
<Circle className="h-1.5 w-1.5 fill-current text-blue-400" />
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFileClose?.(file.id);
|
||||
}}
|
||||
className="ml-1 rounded p-0.5 opacity-0 hover:bg-white/10 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 px-2 border-l border-white/5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => toast.success("Running script...", { description: "Output will appear in the Console panel" })}
|
||||
title="Run Script"
|
||||
>
|
||||
<Play className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => toast.info("Debug mode enabled", { description: "Click line numbers to set breakpoints" })}
|
||||
title="Debug Mode"
|
||||
>
|
||||
<Bug className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={() => toast.success("Code formatted")}
|
||||
title="Format Code"
|
||||
>
|
||||
<Braces className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
{activeFile && (
|
||||
<div className="flex items-center gap-1 px-4 py-1.5 text-[11px] text-muted-foreground border-b border-white/5 bg-[#0d0d10]">
|
||||
<span className="hover:text-foreground cursor-pointer">src</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="hover:text-foreground cursor-pointer">scripts</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-foreground">{activeFile.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor area */}
|
||||
{activeFile && (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="relative min-h-full">
|
||||
{/* Line numbers gutter */}
|
||||
<div className="absolute left-0 top-0 w-14 border-r border-white/5 bg-[#0a0a0c] text-right pr-4 pt-3 text-[11px] text-muted-foreground/30 select-none font-mono">
|
||||
{activeFile.content.split("\n").map((_, i) => (
|
||||
<div key={i} className="leading-6 hover:text-muted-foreground transition-colors">
|
||||
{i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Code content */}
|
||||
<pre className="pl-20 pt-3 pr-4 pb-8 text-[13px] font-mono leading-6 text-foreground/90">
|
||||
<code>{activeFile.content}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
243
src/components/aethex/navbar.tsx
Normal file
243
src/components/aethex/navbar.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Menu,
|
||||
Play,
|
||||
Save,
|
||||
Undo,
|
||||
Redo,
|
||||
Settings,
|
||||
Search,
|
||||
Terminal,
|
||||
PanelLeft,
|
||||
Download,
|
||||
Upload,
|
||||
GitBranch,
|
||||
Users,
|
||||
Zap,
|
||||
Plus,
|
||||
FolderOpen,
|
||||
} from "lucide-react";
|
||||
import { AethexLogo } from "./icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const platformColors = {
|
||||
roblox: "bg-red-500",
|
||||
uefn: "bg-purple-500",
|
||||
spatial: "bg-emerald-500",
|
||||
web: "bg-blue-500",
|
||||
};
|
||||
|
||||
interface NavbarProps {
|
||||
onToggleSidebar?: () => void;
|
||||
onToggleAI?: () => void;
|
||||
onToggleBottomPanel?: () => void;
|
||||
onNewFile?: () => void;
|
||||
onSearch?: () => void;
|
||||
onRun?: () => void;
|
||||
onSave?: () => void;
|
||||
onSettings?: () => void;
|
||||
onGit?: () => void;
|
||||
onExport?: () => void;
|
||||
sidebarOpen?: boolean;
|
||||
aiOpen?: boolean;
|
||||
bottomPanelOpen?: boolean;
|
||||
}
|
||||
|
||||
export function Navbar({
|
||||
onToggleSidebar,
|
||||
onToggleAI,
|
||||
onToggleBottomPanel,
|
||||
onNewFile,
|
||||
onSearch,
|
||||
onRun,
|
||||
onSave,
|
||||
onSettings,
|
||||
onGit,
|
||||
onExport,
|
||||
sidebarOpen = true,
|
||||
aiOpen = true,
|
||||
bottomPanelOpen = true,
|
||||
}: NavbarProps) {
|
||||
const [platform, setPlatform] = useState<keyof typeof platformColors>("roblox");
|
||||
|
||||
const handleCopy = () => {
|
||||
const selection = window.getSelection()?.toString();
|
||||
if (selection) {
|
||||
navigator.clipboard.writeText(selection);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
// Note: Paste would need to be implemented in the editor component
|
||||
console.log("Paste:", text);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="flex h-11 items-center justify-between border-b border-white/5 bg-[#0d0d10] px-2">
|
||||
{/* Left section */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
onClick={onToggleSidebar}
|
||||
>
|
||||
<PanelLeft className={cn("h-4 w-4 transition-transform", !sidebarOpen && "rotate-180")} />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<div className="h-6 w-6 rounded-lg bg-gradient-to-br from-purple-500 via-blue-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-purple-500/20">
|
||||
<AethexLogo className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="font-semibold text-sm bg-gradient-to-r from-white to-white/70 bg-clip-text text-transparent">AeThex</span>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-white/10 mx-1" />
|
||||
|
||||
{/* File Menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
|
||||
File
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={onNewFile}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New File
|
||||
<span className="ml-auto text-xs text-muted-foreground">⌘N</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => toast.info("Use the sidebar to navigate project files")}>
|
||||
<FolderOpen className="mr-2 h-4 w-4" />
|
||||
Open...
|
||||
<span className="ml-auto text-xs text-muted-foreground">⌘O</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onSave}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save
|
||||
<span className="ml-auto text-xs text-muted-foreground">⌘S</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onSave}>Save As...</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onExport}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export Project...
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
|
||||
Edit
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={() => document.execCommand('undo')}>
|
||||
<Undo className="mr-2 h-4 w-4" />
|
||||
Undo
|
||||
<span className="ml-auto text-xs text-muted-foreground">⌘Z</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => document.execCommand('redo')}>
|
||||
<Redo className="mr-2 h-4 w-4" />
|
||||
Redo
|
||||
<span className="ml-auto text-xs text-muted-foreground">⇧⌘Z</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => document.execCommand('cut')}>Cut</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleCopy}>Copy</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handlePaste}>Paste</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
|
||||
View
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={onSearch}>Command Palette</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onToggleSidebar}>Explorer</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onSearch}>Search</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onToggleBottomPanel}>Terminal</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onToggleAI}>AI Assistant</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Center section - Platform selector */}
|
||||
<div className="flex items-center gap-0.5 rounded-full bg-white/5 p-0.5 border border-white/5">
|
||||
{(Object.keys(platformColors) as Array<keyof typeof platformColors>).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 h-6 px-3 text-[11px] font-medium capitalize rounded-full transition-all",
|
||||
platform === p
|
||||
? "bg-white/10 text-white shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setPlatform(p)}
|
||||
>
|
||||
<div className={cn(
|
||||
"h-1.5 w-1.5 rounded-full transition-all",
|
||||
platformColors[p],
|
||||
platform === p && "shadow-lg"
|
||||
)} />
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onSearch}>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onGit}>
|
||||
<GitBranch className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={() => toast.info("1 user online", { description: "Invite others to collaborate in real-time" })}>
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
<div className="h-4 w-px bg-white/10 mx-1" />
|
||||
|
||||
<Button size="sm" className="h-7 gap-1.5 px-3 text-xs bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-500 hover:to-emerald-400 shadow-lg shadow-emerald-500/20 border-0" onClick={onRun}>
|
||||
<Play className="h-3 w-3" />
|
||||
Run
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7 hover:bg-white/5", aiOpen ? "text-purple-400" : "text-muted-foreground")}
|
||||
onClick={onToggleAI}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onSettings}>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
74
src/components/aethex/new-project-modal.tsx
Normal file
74
src/components/aethex/new-project-modal.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface NewProjectModalProps {
|
||||
onCreate: (name: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const NewProjectModal: React.FC<NewProjectModalProps> = ({ onCreate, onClose }) => {
|
||||
const [name, setName] = React.useState('');
|
||||
|
||||
const handleCreate = () => {
|
||||
if (name.trim()) {
|
||||
onCreate(name.trim());
|
||||
setName('');
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-[#1a1a1a] border border-[#333] rounded-xl shadow-2xl w-full max-w-md mx-4 p-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-[#666] hover:text-white transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<h2 className="text-xl font-semibold text-white mb-4">New Project</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#888] mb-2">
|
||||
Project Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleCreate()}
|
||||
placeholder="my-awesome-game"
|
||||
autoFocus
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2.5 rounded-lg border border-[#333] text-[#888] hover:text-white hover:border-[#444] transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={!name.trim()}
|
||||
className="flex-1 px-4 py-2.5 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
src/components/aethex/nexus-sync-monitor.tsx
Normal file
16
src/components/aethex/nexus-sync-monitor.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
interface NexusSyncMonitorProps {
|
||||
status: string;
|
||||
onSync: () => void;
|
||||
}
|
||||
|
||||
export const NexusSyncMonitor: React.FC<NexusSyncMonitorProps> = ({ status, onSync }) => {
|
||||
return (
|
||||
<div className="nexus-sync-monitor">
|
||||
<h2>Nexus Sync</h2>
|
||||
<p>Status: {status}</p>
|
||||
<button onClick={onSync}>Sync Now</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue