modified: app/App.tsx
This commit is contained in:
parent
42a1e2c3e6
commit
4bc31a32e2
110 changed files with 21551 additions and 7019 deletions
70
.gitignore
vendored
70
.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,12 @@ pids
|
|||
.devcontainer/
|
||||
|
||||
.spark-workbench-id
|
||||
|
||||
.env
|
||||
**/agent-eval-report*
|
||||
packages
|
||||
pids
|
||||
.file-manifest
|
||||
.devcontainer/
|
||||
|
||||
.spark-workbench-id
|
||||
|
|
|
|||
7404
PROJECT_BACKUP.md
Normal file
7404
PROJECT_BACKUP.md
Normal file
File diff suppressed because it is too large
Load diff
22
README.md
22
README.md
|
|
@ -1,23 +1,9 @@
|
|||
# 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
|
||||
|
||||
**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!
|
||||
To get started, take a look at src/app/page.tsx.
|
||||
**Platform Switching** - Work with Roblox, UEFN, Spatial, or Core
|
||||
- **Platform Switching** - Work with Roblox, UEFN, Spatial, or Core
|
||||
- **Platform-Specific Templates**:
|
||||
- 🎮 **Roblox**: 25 Lua templates
|
||||
|
|
|
|||
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>
|
||||
25
app/App.tsx
25
app/App.tsx
|
|
@ -1,18 +1,17 @@
|
|||
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';
|
||||
import { Toaster } from '../src/components/ui/sonner';
|
||||
import { CodeEditor } from '../src/components/CodeEditor';
|
||||
import { AIChat } from '../src/components/AIChat';
|
||||
import { Toolbar } from '../src/components/Toolbar';
|
||||
import { TemplatesDrawer } from '../src/components/TemplatesDrawer';
|
||||
import { FileTree, FileNode } from '../src/components/FileTree';
|
||||
import { FileTabs } from '../src/components/FileTabs';
|
||||
import { PreviewModal } from '../src/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 { ConsolePanel } from '../src/components/ConsolePanel';
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../src/components/ui/resizable';
|
||||
import { useIsMobile } from '../src/hooks/use-mobile';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../src/components/ui/tabs';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "./studio-theme.css";
|
||||
import StudioLayout from "../components/StudioLayout";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
|
|
|
|||
18
app/page.tsx
18
app/page.tsx
|
|
@ -1,17 +1,5 @@
|
|||
"use client";
|
||||
import { LoginPage } from "@/components/aethex/login-page";
|
||||
|
||||
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 />;
|
||||
export default function Page() {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
|
|
|||
865
app/studio-theme.css
Normal file
865
app/studio-theme.css
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
/* Tablet region: 1015x768px - global UI sizing */
|
||||
@media (min-width: 900px) and (max-width: 1100px) and (min-height: 700px) and (max-height: 800px) {
|
||||
.ide-container.grid-layout {
|
||||
max-width: 1015px;
|
||||
max-height: 768px;
|
||||
margin: 0 auto;
|
||||
grid-template-rows: 60px 1fr 120px;
|
||||
grid-template-columns: 90px 1fr 220px;
|
||||
grid-template-areas:
|
||||
"title title title"
|
||||
"sidebar editor right"
|
||||
"bottom bottom bottom";
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 24px 0 rgba(0,0,0,0.20);
|
||||
background: #101014;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-title {
|
||||
font-size: 1.25em;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.grid-sidebar {
|
||||
min-width: 90px;
|
||||
max-width: 120px;
|
||||
font-size: 1.18em;
|
||||
padding: 16px 0;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
}
|
||||
.grid-right {
|
||||
min-width: 180px;
|
||||
max-width: 220px;
|
||||
font-size: 1.15em;
|
||||
padding: 16px 0;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
}
|
||||
.grid-editor {
|
||||
min-width: 0;
|
||||
padding: 0 3vw;
|
||||
font-size: 1.22em;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
background: #181818;
|
||||
}
|
||||
.grid-bottom {
|
||||
min-height: 120px;
|
||||
max-height: 180px;
|
||||
font-size: 1.15em;
|
||||
padding: 24px 2vw 56px 2vw;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
background: #15151a;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.title-bar {
|
||||
padding: 16px 20px;
|
||||
font-size: 1.25em;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
background: #18181c;
|
||||
}
|
||||
.editor-tab, .file-item, .bottom-tab {
|
||||
min-height: 52px;
|
||||
font-size: 1.22em;
|
||||
padding: 0 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
background: #222226;
|
||||
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.10);
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.bottom-collapse-btn {
|
||||
font-size: 1.6em !important;
|
||||
min-width: 56px;
|
||||
min-height: 56px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.network-viz.docked {
|
||||
max-width: 440px;
|
||||
padding: 18px 20px;
|
||||
margin: 12px auto;
|
||||
font-size: 1.15em;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
|
||||
}
|
||||
}
|
||||
/* Responsive: docked status panel inside bottom panel for tablet/mobile */
|
||||
.network-viz.docked {
|
||||
margin: 16px auto;
|
||||
padding: 16px 18px;
|
||||
border-radius: 14px;
|
||||
background: #18181c;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.network-viz-header {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.network-nodes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 18px;
|
||||
}
|
||||
.network-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.node-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.node-dot.foundation { background: #ff0000; }
|
||||
.node-dot.corporation { background: #0066ff; }
|
||||
.node-dot.labs { background: #ffa500; }
|
||||
.node-label {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.node-status {
|
||||
font-size: 0.95em;
|
||||
color: #b0b0b0;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.network-viz.docked {
|
||||
max-width: 98vw;
|
||||
padding: 12px 8px;
|
||||
margin: 8px auto;
|
||||
font-size: 1.08em;
|
||||
}
|
||||
.network-nodes {
|
||||
gap: 10px;
|
||||
}
|
||||
.network-node {
|
||||
gap: 2px;
|
||||
}
|
||||
.node-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.ide-container.grid-layout {
|
||||
grid-template-rows: 56px 1fr 120px;
|
||||
grid-template-columns: 72px 1fr 180px;
|
||||
grid-template-areas:
|
||||
"title title title"
|
||||
"sidebar editor right"
|
||||
"bottom bottom bottom";
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 16px 0 rgba(0,0,0,0.18);
|
||||
background: #101014;
|
||||
padding: 8px;
|
||||
}
|
||||
.grid-sidebar {
|
||||
min-width: 56px;
|
||||
max-width: 90px;
|
||||
font-size: 1.15em;
|
||||
padding: 12px 0;
|
||||
border-right: none;
|
||||
background: #18181c;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
.grid-right {
|
||||
min-width: 100px;
|
||||
max-width: 180px;
|
||||
font-size: 1.1em;
|
||||
border-left: none;
|
||||
background: #18181c;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
}
|
||||
.grid-editor {
|
||||
min-width: 0;
|
||||
padding: 0 3vw;
|
||||
font-size: 1.18em;
|
||||
touch-action: manipulation;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
background: #181818;
|
||||
}
|
||||
.grid-bottom {
|
||||
min-height: 80px;
|
||||
max-height: 160px;
|
||||
font-size: 1.1em;
|
||||
padding: 16px 2vw 40px 2vw;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
background: #15151a;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.grid-bottom {
|
||||
padding-bottom: 56px;
|
||||
padding-top: 24px;
|
||||
min-height: 100px;
|
||||
max-height: 180px;
|
||||
}
|
||||
}
|
||||
.title-bar {
|
||||
padding: 12px 12px;
|
||||
font-size: 1.18em;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
|
||||
background: #18181c;
|
||||
}
|
||||
.editor-tab, .file-item, .bottom-tab {
|
||||
min-height: 48px;
|
||||
font-size: 1.18em;
|
||||
padding: 0 16px;
|
||||
touch-action: manipulation;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: #222226;
|
||||
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ide-container.grid-layout {
|
||||
grid-template-rows: 56px 1fr 100px;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"title"
|
||||
"editor"
|
||||
"bottom";
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: #101014;
|
||||
padding: 0;
|
||||
}
|
||||
.grid-sidebar,
|
||||
.grid-right {
|
||||
display: none !important;
|
||||
}
|
||||
.grid-editor {
|
||||
grid-area: editor;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 56px - 100px);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
font-size: 1.22em;
|
||||
padding: 0 4vw;
|
||||
touch-action: manipulation;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: #181818;
|
||||
}
|
||||
.grid-bottom {
|
||||
min-height: 64px;
|
||||
max-height: 120px;
|
||||
width: 100vw;
|
||||
overflow-y: auto;
|
||||
font-size: 1.15em;
|
||||
padding: 0 2vw;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: #15151a;
|
||||
}
|
||||
.title-bar {
|
||||
width: 100vw;
|
||||
min-width: 0;
|
||||
padding: 12px 12px;
|
||||
font-size: 1.22em;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: #18181c;
|
||||
}
|
||||
.editor-tab, .file-item, .bottom-tab {
|
||||
min-height: 48px;
|
||||
font-size: 1.22em;
|
||||
padding: 0 16px;
|
||||
touch-action: manipulation;
|
||||
border-radius: 0;
|
||||
margin-bottom: 8px;
|
||||
background: #222226;
|
||||
box-shadow: none;
|
||||
}
|
||||
.grid-bottom.collapsed {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet-specific layout: 768px to 1024px */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.ide-container.grid-layout {
|
||||
grid-template-rows: 48px 1fr 120px;
|
||||
grid-template-columns: 80px 1fr 220px;
|
||||
grid-template-areas:
|
||||
"title title title"
|
||||
"sidebar editor right"
|
||||
"bottom bottom bottom";
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-sidebar {
|
||||
min-width: 60px;
|
||||
max-width: 100px;
|
||||
font-size: 1.1em;
|
||||
padding: 8px 0;
|
||||
border-right: 1px solid #222;
|
||||
}
|
||||
.grid-right {
|
||||
min-width: 120px;
|
||||
max-width: 220px;
|
||||
font-size: 1em;
|
||||
border-left: 1px solid #222;
|
||||
}
|
||||
.grid-editor {
|
||||
min-width: 0;
|
||||
padding: 0 2vw;
|
||||
font-size: 1.1em;
|
||||
.bottom-tab {
|
||||
display: inline-block;
|
||||
padding: 14px 28px;
|
||||
font-size: 1.18em;
|
||||
border-radius: 10px 10px 0 0;
|
||||
background: #222226;
|
||||
color: #e0e0e0;
|
||||
margin-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
min-width: 56px;
|
||||
min-height: 48px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.bottom-tab.active {
|
||||
background: #333344;
|
||||
color: #fff;
|
||||
}
|
||||
.bottom-collapse-btn {
|
||||
font-size: 1.35em !important;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.bottom-tab {
|
||||
font-size: 1.22em;
|
||||
min-width: 64px;
|
||||
min-height: 52px;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
.bottom-collapse-btn {
|
||||
font-size: 1.5em !important;
|
||||
min-width: 52px;
|
||||
min-height: 52px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
padding: 0 12px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile layout: <768px */
|
||||
@media (max-width: 768px) {
|
||||
.ide-container.grid-layout {
|
||||
grid-template-rows: 48px 1fr 120px;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"title"
|
||||
"editor"
|
||||
"bottom";
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-sidebar,
|
||||
.grid-right {
|
||||
display: none !important;
|
||||
}
|
||||
.grid-editor {
|
||||
grid-area: editor;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 48px - 120px);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
font-size: 1.15em;
|
||||
padding: 0 2vw;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.grid-bottom {
|
||||
min-height: 80px;
|
||||
max-height: 160px;
|
||||
width: 100vw;
|
||||
overflow-y: auto;
|
||||
font-size: 1.1em;
|
||||
padding: 0 1vw;
|
||||
}
|
||||
.title-bar {
|
||||
width: 100vw;
|
||||
min-width: 0;
|
||||
padding: 8px 8px;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
.editor-tab, .file-item, .bottom-tab {
|
||||
min-height: 44px;
|
||||
font-size: 1.15em;
|
||||
padding: 0 12px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.grid-bottom.collapsed {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
/* AeThex Studio Mockup Theme */
|
||||
@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, .ide-container {
|
||||
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ide-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
.keyword { color: #ff0000; font-weight: 700; }
|
||||
.function { color: #0066ff; }
|
||||
.comment { color: #ffa500; font-style: italic; }
|
||||
.string { color: #00ff88; }
|
||||
.number { color: #ff6b9d; }
|
||||
.variable { color: #e0e0e0; }
|
||||
.operator { color: #999; }
|
||||
.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 {
|
||||
height: 200px;
|
||||
background: #0d0d0d;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bottom-tabs-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.bottom-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
.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-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;
|
||||
z-index: 100;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
grid-area: title;
|
||||
z-index: 2;
|
||||
}
|
||||
.grid-sidebar {
|
||||
grid-area: sidebar;
|
||||
min-width: 180px;
|
||||
max-width: 260px;
|
||||
overflow-y: auto;
|
||||
border-right: 2px solid #181818;
|
||||
background: #111;
|
||||
z-index: 1;
|
||||
}
|
||||
.grid-editor {
|
||||
grid-area: editor;
|
||||
overflow: auto;
|
||||
background: #181818;
|
||||
}
|
||||
.grid-right {
|
||||
grid-area: right;
|
||||
min-width: 220px;
|
||||
max-width: 400px;
|
||||
border-left: 2px solid #181818;
|
||||
background: #15151a;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.grid-bottom {
|
||||
grid-area: bottom;
|
||||
border-top: 2px solid #181818;
|
||||
background: #101014;
|
||||
min-height: 120px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.grid-network {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ...continue copying the rest of the CSS from the mockup as needed... */
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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;
|
||||
15611
package-lock.json
generated
15611
package-lock.json
generated
File diff suppressed because it is too large
Load diff
117
package.json
117
package.json
|
|
@ -1,73 +1,72 @@
|
|||
{
|
||||
"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",
|
||||
"@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",
|
||||
"@genkit-ai/google-genai": "^1.20.0",
|
||||
"@genkit-ai/next": "^1.20.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@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-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@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-tooltip": "^1.1.8",
|
||||
"@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",
|
||||
"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",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"firebase": "^11.9.1",
|
||||
"genkit": "^1.20.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"marked": "^12.0.2",
|
||||
"next": "15.5.9",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^19.2.1",
|
||||
"react-day-picker": "^9.11.3",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^5.0.3"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
695
src/App.tsx
695
src/App.tsx
|
|
@ -1,602 +1,65 @@
|
|||
import React, { useState, lazy, Suspense } from 'react';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { CodeEditor } from './components/CodeEditor';
|
||||
import { AIChat } from './components/AIChat';
|
||||
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 { 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 { 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 })));
|
||||
|
||||
function App() {
|
||||
const [currentCode, setCurrentCode] = useState('');
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [showNewProject, setShowNewProject] = useState(false);
|
||||
const [showFileSearch, setShowFileSearch] = useState(false);
|
||||
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
||||
const [showSearchInFiles, setShowSearchInFiles] = useState(false);
|
||||
const [showTranslation, setShowTranslation] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [currentPlatform, setCurrentPlatform] = useState<PlatformId>('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;
|
||||
}
|
||||
});
|
||||
|
||||
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 handleTemplateSelect = (templateCode: string) => {
|
||||
setCode(templateCode);
|
||||
setCurrentCode(templateCode);
|
||||
// Update active file content
|
||||
if (activeFileId) {
|
||||
handleCodeChange(templateCode);
|
||||
}
|
||||
};
|
||||
|
||||
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 handleFileSelect = (file: FileNode) => {
|
||||
if (file.type === 'file') {
|
||||
setActiveFileId(file.id);
|
||||
if (!(openFiles || []).find((f) => f.id === file.id)) {
|
||||
setOpenFiles((prev) => [...(prev || []), file]);
|
||||
}
|
||||
setCode(file.content || '');
|
||||
setCurrentCode(file.content || '');
|
||||
}
|
||||
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 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.');
|
||||
}
|
||||
};
|
||||
|
||||
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 || []);
|
||||
});
|
||||
|
||||
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);
|
||||
captureError(error as Error, { context: 'file_delete', id });
|
||||
toast.error('Failed to delete file. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
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 || [];
|
||||
});
|
||||
|
||||
captureEvent('file_move', { fileId, targetParentId });
|
||||
} catch (error) {
|
||||
console.error('Failed to move file:', error);
|
||||
captureError(error as Error, { context: 'file_move', fileId, targetParentId });
|
||||
toast.error('Failed to move file. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
// TODO: restore all state, handlers, and imports here
|
||||
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)}
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
<StudioSidebar
|
||||
user={user}
|
||||
onLogout={() => setShowPassportLogin(true)}
|
||||
onNewProject={() => setShowNewProject(true)}
|
||||
onOpenTemplates={() => setShowTemplates(true)}
|
||||
onOpenTranslation={() => setShowTranslation(true)}
|
||||
onFileSearch={() => setShowFileSearch(true)}
|
||||
onCommandPalette={() => setShowCommandPalette(true)}
|
||||
consoleCollapsed={consoleCollapsed}
|
||||
onConsoleToggle={() => setConsoleCollapsed((prev) => !prev)}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
<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 || []}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<StudioEditor
|
||||
code={code}
|
||||
onCodeChange={handleCodeChange}
|
||||
currentFileId={activeFileId}
|
||||
onFileSelect={handleFileSelect}
|
||||
isOpen={showSearchInFiles}
|
||||
onClose={() => setShowSearchInFiles(false)}
|
||||
onFileClose={handleFileClose}
|
||||
openFiles={openFiles}
|
||||
/>
|
||||
<StudioBottomPanel
|
||||
onRun={() => {
|
||||
toast.success('Code executed!');
|
||||
captureEvent('run_code');
|
||||
}}
|
||||
onStop={() => {
|
||||
toast.success('Code stopped.');
|
||||
captureEvent('stop_code');
|
||||
}}
|
||||
/>
|
||||
<StudioRightPanel
|
||||
fileId={activeFileId}
|
||||
onFileRename={handleFileRename}
|
||||
onFileDelete={handleFileDelete}
|
||||
onFileMove={handleFileMove}
|
||||
/>
|
||||
<StudioNetworkViz
|
||||
data={{}} // TODO: wire up actual network data
|
||||
onNodeClick={(node) => {
|
||||
setActiveFileId(node.id);
|
||||
setCode(node.content);
|
||||
setCurrentCode(node.content);
|
||||
}}
|
||||
/>
|
||||
<div className="w-full border-t border-border mt-2">
|
||||
<ExtraTabs user={demoUser} />
|
||||
</div>
|
||||
</div>
|
||||
</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 onSelect={handleTemplateSelect} onClose={() => setShowTemplates(false)} />}
|
||||
{showPreview && <PreviewModal code={currentCode} onClose={() => setShowPreview(false)} />}
|
||||
{showNewProject && <NewProjectModal onClose={() => setShowNewProject(false)} />}
|
||||
{showTranslation && <TranslationPanel onClose={() => setShowTranslation(false)} />}
|
||||
{showPassportLogin && <PassportLogin onClose={() => setShowPassportLogin(false)} />}
|
||||
</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)}
|
||||
|
|
@ -604,56 +67,14 @@ end)`,
|
|||
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');
|
||||
}
|
||||
},
|
||||
onExport: () => toast.success('Exported!'),
|
||||
onCopy: () => 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,7 +3,12 @@ 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;
|
||||
|
|
|
|||
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';
|
||||
129
src/ai/flows/ai-help-from-prompt.ts
Normal file
129
src/ai/flows/ai-help-from-prompt.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
'use server';
|
||||
|
||||
/**
|
||||
* @fileOverview This file defines a Genkit flow that helps new users by suggesting an initial set of code files
|
||||
* and project structure based on a simple prompt describing the desired application.
|
||||
*
|
||||
* - aiHelpFromPrompt - A function that takes a prompt and returns suggested code files and project structure.
|
||||
* - AIHelpFromPromptInput - The input type for the aiHelpFromPrompt function.
|
||||
* - AIHelpFromPromptOutput - The return type for the aiHelpFromPrompt function.
|
||||
*/
|
||||
|
||||
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>;
|
||||
|
||||
export async function aiHelpFromPrompt(input: AIHelpFromPromptInput): Promise<AIHelpFromPromptOutput> {
|
||||
return aiHelpFromPromptFlow(input);
|
||||
}
|
||||
|
||||
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!;
|
||||
}
|
||||
);
|
||||
'use server';
|
||||
|
||||
/**
|
||||
* @fileOverview This file defines a Genkit flow that helps new users by suggesting an initial set of code files
|
||||
* and project structure based on a simple prompt describing the desired application.
|
||||
*
|
||||
* - aiHelpFromPrompt - A function that takes a prompt and returns suggested code files and project structure.
|
||||
* - AIHelpFromPromptInput - The input type for the aiHelpFromPrompt function.
|
||||
* - AIHelpFromPromptOutput - The return type for the aiHelpFromPrompt function.
|
||||
*/
|
||||
|
||||
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>;
|
||||
|
||||
export async function aiHelpFromPrompt(input: AIHelpFromPromptInput): Promise<AIHelpFromPromptOutput> {
|
||||
return aiHelpFromPromptFlow(input);
|
||||
}
|
||||
|
||||
const prompt = ai.definePrompt({
|
||||
name: 'aiHelpFromPromptPrompt',
|
||||
input: {schema: AIHelpFromPromptInputSchema},
|
||||
120
src/ai/flows/ai-suggested-sync-conflict-resolution.ts
Normal file
120
src/ai/flows/ai-suggested-sync-conflict-resolution.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
'use server';
|
||||
|
||||
/**
|
||||
* @fileOverview An AI agent that detects synchronization conflicts between platform-specific code and data,
|
||||
* and suggests solutions to resolve them.
|
||||
*
|
||||
* - aiSuggestedSyncConflictResolution - A function that handles the conflict resolution process.
|
||||
* - AISuggestedSyncConflictResolutionInput - The input type for the aiSuggestedSyncConflictResolution function.
|
||||
* - AISuggestedSyncConflictResolutionOutput - The return type for the aiSuggestedSyncConflictResolution function.
|
||||
*/
|
||||
|
||||
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>;
|
||||
|
||||
export async function aiSuggestedSyncConflictResolution(input: AISuggestedSyncConflictResolutionInput): Promise<AISuggestedSyncConflictResolutionOutput> {
|
||||
return aiSuggestedSyncConflictResolutionFlow(input);
|
||||
}
|
||||
|
||||
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 => {
|
||||
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!;
|
||||
}
|
||||
);
|
||||
'use server';
|
||||
|
||||
/**
|
||||
* @fileOverview An AI agent that detects synchronization conflicts between platform-specific code and data,
|
||||
* and suggests solutions to resolve them.
|
||||
*
|
||||
* - aiSuggestedSyncConflictResolution - A function that handles the conflict resolution process.
|
||||
* - AISuggestedSyncConflictResolutionInput - The input type for the aiSuggestedSyncConflictResolution function.
|
||||
* - AISuggestedSyncConflictResolutionOutput - The return type for the aiSuggestedSyncConflictResolution function.
|
||||
*/
|
||||
|
||||
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>;
|
||||
|
||||
export async function aiSuggestedSyncConflictResolution(input: AISuggestedSyncConflictResolutionInput): Promise<AISuggestedSyncConflictResolutionOutput> {
|
||||
return aiSuggestedSyncConflictResolutionFlow(input);
|
||||
}
|
||||
111
src/ai/flows/contextual-code-suggestions.ts
Normal file
111
src/ai/flows/contextual-code-suggestions.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
'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 => {
|
||||
const {output} = await prompt(input);
|
||||
return output!;
|
||||
}
|
||||
);
|
||||
'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> {
|
||||
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',
|
||||
});
|
||||
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 />;
|
||||
}
|
||||
23
src/app/globals.css
Normal file
23
src/app/globals.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@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: 278 52% 49%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 277 100% 25%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 180 100% 25%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
9
src/app/ide/page.tsx
Normal file
9
src/app/ide/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { AethexStudio } from "@/components/aethex/aethex-studio";
|
||||
|
||||
export default function IdePage() {
|
||||
return (
|
||||
<main className="h-[100svh] w-screen overflow-hidden bg-background">
|
||||
<AethexStudio />
|
||||
</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@400;700&family=Source+Code+Pro:wght@400&family=Space+Grotesk:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="font-body antialiased">
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
src/app/page.tsx
Normal file
5
src/app/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { LoginPage } from "@/components/aethex/login-page";
|
||||
|
||||
export default function Page() {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
|
@ -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,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');
|
||||
|
|
|
|||
109
src/components/aethex/aethex-studio.tsx
Normal file
109
src/components/aethex/aethex-studio.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
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 {
|
||||
openFiles as initialOpenFiles,
|
||||
fileTree as initialFileTree,
|
||||
File as OpenFileType,
|
||||
FolderNode,
|
||||
FileNode,
|
||||
} from "@/lib/aethex-data";
|
||||
import { NewProjectModal } from "./new-project-modal";
|
||||
import {
|
||||
ProjectTemplate,
|
||||
generateFileContent,
|
||||
} from "@/lib/templates";
|
||||
import type { NewProjectFormValues } from "./new-project-modal";
|
||||
|
||||
export type { OpenFileType };
|
||||
|
||||
export function AethexStudio() {
|
||||
const [openFiles, setOpenFiles] = useState<OpenFileType[]>(initialOpenFiles);
|
||||
const [activeTab, setActiveTab] = useState<string>(openFiles[0]?.id || "");
|
||||
const [fileTree, setFileTree] = useState<FolderNode>(initialFileTree);
|
||||
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
||||
|
||||
const handleOpenFile = (file: OpenFileType) => {
|
||||
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 = (
|
||||
template: ProjectTemplate,
|
||||
config: NewProjectFormValues
|
||||
) => {
|
||||
const newFileTree: FolderNode = {
|
||||
...template.fileTree,
|
||||
name: config.projectName,
|
||||
};
|
||||
setFileTree(newFileTree);
|
||||
|
||||
let mainFileToOpen: OpenFileType | undefined;
|
||||
|
||||
const findMainFile = (node: FolderNode | FileNode, currentPath: string) => {
|
||||
if (mainFileToOpen) return;
|
||||
const newPath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
||||
if (node.type === "file" && node.name === template.mainFile) {
|
||||
mainFileToOpen = {
|
||||
id: newPath,
|
||||
name: node.name,
|
||||
content: generateFileContent(node, template),
|
||||
};
|
||||
} else if (node.type === "folder" && node.children) {
|
||||
node.children.forEach((child) => findMainFile(child, newPath));
|
||||
}
|
||||
};
|
||||
|
||||
findMainFile(newFileTree, "");
|
||||
if (mainFileToOpen) {
|
||||
setOpenFiles([mainFileToOpen]);
|
||||
setActiveTab(mainFileToOpen.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aethex-studio">
|
||||
{/* ...rest of the studio UI... */}
|
||||
<Navbar />
|
||||
<FileNavigator fileTree={fileTree} onOpenFile={handleOpenFile} />
|
||||
<MainView
|
||||
openFiles={openFiles}
|
||||
activeTab={activeTab}
|
||||
onCloseFile={handleCloseFile}
|
||||
onOpenFile={handleOpenFile}
|
||||
/>
|
||||
<BottomPanel />
|
||||
<AiAssistant />
|
||||
<NewProjectModal
|
||||
open={isNewProjectModalOpen}
|
||||
onOpenChange={setIsNewProjectModalOpen}
|
||||
onCreate={handleCreateProject}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/aethex/ai-assistant.tsx
Normal file
40
src/components/aethex/ai-assistant.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use client";
|
||||
|
||||
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,
|
||||
} from "lucide-react";
|
||||
import { AethexLogo } from "./icons";
|
||||
import { useState, useRef, useEffect, memo } from "react";
|
||||
import { aiHelpFromPrompt } from "@/ai/flows/ai-help-from-prompt";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { marked } from "marked";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
// ...rest of the AiAssistant code from backup...
|
||||
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>
|
||||
);
|
||||
};
|
||||
62
src/components/aethex/bottom-panel.tsx
Normal file
62
src/components/aethex/bottom-panel.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { consoleLogs } from "@/lib/aethex-data";
|
||||
import { ChevronRight, HardDrive } from "lucide-react";
|
||||
|
||||
export function BottomPanel() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<Tabs defaultValue="console" className="flex h-full flex-col">
|
||||
<TabsList className="mx-2 mt-2 self-start rounded-md">
|
||||
<TabsTrigger value="console">Console</TabsTrigger>
|
||||
<TabsTrigger value="terminal">Terminal</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="console" className="flex-1 overflow-auto p-4 text-xs">
|
||||
<div className="font-code">
|
||||
{consoleLogs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-start gap-2 border-b border-border/50 py-1 ${
|
||||
log.type === "error"
|
||||
? "text-destructive"
|
||||
: log.type === "warn"
|
||||
? "text-yellow-400"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<span className="w-20 shrink-0 text-foreground/50">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span
|
||||
className={`w-12 shrink-0 font-bold ${
|
||||
log.platform === "Roblox"
|
||||
? "text-red-500"
|
||||
: log.platform === "Web"
|
||||
? "text-blue-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
[{log.platform}]
|
||||
</span>
|
||||
<p className="flex-1 whitespace-pre-wrap">{log.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="terminal" className="h-full">
|
||||
<div className="flex h-full flex-col bg-background p-4 font-code text-xs">
|
||||
<p>AeThex Terminal</p>
|
||||
<p>Copyright (c) 2024. All rights reserved.</p>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<HardDrive className="h-3 w-3 text-accent" />
|
||||
<span className="text-accent">~/aethex-project</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="flex-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</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 { OpenFileType } from "./aethex-studio";
|
||||
|
||||
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 direction="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>
|
||||
);
|
||||
}
|
||||
115
src/components/aethex/dashboard-page.tsx
Normal file
115
src/components/aethex/dashboard-page.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"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 { ProjectTemplate } from "@/lib/templates";
|
||||
import { NewProjectFormValues } 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 = (
|
||||
template: ProjectTemplate,
|
||||
config: NewProjectFormValues
|
||||
) => {
|
||||
// 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: config.projectName,
|
||||
lastModified: "Just now",
|
||||
platforms: config.platforms.map((p) => {
|
||||
if (p === "roblox") return RobloxIcon;
|
||||
if (p === "web") return WebIcon;
|
||||
return MobileIcon;
|
||||
}),
|
||||
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>
|
||||
<NewProjectModal
|
||||
isOpen={isNewProjectModalOpen}
|
||||
onClose={() => setIsNewProjectModalOpen(false)}
|
||||
onCreateProject={handleCreateProject}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
24
src/components/aethex/file-navigator.tsx
Normal file
24
src/components/aethex/file-navigator.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
ChevronRight,
|
||||
FolderPlus,
|
||||
FilePlus,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FileNode, FolderNode, File as OpenFileType } from "@/lib/aethex-data";
|
||||
import { generateFileContent } from "@/lib/templates";
|
||||
|
||||
type FileNavigatorProps = {
|
||||
onOpenFile: (file: OpenFileType) => void;
|
||||
fileTree: FolderNode;
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
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>
|
||||
)
|
||||
}
|
||||
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 "@/components/aethex/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) => 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>
|
||||
);
|
||||
}
|
||||
24
src/components/aethex/new-project-modal.tsx
Normal file
24
src/components/aethex/new-project-modal.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
interface NewProjectModalProps {
|
||||
onCreate: (name: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const NewProjectModal: React.FC<NewProjectModalProps> = ({ onCreate, onClose }) => {
|
||||
const [name, setName] = React.useState('');
|
||||
|
||||
return (
|
||||
<div className="new-project-modal">
|
||||
<h2>New Project</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Project Name"
|
||||
/>
|
||||
<button onClick={() => onCreate(name)} disabled={!name}>Create</button>
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
15
src/components/aethex/onboarding-dialog.tsx
Normal file
15
src/components/aethex/onboarding-dialog.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export const OnboardingDialog: React.FC<OnboardingDialogProps> = ({ onComplete }) => {
|
||||
return (
|
||||
<div className="onboarding-dialog">
|
||||
<h2>Onboarding</h2>
|
||||
<p>Follow the steps to set up your workspace and start building.</p>
|
||||
<button onClick={onComplete}>Finish</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
15
src/components/aethex/passport-login.tsx
Normal file
15
src/components/aethex/passport-login.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
interface PassportLoginProps {
|
||||
onLogin: (provider: string) => void;
|
||||
}
|
||||
|
||||
export const PassportLogin: React.FC<PassportLoginProps> = ({ onLogin }) => {
|
||||
return (
|
||||
<div className="passport-login">
|
||||
<h2>Login</h2>
|
||||
<button onClick={() => onLogin('google')}>Login with Google</button>
|
||||
<button onClick={() => onLogin('github')}>Login with GitHub</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
src/components/aethex/preview-modal.tsx
Normal file
16
src/components/aethex/preview-modal.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
interface PreviewModalProps {
|
||||
url: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PreviewModal: React.FC<PreviewModalProps> = ({ url, onClose }) => {
|
||||
return (
|
||||
<div className="preview-modal">
|
||||
<h2>Preview</h2>
|
||||
<iframe src={url} title="Preview" width="100%" height="500px" frameBorder="0" />
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
src/components/aethex/progress.tsx
Normal file
12
src/components/aethex/progress.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
interface ProgressProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export const Progress: React.FC<ProgressProps> = ({ value, max = 100 }) => {
|
||||
return (
|
||||
<progress className="progress" value={value} max={max} />
|
||||
);
|
||||
};
|
||||
14
src/components/aethex/spatial-panel.tsx
Normal file
14
src/components/aethex/spatial-panel.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
interface SpatialPanelProps {
|
||||
sceneUrl: string;
|
||||
}
|
||||
|
||||
export const SpatialPanel: React.FC<SpatialPanelProps> = ({ sceneUrl }) => {
|
||||
return (
|
||||
<div className="spatial-panel">
|
||||
<h2>Spatial View</h2>
|
||||
<iframe src={sceneUrl} title="Spatial View" width="100%" height="500px" frameBorder="0" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
src/components/aethex/switch.tsx
Normal file
19
src/components/aethex/switch.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
interface SwitchProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const Switch: React.FC<SwitchProps> = ({ checked, onChange }) => {
|
||||
return (
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
/>
|
||||
<span className="slider" />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
23
src/components/aethex/tabs.tsx
Normal file
23
src/components/aethex/tabs.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TabsProps {
|
||||
tabs: Array<{ id: string; label: string }>;
|
||||
activeTabId: string;
|
||||
onTabSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const Tabs: React.FC<TabsProps> = ({ tabs, activeTabId, onTabSelect }) => {
|
||||
return (
|
||||
<div className="tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={tab.id === activeTabId ? 'active' : ''}
|
||||
onClick={() => onTabSelect(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
src/components/aethex/teacher-dashboard.tsx
Normal file
22
src/components/aethex/teacher-dashboard.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TeacherDashboardProps {
|
||||
students: Array<{ id: string; name: string; progress: number }>;
|
||||
onStudentSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TeacherDashboard: React.FC<TeacherDashboardProps> = ({ students, onStudentSelect }) => {
|
||||
return (
|
||||
<div className="teacher-dashboard">
|
||||
<h2>Teacher Dashboard</h2>
|
||||
<ul>
|
||||
{students.map((student) => (
|
||||
<li key={student.id} onClick={() => onStudentSelect(student.id)}>
|
||||
<span>{student.name}</span>
|
||||
<progress value={student.progress} max={100} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
src/components/aethex/team-collab-panel.tsx
Normal file
22
src/components/aethex/team-collab-panel.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TeamCollabPanelProps {
|
||||
members: Array<{ id: string; name: string; online: boolean }>;
|
||||
onMemberClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TeamCollabPanel: React.FC<TeamCollabPanelProps> = ({ members, onMemberClick }) => {
|
||||
return (
|
||||
<div className="team-collab-panel">
|
||||
<h2>Team Collaboration</h2>
|
||||
<ul>
|
||||
{members.map((member) => (
|
||||
<li key={member.id} onClick={() => onMemberClick(member.id)} className={member.online ? 'online' : 'offline'}>
|
||||
<span>{member.name}</span>
|
||||
{member.online && <span className="dot online-dot" />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
src/components/aethex/templates-drawer.tsx
Normal file
22
src/components/aethex/templates-drawer.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TemplatesDrawerProps {
|
||||
templates: Array<{ id: string; name: string; description: string }>;
|
||||
onTemplateSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TemplatesDrawer: React.FC<TemplatesDrawerProps> = ({ templates, onTemplateSelect }) => {
|
||||
return (
|
||||
<div className="templates-drawer">
|
||||
<h2>Templates</h2>
|
||||
<ul>
|
||||
{templates.map((tpl) => (
|
||||
<li key={tpl.id} onClick={() => onTemplateSelect(tpl.id)}>
|
||||
<span>{tpl.name}</span>
|
||||
<p>{tpl.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
20
src/components/aethex/textarea.tsx
Normal file
20
src/components/aethex/textarea.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TextareaProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const Textarea: React.FC<TextareaProps> = ({ value, onChange, placeholder }) => {
|
||||
return (
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={5}
|
||||
cols={40}
|
||||
/>
|
||||
);
|
||||
};
|
||||
21
src/components/aethex/translation-panel.tsx
Normal file
21
src/components/aethex/translation-panel.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
interface TranslationPanelProps {
|
||||
translations: Array<{ id: string; language: string; text: string }>;
|
||||
onTranslationSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TranslationPanel: React.FC<TranslationPanelProps> = ({ translations, onTranslationSelect }) => {
|
||||
return (
|
||||
<div className="translation-panel">
|
||||
<h2>Translations</h2>
|
||||
<ul>
|
||||
{translations.map((t) => (
|
||||
<li key={t.id} onClick={() => onTranslationSelect(t.id)}>
|
||||
<span>{t.language}:</span> <span>{t.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
src/components/aethex/uefn-panel.tsx
Normal file
21
src/components/aethex/uefn-panel.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
interface UEFNPanelProps {
|
||||
projects: Array<{ id: string; name: string; status: string }>;
|
||||
onProjectSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const UEFNPanel: React.FC<UEFNPanelProps> = ({ projects, onProjectSelect }) => {
|
||||
return (
|
||||
<div className="uefn-panel">
|
||||
<h2>UEFN Projects</h2>
|
||||
<ul>
|
||||
{projects.map((proj) => (
|
||||
<li key={proj.id} onClick={() => onProjectSelect(proj.id)}>
|
||||
<span>{proj.name}</span> <span className={`status ${proj.status}`}>{proj.status}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
15
src/components/aethex/user-profile.tsx
Normal file
15
src/components/aethex/user-profile.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
interface UserProfileProps {
|
||||
user: { id: string; name: string; avatar: string; email: string };
|
||||
}
|
||||
|
||||
export const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<img src={user.avatar} alt={user.name} className="avatar" />
|
||||
<h2>{user.name}</h2>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
15
src/components/aethex/welcome-dialog.tsx
Normal file
15
src/components/aethex/welcome-dialog.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
interface WelcomeDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WelcomeDialog: React.FC<WelcomeDialogProps> = ({ onClose }) => {
|
||||
return (
|
||||
<div className="welcome-dialog">
|
||||
<h2>Welcome to AeThex Studio!</h2>
|
||||
<p>Get started by exploring the panels and features on the left.</p>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
src/components/aethex/workspace-card-skeleton.tsx
Normal file
12
src/components/aethex/workspace-card-skeleton.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
|
||||
export function WorkspaceCardSkeleton() {
|
||||
return (
|
||||
<div className="animate-pulse rounded-lg border bg-card p-4 shadow">
|
||||
<div className="h-32 w-full rounded bg-muted" />
|
||||
<div className="mt-4 h-4 w-1/2 rounded bg-muted" />
|
||||
<div className="mt-2 h-3 w-1/3 rounded bg-muted" />
|
||||
<div className="mt-2 h-3 w-1/4 rounded bg-muted" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/components/aethex/workspace-card.tsx
Normal file
27
src/components/aethex/workspace-card.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react";
|
||||
|
||||
interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
lastModified: string;
|
||||
platforms: React.ComponentType[];
|
||||
thumbnailUrlId: string;
|
||||
thumbnailImageHint: string;
|
||||
}
|
||||
|
||||
export function WorkspaceCard({ workspace }: { workspace: Workspace }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 shadow">
|
||||
<div className="h-32 w-full rounded bg-muted flex items-center justify-center">
|
||||
<span className="text-4xl">🗂️</span>
|
||||
</div>
|
||||
<div className="mt-4 font-semibold text-lg">{workspace.name}</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">{workspace.lastModified}</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
{workspace.platforms.map((PlatformIcon, i) => (
|
||||
<PlatformIcon key={i} className="h-4 w-4" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
"relative w-full rounded-xl border-2 border-accent/40 bg-card/80 backdrop-blur-md px-6 py-4 text-sm shadow-lg grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-4 gap-y-1 items-start [&>svg]:size-5 [&>svg]:translate-y-0.5 [&>svg]:text-accent",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold shadow-lg bg-gradient-to-br from-accent/80 to-primary/90 backdrop-blur-md border border-accent/40 transition-all duration-200 hover:scale-[1.03] hover:shadow-xl focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Card({ className, ...props }: ComponentProps<"div">) {
|
|||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
"bg-card/80 backdrop-blur-lg text-card-foreground flex flex-col gap-8 rounded-2xl border-2 border-accent/30 py-8 px-6 shadow-2xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -102,9 +102,25 @@ ${colorConfig
|
|||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
interface ChartTooltipContentProps {
|
||||
active?: boolean;
|
||||
payload?: any[];
|
||||
className?: string;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
label?: any;
|
||||
labelFormatter?: (label: any, payload: any[]) => ReactNode;
|
||||
labelClassName?: string;
|
||||
formatter?: (value: any, name: any, item: any, index: number, payload: any) => ReactNode;
|
||||
color?: string;
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
payload = [],
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
|
|
@ -116,14 +132,7 @@ function ChartTooltipContent({
|
|||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
}: ChartTooltipContentProps) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = useMemo(() => {
|
||||
|
|
@ -248,17 +257,21 @@ function ChartTooltipContent({
|
|||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
interface ChartLegendContentProps {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
payload?: any[];
|
||||
verticalAlign?: string;
|
||||
nameKey?: string;
|
||||
}
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
payload = [],
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
}: ChartLegendContentProps) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
"use client"
|
||||
|
||||
import { ComponentProps } from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import {
|
||||
Root as ContextMenuRoot,
|
||||
Trigger as ContextMenuTrigger,
|
||||
Content as ContextMenuContent,
|
||||
Item as ContextMenuItem,
|
||||
Separator as ContextMenuSeparator,
|
||||
CheckboxItem as ContextMenuCheckboxItem,
|
||||
RadioGroup as ContextMenuRadioGroup,
|
||||
RadioItem as ContextMenuRadioItem,
|
||||
Sub as ContextMenuSub,
|
||||
SubTrigger as ContextMenuSubTrigger,
|
||||
SubContent as ContextMenuSubContent,
|
||||
Label as ContextMenuLabel,
|
||||
Group as ContextMenuGroup,
|
||||
} from "@radix-ui/react-context-menu"
|
||||
import CheckIcon from "lucide-react/dist/esm/icons/check";
|
||||
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
|
||||
import CircleIcon from "lucide-react/dist/esm/icons/circle"
|
||||
|
|
@ -10,45 +24,45 @@ import { cn } from "@/lib/utils"
|
|||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
}: ComponentProps<typeof ContextMenuRoot>) {
|
||||
return <ContextMenuRoot data-slot="context-menu" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
}: ComponentProps<typeof ContextMenuTrigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
<ContextMenuTrigger data-slot="context-menu-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
}: ComponentProps<typeof ContextMenuGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
<ContextMenuGroup data-slot="context-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
}: ComponentProps<typeof ContextMenuPortal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
<ContextMenuPortal data-slot="context-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
}: ComponentProps<typeof ContextMenuSub>) {
|
||||
return <ContextMenuSub data-slot="context-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
}: ComponentProps<typeof ContextMenuRadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
<ContextMenuRadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -60,11 +74,11 @@ function ContextMenuSubTrigger({
|
|||
inset,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
}: ComponentProps<typeof ContextMenuSubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
<ContextMenuSubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
|
|
@ -75,21 +89,21 @@ function ContextMenuSubTrigger({
|
|||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
</ContextMenuSubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
}: ComponentProps<typeof ContextMenuSubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
<SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
)
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -98,18 +112,18 @@ function ContextMenuSubContent({
|
|||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
}: ComponentProps<typeof ContextMenuContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
<ContextMenuPortal>
|
||||
<Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
)
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
</ContextMenuPortal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -118,19 +132,19 @@ function ContextMenuItem({
|
|||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
}: ComponentProps<typeof Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
<Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
)
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -141,24 +155,24 @@ function ContextMenuCheckboxItem({
|
|||
children,
|
||||
checked,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
}: ComponentProps<typeof CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
<CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
)
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<span>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
</CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -166,23 +180,23 @@ function ContextMenuRadioItem({
|
|||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
}: ComponentProps<typeof RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
<RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
)
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<span>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
</RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -190,17 +204,17 @@ function ContextMenuLabel({
|
|||
className,
|
||||
inset,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
}: ComponentProps<typeof Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
<Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
)
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -209,9 +223,9 @@ function ContextMenuLabel({
|
|||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
}: ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
<Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import { DesktopAppPanel } from '../DesktopAppPanel';
|
|||
import { TeamCollabPanel } from '../TeamCollabPanel';
|
||||
import { EnterpriseAnalyticsPanel } from '../EnterpriseAnalyticsPanel';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ExtraTabs({ user }: { user: any }) {
|
||||
const [onboardingOpen, setOnboardingOpen] = useState(true);
|
||||
return (
|
||||
<Tabs defaultValue="onboarding" className="h-full flex flex-col">
|
||||
<TabsList className="w-full rounded-none border-b border-border">
|
||||
|
|
@ -30,7 +33,9 @@ export function ExtraTabs({ user }: { user: any }) {
|
|||
<TabsTrigger value="team">Team</TabsTrigger>
|
||||
<TabsTrigger value="enterprise">Enterprise</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="onboarding"><OnboardingDialog open={true} onClose={() => {}} /></TabsContent>
|
||||
<TabsContent value="onboarding">
|
||||
<OnboardingDialog open={onboardingOpen} onClose={() => setOnboardingOpen(false)} />
|
||||
</TabsContent>
|
||||
<TabsContent value="profile"><UserProfile user={user} /></TabsContent>
|
||||
<TabsContent value="uefn"><UEFNPanel /></TabsContent>
|
||||
<TabsContent value="preview"><GamePreviewPanel /></TabsContent>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function TabsList({
|
|||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
"bg-muted/60 backdrop-blur-md text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-xl border border-accent/30 shadow-md p-[4px] transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -42,7 +42,7 @@ function TabsTrigger({
|
|||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[state=active]:bg-background/80 data-[state=active]:border-accent/60 data-[state=active]:text-accent focus-visible:border-accent focus-visible:ring-accent/40 focus-visible:outline-accent dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-2px)] flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent px-3 py-1.5 text-base font-semibold whitespace-nowrap transition-all duration-200 data-[state=active]:shadow-lg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue