Improve error handling across components
- Add try-catch blocks for localStorage operations (user state, login, signout) - Add validation and error handling for file operations (create, rename, delete, move) - Add error handling for project creation with validation - Improve error logging with Sentry context in AIChat - Add loading state and error handling for Monaco editor - Add user-friendly error messages via toast notifications - Add console.error logging for debugging - Validate inputs (empty names, etc.) before processing
This commit is contained in:
parent
ebd535f106
commit
d43e0a3a27
5 changed files with 754 additions and 144 deletions
268
CONTRIBUTING.md
Normal file
268
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# Contributing to AeThex Studio
|
||||
|
||||
Thank you for your interest in contributing to AeThex Studio! We welcome contributions from the community.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Fork the repository** on GitHub
|
||||
2. **Clone your fork** locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/aethex-studio.git
|
||||
cd aethex-studio
|
||||
```
|
||||
3. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
4. **Create a branch** for your feature:
|
||||
```bash
|
||||
git checkout -b feature/my-amazing-feature
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Development Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see your changes in real-time.
|
||||
|
||||
### Code Style
|
||||
|
||||
- We use **TypeScript** for type safety
|
||||
- Follow existing code patterns and conventions
|
||||
- Use **ESLint** to check your code:
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Before submitting a PR, ensure all tests pass:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode during development
|
||||
npm run test:watch
|
||||
|
||||
# Check test coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
#### Writing Tests
|
||||
|
||||
- Place tests in `src/components/__tests__/` or `src/hooks/__tests__/`
|
||||
- Use descriptive test names
|
||||
- Test both success and error cases
|
||||
- Example:
|
||||
```typescript
|
||||
describe('MyComponent', () => {
|
||||
it('should render correctly', () => {
|
||||
// Your test here
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
// Your test here
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
1. Check if the bug is already reported in [Issues](https://github.com/AeThex-LABS/aethex-studio/issues)
|
||||
2. If not, create a new issue with:
|
||||
- Clear description
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Screenshots (if applicable)
|
||||
3. Create a fix and submit a PR
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
1. **Discuss first** - Open an issue to discuss the feature before implementing
|
||||
2. Wait for approval from maintainers
|
||||
3. Implement the feature
|
||||
4. Add tests
|
||||
5. Update documentation
|
||||
6. Submit a PR
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
- Fix typos
|
||||
- Improve clarity
|
||||
- Add examples
|
||||
- Update outdated information
|
||||
|
||||
### 🎨 UI/UX Improvements
|
||||
|
||||
- Follow the existing design system
|
||||
- Ensure responsive design (mobile + desktop)
|
||||
- Test accessibility
|
||||
- Add screenshots to PR
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Submitting
|
||||
|
||||
- [ ] Code builds without errors (`npm run build`)
|
||||
- [ ] All tests pass (`npm test`)
|
||||
- [ ] Linting passes (`npm run lint`)
|
||||
- [ ] Code is well-commented
|
||||
- [ ] Documentation is updated
|
||||
- [ ] Commit messages are clear and descriptive
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Use clear, descriptive commit messages:
|
||||
|
||||
```bash
|
||||
# Good
|
||||
git commit -m "Add drag-and-drop support for file tree"
|
||||
git commit -m "Fix search highlighting bug in SearchPanel"
|
||||
|
||||
# Bad
|
||||
git commit -m "Update stuff"
|
||||
git commit -m "Fix bug"
|
||||
```
|
||||
|
||||
### Submitting
|
||||
|
||||
1. Push your branch to your fork
|
||||
2. Open a PR against the `main` branch
|
||||
3. Fill out the PR template
|
||||
4. Wait for review
|
||||
|
||||
### PR Template
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
Brief description of what this PR does
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Documentation update
|
||||
- [ ] Performance improvement
|
||||
- [ ] Code refactoring
|
||||
|
||||
## Testing
|
||||
Describe how you tested this change
|
||||
|
||||
## Screenshots (if applicable)
|
||||
Add screenshots here
|
||||
|
||||
## Checklist
|
||||
- [ ] Tests pass
|
||||
- [ ] Linting passes
|
||||
- [ ] Documentation updated
|
||||
- [ ] Responsive on mobile
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # React components
|
||||
│ ├── ui/ # Reusable UI components
|
||||
│ ├── __tests__/ # Component tests
|
||||
│ └── ComponentName.tsx
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── __tests__/ # Hook tests
|
||||
│ └── use-hook-name.ts
|
||||
├── lib/ # Utilities and helpers
|
||||
│ └── helpers.ts
|
||||
└── App.tsx # Main application
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Components**: PascalCase (`FileTree.tsx`, `AIChat.tsx`)
|
||||
- **Hooks**: camelCase with `use` prefix (`use-theme.ts`, `use-keyboard-shortcuts.ts`)
|
||||
- **Utilities**: camelCase (`helpers.ts`, `validators.ts`)
|
||||
- **Tests**: Match component name with `.test.tsx` or `.test.ts`
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New Template
|
||||
|
||||
1. Edit `src/lib/templates.ts`
|
||||
2. Add your template to the `templates` array:
|
||||
```typescript
|
||||
{
|
||||
id: 'your-template-id',
|
||||
name: 'Template Name',
|
||||
description: 'Brief description',
|
||||
category: 'beginner' | 'gameplay' | 'ui' | 'tools' | 'advanced',
|
||||
code: `-- Your Lua code here`,
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Component
|
||||
|
||||
1. Create the component file in `src/components/`
|
||||
2. Write the component with TypeScript types
|
||||
3. Add tests in `src/components/__tests__/`
|
||||
4. Export from the component file
|
||||
5. Import and use in `App.tsx` or parent component
|
||||
|
||||
### Adding a Keyboard Shortcut
|
||||
|
||||
1. Edit `src/App.tsx`
|
||||
2. Add to the `useKeyboardShortcuts` array:
|
||||
```typescript
|
||||
{
|
||||
key: 'x',
|
||||
meta: true,
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
// Your action here
|
||||
},
|
||||
description: 'Description of shortcut',
|
||||
}
|
||||
```
|
||||
3. Update README.md keyboard shortcuts table
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
- Use React.lazy() for code splitting
|
||||
- Memoize expensive computations with useMemo()
|
||||
- Use useCallback() for function props
|
||||
- Optimize re-renders with React.memo()
|
||||
- Lazy load images and heavy components
|
||||
|
||||
## Accessibility Guidelines
|
||||
|
||||
- Add proper ARIA labels
|
||||
- Ensure keyboard navigation works
|
||||
- Test with screen readers
|
||||
- Maintain good color contrast
|
||||
- Add focus indicators
|
||||
|
||||
## Questions?
|
||||
|
||||
- Open an issue for questions
|
||||
- Join our community discussions
|
||||
- Check existing issues and PRs
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
- Be respectful and inclusive
|
||||
- Welcome newcomers
|
||||
- Give constructive feedback
|
||||
- Focus on the code, not the person
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to AeThex Studio! 🎉
|
||||
270
README.md
270
README.md
|
|
@ -1,23 +1,261 @@
|
|||
# ✨ Welcome to Your Spark Template!
|
||||
You've just launched your brand-new Spark Template Codespace — everything’s fired up and ready for you to explore, build, and create with Spark!
|
||||
# AeThex Studio
|
||||
|
||||
This template is your blank canvas. It comes with a minimal setup to help you get started quickly with Spark development.
|
||||
A powerful, browser-based IDE specifically designed for Roblox Lua development with AI assistance, modern tooling, and an intuitive interface.
|
||||
|
||||
🚀 What's Inside?
|
||||
- A clean, minimal Spark environment
|
||||
- Pre-configured for local development
|
||||
- Ready to scale with your ideas
|
||||
|
||||
🧠 What Can You Do?
|
||||
   
|
||||
|
||||
Right now, this is just a starting point — the perfect place to begin building and testing your Spark applications.
|
||||
## ✨ Features
|
||||
|
||||
🧹 Just Exploring?
|
||||
No problem! If you were just checking things out and don’t need to keep this code:
|
||||
### 🎨 **Modern Code Editor**
|
||||
- **Monaco Editor** - The same editor that powers VS Code
|
||||
- **Lua syntax highlighting** with autocomplete
|
||||
- **Real-time code validation** and linting
|
||||
- **Multi-file editing** with tab management
|
||||
- **File tree navigation** with drag-and-drop organization
|
||||
|
||||
- Simply delete your Spark.
|
||||
- Everything will be cleaned up — no traces left behind.
|
||||
### 🤖 **AI-Powered Assistant**
|
||||
- Built-in AI chat for code help and debugging
|
||||
- Context-aware suggestions
|
||||
- Code explanation and documentation
|
||||
- Roblox API knowledge
|
||||
|
||||
📄 License For Spark Template Resources
|
||||
### 📁 **Project Management**
|
||||
- **File Tree** - Organize your scripts into folders
|
||||
- **Drag-and-drop** - Rearrange files easily
|
||||
- **Quick file search** (Cmd/Ctrl+P) - Find files instantly
|
||||
- **Search in files** (Cmd/Ctrl+Shift+F) - Global text search
|
||||
|
||||
The Spark Template files and resources from GitHub are licensed under the terms of the MIT license, Copyright GitHub, Inc.
|
||||
### 🎯 **Productivity Features**
|
||||
- **25+ Code Templates** - Ready-made scripts for common tasks
|
||||
- Beginner templates (Hello World, Touch Detectors, etc.)
|
||||
- Gameplay systems (DataStore, Teams, Combat, etc.)
|
||||
- UI components (GUIs, Timers, etc.)
|
||||
- Advanced features (Pathfinding, Inventory, etc.)
|
||||
- **Command Palette** (Cmd/Ctrl+K) - Quick access to all commands
|
||||
- **Keyboard Shortcuts** - Professional IDE shortcuts
|
||||
- **Code Preview** - Test your scripts instantly
|
||||
|
||||
### 🎨 **Customization**
|
||||
- **5 Beautiful Themes**:
|
||||
- **Dark** - Classic dark theme for comfortable coding
|
||||
- **Light** - Clean light theme for bright environments
|
||||
- **Synthwave** - Retro neon aesthetic
|
||||
- **Forest** - Calming green tones
|
||||
- **Ocean** - Deep blue theme
|
||||
- **Persistent preferences** - Your settings are saved
|
||||
|
||||
### 📱 **Mobile Responsive**
|
||||
- Optimized layouts for phones and tablets
|
||||
- Touch-friendly controls
|
||||
- Hamburger menu for mobile
|
||||
- Collapsible panels
|
||||
|
||||
### 🚀 **Developer Experience**
|
||||
- **Code splitting** for fast loading
|
||||
- **Error boundaries** with graceful error handling
|
||||
- **Loading states** with spinners
|
||||
- **Toast notifications** for user feedback
|
||||
- **Testing infrastructure** with Vitest
|
||||
|
||||
## 🎮 Perfect For
|
||||
|
||||
- **Roblox Developers** - Build game scripts faster
|
||||
- **Students & Learners** - Learn Lua with templates and AI help
|
||||
- **Rapid Prototyping** - Quickly test game ideas
|
||||
- **Web-Based Development** - Code anywhere, no installation needed
|
||||
|
||||
## ⌨️ Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Cmd/Ctrl + S` | Save file (auto-save enabled) |
|
||||
| `Cmd/Ctrl + P` | Quick file search |
|
||||
| `Cmd/Ctrl + K` | Command palette |
|
||||
| `Cmd/Ctrl + N` | New project |
|
||||
| `Cmd/Ctrl + F` | Find in editor |
|
||||
| `Cmd/Ctrl + Shift + F` | Search in all files |
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/AeThex-LABS/aethex-studio.git
|
||||
|
||||
# Navigate to the project directory
|
||||
cd aethex-studio
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start the development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see the application.
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Build the application
|
||||
npm run build
|
||||
|
||||
# Start the production server
|
||||
npm start
|
||||
```
|
||||
|
||||
## 📖 Usage Guide
|
||||
|
||||
### Creating Your First Script
|
||||
|
||||
1. Click **"New File"** in the file tree
|
||||
2. Choose a template or start from scratch
|
||||
3. Write your Lua code in the Monaco editor
|
||||
4. Click **"Preview"** to test
|
||||
5. **Copy** or **Export** your script
|
||||
|
||||
### Using Templates
|
||||
|
||||
1. Click the **Templates** button in the toolbar
|
||||
2. Browse categories: Beginner, Gameplay, UI, Tools, Advanced
|
||||
3. Click a template to load it into your editor
|
||||
4. Customize the code for your needs
|
||||
|
||||
### AI Assistant
|
||||
|
||||
1. Open the **AI Chat** panel (right side on desktop)
|
||||
2. Ask questions about:
|
||||
- Roblox scripting
|
||||
- Code debugging
|
||||
- API usage
|
||||
- Best practices
|
||||
3. Get instant, context-aware answers
|
||||
|
||||
### Organizing Files
|
||||
|
||||
- **Create folders** - Right-click in file tree
|
||||
- **Drag and drop** - Move files between folders
|
||||
- **Rename** - Click the menu (⋯) next to a file
|
||||
- **Delete** - Use the menu to remove files
|
||||
|
||||
### Searching
|
||||
|
||||
- **Quick search** - `Cmd/Ctrl+P` to find files by name
|
||||
- **Global search** - `Cmd/Ctrl+Shift+F` to search text across all files
|
||||
- **In-editor search** - `Cmd/Ctrl+F` to find text in current file
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Next.js 14** - React framework
|
||||
- **React 18** - UI library
|
||||
- **TypeScript** - Type safety
|
||||
- **Monaco Editor** - Code editor
|
||||
- **Tailwind CSS** - Styling
|
||||
- **Radix UI** - Component primitives
|
||||
- **Phosphor Icons** - Icon library
|
||||
- **Vitest** - Testing framework
|
||||
- **PostHog** - Analytics (optional)
|
||||
- **Sentry** - Error tracking (optional)
|
||||
|
||||
## 🧪 Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run tests with UI
|
||||
npm run test:ui
|
||||
|
||||
# Generate coverage report
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
```
|
||||
aethex-studio/
|
||||
├── src/
|
||||
│ ├── components/ # React components
|
||||
│ │ ├── ui/ # Reusable UI components
|
||||
│ │ ├── CodeEditor.tsx
|
||||
│ │ ├── FileTree.tsx
|
||||
│ │ ├── AIChat.tsx
|
||||
│ │ └── ...
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── lib/ # Utility functions
|
||||
│ │ ├── templates.ts # Code templates
|
||||
│ │ └── ...
|
||||
│ └── App.tsx # Main application
|
||||
├── app/ # Next.js app directory
|
||||
│ └── globals.css # Global styles
|
||||
├── public/ # Static assets
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📝 Code Templates
|
||||
|
||||
AeThex Studio includes 25+ production-ready templates:
|
||||
|
||||
**Beginner:**
|
||||
- Hello World, Player Join Handler, Part Touch Detector, etc.
|
||||
|
||||
**Gameplay:**
|
||||
- DataStore System, Teleport Part, Team System, Combat System, etc.
|
||||
|
||||
**UI:**
|
||||
- GUI Buttons, Proximity Prompts, Countdown Timers, etc.
|
||||
|
||||
**Tools:**
|
||||
- Give Tool, Sound Manager, Admin Commands, Chat Commands, etc.
|
||||
|
||||
**Advanced:**
|
||||
- Round System, Inventory System, Pathfinding NPC, Shop System, etc.
|
||||
|
||||
## 🐛 Bug Reports
|
||||
|
||||
Found a bug? Please open an issue on GitHub with:
|
||||
- Description of the bug
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Screenshots (if applicable)
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **Monaco Editor** - For the powerful code editor
|
||||
- **Roblox** - For the game platform
|
||||
- **Radix UI** - For accessible component primitives
|
||||
- **Vercel** - For Next.js framework
|
||||
|
||||
## 📧 Contact
|
||||
|
||||
- **Website**: [aethex.com](https://aethex.com)
|
||||
- **GitHub**: [@AeThex-LABS](https://github.com/AeThex-LABS)
|
||||
- **Issues**: [GitHub Issues](https://github.com/AeThex-LABS/aethex-studio/issues)
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ by the AeThex team**
|
||||
|
||||
Happy coding! 🎮✨
|
||||
|
|
|
|||
323
src/App.tsx
323
src/App.tsx
|
|
@ -42,13 +42,23 @@ function App() {
|
|||
const [showPassportLogin, setShowPassportLogin] = useState(false);
|
||||
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
|
||||
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
|
||||
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
|
||||
return stored ? JSON.parse(stored) : 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(() => {
|
||||
initPostHog();
|
||||
initSentry();
|
||||
try {
|
||||
initPostHog();
|
||||
initSentry();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize analytics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcuts
|
||||
|
|
@ -118,14 +128,28 @@ function App() {
|
|||
]);
|
||||
|
||||
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
||||
setUser(user);
|
||||
localStorage.setItem('aethex-user', JSON.stringify(user));
|
||||
captureEvent('login', { user });
|
||||
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 = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('aethex-user');
|
||||
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[]>([
|
||||
|
|
@ -215,121 +239,158 @@ end)`,
|
|||
};
|
||||
|
||||
const handleFileCreate = (name: string, parentId?: string) => {
|
||||
const newFile: FileNode = {
|
||||
id: `file-${Date.now()}`,
|
||||
name: name.endsWith('.lua') ? name : `${name}.lua`,
|
||||
type: 'file',
|
||||
content: '-- New file\n',
|
||||
};
|
||||
try {
|
||||
if (!name || name.trim() === '') {
|
||||
toast.error('File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
const newFile: FileNode = {
|
||||
id: `file-${Date.now()}`,
|
||||
name: name.endsWith('.lua') ? name : `${name}.lua`,
|
||||
type: 'file',
|
||||
content: '-- New file\n',
|
||||
};
|
||||
return addToFolder(prev || []);
|
||||
});
|
||||
|
||||
captureEvent('file_create', { name, parentId });
|
||||
toast.success(`Created ${newFile.name}`);
|
||||
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) => {
|
||||
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 || []);
|
||||
});
|
||||
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) => {
|
||||
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 || []);
|
||||
});
|
||||
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 || '');
|
||||
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.');
|
||||
}
|
||||
captureEvent('file_delete', { id });
|
||||
};
|
||||
|
||||
const handleFileMove = (fileId: string, targetParentId: string) => {
|
||||
setFiles((prev) => {
|
||||
let movedNode: FileNode | null = null;
|
||||
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;
|
||||
});
|
||||
};
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
// Then, add the node to the target folder
|
||||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === targetParentId && node.type === 'folder') {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), movedNode!],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addToTarget(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const withoutMoved = removeNode(prev || []);
|
||||
if (movedNode) {
|
||||
return addToTarget(withoutMoved);
|
||||
}
|
||||
return prev || [];
|
||||
});
|
||||
const withoutMoved = removeNode(prev || []);
|
||||
if (movedNode) {
|
||||
return addToTarget(withoutMoved);
|
||||
}
|
||||
return prev || [];
|
||||
});
|
||||
|
||||
captureEvent('file_move', { fileId, targetParentId });
|
||||
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) => {
|
||||
|
|
@ -341,25 +402,39 @@ end)`,
|
|||
};
|
||||
|
||||
const handleCreateProject = (config: ProjectConfig) => {
|
||||
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!")`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
try {
|
||||
if (!config.name || config.name.trim() === '') {
|
||||
toast.error('Project name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(projectFiles);
|
||||
setOpenFiles([]);
|
||||
setActiveFileId('');
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +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';
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
|
|
@ -53,8 +54,10 @@ export function AIChat({ currentCode }: AIChatProps) {
|
|||
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI chat error:', error);
|
||||
captureError(error as Error, { context: 'ai_chat', userMessage, codeLength: currentCode.length });
|
||||
toast.error('Failed to get AI response. Please try again.');
|
||||
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error. Please try asking again.' }]);
|
||||
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error. Please try asking again or check your connection.' }]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import Editor from '@monaco-editor/react';
|
||||
import { useKV } from '@github/spark/hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { LoadingSpinner } from './ui/loading-spinner';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CodeEditorProps {
|
||||
onCodeChange?: (code: string) => void;
|
||||
|
|
@ -27,13 +29,31 @@ end)
|
|||
`);
|
||||
|
||||
useEffect(() => {
|
||||
if (onCodeChange && code) {
|
||||
onCodeChange(code);
|
||||
try {
|
||||
if (onCodeChange && code) {
|
||||
onCodeChange(code);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update code:', error);
|
||||
}
|
||||
}, [code, onCodeChange]);
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
setCode(value || '');
|
||||
try {
|
||||
setCode(value || '');
|
||||
} catch (error) {
|
||||
console.error('Failed to save code:', error);
|
||||
toast.error('Failed to save changes. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorMount = () => {
|
||||
console.log('Monaco editor mounted successfully');
|
||||
};
|
||||
|
||||
const handleEditorError = (error: Error) => {
|
||||
console.error('Monaco editor error:', error);
|
||||
toast.error('Editor failed to load. Please refresh the page.');
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -45,6 +65,12 @@ end)
|
|||
theme="vs-dark"
|
||||
value={code}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorMount}
|
||||
loading={
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
}
|
||||
options={{
|
||||
minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
|
||||
fontSize: 13,
|
||||
|
|
|
|||
Loading…
Reference in a new issue