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! 🎉
|
||||||
268
README.md
268
README.md
|
|
@ -1,23 +1,261 @@
|
||||||
# ✨ Welcome to Your Spark Template!
|
# AeThex Studio
|
||||||
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!
|
|
||||||
|
|
||||||
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?
|
## ✨ Features
|
||||||
|
|
||||||
Right now, this is just a starting point — the perfect place to begin building and testing your Spark applications.
|
### 🎨 **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
|
||||||
|
|
||||||
🧹 Just Exploring?
|
### 🤖 **AI-Powered Assistant**
|
||||||
No problem! If you were just checking things out and don’t need to keep this code:
|
- Built-in AI chat for code help and debugging
|
||||||
|
- Context-aware suggestions
|
||||||
|
- Code explanation and documentation
|
||||||
|
- Roblox API knowledge
|
||||||
|
|
||||||
- Simply delete your Spark.
|
### 📁 **Project Management**
|
||||||
- Everything will be cleaned up — no traces left behind.
|
- **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
|
||||||
|
|
||||||
📄 License For Spark Template Resources
|
### 🎯 **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
|
||||||
|
|
||||||
The Spark Template files and resources from GitHub are licensed under the terms of the MIT license, Copyright GitHub, Inc.
|
### 🎨 **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 [showPassportLogin, setShowPassportLogin] = useState(false);
|
||||||
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
|
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
|
||||||
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
|
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
|
||||||
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
|
try {
|
||||||
return stored ? JSON.parse(stored) : null;
|
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(() => {
|
React.useEffect(() => {
|
||||||
initPostHog();
|
try {
|
||||||
initSentry();
|
initPostHog();
|
||||||
|
initSentry();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize analytics:', error);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
|
|
@ -118,14 +128,28 @@ function App() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
||||||
setUser(user);
|
try {
|
||||||
localStorage.setItem('aethex-user', JSON.stringify(user));
|
setUser(user);
|
||||||
captureEvent('login', { 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 = () => {
|
const handleSignOut = () => {
|
||||||
setUser(null);
|
try {
|
||||||
localStorage.removeItem('aethex-user');
|
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[]>([
|
const [files, setFiles] = useState<FileNode[]>([
|
||||||
|
|
@ -215,121 +239,158 @@ end)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileCreate = (name: string, parentId?: string) => {
|
const handleFileCreate = (name: string, parentId?: string) => {
|
||||||
const newFile: FileNode = {
|
try {
|
||||||
id: `file-${Date.now()}`,
|
if (!name || name.trim() === '') {
|
||||||
name: name.endsWith('.lua') ? name : `${name}.lua`,
|
toast.error('File name cannot be empty');
|
||||||
type: 'file',
|
return;
|
||||||
content: '-- New file\n',
|
}
|
||||||
};
|
|
||||||
|
|
||||||
setFiles((prev) => {
|
const newFile: FileNode = {
|
||||||
const addToFolder = (nodes: FileNode[]): FileNode[] => {
|
id: `file-${Date.now()}`,
|
||||||
return nodes.map((node) => {
|
name: name.endsWith('.lua') ? name : `${name}.lua`,
|
||||||
if (node.id === 'root' && !parentId) {
|
type: 'file',
|
||||||
return {
|
content: '-- New file\n',
|
||||||
...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 });
|
setFiles((prev) => {
|
||||||
toast.success(`Created ${newFile.name}`);
|
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) => {
|
const handleFileRename = (id: string, newName: string) => {
|
||||||
setFiles((prev) => {
|
try {
|
||||||
const rename = (nodes: FileNode[]): FileNode[] => {
|
if (!newName || newName.trim() === '') {
|
||||||
return nodes.map((node) => {
|
toast.error('File name cannot be empty');
|
||||||
if (node.id === id) {
|
return;
|
||||||
return { ...node, name: newName };
|
}
|
||||||
}
|
|
||||||
if (node.children) {
|
setFiles((prev) => {
|
||||||
return { ...node, children: rename(node.children) };
|
const rename = (nodes: FileNode[]): FileNode[] => {
|
||||||
}
|
return nodes.map((node) => {
|
||||||
return node;
|
if (node.id === id) {
|
||||||
});
|
return { ...node, name: newName };
|
||||||
};
|
}
|
||||||
return rename(prev || []);
|
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) => {
|
const handleFileDelete = (id: string) => {
|
||||||
setFiles((prev) => {
|
try {
|
||||||
const deleteNode = (nodes: FileNode[]): FileNode[] => {
|
setFiles((prev) => {
|
||||||
return nodes.filter((node) => {
|
const deleteNode = (nodes: FileNode[]): FileNode[] => {
|
||||||
if (node.id === id) return false;
|
return nodes.filter((node) => {
|
||||||
if (node.children) {
|
if (node.id === id) return false;
|
||||||
node.children = deleteNode(node.children);
|
if (node.children) {
|
||||||
}
|
node.children = deleteNode(node.children);
|
||||||
return true;
|
}
|
||||||
});
|
return true;
|
||||||
};
|
});
|
||||||
return deleteNode(prev || []);
|
};
|
||||||
});
|
return deleteNode(prev || []);
|
||||||
|
});
|
||||||
|
|
||||||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||||||
if (activeFileId === id) {
|
if (activeFileId === id) {
|
||||||
setActiveFileId((openFiles || [])[0]?.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) => {
|
const handleFileMove = (fileId: string, targetParentId: string) => {
|
||||||
setFiles((prev) => {
|
try {
|
||||||
let movedNode: FileNode | null = null;
|
setFiles((prev) => {
|
||||||
|
let movedNode: FileNode | null = null;
|
||||||
|
|
||||||
// First, find and remove the node from its current location
|
// First, find and remove the node from its current location
|
||||||
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
||||||
return nodes.filter((node) => {
|
return nodes.filter((node) => {
|
||||||
if (node.id === fileId) {
|
if (node.id === fileId) {
|
||||||
movedNode = node;
|
movedNode = node;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
node.children = removeNode(node.children);
|
node.children = removeNode(node.children);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Then, add the node to the target folder
|
// Then, add the node to the target folder
|
||||||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (node.id === targetParentId && node.type === 'folder') {
|
if (node.id === targetParentId && node.type === 'folder') {
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
children: [...(node.children || []), movedNode!],
|
children: [...(node.children || []), movedNode!],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
return { ...node, children: addToTarget(node.children) };
|
return { ...node, children: addToTarget(node.children) };
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const withoutMoved = removeNode(prev || []);
|
const withoutMoved = removeNode(prev || []);
|
||||||
if (movedNode) {
|
if (movedNode) {
|
||||||
return addToTarget(withoutMoved);
|
return addToTarget(withoutMoved);
|
||||||
}
|
}
|
||||||
return prev || [];
|
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) => {
|
const handleFileClose = (id: string) => {
|
||||||
|
|
@ -341,25 +402,39 @@ end)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateProject = (config: ProjectConfig) => {
|
const handleCreateProject = (config: ProjectConfig) => {
|
||||||
const projectFiles: FileNode[] = [
|
try {
|
||||||
{
|
if (!config.name || config.name.trim() === '') {
|
||||||
id: 'root',
|
toast.error('Project name cannot be empty');
|
||||||
name: config.name,
|
return;
|
||||||
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);
|
const projectFiles: FileNode[] = [
|
||||||
setOpenFiles([]);
|
{
|
||||||
setActiveFileId('');
|
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
|
// Example user stub for profile
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Sparkle, PaperPlaneRight } from '@phosphor-icons/react';
|
import { Sparkle, PaperPlaneRight } from '@phosphor-icons/react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { captureError } from '@/lib/sentry';
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
role: 'user' | 'assistant';
|
role: 'user' | 'assistant';
|
||||||
|
|
@ -53,8 +54,10 @@ export function AIChat({ currentCode }: AIChatProps) {
|
||||||
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
|
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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.');
|
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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { useKV } from '@github/spark/hooks';
|
import { useKV } from '@github/spark/hooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { LoadingSpinner } from './ui/loading-spinner';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
onCodeChange?: (code: string) => void;
|
onCodeChange?: (code: string) => void;
|
||||||
|
|
@ -27,13 +29,31 @@ end)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onCodeChange && code) {
|
try {
|
||||||
onCodeChange(code);
|
if (onCodeChange && code) {
|
||||||
|
onCodeChange(code);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update code:', error);
|
||||||
}
|
}
|
||||||
}, [code, onCodeChange]);
|
}, [code, onCodeChange]);
|
||||||
|
|
||||||
const handleEditorChange = (value: string | undefined) => {
|
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 (
|
return (
|
||||||
|
|
@ -45,6 +65,12 @@ end)
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
value={code}
|
value={code}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
|
onMount={handleEditorMount}
|
||||||
|
loading={
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
|
minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue