diff --git a/.gitignore b/.gitignore
index 37d2abfaf8..4bc03e175d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,4 +46,3 @@ docs/instructions/Roadmap.md
.cursorrules
*.md
.qodo
-CLAUDE.md
diff --git a/MULTIUSER_DOCUMENTATION.md b/MULTIUSER_DOCUMENTATION.md
deleted file mode 100644
index 82a07020b6..0000000000
--- a/MULTIUSER_DOCUMENTATION.md
+++ /dev/null
@@ -1,512 +0,0 @@
-# 🚀 bolt.diy Multi-User System Documentation
-
-**Developer: Keoma Wright**
-**Version: 1.0.0**
-**Date: 27 August 2025**
-
-## 📋 Table of Contents
-1. [Overview](#overview)
-2. [Architecture](#architecture)
-3. [Features](#features)
-4. [Installation & Setup](#installation--setup)
-5. [User Guide](#user-guide)
-6. [Admin Guide](#admin-guide)
-7. [Security](#security)
-8. [API Reference](#api-reference)
-9. [Technical Details](#technical-details)
-10. [Troubleshooting](#troubleshooting)
-
-## Overview
-
-The bolt.diy Multi-User System transforms the single-user bolt.diy application into a comprehensive multi-user platform with isolated workspaces, personalized settings, and robust user management - all without requiring a traditional database.
-
-### Key Highlights
-- ✅ **No Database Required** - File-based storage system
-- ✅ **Isolated Workspaces** - Each user has their own chat history and projects
-- ✅ **Beautiful UI** - Stunning login/signup pages with glassmorphism design
-- ✅ **Avatar Support** - Users can upload custom avatars
-- ✅ **Admin Panel** - Comprehensive user management interface
-- ✅ **Security** - JWT authentication with bcrypt password hashing
-- ✅ **Personalized Experience** - Custom greetings and user preferences
-
-## Architecture
-
-### System Components
-
-```
-┌─────────────────────────────────────────────────┐
-│ Frontend │
-├─────────────────────────────────────────────────┤
-│ Authentication Pages │ Protected Routes │
-│ - Login/Signup │ - Chat Interface │
-│ - Avatar Upload │ - User Management │
-│ │ - Settings │
-├─────────────────────────────────────────────────┤
-│ Authentication Layer │
-│ - JWT Tokens │ - Session Management │
-│ - Auth Store │ - Protected HOCs │
-├─────────────────────────────────────────────────┤
-│ Storage Layer │
-│ File-Based Storage │ User-Specific DBs │
-│ - .users/ │ - IndexedDB per user │
-│ - Security logs │ - Isolated workspaces │
-└─────────────────────────────────────────────────┘
-```
-
-### File Structure
-
-```
-/root/bolt/
-├── .users/ # User data directory (secured)
-│ ├── users.json # User registry
-│ ├── security.log # Security audit logs
-│ └── data/ # User-specific data
-│ └── {userId}/ # Individual user directories
-├── app/
-│ ├── components/
-│ │ ├── auth/
-│ │ │ ├── LoginForm.tsx
-│ │ │ ├── SignupForm.tsx
-│ │ │ └── ProtectedRoute.tsx
-│ │ ├── chat/
-│ │ │ ├── AuthenticatedChat.tsx
-│ │ │ └── WelcomeMessage.tsx
-│ │ ├── header/
-│ │ │ └── UserMenu.tsx
-│ │ └── admin/
-│ │ └── UserManager.tsx
-│ ├── lib/
-│ │ ├── stores/
-│ │ │ └── auth.ts
-│ │ ├── utils/
-│ │ │ ├── crypto.ts
-│ │ │ └── fileUserStorage.ts
-│ │ └── persistence/
-│ │ └── userDb.ts
-│ └── routes/
-│ ├── auth.tsx
-│ ├── admin.users.tsx
-│ └── api.auth.*.ts
-```
-
-## Features
-
-### 🔐 Authentication System
-
-#### Login Page
-- Beautiful gradient animated background
-- Glassmorphism card design
-- Remember me functionality (7-day sessions)
-- Smooth tab transitions between login/signup
-- Real-time validation feedback
-
-#### Signup Page
-- Avatar upload with preview
-- Password strength indicator
-- First name for personalization
-- Username validation
-- Animated form transitions
-
-### 👤 User Management
-
-#### User Profile
-- Unique user ID generation
-- Avatar storage as base64
-- Preferences storage
-- Last login tracking
-- Creation date tracking
-
-#### Admin Panel
-- User grid with search
-- User statistics dashboard
-- Delete user with confirmation
-- Edit user capabilities
-- Activity monitoring
-
-### 💬 Personalized Chat Experience
-
-#### Welcome Message
-- Personalized greeting: "{First Name}, What would you like to build today?"
-- Time-based greetings (morning/afternoon/evening)
-- User statistics display
-- Example prompts
-
-#### Chat History Isolation
-- User-specific IndexedDB
-- Isolated chat sessions
-- Personal workspace files
-- Settings per user
-
-### 🎨 UI/UX Enhancements
-
-#### Design Elements
-- Glassmorphism effects
-- Animated gradients
-- Smooth transitions (Framer Motion)
-- Dark/light theme support
-- Responsive design
-
-#### User Menu
-- Avatar display
-- Quick access to settings
-- User management link
-- Sign out option
-- Member since date
-
-## Installation & Setup
-
-### Prerequisites
-```bash
-# Required packages
-pnpm add bcryptjs jsonwebtoken
-pnpm add -D @types/bcryptjs @types/jsonwebtoken
-```
-
-### Initial Setup
-
-1. **Create user directory**
-```bash
-mkdir -p .users
-chmod 700 .users
-```
-
-2. **Environment Variables**
-```env
-JWT_SECRET=your-secure-secret-key-here
-```
-
-3. **Start the application**
-```bash
-pnpm run dev
-```
-
-4. **Access the application**
-Navigate to `http://localhost:5173/auth` to create your first account.
-
-## User Guide
-
-### Getting Started
-
-1. **Create an Account**
- - Navigate to `/auth`
- - Click "Sign Up" tab
- - Upload an avatar (optional)
- - Enter your details
- - Create a strong password
-
-2. **Login**
- - Enter username and password
- - Check "Remember me" for persistent sessions
- - Click "Sign In"
-
-3. **Using the Chat**
- - Personalized greeting appears
- - Your chat history is private
- - Settings are saved per user
-
-4. **Managing Your Profile**
- - Click your avatar in the header
- - Access settings
- - View member information
-
-## Admin Guide
-
-### User Management
-
-1. **Access Admin Panel**
- - Click user menu → "Manage Users"
- - Or navigate to `/admin/users`
-
-2. **View Users**
- - See all registered users
- - View statistics
- - Search and filter
-
-3. **Delete Users**
- - Click trash icon
- - Confirm deletion
- - User data is permanently removed
-
-4. **Monitor Activity**
- - Check security logs
- - View last login times
- - Track user creation
-
-### Security Logs
-
-Security events are logged to `.users/security.log`:
-- Login attempts (successful/failed)
-- User creation
-- User deletion
-- Errors
-
-Example log entry:
-```json
-{
- "timestamp": "2024-12-27T10:30:45.123Z",
- "userId": "user_123456_abc",
- "username": "john_doe",
- "action": "login",
- "details": "Successful login",
- "ip": "192.168.1.1"
-}
-```
-
-## Security
-
-### Password Security
-- **Bcrypt hashing** with salt rounds
-- **Complexity requirements**:
- - Minimum 8 characters
- - At least one uppercase letter
- - At least one lowercase letter
- - At least one number
-
-### Session Management
-- **JWT tokens** with expiration
-- **7-day session** option
-- **Automatic logout** on expiration
-- **Secure cookie storage**
-
-### File Permissions
-- `.users/` directory: `700` (owner only)
-- User data files: JSON format
-- Security logs: Append-only
-
-### Best Practices
-- Never store plain passwords
-- Use environment variables for secrets
-- Regular security log reviews
-- Implement rate limiting (future)
-
-## API Reference
-
-### Authentication Endpoints
-
-#### POST `/api/auth/login`
-```typescript
-Request: {
- username: string;
- password: string;
-}
-
-Response: {
- success: boolean;
- user?: UserProfile;
- token?: string;
- error?: string;
-}
-```
-
-#### POST `/api/auth/signup`
-```typescript
-Request: {
- username: string;
- password: string;
- firstName: string;
- avatar?: string;
-}
-
-Response: {
- success: boolean;
- user?: UserProfile;
- token?: string;
- error?: string;
-}
-```
-
-#### POST `/api/auth/logout`
-```typescript
-Headers: {
- Authorization: "Bearer {token}"
-}
-
-Response: {
- success: boolean;
-}
-```
-
-#### POST `/api/auth/verify`
-```typescript
-Headers: {
- Authorization: "Bearer {token}"
-}
-
-Response: {
- success: boolean;
- user?: UserProfile;
-}
-```
-
-### User Management Endpoints
-
-#### GET `/api/users`
-Get all users (requires authentication)
-
-#### DELETE `/api/users/:id`
-Delete a specific user (requires authentication)
-
-## Technical Details
-
-### Storage System
-
-#### User Registry (`users.json`)
-```json
-{
- "users": [
- {
- "id": "user_123456_abc",
- "username": "john_doe",
- "firstName": "John",
- "passwordHash": "$2a$10$...",
- "avatar": "data:image/png;base64,...",
- "createdAt": "2024-12-27T10:00:00.000Z",
- "lastLogin": "2024-12-27T15:30:00.000Z",
- "preferences": {
- "theme": "dark",
- "deploySettings": {},
- "githubSettings": {},
- "workspaceConfig": {}
- }
- }
- ]
-}
-```
-
-#### User-Specific IndexedDB
-Each user has their own database: `boltHistory_{userId}`
-- Chats store
-- Snapshots store
-- Settings store
-- Workspaces store
-
-### Authentication Flow
-
-```mermaid
-sequenceDiagram
- User->>Frontend: Enter credentials
- Frontend->>API: POST /api/auth/login
- API->>FileStorage: Verify user
- API->>Crypto: Verify password
- API->>Crypto: Generate JWT
- API->>SecurityLog: Log attempt
- API->>Frontend: Return token + user
- Frontend->>AuthStore: Save state
- Frontend->>Cookie: Store token
- Frontend->>Chat: Redirect to chat
-```
-
-### Workspace Isolation
-
-Each user's workspace is completely isolated:
-1. **Chat History** - Stored in user-specific IndexedDB
-2. **Settings** - LocalStorage with user prefix
-3. **Files** - Virtual file system per user
-4. **Deploy Settings** - User-specific configurations
-
-## Troubleshooting
-
-### Common Issues
-
-#### Cannot Login
-- Verify username/password
-- Check security logs
-- Ensure `.users/` directory exists
-
-#### Session Expired
-- Re-login required
-- Use "Remember me" for longer sessions
-
-#### User Data Not Loading
-- Check browser IndexedDB
-- Verify user ID in auth store
-- Clear browser cache if needed
-
-#### Avatar Not Displaying
-- Check file size (max 2MB recommended)
-- Verify base64 encoding
-- Test with different image formats
-
-### Debug Mode
-
-Enable debug logging:
-```javascript
-// In browser console
-localStorage.setItem('DEBUG', 'true');
-```
-
-View security logs:
-```bash
-tail -f .users/security.log
-```
-
-### Recovery
-
-#### Reset User Password
-Currently requires manual intervention:
-1. Generate new hash using bcrypt
-2. Update users.json
-3. Restart application
-
-#### Restore Deleted User
-If backup exists:
-1. Restore from users.json backup
-2. Recreate user data directory
-3. Restore IndexedDB if available
-
-## Future Enhancements
-
-### Planned Features
-- [ ] Password reset via email
-- [ ] Two-factor authentication
-- [ ] User roles and permissions
-- [ ] Team workspaces
-- [ ] Usage analytics
-- [ ] Export/import user data
-- [ ] Social login integration
-- [ ] Rate limiting
-- [ ] Session management UI
-- [ ] Audit trail viewer
-
-### Performance Optimizations
-- [ ] Database indexing strategies
-- [ ] Lazy loading user data
-- [ ] Caching layer
-- [ ] CDN for avatars
-
-## Contributing
-
-This system was developed by **Keoma Wright** as an enhancement to the bolt.diy project.
-
-### Development Guidelines
-1. Maintain backward compatibility
-2. Follow existing code patterns
-3. Add tests for new features
-4. Update documentation
-5. Consider security implications
-
-### Testing
-```bash
-# Run tests
-pnpm test
-
-# Type checking
-pnpm typecheck
-
-# Linting
-pnpm lint
-```
-
-## License
-
-This multi-user system is an extension of the bolt.diy project and follows the same license terms.
-
-## Credits
-
-**Developer**: Keoma Wright
-**Project**: bolt.diy Multi-User Edition
-**Year**: 2025
-
----
-
-*This documentation provides a comprehensive guide to the bolt.diy Multi-User System. For questions or issues, please contact the developer or submit an issue to the repository.*
\ No newline at end of file
diff --git a/README.md b/README.md
index 07f6939394..1d7ae4004d 100644
--- a/README.md
+++ b/README.md
@@ -1,212 +1,493 @@
-# Bolt.gives - Enhanced AI Development Platform
-
-
-
-## 🚀 About This Fork
-
-**Bolt.gives** is an advanced fork of bolt.diy that takes AI-powered development to the next level. While maintaining compatibility with the original, we're developing in a different direction focused on enterprise features, enhanced deployment capabilities, and multi-user collaboration.
-
-### 🎯 Our Vision
-
-We believe in making AI development accessible to everyone while providing professional-grade features for teams and enterprises. Bolt.gives is evolving beyond a simple development tool into a comprehensive platform for collaborative AI-assisted development.
-
-## 🌟 Exclusive Features Not Available in bolt.diy
-
-Our fork includes numerous advanced features that were submitted as PRs to bolt.diy but were not integrated into the main application:
-
-### 🤖 **NEW: SmartAI - Detailed Conversational Coding (Bolt.gives Exclusive)**
-- **Real-time explanations** of what the AI is doing and why
-- **Educational feedback** that helps you learn as you code
-- **Step-by-step narration** of the coding process
-- **Best practices insights** shared during implementation
-- **Debugging explanations** showing how issues are identified and fixed
-- **Available for Claude Sonnet 4** with more models coming soon
-- Transform "Generating Response..." into an interactive coding companion!
-
-### ✨ **Comprehensive Save All System**
-- **One-click save** for all modified files
-- **Auto-save functionality** with customizable intervals
-- **Visual indicators** showing file modification status
-- **Keyboard shortcuts** (Ctrl/Cmd+S) for quick saving
-- **Smart file tracking** with modification timestamps
-- Never lose your work again!
-
-### 🚀 **Advanced Import Capabilities**
-- **Import existing projects** from local folders
-- **GitHub template integration** for quick starts
-- **Automatic dependency detection**
-- **File structure preservation**
-- **Support for complex project hierarchies**
-- Seamlessly migrate your existing projects
-
-### 🔐 **Multi-User Authentication System**
-- **User registration and login**
-- **Workspace isolation** for security
-- **Personalized settings** per user
-- **File-based secure storage**
-- **JWT authentication**
-- **Optional guest mode** for quick access
-- Perfect for teams and organizations
-
-### 📦 **Quick Deploy to Netlify**
-- **One-click Netlify deployment**
-- **Automatic build configuration**
-- **Environment variable management**
-- **Site preview functionality**
-- **Custom domain support**
-- Deploy your projects in seconds
-
-### 🤖 **Extended AI Model Support**
-- **Claude 4 models** (Opus, Sonnet, Haiku)
-- **Claude Sonnet 4 (SmartAI)** - Exclusive conversational coding variant
-- **Auto-detection of Ollama** when configured
-- **Enhanced provider management**
-- **Automatic provider enablement** based on environment
-- Access to the latest and most powerful AI models
-
-### 🎨 **Enhanced UI/UX Features**
-- **Project import from folders**
-- **GitHub template integration**
-- **Advanced model selector** with provider filtering
-- **Improved error handling** and user feedback
-- **Responsive design improvements**
-- **Theme customization options**
-- A more intuitive development experience
-
-### 🔧 **Developer-Focused Improvements**
-- **Better TypeScript support**
-- **Enhanced debugging tools**
-- **Improved console output**
-- **Advanced file management**
-- **Git integration enhancements**
-- **Terminal improvements**
-- Tools built by developers, for developers
-
-## 💰 Coming Soon: Hosted Instances
-
-We will be offering **Hosted Bolt.gives Instances** starting from **$5 per month** for a basic instance with the ability for donors to upgrade their resources. This will provide:
-- Dedicated cloud environment
-- No setup required
-- Automatic updates
-- Priority support
-- Custom domain options
-- Team collaboration features
-
-## 🚀 Latest Updates from bolt.diy
-
-We regularly merge the latest improvements from bolt.diy including:
-
-### Recent Merged Features:
-- **GitLab Integration** - Full GitLab support alongside GitHub
-- **Branch Cloning Support** - Clone specific branches from repositories
-- **Token Limit Fixes** - Improved handling of model token limits
-- **Documentation Updates** - Enhanced help and documentation
-- **UI Improvements** - Better header layout and help accessibility
-- **Bug Fixes** - Various stability and performance improvements
-
-## 📊 Feature Comparison
-
-| Feature | bolt.diy | Bolt.gives |
-|---------|----------|------------|
-| Open Source | ✅ | ✅ |
-| Free to Use | ✅ | ✅ |
-| Claude 3.5 Support | ✅ | ✅ |
-| Claude 4 Models | ❌ | ✅ |
-| SmartAI Conversational Coding | ❌ | ✅ |
-| Save All System | ❌ | ✅ |
-| Import from Folder | ❌ | ✅ |
-| Multi-User Auth | ❌ | ✅ |
-| Quick Deploy to Netlify | ❌ | ✅ |
-| Auto-detect Ollama | ❌ | ✅ |
-| Hosted Instances | ❌ | Coming Soon |
-
-## 📦 Installation
-
-Bolt.gives maintains full compatibility with bolt.diy's installation process while adding additional features:
-
-### Quick Installation
+# bolt.diy
+
+[](https://bolt.diy)
+
+Welcome to bolt.diy, the official open source version of Bolt.new, which allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, Groq, Cohere, Together, Perplexity, Moonshot (Kimi), Hyperbolic, GitHub Models, Amazon Bedrock, and OpenAI-like providers - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
+
+-----
+Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more official installation instructions and additional information.
-```bash
-# Clone the repository
-git clone https://github.com/embire2/bolt.gives.git
-cd bolt.gives
+-----
+Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos-tutorial-helpful-content/3243) has a bunch of incredible resources for running and deploying bolt.diy yourself!
-# Install dependencies
-pnpm install
+We have also launched an experimental agent called the "bolt.diy Expert" that can answer common questions about bolt.diy. Find it here on the [oTTomator Live Agent Studio](https://studio.ottomator.ai/).
-# Set up environment variables
-cp .env.example .env
+bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMedin) but has quickly grown into a massive community effort to build the BEST open source AI coding assistant!
-# Start the development server
-pnpm run dev
-```
+## Table of Contents
+
+- [Join the Community](#join-the-community)
+- [Recent Major Additions](#recent-major-additions)
+- [Features](#features)
+- [Setup](#setup)
+- [Quick Installation](#quick-installation)
+- [Manual Installation](#manual-installation)
+- [Configuring API Keys and Providers](#configuring-api-keys-and-providers)
+- [Setup Using Git (For Developers only)](#setup-using-git-for-developers-only)
+- [Available Scripts](#available-scripts)
+- [Contributing](#contributing)
+- [Roadmap](#roadmap)
+- [FAQ](#faq)
+
+## Join the community
+
+[Join the bolt.diy community here, in the oTTomator Think Tank!](https://thinktank.ottomator.ai)
+
+## Project management
+
+Bolt.diy is a community effort! Still, the core team of contributors aims at organizing the project in way that allows
+you to understand where the current areas of focus are.
+
+If you want to know what we are working on, what we are planning to work on, or if you want to contribute to the
+project, please check the [project management guide](./PROJECT.md) to get started easily.
+
+## Recent Major Additions
+
+### ✅ Completed Features
+- **19+ AI Provider Integrations** - OpenAI, Anthropic, Google, Groq, xAI, DeepSeek, Mistral, Cohere, Together, Perplexity, HuggingFace, Ollama, LM Studio, OpenRouter, Moonshot, Hyperbolic, GitHub Models, Amazon Bedrock, OpenAI-like
+- **Electron Desktop App** - Native desktop experience with full functionality
+- **Advanced Deployment Options** - Netlify, Vercel, and GitHub Pages deployment
+- **Supabase Integration** - Database management and query capabilities
+- **Data Visualization & Analysis** - Charts, graphs, and data analysis tools
+- **MCP (Model Context Protocol)** - Enhanced AI tool integration
+- **Search Functionality** - Codebase search and navigation
+- **File Locking System** - Prevents conflicts during AI code generation
+- **Diff View** - Visual representation of AI-made changes
+- **Git Integration** - Clone, import, and deployment capabilities
+- **Expo App Creation** - React Native development support
+- **Voice Prompting** - Audio input for prompts
+- **Bulk Chat Operations** - Delete multiple chats at once
+- **Project Snapshot Restoration** - Restore projects from snapshots on reload
+
+### 🔄 In Progress / Planned
+- **File Locking & Diff Improvements** - Enhanced conflict prevention
+- **Backend Agent Architecture** - Move from single model calls to agent-based system
+- **LLM Prompt Optimization** - Better performance for smaller models
+- **Project Planning Documentation** - LLM-generated project plans in markdown
+- **VSCode Integration** - Git-like confirmations and workflows
+- **Document Upload for Knowledge** - Reference materials and coding style guides
+- **Additional Provider Integrations** - Azure OpenAI, Vertex AI, Granite
+
+## Features
+
+- **AI-powered full-stack web development** for **NodeJS based applications** directly in your browser.
+- **Support for 19+ LLMs** with an extensible architecture to integrate additional models.
+- **Attach images to prompts** for better contextual understanding.
+- **Integrated terminal** to view output of LLM-run commands.
+- **Revert code to earlier versions** for easier debugging and quicker changes.
+- **Download projects as ZIP** for easy portability and sync to a folder on the host.
+- **Integration-ready Docker support** for a hassle-free setup.
+- **Deploy directly** to **Netlify**, **Vercel**, or **GitHub Pages**.
+- **Electron desktop app** for native desktop experience.
+- **Data visualization and analysis** with integrated charts and graphs.
+- **Git integration** with clone, import, and deployment capabilities.
+- **MCP (Model Context Protocol)** support for enhanced AI tool integration.
+- **Search functionality** to search through your codebase.
+- **File locking system** to prevent conflicts during AI code generation.
+- **Diff view** to see changes made by the AI.
+- **Supabase integration** for database management and queries.
+- **Expo app creation** for React Native development.
+
+## Setup
+
+If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time.
+
+Let's get you up and running with the stable version of Bolt.DIY!
+
+## Quick Installation
+
+[](https://github.com/stackblitz-labs/bolt.diy/releases/latest) ← Click here to go to the latest release version!
+
+- Download the binary for your platform (available for Windows, macOS, and Linux)
+- **Note**: For macOS, if you get the error "This app is damaged", run:
+ ```bash
+ xattr -cr /path/to/Bolt.app
+ ```
+
+## Manual installation
+
+
+### Option 1: Node.js
+
+Node.js is required to run the application.
+
+1. Visit the [Node.js Download Page](https://nodejs.org/en/download/)
+2. Download the "LTS" (Long Term Support) version for your operating system
+3. Run the installer, accepting the default settings
+4. Verify Node.js is properly installed:
+ - **For Windows Users**:
+ 1. Press `Windows + R`
+ 2. Type "sysdm.cpl" and press Enter
+ 3. Go to "Advanced" tab → "Environment Variables"
+ 4. Check if `Node.js` appears in the "Path" variable
+ - **For Mac/Linux Users**:
+ 1. Open Terminal
+ 2. Type this command:
+ ```bash
+ echo $PATH
+ ```
+ 3. Look for `/usr/local/bin` in the output
+
+## Running the Application
+
+You have two options for running Bolt.DIY: directly on your machine or using Docker.
+
+### Option 1: Direct Installation (Recommended for Beginners)
+
+1. **Install Package Manager (pnpm)**:
+
+ ```bash
+ npm install -g pnpm
+ ```
+
+2. **Install Project Dependencies**:
+
+ ```bash
+ pnpm install
+ ```
+
+3. **Start the Application**:
+
+ ```bash
+ pnpm run dev
+ ```
+
+### Option 2: Using Docker
+
+This option requires some familiarity with Docker but provides a more isolated environment.
+
+#### Additional Prerequisite
+
+- Install Docker: [Download Docker](https://www.docker.com/)
+
+#### Steps:
+
+1. **Build the Docker Image**:
+
+ ```bash
+ # Using npm script:
+ npm run dockerbuild
+
+ # OR using direct Docker command:
+ docker build . --target bolt-ai-development
+ ```
+
+2. **Run the Container**:
+ ```bash
+ docker compose --profile development up
+ ```
+
+### Option 3: Desktop Application (Electron)
+
+For users who prefer a native desktop experience, bolt.diy is also available as an Electron desktop application:
+
+1. **Download the Desktop App**:
+ - Visit the [latest release](https://github.com/stackblitz-labs/bolt.diy/releases/latest)
+ - Download the appropriate binary for your operating system
+ - For macOS: Extract and run the `.dmg` file
+ - For Windows: Run the `.exe` installer
+ - For Linux: Extract and run the AppImage or install the `.deb` package
+
+2. **Alternative**: Build from Source:
+ ```bash
+ # Install dependencies
+ pnpm install
+
+ # Build the Electron app
+ pnpm electron:build:dist # For all platforms
+ # OR platform-specific:
+ pnpm electron:build:mac # macOS
+ pnpm electron:build:win # Windows
+ pnpm electron:build:linux # Linux
+ ```
+
+The desktop app provides the same full functionality as the web version with additional native features.
+
+## Configuring API Keys and Providers
+
+Bolt.diy features a modern, intuitive settings interface for managing AI providers and API keys. The settings are organized into dedicated panels for easy navigation and configuration.
+
+### Accessing Provider Settings
+
+1. **Open Settings**: Click the settings icon (⚙️) in the sidebar to access the settings panel
+2. **Navigate to Providers**: Select the "Providers" tab from the settings menu
+3. **Choose Provider Type**: Switch between "Cloud Providers" and "Local Providers" tabs
+
+### Cloud Providers Configuration
+
+The Cloud Providers tab displays all cloud-based AI services in an organized card layout:
+
+#### Adding API Keys
+1. **Select Provider**: Browse the grid of available cloud providers (OpenAI, Anthropic, Google, etc.)
+2. **Toggle Provider**: Use the switch to enable/disable each provider
+3. **Set API Key**:
+ - Click the provider card to expand its configuration
+ - Click on the "API Key" field to enter edit mode
+ - Paste your API key and press Enter to save
+ - The interface shows real-time validation with green checkmarks for valid keys
+
+#### Advanced Features
+- **Bulk Toggle**: Use "Enable All Cloud" to toggle all cloud providers at once
+- **Visual Status**: Green checkmarks indicate properly configured providers
+- **Provider Icons**: Each provider has a distinctive icon for easy identification
+- **Descriptions**: Helpful descriptions explain each provider's capabilities
-### Docker Installation
+### Local Providers Configuration
+The Local Providers tab manages local AI installations and custom endpoints:
+
+#### Ollama Configuration
+1. **Enable Ollama**: Toggle the Ollama provider switch
+2. **Configure Endpoint**: Set the API endpoint (defaults to `http://127.0.0.1:11434`)
+3. **Model Management**:
+ - View all installed models with size and parameter information
+ - Update models to latest versions with one click
+ - Delete unused models
+ - Install new models by entering model names
+
+#### Other Local Providers
+- **LM Studio**: Configure custom base URLs for LM Studio endpoints
+- **OpenAI-like**: Connect to any OpenAI-compatible API endpoint
+- **Auto-detection**: The system automatically detects environment variables for base URLs
+
+### Environment Variables vs UI Configuration
+
+Bolt.diy supports both methods for maximum flexibility:
+
+#### Environment Variables (Recommended for Production)
+Set API keys and base URLs in your `.env.local` file:
```bash
-docker-compose up --build
+# API Keys
+OPENAI_API_KEY=your_openai_key_here
+ANTHROPIC_API_KEY=your_anthropic_key_here
+
+# Custom Base URLs
+OLLAMA_BASE_URL=http://127.0.0.1:11434
+LMSTUDIO_BASE_URL=http://127.0.0.1:1234
```
-### Environment Variables
+#### UI-Based Configuration
+- **Real-time Updates**: Changes take effect immediately
+- **Secure Storage**: API keys are stored securely in browser cookies
+- **Visual Feedback**: Clear indicators show configuration status
+- **Easy Management**: Edit, view, and manage keys through the interface
-Create a `.env` file with your API keys:
+### Provider-Specific Features
+
+#### OpenRouter
+- **Free Models Filter**: Toggle to show only free models when browsing
+- **Pricing Information**: View input/output costs for each model
+- **Model Search**: Fuzzy search through all available models
+
+#### Ollama
+- **Model Installer**: Built-in interface to install new models
+- **Progress Tracking**: Real-time download progress for model updates
+- **Model Details**: View model size, parameters, and quantization levels
+- **Auto-refresh**: Automatically detects newly installed models
+
+#### Search & Navigation
+- **Fuzzy Search**: Type-ahead search across all providers and models
+- **Keyboard Navigation**: Use arrow keys and Enter to navigate quickly
+- **Clear Search**: Press `Cmd+K` (Mac) or `Ctrl+K` (Windows/Linux) to clear search
-```env
-# Core AI Providers
-ANTHROPIC_API_KEY=your_anthropic_key
-OPENAI_API_KEY=your_openai_key
-GOOGLE_GENERATIVE_AI_API_KEY=your_google_key
+### Troubleshooting
-# Optional Providers
-GROQ_API_KEY=your_groq_key
-OLLAMA_API_BASE_URL=http://localhost:11434
-OPENROUTER_API_KEY=your_openrouter_key
+#### Common Issues
+- **API Key Not Recognized**: Ensure you're using the correct API key format for each provider
+- **Base URL Issues**: Verify the endpoint URL is correct and accessible
+- **Model Not Loading**: Check that the provider is enabled and properly configured
+- **Environment Variables Not Working**: Restart the application after adding new environment variables
-# Deployment (Bolt.gives Exclusive)
-NETLIFY_AUTH_TOKEN=your_netlify_token
-GITHUB_TOKEN=your_github_token
-```
+#### Status Indicators
+- 🟢 **Green Checkmark**: Provider properly configured and ready to use
+- 🔴 **Red X**: Configuration missing or invalid
+- 🟡 **Yellow Indicator**: Provider enabled but may need additional setup
+- 🔵 **Blue Pencil**: Click to edit configuration
+
+### Supported Providers Overview
+
+#### Cloud Providers
+- **OpenAI** - GPT-4, GPT-3.5, and other OpenAI models
+- **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, and other Claude models
+- **Google (Gemini)** - Gemini 1.5 Pro, Gemini 1.5 Flash, and other Gemini models
+- **Groq** - Fast inference with Llama, Mixtral, and other models
+- **xAI** - Grok models including Grok-2 and Grok-2 Vision
+- **DeepSeek** - DeepSeek Coder and other DeepSeek models
+- **Mistral** - Mixtral, Mistral 7B, and other Mistral models
+- **Cohere** - Command R, Command R+, and other Cohere models
+- **Together AI** - Various open-source models
+- **Perplexity** - Sonar models for search and reasoning
+- **HuggingFace** - Access to HuggingFace model hub
+- **OpenRouter** - Unified API for multiple model providers
+- **Moonshot (Kimi)** - Kimi AI models
+- **Hyperbolic** - High-performance model inference
+- **GitHub Models** - Models available through GitHub
+- **Amazon Bedrock** - AWS managed AI models
+
+#### Local Providers
+- **Ollama** - Run open-source models locally with advanced model management
+- **LM Studio** - Local model inference with LM Studio
+- **OpenAI-like** - Connect to any OpenAI-compatible API endpoint
+
+> **💡 Pro Tip**: Start with OpenAI or Anthropic for the best results, then explore other providers based on your specific needs and budget considerations.
+
+## Setup Using Git (For Developers only)
+
+This method is recommended for developers who want to:
+
+- Contribute to the project
+- Stay updated with the latest changes
+- Switch between different versions
+- Create custom modifications
+
+#### Prerequisites
+
+1. Install Git: [Download Git](https://git-scm.com/downloads)
+
+#### Initial Setup
+
+1. **Clone the Repository**:
+
+ ```bash
+ git clone -b stable https://github.com/stackblitz-labs/bolt.diy.git
+ ```
+
+2. **Navigate to Project Directory**:
+
+ ```bash
+ cd bolt.diy
+ ```
+
+3. **Install Dependencies**:
+
+ ```bash
+ pnpm install
+ ```
-## 🛠️ Development
+4. **Start the Development Server**:
+ ```bash
+ pnpm run dev
+ ```
-### Available Scripts
+5. **(OPTIONAL)** Switch to the Main Branch if you want to use pre-release/testbranch:
+ ```bash
+ git checkout main
+ pnpm install
+ pnpm run dev
+ ```
+ Hint: Be aware that this can have beta-features and more likely got bugs than the stable release
-- `pnpm run dev` - Start development server
-- `pnpm run build` - Build for production
-- `pnpm run start` - Start production server
-- `pnpm run lint` - Run linting
-- `pnpm run typecheck` - Check TypeScript types
+>**Open the WebUI to test (Default: http://localhost:5173)**
+> - Beginners:
+> - Try to use a sophisticated Provider/Model like Anthropic with Claude Sonnet 3.x Models to get best results
+> - Explanation: The System Prompt currently implemented in bolt.diy cant cover the best performance for all providers and models out there. So it works better with some models, then other, even if the models itself are perfect for >programming
+> - Future: Planned is a Plugin/Extentions-Library so there can be different System Prompts for different Models, which will help to get better results
-## 🤝 Contributing
+#### Staying Updated
-We welcome contributions! Our exclusive features are what make Bolt.gives special:
+To get the latest changes from the repository:
-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
+1. **Save Your Local Changes** (if any):
-## 📜 License
+ ```bash
+ git stash
+ ```
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+2. **Pull Latest Updates**:
-## 🙏 Acknowledgments
+ ```bash
+ git pull
+ ```
-- Original [bolt.diy](https://github.com/stackblitz-labs/bolt.diy) team for the foundation
-- All contributors who have submitted PRs and features
-- The open-source community for continuous support and feedback
+3. **Update Dependencies**:
-## 📞 Support
+ ```bash
+ pnpm install
+ ```
-- **GitHub Issues**: [Report bugs or request features](https://github.com/embire2/bolt.gives/issues)
-- **Community**: Join the discussion in our GitHub Discussions
-- **Email**: support@bolt.gives (coming soon)
+4. **Restore Your Local Changes** (if any):
+ ```bash
+ git stash pop
+ ```
+
+#### Troubleshooting Git Setup
+
+If you encounter issues:
+
+1. **Clean Installation**:
+
+ ```bash
+ # Remove node modules and lock files
+ rm -rf node_modules pnpm-lock.yaml
+
+ # Clear pnpm cache
+ pnpm store prune
+
+ # Reinstall dependencies
+ pnpm install
+ ```
+
+2. **Reset Local Changes**:
+ ```bash
+ # Discard all local changes
+ git reset --hard origin/main
+ ```
+
+Remember to always commit your local changes or stash them before pulling updates to avoid conflicts.
+
+---
+
+## Available Scripts
+
+- **`pnpm run dev`**: Starts the development server.
+- **`pnpm run build`**: Builds the project.
+- **`pnpm run start`**: Runs the built application locally using Wrangler Pages.
+- **`pnpm run preview`**: Builds and runs the production build locally.
+- **`pnpm test`**: Runs the test suite using Vitest.
+- **`pnpm run typecheck`**: Runs TypeScript type checking.
+- **`pnpm run typegen`**: Generates TypeScript types using Wrangler.
+- **`pnpm run deploy`**: Deploys the project to Cloudflare Pages.
+- **`pnpm run lint`**: Runs ESLint to check for code issues.
+- **`pnpm run lint:fix`**: Automatically fixes linting issues.
+- **`pnpm run clean`**: Cleans build artifacts and cache.
+- **`pnpm run prepare`**: Sets up husky for git hooks.
+- **Docker Scripts**:
+ - **`pnpm run dockerbuild`**: Builds the Docker image for development.
+ - **`pnpm run dockerbuild:prod`**: Builds the Docker image for production.
+ - **`pnpm run dockerrun`**: Runs the Docker container.
+ - **`pnpm run dockerstart`**: Starts the Docker container with proper bindings.
+- **Electron Scripts**:
+ - **`pnpm electron:build:deps`**: Builds Electron main and preload scripts.
+ - **`pnpm electron:build:main`**: Builds the Electron main process.
+ - **`pnpm electron:build:preload`**: Builds the Electron preload script.
+ - **`pnpm electron:build:renderer`**: Builds the Electron renderer.
+ - **`pnpm electron:build:unpack`**: Creates an unpacked Electron build.
+ - **`pnpm electron:build:mac`**: Builds for macOS.
+ - **`pnpm electron:build:win`**: Builds for Windows.
+ - **`pnpm electron:build:linux`**: Builds for Linux.
+ - **`pnpm electron:build:dist`**: Builds for all platforms.
---
-
- Built with ❤️ by the Bolt.gives Community
-
- Taking AI Development to the Next Level
-
\ No newline at end of file
+## Contributing
+
+We welcome contributions! Check out our [Contributing Guide](CONTRIBUTING.md) to get started.
+
+---
+
+## Roadmap
+
+Explore upcoming features and priorities on our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo).
+
+---
+
+## FAQ
+
+For answers to common questions, issues, and to see a list of recommended models, visit our [FAQ Page](FAQ.md).
+
+
+# Licensing
+**Who needs a commercial WebContainer API license?**
+
+bolt.diy source code is distributed as MIT, but it uses WebContainers API that [requires licensing](https://webcontainers.io/enterprise) for production usage in a commercial, for-profit setting. (Prototypes or POCs do not require a commercial license.) If you're using the API to meet the needs of your customers, prospective customers, and/or employees, you need a license to ensure compliance with our Terms of Service. Usage of the API in violation of these terms may result in your access being revoked.
+# Test commit to trigger Security Analysis workflow
diff --git a/app/components/@settings/tabs/connections/netlify/NetlifyConnection.tsx b/app/components/@settings/tabs/connections/netlify/NetlifyConnection.tsx
index ccba7dac1e..7a0f2388ce 100644
--- a/app/components/@settings/tabs/connections/netlify/NetlifyConnection.tsx
+++ b/app/components/@settings/tabs/connections/netlify/NetlifyConnection.tsx
@@ -3,8 +3,7 @@ import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { useStore } from '@nanostores/react';
import { netlifyConnection, updateNetlifyConnection, initializeNetlifyConnection } from '~/lib/stores/netlify';
-import type { NetlifySite, NetlifyDeploy, NetlifyBuild } from '~/types/netlify';
-import { NetlifyQuickConnect } from './NetlifyQuickConnect';
+import type { NetlifySite, NetlifyDeploy, NetlifyBuild, NetlifyUser } from '~/types/netlify';
import {
CloudIcon,
BuildingLibraryIcon,
@@ -43,16 +42,29 @@ interface SiteAction {
}
export default function NetlifyConnection() {
+ console.log('NetlifyConnection component mounted');
+
const connection = useStore(netlifyConnection);
+ const [tokenInput, setTokenInput] = useState('');
const [fetchingStats, setFetchingStats] = useState(false);
const [sites, setSites] = useState([]);
const [deploys, setDeploys] = useState([]);
const [builds, setBuilds] = useState([]);
+
+ console.log('NetlifyConnection initial state:', {
+ connection: {
+ user: connection.user,
+ token: connection.token ? '[TOKEN_EXISTS]' : '[NO_TOKEN]',
+ },
+ envToken: import.meta.env?.VITE_NETLIFY_ACCESS_TOKEN ? '[ENV_TOKEN_EXISTS]' : '[NO_ENV_TOKEN]',
+ });
+
const [deploymentCount, setDeploymentCount] = useState(0);
const [lastUpdated, setLastUpdated] = useState('');
const [isStatsOpen, setIsStatsOpen] = useState(false);
const [activeSiteIndex, setActiveSiteIndex] = useState(0);
const [isActionLoading, setIsActionLoading] = useState(false);
+ const [isConnecting, setIsConnecting] = useState(false);
// Add site actions
const siteActions: SiteAction[] = [
@@ -139,6 +151,8 @@ export default function NetlifyConnection() {
};
useEffect(() => {
+ console.log('Netlify: Running initialization useEffect');
+
// Initialize connection with environment token if available
initializeNetlifyConnection();
}, []);
@@ -159,6 +173,46 @@ export default function NetlifyConnection() {
}
}, [connection]);
+ const handleConnect = async () => {
+ if (!tokenInput) {
+ toast.error('Please enter a Netlify API token');
+ return;
+ }
+
+ setIsConnecting(true);
+
+ try {
+ const response = await fetch('https://api.netlify.com/api/v1/user', {
+ headers: {
+ Authorization: `Bearer ${tokenInput}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const userData = (await response.json()) as NetlifyUser;
+
+ // Update the connection store
+ updateNetlifyConnection({
+ user: userData,
+ token: tokenInput,
+ });
+
+ toast.success('Connected to Netlify successfully');
+
+ // Fetch stats after successful connection
+ fetchNetlifyStats(tokenInput);
+ } catch (error) {
+ console.error('Error connecting to Netlify:', error);
+ toast.error(`Failed to connect to Netlify: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ } finally {
+ setIsConnecting(false);
+ setTokenInput('');
+ }
+ };
+
const handleDisconnect = () => {
// Clear from localStorage
localStorage.removeItem('netlify_connection');
@@ -608,15 +662,76 @@ export default function NetlifyConnection() {
{!connection.user ? (
-
{
- // Fetch stats after successful connection
- if (connection.token) {
- fetchNetlifyStats(connection.token);
- }
- }}
- showInstructions={true}
+
+ API Token
+
+ setTokenInput(e.target.value)}
+ placeholder="Enter your Netlify API token"
+ className={classNames(
+ 'w-full px-3 py-2 rounded-lg text-sm',
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
+ 'border border-[#E5E5E5] dark:border-[#333333]',
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
+ 'disabled:opacity-50',
+ )}
/>
+
+ {/* Debug info - remove this later */}
+
+
Debug: Token present: {connection.token ? '✅' : '❌'}
+
Debug: User present: {connection.user ? '✅' : '❌'}
+
Debug: Env token: {import.meta.env?.VITE_NETLIFY_ACCESS_TOKEN ? '✅' : '❌'}
+
+
+
+ {isConnecting ? (
+ <>
+
+ Connecting...
+ >
+ ) : (
+ <>
+
+ Connect
+ >
+ )}
+
+
+ {/* Debug button - remove this later */}
+
{
+ console.log('Manual Netlify auto-connect test');
+ await initializeNetlifyConnection();
+ }}
+ className="px-3 py-2 rounded-lg text-xs bg-blue-500 text-white hover:bg-blue-600"
+ >
+ Test Auto-Connect
+
+
) : (
diff --git a/app/components/@settings/tabs/connections/netlify/NetlifyQuickConnect.tsx b/app/components/@settings/tabs/connections/netlify/NetlifyQuickConnect.tsx
deleted file mode 100644
index b2f10c1e6c..0000000000
--- a/app/components/@settings/tabs/connections/netlify/NetlifyQuickConnect.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React, { useState } from 'react';
-import { toast } from 'react-toastify';
-import { updateNetlifyConnection } from '~/lib/stores/netlify';
-import { classNames } from '~/utils/classNames';
-
-interface NetlifyQuickConnectProps {
- onSuccess?: () => void;
- showInstructions?: boolean;
-}
-
-export const NetlifyQuickConnect: React.FC
= ({ onSuccess, showInstructions = true }) => {
- const [token, setToken] = useState('');
- const [isConnecting, setIsConnecting] = useState(false);
- const [showHelp, setShowHelp] = useState(false);
-
- const handleConnect = async () => {
- if (!token.trim()) {
- toast.error('Please enter your Netlify API token');
- return;
- }
-
- setIsConnecting(true);
-
- try {
- // Validate token with Netlify API
- const response = await fetch('https://api.netlify.com/api/v1/user', {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- if (!response.ok) {
- throw new Error('Invalid token or authentication failed');
- }
-
- const userData = (await response.json()) as any;
-
- // Fetch initial site statistics
- const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- let sites: any[] = [];
-
- if (sitesResponse.ok) {
- sites = (await sitesResponse.json()) as any[];
- }
-
- // Update the connection store
- updateNetlifyConnection({
- user: userData,
- token,
- stats: {
- sites,
- totalSites: sites.length,
- deploys: [],
- builds: [],
- lastDeployTime: '',
- },
- });
-
- toast.success(`Connected to Netlify as ${userData.email || userData.name || 'User'}`);
- setToken(''); // Clear the token field
-
- if (onSuccess) {
- onSuccess();
- }
- } catch (error) {
- console.error('Netlify connection error:', error);
- toast.error('Failed to connect to Netlify. Please check your token.');
- } finally {
- setIsConnecting(false);
- }
- };
-
- return (
-
-
-
-
- Personal Access Token
- {showInstructions && (
- setShowHelp(!showHelp)}
- className="text-xs text-accent-500 hover:text-accent-600 flex items-center gap-1"
- >
-
- How to get token
-
- )}
-
-
- setToken(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && token.trim() && !isConnecting) {
- handleConnect();
- }
- }}
- placeholder="Enter your Netlify API token"
- className={classNames(
- 'w-full px-3 py-2 pr-10 rounded-lg text-sm',
- 'bg-bolt-elements-background-depth-1',
- 'border border-bolt-elements-borderColor',
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
- 'focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent',
- 'disabled:opacity-50',
- )}
- disabled={isConnecting}
- />
- {token && (
- setToken('')}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary"
- >
-
-
- )}
-
-
-
- {showHelp && showInstructions && (
-
-
-
-
-
- Getting your Netlify Personal Access Token:
-
-
-
- 1.
-
- Go to{' '}
-
- Netlify Account Settings
-
-
-
-
-
- 2.
- Navigate to "Applications" → "Personal access tokens"
-
-
- 3.
- Click "New access token"
-
-
- 4.
- Give it a descriptive name (e.g., "bolt.diy deployment")
-
-
- 5.
- Copy the token and paste it above
-
-
-
-
- Note: Keep your token safe! It provides full access to your Netlify account.
-
-
-
-
-
- )}
-
-
-
-
- Get Token
-
-
- {isConnecting ? (
- <>
-
- Connecting...
- >
- ) : (
- <>
-
- Connect to Netlify
- >
- )}
-
-
-
-
-
-
-
-
-
Quick Tip
-
- Once connected, you can deploy any project with a single click directly from the editor!
-
-
-
-
-
- );
-};
diff --git a/app/components/auth/ProtectedRoute.tsx b/app/components/auth/ProtectedRoute.tsx
deleted file mode 100644
index 5791cc6602..0000000000
--- a/app/components/auth/ProtectedRoute.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect } from 'react';
-import { useNavigate } from '@remix-run/react';
-import { useStore } from '@nanostores/react';
-import { authStore } from '~/lib/stores/auth';
-import { motion } from 'framer-motion';
-
-interface ProtectedRouteProps {
- children: React.ReactNode;
-}
-
-export function ProtectedRoute({ children }: ProtectedRouteProps) {
- const navigate = useNavigate();
- const authState = useStore(authStore);
-
- useEffect(() => {
- // If not loading and not authenticated, redirect to auth page
- if (!authState.loading && !authState.isAuthenticated) {
- navigate('/auth');
- }
- }, [authState.loading, authState.isAuthenticated, navigate]);
-
- // Show loading state
- if (authState.loading) {
- return (
-
-
-
-
-
- Loading your workspace...
-
-
- );
- }
-
- // If not authenticated, don't render children (will redirect)
- if (!authState.isAuthenticated) {
- return null;
- }
-
- // Render protected content
- return <>{children}>;
-}
-
-// HOC for protecting pages
-export function withAuth(wrappedComponent: React.ComponentType
) {
- const Component = wrappedComponent;
-
- return function ProtectedComponent(props: P) {
- return (
-
-
-
- );
- };
-}
diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx
index cef935cb98..7c39276b99 100644
--- a/app/components/chat/AssistantMessage.tsx
+++ b/app/components/chat/AssistantMessage.tsx
@@ -176,18 +176,9 @@ export const AssistantMessage = memo(
>
-
-
- {content}
-
-
+
+ {content}
+
{toolInvocations && toolInvocations.length > 0 && (
{
- // Check authentication status after component mounts
- const checkAuth = async () => {
- // Give auth store time to initialize
- await new Promise((resolve) => setTimeout(resolve, 100));
-
- const state = authStore.get();
-
- if (!state.loading) {
- if (!state.isAuthenticated) {
- navigate('/auth');
- } else {
- setIsInitialized(true);
- }
- }
- };
-
- checkAuth();
- }, [navigate]);
-
- useEffect(() => {
- // Subscribe to auth changes
- const unsubscribe = authStore.subscribe((state) => {
- if (!state.loading && !state.isAuthenticated) {
- navigate('/auth');
- }
- });
-
- return () => {
- unsubscribe();
- };
- }, [navigate]);
-
- // Show loading state
- if (authState.loading || !isInitialized) {
- return (
-
-
-
-
-
-
-
- Initializing workspace...
-
-
-
- );
- }
-
- // If not authenticated, don't render (will redirect)
- if (!authState.isAuthenticated) {
- return null;
- }
-
- // Render authenticated content with enhanced header
- return (
-
-
-
- }>{() => }
-
- );
-}
diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx
index f3ac16ceed..17600f0e10 100644
--- a/app/components/chat/Messages.client.tsx
+++ b/app/components/chat/Messages.client.tsx
@@ -73,28 +73,20 @@ export const Messages = forwardRef(
{isUserMessage ? (
) : (
- <>
- {props.model?.includes('smartai') && index === messages.length - 1 && isStreaming && (
-
-
- SmartAI is explaining the process...
-
- )}
-
- >
+
)}
diff --git a/app/components/chat/ModelSelector.tsx b/app/components/chat/ModelSelector.tsx
index 19388d8291..2ccb9a5277 100644
--- a/app/components/chat/ModelSelector.tsx
+++ b/app/components/chat/ModelSelector.tsx
@@ -1,9 +1,84 @@
import type { ProviderInfo } from '~/types/model';
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import type { KeyboardEvent } from 'react';
import type { ModelInfo } from '~/lib/modules/llm/types';
import { classNames } from '~/utils/classNames';
+// Fuzzy search utilities
+const levenshteinDistance = (str1: string, str2: string): number => {
+ const matrix = [];
+
+ for (let i = 0; i <= str2.length; i++) {
+ matrix[i] = [i];
+ }
+
+ for (let j = 0; j <= str1.length; j++) {
+ matrix[0][j] = j;
+ }
+
+ for (let i = 1; i <= str2.length; i++) {
+ for (let j = 1; j <= str1.length; j++) {
+ if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
+ matrix[i][j] = matrix[i - 1][j - 1];
+ } else {
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
+ }
+ }
+ }
+
+ return matrix[str2.length][str1.length];
+};
+
+const fuzzyMatch = (query: string, text: string): { score: number; matches: boolean } => {
+ if (!query) {
+ return { score: 0, matches: true };
+ }
+
+ if (!text) {
+ return { score: 0, matches: false };
+ }
+
+ const queryLower = query.toLowerCase();
+ const textLower = text.toLowerCase();
+
+ // Exact substring match gets highest score
+ if (textLower.includes(queryLower)) {
+ return { score: 100 - (textLower.indexOf(queryLower) / textLower.length) * 20, matches: true };
+ }
+
+ // Fuzzy match with reasonable threshold
+ const distance = levenshteinDistance(queryLower, textLower);
+ const maxLen = Math.max(queryLower.length, textLower.length);
+ const similarity = 1 - distance / maxLen;
+
+ return {
+ score: similarity > 0.6 ? similarity * 80 : 0,
+ matches: similarity > 0.6,
+ };
+};
+
+const highlightText = (text: string, query: string): string => {
+ if (!query) {
+ return text;
+ }
+
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
+
+ return text.replace(regex, '$1 ');
+};
+
+const formatContextSize = (tokens: number): string => {
+ if (tokens >= 1000000) {
+ return `${(tokens / 1000000).toFixed(1)}M`;
+ }
+
+ if (tokens >= 1000) {
+ return `${(tokens / 1000).toFixed(0)}K`;
+ }
+
+ return tokens.toString();
+};
+
interface ModelSelectorProps {
model?: string;
setModel?: (model: string) => void;
@@ -40,12 +115,14 @@ export const ModelSelector = ({
modelLoading,
}: ModelSelectorProps) => {
const [modelSearchQuery, setModelSearchQuery] = useState('');
+ const [debouncedModelSearchQuery, setDebouncedModelSearchQuery] = useState('');
const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
const [focusedModelIndex, setFocusedModelIndex] = useState(-1);
const modelSearchInputRef = useRef(null);
const modelOptionsRef = useRef<(HTMLDivElement | null)[]>([]);
const modelDropdownRef = useRef(null);
const [providerSearchQuery, setProviderSearchQuery] = useState('');
+ const [debouncedProviderSearchQuery, setDebouncedProviderSearchQuery] = useState('');
const [isProviderDropdownOpen, setIsProviderDropdownOpen] = useState(false);
const [focusedProviderIndex, setFocusedProviderIndex] = useState(-1);
const providerSearchInputRef = useRef(null);
@@ -53,6 +130,23 @@ export const ModelSelector = ({
const providerDropdownRef = useRef(null);
const [showFreeModelsOnly, setShowFreeModelsOnly] = useState(false);
+ // Debounce search queries
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedModelSearchQuery(modelSearchQuery);
+ }, 150);
+
+ return () => clearTimeout(timer);
+ }, [modelSearchQuery]);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedProviderSearchQuery(providerSearchQuery);
+ }, 150);
+
+ return () => clearTimeout(timer);
+ }, [providerSearchQuery]);
+
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
@@ -71,24 +165,64 @@ export const ModelSelector = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
- const filteredModels = [...modelList]
- .filter((e) => e.provider === provider?.name && e.name)
- .filter((model) => {
- // Apply free models filter
- if (showFreeModelsOnly && !isModelLikelyFree(model, provider?.name)) {
- return false;
- }
+ const filteredModels = useMemo(() => {
+ const baseModels = [...modelList].filter((e) => e.provider === provider?.name && e.name);
- // Apply search filter
- return (
- model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
- model.name.toLowerCase().includes(modelSearchQuery.toLowerCase())
- );
- });
+ return baseModels
+ .filter((model) => {
+ // Apply free models filter
+ if (showFreeModelsOnly && !isModelLikelyFree(model, provider?.name)) {
+ return false;
+ }
- const filteredProviders = providerList.filter((p) =>
- p.name.toLowerCase().includes(providerSearchQuery.toLowerCase()),
- );
+ return true;
+ })
+ .map((model) => {
+ // Calculate search scores for fuzzy matching
+ const labelMatch = fuzzyMatch(debouncedModelSearchQuery, model.label);
+ const nameMatch = fuzzyMatch(debouncedModelSearchQuery, model.name);
+ const contextMatch = fuzzyMatch(debouncedModelSearchQuery, formatContextSize(model.maxTokenAllowed));
+
+ const bestScore = Math.max(labelMatch.score, nameMatch.score, contextMatch.score);
+ const matches = labelMatch.matches || nameMatch.matches || contextMatch.matches || !debouncedModelSearchQuery; // Show all if no query
+
+ return {
+ ...model,
+ searchScore: bestScore,
+ searchMatches: matches,
+ highlightedLabel: highlightText(model.label, debouncedModelSearchQuery),
+ highlightedName: highlightText(model.name, debouncedModelSearchQuery),
+ };
+ })
+ .filter((model) => model.searchMatches)
+ .sort((a, b) => {
+ // Sort by search score (highest first), then by label
+ if (debouncedModelSearchQuery) {
+ return b.searchScore - a.searchScore;
+ }
+
+ return a.label.localeCompare(b.label);
+ });
+ }, [modelList, provider?.name, showFreeModelsOnly, debouncedModelSearchQuery]);
+
+ const filteredProviders = useMemo(() => {
+ if (!debouncedProviderSearchQuery) {
+ return providerList;
+ }
+
+ return providerList
+ .map((provider) => {
+ const match = fuzzyMatch(debouncedProviderSearchQuery, provider.name);
+ return {
+ ...provider,
+ searchScore: match.score,
+ searchMatches: match.matches,
+ highlightedName: highlightText(provider.name, debouncedProviderSearchQuery),
+ };
+ })
+ .filter((provider) => provider.searchMatches)
+ .sort((a, b) => b.searchScore - a.searchScore);
+ }, [providerList, debouncedProviderSearchQuery]);
// Reset free models filter when provider changes
useEffect(() => {
@@ -97,11 +231,30 @@ export const ModelSelector = ({
useEffect(() => {
setFocusedModelIndex(-1);
- }, [modelSearchQuery, isModelDropdownOpen, showFreeModelsOnly]);
+ }, [debouncedModelSearchQuery, isModelDropdownOpen, showFreeModelsOnly]);
useEffect(() => {
setFocusedProviderIndex(-1);
- }, [providerSearchQuery, isProviderDropdownOpen]);
+ }, [debouncedProviderSearchQuery, isProviderDropdownOpen]);
+
+ // Clear search functions
+ const clearModelSearch = useCallback(() => {
+ setModelSearchQuery('');
+ setDebouncedModelSearchQuery('');
+
+ if (modelSearchInputRef.current) {
+ modelSearchInputRef.current.focus();
+ }
+ }, []);
+
+ const clearProviderSearch = useCallback(() => {
+ setProviderSearchQuery('');
+ setDebouncedProviderSearchQuery('');
+
+ if (providerSearchInputRef.current) {
+ providerSearchInputRef.current.focus();
+ }
+ }, []);
useEffect(() => {
if (isModelDropdownOpen && modelSearchInputRef.current) {
@@ -137,6 +290,7 @@ export const ModelSelector = ({
setModel?.(selectedModel.name);
setIsModelDropdownOpen(false);
setModelSearchQuery('');
+ setDebouncedModelSearchQuery('');
}
break;
@@ -144,12 +298,20 @@ export const ModelSelector = ({
e.preventDefault();
setIsModelDropdownOpen(false);
setModelSearchQuery('');
+ setDebouncedModelSearchQuery('');
break;
case 'Tab':
if (!e.shiftKey && focusedModelIndex === filteredModels.length - 1) {
setIsModelDropdownOpen(false);
}
+ break;
+ case 'k':
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault();
+ clearModelSearch();
+ }
+
break;
}
};
@@ -186,6 +348,7 @@ export const ModelSelector = ({
setIsProviderDropdownOpen(false);
setProviderSearchQuery('');
+ setDebouncedProviderSearchQuery('');
}
break;
@@ -193,12 +356,20 @@ export const ModelSelector = ({
e.preventDefault();
setIsProviderDropdownOpen(false);
setProviderSearchQuery('');
+ setDebouncedProviderSearchQuery('');
break;
case 'Tab':
if (!e.shiftKey && focusedProviderIndex === filteredProviders.length - 1) {
setIsProviderDropdownOpen(false);
}
+ break;
+ case 'k':
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault();
+ clearProviderSearch();
+ }
+
break;
}
};
@@ -292,9 +463,9 @@ export const ModelSelector = ({
type="text"
value={providerSearchQuery}
onChange={(e) => setProviderSearchQuery(e.target.value)}
- placeholder="Search providers..."
+ placeholder="Search providers... (⌘K to clear)"
className={classNames(
- 'w-full pl-2 py-1.5 rounded-md text-sm',
+ 'w-full pl-8 pr-8 py-1.5 rounded-md text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
@@ -307,6 +478,19 @@ export const ModelSelector = ({
+ {providerSearchQuery && (
+ {
+ e.stopPropagation();
+ clearProviderSearch();
+ }}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bolt-elements-background-depth-3 transition-colors"
+ aria-label="Clear search"
+ >
+
+
+ )}
@@ -327,7 +511,18 @@ export const ModelSelector = ({
)}
>
{filteredProviders.length === 0 ? (
- No providers found
+
+
+ {debouncedProviderSearchQuery
+ ? `No providers match "${debouncedProviderSearchQuery}"`
+ : 'No providers found'}
+
+ {debouncedProviderSearchQuery && (
+
+ Try searching for provider names like "OpenAI", "Anthropic", or "Google"
+
+ )}
+
) : (
filteredProviders.map((providerOption, index) => (
- {providerOption.name}
+
))
)}
@@ -396,15 +596,7 @@ export const ModelSelector = ({
tabIndex={0}
>
-
- {modelList.find((m) => m.name === model)?.label || 'Select model'}
- {modelList.find((m) => m.name === model)?.isSmartAIEnabled && (
-
-
- Active
-
- )}
-
+
{modelList.find((m) => m.name === model)?.label || 'Select model'}
)}
+ {/* Search Result Count */}
+ {debouncedModelSearchQuery && filteredModels.length > 0 && (
+
+ {filteredModels.length} model{filteredModels.length !== 1 ? 's' : ''} found
+ {filteredModels.length > 5 && ' (showing best matches)'}
+
+ )}
+
{/* Search Input */}
setModelSearchQuery(e.target.value)}
- placeholder="Search models..."
+ placeholder="Search models... (⌘K to clear)"
className={classNames(
- 'w-full pl-2 py-1.5 rounded-md text-sm',
+ 'w-full pl-8 pr-8 py-1.5 rounded-md text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
@@ -471,6 +671,19 @@ export const ModelSelector = ({
+ {modelSearchQuery && (
+
{
+ e.stopPropagation();
+ clearModelSearch();
+ }}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bolt-elements-background-depth-3 transition-colors"
+ aria-label="Clear search"
+ >
+
+
+ )}
@@ -491,16 +704,37 @@ export const ModelSelector = ({
)}
>
{modelLoading === 'all' || modelLoading === provider?.name ? (
-
Loading...
+
+
+
+ Loading models...
+
+
) : filteredModels.length === 0 ? (
-
- {showFreeModelsOnly ? 'No free models found' : 'No models found'}
+
+
+ {debouncedModelSearchQuery
+ ? `No models match "${debouncedModelSearchQuery}"${showFreeModelsOnly ? ' (free only)' : ''}`
+ : showFreeModelsOnly
+ ? 'No free models available'
+ : 'No models available'}
+
+ {debouncedModelSearchQuery && (
+
+ Try searching for model names, context sizes (e.g., "128k", "1M"), or capabilities
+
+ )}
+ {showFreeModelsOnly && !debouncedModelSearchQuery && (
+
+ Try disabling the "Free models only" filter to see all available models
+
+ )}
) : (
filteredModels.map((modelOption, index) => (
(modelOptionsRef.current[index] = el)}
- key={index} // Consider using modelOption.name if unique
+ key={modelOption.name}
role="option"
aria-selected={model === modelOption.name}
className={classNames(
@@ -518,22 +752,38 @@ export const ModelSelector = ({
setModel?.(modelOption.name);
setIsModelDropdownOpen(false);
setModelSearchQuery('');
+ setDebouncedModelSearchQuery('');
}}
tabIndex={focusedModelIndex === index ? 0 : -1}
>
-
- {modelOption.label}
- {modelOption.isSmartAIEnabled && (
-
-
- SmartAI
+
+
+
+
+
+
+ {formatContextSize(modelOption.maxTokenAllowed)} tokens
+ {debouncedModelSearchQuery && (modelOption as any).searchScore > 70 && (
+
+ {(modelOption as any).searchScore.toFixed(0)}% match
+
+ )}
+
+
+
+ {isModelLikelyFree(modelOption, provider?.name) && (
+
+ )}
+ {model === modelOption.name && (
+
)}
-
- {isModelLikelyFree(modelOption, provider?.name) && (
-
- )}
+
))
diff --git a/app/components/chat/SmartAIToggle.tsx b/app/components/chat/SmartAIToggle.tsx
deleted file mode 100644
index 752d2954dc..0000000000
--- a/app/components/chat/SmartAIToggle.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { classNames } from '~/utils/classNames';
-import type { ModelInfo } from '~/lib/modules/llm/types';
-import type { ProviderInfo } from '~/types/model';
-
-interface SmartAIToggleProps {
- enabled: boolean;
- onToggle: (enabled: boolean) => void;
- provider?: ProviderInfo;
- model?: string;
- modelList: ModelInfo[];
-}
-
-export const SmartAiToggle: React.FC
= ({ enabled, onToggle, provider, model, modelList }) => {
- // Check if current model supports SmartAI
- const currentModel = modelList.find((m) => m.name === model);
- const isSupported = currentModel?.supportsSmartAI && (provider?.name === 'Anthropic' || provider?.name === 'OpenAI');
-
- if (!isSupported) {
- return null;
- }
-
- return (
- onToggle(!enabled)}
- className={classNames(
- 'flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all',
- 'border border-bolt-elements-borderColor',
- enabled
- ? 'bg-gradient-to-r from-blue-500/20 to-purple-500/20 border-blue-500/30'
- : 'bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3',
- )}
- title="Toggle SmartAI for detailed conversational feedback"
- >
-
-
- SmartAI {enabled ? 'ON' : 'OFF'}
-
-
- );
-};
diff --git a/app/components/chat/WelcomeMessage.tsx b/app/components/chat/WelcomeMessage.tsx
deleted file mode 100644
index 9250e3b873..0000000000
--- a/app/components/chat/WelcomeMessage.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-import { useStore } from '@nanostores/react';
-import { authStore } from '~/lib/stores/auth';
-import { motion } from 'framer-motion';
-
-const EXAMPLE_PROMPTS = [
- { text: 'Create a mobile app about bolt.diy' },
- { text: 'Build a todo app in React using Tailwind' },
- { text: 'Build a simple blog using Astro' },
- { text: 'Create a cookie consent form using Material UI' },
- { text: 'Make a space invaders game' },
- { text: 'Make a Tic Tac Toe game in html, css and js only' },
-];
-
-interface WelcomeMessageProps {
- sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
-}
-
-export function WelcomeMessage({ sendMessage }: WelcomeMessageProps) {
- const authState = useStore(authStore);
- const timeOfDay = new Date().getHours();
-
- const getGreeting = () => {
- if (timeOfDay < 12) {
- return 'Good morning';
- }
-
- if (timeOfDay < 17) {
- return 'Good afternoon';
- }
-
- return 'Good evening';
- };
-
- return (
-
- {/* Personalized Greeting */}
-
-
- {getGreeting()}, {authState.user?.firstName || 'Developer'}!
-
- What would you like to build today?
-
-
- {/* Example Prompts */}
-
- Try one of these examples to get started:
-
- {EXAMPLE_PROMPTS.map((examplePrompt, index) => (
- sendMessage?.(event, examplePrompt.text)}
- className="border border-bolt-elements-borderColor rounded-full bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary px-3 py-1 text-xs transition-all hover:scale-105"
- >
- {examplePrompt.text}
-
- ))}
-
-
-
- {/* User Stats */}
- {authState.user && (
-
-
- Logged in as{' '}
- @{authState.user.username}
-
-
- )}
-
- );
-}
diff --git a/app/components/deploy/DeployButton.tsx b/app/components/deploy/DeployButton.tsx
index ffc58ada95..ffdeb37e9b 100644
--- a/app/components/deploy/DeployButton.tsx
+++ b/app/components/deploy/DeployButton.tsx
@@ -1,29 +1,277 @@
-import { useState } from 'react';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useStore } from '@nanostores/react';
+import { netlifyConnection } from '~/lib/stores/netlify';
+import { vercelConnection } from '~/lib/stores/vercel';
+import { isGitLabConnected } from '~/lib/stores/gitlabConnection';
import { workbenchStore } from '~/lib/stores/workbench';
import { streamingState } from '~/lib/stores/streaming';
-import { DeployDialog } from './DeployDialog';
+import { classNames } from '~/utils/classNames';
+import { useState } from 'react';
+import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
+import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
+import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
+import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
+import { useGitHubDeploy } from '~/components/deploy/GitHubDeploy.client';
+import { useGitLabDeploy } from '~/components/deploy/GitLabDeploy.client';
+import { GitHubDeploymentDialog } from '~/components/deploy/GitHubDeploymentDialog';
+import { GitLabDeploymentDialog } from '~/components/deploy/GitLabDeploymentDialog';
+
+interface DeployButtonProps {
+ onVercelDeploy?: () => Promise;
+ onNetlifyDeploy?: () => Promise;
+ onGitHubDeploy?: () => Promise;
+ onGitLabDeploy?: () => Promise;
+}
-export const DeployButton = () => {
- const [isDialogOpen, setIsDialogOpen] = useState(false);
+export const DeployButton = ({
+ onVercelDeploy,
+ onNetlifyDeploy,
+ onGitHubDeploy,
+ onGitLabDeploy,
+}: DeployButtonProps) => {
+ const netlifyConn = useStore(netlifyConnection);
+ const vercelConn = useStore(vercelConnection);
+ const gitlabIsConnected = useStore(isGitLabConnected);
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
+ const [isDeploying, setIsDeploying] = useState(false);
+ const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | 'github' | 'gitlab' | null>(null);
const isStreaming = useStore(streamingState);
+ const { handleVercelDeploy } = useVercelDeploy();
+ const { handleNetlifyDeploy } = useNetlifyDeploy();
+ const { handleGitHubDeploy } = useGitHubDeploy();
+ const { handleGitLabDeploy } = useGitLabDeploy();
+ const [showGitHubDeploymentDialog, setShowGitHubDeploymentDialog] = useState(false);
+ const [showGitLabDeploymentDialog, setShowGitLabDeploymentDialog] = useState(false);
+ const [githubDeploymentFiles, setGithubDeploymentFiles] = useState | null>(null);
+ const [gitlabDeploymentFiles, setGitlabDeploymentFiles] = useState | null>(null);
+ const [githubProjectName, setGithubProjectName] = useState('');
+ const [gitlabProjectName, setGitlabProjectName] = useState('');
+
+ const handleVercelDeployClick = async () => {
+ setIsDeploying(true);
+ setDeployingTo('vercel');
+
+ try {
+ if (onVercelDeploy) {
+ await onVercelDeploy();
+ } else {
+ await handleVercelDeploy();
+ }
+ } finally {
+ setIsDeploying(false);
+ setDeployingTo(null);
+ }
+ };
+
+ const handleNetlifyDeployClick = async () => {
+ setIsDeploying(true);
+ setDeployingTo('netlify');
+
+ try {
+ if (onNetlifyDeploy) {
+ await onNetlifyDeploy();
+ } else {
+ await handleNetlifyDeploy();
+ }
+ } finally {
+ setIsDeploying(false);
+ setDeployingTo(null);
+ }
+ };
+
+ const handleGitHubDeployClick = async () => {
+ setIsDeploying(true);
+ setDeployingTo('github');
+
+ try {
+ if (onGitHubDeploy) {
+ await onGitHubDeploy();
+ } else {
+ const result = await handleGitHubDeploy();
+
+ if (result && result.success && result.files) {
+ setGithubDeploymentFiles(result.files);
+ setGithubProjectName(result.projectName);
+ setShowGitHubDeploymentDialog(true);
+ }
+ }
+ } finally {
+ setIsDeploying(false);
+ setDeployingTo(null);
+ }
+ };
+
+ const handleGitLabDeployClick = async () => {
+ setIsDeploying(true);
+ setDeployingTo('gitlab');
+
+ try {
+ if (onGitLabDeploy) {
+ await onGitLabDeploy();
+ } else {
+ const result = await handleGitLabDeploy();
+
+ if (result && result.success && result.files) {
+ setGitlabDeploymentFiles(result.files);
+ setGitlabProjectName(result.projectName);
+ setShowGitLabDeploymentDialog(true);
+ }
+ }
+ } finally {
+ setIsDeploying(false);
+ setDeployingTo(null);
+ }
+ };
return (
<>
- setIsDialogOpen(true)}
- disabled={!activePreview || isStreaming}
- className="px-4 py-1.5 rounded-lg bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2 text-sm font-medium"
- title="Deploy your project"
- >
-
- Deploy
-
-
- setIsDialogOpen(false)} />
+
+
+ {/* GitHub Deployment Dialog */}
+ {showGitHubDeploymentDialog && githubDeploymentFiles && (
+ setShowGitHubDeploymentDialog(false)}
+ projectName={githubProjectName}
+ files={githubDeploymentFiles}
+ />
+ )}
+
+ {/* GitLab Deployment Dialog */}
+ {showGitLabDeploymentDialog && gitlabDeploymentFiles && (
+ setShowGitLabDeploymentDialog(false)}
+ projectName={gitlabProjectName}
+ files={gitlabDeploymentFiles}
+ />
+ )}
>
);
};
diff --git a/app/components/deploy/DeployDialog.tsx b/app/components/deploy/DeployDialog.tsx
deleted file mode 100644
index 7526973ae5..0000000000
--- a/app/components/deploy/DeployDialog.tsx
+++ /dev/null
@@ -1,466 +0,0 @@
-import React, { useState } from 'react';
-import * as RadixDialog from '@radix-ui/react-dialog';
-import { Dialog, DialogTitle, DialogDescription } from '~/components/ui/Dialog';
-import { useStore } from '@nanostores/react';
-import { netlifyConnection, updateNetlifyConnection } from '~/lib/stores/netlify';
-import { vercelConnection } from '~/lib/stores/vercel';
-import { useNetlifyDeploy } from './NetlifyDeploy.client';
-import { useVercelDeploy } from './VercelDeploy.client';
-import { useGitHubDeploy } from './GitHubDeploy.client';
-import { GitHubDeploymentDialog } from './GitHubDeploymentDialog';
-import { toast } from 'react-toastify';
-import { classNames } from '~/utils/classNames';
-
-interface DeployDialogProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-interface DeployProvider {
- id: 'netlify' | 'vercel' | 'github' | 'cloudflare';
- name: string;
- iconClass: string;
- iconColor?: string;
- connected: boolean;
- comingSoon?: boolean;
- description: string;
- features: string[];
-}
-
-const NetlifyConnectForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
- const [token, setToken] = useState('');
- const [isConnecting, setIsConnecting] = useState(false);
-
- const handleConnect = async () => {
- if (!token.trim()) {
- toast.error('Please enter your Netlify API token');
- return;
- }
-
- setIsConnecting(true);
-
- try {
- // Validate token with Netlify API
- const response = await fetch('https://api.netlify.com/api/v1/user', {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- if (!response.ok) {
- throw new Error('Invalid token or authentication failed');
- }
-
- const userData = (await response.json()) as any;
-
- // Update the connection store
- updateNetlifyConnection({
- user: userData,
- token,
- });
-
- toast.success(`Connected to Netlify as ${userData.email || userData.name || 'User'}`);
- onSuccess();
- } catch (error) {
- console.error('Netlify connection error:', error);
- toast.error('Failed to connect to Netlify. Please check your token.');
- } finally {
- setIsConnecting(false);
- }
- };
-
- return (
-
-
-
Connect to Netlify
-
- To deploy your project to Netlify, you need to connect your account using a Personal Access Token.
-
-
-
-
-
- Personal Access Token
- setToken(e.target.value)}
- placeholder="Enter your Netlify API token"
- className={classNames(
- 'w-full px-3 py-2 rounded-lg text-sm',
- 'bg-bolt-elements-background-depth-1',
- 'border border-bolt-elements-borderColor',
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
- 'focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent',
- 'disabled:opacity-50',
- )}
- disabled={isConnecting}
- />
-
-
-
-
-
-
How to get your token:
-
- Go to your Netlify account settings
- Navigate to "Applications" → "Personal access tokens"
- Click "New access token"
- Give it a descriptive name (e.g., "bolt.diy deployment")
- Copy the token and paste it here
-
-
-
-
-
- {isConnecting ? (
- <>
-
- Connecting...
- >
- ) : (
- <>
-
- Connect Account
- >
- )}
-
-
-
-
- );
-};
-
-export const DeployDialog: React.FC = ({ isOpen, onClose }) => {
- const netlifyConn = useStore(netlifyConnection);
- const vercelConn = useStore(vercelConnection);
- const [selectedProvider, setSelectedProvider] = useState<'netlify' | 'vercel' | 'github' | null>(null);
- const [isDeploying, setIsDeploying] = useState(false);
- const [showGitHubDialog, setShowGitHubDialog] = useState(false);
- const [githubFiles, setGithubFiles] = useState | null>(null);
- const [githubProjectName, setGithubProjectName] = useState('');
- const { handleNetlifyDeploy } = useNetlifyDeploy();
- const { handleVercelDeploy } = useVercelDeploy();
- const { handleGitHubDeploy } = useGitHubDeploy();
-
- const providers: DeployProvider[] = [
- {
- id: 'netlify',
- name: 'Netlify',
- iconClass: 'i-simple-icons:netlify',
- iconColor: 'text-[#00C7B7]',
- connected: !!netlifyConn.user,
- description: 'Deploy your site with automatic SSL, global CDN, and continuous deployment',
- features: [
- 'Automatic SSL certificates',
- 'Global CDN',
- 'Instant rollbacks',
- 'Deploy previews',
- 'Form handling',
- 'Serverless functions',
- ],
- },
- {
- id: 'vercel',
- name: 'Vercel',
- iconClass: 'i-simple-icons:vercel',
- connected: !!vercelConn.user,
- description: 'Deploy with the platform built for frontend developers',
- features: [
- 'Zero-config deployments',
- 'Edge Functions',
- 'Analytics',
- 'Web Vitals monitoring',
- 'Preview deployments',
- 'Automatic HTTPS',
- ],
- },
- {
- id: 'github',
- name: 'GitHub',
- iconClass: 'i-simple-icons:github',
- connected: true, // GitHub doesn't require separate auth
- description: 'Deploy to GitHub Pages or create a repository',
- features: [
- 'Free hosting with GitHub Pages',
- 'Version control integration',
- 'Collaborative development',
- 'Actions & Workflows',
- 'Issue tracking',
- 'Pull requests',
- ],
- },
- {
- id: 'cloudflare',
- name: 'Cloudflare Pages',
- iconClass: 'i-simple-icons:cloudflare',
- iconColor: 'text-[#F38020]',
- connected: false,
- comingSoon: true,
- description: "Deploy on Cloudflare's global network",
- features: [
- 'Unlimited bandwidth',
- 'DDoS protection',
- 'Web Analytics',
- 'Edge Workers',
- 'Custom domains',
- 'Automatic builds',
- ],
- },
- ];
-
- const handleDeploy = async (provider: 'netlify' | 'vercel' | 'github') => {
- setIsDeploying(true);
-
- try {
- let success = false;
-
- if (provider === 'netlify') {
- success = await handleNetlifyDeploy();
- } else if (provider === 'vercel') {
- success = await handleVercelDeploy();
- } else if (provider === 'github') {
- const result = await handleGitHubDeploy();
-
- if (result && typeof result === 'object' && result.success && result.files) {
- setGithubFiles(result.files);
- setGithubProjectName(result.projectName);
- setShowGitHubDialog(true);
- onClose();
-
- return;
- }
-
- success = result && typeof result === 'object' ? result.success : false;
- }
-
- if (success) {
- toast.success(
- `Successfully deployed to ${provider === 'netlify' ? 'Netlify' : provider === 'vercel' ? 'Vercel' : 'GitHub'}`,
- );
- onClose();
- }
- } catch (error) {
- console.error('Deployment error:', error);
- toast.error(
- `Failed to deploy to ${provider === 'netlify' ? 'Netlify' : provider === 'vercel' ? 'Vercel' : 'GitHub'}`,
- );
- } finally {
- setIsDeploying(false);
- }
- };
-
- const renderProviderContent = () => {
- if (!selectedProvider) {
- return (
-
- {providers.map((provider) => (
-
- !provider.comingSoon && setSelectedProvider(provider.id as 'netlify' | 'vercel' | 'github')
- }
- disabled={provider.comingSoon}
- className={classNames(
- 'p-4 rounded-lg border-2 transition-all text-left',
- 'hover:border-accent-500 hover:bg-bolt-elements-background-depth-2',
- provider.comingSoon
- ? 'border-bolt-elements-borderColor opacity-50 cursor-not-allowed'
- : 'border-bolt-elements-borderColor cursor-pointer',
- )}
- >
-
-
-
-
-
-
-
{provider.name}
- {provider.connected && (
- Connected
- )}
- {provider.comingSoon && (
-
- Coming Soon
-
- )}
-
-
{provider.description}
-
- {provider.features.slice(0, 3).map((feature, index) => (
-
- {feature}
-
- ))}
- {provider.features.length > 3 && (
-
- +{provider.features.length - 3} more
-
- )}
-
-
-
-
- ))}
-
- );
- }
-
- const provider = providers.find((p) => p.id === selectedProvider);
-
- if (!provider) {
- return null;
- }
-
- // If provider is not connected, show connection form
- if (!provider.connected) {
- if (selectedProvider === 'netlify') {
- return (
- {
- handleDeploy('netlify');
- }}
- />
- );
- }
-
- // Add Vercel connection form here if needed
- return Vercel connection form coming soon...
;
- }
-
- // If connected, show deployment confirmation
- return (
-
-
-
-
-
{provider.name}
-
Ready to deploy to your {provider.name} account
-
-
Connected
-
-
-
-
Deployment Features:
-
- {provider.features.map((feature, index) => (
-
-
- {feature}
-
- ))}
-
-
-
-
- setSelectedProvider(null)}
- className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2"
- >
- Back
-
- handleDeploy(selectedProvider as 'netlify' | 'vercel' | 'github')}
- disabled={isDeploying}
- className={classNames(
- 'flex-1 px-4 py-2 rounded-lg font-medium transition-all',
- 'bg-accent-500 text-white',
- 'hover:bg-accent-600',
- 'disabled:opacity-50 disabled:cursor-not-allowed',
- 'flex items-center justify-center gap-2',
- )}
- >
- {isDeploying ? (
- <>
-
- Deploying...
- >
- ) : (
- <>
-
- Deploy Now
- >
- )}
-
-
-
- );
- };
-
- return (
- <>
- !open && onClose()}>
-
-
-
- Deploy Your Project
-
- Choose a deployment platform to publish your project to the web
-
-
-
-
- {renderProviderContent()}
-
-
- {!selectedProvider && (
-
-
- Cancel
-
-
- )}
-
-
-
-
- {/* GitHub Deployment Dialog */}
- {showGitHubDialog && githubFiles && (
- setShowGitHubDialog(false)}
- projectName={githubProjectName}
- files={githubFiles}
- />
- )}
- >
- );
-};
diff --git a/app/components/deploy/EnhancedDeployButton.tsx b/app/components/deploy/EnhancedDeployButton.tsx
deleted file mode 100644
index 01147779fa..0000000000
--- a/app/components/deploy/EnhancedDeployButton.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-/**
- * Enhanced Deploy Button with Quick Deploy Option
- * Contributed by Keoma Wright
- *
- * This component provides both authenticated and quick deployment options
- */
-
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
-import { useStore } from '@nanostores/react';
-import { netlifyConnection } from '~/lib/stores/netlify';
-import { vercelConnection } from '~/lib/stores/vercel';
-import { workbenchStore } from '~/lib/stores/workbench';
-import { streamingState } from '~/lib/stores/streaming';
-import { classNames } from '~/utils/classNames';
-import { useState } from 'react';
-import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
-import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
-import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
-import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
-import { QuickNetlifyDeploy } from '~/components/deploy/QuickNetlifyDeploy.client';
-import * as Dialog from '@radix-ui/react-dialog';
-
-interface EnhancedDeployButtonProps {
- onVercelDeploy?: () => Promise;
- onNetlifyDeploy?: () => Promise;
-}
-
-export const EnhancedDeployButton = ({ onVercelDeploy, onNetlifyDeploy }: EnhancedDeployButtonProps) => {
- const netlifyConn = useStore(netlifyConnection);
- const vercelConn = useStore(vercelConnection);
- const [activePreviewIndex] = useState(0);
- const previews = useStore(workbenchStore.previews);
- const activePreview = previews[activePreviewIndex];
- const [isDeploying, setIsDeploying] = useState(false);
- const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | 'quick' | null>(null);
- const [showQuickDeploy, setShowQuickDeploy] = useState(false);
- const isStreaming = useStore(streamingState);
- const { handleVercelDeploy } = useVercelDeploy();
- const { handleNetlifyDeploy } = useNetlifyDeploy();
-
- const handleVercelDeployClick = async () => {
- setIsDeploying(true);
- setDeployingTo('vercel');
-
- try {
- if (onVercelDeploy) {
- await onVercelDeploy();
- } else {
- await handleVercelDeploy();
- }
- } finally {
- setIsDeploying(false);
- setDeployingTo(null);
- }
- };
-
- const handleNetlifyDeployClick = async () => {
- setIsDeploying(true);
- setDeployingTo('netlify');
-
- try {
- if (onNetlifyDeploy) {
- await onNetlifyDeploy();
- } else {
- await handleNetlifyDeploy();
- }
- } finally {
- setIsDeploying(false);
- setDeployingTo(null);
- }
- };
-
- return (
- <>
-
-
-
- {isDeploying ? `Deploying${deployingTo ? ` to ${deployingTo}` : ''}...` : 'Deploy'}
-
-
-
- {/* Quick Deploy Option - Always Available */}
- setShowQuickDeploy(true)}
- >
-
-
-
NEW
-
- Quick Deploy to Netlify (No Login)
-
-
-
-
- {/* Authenticated Netlify Deploy */}
-
-
-
- {!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
-
- {netlifyConn.user && }
-
-
- {/* Vercel Deploy */}
-
-
- {!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}
- {vercelConn.user && }
-
-
- {/* Cloudflare - Coming Soon */}
-
-
- Deploy to Cloudflare (Coming Soon)
-
-
-
-
-
- {/* Quick Deploy Dialog */}
-
-
-
-
-
-
-
Quick Deploy to Netlify
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
diff --git a/app/components/deploy/QuickNetlifyDeploy.client.tsx b/app/components/deploy/QuickNetlifyDeploy.client.tsx
deleted file mode 100644
index 513199ce94..0000000000
--- a/app/components/deploy/QuickNetlifyDeploy.client.tsx
+++ /dev/null
@@ -1,362 +0,0 @@
-/**
- * Quick Netlify Deployment Component
- * Contributed by Keoma Wright
- *
- * This component provides a streamlined one-click deployment to Netlify
- * with automatic build detection and configuration.
- */
-
-import { useState } from 'react';
-import { toast } from 'react-toastify';
-import { useStore } from '@nanostores/react';
-import { workbenchStore } from '~/lib/stores/workbench';
-import { webcontainer } from '~/lib/webcontainer';
-import { path } from '~/utils/path';
-import { chatId } from '~/lib/persistence/useChatHistory';
-import type { ActionCallbackData } from '~/lib/runtime/message-parser';
-
-interface QuickDeployConfig {
- framework?: 'react' | 'vue' | 'angular' | 'svelte' | 'next' | 'nuxt' | 'gatsby' | 'static';
- buildCommand?: string;
- outputDirectory?: string;
- nodeVersion?: string;
-}
-
-export function QuickNetlifyDeploy() {
- const [isDeploying, setIsDeploying] = useState(false);
- const [deployUrl, setDeployUrl] = useState(null);
- const [showAdvanced, setShowAdvanced] = useState(false);
- const currentChatId = useStore(chatId);
-
- const detectFramework = async (): Promise => {
- try {
- const container = await webcontainer;
-
- // Read package.json to detect framework
- let packageJson: any = {};
-
- try {
- const packageContent = await container.fs.readFile('/package.json', 'utf-8');
- packageJson = JSON.parse(packageContent);
- } catch {
- console.log('No package.json found, assuming static site');
- return {
- framework: 'static',
- buildCommand: '',
- outputDirectory: '/',
- nodeVersion: '18',
- };
- }
-
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
- const scripts = packageJson.scripts || {};
-
- // Detect framework based on dependencies
- const config: QuickDeployConfig = {
- nodeVersion: '18',
- };
-
- if (deps.next) {
- config.framework = 'next';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = '.next';
- } else if (deps.nuxt || deps.nuxt3) {
- config.framework = 'nuxt';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = '.output/public';
- } else if (deps.gatsby) {
- config.framework = 'gatsby';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = 'public';
- } else if (deps['@angular/core']) {
- config.framework = 'angular';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = 'dist';
- } else if (deps.vue) {
- config.framework = 'vue';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = 'dist';
- } else if (deps.svelte) {
- config.framework = 'svelte';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = 'public';
- } else if (deps.react) {
- config.framework = 'react';
- config.buildCommand = scripts.build || 'npm run build';
- config.outputDirectory = 'build';
-
- // Check for Vite
- if (deps.vite) {
- config.outputDirectory = 'dist';
- }
- } else {
- config.framework = 'static';
- config.buildCommand = scripts.build || '';
- config.outputDirectory = '/';
- }
-
- return config;
- } catch (error) {
- console.error('Error detecting framework:', error);
- return {
- framework: 'static',
- buildCommand: '',
- outputDirectory: '/',
- nodeVersion: '18',
- };
- }
- };
-
- const handleQuickDeploy = async (): Promise => {
- if (!currentChatId) {
- toast.error('No active project found');
- return null;
- }
-
- try {
- setIsDeploying(true);
- setDeployUrl(null);
-
- const artifact = workbenchStore.firstArtifact;
-
- if (!artifact) {
- throw new Error('No active project found');
- }
-
- // Detect framework and configuration
- const config = await detectFramework();
-
- toast.info(`Detected ${config.framework || 'static'} project. Starting deployment...`);
-
- // Create deployment artifact for visual feedback
- const deploymentId = `quick-deploy-${Date.now()}`;
- workbenchStore.addArtifact({
- id: deploymentId,
- messageId: deploymentId,
- title: 'Quick Netlify Deployment',
- type: 'standalone',
- });
-
- const deployArtifact = workbenchStore.artifacts.get()[deploymentId];
-
- // Build the project if needed
- if (config.buildCommand) {
- deployArtifact.runner.handleDeployAction('building', 'running', { source: 'netlify' });
-
- const actionId = 'build-' + Date.now();
- const actionData: ActionCallbackData = {
- messageId: 'quick-netlify-build',
- artifactId: artifact.id,
- actionId,
- action: {
- type: 'build' as const,
- content: config.buildCommand,
- },
- };
-
- artifact.runner.addAction(actionData);
- await artifact.runner.runAction(actionData);
-
- if (!artifact.runner.buildOutput) {
- deployArtifact.runner.handleDeployAction('building', 'failed', {
- error: 'Build failed. Check the terminal for details.',
- source: 'netlify',
- });
- throw new Error('Build failed');
- }
- }
-
- // Prepare deployment
- deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'netlify' });
-
- const container = await webcontainer;
-
- // Determine the output directory
- let outputPath = config.outputDirectory || '/';
-
- if (artifact.runner.buildOutput && artifact.runner.buildOutput.path) {
- outputPath = artifact.runner.buildOutput.path.replace('/home/project', '');
- }
-
- // Collect files for deployment
- async function getAllFiles(dirPath: string): Promise> {
- const files: Record = {};
-
- try {
- const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = path.join(dirPath, entry.name);
-
- // Skip node_modules and other build artifacts
- if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.cache') {
- continue;
- }
-
- if (entry.isFile()) {
- try {
- const content = await container.fs.readFile(fullPath, 'utf-8');
- const deployPath = fullPath.replace(outputPath, '');
- files[deployPath] = content;
- } catch (e) {
- console.warn(`Could not read file ${fullPath}:`, e);
- }
- } else if (entry.isDirectory()) {
- const subFiles = await getAllFiles(fullPath);
- Object.assign(files, subFiles);
- }
- }
- } catch (e) {
- console.error(`Error reading directory ${dirPath}:`, e);
- }
-
- return files;
- }
-
- const fileContents = await getAllFiles(outputPath);
-
- // Create netlify.toml configuration
- const netlifyConfig = `
-[build]
- publish = "${config.outputDirectory || '/'}"
- ${config.buildCommand ? `command = "${config.buildCommand}"` : ''}
-
-[build.environment]
- NODE_VERSION = "${config.nodeVersion}"
-
-[[redirects]]
- from = "/*"
- to = "/index.html"
- status = 200
-
-[[headers]]
- for = "/*"
- [headers.values]
- X-Frame-Options = "DENY"
- X-XSS-Protection = "1; mode=block"
- X-Content-Type-Options = "nosniff"
-`;
-
- fileContents['/netlify.toml'] = netlifyConfig;
-
- // Deploy to Netlify using the quick deploy endpoint
- const response = await fetch('/api/netlify-quick-deploy', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- files: fileContents,
- chatId: currentChatId,
- framework: config.framework,
- }),
- });
-
- const data = (await response.json()) as { success: boolean; url?: string; siteId?: string; error?: string };
-
- if (!response.ok || !data.success) {
- deployArtifact.runner.handleDeployAction('deploying', 'failed', {
- error: data.error || 'Deployment failed',
- source: 'netlify',
- });
- throw new Error(data.error || 'Deployment failed');
- }
-
- // Deployment successful
- setDeployUrl(data.url || null);
-
- deployArtifact.runner.handleDeployAction('complete', 'complete', {
- url: data.url || '',
- source: 'netlify',
- });
-
- toast.success('Deployment successful! Your app is live.');
-
- // Store deployment info
- if (data.siteId) {
- localStorage.setItem(`netlify-quick-site-${currentChatId}`, data.siteId);
- }
-
- return data.url || null;
- } catch (error) {
- console.error('Quick deploy error:', error);
- toast.error(error instanceof Error ? error.message : 'Deployment failed');
-
- return null;
- } finally {
- setIsDeploying(false);
- }
- };
-
- return (
-
-
-
-
- Deploy your project to Netlify instantly with automatic framework detection and configuration.
-
-
-
- {isDeploying ? (
- <>
-
- Deploying...
- >
- ) : (
- <>
-
- Deploy Now
- >
- )}
-
-
- {deployUrl && (
-
- )}
-
-
setShowAdvanced(!showAdvanced)}
- className="text-sm text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors flex items-center gap-1"
- >
-
- Advanced Options
-
-
- {showAdvanced && (
-
-
• Automatic framework detection (React, Vue, Next.js, etc.)
-
• Smart build command configuration
-
• Optimized output directory selection
-
• SSL/HTTPS enabled by default
-
• Global CDN distribution
-
- )}
-
- );
-}
diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx
index ccca5e105e..1d509ce82e 100644
--- a/app/components/header/Header.tsx
+++ b/app/components/header/Header.tsx
@@ -5,7 +5,7 @@ import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
-export function Header({ children }: { children?: React.ReactNode }) {
+export function Header() {
const chat = useStore(chatStore);
return (
@@ -37,8 +37,6 @@ export function Header({ children }: { children?: React.ReactNode }) {
>
)}
- {!chat.started &&
}
- {children}
);
}
diff --git a/app/components/header/HeaderActionButtons.client.tsx b/app/components/header/HeaderActionButtons.client.tsx
index 0791627b13..41d6354ed6 100644
--- a/app/components/header/HeaderActionButtons.client.tsx
+++ b/app/components/header/HeaderActionButtons.client.tsx
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { useStore } from '@nanostores/react';
import { workbenchStore } from '~/lib/stores/workbench';
import { DeployButton } from '~/components/deploy/DeployButton';
-import { MultiUserToggle } from '~/components/multiuser/MultiUserToggle';
interface HeaderActionButtonsProps {
chatStarted: boolean;
@@ -16,10 +15,7 @@ export function HeaderActionButtons({ chatStarted: _chatStarted }: HeaderActionB
const shouldShowButtons = activePreview;
return (
-
- {/* Multi-User Sessions Toggle (Bolt.gives Exclusive) */}
-
-
+
{/* Deploy Button */}
{shouldShowButtons &&
}
diff --git a/app/components/header/UserMenu.tsx b/app/components/header/UserMenu.tsx
deleted file mode 100644
index a61f30cfaf..0000000000
--- a/app/components/header/UserMenu.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import { useState, useRef, useEffect } from 'react';
-import { useNavigate } from '@remix-run/react';
-import { useStore } from '@nanostores/react';
-import { authStore, logout } from '~/lib/stores/auth';
-import { motion, AnimatePresence } from 'framer-motion';
-import { classNames } from '~/utils/classNames';
-
-export function UserMenu() {
- const navigate = useNavigate();
- const authState = useStore(authStore);
- const [isOpen, setIsOpen] = useState(false);
- const menuRef = useRef
(null);
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsOpen(false);
- }
- }
-
- document.addEventListener('mousedown', handleClickOutside);
-
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- const handleLogout = async () => {
- await logout();
- navigate('/auth');
- };
-
- const handleManageUsers = () => {
- setIsOpen(false);
- navigate('/admin/users');
- };
-
- const handleSettings = () => {
- setIsOpen(false);
-
- // Open settings modal or navigate to settings
- };
-
- if (!authState.isAuthenticated || !authState.user) {
- return null;
- }
-
- return (
-
- {/* User Avatar Button */}
-
setIsOpen(!isOpen)}
- className={classNames(
- 'flex items-center gap-2 px-3 py-2 rounded-lg',
- 'hover:bg-bolt-elements-background-depth-2',
- 'transition-colors',
- )}
- >
-
- {authState.user.avatar ? (
-
- ) : (
-
- {authState.user.firstName[0].toUpperCase()}
-
- )}
-
-
-
{authState.user.firstName}
-
@{authState.user.username}
-
-
-
-
- {/* Dropdown Menu */}
-
- {isOpen && (
-
- {/* User Info */}
-
-
-
- {authState.user.avatar ? (
-
- ) : (
-
- {authState.user.firstName[0].toUpperCase()}
-
- )}
-
-
-
{authState.user.firstName}
-
@{authState.user.username}
-
-
-
-
- {/* Menu Items */}
-
-
-
- Settings
-
-
-
-
- Manage Users
-
-
-
-
-
-
- Sign Out
-
-
-
- {/* Footer */}
-
-
- Member since {new Date(authState.user.createdAt).toLocaleDateString()}
-
-
-
- )}
-
-
- );
-}
diff --git a/app/components/import-project/ImportProjectButton.tsx b/app/components/import-project/ImportProjectButton.tsx
deleted file mode 100644
index b932785197..0000000000
--- a/app/components/import-project/ImportProjectButton.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import { ImportProjectDialog } from './ImportProjectDialog';
-import { workbenchStore } from '~/lib/stores/workbench';
-import { toast } from 'react-toastify';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-export const ImportProjectButton: React.FC = () => {
- const [isDialogOpen, setIsDialogOpen] = useState(false);
-
- // Add keyboard shortcut
- useHotkeys('ctrl+shift+i, cmd+shift+i', (e) => {
- e.preventDefault();
- setIsDialogOpen(true);
- });
-
- const handleImport = useCallback(async (files: Map) => {
- try {
- console.log('[ImportProject] Starting import of', files.size, 'files');
-
- // Add files to workbench
- for (const [path, content] of files.entries()) {
- // Ensure path starts with /
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
-
- console.log('[ImportProject] Adding file:', normalizedPath);
-
- // Add file to workbench file system
- workbenchStore.files.setKey(normalizedPath, {
- type: 'file',
- content,
- isBinary: false,
- });
- }
-
- // Open the first file in the editor if any
- const firstFile = Array.from(files.keys())[0];
-
- if (firstFile) {
- const normalizedPath = firstFile.startsWith('/') ? firstFile : `/${firstFile}`;
- workbenchStore.setSelectedFile(normalizedPath);
- }
-
- toast.success(`Successfully imported ${files.size} files`, {
- position: 'bottom-right',
- autoClose: 3000,
- });
-
- setIsDialogOpen(false);
- } catch (error) {
- console.error('[ImportProject] Import failed:', error);
- toast.error('Failed to import project files', {
- position: 'bottom-right',
- autoClose: 5000,
- });
- }
- }, []);
-
- return (
- <>
- setIsDialogOpen(true)}
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text transition-colors duration-200"
- title="Import existing project (Ctrl+Shift+I)"
- >
-
- Import Project
-
-
- setIsDialogOpen(false)} onImport={handleImport} />
- >
- );
-};
diff --git a/app/components/import-project/ImportProjectDialog.tsx b/app/components/import-project/ImportProjectDialog.tsx
deleted file mode 100644
index f58704fe06..0000000000
--- a/app/components/import-project/ImportProjectDialog.tsx
+++ /dev/null
@@ -1,567 +0,0 @@
-import React, { useState, useCallback, useRef } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import JSZip from 'jszip';
-import { toast } from 'react-toastify';
-import * as RadixDialog from '@radix-ui/react-dialog';
-import { Dialog, DialogTitle, DialogDescription } from '~/components/ui/Dialog';
-import { classNames } from '~/utils/classNames';
-
-interface ImportProjectDialogProps {
- isOpen: boolean;
- onClose: () => void;
- onImport?: (files: Map) => void;
-}
-
-interface FileStructure {
- [path: string]: string | ArrayBuffer;
-}
-
-interface ImportStats {
- totalFiles: number;
- totalSize: number;
- fileTypes: Map;
- directories: Set;
-}
-
-const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB max per file
-const MAX_TOTAL_SIZE = 200 * 1024 * 1024; // 200MB max total
-
-const IGNORED_PATTERNS = [
- /node_modules\//,
- /\.git\//,
- /\.next\//,
- /dist\//,
- /build\//,
- /\.cache\//,
- /\.vscode\//,
- /\.idea\//,
- /\.DS_Store$/,
- /Thumbs\.db$/,
- /\.env\.local$/,
- /\.env\.production$/,
-];
-
-const BINARY_EXTENSIONS = [
- '.png',
- '.jpg',
- '.jpeg',
- '.gif',
- '.webp',
- '.svg',
- '.ico',
- '.pdf',
- '.zip',
- '.tar',
- '.gz',
- '.rar',
- '.mp3',
- '.mp4',
- '.avi',
- '.mov',
- '.exe',
- '.dll',
- '.so',
- '.dylib',
- '.woff',
- '.woff2',
- '.ttf',
- '.eot',
-];
-
-export const ImportProjectDialog: React.FC = ({ isOpen, onClose, onImport }) => {
- const [isDragging, setIsDragging] = useState(false);
- const [isProcessing, setIsProcessing] = useState(false);
- const [importProgress, setImportProgress] = useState(0);
- const [importStats, setImportStats] = useState(null);
- const [selectedFiles, setSelectedFiles] = useState({});
- const [errorMessage, setErrorMessage] = useState(null);
- const fileInputRef = useRef(null);
- const dropZoneRef = useRef(null);
-
- const resetState = useCallback(() => {
- setSelectedFiles({});
- setImportStats(null);
- setImportProgress(0);
- setErrorMessage(null);
- setIsProcessing(false);
- }, []);
-
- const shouldIgnoreFile = (path: string): boolean => {
- return IGNORED_PATTERNS.some((pattern) => pattern.test(path));
- };
-
- const isBinaryFile = (filename: string): boolean => {
- return BINARY_EXTENSIONS.some((ext) => filename.toLowerCase().endsWith(ext));
- };
-
- const formatFileSize = (bytes: number): string => {
- if (bytes < 1024) {
- return `${bytes} B`;
- }
-
- if (bytes < 1024 * 1024) {
- return `${(bytes / 1024).toFixed(2)} KB`;
- }
-
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
- };
-
- const processZipFile = async (file: File): Promise => {
- const zip = new JSZip();
- const zipData = await zip.loadAsync(file);
- const files: FileStructure = {};
- const stats: ImportStats = {
- totalFiles: 0,
- totalSize: 0,
- fileTypes: new Map(),
- directories: new Set(),
- };
-
- const filePromises: Promise[] = [];
-
- zipData.forEach((relativePath, zipEntry) => {
- if (!zipEntry.dir && !shouldIgnoreFile(relativePath)) {
- const promise = (async () => {
- try {
- const content = await zipEntry.async(isBinaryFile(relativePath) ? 'arraybuffer' : 'string');
- files[relativePath] = content;
-
- stats.totalFiles++;
-
- // Use a safe method to get uncompressed size
- const size = (zipEntry as any)._data?.uncompressedSize || 0;
- stats.totalSize += size;
-
- const ext = relativePath.split('.').pop() || 'unknown';
- stats.fileTypes.set(ext, (stats.fileTypes.get(ext) || 0) + 1);
-
- const dir = relativePath.substring(0, relativePath.lastIndexOf('/'));
-
- if (dir) {
- stats.directories.add(dir);
- }
-
- setImportProgress((prev) => Math.min(prev + 100 / Object.keys(zipData.files).length, 100));
- } catch (err) {
- console.error(`Failed to process ${relativePath}:`, err);
- }
- })();
- filePromises.push(promise);
- }
- });
-
- await Promise.all(filePromises);
- setImportStats(stats);
-
- return files;
- };
-
- const processFileList = async (fileList: FileList): Promise => {
- const files: FileStructure = {};
- const stats: ImportStats = {
- totalFiles: 0,
- totalSize: 0,
- fileTypes: new Map(),
- directories: new Set(),
- };
-
- let totalSize = 0;
-
- for (let i = 0; i < fileList.length; i++) {
- const file = fileList[i];
- const path = (file as any).webkitRelativePath || file.name;
-
- if (shouldIgnoreFile(path)) {
- continue;
- }
-
- if (file.size > MAX_FILE_SIZE) {
- toast.warning(`Skipping ${file.name}: File too large (${formatFileSize(file.size)})`);
- continue;
- }
-
- totalSize += file.size;
-
- if (totalSize > MAX_TOTAL_SIZE) {
- toast.error('Total size exceeds 200MB limit');
- break;
- }
-
- try {
- const content = await (isBinaryFile(file.name) ? file.arrayBuffer() : file.text());
-
- files[path] = content;
- stats.totalFiles++;
- stats.totalSize += file.size;
-
- const ext = file.name.split('.').pop() || 'unknown';
- stats.fileTypes.set(ext, (stats.fileTypes.get(ext) || 0) + 1);
-
- const dir = path.substring(0, path.lastIndexOf('/'));
-
- if (dir) {
- stats.directories.add(dir);
- }
-
- setImportProgress(((i + 1) / fileList.length) * 100);
- } catch (err) {
- console.error(`Failed to read ${file.name}:`, err);
- }
- }
-
- setImportStats(stats);
-
- return files;
- };
-
- const handleFileSelect = async (event: React.ChangeEvent) => {
- const files = event.target.files;
-
- if (!files || files.length === 0) {
- return;
- }
-
- setIsProcessing(true);
- setErrorMessage(null);
- setImportProgress(0);
-
- try {
- let processedFiles: FileStructure = {};
-
- if (files.length === 1 && files[0].name.endsWith('.zip')) {
- processedFiles = await processZipFile(files[0]);
- } else {
- processedFiles = await processFileList(files);
- }
-
- if (Object.keys(processedFiles).length === 0) {
- toast.warning('No valid files found to import');
- setIsProcessing(false);
-
- return;
- }
-
- setSelectedFiles(processedFiles);
- toast.info(`Ready to import ${Object.keys(processedFiles).length} files`);
- } catch (error) {
- console.error('Error processing files:', error);
- setErrorMessage(error instanceof Error ? error.message : 'Failed to process files');
- toast.error('Failed to process files');
- } finally {
- setIsProcessing(false);
- setImportProgress(0);
- }
- };
-
- const handleDrop = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDragging(false);
-
- const files = e.dataTransfer.files;
-
- if (files.length > 0) {
- const input = fileInputRef.current;
-
- if (input) {
- const dataTransfer = new DataTransfer();
- Array.from(files).forEach((file) => dataTransfer.items.add(file));
- input.files = dataTransfer.files;
- handleFileSelect({ target: input } as any);
- }
- }
- }, []);
-
- const handleDragOver = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDragging(true);
- }, []);
-
- const handleDragLeave = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- if (e.currentTarget === e.target) {
- setIsDragging(false);
- }
- }, []);
-
- const getFileExtension = (filename: string): string => {
- const parts = filename.split('.');
- return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'file';
- };
-
- const getFileIcon = (filename: string): string => {
- const ext = getFileExtension(filename);
- const iconMap: { [key: string]: string } = {
- js: 'i-vscode-icons:file-type-js',
- jsx: 'i-vscode-icons:file-type-reactjs',
- ts: 'i-vscode-icons:file-type-typescript',
- tsx: 'i-vscode-icons:file-type-reactts',
- css: 'i-vscode-icons:file-type-css',
- scss: 'i-vscode-icons:file-type-scss',
- html: 'i-vscode-icons:file-type-html',
- json: 'i-vscode-icons:file-type-json',
- md: 'i-vscode-icons:file-type-markdown',
- py: 'i-vscode-icons:file-type-python',
- vue: 'i-vscode-icons:file-type-vue',
- svg: 'i-vscode-icons:file-type-svg',
- git: 'i-vscode-icons:file-type-git',
- folder: 'i-vscode-icons:default-folder',
- };
-
- return iconMap[ext] || 'i-vscode-icons:default-file';
- };
-
- const handleImportClick = useCallback(async () => {
- if (Object.keys(selectedFiles).length === 0) {
- return;
- }
-
- setIsProcessing(true);
-
- try {
- const fileMap = new Map();
-
- for (const [path, content] of Object.entries(selectedFiles)) {
- if (typeof content === 'string') {
- fileMap.set(path, content);
- } else if (content instanceof ArrayBuffer) {
- // Convert ArrayBuffer to base64 string for binary files
- const bytes = new Uint8Array(content);
- const binary = String.fromCharCode(...bytes);
- const base64 = btoa(binary);
- fileMap.set(path, base64);
- }
- }
-
- if (onImport) {
- // Use the provided onImport callback
- await onImport(fileMap);
- }
-
- toast.success(`Successfully imported ${importStats?.totalFiles || 0} files`, {
- position: 'bottom-right',
- autoClose: 3000,
- });
-
- resetState();
- onClose();
- } catch (error) {
- toast.error('Failed to import project', { position: 'bottom-right' });
- setErrorMessage(error instanceof Error ? error.message : 'Import failed');
- } finally {
- setIsProcessing(false);
- }
- }, [selectedFiles, importStats, onImport, onClose, resetState]);
-
- return (
- !open && onClose()}>
-
-
-
-
- Import Existing Project
-
-
- Upload your project files or drag and drop them here. Supports individual files, folders, or ZIP archives.
-
-
-
-
- {!Object.keys(selectedFiles).length ? (
-
-
-
-
-
-
-
-
-
-
-
- {isDragging ? 'Drop your project here' : 'Drag & Drop your project'}
-
-
- Support for folders, multiple files, or ZIP archives
-
-
-
-
- fileInputRef.current?.click()}
- disabled={isProcessing}
- className="px-6 py-2.5 bg-accent-500 hover:bg-accent-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Browse Files
-
- {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.zip';
-
- input.onchange = (e) => {
- const target = e.target as HTMLInputElement;
-
- if (target.files) {
- handleFileSelect({ target } as any);
- }
- };
- input.click();
- }}
- disabled={isProcessing}
- className="px-6 py-2.5 bg-transparent border border-bolt-elements-borderColor hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textPrimary rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Upload ZIP
-
-
-
-
- {isProcessing && (
-
-
-
-
- Processing files... {Math.round(importProgress)}%
-
-
-
- )}
-
-
- {errorMessage && (
-
- {errorMessage}
-
- )}
-
- ) : (
-
- {importStats && (
-
-
-
Total Files
-
{importStats.totalFiles}
-
-
-
Total Size
-
- {formatFileSize(importStats.totalSize)}
-
-
-
-
Directories
-
- {importStats.directories.size}
-
-
-
- )}
-
-
-
-
Files to Import
-
-
- {Object.keys(selectedFiles)
- .slice(0, 50)
- .map((path, index) => (
-
- ))}
- {Object.keys(selectedFiles).length > 50 && (
-
- ... and {Object.keys(selectedFiles).length - 50} more files
-
- )}
-
-
-
-
- {
- resetState();
- }}
- className="px-4 py-2 text-sm text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
- >
- Clear Selection
-
-
- Cancel
-
-
- {isProcessing ? (
- <>
-
- Importing...
- >
- ) : (
- `Import ${Object.keys(selectedFiles).length} Files`
- )}
-
-
-
- )}
-
-
-
-
-
- );
-};
diff --git a/app/components/multiuser/MultiUserSessionManager.tsx b/app/components/multiuser/MultiUserSessionManager.tsx
deleted file mode 100644
index 093dc316b7..0000000000
--- a/app/components/multiuser/MultiUserSessionManager.tsx
+++ /dev/null
@@ -1,346 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { classNames } from '~/utils/classNames';
-import { Dialog } from '~/components/ui/Dialog';
-import * as RadixDialog from '@radix-ui/react-dialog';
-import { Button } from '~/components/ui/Button';
-import { toast } from 'react-toastify';
-import Cookies from 'js-cookie';
-
-interface User {
- id: string;
- email: string;
- name: string;
- role: 'admin' | 'developer' | 'viewer' | 'guest';
- status: 'active' | 'idle' | 'offline';
- lastActivity: string;
- avatar?: string;
-}
-
-interface Session {
- userId: string;
- sessionId: string;
- startTime: string;
- lastActivity: string;
- ipAddress: string;
- device: string;
-}
-
-export const MultiUserSessionManager: React.FC = () => {
- const [isOpen, setIsOpen] = useState(false);
- const [activeUsers, setActiveUsers] = useState([]);
- const [sessions, setSessions] = useState([]);
- const [currentUser, setCurrentUser] = useState(null);
- const [inviteEmail, setInviteEmail] = useState('');
- const [inviteRole, setInviteRole] = useState<'developer' | 'viewer'>('developer');
-
- useEffect(() => {
- loadSessionData();
-
- const interval = setInterval(loadSessionData, 5000);
-
- // Refresh every 5 seconds
- return () => clearInterval(interval);
- }, []);
-
- const loadSessionData = async () => {
- try {
- // Get current user
- const token = Cookies.get('auth_token');
-
- if (token) {
- const userResponse = await fetch('/api/auth/verify', {
- headers: { Authorization: `Bearer ${token}` },
- });
-
- if (userResponse.ok) {
- const userData = await userResponse.json();
- setCurrentUser(userData as User);
- }
- }
-
- // Get active users (mock data for demo)
- const mockUsers: User[] = [
- {
- id: '1',
- email: 'admin@example.com',
- name: 'Admin User',
- role: 'admin',
- status: 'active',
- lastActivity: new Date().toISOString(),
- },
- {
- id: '2',
- email: 'dev@example.com',
- name: 'Developer',
- role: 'developer',
- status: 'idle',
- lastActivity: new Date(Date.now() - 5 * 60000).toISOString(),
- },
- ];
- setActiveUsers(mockUsers);
-
- // Get active sessions (mock data for demo)
- const mockSessions: Session[] = [
- {
- userId: '1',
- sessionId: 'session-1',
- startTime: new Date(Date.now() - 30 * 60000).toISOString(),
- lastActivity: new Date().toISOString(),
- ipAddress: '192.168.1.1',
- device: 'Chrome on Windows',
- },
- {
- userId: '2',
- sessionId: 'session-2',
- startTime: new Date(Date.now() - 60 * 60000).toISOString(),
- lastActivity: new Date(Date.now() - 5 * 60000).toISOString(),
- ipAddress: '192.168.1.2',
- device: 'Safari on Mac',
- },
- ];
- setSessions(mockSessions);
- } catch (error) {
- console.error('Failed to load session data:', error);
- }
- };
-
- const handleInviteUser = async () => {
- if (!inviteEmail.trim()) {
- toast.error('Please enter an email address');
- return;
- }
-
- try {
- // Send invitation
- const response = await fetch('/api/users', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- email: inviteEmail,
- role: inviteRole,
- action: 'invite',
- }),
- });
-
- if (response.ok) {
- toast.success(`Invitation sent to ${inviteEmail}`);
- setInviteEmail('');
- } else {
- toast.error('Failed to send invitation');
- }
- } catch (error) {
- console.error('Invite error:', error);
- toast.error('Failed to send invitation');
- }
- };
-
- const handleRemoveUser = async (userId: string) => {
- if (!window.confirm('Are you sure you want to remove this user?')) {
- return;
- }
-
- try {
- const response = await fetch(`/api/users/${userId}`, {
- method: 'DELETE',
- });
-
- if (response.ok) {
- toast.success('User removed successfully');
- loadSessionData();
- } else {
- toast.error('Failed to remove user');
- }
- } catch (error) {
- console.error('Remove user error:', error);
- toast.error('Failed to remove user');
- }
- };
-
- /**
- * const handleTerminateSession = async (_sessionId: string) => {
- * if (!window.confirm('Are you sure you want to terminate this session?')) {
- * return;
- * }
- *
- * try {
- * // Terminate session
- * toast.success('Session terminated');
- * loadSessionData();
- * } catch (error) {
- * console.error('Terminate session error:', error);
- * toast.error('Failed to terminate session');
- * }
- * };
- */
-
- const getRoleBadgeColor = (role: string) => {
- switch (role) {
- case 'admin':
- return 'bg-red-500/20 text-red-400 border-red-500/30';
- case 'developer':
- return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
- case 'viewer':
- return 'bg-green-500/20 text-green-400 border-green-500/30';
- case 'guest':
- return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
- default:
- return 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary';
- }
- };
-
- const getStatusIcon = (status: string) => {
- switch (status) {
- case 'active':
- return 'i-ph:circle-fill text-green-400';
- case 'idle':
- return 'i-ph:circle-fill text-yellow-400';
- case 'offline':
- return 'i-ph:circle-fill text-gray-400';
- default:
- return 'i-ph:circle text-gray-400';
- }
- };
-
- const formatTimeAgo = (dateString: string) => {
- const date = new Date(dateString);
- const now = new Date();
- const diff = Math.floor((now.getTime() - date.getTime()) / 1000);
-
- if (diff < 60) {
- return 'Just now';
- }
-
- if (diff < 3600) {
- return `${Math.floor(diff / 60)} min ago`;
- }
-
- if (diff < 86400) {
- return `${Math.floor(diff / 3600)} hours ago`;
- }
-
- return `${Math.floor(diff / 86400)} days ago`;
- };
-
- const multiUserEnabled = localStorage.getItem('multiUserEnabled') === 'true';
-
- if (!multiUserEnabled) {
- return null;
- }
-
- return (
- <>
- setIsOpen(true)}
- className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor hover:bg-bolt-elements-background-depth-2 transition-all"
- title="Manage Sessions"
- >
-
- {activeUsers.length} Active
-
-
- {isOpen && (
-
- setIsOpen(false)}>
-
-
Multi-User Session Manager
-
- {/* Tabs */}
-
-
- Active Users ({activeUsers.length})
-
-
- Sessions ({sessions.length})
-
-
- Invite Users
-
-
-
- {/* Active Users List */}
-
- {activeUsers.map((user) => (
-
-
- {/* Avatar */}
-
- {user.name.charAt(0).toUpperCase()}
-
-
- {/* User Info */}
-
-
- {user.name}
-
-
- {user.role}
-
-
-
- {user.email}
-
- Active {formatTimeAgo(user.lastActivity)}
-
-
-
-
-
- {/* Actions */}
- {currentUser?.role === 'admin' && user.id !== currentUser.id && (
-
- handleRemoveUser(user.id)}
- className="p-2 text-red-400 hover:bg-red-500/10 rounded-lg transition-all"
- title="Remove User"
- >
-
-
-
- )}
-
- ))}
-
-
- {/* Invite User Section */}
-
-
Invite New User
-
- setInviteEmail(e.target.value)}
- placeholder="Enter email address"
- className="flex-1 px-3 py-1.5 text-sm bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary"
- />
- setInviteRole(e.target.value as 'developer' | 'viewer')}
- className="px-3 py-1.5 text-sm bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg text-bolt-elements-textPrimary"
- >
- Developer
- Viewer
-
-
- Send Invite
-
-
-
-
-
-
- )}
- >
- );
-};
diff --git a/app/components/multiuser/MultiUserToggle.tsx b/app/components/multiuser/MultiUserToggle.tsx
deleted file mode 100644
index 19ceeb5e32..0000000000
--- a/app/components/multiuser/MultiUserToggle.tsx
+++ /dev/null
@@ -1,399 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { classNames } from '~/utils/classNames';
-import { Dialog } from '~/components/ui/Dialog';
-import * as RadixDialog from '@radix-ui/react-dialog';
-import { Button } from '~/components/ui/Button';
-import { Input } from '~/components/ui/Input';
-import { toast } from 'react-toastify';
-import { MultiUserSessionManager } from './MultiUserSessionManager';
-
-interface MultiUserToggleProps {
- className?: string;
-}
-
-export const MultiUserToggle: React.FC = ({ className }) => {
- const [isEnabled, setIsEnabled] = useState(false);
- const [showWizard, setShowWizard] = useState(false);
- const [currentStep, setCurrentStep] = useState(1);
- const [organizationName, setOrganizationName] = useState('');
- const [adminEmail, setAdminEmail] = useState('');
- const [adminPassword, setAdminPassword] = useState('');
- const [maxUsers, setMaxUsers] = useState('10');
- const [sessionTimeout, setSessionTimeout] = useState('30');
- const [allowGuestAccess, setAllowGuestAccess] = useState(false);
-
- // Check if this is bolt.gives (exclusive feature)
- const isBoltGives = window.location.hostname === 'bolt.openweb.live' || window.location.hostname === 'localhost';
-
- useEffect(() => {
- // Check if multi-user is already enabled
- const multiUserEnabled = localStorage.getItem('multiUserEnabled') === 'true';
- setIsEnabled(multiUserEnabled);
- }, []);
-
- const handleToggle = () => {
- if (!isBoltGives) {
- toast.error('Multi-User Sessions is a Bolt.gives exclusive feature');
- return;
- }
-
- if (!isEnabled) {
- // Show wizard to set up multi-user
- setShowWizard(true);
- setCurrentStep(1);
- } else {
- // Confirm disable
- if (window.confirm('Are you sure you want to disable Multi-User Sessions?')) {
- setIsEnabled(false);
- localStorage.setItem('multiUserEnabled', 'false');
- toast.success('Multi-User Sessions disabled');
- }
- }
- };
-
- const handleNextStep = () => {
- if (currentStep === 1) {
- if (!organizationName.trim()) {
- toast.error('Please enter an organization name');
- return;
- }
- } else if (currentStep === 2) {
- if (!adminEmail.trim() || !adminPassword.trim()) {
- toast.error('Please enter admin credentials');
- return;
- }
-
- if (adminPassword.length < 8) {
- toast.error('Password must be at least 8 characters');
- return;
- }
- }
-
- if (currentStep < 4) {
- setCurrentStep(currentStep + 1);
- } else {
- // Complete setup
- handleCompleteSetup();
- }
- };
-
- const handleCompleteSetup = async () => {
- try {
- // Save configuration
- const config = {
- organizationName,
- adminEmail,
- maxUsers: parseInt(maxUsers),
- sessionTimeout: parseInt(sessionTimeout),
- allowGuestAccess,
- enabled: true,
- setupDate: new Date().toISOString(),
- };
-
- // Store in localStorage (in production, this would be server-side)
- localStorage.setItem('multiUserConfig', JSON.stringify(config));
- localStorage.setItem('multiUserEnabled', 'true');
-
- // Create admin user
- const response = await fetch('/api/auth/signup', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- email: adminEmail,
- password: adminPassword,
- role: 'admin',
- organization: organizationName,
- }),
- });
-
- if (response.ok) {
- setIsEnabled(true);
- setShowWizard(false);
- toast.success('Multi-User Sessions enabled successfully!');
-
- // Auto-login the admin
- const loginResponse = await fetch('/api/auth/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- email: adminEmail,
- password: adminPassword,
- }),
- });
-
- if (loginResponse.ok) {
- window.location.reload();
- }
- } else {
- const error = (await response.json()) as { message?: string };
- toast.error(error.message || 'Failed to setup Multi-User Sessions');
- }
- } catch (error) {
- console.error('Setup error:', error);
-
- const errorMessage = error instanceof Error ? error.message : 'Failed to setup Multi-User Sessions';
- toast.error(errorMessage);
- }
- };
-
- if (!isBoltGives) {
- return null; // Feature not available for non-bolt.gives deployments
- }
-
- // If multi-user is enabled, show the session manager instead
- if (isEnabled) {
- return (
-
-
-
-
-
-
- );
- }
-
- return (
- <>
-
-
-
- {isEnabled ? 'Multi-User' : 'Single User'}
-
-
-
- {showWizard && (
-
- setShowWizard(false)}>
-
-
-
Setup Multi-User Sessions
- Step {currentStep} of 4
-
-
- {/* Progress Bar */}
-
- {[1, 2, 3, 4].map((step) => (
-
- ))}
-
-
- {/* Step 1: Organization Setup */}
- {currentStep === 1 && (
-
-
-
Organization Setup
-
- Configure your organization for multi-user collaboration
-
-
-
-
-
- Organization Name
-
- setOrganizationName(e.target.value)}
- placeholder="e.g., Acme Corp"
- className="w-full"
- />
-
-
- )}
-
- {/* Step 2: Admin Account */}
- {currentStep === 2 && (
-
-
-
Admin Account
-
- Create the administrator account for managing users
-
-
-
-
- Admin Email
- setAdminEmail(e.target.value)}
- placeholder="admin@example.com"
- className="w-full"
- />
-
-
-
-
- Admin Password
-
- setAdminPassword(e.target.value)}
- placeholder="Minimum 8 characters"
- className="w-full"
- />
-
-
- )}
-
- {/* Step 3: Session Settings */}
- {currentStep === 3 && (
-
-
-
Session Settings
-
- Configure session limits and security
-
-
-
-
-
- Maximum Concurrent Users
-
- setMaxUsers(e.target.value)}
- min="2"
- max="100"
- className="w-full"
- />
-
-
-
-
- Session Timeout (minutes)
-
- setSessionTimeout(e.target.value)}
- min="5"
- max="1440"
- className="w-full"
- />
-
-
-
-
- setAllowGuestAccess(e.target.checked)}
- className="rounded border-bolt-elements-borderColor"
- />
- Allow guest access (read-only)
-
-
-
- )}
-
- {/* Step 4: Review & Confirm */}
- {currentStep === 4 && (
-
-
-
Review Configuration
-
- Please review your settings before enabling Multi-User Sessions
-
-
-
-
-
- Organization:
- {organizationName}
-
-
- Admin Email:
- {adminEmail}
-
-
- Max Users:
- {maxUsers}
-
-
- Session Timeout:
- {sessionTimeout} minutes
-
-
- Guest Access:
-
- {allowGuestAccess ? 'Enabled' : 'Disabled'}
-
-
-
-
-
-
- Note: Multi-User Sessions is a Bolt.gives exclusive
- feature. You can manage users, sessions, and permissions from the admin panel after setup.
-
-
-
- )}
-
- {/* Actions */}
-
- {
- if (currentStep > 1) {
- setCurrentStep(currentStep - 1);
- } else {
- setShowWizard(false);
- }
- }}
- >
- {currentStep === 1 ? 'Cancel' : 'Back'}
-
-
-
- {currentStep === 4 ? 'Enable Multi-User' : 'Next'}
-
-
-
-
-
- )}
- >
- );
-};
diff --git a/app/components/workbench/AutoSaveSettings.tsx b/app/components/workbench/AutoSaveSettings.tsx
deleted file mode 100644
index 455d102316..0000000000
--- a/app/components/workbench/AutoSaveSettings.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-import { memo, useState, useEffect } from 'react';
-import * as Dialog from '@radix-ui/react-dialog';
-import * as Switch from '@radix-ui/react-switch';
-import * as Slider from '@radix-ui/react-slider';
-import { classNames } from '~/utils/classNames';
-import { motion, AnimatePresence } from 'framer-motion';
-
-interface AutoSaveSettingsProps {
- onSettingsChange?: (settings: AutoSaveConfig) => void;
- trigger?: React.ReactNode;
-}
-
-export interface AutoSaveConfig {
- enabled: boolean;
- interval: number; // in seconds
- minChanges: number;
- saveOnBlur: boolean;
- saveBeforeRun: boolean;
- showNotifications: boolean;
-}
-
-const DEFAULT_CONFIG: AutoSaveConfig = {
- enabled: false,
- interval: 30,
- minChanges: 1,
- saveOnBlur: true,
- saveBeforeRun: true,
- showNotifications: true,
-};
-
-const PRESET_INTERVALS = [
- { label: '10s', value: 10 },
- { label: '30s', value: 30 },
- { label: '1m', value: 60 },
- { label: '2m', value: 120 },
- { label: '5m', value: 300 },
-];
-
-export const AutoSaveSettings = memo(({ onSettingsChange, trigger }: AutoSaveSettingsProps) => {
- const [isOpen, setIsOpen] = useState(false);
- const [config, setConfig] = useState(() => {
- // Load from localStorage if available
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('bolt-autosave-config');
-
- if (saved) {
- try {
- return JSON.parse(saved);
- } catch {
- // Invalid JSON, use defaults
- }
- }
- }
-
- return DEFAULT_CONFIG;
- });
-
- // Save to localStorage whenever config changes
- useEffect(() => {
- if (typeof window !== 'undefined') {
- localStorage.setItem('bolt-autosave-config', JSON.stringify(config));
- }
-
- onSettingsChange?.(config);
- }, [config, onSettingsChange]);
-
- const updateConfig = (key: K, value: AutoSaveConfig[K]) => {
- setConfig((prev) => ({ ...prev, [key]: value }));
- };
-
- return (
-
-
- {trigger || (
-
-
- Auto-save Settings
-
- )}
-
-
-
- {isOpen && (
-
-
-
-
-
-
-
-
- {/* Header */}
-
-
- Auto-save Settings
-
-
-
-
-
-
- {/* Content */}
-
- {/* Enable/Disable Auto-save */}
-
-
-
Enable Auto-save
-
- Automatically save files at regular intervals
-
-
-
updateConfig('enabled', checked)}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- config.enabled ? 'bg-accent-500' : 'bg-bolt-elements-background-depth-3',
- )}
- >
-
-
-
-
- {/* Save Interval */}
-
-
-
- Save Interval: {config.interval}s
-
-
How often to save changes
-
-
-
updateConfig('interval', value)}
- min={5}
- max={300}
- step={5}
- className="relative flex items-center select-none touch-none w-full h-5"
- >
-
-
-
-
-
-
- {/* Preset buttons */}
-
- {PRESET_INTERVALS.map((preset) => (
- updateConfig('interval', preset.value)}
- className={classNames(
- 'px-2 py-1 text-xs rounded-md transition-colors',
- config.interval === preset.value
- ? 'bg-accent-500 text-white'
- : 'bg-bolt-elements-background-depth-2 text-bolt-elements-textTertiary hover:bg-bolt-elements-background-depth-3',
- )}
- >
- {preset.label}
-
- ))}
-
-
-
- {/* Minimum Changes */}
-
-
-
- Minimum Changes: {config.minChanges}
-
-
- Minimum number of files to trigger auto-save
-
-
-
-
updateConfig('minChanges', value)}
- min={1}
- max={10}
- step={1}
- className="relative flex items-center select-none touch-none w-full h-5"
- >
-
-
-
-
-
-
-
- {/* Additional Options */}
-
-
-
-
- Save on Tab Switch
-
-
- Save when switching to another tab
-
-
-
updateConfig('saveOnBlur', checked)}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- config.saveOnBlur ? 'bg-accent-500' : 'bg-bolt-elements-background-depth-3',
- )}
- >
-
-
-
-
-
-
-
Save Before Run
-
- Save all files before running commands
-
-
-
updateConfig('saveBeforeRun', checked)}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- config.saveBeforeRun ? 'bg-accent-500' : 'bg-bolt-elements-background-depth-3',
- )}
- >
-
-
-
-
-
-
-
- Show Notifications
-
-
- Display toast notifications on save
-
-
-
updateConfig('showNotifications', checked)}
- className={classNames(
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
- config.showNotifications ? 'bg-accent-500' : 'bg-bolt-elements-background-depth-3',
- )}
- >
-
-
-
-
-
-
- {/* Footer */}
-
- setConfig(DEFAULT_CONFIG)}
- className="px-4 py-2 text-sm text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-colors"
- >
- Reset to Defaults
-
-
- Done
-
-
-
-
-
-
- )}
-
-
- );
-});
-
-AutoSaveSettings.displayName = 'AutoSaveSettings';
diff --git a/app/components/workbench/FileStatusIndicator.tsx b/app/components/workbench/FileStatusIndicator.tsx
deleted file mode 100644
index 7c1af5c5e5..0000000000
--- a/app/components/workbench/FileStatusIndicator.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { useStore } from '@nanostores/react';
-import { memo, useMemo } from 'react';
-import { workbenchStore } from '~/lib/stores/workbench';
-import { classNames } from '~/utils/classNames';
-import { motion } from 'framer-motion';
-
-interface FileStatusIndicatorProps {
- className?: string;
- showDetails?: boolean;
-}
-
-export const FileStatusIndicator = memo(({ className = '', showDetails = true }: FileStatusIndicatorProps) => {
- const unsavedFiles = useStore(workbenchStore.unsavedFiles);
- const files = useStore(workbenchStore.files);
-
- const stats = useMemo(() => {
- let totalFiles = 0;
- let totalFolders = 0;
- let totalSize = 0;
-
- Object.entries(files).forEach(([_path, dirent]) => {
- if (dirent?.type === 'file') {
- totalFiles++;
- totalSize += dirent.content?.length || 0;
- } else if (dirent?.type === 'folder') {
- totalFolders++;
- }
- });
-
- return {
- totalFiles,
- totalFolders,
- unsavedCount: unsavedFiles.size,
- totalSize: formatFileSize(totalSize),
- modifiedPercentage: totalFiles > 0 ? Math.round((unsavedFiles.size / totalFiles) * 100) : 0,
- };
- }, [files, unsavedFiles]);
-
- function formatFileSize(bytes: number): string {
- if (bytes === 0) {
- return '0 B';
- }
-
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
- }
-
- const getStatusColor = () => {
- if (stats.unsavedCount === 0) {
- return 'text-green-500';
- }
-
- if (stats.modifiedPercentage > 50) {
- return 'text-red-500';
- }
-
- if (stats.modifiedPercentage > 20) {
- return 'text-yellow-500';
- }
-
- return 'text-orange-500';
- };
-
- return (
-
- {/* Status dot with pulse animation */}
-
- 0 ? [1, 1.2, 1] : 1,
- }}
- transition={{
- duration: 2,
- repeat: stats.unsavedCount > 0 ? Infinity : 0,
- repeatType: 'loop',
- }}
- className={classNames(
- 'w-2 h-2 rounded-full',
- getStatusColor(),
- stats.unsavedCount > 0 ? 'bg-current' : 'bg-green-500',
- )}
- />
-
- {stats.unsavedCount === 0 ? 'All saved' : `${stats.unsavedCount} unsaved`}
-
-
-
- {showDetails && (
- <>
- {/* File count */}
-
-
-
{stats.totalFiles} files
-
-
- {/* Folder count */}
-
-
-
{stats.totalFolders} folders
-
-
- {/* Total size */}
-
-
- {/* Progress bar for unsaved files */}
- {stats.unsavedCount > 0 && (
-
-
{stats.modifiedPercentage}% modified
-
- 50
- ? 'bg-red-500'
- : stats.modifiedPercentage > 20
- ? 'bg-yellow-500'
- : 'bg-orange-500',
- )}
- />
-
-
- )}
- >
- )}
-
- );
-});
-
-FileStatusIndicator.displayName = 'FileStatusIndicator';
diff --git a/app/components/workbench/KeyboardSaveAll.tsx b/app/components/workbench/KeyboardSaveAll.tsx
deleted file mode 100644
index 952755abd5..0000000000
--- a/app/components/workbench/KeyboardSaveAll.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useEffect } from 'react';
-import { toast } from 'react-toastify';
-import { workbenchStore } from '~/lib/stores/workbench';
-
-export function useKeyboardSaveAll() {
- useEffect(() => {
- const handleKeyPress = async (e: KeyboardEvent) => {
- // Ctrl+Shift+S or Cmd+Shift+S to save all
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 's') {
- e.preventDefault();
-
- const unsavedFiles = workbenchStore.unsavedFiles.get();
-
- if (unsavedFiles.size === 0) {
- toast.info('All files are already saved', {
- position: 'bottom-right',
- autoClose: 2000,
- });
- return;
- }
-
- try {
- const count = unsavedFiles.size;
- await workbenchStore.saveAllFiles();
-
- toast.success(`Saved ${count} file${count > 1 ? 's' : ''}`, {
- position: 'bottom-right',
- autoClose: 2000,
- });
- } catch {
- toast.error('Failed to save some files', {
- position: 'bottom-right',
- autoClose: 3000,
- });
- }
- }
- };
-
- window.addEventListener('keydown', handleKeyPress);
-
- return () => window.removeEventListener('keydown', handleKeyPress);
- }, []);
-}
diff --git a/app/components/workbench/SaveAllButton.tsx b/app/components/workbench/SaveAllButton.tsx
deleted file mode 100644
index 2e188cf45d..0000000000
--- a/app/components/workbench/SaveAllButton.tsx
+++ /dev/null
@@ -1,305 +0,0 @@
-import { useStore } from '@nanostores/react';
-import { memo, useCallback, useEffect, useState, useRef } from 'react';
-import { toast } from 'react-toastify';
-import * as Tooltip from '@radix-ui/react-tooltip';
-import { workbenchStore } from '~/lib/stores/workbench';
-import { classNames } from '~/utils/classNames';
-
-interface SaveAllButtonProps {
- className?: string;
- variant?: 'icon' | 'button';
- showCount?: boolean;
- autoSave?: boolean;
- autoSaveInterval?: number;
-}
-
-export const SaveAllButton = memo(
- ({
- className = '',
- variant = 'icon',
- showCount = true,
- autoSave = false,
- autoSaveInterval = 30000,
- }: SaveAllButtonProps) => {
- const unsavedFiles = useStore(workbenchStore.unsavedFiles);
- const [isSaving, setIsSaving] = useState(false);
- const [lastSaved, setLastSaved] = useState(null);
- const [timeUntilAutoSave, setTimeUntilAutoSave] = useState(null);
- const autoSaveTimerRef = useRef(null);
- const countdownTimerRef = useRef(null);
-
- const unsavedCount = unsavedFiles.size;
- const hasUnsavedFiles = unsavedCount > 0;
-
- // Log unsaved files state changes
- useEffect(() => {
- console.log('[SaveAllButton] Unsaved files changed:', {
- count: unsavedCount,
- files: Array.from(unsavedFiles),
- hasUnsavedFiles,
- });
- }, [unsavedFiles, unsavedCount, hasUnsavedFiles]);
-
- // Auto-save logic
- useEffect(() => {
- if (!autoSave || !hasUnsavedFiles) {
- if (autoSaveTimerRef.current) {
- clearTimeout(autoSaveTimerRef.current);
- autoSaveTimerRef.current = null;
- }
-
- if (countdownTimerRef.current) {
- clearInterval(countdownTimerRef.current);
- countdownTimerRef.current = null;
- }
-
- setTimeUntilAutoSave(null);
-
- return;
- }
-
- // Set up auto-save timer
- console.log('[SaveAllButton] Setting up auto-save timer for', autoSaveInterval, 'ms');
- autoSaveTimerRef.current = setTimeout(async () => {
- if (hasUnsavedFiles && !isSaving) {
- console.log('[SaveAllButton] Auto-save triggered');
- await handleSaveAll(true);
- }
- }, autoSaveInterval);
-
- // Set up countdown timer
- const startTime = Date.now();
- setTimeUntilAutoSave(Math.ceil(autoSaveInterval / 1000));
-
- countdownTimerRef.current = setInterval(() => {
- const elapsed = Date.now() - startTime;
- const remaining = Math.max(0, autoSaveInterval - elapsed);
- setTimeUntilAutoSave(Math.ceil(remaining / 1000));
-
- if (remaining <= 0 && countdownTimerRef.current) {
- clearInterval(countdownTimerRef.current);
- countdownTimerRef.current = null;
- }
- }, 1000);
- }, [autoSave, hasUnsavedFiles, autoSaveInterval, isSaving]);
-
- // Cleanup effect
- useEffect(() => {
- return () => {
- if (autoSaveTimerRef.current) {
- clearTimeout(autoSaveTimerRef.current);
- }
-
- if (countdownTimerRef.current) {
- clearInterval(countdownTimerRef.current);
- }
- };
- }, []);
-
- const handleSaveAll = useCallback(
- async (isAutoSave = false) => {
- if (!hasUnsavedFiles || isSaving) {
- console.log('[SaveAllButton] Skipping save:', { hasUnsavedFiles, isSaving });
- return;
- }
-
- console.log('[SaveAllButton] Starting save:', {
- unsavedCount,
- isAutoSave,
- files: Array.from(unsavedFiles),
- });
-
- setIsSaving(true);
-
- const startTime = performance.now();
- const savedFiles: string[] = [];
- const failedFiles: string[] = [];
-
- try {
- // Save each file individually with detailed logging
- for (const filePath of unsavedFiles) {
- try {
- console.log(`[SaveAllButton] Saving file: ${filePath}`);
- await workbenchStore.saveFile(filePath);
- savedFiles.push(filePath);
- console.log(`[SaveAllButton] Successfully saved: ${filePath}`);
- } catch (error) {
- console.error(`[SaveAllButton] Failed to save ${filePath}:`, error);
- failedFiles.push(filePath);
- }
- }
-
- const endTime = performance.now();
- const duration = Math.round(endTime - startTime);
- setLastSaved(new Date());
-
- // Check final state
- const remainingUnsaved = workbenchStore.unsavedFiles.get();
- console.log('[SaveAllButton] Save complete:', {
- savedCount: savedFiles.length,
- failedCount: failedFiles.length,
- remainingUnsaved: Array.from(remainingUnsaved),
- duration,
- });
-
- // Show appropriate feedback
- if (failedFiles.length === 0) {
- const message = isAutoSave
- ? `Auto-saved ${savedFiles.length} file${savedFiles.length > 1 ? 's' : ''}`
- : `Saved ${savedFiles.length} file${savedFiles.length > 1 ? 's' : ''}`;
-
- toast.success(message, {
- position: 'bottom-right',
- autoClose: 2000,
- });
- } else {
- toast.warning(`Saved ${savedFiles.length} files, ${failedFiles.length} failed`, {
- position: 'bottom-right',
- autoClose: 3000,
- });
- }
- } catch (error) {
- console.error('[SaveAllButton] Critical error during save:', error);
- toast.error('Failed to save files', {
- position: 'bottom-right',
- autoClose: 3000,
- });
- } finally {
- setIsSaving(false);
- }
- },
- [hasUnsavedFiles, isSaving, unsavedCount, unsavedFiles],
- );
-
- // Keyboard shortcut
- useEffect(() => {
- const handleKeyPress = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 's') {
- e.preventDefault();
- handleSaveAll(false);
- }
- };
-
- window.addEventListener('keydown', handleKeyPress);
-
- return () => window.removeEventListener('keydown', handleKeyPress);
- }, [handleSaveAll]);
-
- const formatLastSaved = () => {
- if (!lastSaved) {
- return null;
- }
-
- const now = new Date();
- const diff = Math.floor((now.getTime() - lastSaved.getTime()) / 1000);
-
- if (diff < 60) {
- return `${diff}s ago`;
- }
-
- if (diff < 3600) {
- return `${Math.floor(diff / 60)}m ago`;
- }
-
- return `${Math.floor(diff / 3600)}h ago`;
- };
-
- // Icon-only variant for header
- if (variant === 'icon') {
- return (
-
-
-
- handleSaveAll(false)}
- disabled={!hasUnsavedFiles || isSaving}
- className={classNames(
- 'relative p-1.5 rounded-md transition-colors',
- hasUnsavedFiles
- ? 'text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive'
- : 'text-bolt-elements-textTertiary cursor-not-allowed opacity-50',
- className,
- )}
- >
-
-
- {hasUnsavedFiles && showCount && !isSaving && (
-
- {unsavedCount}
-
- )}
-
-
-
-
-
-
-
- {hasUnsavedFiles ? `${unsavedCount} unsaved file${unsavedCount > 1 ? 's' : ''}` : 'All files saved'}
-
- {lastSaved &&
Last saved: {formatLastSaved()}
}
- {autoSave && hasUnsavedFiles && timeUntilAutoSave && (
-
Auto-save in: {timeUntilAutoSave}s
- )}
-
- Ctrl+Shift+S to save all
-
-
-
-
-
-
-
- );
- }
-
- // Button variant
- return (
-
-
-
- handleSaveAll(false)}
- disabled={!hasUnsavedFiles || isSaving}
- className={classNames(
- 'inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
- hasUnsavedFiles
- ? 'bg-accent-500 hover:bg-accent-600 text-white'
- : 'bg-bolt-elements-background-depth-1 text-bolt-elements-textTertiary border border-bolt-elements-borderColor cursor-not-allowed opacity-60',
- className,
- )}
- >
-
-
- {isSaving ? 'Saving...' : `Save All${showCount && hasUnsavedFiles ? ` (${unsavedCount})` : ''}`}
-
- {autoSave && timeUntilAutoSave && hasUnsavedFiles && (
- ({timeUntilAutoSave}s)
- )}
-
-
-
-
-
- Ctrl+Shift+S to save all
-
-
-
-
-
-
- );
- },
-);
-
-SaveAllButton.displayName = 'SaveAllButton';
diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx
index 33d3500f31..3661c494a8 100644
--- a/app/components/workbench/Workbench.client.tsx
+++ b/app/components/workbench/Workbench.client.tsx
@@ -13,7 +13,6 @@ import {
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import { IconButton } from '~/components/ui/IconButton';
-import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
import { Slider, type SliderOptions } from '~/components/ui/Slider';
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
@@ -23,11 +22,13 @@ import { EditorPanel } from './EditorPanel';
import { Preview } from './Preview';
import useViewport from '~/lib/hooks';
-// import { GitLabDeploymentDialog } from '~/components/deploy/GitLabDeploymentDialog';
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { usePreviewStore } from '~/lib/stores/previews';
import { chatStore } from '~/lib/stores/chat';
import type { ElementInfo } from './Inspector';
+import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
+import { useChatHistory } from '~/lib/persistence';
+import { streamingState } from '~/lib/stores/streaming';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
interface WorkspaceProps {
chatStarted?: boolean;
@@ -279,298 +280,239 @@ const FileModifiedDropdown = memo(
},
);
-export const Workbench = memo(({ chatStarted, isStreaming, setSelectedElement }: WorkspaceProps) => {
- renderLogger.trace('Workbench');
-
- const [isSyncing, setIsSyncing] = useState(false);
-
- // const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
- const [fileHistory, setFileHistory] = useState>({});
-
- // Keyboard shortcut for Save All (Ctrl+Shift+S)
- useEffect(() => {
- const handleKeyPress = async (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 's') {
- e.preventDefault();
-
- const unsavedFiles = workbenchStore.unsavedFiles.get();
-
- if (unsavedFiles.size > 0) {
- try {
- await workbenchStore.saveAllFiles();
- toast.success(`Saved ${unsavedFiles.size} file${unsavedFiles.size > 1 ? 's' : ''}`, {
- position: 'bottom-right',
- autoClose: 2000,
- });
- } catch {
- toast.error('Failed to save some files', {
- position: 'bottom-right',
- autoClose: 3000,
- });
- }
- } else {
- toast.info('All files are already saved', {
- position: 'bottom-right',
- autoClose: 2000,
- });
- }
- }
+export const Workbench = memo(
+ ({
+ chatStarted,
+ isStreaming,
+ metadata: _metadata,
+ updateChatMestaData: _updateChatMestaData,
+ setSelectedElement,
+ }: WorkspaceProps) => {
+ renderLogger.trace('Workbench');
+
+ const [fileHistory, setFileHistory] = useState>({});
+
+ // const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
+
+ const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
+ const showWorkbench = useStore(workbenchStore.showWorkbench);
+ const selectedFile = useStore(workbenchStore.selectedFile);
+ const currentDocument = useStore(workbenchStore.currentDocument);
+ const unsavedFiles = useStore(workbenchStore.unsavedFiles);
+ const files = useStore(workbenchStore.files);
+ const selectedView = useStore(workbenchStore.currentView);
+ const { showChat } = useStore(chatStore);
+ const canHideChat = showWorkbench || !showChat;
+
+ const isSmallViewport = useViewport(1024);
+ const streaming = useStore(streamingState);
+ const { exportChat } = useChatHistory();
+ const [isSyncing, setIsSyncing] = useState(false);
+
+ const setSelectedView = (view: WorkbenchViewType) => {
+ workbenchStore.currentView.set(view);
};
- window.addEventListener('keydown', handleKeyPress);
-
- return () => window.removeEventListener('keydown', handleKeyPress);
- }, []);
-
- // const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
-
- const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
- const showWorkbench = useStore(workbenchStore.showWorkbench);
- const selectedFile = useStore(workbenchStore.selectedFile);
- const currentDocument = useStore(workbenchStore.currentDocument);
- const unsavedFiles = useStore(workbenchStore.unsavedFiles);
- const files = useStore(workbenchStore.files);
- const selectedView = useStore(workbenchStore.currentView);
- const { showChat } = useStore(chatStore);
- const canHideChat = showWorkbench || !showChat;
-
- const isSmallViewport = useViewport(1024);
-
- const setSelectedView = (view: WorkbenchViewType) => {
- workbenchStore.currentView.set(view);
- };
- useEffect(() => {
- if (hasPreview) {
- setSelectedView('preview');
- }
- }, [hasPreview]);
-
- useEffect(() => {
- workbenchStore.setDocuments(files);
- }, [files]);
-
- const onEditorChange = useCallback((update) => {
- workbenchStore.setCurrentDocumentContent(update.content);
- }, []);
-
- const onEditorScroll = useCallback((position) => {
- workbenchStore.setCurrentDocumentScrollPosition(position);
- }, []);
-
- const onFileSelect = useCallback((filePath: string | undefined) => {
- workbenchStore.setSelectedFile(filePath);
- }, []);
-
- const onFileSave = useCallback(() => {
- workbenchStore
- .saveCurrentDocument()
- .then(() => {
- // Explicitly refresh all previews after a file save
- const previewStore = usePreviewStore();
- previewStore.refreshAllPreviews();
- })
- .catch(() => {
- toast.error('Failed to update file content');
- });
- }, []);
-
- const onFileReset = useCallback(() => {
- workbenchStore.resetCurrentDocument();
- }, []);
-
- const handleSyncFiles = useCallback(async () => {
- setIsSyncing(true);
-
- try {
- const directoryHandle = await window.showDirectoryPicker();
- await workbenchStore.syncFiles(directoryHandle);
- toast.success('Files synced successfully');
- } catch {
- console.error('Error syncing files');
- toast.error('Failed to sync files');
- } finally {
- setIsSyncing(false);
- }
- }, []);
-
- const handleSelectFile = useCallback((filePath: string) => {
- workbenchStore.setSelectedFile(filePath);
- workbenchStore.currentView.set('diff');
- }, []);
+ useEffect(() => {
+ if (hasPreview) {
+ setSelectedView('preview');
+ }
+ }, [hasPreview]);
+
+ useEffect(() => {
+ workbenchStore.setDocuments(files);
+ }, [files]);
+
+ const onEditorChange = useCallback((update) => {
+ workbenchStore.setCurrentDocumentContent(update.content);
+ }, []);
+
+ const onEditorScroll = useCallback((position) => {
+ workbenchStore.setCurrentDocumentScrollPosition(position);
+ }, []);
+
+ const onFileSelect = useCallback((filePath: string | undefined) => {
+ workbenchStore.setSelectedFile(filePath);
+ }, []);
+
+ const onFileSave = useCallback(() => {
+ workbenchStore
+ .saveCurrentDocument()
+ .then(() => {
+ // Explicitly refresh all previews after a file save
+ const previewStore = usePreviewStore();
+ previewStore.refreshAllPreviews();
+ })
+ .catch(() => {
+ toast.error('Failed to update file content');
+ });
+ }, []);
+
+ const onFileReset = useCallback(() => {
+ workbenchStore.resetCurrentDocument();
+ }, []);
+
+ const handleSelectFile = useCallback((filePath: string) => {
+ workbenchStore.setSelectedFile(filePath);
+ workbenchStore.currentView.set('diff');
+ }, []);
+
+ const handleSyncFiles = useCallback(async () => {
+ setIsSyncing(true);
+
+ try {
+ const directoryHandle = await window.showDirectoryPicker();
+ await workbenchStore.syncFiles(directoryHandle);
+ toast.success('Files synced successfully');
+ } catch (error) {
+ console.error('Error syncing files:', error);
+ toast.error('Failed to sync files');
+ } finally {
+ setIsSyncing(false);
+ }
+ }, []);
- return (
- chatStarted && (
-
-
-
-
-
-
{
- if (canHideChat) {
- chatStore.setKey('showChat', !showChat);
- }
- }}
- />
-
-
- {selectedView === 'code' && (
-
-
{
- console.log('[SaveAll] Button clicked');
-
- const unsavedFiles = workbenchStore.unsavedFiles.get();
- console.log('[SaveAll] Unsaved files:', Array.from(unsavedFiles));
-
- if (unsavedFiles.size > 0) {
- try {
- console.log('[SaveAll] Starting save...');
- await workbenchStore.saveAllFiles();
- toast.success(`Saved ${unsavedFiles.size} file${unsavedFiles.size > 1 ? 's' : ''}`, {
- position: 'bottom-right',
- autoClose: 2000,
- });
- console.log('[SaveAll] Save successful');
- } catch {
- console.error('[SaveAll] Save failed');
- toast.error('Failed to save files', {
- position: 'bottom-right',
- autoClose: 3000,
- });
- }
- } else {
- console.log('[SaveAll] No unsaved files');
- toast.info('All files are already saved', {
- position: 'bottom-right',
- autoClose: 2000,
- });
- }
- }}
- >
-
- Save All
-
-
{
- workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
- }}
- >
-
- Toggle Terminal
-
-
-
-
- Sync
-
-
-
-
- {isSyncing ?
:
}
-
{isSyncing ? 'Syncing...' : 'Sync Files'}
-
-
-
+
+
+
+
{
+ if (canHideChat) {
+ chatStore.setKey('showChat', !showChat);
+ }
+ }}
+ />
+
+
+ {selectedView === 'code' && (
+
+ {/* Export Chat Button */}
+
+
+ {/* Sync Button */}
+
+
+
+ {isSyncing ? 'Syncing...' : 'Sync'}
+
+
+
+
+
+ {isSyncing ? (
+
+ ) : (
+
+ )}
+
{isSyncing ? 'Syncing...' : 'Sync Files'}
+
+
+
+
+
+
+ {/* Toggle Terminal Button */}
+
+
{
- /* GitHub push temporarily disabled */
+ workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
}}
+ className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
-
-
-
-
-
- )}
+
+ Toggle Terminal
+
+
+
+ )}
- {selectedView === 'diff' && (
-
- )}
-
{
- workbenchStore.showWorkbench.set(false);
- }}
- />
-
-
-
-
+ )}
+ {
+ workbenchStore.showWorkbench.set(false);
+ }}
/>
-
-
-
-
-
-
-
+
+
-
- {/* GitHub push dialog temporarily disabled during merge - will be re-enabled with new GitLab integration */}
-
- )
- );
-});
+
+ )
+ );
+ },
+);
// View component for rendering content with motion transitions
interface ViewProps extends HTMLMotionProps<'div'> {
diff --git a/app/lib/.server/llm/stream-recovery.ts b/app/lib/.server/llm/stream-recovery.ts
deleted file mode 100644
index dd979832f6..0000000000
--- a/app/lib/.server/llm/stream-recovery.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/**
- * Stream Recovery Module
- * Handles stream failures and provides automatic recovery mechanisms
- * Fixes chat conversation hanging issues
- * Author: Keoma Wright
- */
-
-import { createScopedLogger } from '~/utils/logger';
-
-const logger = createScopedLogger('stream-recovery');
-
-export interface StreamRecoveryOptions {
- maxRetries?: number;
- retryDelay?: number;
- timeout?: number;
- onRetry?: (attempt: number) => void;
- onTimeout?: () => void;
- onError?: (error: any) => void;
-}
-
-export class StreamRecoveryManager {
- private _retryCount = 0;
- private _timeoutHandle: NodeJS.Timeout | null = null;
- private _lastActivity: number = Date.now();
- private _isActive = true;
-
- constructor(private _options: StreamRecoveryOptions = {}) {
- this._options = {
- maxRetries: 3,
- retryDelay: 1000,
- timeout: 30000, // 30 seconds default timeout
- ..._options,
- };
- }
-
- /**
- * Start monitoring the stream for inactivity
- */
- startMonitoring() {
- this._resetTimeout();
- }
-
- /**
- * Reset the timeout when activity is detected
- */
- recordActivity() {
- this._lastActivity = Date.now();
- this._resetTimeout();
- }
-
- /**
- * Reset the timeout timer
- */
- private _resetTimeout() {
- if (this._timeoutHandle) {
- clearTimeout(this._timeoutHandle);
- }
-
- if (!this._isActive) {
- return;
- }
-
- this._timeoutHandle = setTimeout(() => {
- const inactiveTime = Date.now() - this._lastActivity;
- logger.warn(`Stream timeout detected after ${inactiveTime}ms of inactivity`);
-
- if (this._options.onTimeout) {
- this._options.onTimeout();
- }
-
- this._handleTimeout();
- }, this._options.timeout!);
- }
-
- /**
- * Handle stream timeout
- */
- private _handleTimeout() {
- logger.error('Stream timeout - attempting recovery');
-
- // Signal that recovery is needed
- this.attemptRecovery();
- }
-
- /**
- * Attempt to recover from a stream failure
- */
- async attemptRecovery(): Promise
{
- if (this._retryCount >= this._options.maxRetries!) {
- logger.error(`Max retries (${this._options.maxRetries}) reached - cannot recover`);
- return false;
- }
-
- this._retryCount++;
- logger.info(`Attempting recovery (attempt ${this._retryCount}/${this._options.maxRetries})`);
-
- if (this._options.onRetry) {
- this._options.onRetry(this._retryCount);
- }
-
- // Wait before retrying
- await new Promise((resolve) => setTimeout(resolve, this._options.retryDelay! * this._retryCount));
-
- // Reset activity tracking
- this.recordActivity();
-
- return true;
- }
-
- /**
- * Handle stream errors with recovery
- */
- async handleError(error: any): Promise {
- logger.error('Stream error detected:', error);
-
- if (this._options.onError) {
- this._options.onError(error);
- }
-
- // Check if error is recoverable
- if (this._isRecoverableError(error)) {
- return await this.attemptRecovery();
- }
-
- logger.error('Non-recoverable error - cannot continue');
-
- return false;
- }
-
- /**
- * Check if an error is recoverable
- */
- private _isRecoverableError(error: any): boolean {
- const errorMessage = error?.message || error?.toString() || '';
-
- // List of recoverable error patterns
- const recoverablePatterns = [
- 'ECONNRESET',
- 'ETIMEDOUT',
- 'ENOTFOUND',
- 'socket hang up',
- 'network',
- 'timeout',
- 'abort',
- 'EPIPE',
- '502',
- '503',
- '504',
- 'rate limit',
- ];
-
- return recoverablePatterns.some((pattern) => errorMessage.toLowerCase().includes(pattern.toLowerCase()));
- }
-
- /**
- * Stop monitoring and cleanup
- */
- stop() {
- this._isActive = false;
-
- if (this._timeoutHandle) {
- clearTimeout(this._timeoutHandle);
- this._timeoutHandle = null;
- }
- }
-
- /**
- * Reset the recovery manager
- */
- reset() {
- this._retryCount = 0;
- this._lastActivity = Date.now();
- this._isActive = true;
- this._resetTimeout();
- }
-}
-
-/**
- * Create a wrapped stream with recovery capabilities
- */
-export function createRecoverableStream(
- streamFactory: () => Promise>,
- options?: StreamRecoveryOptions,
-): ReadableStream {
- const recovery = new StreamRecoveryManager(options);
- let currentStream: ReadableStream | null = null;
- let reader: ReadableStreamDefaultReader | null = null;
-
- return new ReadableStream({
- async start(controller) {
- recovery.startMonitoring();
-
- try {
- currentStream = await streamFactory();
- reader = currentStream.getReader();
- } catch (error) {
- logger.error('Failed to create initial stream:', error);
-
- const canRecover = await recovery.handleError(error);
-
- if (canRecover) {
- // Retry creating the stream
- currentStream = await streamFactory();
- reader = currentStream.getReader();
- } else {
- controller.error(error);
- return;
- }
- }
- },
-
- async pull(controller) {
- if (!reader) {
- controller.error(new Error('No reader available'));
- return;
- }
-
- try {
- const { done, value } = await reader.read();
-
- if (done) {
- controller.close();
- recovery.stop();
-
- return;
- }
-
- // Record activity to reset timeout
- recovery.recordActivity();
- controller.enqueue(value);
- } catch (error) {
- logger.error('Error reading from stream:', error);
-
- const canRecover = await recovery.handleError(error);
-
- if (canRecover) {
- // Try to recreate the stream
- try {
- if (reader) {
- reader.releaseLock();
- }
-
- currentStream = await streamFactory();
- reader = currentStream.getReader();
-
- // Continue reading
- await this.pull!(controller);
- } catch (retryError) {
- logger.error('Recovery failed:', retryError);
- controller.error(retryError);
- recovery.stop();
- }
- } else {
- controller.error(error);
- recovery.stop();
- }
- }
- },
-
- cancel() {
- recovery.stop();
-
- if (reader) {
- reader.releaseLock();
- }
- },
- });
-}
diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts
index f21ef286c2..40774a8d07 100644
--- a/app/lib/.server/llm/stream-text.ts
+++ b/app/lib/.server/llm/stream-text.ts
@@ -11,65 +11,6 @@ import { createFilesContext, extractPropertiesFromMessage } from './utils';
import { discussPrompt } from '~/lib/common/prompts/discuss-prompt';
import type { DesignScheme } from '~/types/design-scheme';
-function getSmartAISystemPrompt(basePrompt: string): string {
- const smartAIEnhancement = `
-## SmartAI Mode - Enhanced Conversational Coding Assistant
-
-You are operating in SmartAI mode, a premium Bolt.gives feature that provides detailed, educational feedback throughout the coding process.
-
-### Your Communication Style:
-- Be conversational and friendly, as if pair programming with a colleague
-- Explain your thought process clearly and educationally
-- Use natural language, not technical jargon unless necessary
-- Keep responses visible and engaging
-
-### What to Communicate:
-
-**When Starting Tasks:**
-✨ "I see you want [task description]. Let me [approach explanation]..."
-✨ Explain your understanding and planned approach
-✨ Share why you're choosing specific solutions
-
-**During Implementation:**
-📝 "Now I'm creating/updating [file] to [purpose]..."
-📝 Explain what each code section does
-📝 Share the patterns and best practices you're using
-📝 Discuss any trade-offs or alternatives considered
-
-**When Problem-Solving:**
-🔍 "I noticed [issue]. This is likely because [reasoning]..."
-🔍 Share your debugging thought process
-🔍 Explain how you're identifying and fixing issues
-🔍 Describe why your solution will work
-
-**After Completing Work:**
-✅ "I've successfully [what was done]. The key changes include..."
-✅ Summarize what was accomplished
-✅ Highlight important decisions made
-✅ Suggest potential improvements or next steps
-
-### Example Responses:
-
-Instead of silence:
-"I understand you need a contact form. Let me create a modern, accessible form with proper validation. I'll start by setting up the form structure with semantic HTML..."
-
-While coding:
-"I'm now adding email validation to ensure users enter valid email addresses. I'll use a regex pattern that covers most common email formats while keeping it user-friendly..."
-
-When debugging:
-"I see the button isn't aligning properly with the other elements. This looks like a flexbox issue. Let me adjust the container's display properties to fix the alignment..."
-
-### Remember:
-- Users chose SmartAI to learn from your process
-- Make every action visible and understandable
-- Be their coding companion, not just a silent worker
-- Keep the conversation flowing naturally
-
-${basePrompt}`;
-
- return smartAIEnhancement;
-}
-
export type Messages = Message[];
export interface StreamingOptions extends Omit[0], 'model'> {
@@ -141,19 +82,13 @@ export async function streamText(props: {
} = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
- let smartAIEnabled = false;
let processedMessages = messages.map((message) => {
const newMessage = { ...message };
if (message.role === 'user') {
- const { model, provider, content, smartAI } = extractPropertiesFromMessage(message);
+ const { model, provider, content } = extractPropertiesFromMessage(message);
currentModel = model;
currentProvider = provider;
-
- if (smartAI !== undefined) {
- smartAIEnabled = smartAI;
- }
-
newMessage.content = sanitizeText(content);
} else if (message.role == 'assistant') {
newMessage.content = sanitizeText(message.content);
@@ -207,39 +142,13 @@ export async function streamText(props: {
const dynamicMaxTokens = modelDetails ? getCompletionTokenLimit(modelDetails) : Math.min(MAX_TOKENS, 16384);
- // Additional safety cap - respect model-specific limits
- let safeMaxTokens = dynamicMaxTokens;
-
- // Apply model-specific caps for Anthropic models
- if (modelDetails?.provider === 'Anthropic') {
- if (modelDetails.name.includes('claude-sonnet-4') || modelDetails.name.includes('claude-opus-4')) {
- safeMaxTokens = Math.min(dynamicMaxTokens, 64000);
- } else if (modelDetails.name.includes('claude-3-7-sonnet')) {
- safeMaxTokens = Math.min(dynamicMaxTokens, 64000);
- } else if (modelDetails.name.includes('claude-3-5-sonnet')) {
- safeMaxTokens = Math.min(dynamicMaxTokens, 8192);
- } else {
- safeMaxTokens = Math.min(dynamicMaxTokens, 4096);
- }
- } else {
- // General safety cap for other providers
- safeMaxTokens = Math.min(dynamicMaxTokens, 128000);
- }
+ // Use model-specific limits directly - no artificial cap needed
+ const safeMaxTokens = dynamicMaxTokens;
logger.info(
- `Max tokens for model ${modelDetails.name} is ${safeMaxTokens} (capped from ${dynamicMaxTokens}) based on model limits`,
+ `Token limits for model ${modelDetails.name}: maxTokens=${safeMaxTokens}, maxTokenAllowed=${modelDetails.maxTokenAllowed}, maxCompletionTokens=${modelDetails.maxCompletionTokens}`,
);
- /*
- * Check if SmartAI is enabled for supported models
- * SmartAI is enabled if either:
- * 1. The model itself has isSmartAIEnabled flag (for models with SmartAI in name)
- * 2. The user explicitly enabled it via message flag
- */
- const isSmartAISupported =
- modelDetails?.supportsSmartAI && (provider.name === 'Anthropic' || provider.name === 'OpenAI');
- const useSmartAI = (modelDetails?.isSmartAIEnabled || smartAIEnabled) && isSmartAISupported;
-
let systemPrompt =
PromptLibrary.getPropmtFromLibrary(promptId || 'default', {
cwd: WORK_DIR,
@@ -253,11 +162,6 @@ export async function streamText(props: {
},
}) ?? getSystemPrompt();
- // Enhance system prompt for SmartAI if enabled and supported
- if (useSmartAI) {
- systemPrompt = getSmartAISystemPrompt(systemPrompt);
- }
-
if (chatMode === 'build' && contextFiles && contextOptimization) {
const codeContext = createFilesContext(contextFiles, true);
@@ -317,11 +221,18 @@ export async function streamText(props: {
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
- // DEBUG: Log reasoning model detection
+ // Log reasoning model detection and token parameters
const isReasoning = isReasoningModel(modelDetails.name);
- logger.info(`DEBUG STREAM: Model "${modelDetails.name}" detected as reasoning model: ${isReasoning}`);
+ logger.info(
+ `Model "${modelDetails.name}" is reasoning model: ${isReasoning}, using ${isReasoning ? 'maxCompletionTokens' : 'maxTokens'}: ${safeMaxTokens}`,
+ );
- // console.log(systemPrompt, processedMessages);
+ // Validate token limits before API call
+ if (safeMaxTokens > (modelDetails.maxTokenAllowed || 128000)) {
+ logger.warn(
+ `Token limit warning: requesting ${safeMaxTokens} tokens but model supports max ${modelDetails.maxTokenAllowed || 128000}`,
+ );
+ }
// Use maxCompletionTokens for reasoning models (o1, GPT-5), maxTokens for traditional models
const tokenParams = isReasoning ? { maxCompletionTokens: safeMaxTokens } : { maxTokens: safeMaxTokens };
diff --git a/app/lib/.server/llm/utils.ts b/app/lib/.server/llm/utils.ts
index b1f0b806bb..e019a92992 100644
--- a/app/lib/.server/llm/utils.ts
+++ b/app/lib/.server/llm/utils.ts
@@ -8,7 +8,6 @@ export function extractPropertiesFromMessage(message: Omit): {
model: string;
provider: string;
content: string;
- smartAI?: boolean;
} {
const textContent = Array.isArray(message.content)
? message.content.find((item) => item.type === 'text')?.text || ''
@@ -17,10 +16,6 @@ export function extractPropertiesFromMessage(message: Omit): {
const modelMatch = textContent.match(MODEL_REGEX);
const providerMatch = textContent.match(PROVIDER_REGEX);
- // Check for SmartAI toggle in the message
- const smartAIMatch = textContent.match(/\[SmartAI:(true|false)\]/);
- const smartAI = smartAIMatch ? smartAIMatch[1] === 'true' : undefined;
-
/*
* Extract model
* const modelMatch = message.content.match(MODEL_REGEX);
@@ -38,21 +33,15 @@ export function extractPropertiesFromMessage(message: Omit): {
if (item.type === 'text') {
return {
type: 'text',
- text: item.text
- ?.replace(MODEL_REGEX, '')
- .replace(PROVIDER_REGEX, '')
- .replace(/\[SmartAI:(true|false)\]/g, ''),
+ text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
};
}
return item; // Preserve image_url and other types as is
})
- : textContent
- .replace(MODEL_REGEX, '')
- .replace(PROVIDER_REGEX, '')
- .replace(/\[SmartAI:(true|false)\]/g, '');
+ : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
- return { model, provider, content: cleanedContent, smartAI };
+ return { model, provider, content: cleanedContent };
}
export function simplifyBoltActions(input: string): string {
diff --git a/app/lib/deployment/netlify-config.ts b/app/lib/deployment/netlify-config.ts
deleted file mode 100644
index f94cc5c4b5..0000000000
--- a/app/lib/deployment/netlify-config.ts
+++ /dev/null
@@ -1,374 +0,0 @@
-/**
- * Netlify Configuration Helper
- * Contributed by Keoma Wright
- *
- * This module provides automatic configuration generation for Netlify deployments
- */
-
-export interface NetlifyConfig {
- build: {
- command?: string;
- publish: string;
- functions?: string;
- environment?: Record;
- };
- redirects?: Array<{
- from: string;
- to: string;
- status?: number;
- force?: boolean;
- }>;
- headers?: Array<{
- for: string;
- values: Record;
- }>;
- functions?: {
- [key: string]: {
- included_files?: string[];
- external_node_modules?: string[];
- };
- };
-}
-
-export interface FrameworkConfig {
- name: string;
- buildCommand: string;
- outputDirectory: string;
- nodeVersion: string;
- installCommand?: string;
- envVars?: Record;
-}
-
-const FRAMEWORK_CONFIGS: Record = {
- react: {
- name: 'React',
- buildCommand: 'npm run build',
- outputDirectory: 'build',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- 'react-vite': {
- name: 'React (Vite)',
- buildCommand: 'npm run build',
- outputDirectory: 'dist',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- vue: {
- name: 'Vue',
- buildCommand: 'npm run build',
- outputDirectory: 'dist',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- angular: {
- name: 'Angular',
- buildCommand: 'npm run build',
- outputDirectory: 'dist',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- svelte: {
- name: 'Svelte',
- buildCommand: 'npm run build',
- outputDirectory: 'public',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- 'svelte-kit': {
- name: 'SvelteKit',
- buildCommand: 'npm run build',
- outputDirectory: '.svelte-kit',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- next: {
- name: 'Next.js',
- buildCommand: 'npm run build',
- outputDirectory: '.next',
- nodeVersion: '18',
- installCommand: 'npm install',
- envVars: {
- NEXT_TELEMETRY_DISABLED: '1',
- },
- },
- nuxt: {
- name: 'Nuxt',
- buildCommand: 'npm run build',
- outputDirectory: '.output/public',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- gatsby: {
- name: 'Gatsby',
- buildCommand: 'npm run build',
- outputDirectory: 'public',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- remix: {
- name: 'Remix',
- buildCommand: 'npm run build',
- outputDirectory: 'public',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- astro: {
- name: 'Astro',
- buildCommand: 'npm run build',
- outputDirectory: 'dist',
- nodeVersion: '18',
- installCommand: 'npm install',
- },
- static: {
- name: 'Static Site',
- buildCommand: '',
- outputDirectory: '.',
- nodeVersion: '18',
- },
-};
-
-export function detectFramework(packageJson: any): string {
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
-
- // Check for specific frameworks
- if (deps.next) {
- return 'next';
- }
-
- if (deps.nuxt || deps.nuxt3) {
- return 'nuxt';
- }
-
- if (deps.gatsby) {
- return 'gatsby';
- }
-
- if (deps['@remix-run/react']) {
- return 'remix';
- }
-
- if (deps.astro) {
- return 'astro';
- }
-
- if (deps['@angular/core']) {
- return 'angular';
- }
-
- if (deps['@sveltejs/kit']) {
- return 'svelte-kit';
- }
-
- if (deps.svelte) {
- return 'svelte';
- }
-
- if (deps.vue) {
- return 'vue';
- }
-
- if (deps.react) {
- if (deps.vite) {
- return 'react-vite';
- }
-
- return 'react';
- }
-
- return 'static';
-}
-
-export function generateNetlifyConfig(framework: string, customConfig?: Partial): NetlifyConfig {
- const frameworkConfig = FRAMEWORK_CONFIGS[framework] || FRAMEWORK_CONFIGS.static;
-
- const config: NetlifyConfig = {
- build: {
- command: frameworkConfig.buildCommand,
- publish: frameworkConfig.outputDirectory,
- environment: {
- NODE_VERSION: frameworkConfig.nodeVersion,
- ...frameworkConfig.envVars,
- ...customConfig?.build?.environment,
- },
- },
- redirects: [],
- headers: [
- {
- for: '/*',
- values: {
- 'X-Frame-Options': 'DENY',
- 'X-XSS-Protection': '1; mode=block',
- 'X-Content-Type-Options': 'nosniff',
- 'Referrer-Policy': 'strict-origin-when-cross-origin',
- },
- },
- ],
- };
-
- // Add SPA redirect for client-side routing frameworks
- if (['react', 'react-vite', 'vue', 'angular', 'svelte'].includes(framework)) {
- config.redirects!.push({
- from: '/*',
- to: '/index.html',
- status: 200,
- });
- }
-
- // Add custom headers for static assets
- config.headers!.push({
- for: '/assets/*',
- values: {
- 'Cache-Control': 'public, max-age=31536000, immutable',
- },
- });
-
- // Merge with custom config
- if (customConfig) {
- if (customConfig.redirects) {
- config.redirects!.push(...customConfig.redirects);
- }
-
- if (customConfig.headers) {
- config.headers!.push(...customConfig.headers);
- }
-
- if (customConfig.functions) {
- config.functions = customConfig.functions;
- }
- }
-
- return config;
-}
-
-export function generateNetlifyToml(config: NetlifyConfig): string {
- let toml = '';
-
- // Build configuration
- toml += '[build]\n';
-
- if (config.build.command) {
- toml += ` command = "${config.build.command}"\n`;
- }
-
- toml += ` publish = "${config.build.publish}"\n`;
-
- if (config.build.functions) {
- toml += ` functions = "${config.build.functions}"\n`;
- }
-
- // Environment variables
- if (config.build.environment && Object.keys(config.build.environment).length > 0) {
- toml += '\n[build.environment]\n';
-
- for (const [key, value] of Object.entries(config.build.environment)) {
- toml += ` ${key} = "${value}"\n`;
- }
- }
-
- // Redirects
- if (config.redirects && config.redirects.length > 0) {
- for (const redirect of config.redirects) {
- toml += '\n[[redirects]]\n';
- toml += ` from = "${redirect.from}"\n`;
- toml += ` to = "${redirect.to}"\n`;
-
- if (redirect.status) {
- toml += ` status = ${redirect.status}\n`;
- }
-
- if (redirect.force) {
- toml += ` force = ${redirect.force}\n`;
- }
- }
- }
-
- // Headers
- if (config.headers && config.headers.length > 0) {
- for (const header of config.headers) {
- toml += '\n[[headers]]\n';
- toml += ` for = "${header.for}"\n`;
-
- if (Object.keys(header.values).length > 0) {
- toml += ' [headers.values]\n';
-
- for (const [key, value] of Object.entries(header.values)) {
- toml += ` "${key}" = "${value}"\n`;
- }
- }
- }
- }
-
- // Functions configuration
- if (config.functions) {
- for (const [funcName, funcConfig] of Object.entries(config.functions)) {
- toml += `\n[functions."${funcName}"]\n`;
-
- if (funcConfig.included_files) {
- toml += ` included_files = ${JSON.stringify(funcConfig.included_files)}\n`;
- }
-
- if (funcConfig.external_node_modules) {
- toml += ` external_node_modules = ${JSON.stringify(funcConfig.external_node_modules)}\n`;
- }
- }
- }
-
- return toml;
-}
-
-export function validateDeploymentFiles(files: Record): {
- valid: boolean;
- errors: string[];
- warnings: string[];
-} {
- const errors: string[] = [];
- const warnings: string[] = [];
-
- // Check for index.html
- const hasIndex = Object.keys(files).some(
- (path) => path === '/index.html' || path === 'index.html' || path.endsWith('/index.html'),
- );
-
- if (!hasIndex) {
- warnings.push('No index.html file found. Make sure your build output includes an entry point.');
- }
-
- // Check file sizes
- const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
- const WARN_FILE_SIZE = 10 * 1024 * 1024; // 10MB
-
- for (const [path, content] of Object.entries(files)) {
- const size = new Blob([content]).size;
-
- if (size > MAX_FILE_SIZE) {
- errors.push(`File ${path} exceeds maximum size of 100MB`);
- } else if (size > WARN_FILE_SIZE) {
- warnings.push(`File ${path} is large (${Math.round(size / 1024 / 1024)}MB)`);
- }
- }
-
- // Check total deployment size
- const totalSize = Object.values(files).reduce((sum, content) => sum + new Blob([content]).size, 0);
-
- const MAX_TOTAL_SIZE = 500 * 1024 * 1024; // 500MB
-
- if (totalSize > MAX_TOTAL_SIZE) {
- errors.push(`Total deployment size exceeds 500MB limit`);
- }
-
- // Check for common issues
- if (Object.keys(files).some((path) => path.includes('node_modules'))) {
- warnings.push('Deployment includes node_modules - these should typically be excluded');
- }
-
- if (Object.keys(files).some((path) => path.includes('.env'))) {
- errors.push('Deployment includes .env file - remove sensitive configuration files');
- }
-
- return {
- valid: errors.length === 0,
- errors,
- warnings,
- };
-}
diff --git a/app/lib/modules/llm/providers/amazon-bedrock.ts b/app/lib/modules/llm/providers/amazon-bedrock.ts
index c55839acdb..6a4cbc961b 100644
--- a/app/lib/modules/llm/providers/amazon-bedrock.ts
+++ b/app/lib/modules/llm/providers/amazon-bedrock.ts
@@ -20,18 +20,6 @@ export default class AmazonBedrockProvider extends BaseProvider {
};
staticModels: ModelInfo[] = [
- {
- name: 'anthropic.claude-sonnet-4-20250514-v1:0',
- label: 'Claude Sonnet 4 (Bedrock)',
- provider: 'AmazonBedrock',
- maxTokenAllowed: 200000,
- },
- {
- name: 'anthropic.claude-opus-4-1-20250805-v1:0',
- label: 'Claude Opus 4.1 (Bedrock)',
- provider: 'AmazonBedrock',
- maxTokenAllowed: 200000,
- },
{
name: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
label: 'Claude 3.5 Sonnet v2 (Bedrock)',
diff --git a/app/lib/modules/llm/providers/anthropic.ts b/app/lib/modules/llm/providers/anthropic.ts
index b442589f61..56899e0235 100644
--- a/app/lib/modules/llm/providers/anthropic.ts
+++ b/app/lib/modules/llm/providers/anthropic.ts
@@ -1,10 +1,10 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
-import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
+import type { IProviderSetting } from '~/types/model';
import { createAnthropic } from '@ai-sdk/anthropic';
-export class AnthropicProvider extends BaseProvider {
+export default class AnthropicProvider extends BaseProvider {
name = 'Anthropic';
getApiKeyLink = 'https://console.anthropic.com/settings/keys';
@@ -13,50 +13,6 @@ export class AnthropicProvider extends BaseProvider {
};
staticModels: ModelInfo[] = [
- /*
- * Claude Opus 4.1: Most powerful model for coding and reasoning
- * Released August 5, 2025
- */
- {
- name: 'claude-opus-4-1-20250805',
- label: 'Claude Opus 4.1',
- provider: 'Anthropic',
- maxTokenAllowed: 200000,
- maxCompletionTokens: 64000,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'claude-opus-4-1-20250805-smartai',
- label: 'Claude Opus 4.1 (SmartAI)',
- provider: 'Anthropic',
- maxTokenAllowed: 200000,
- maxCompletionTokens: 64000,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
- },
-
- /*
- * Claude Sonnet 4: Hybrid instant/extended response model
- * Released May 14, 2025
- */
- {
- name: 'claude-sonnet-4-20250514',
- label: 'Claude Sonnet 4',
- provider: 'Anthropic',
- maxTokenAllowed: 200000,
- maxCompletionTokens: 64000,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'claude-sonnet-4-20250514-smartai',
- label: 'Claude Sonnet 4 (SmartAI)',
- provider: 'Anthropic',
- maxTokenAllowed: 200000,
- maxCompletionTokens: 64000,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
- },
-
/*
* Essential fallback models - only the most stable/reliable ones
* Claude 3.5 Sonnet: 200k context, excellent for complex reasoning and coding
@@ -66,17 +22,7 @@ export class AnthropicProvider extends BaseProvider {
label: 'Claude 3.5 Sonnet',
provider: 'Anthropic',
maxTokenAllowed: 200000,
- maxCompletionTokens: 8192,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'claude-3-5-sonnet-20241022-smartai',
- label: 'Claude 3.5 Sonnet (SmartAI)',
- provider: 'Anthropic',
- maxTokenAllowed: 200000,
- maxCompletionTokens: 8192,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
+ maxCompletionTokens: 128000,
},
// Claude 3 Haiku: 200k context, fastest and most cost-effective
@@ -85,17 +31,16 @@ export class AnthropicProvider extends BaseProvider {
label: 'Claude 3 Haiku',
provider: 'Anthropic',
maxTokenAllowed: 200000,
- maxCompletionTokens: 4096,
- supportsSmartAI: false, // Base model without SmartAI
+ maxCompletionTokens: 128000,
},
+
+ // Claude Opus 4: 200k context, 32k output limit (latest flagship model)
{
- name: 'claude-3-haiku-20240307-smartai',
- label: 'Claude 3 Haiku (SmartAI)',
+ name: 'claude-opus-4-20250514',
+ label: 'Claude 4 Opus',
provider: 'Anthropic',
maxTokenAllowed: 200000,
- maxCompletionTokens: 4096,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
+ maxCompletionTokens: 32000,
},
];
@@ -119,8 +64,7 @@ export class AnthropicProvider extends BaseProvider {
const response = await fetch(`https://api.anthropic.com/v1/models`, {
headers: {
'x-api-key': `${apiKey}`,
- ['anthropic-version']: '2023-06-01',
- ['Content-Type']: 'application/json',
+ 'anthropic-version': '2023-06-01',
},
});
@@ -146,21 +90,15 @@ export class AnthropicProvider extends BaseProvider {
contextWindow = 200000; // Claude 3 Sonnet has 200k context
}
- // Determine max completion tokens based on model
- let maxCompletionTokens = 4096; // default fallback
+ // Determine completion token limits based on specific model
+ let maxCompletionTokens = 128000; // default for older Claude 3 models
- if (m.id?.includes('claude-sonnet-4') || m.id?.includes('claude-opus-4')) {
- maxCompletionTokens = 64000;
- } else if (m.id?.includes('claude-3-7-sonnet')) {
- maxCompletionTokens = 64000;
- } else if (m.id?.includes('claude-3-5-sonnet')) {
- maxCompletionTokens = 8192;
- } else if (m.id?.includes('claude-3-haiku')) {
- maxCompletionTokens = 4096;
- } else if (m.id?.includes('claude-3-opus')) {
- maxCompletionTokens = 4096;
- } else if (m.id?.includes('claude-3-sonnet')) {
- maxCompletionTokens = 4096;
+ if (m.id?.includes('claude-opus-4')) {
+ maxCompletionTokens = 32000; // Claude 4 Opus: 32K output limit
+ } else if (m.id?.includes('claude-sonnet-4')) {
+ maxCompletionTokens = 64000; // Claude 4 Sonnet: 64K output limit
+ } else if (m.id?.includes('claude-4')) {
+ maxCompletionTokens = 32000; // Other Claude 4 models: conservative 32K limit
}
return {
@@ -169,7 +107,6 @@ export class AnthropicProvider extends BaseProvider {
provider: this.name,
maxTokenAllowed: contextWindow,
maxCompletionTokens,
- supportsSmartAI: true, // All Anthropic models support SmartAI
};
});
}
@@ -180,27 +117,19 @@ export class AnthropicProvider extends BaseProvider {
apiKeys?: Record;
providerSettings?: Record;
}) => LanguageModelV1 = (options) => {
- const { model, serverEnv, apiKeys, providerSettings } = options;
- const { apiKey, baseUrl } = this.getProviderBaseUrlAndKey({
+ const { apiKeys, providerSettings, serverEnv, model } = options;
+ const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
- providerSettings: providerSettings?.[this.name],
+ providerSettings,
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'ANTHROPIC_API_KEY',
});
-
- if (!apiKey) {
- throw `Missing API key for ${this.name} provider`;
- }
-
const anthropic = createAnthropic({
apiKey,
- baseURL: baseUrl || 'https://api.anthropic.com/v1',
+ headers: { 'anthropic-beta': 'output-128k-2025-02-19' },
});
- // Handle SmartAI variant by using the base model name
- const actualModel = model.replace('-smartai', '');
-
- return anthropic(actualModel);
+ return anthropic(model);
};
}
diff --git a/app/lib/modules/llm/providers/open-router.ts b/app/lib/modules/llm/providers/open-router.ts
index 871560edf9..e3d12d1456 100644
--- a/app/lib/modules/llm/providers/open-router.ts
+++ b/app/lib/modules/llm/providers/open-router.ts
@@ -31,18 +31,6 @@ export default class OpenRouterProvider extends BaseProvider {
* Essential fallback models - only the most stable/reliable ones
* Claude 3.5 Sonnet via OpenRouter: 200k context
*/
- {
- name: 'anthropic/claude-sonnet-4-20250514',
- label: 'Anthropic: Claude Sonnet 4 (OpenRouter)',
- provider: 'OpenRouter',
- maxTokenAllowed: 200000,
- },
- {
- name: 'anthropic/claude-opus-4-1-20250805',
- label: 'Anthropic: Claude Opus 4.1 (OpenRouter)',
- provider: 'OpenRouter',
- maxTokenAllowed: 200000,
- },
{
name: 'anthropic/claude-3.5-sonnet',
label: 'Claude 3.5 Sonnet',
diff --git a/app/lib/modules/llm/providers/openai.ts b/app/lib/modules/llm/providers/openai.ts
index 97add9d293..fef72940ca 100644
--- a/app/lib/modules/llm/providers/openai.ts
+++ b/app/lib/modules/llm/providers/openai.ts
@@ -17,23 +17,7 @@ export default class OpenAIProvider extends BaseProvider {
* Essential fallback models - only the most stable/reliable ones
* GPT-4o: 128k context, 4k standard output (64k with long output mode)
*/
- {
- name: 'gpt-4o',
- label: 'GPT-4o',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 4096,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'gpt-4o-smartai',
- label: 'GPT-4o (SmartAI)',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 4096,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
- },
+ { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI', maxTokenAllowed: 128000, maxCompletionTokens: 4096 },
// GPT-4o Mini: 128k context, cost-effective alternative
{
@@ -42,16 +26,6 @@ export default class OpenAIProvider extends BaseProvider {
provider: 'OpenAI',
maxTokenAllowed: 128000,
maxCompletionTokens: 4096,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'gpt-4o-mini-smartai',
- label: 'GPT-4o Mini (SmartAI)',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 4096,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
},
// GPT-3.5-turbo: 16k context, fast and cost-effective
@@ -61,16 +35,6 @@ export default class OpenAIProvider extends BaseProvider {
provider: 'OpenAI',
maxTokenAllowed: 16000,
maxCompletionTokens: 4096,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'gpt-3.5-turbo-smartai',
- label: 'GPT-3.5 Turbo (SmartAI)',
- provider: 'OpenAI',
- maxTokenAllowed: 16000,
- maxCompletionTokens: 4096,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
},
// o1-preview: 128k context, 32k output limit (reasoning model)
@@ -80,36 +44,10 @@ export default class OpenAIProvider extends BaseProvider {
provider: 'OpenAI',
maxTokenAllowed: 128000,
maxCompletionTokens: 32000,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'o1-preview-smartai',
- label: 'o1-preview (SmartAI)',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 32000,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
},
// o1-mini: 128k context, 65k output limit (reasoning model)
- {
- name: 'o1-mini',
- label: 'o1-mini',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 65000,
- supportsSmartAI: false, // Base model without SmartAI
- },
- {
- name: 'o1-mini-smartai',
- label: 'o1-mini (SmartAI)',
- provider: 'OpenAI',
- maxTokenAllowed: 128000,
- maxCompletionTokens: 65000,
- supportsSmartAI: true,
- isSmartAIEnabled: true,
- },
+ { name: 'o1-mini', label: 'o1-mini', provider: 'OpenAI', maxTokenAllowed: 128000, maxCompletionTokens: 65000 },
];
async getDynamicModels(
@@ -187,7 +125,6 @@ export default class OpenAIProvider extends BaseProvider {
provider: this.name,
maxTokenAllowed: Math.min(contextWindow, 128000), // Cap at 128k for safety
maxCompletionTokens,
- supportsSmartAI: true, // All OpenAI models support SmartAI
};
});
}
@@ -216,9 +153,6 @@ export default class OpenAIProvider extends BaseProvider {
apiKey,
});
- // Handle SmartAI variant by using the base model name
- const actualModel = model.replace('-smartai', '');
-
- return openai(actualModel);
+ return openai(model);
}
}
diff --git a/app/lib/modules/llm/registry.ts b/app/lib/modules/llm/registry.ts
index 27a5dd9508..a28e4f9f3a 100644
--- a/app/lib/modules/llm/registry.ts
+++ b/app/lib/modules/llm/registry.ts
@@ -1,4 +1,4 @@
-import { AnthropicProvider } from './providers/anthropic';
+import AnthropicProvider from './providers/anthropic';
import CohereProvider from './providers/cohere';
import DeepseekProvider from './providers/deepseek';
import GoogleProvider from './providers/google';
diff --git a/app/lib/modules/llm/types.ts b/app/lib/modules/llm/types.ts
index 21cabdea8f..330886deec 100644
--- a/app/lib/modules/llm/types.ts
+++ b/app/lib/modules/llm/types.ts
@@ -11,12 +11,6 @@ export interface ModelInfo {
/** Maximum completion/output tokens - how many tokens the model can generate. If not specified, falls back to provider defaults */
maxCompletionTokens?: number;
-
- /** Indicates if this model supports SmartAI enhanced feedback */
- supportsSmartAI?: boolean;
-
- /** Indicates if SmartAI is currently enabled for this model variant */
- isSmartAIEnabled?: boolean;
}
export interface ProviderInfo {
diff --git a/app/lib/persistence/userDb.ts b/app/lib/persistence/userDb.ts
deleted file mode 100644
index 5a0ad7bb74..0000000000
--- a/app/lib/persistence/userDb.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-import { createScopedLogger } from '~/utils/logger';
-import type { ChatHistoryItem } from './useChatHistory';
-import { authStore } from '~/lib/stores/auth';
-
-export interface IUserChatMetadata {
- userId: string;
- gitUrl?: string;
- gitBranch?: string;
- netlifySiteId?: string;
-}
-
-const logger = createScopedLogger('UserChatHistory');
-
-/**
- * Open user-specific database
- */
-export async function openUserDatabase(): Promise {
- if (typeof indexedDB === 'undefined') {
- console.error('indexedDB is not available in this environment.');
- return undefined;
- }
-
- const authState = authStore.get();
-
- if (!authState.user?.id) {
- console.error('No authenticated user found.');
- return undefined;
- }
-
- // Use user-specific database name
- const dbName = `boltHistory_${authState.user.id}`;
-
- return new Promise((resolve) => {
- const request = indexedDB.open(dbName, 1);
-
- request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
- const db = (event.target as IDBOpenDBRequest).result;
-
- if (!db.objectStoreNames.contains('chats')) {
- const store = db.createObjectStore('chats', { keyPath: 'id' });
- store.createIndex('id', 'id', { unique: true });
- store.createIndex('urlId', 'urlId', { unique: true });
- store.createIndex('userId', 'userId', { unique: false });
- store.createIndex('timestamp', 'timestamp', { unique: false });
- }
-
- if (!db.objectStoreNames.contains('snapshots')) {
- db.createObjectStore('snapshots', { keyPath: 'chatId' });
- }
-
- if (!db.objectStoreNames.contains('settings')) {
- db.createObjectStore('settings', { keyPath: 'key' });
- }
-
- if (!db.objectStoreNames.contains('workspaces')) {
- const workspaceStore = db.createObjectStore('workspaces', { keyPath: 'id' });
- workspaceStore.createIndex('name', 'name', { unique: false });
- workspaceStore.createIndex('createdAt', 'createdAt', { unique: false });
- }
- };
-
- request.onsuccess = (event: Event) => {
- resolve((event.target as IDBOpenDBRequest).result);
- };
-
- request.onerror = (event: Event) => {
- resolve(undefined);
- logger.error((event.target as IDBOpenDBRequest).error);
- };
- });
-}
-
-/**
- * Get all chats for current user
- */
-export async function getUserChats(db: IDBDatabase): Promise {
- const authState = authStore.get();
-
- if (!authState.user?.id) {
- return [];
- }
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('chats', 'readonly');
- const store = transaction.objectStore('chats');
- const request = store.getAll();
-
- request.onsuccess = () => {
- // Filter by userId and sort by timestamp
- const chats = (request.result as ChatHistoryItem[]).sort(
- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
- );
-
- resolve(chats);
- };
-
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Save user-specific settings
- */
-export async function saveUserSetting(db: IDBDatabase, key: string, value: any): Promise {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('settings', 'readwrite');
- const store = transaction.objectStore('settings');
-
- const request = store.put({ key, value, updatedAt: new Date().toISOString() });
-
- request.onsuccess = () => resolve();
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Load user-specific settings
- */
-export async function loadUserSetting(db: IDBDatabase, key: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('settings', 'readonly');
- const store = transaction.objectStore('settings');
- const request = store.get(key);
-
- request.onsuccess = () => {
- const result = request.result;
- resolve(result ? result.value : null);
- };
-
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Create a workspace for the user
- */
-export interface Workspace {
- id: string;
- name: string;
- description?: string;
- createdAt: string;
- lastAccessed?: string;
- files?: Record;
-}
-
-export async function createWorkspace(db: IDBDatabase, workspace: Omit): Promise {
- const authState = authStore.get();
-
- if (!authState.user?.id) {
- throw new Error('No authenticated user');
- }
-
- const workspaceId = `workspace_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('workspaces', 'readwrite');
- const store = transaction.objectStore('workspaces');
-
- const fullWorkspace: Workspace = {
- id: workspaceId,
- ...workspace,
- };
-
- const request = store.add(fullWorkspace);
-
- request.onsuccess = () => resolve(workspaceId);
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Get user workspaces
- */
-export async function getUserWorkspaces(db: IDBDatabase): Promise {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('workspaces', 'readonly');
- const store = transaction.objectStore('workspaces');
- const request = store.getAll();
-
- request.onsuccess = () => {
- const workspaces = (request.result as Workspace[]).sort(
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
- );
- resolve(workspaces);
- };
-
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Delete a workspace
- */
-export async function deleteWorkspace(db: IDBDatabase, workspaceId: string): Promise {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction('workspaces', 'readwrite');
- const store = transaction.objectStore('workspaces');
- const request = store.delete(workspaceId);
-
- request.onsuccess = () => resolve();
- request.onerror = () => reject(request.error);
- });
-}
-
-/**
- * Get user statistics
- */
-export async function getUserStats(db: IDBDatabase): Promise<{
- totalChats: number;
- totalWorkspaces: number;
- lastActivity?: string;
- storageUsed?: number;
-}> {
- try {
- const [chats, workspaces] = await Promise.all([getUserChats(db), getUserWorkspaces(db)]);
-
- // Calculate last activity
- let lastActivity: string | undefined;
-
- const allTimestamps = [
- ...chats.map((c) => c.timestamp),
- ...workspaces.map((w) => w.lastAccessed || w.createdAt),
- ].filter(Boolean);
-
- if (allTimestamps.length > 0) {
- lastActivity = allTimestamps.sort().reverse()[0];
- }
-
- return {
- totalChats: chats.length,
- totalWorkspaces: workspaces.length,
- lastActivity,
- };
- } catch (error) {
- logger.error('Failed to get user stats:', error);
- return {
- totalChats: 0,
- totalWorkspaces: 0,
- };
- }
-}
diff --git a/app/lib/stores/auth.ts b/app/lib/stores/auth.ts
deleted file mode 100644
index ecaf61487e..0000000000
--- a/app/lib/stores/auth.ts
+++ /dev/null
@@ -1,300 +0,0 @@
-import { atom, map } from 'nanostores';
-import type { UserProfile } from '~/lib/utils/fileUserStorage';
-import Cookies from 'js-cookie';
-
-export interface AuthState {
- isAuthenticated: boolean;
- user: Omit | null;
- token: string | null;
- loading: boolean;
-}
-
-// Authentication state store
-export const authStore = map({
- isAuthenticated: false,
- user: null,
- token: null,
- loading: true,
-});
-
-// Remember me preference
-export const rememberMeStore = atom(false);
-
-// Session timeout tracking
-let sessionTimeout: NodeJS.Timeout | null = null;
-const SESSION_TIMEOUT = 7 * 24 * 60 * 60 * 1000; // 7 days
-
-/**
- * Initialize auth from stored token
- */
-export async function initializeAuth(): Promise {
- if (typeof window === 'undefined') {
- return;
- }
-
- authStore.setKey('loading', true);
-
- try {
- const token = Cookies.get('auth_token');
-
- if (token) {
- // Verify token with backend
- const response = await fetch('/api/auth/verify', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- });
-
- if (response.ok) {
- const data = (await response.json()) as { user: Omit };
- setAuthState({
- isAuthenticated: true,
- user: data.user,
- token,
- loading: false,
- });
- startSessionTimer();
- } else {
- // Token is invalid, clear it
- clearAuth();
- }
- } else {
- authStore.setKey('loading', false);
- }
- } catch (error) {
- console.error('Failed to initialize auth:', error);
- authStore.setKey('loading', false);
- }
-}
-
-/**
- * Set authentication state
- */
-export function setAuthState(state: AuthState): void {
- authStore.set(state);
-
- if (state.token) {
- // Store token in cookie
- const cookieOptions = rememberMeStore.get()
- ? { expires: 7 } // 7 days
- : undefined; // Session cookie
-
- Cookies.set('auth_token', state.token, cookieOptions);
-
- // Store user preferences in localStorage
- if (state.user) {
- localStorage.setItem(`bolt_user_${state.user.id}`, JSON.stringify(state.user.preferences || {}));
- }
- }
-}
-
-/**
- * Login user
- */
-export async function login(
- username: string,
- password: string,
- rememberMe: boolean = false,
-): Promise<{ success: boolean; error?: string }> {
- try {
- const response = await fetch('/api/auth/login', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ username, password }),
- });
-
- const data = (await response.json()) as {
- success?: boolean;
- error?: string;
- user?: Omit;
- token?: string;
- };
-
- if (response.ok) {
- rememberMeStore.set(rememberMe);
- setAuthState({
- isAuthenticated: true,
- user: data.user || null,
- token: data.token || null,
- loading: false,
- });
- startSessionTimer();
-
- return { success: true };
- } else {
- return { success: false, error: data.error || 'Login failed' };
- }
- } catch (error) {
- console.error('Login error:', error);
- return { success: false, error: 'Network error' };
- }
-}
-
-/**
- * Signup new user
- */
-export async function signup(
- username: string,
- password: string,
- firstName: string,
- avatar?: string,
-): Promise<{ success: boolean; error?: string }> {
- try {
- const response = await fetch('/api/auth/signup', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ username, password, firstName, avatar }),
- });
-
- const data = (await response.json()) as {
- success?: boolean;
- error?: string;
- user?: Omit;
- token?: string;
- };
-
- if (response.ok) {
- setAuthState({
- isAuthenticated: true,
- user: data.user || null,
- token: data.token || null,
- loading: false,
- });
- startSessionTimer();
-
- return { success: true };
- } else {
- return { success: false, error: data.error || 'Signup failed' };
- }
- } catch (error) {
- console.error('Signup error:', error);
- return { success: false, error: 'Network error' };
- }
-}
-
-/**
- * Logout user
- */
-export async function logout(): Promise {
- const state = authStore.get();
-
- if (state.token) {
- try {
- await fetch('/api/auth/logout', {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${state.token}`,
- },
- });
- } catch (error) {
- console.error('Logout error:', error);
- }
- }
-
- clearAuth();
-}
-
-/**
- * Clear authentication state
- */
-function clearAuth(): void {
- authStore.set({
- isAuthenticated: false,
- user: null,
- token: null,
- loading: false,
- });
-
- Cookies.remove('auth_token');
- stopSessionTimer();
-
- // Clear user-specific localStorage
- const currentUser = authStore.get().user;
-
- if (currentUser?.id) {
- // Keep preferences but clear sensitive data
- const prefs = localStorage.getItem(`bolt_user_${currentUser.id}`);
-
- if (prefs) {
- try {
- const parsed = JSON.parse(prefs);
- delete parsed.deploySettings;
- delete parsed.githubSettings;
- localStorage.setItem(`bolt_user_${currentUser.id}`, JSON.stringify(parsed));
- } catch {}
- }
- }
-}
-
-/**
- * Start session timer
- */
-function startSessionTimer(): void {
- stopSessionTimer();
-
- if (!rememberMeStore.get()) {
- sessionTimeout = setTimeout(() => {
- logout();
-
- if (typeof window !== 'undefined') {
- window.location.href = '/auth';
- }
- }, SESSION_TIMEOUT);
- }
-}
-
-/**
- * Stop session timer
- */
-function stopSessionTimer(): void {
- if (sessionTimeout) {
- clearTimeout(sessionTimeout);
- sessionTimeout = null;
- }
-}
-
-/**
- * Update user profile
- */
-export async function updateProfile(
- updates: Partial>,
-): Promise {
- const state = authStore.get();
-
- if (!state.token || !state.user) {
- return false;
- }
-
- try {
- const response = await fetch('/api/users/profile', {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${state.token}`,
- },
- body: JSON.stringify(updates),
- });
-
- if (response.ok) {
- const updatedUser = (await response.json()) as Omit;
- authStore.setKey('user', updatedUser);
-
- return true;
- }
- } catch (error) {
- console.error('Failed to update profile:', error);
- }
-
- return false;
-}
-
-// Initialize auth on load
-if (typeof window !== 'undefined') {
- initializeAuth();
-}
diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts
index 9aef3b5b58..6fdf35eaf4 100644
--- a/app/lib/stores/workbench.ts
+++ b/app/lib/stores/workbench.ts
@@ -223,13 +223,10 @@ export class WorkbenchStore {
}
async saveFile(filePath: string) {
- console.log(`[WorkbenchStore] saveFile called for: ${filePath}`);
-
const documents = this.#editorStore.documents.get();
const document = documents[filePath];
if (document === undefined) {
- console.warn(`[WorkbenchStore] No document found for: ${filePath}`);
return;
}
@@ -239,39 +236,21 @@ export class WorkbenchStore {
* This is a more complex feature that would be implemented in a future update
*/
- try {
- console.log(`[WorkbenchStore] Saving to file system: ${filePath}`);
- await this.#filesStore.saveFile(filePath, document.value);
- console.log(`[WorkbenchStore] File saved successfully: ${filePath}`);
-
- const newUnsavedFiles = new Set(this.unsavedFiles.get());
- const wasUnsaved = newUnsavedFiles.has(filePath);
- newUnsavedFiles.delete(filePath);
-
- console.log(`[WorkbenchStore] Updating unsaved files:`, {
- filePath,
- wasUnsaved,
- previousCount: this.unsavedFiles.get().size,
- newCount: newUnsavedFiles.size,
- remainingFiles: Array.from(newUnsavedFiles),
- });
+ await this.#filesStore.saveFile(filePath, document.value);
- this.unsavedFiles.set(newUnsavedFiles);
- } catch (error) {
- console.error(`[WorkbenchStore] Failed to save file ${filePath}:`, error);
- throw error;
- }
+ const newUnsavedFiles = new Set(this.unsavedFiles.get());
+ newUnsavedFiles.delete(filePath);
+
+ this.unsavedFiles.set(newUnsavedFiles);
}
async saveCurrentDocument() {
const currentDocument = this.currentDocument.get();
if (currentDocument === undefined) {
- console.warn('[WorkbenchStore] No current document to save');
return;
}
- console.log(`[WorkbenchStore] Saving current document: ${currentDocument.filePath}`);
await this.saveFile(currentDocument.filePath);
}
@@ -293,14 +272,9 @@ export class WorkbenchStore {
}
async saveAllFiles() {
- const filesToSave = Array.from(this.unsavedFiles.get());
- console.log(`[WorkbenchStore] saveAllFiles called for ${filesToSave.length} files:`, filesToSave);
-
- for (const filePath of filesToSave) {
+ for (const filePath of this.unsavedFiles.get()) {
await this.saveFile(filePath);
}
-
- console.log('[WorkbenchStore] saveAllFiles complete. Remaining unsaved:', Array.from(this.unsavedFiles.get()));
}
getFileModifcations() {
diff --git a/app/lib/utils/crypto.ts b/app/lib/utils/crypto.ts
deleted file mode 100644
index 0766f58032..0000000000
--- a/app/lib/utils/crypto.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import bcrypt from 'bcryptjs';
-import jwt from 'jsonwebtoken';
-
-// Use a secure secret key (in production, this should be an environment variable)
-const JWT_SECRET = process.env.JWT_SECRET || 'bolt-multi-user-secret-key-2024-secure';
-const SALT_ROUNDS = 10;
-
-export interface JWTPayload {
- userId: string;
- username: string;
- firstName: string;
- exp?: number;
-}
-
-/**
- * Hash a password using bcrypt
- */
-export async function hashPassword(password: string): Promise {
- return bcrypt.hash(password, SALT_ROUNDS);
-}
-
-/**
- * Verify a password against a hash
- */
-export async function verifyPassword(password: string, hash: string): Promise {
- return bcrypt.compare(password, hash);
-}
-
-/**
- * Generate a JWT token
- */
-export function generateToken(payload: Omit): string {
- return jwt.sign(
- {
- ...payload,
- exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 days
- },
- JWT_SECRET,
- );
-}
-
-/**
- * Verify and decode a JWT token
- */
-export function verifyToken(token: string): JWTPayload | null {
- try {
- return jwt.verify(token, JWT_SECRET) as JWTPayload;
- } catch {
- return null;
- }
-}
-
-/**
- * Generate a secure user ID
- */
-export function generateUserId(): string {
- return `user_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
-}
-
-/**
- * Validate password strength
- */
-export function validatePassword(password: string): { valid: boolean; errors: string[] } {
- const errors: string[] = [];
-
- if (password.length < 8) {
- errors.push('Password must be at least 8 characters long');
- }
-
- if (!/[A-Z]/.test(password)) {
- errors.push('Password must contain at least one uppercase letter');
- }
-
- if (!/[a-z]/.test(password)) {
- errors.push('Password must contain at least one lowercase letter');
- }
-
- if (!/[0-9]/.test(password)) {
- errors.push('Password must contain at least one number');
- }
-
- return {
- valid: errors.length === 0,
- errors,
- };
-}
diff --git a/app/lib/utils/fileUserStorage.ts b/app/lib/utils/fileUserStorage.ts
deleted file mode 100644
index 1ddf48373e..0000000000
--- a/app/lib/utils/fileUserStorage.ts
+++ /dev/null
@@ -1,338 +0,0 @@
-import fs from 'fs/promises';
-import path from 'path';
-import { generateUserId, hashPassword } from './crypto';
-
-const USERS_DIR = path.join(process.cwd(), '.users');
-const USERS_INDEX_FILE = path.join(USERS_DIR, 'users.json');
-const USER_DATA_DIR = path.join(USERS_DIR, 'data');
-
-export interface UserProfile {
- id: string;
- username: string;
- firstName: string;
- passwordHash: string;
- avatar?: string;
- createdAt: string;
- lastLogin?: string;
- preferences: UserPreferences;
-}
-
-export interface UserPreferences {
- theme: 'light' | 'dark';
- deploySettings: {
- netlify?: any;
- vercel?: any;
- };
- githubSettings?: any;
- workspaceConfig: any;
-}
-
-export interface SecurityLog {
- timestamp: string;
- userId?: string;
- username?: string;
- action: 'login' | 'logout' | 'signup' | 'delete' | 'error' | 'failed_login';
- details: string;
- ip?: string;
-}
-
-/**
- * Initialize the user storage system
- */
-export async function initializeUserStorage(): Promise {
- try {
- // Create directories if they don't exist
- await fs.mkdir(USERS_DIR, { recursive: true });
- await fs.mkdir(USER_DATA_DIR, { recursive: true });
-
- // Create users index if it doesn't exist
- try {
- await fs.access(USERS_INDEX_FILE);
- } catch {
- await fs.writeFile(USERS_INDEX_FILE, JSON.stringify({ users: [] }, null, 2));
- }
- } catch (error) {
- console.error('Failed to initialize user storage:', error);
- throw error;
- }
-}
-
-/**
- * Get all users (without passwords)
- */
-export async function getAllUsers(): Promise[]> {
- try {
- await initializeUserStorage();
-
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- return users.map(({ passwordHash, ...user }) => user);
- } catch (error) {
- console.error('Failed to get users:', error);
- return [];
- }
-}
-
-/**
- * Get a user by username
- */
-export async function getUserByUsername(username: string): Promise {
- try {
- await initializeUserStorage();
-
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- return users.find((u) => u.username === username) || null;
- } catch (error) {
- console.error('Failed to get user:', error);
- return null;
- }
-}
-
-/**
- * Get a user by ID
- */
-export async function getUserById(id: string): Promise {
- try {
- await initializeUserStorage();
-
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- return users.find((u) => u.id === id) || null;
- } catch (error) {
- console.error('Failed to get user:', error);
- return null;
- }
-}
-
-/**
- * Create a new user
- */
-export async function createUser(
- username: string,
- password: string,
- firstName: string,
- avatar?: string,
-): Promise {
- try {
- await initializeUserStorage();
-
- // Check if username already exists
- const existingUser = await getUserByUsername(username);
-
- if (existingUser) {
- throw new Error('Username already exists');
- }
-
- // Create new user
- const newUser: UserProfile = {
- id: generateUserId(),
- username,
- firstName,
- passwordHash: await hashPassword(password),
- avatar,
- createdAt: new Date().toISOString(),
- preferences: {
- theme: 'dark',
- deploySettings: {},
- workspaceConfig: {},
- },
- };
-
- // Load existing users
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- // Add new user
- users.push(newUser);
-
- // Save updated users
- await fs.writeFile(USERS_INDEX_FILE, JSON.stringify({ users }, null, 2));
-
- // Create user data directory
- const userDataDir = path.join(USER_DATA_DIR, newUser.id);
- await fs.mkdir(userDataDir, { recursive: true });
-
- // Log the signup
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId: newUser.id,
- username: newUser.username,
- action: 'signup',
- details: `User ${newUser.username} created successfully`,
- });
-
- return newUser;
- } catch (error) {
- console.error('Failed to create user:', error);
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- action: 'error',
- details: `Failed to create user ${username}: ${error}`,
- });
- throw error;
- }
-}
-
-/**
- * Update user profile
- */
-export async function updateUser(userId: string, updates: Partial): Promise {
- try {
- await initializeUserStorage();
-
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- const userIndex = users.findIndex((u) => u.id === userId);
-
- if (userIndex === -1) {
- return false;
- }
-
- // Update user (excluding certain fields)
- const { id, username, passwordHash, ...safeUpdates } = updates;
- users[userIndex] = {
- ...users[userIndex],
- ...safeUpdates,
- };
-
- // Save updated users
- await fs.writeFile(USERS_INDEX_FILE, JSON.stringify({ users }, null, 2));
-
- return true;
- } catch (error) {
- console.error('Failed to update user:', error);
- return false;
- }
-}
-
-/**
- * Update user's last login time
- */
-export async function updateLastLogin(userId: string): Promise {
- await updateUser(userId, { lastLogin: new Date().toISOString() });
-}
-
-/**
- * Delete a user
- */
-export async function deleteUser(userId: string): Promise {
- try {
- await initializeUserStorage();
-
- const data = await fs.readFile(USERS_INDEX_FILE, 'utf-8');
- const { users } = JSON.parse(data) as { users: UserProfile[] };
-
- const userIndex = users.findIndex((u) => u.id === userId);
-
- if (userIndex === -1) {
- return false;
- }
-
- const deletedUser = users[userIndex];
-
- // Remove user from list
- users.splice(userIndex, 1);
-
- // Save updated users
- await fs.writeFile(USERS_INDEX_FILE, JSON.stringify({ users }, null, 2));
-
- // Delete user data directory
- const userDataDir = path.join(USER_DATA_DIR, userId);
-
- try {
- await fs.rm(userDataDir, { recursive: true, force: true });
- } catch (error) {
- console.warn(`Failed to delete user data directory: ${error}`);
- }
-
- // Log the deletion
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId,
- username: deletedUser.username,
- action: 'delete',
- details: `User ${deletedUser.username} deleted`,
- });
-
- return true;
- } catch (error) {
- console.error('Failed to delete user:', error);
- return false;
- }
-}
-
-/**
- * Save user-specific data
- */
-export async function saveUserData(userId: string, key: string, data: any): Promise {
- try {
- const userDataDir = path.join(USER_DATA_DIR, userId);
- await fs.mkdir(userDataDir, { recursive: true });
-
- const filePath = path.join(userDataDir, `${key}.json`);
- await fs.writeFile(filePath, JSON.stringify(data, null, 2));
- } catch (error) {
- console.error(`Failed to save user data for ${userId}:`, error);
- throw error;
- }
-}
-
-/**
- * Load user-specific data
- */
-export async function loadUserData(userId: string, key: string): Promise {
- try {
- const filePath = path.join(USER_DATA_DIR, userId, `${key}.json`);
- const data = await fs.readFile(filePath, 'utf-8');
-
- return JSON.parse(data);
- } catch {
- return null;
- }
-}
-
-/**
- * Log security events
- */
-export async function logSecurityEvent(event: SecurityLog): Promise {
- try {
- const logFile = path.join(USERS_DIR, 'security.log');
- const logEntry = `${JSON.stringify(event)}\n`;
-
- await fs.appendFile(logFile, logEntry);
- } catch (error) {
- console.error('Failed to log security event:', error);
- }
-}
-
-/**
- * Get security logs
- */
-export async function getSecurityLogs(limit: number = 100): Promise {
- try {
- const logFile = path.join(USERS_DIR, 'security.log');
- const data = await fs.readFile(logFile, 'utf-8');
-
- const logs = data
- .trim()
- .split('\n')
- .filter((line) => line)
- .map((line) => {
- try {
- return JSON.parse(line) as SecurityLog;
- } catch {
- return null;
- }
- })
- .filter(Boolean) as SecurityLog[];
-
- return logs.slice(-limit).reverse();
- } catch {
- return [];
- }
-}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index dda2285e76..65df404aad 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,173 +1,28 @@
import { json, type MetaFunction } from '@remix-run/cloudflare';
-import { useLoaderData } from '@remix-run/react';
import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header';
import BackgroundRays from '~/components/ui/BackgroundRays';
-import { useEffect, useState } from 'react';
-import { providersStore } from '~/lib/stores/settings';
-import { authStore } from '~/lib/stores/auth';
-import { useNavigate } from '@remix-run/react';
-import { motion, AnimatePresence } from 'framer-motion';
export const meta: MetaFunction = () => {
- return [
- { title: 'Bolt.gives' },
- {
- name: 'description',
- content: 'Build web applications with AI assistance - Enhanced fork with advanced features',
- },
- ];
+ return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
};
-export const loader = ({ context }: { context: any }) => {
- // Check which local providers are configured
- const configuredProviders: string[] = [];
-
- // Check Ollama
- if (context.cloudflare?.env?.OLLAMA_API_BASE_URL || process.env?.OLLAMA_API_BASE_URL) {
- configuredProviders.push('Ollama');
- }
-
- // Check LMStudio
- if (context.cloudflare?.env?.LMSTUDIO_API_BASE_URL || process.env?.LMSTUDIO_API_BASE_URL) {
- configuredProviders.push('LMStudio');
- }
-
- // Check OpenAILike
- if (context.cloudflare?.env?.OPENAI_LIKE_API_BASE_URL || process.env?.OPENAI_LIKE_API_BASE_URL) {
- configuredProviders.push('OpenAILike');
- }
-
- return json({ configuredProviders });
-};
+export const loader = () => json({});
/**
- * Landing page component for Bolt.gives
- * Enhanced fork with multi-user authentication, advanced features, and provider auto-detection
+ * Landing page component for Bolt
* Note: Settings functionality should ONLY be accessed through the sidebar menu.
+ * Do not add settings button/panel to this landing page as it was intentionally removed
+ * to keep the UI clean and consistent with the design system.
*/
export default function Index() {
- const data = useLoaderData<{ configuredProviders: string[] }>();
- const [showMultiUserBanner, setShowMultiUserBanner] = useState(false);
- const navigate = useNavigate();
-
- useEffect(() => {
- // Enable configured providers if they haven't been manually configured yet
- if (data?.configuredProviders && data.configuredProviders.length > 0) {
- const savedSettings = localStorage.getItem('provider_settings');
-
- if (!savedSettings) {
- // No saved settings, so enable the configured providers
- const currentProviders = providersStore.get();
- data.configuredProviders.forEach((providerName) => {
- if (currentProviders[providerName]) {
- providersStore.setKey(providerName, {
- ...currentProviders[providerName],
- settings: {
- ...currentProviders[providerName].settings,
- enabled: true,
- },
- });
- }
- });
-
- // Save to localStorage so this only happens once
- localStorage.setItem('provider_settings', JSON.stringify(providersStore.get()));
- }
- }
- }, [data?.configuredProviders]);
-
- useEffect(() => {
- // Check if user is authenticated
- const authState = authStore.get();
-
- // Show banner only if not authenticated and hasn't been dismissed
- const bannerDismissed = localStorage.getItem('multiUserBannerDismissed');
-
- if (!authState.isAuthenticated && !bannerDismissed) {
- setTimeout(() => setShowMultiUserBanner(true), 2000);
- }
- }, []);
-
- const handleActivateMultiUser = () => {
- navigate('/auth');
- };
-
- const handleDismissBanner = () => {
- setShowMultiUserBanner(false);
- localStorage.setItem('multiUserBannerDismissed', 'true');
- };
-
return (
}>{() =>
}
-
- {/* Optional Multi-User Activation Banner */}
-
- {showMultiUserBanner && (
-
-
-
-
-
-
-
-
-
-
-
-
-
- Unlock Multi-User Features
-
-
- Save your projects, personalized settings, and collaborate with workspace isolation.
-
-
-
- Activate Now
-
-
- Continue as Guest
-
-
-
-
-
-
- )}
-
);
}
diff --git a/app/routes/admin.users.tsx b/app/routes/admin.users.tsx
deleted file mode 100644
index 5247bd6e08..0000000000
--- a/app/routes/admin.users.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useNavigate } from '@remix-run/react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { useStore } from '@nanostores/react';
-import { authStore } from '~/lib/stores/auth';
-import { ProtectedRoute } from '~/components/auth/ProtectedRoute';
-import { classNames } from '~/utils/classNames';
-
-interface User {
- id: string;
- username: string;
- firstName: string;
- avatar?: string;
- createdAt: string;
- lastLogin?: string;
-}
-
-export default function UserManagement() {
- const navigate = useNavigate();
- const authState = useStore(authStore);
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [searchQuery, setSearchQuery] = useState('');
- const [selectedUser, setSelectedUser] = useState(null);
- const [showDeleteModal, setShowDeleteModal] = useState(false);
- const [deleting, setDeleting] = useState(false);
-
- useEffect(() => {
- fetchUsers();
- }, []);
-
- const fetchUsers = async () => {
- try {
- const response = await fetch('/api/users', {
- headers: {
- Authorization: `Bearer ${authState.token}`,
- },
- });
-
- if (response.ok) {
- const data = (await response.json()) as { users: User[] };
- setUsers(data.users);
- }
- } catch (error) {
- console.error('Failed to fetch users:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const handleDeleteUser = async () => {
- if (!selectedUser) {
- return;
- }
-
- setDeleting(true);
-
- try {
- const response = await fetch(`/api/users/${selectedUser.id}`, {
- method: 'DELETE',
- headers: {
- Authorization: `Bearer ${authState.token}`,
- },
- });
-
- if (response.ok) {
- setUsers(users.filter((u) => u.id !== selectedUser.id));
- setShowDeleteModal(false);
- setSelectedUser(null);
- }
- } catch (error) {
- console.error('Failed to delete user:', error);
- } finally {
- setDeleting(false);
- }
- };
-
- const filteredUsers = users.filter(
- (user) =>
- user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
- user.firstName.toLowerCase().includes(searchQuery.toLowerCase()),
- );
-
- return (
-
-
- {/* Header */}
-
-
-
-
-
navigate('/')}
- className="p-2 rounded-lg hover:bg-bolt-elements-background-depth-3 transition-colors"
- >
-
-
-
-
User Management
-
Manage system users
-
-
-
-
-
- setSearchQuery(e.target.value)}
- className={classNames(
- 'w-64 px-4 py-2 pl-10 rounded-lg',
- 'bg-bolt-elements-background-depth-1',
- 'border border-bolt-elements-borderColor',
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
- 'focus:outline-none focus:ring-2 focus:ring-accent-500',
- )}
- />
-
-
-
-
navigate('/auth')}
- className={classNames(
- 'px-4 py-2 rounded-lg',
- 'bg-accent-500 text-white',
- 'hover:bg-accent-600',
- 'transition-colors',
- 'flex items-center gap-2',
- )}
- >
-
- Add User
-
-
-
-
-
-
- {/* User Stats */}
-
-
-
-
Total Users
-
{users.length}
-
-
-
Active Today
-
- {
- users.filter((u) => {
- if (!u.lastLogin) {
- return false;
- }
-
- const lastLogin = new Date(u.lastLogin);
- const today = new Date();
-
- return lastLogin.toDateString() === today.toDateString();
- }).length
- }
-
-
-
-
New This Week
-
- {
- users.filter((u) => {
- const created = new Date(u.createdAt);
- const weekAgo = new Date();
- weekAgo.setDate(weekAgo.getDate() - 7);
-
- return created > weekAgo;
- }).length
- }
-
-
-
-
-
-
- {/* User List */}
-
- {loading ? (
-
-
-
- ) : filteredUsers.length === 0 ? (
-
-
-
- {searchQuery ? 'No users found matching your search' : 'No users yet'}
-
-
- ) : (
-
-
- {filteredUsers.map((user, index) => (
-
-
-
-
- {user.avatar ? (
-
- ) : (
-
- {user.firstName[0].toUpperCase()}
-
- )}
-
-
-
- {user.firstName}
- {user.id === authState.user?.id && (
-
- You
-
- )}
-
-
@{user.username}
-
-
-
-
-
-
-
- {user.id !== authState.user?.id && (
- {
- setSelectedUser(user);
- setShowDeleteModal(true);
- }}
- className="p-1 rounded hover:bg-red-500/10 transition-colors"
- title="Delete user"
- >
-
-
- )}
-
-
-
-
-
-
- Joined {new Date(user.createdAt).toLocaleDateString()}
-
- {user.lastLogin && (
-
-
- Last active {new Date(user.lastLogin).toLocaleDateString()}
-
- )}
-
-
- ))}
-
-
- )}
-
-
- {/* Delete Confirmation Modal */}
-
- {showDeleteModal && selectedUser && (
- !deleting && setShowDeleteModal(false)}
- >
- e.stopPropagation()}
- >
- Delete User
-
- Are you sure you want to delete{' '}
- @{selectedUser.username} ? This
- action cannot be undone and will permanently remove all user data.
-
-
-
- setShowDeleteModal(false)}
- disabled={deleting}
- className={classNames(
- 'flex-1 px-4 py-2 rounded-lg',
- 'border border-bolt-elements-borderColor',
- 'text-bolt-elements-textPrimary',
- 'hover:bg-bolt-elements-background-depth-3',
- 'disabled:opacity-50',
- 'transition-colors',
- )}
- >
- Cancel
-
-
- {deleting ? (
- <>
-
- Deleting...
- >
- ) : (
- <>
-
- Delete User
- >
- )}
-
-
-
-
- )}
-
-
-
- );
-}
diff --git a/app/routes/api.auth.login.ts b/app/routes/api.auth.login.ts
deleted file mode 100644
index 1037ff8bf0..0000000000
--- a/app/routes/api.auth.login.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { ActionFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { getUserByUsername, updateLastLogin, logSecurityEvent } from '~/lib/utils/fileUserStorage';
-import { verifyPassword, generateToken } from '~/lib/utils/crypto';
-
-export async function action({ request }: ActionFunctionArgs) {
- if (request.method !== 'POST') {
- return json({ error: 'Method not allowed' }, { status: 405 });
- }
-
- try {
- const body = (await request.json()) as { username?: string; password?: string };
- const { username, password } = body;
-
- if (!username || !password) {
- return json({ error: 'Username and password are required' }, { status: 400 });
- }
-
- // Get user from storage
- const user = await getUserByUsername(username);
-
- if (!user) {
- // Log failed login attempt
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- username,
- action: 'failed_login',
- details: `Failed login attempt for non-existent user: ${username}`,
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- return json({ error: 'Invalid username or password' }, { status: 401 });
- }
-
- // Verify password
- const isValid = await verifyPassword(password, user.passwordHash);
-
- if (!isValid) {
- // Log failed login attempt
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId: user.id,
- username: user.username,
- action: 'failed_login',
- details: `Failed login attempt with incorrect password`,
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- return json({ error: 'Invalid username or password' }, { status: 401 });
- }
-
- // Update last login time
- await updateLastLogin(user.id);
-
- // Generate JWT token
- const token = generateToken({
- userId: user.id,
- username: user.username,
- firstName: user.firstName,
- });
-
- // Log successful login
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId: user.id,
- username: user.username,
- action: 'login',
- details: 'Successful login',
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- // Return user data without password
- const { passwordHash, ...userWithoutPassword } = user;
-
- return json({
- success: true,
- user: userWithoutPassword,
- token,
- });
- } catch (error) {
- console.error('Login error:', error);
-
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- action: 'error',
- details: `Login error: ${error}`,
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/api.auth.logout.ts b/app/routes/api.auth.logout.ts
deleted file mode 100644
index 3d25c5582e..0000000000
--- a/app/routes/api.auth.logout.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { ActionFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { verifyToken } from '~/lib/utils/crypto';
-import { logSecurityEvent } from '~/lib/utils/fileUserStorage';
-
-export async function action({ request }: ActionFunctionArgs) {
- if (request.method !== 'POST') {
- return json({ error: 'Method not allowed' }, { status: 405 });
- }
-
- try {
- // Get token from Authorization header
- const authHeader = request.headers.get('Authorization');
-
- if (authHeader && authHeader.startsWith('Bearer ')) {
- const token = authHeader.substring(7);
- const payload = verifyToken(token);
-
- if (payload) {
- // Log logout event
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId: payload.userId,
- username: payload.username,
- action: 'logout',
- details: 'User logged out',
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
- }
- }
-
- return json({ success: true });
- } catch (error) {
- console.error('Logout error:', error);
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/api.auth.signup.ts b/app/routes/api.auth.signup.ts
deleted file mode 100644
index 849e1cbc29..0000000000
--- a/app/routes/api.auth.signup.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import type { ActionFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { createUser, getUserByUsername, logSecurityEvent } from '~/lib/utils/fileUserStorage';
-import { validatePassword, generateToken } from '~/lib/utils/crypto';
-
-export async function action({ request }: ActionFunctionArgs) {
- if (request.method !== 'POST') {
- return json({ error: 'Method not allowed' }, { status: 405 });
- }
-
- try {
- const body = (await request.json()) as {
- username?: string;
- password?: string;
- firstName?: string;
- avatar?: string;
- };
- const { username, password, firstName, avatar } = body;
-
- // Validate required fields
- if (!username || !password || !firstName) {
- return json({ error: 'Username, password, and first name are required' }, { status: 400 });
- }
-
- // Validate username format
- if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) {
- return json(
- {
- error: 'Username must be 3-20 characters and contain only letters, numbers, and underscores',
- },
- { status: 400 },
- );
- }
-
- // Validate password strength
- const passwordValidation = validatePassword(password);
-
- if (!passwordValidation.valid) {
- return json({ error: passwordValidation.errors.join('. ') }, { status: 400 });
- }
-
- // Check if username already exists
- const existingUser = await getUserByUsername(username);
-
- if (existingUser) {
- return json({ error: 'Username already exists' }, { status: 400 });
- }
-
- // Create new user
- const user = await createUser(username, password, firstName, avatar);
-
- if (!user) {
- return json({ error: 'Failed to create user' }, { status: 500 });
- }
-
- // Generate JWT token
- const token = generateToken({
- userId: user.id,
- username: user.username,
- firstName: user.firstName,
- });
-
- // Log successful signup
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- userId: user.id,
- username: user.username,
- action: 'signup',
- details: 'New user registration',
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- // Return user data without password
- const { passwordHash, ...userWithoutPassword } = user;
-
- return json({
- success: true,
- user: userWithoutPassword,
- token,
- });
- } catch (error) {
- console.error('Signup error:', error);
-
- await logSecurityEvent({
- timestamp: new Date().toISOString(),
- action: 'error',
- details: `Signup error: ${error}`,
- ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined,
- });
-
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/api.auth.verify.ts b/app/routes/api.auth.verify.ts
deleted file mode 100644
index f66585fba4..0000000000
--- a/app/routes/api.auth.verify.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { ActionFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { verifyToken } from '~/lib/utils/crypto';
-import { getUserById } from '~/lib/utils/fileUserStorage';
-
-export async function action({ request }: ActionFunctionArgs) {
- if (request.method !== 'POST') {
- return json({ error: 'Method not allowed' }, { status: 405 });
- }
-
- try {
- // Get token from Authorization header
- const authHeader = request.headers.get('Authorization');
-
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
- return json({ error: 'No token provided' }, { status: 401 });
- }
-
- const token = authHeader.substring(7);
- const payload = verifyToken(token);
-
- if (!payload) {
- return json({ error: 'Invalid token' }, { status: 401 });
- }
-
- // Get user from storage
- const user = await getUserById(payload.userId);
-
- if (!user) {
- return json({ error: 'User not found' }, { status: 404 });
- }
-
- // Return user data without password
- const { passwordHash, ...userWithoutPassword } = user;
-
- return json({
- success: true,
- user: userWithoutPassword,
- });
- } catch (error) {
- console.error('Token verification error:', error);
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts
index c316170e57..4deb05549f 100644
--- a/app/routes/api.chat.ts
+++ b/app/routes/api.chat.ts
@@ -13,7 +13,6 @@ import { createSummary } from '~/lib/.server/llm/create-summary';
import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils';
import type { DesignScheme } from '~/types/design-scheme';
import { MCPService } from '~/lib/services/mcpService';
-import { StreamRecoveryManager } from '~/lib/.server/llm/stream-recovery';
export async function action(args: ActionFunctionArgs) {
return chatAction(args);
@@ -75,22 +74,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const encoder: TextEncoder = new TextEncoder();
let progressCounter: number = 1;
- // Initialize stream recovery manager
- const recovery = new StreamRecoveryManager({
- maxRetries: 3,
- retryDelay: 2000,
- timeout: 45000, // 45 seconds timeout
- onTimeout: () => {
- logger.warn('Stream timeout detected - attempting recovery');
- },
- onRetry: (attempt) => {
- logger.info(`Stream recovery attempt ${attempt}`);
- },
- onError: (error) => {
- logger.error('Stream error in recovery:', error);
- },
- });
-
try {
const mcpService = MCPService.getInstance();
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
@@ -330,77 +313,28 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
});
(async () => {
- try {
- recovery.startMonitoring();
-
- let lastActivityTime = Date.now();
- const activityCheckInterval = 5000; // Check every 5 seconds
-
- // Set up activity monitoring
- const activityChecker = setInterval(() => {
- const timeSinceLastActivity = Date.now() - lastActivityTime;
-
- if (timeSinceLastActivity > 30000) {
- logger.warn(`No stream activity for ${timeSinceLastActivity}ms`);
-
- // Attempt to recover if stream appears stuck
- recovery.attemptRecovery();
+ for await (const part of result.fullStream) {
+ if (part.type === 'error') {
+ const error: any = part.error;
+ logger.error('Streaming error:', error);
+
+ // Enhanced error handling for common streaming issues
+ if (error.message?.includes('Invalid JSON response')) {
+ logger.error('Invalid JSON response detected - likely malformed API response');
+ } else if (error.message?.includes('token')) {
+ logger.error('Token-related error detected - possible token limit exceeded');
}
- }, activityCheckInterval);
-
- for await (const part of result.fullStream) {
- // Record activity
- lastActivityTime = Date.now();
- recovery.recordActivity();
-
- if (part.type === 'error') {
- const error: any = part.error;
- logger.error('Streaming error:', error);
-
- // Enhanced error handling for common streaming issues
- if (error.message?.includes('Invalid JSON response')) {
- logger.error('Invalid JSON response detected - likely malformed API response');
- } else if (error.message?.includes('token')) {
- logger.error('Token-related error detected - possible token limit exceeded');
- }
- // Attempt recovery for certain errors
- const canRecover = await recovery.handleError(error);
-
- if (!canRecover) {
- clearInterval(activityChecker);
- recovery.stop();
-
- return;
- }
- }
+ return;
}
-
- // Clean up
- clearInterval(activityChecker);
- recovery.stop();
- } catch (streamError) {
- logger.error('Fatal stream error:', streamError);
- recovery.stop();
- throw streamError;
}
})();
result.mergeIntoDataStream(dataStream);
},
onError: (error: any) => {
- // Stop recovery manager on error
- recovery.stop();
-
// Provide more specific error messages for common issues
const errorMessage = error.message || 'Unknown error';
- // Log detailed error for debugging
- logger.error('Chat API error:', {
- message: errorMessage,
- stack: error.stack,
- code: error.code,
- });
-
if (errorMessage.includes('model') && errorMessage.includes('not found')) {
return 'Custom error: Invalid model selected. Please check that the model name is correct and available.';
}
@@ -426,11 +360,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}
if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
- return 'Custom error: Network error or timeout. The connection was interrupted. Please try again or switch to a different AI model.';
- }
-
- if (errorMessage.includes('stream') || errorMessage.includes('hang')) {
- return 'Custom error: The conversation stream was interrupted. Please refresh the page and try again.';
+ return 'Custom error: Network error. Please check your internet connection and try again.';
}
return `Custom error: ${errorMessage}`;
@@ -473,32 +403,17 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}),
);
- // Set up cleanup for recovery manager
- const cleanupStream = dataStream.pipeThrough(
- new TransformStream({
- flush() {
- recovery.stop();
- },
- }),
- );
-
- return new Response(cleanupStream, {
+ return new Response(dataStream, {
status: 200,
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Text-Encoding': 'chunked',
- 'X-Accel-Buffering': 'no', // Disable nginx buffering
},
});
} catch (error: any) {
- logger.error('Fatal error in chat API:', error);
-
- // Ensure recovery manager is stopped on error
- if (typeof recovery !== 'undefined') {
- recovery.stop();
- }
+ logger.error(error);
const errorResponse = {
error: true,
diff --git a/app/routes/api.models.ts b/app/routes/api.models.ts
index 6f301ceebb..5fad834d00 100644
--- a/app/routes/api.models.ts
+++ b/app/routes/api.models.ts
@@ -8,7 +8,6 @@ interface ModelsResponse {
modelList: ModelInfo[];
providers: ProviderInfo[];
defaultProvider: ProviderInfo;
- configuredProviders?: string[];
}
let cachedProviders: ProviderInfo[] | null = null;
@@ -83,28 +82,9 @@ export async function loader({
});
}
- // Check which local providers are configured in environment
- const configuredProviders: string[] = [];
-
- // Check Ollama
- if (context.cloudflare?.env?.OLLAMA_API_BASE_URL || process.env?.OLLAMA_API_BASE_URL) {
- configuredProviders.push('Ollama');
- }
-
- // Check LMStudio
- if (context.cloudflare?.env?.LMSTUDIO_API_BASE_URL || process.env?.LMSTUDIO_API_BASE_URL) {
- configuredProviders.push('LMStudio');
- }
-
- // Check OpenAILike
- if (context.cloudflare?.env?.OPENAI_LIKE_API_BASE_URL || process.env?.OPENAI_LIKE_API_BASE_URL) {
- configuredProviders.push('OpenAILike');
- }
-
return json({
modelList,
providers,
defaultProvider,
- configuredProviders,
});
}
diff --git a/app/routes/api.netlify-quick-deploy.ts b/app/routes/api.netlify-quick-deploy.ts
deleted file mode 100644
index 7029bdcfce..0000000000
--- a/app/routes/api.netlify-quick-deploy.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-/**
- * Netlify Quick Deploy API Endpoint
- * Contributed by Keoma Wright
- *
- * This endpoint handles quick deployments to Netlify without requiring authentication,
- * using Netlify's drop API for instant deployment.
- */
-
-import { type ActionFunctionArgs, json } from '@remix-run/cloudflare';
-import crypto from 'crypto';
-
-interface QuickDeployRequestBody {
- files: Record;
- chatId: string;
- framework?: string;
-}
-
-// Use environment variable or fallback to public token for quick deploys
-const NETLIFY_QUICK_DEPLOY_TOKEN = process.env.NETLIFY_QUICK_DEPLOY_TOKEN || '';
-
-export async function action({ request }: ActionFunctionArgs) {
- try {
- const { files, chatId, framework } = (await request.json()) as QuickDeployRequestBody;
-
- if (!files || Object.keys(files).length === 0) {
- return json({ error: 'No files to deploy' }, { status: 400 });
- }
-
- // Generate a unique site name
- const siteName = `bolt-quick-${chatId.substring(0, 8)}-${Date.now()}`;
-
- // Prepare files for Netlify Drop API
- const deployFiles: Record = {};
-
- // Add index.html if it doesn't exist (for static sites)
- if (!files['/index.html'] && !files['index.html']) {
- // Check if there's any HTML file
- const htmlFile = Object.keys(files).find((f) => f.endsWith('.html'));
-
- if (!htmlFile) {
- // Create a basic index.html
- deployFiles['/index.html'] = `
-
-
-
-
-
- ${framework || 'Bolt'} App
-
-
-
-
-
-`;
- }
- }
-
- // Process and normalize file paths
- for (const [filePath, content] of Object.entries(files)) {
- const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
- deployFiles[normalizedPath] = content;
- }
-
- // Use Netlify's API to create a new site and deploy
- let siteId: string | undefined;
- let deployUrl: string | undefined;
-
- if (NETLIFY_QUICK_DEPLOY_TOKEN) {
- // If we have a token, use the authenticated API
- try {
- // Create a new site
- const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${NETLIFY_QUICK_DEPLOY_TOKEN}`,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: siteName,
- custom_domain: null,
- }),
- });
-
- if (createSiteResponse.ok) {
- const site = (await createSiteResponse.json()) as any;
- siteId = site.id;
-
- // Create file digests for deployment
- const fileDigests: Record = {};
-
- for (const [path, content] of Object.entries(deployFiles)) {
- const hash = crypto.createHash('sha1').update(content).digest('hex');
- fileDigests[path] = hash;
- }
-
- // Create deployment
- const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}/deploys`, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${NETLIFY_QUICK_DEPLOY_TOKEN}`,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- files: fileDigests,
- async: false,
- draft: false,
- }),
- });
-
- if (deployResponse.ok) {
- const deploy = (await deployResponse.json()) as any;
-
- // Upload files
- for (const [path, content] of Object.entries(deployFiles)) {
- await fetch(`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${path}`, {
- method: 'PUT',
- headers: {
- Authorization: `Bearer ${NETLIFY_QUICK_DEPLOY_TOKEN}`,
- 'Content-Type': 'application/octet-stream',
- },
- body: content,
- });
- }
-
- deployUrl = deploy.ssl_url || deploy.url || `https://${siteName}.netlify.app`;
- }
- }
- } catch (error) {
- console.error('Error with authenticated deployment:', error);
- }
- }
-
- // Fallback to Netlify Drop (no authentication required)
- if (!deployUrl) {
- // Create a form data with files
- const formData = new FormData();
-
- // Add each file to the form data
- for (const [path, content] of Object.entries(deployFiles)) {
- const blob = new Blob([content], { type: 'text/plain' });
- const fileName = path.startsWith('/') ? path.substring(1) : path;
- formData.append('file', blob, fileName);
- }
-
- // Deploy using Netlify Drop API (no auth required)
- const dropResponse = await fetch('https://api.netlify.com/api/v1/sites', {
- method: 'POST',
- body: formData,
- });
-
- if (dropResponse.ok) {
- const dropData = (await dropResponse.json()) as any;
- siteId = dropData.id;
- deployUrl = dropData.ssl_url || dropData.url || `https://${dropData.subdomain}.netlify.app`;
- } else {
- // Try alternative deployment method
- const zipContent = await createZipArchive(deployFiles);
-
- const zipResponse = await fetch('https://api.netlify.com/api/v1/sites', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/zip',
- },
- body: zipContent,
- });
-
- if (zipResponse.ok) {
- const zipData = (await zipResponse.json()) as any;
- siteId = zipData.id;
- deployUrl = zipData.ssl_url || zipData.url;
- } else {
- throw new Error('Failed to deploy to Netlify');
- }
- }
- }
-
- if (!deployUrl) {
- return json({ error: 'Deployment failed - could not get deployment URL' }, { status: 500 });
- }
-
- return json({
- success: true,
- url: deployUrl,
- siteId,
- siteName,
- });
- } catch (error) {
- console.error('Quick deploy error:', error);
- return json(
- {
- error: error instanceof Error ? error.message : 'Deployment failed',
- details: error instanceof Error ? error.stack : undefined,
- },
- { status: 500 },
- );
- }
-}
-
-// Helper function to create a simple ZIP archive (minimal implementation)
-async function createZipArchive(files: Record): Promise {
- // This is a simplified ZIP creation - in production, use a proper ZIP library
- const encoder = new TextEncoder();
- const parts: Uint8Array[] = [];
-
- // For simplicity, we'll create a tar-like format
- for (const [path, content] of Object.entries(files)) {
- const pathBytes = encoder.encode(path);
- const contentBytes = encoder.encode(content);
-
- // Simple header: path length (4 bytes) + content length (4 bytes)
- const header = new Uint8Array(8);
- new DataView(header.buffer).setUint32(0, pathBytes.length, true);
- new DataView(header.buffer).setUint32(4, contentBytes.length, true);
-
- parts.push(header);
- parts.push(pathBytes);
- parts.push(contentBytes);
- }
-
- // Combine all parts
- const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
- const result = new Uint8Array(totalLength);
- let offset = 0;
-
- for (const part of parts) {
- result.set(part, offset);
- offset += part.length;
- }
-
- return result.buffer;
-}
diff --git a/app/routes/api.users.$id.ts b/app/routes/api.users.$id.ts
deleted file mode 100644
index 246c0de5af..0000000000
--- a/app/routes/api.users.$id.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { ActionFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { verifyToken } from '~/lib/utils/crypto';
-import { deleteUser } from '~/lib/utils/fileUserStorage';
-
-export async function action({ request, params }: ActionFunctionArgs) {
- try {
- const { id } = params;
-
- if (!id) {
- return json({ error: 'User ID is required' }, { status: 400 });
- }
-
- // Verify authentication
- const authHeader = request.headers.get('Authorization');
-
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
- return json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const token = authHeader.substring(7);
- const payload = verifyToken(token);
-
- if (!payload) {
- return json({ error: 'Invalid token' }, { status: 401 });
- }
-
- // Prevent users from deleting themselves
- if (payload.userId === id) {
- return json({ error: 'Cannot delete your own account' }, { status: 400 });
- }
-
- if (request.method === 'DELETE') {
- // Delete the user
- const success = await deleteUser(id);
-
- if (success) {
- return json({ success: true });
- } else {
- return json({ error: 'User not found' }, { status: 404 });
- }
- }
-
- return json({ error: 'Method not allowed' }, { status: 405 });
- } catch (error) {
- console.error('User operation error:', error);
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/api.users.ts b/app/routes/api.users.ts
deleted file mode 100644
index 1e79f7e2a1..0000000000
--- a/app/routes/api.users.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { verifyToken } from '~/lib/utils/crypto';
-import { getAllUsers } from '~/lib/utils/fileUserStorage';
-
-export async function loader({ request }: LoaderFunctionArgs) {
- try {
- // Verify authentication
- const authHeader = request.headers.get('Authorization');
-
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
- return json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const token = authHeader.substring(7);
- const payload = verifyToken(token);
-
- if (!payload) {
- return json({ error: 'Invalid token' }, { status: 401 });
- }
-
- // Get all users (without passwords)
- const users = await getAllUsers();
-
- return json({ users });
- } catch (error) {
- console.error('Failed to fetch users:', error);
- return json({ error: 'Internal server error' }, { status: 500 });
- }
-}
diff --git a/app/routes/auth.tsx b/app/routes/auth.tsx
deleted file mode 100644
index 10a35dba9d..0000000000
--- a/app/routes/auth.tsx
+++ /dev/null
@@ -1,422 +0,0 @@
-import { useState } from 'react';
-import { useNavigate } from '@remix-run/react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { login, signup } from '~/lib/stores/auth';
-import { validatePassword } from '~/lib/utils/crypto';
-import { classNames } from '~/utils/classNames';
-
-export default function AuthPage() {
- const navigate = useNavigate();
- const [mode, setMode] = useState<'login' | 'signup'>('login');
- const [formData, setFormData] = useState({
- username: '',
- password: '',
- firstName: '',
- confirmPassword: '',
- rememberMe: false,
- });
- const [avatar, setAvatar] = useState();
- const [loading, setLoading] = useState(false);
- const [errors, setErrors] = useState>({});
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const { name, value, type, checked } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: type === 'checkbox' ? checked : value,
- }));
-
- // Clear error for this field
- setErrors((prev) => ({ ...prev, [name]: '' }));
- };
-
- const handleAvatarUpload = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
-
- if (file) {
- const reader = new FileReader();
-
- reader.onloadend = () => {
- setAvatar(reader.result as string);
- };
- reader.readAsDataURL(file);
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setErrors({});
- setLoading(true);
-
- try {
- if (mode === 'signup') {
- // Validate form
- const validationErrors: Record = {};
-
- if (!formData.username) {
- validationErrors.username = 'Username is required';
- }
-
- if (!formData.firstName) {
- validationErrors.firstName = 'First name is required';
- }
-
- const passwordValidation = validatePassword(formData.password);
-
- if (!passwordValidation.valid) {
- validationErrors.password = passwordValidation.errors[0];
- }
-
- if (formData.password !== formData.confirmPassword) {
- validationErrors.confirmPassword = 'Passwords do not match';
- }
-
- if (Object.keys(validationErrors).length > 0) {
- setErrors(validationErrors);
- setLoading(false);
-
- return;
- }
-
- const result = await signup(formData.username, formData.password, formData.firstName, avatar);
-
- if (result.success) {
- navigate('/');
- } else {
- setErrors({ general: result.error || 'Signup failed' });
- }
- } else {
- const result = await login(formData.username, formData.password, formData.rememberMe);
-
- if (result.success) {
- navigate('/');
- } else {
- setErrors({ general: result.error || 'Invalid username or password' });
- }
- }
- } catch {
- setErrors({ general: 'An error occurred. Please try again.' });
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- {/* Animated gradient background */}
-
-
- {/* Logo and Title */}
-
-
-
- ⚡
-
-
-
bolt.diy
-
Multi-User Edition
-
-
-
-
- {/* Auth Card */}
-
-
- {/* Tab Header */}
-
- setMode('login')}
- className={classNames(
- 'flex-1 py-4 text-center font-semibold transition-all',
- mode === 'login'
- ? 'text-white bg-gradient-to-r from-blue-500/20 to-purple-600/20'
- : 'text-white/70 hover:text-white hover:bg-white/5',
- )}
- >
- Sign In
-
- setMode('signup')}
- className={classNames(
- 'flex-1 py-4 text-center font-semibold transition-all',
- mode === 'signup'
- ? 'text-white bg-gradient-to-r from-blue-500/20 to-purple-600/20'
- : 'text-white/70 hover:text-white hover:bg-white/5',
- )}
- >
- Sign Up
-
-
- {/* Sliding indicator */}
-
-
-
- {/* Form Content */}
-
-
-
- {/* Avatar Upload (Signup only) */}
- {mode === 'signup' && (
-
-
-
- {avatar ? (
-
- ) : (
-
👤
- )}
-
-
- 📷
-
-
-
-
- )}
-
- {/* First Name (Signup only) */}
- {mode === 'signup' && (
-
-
First Name
-
- {errors.firstName &&
{errors.firstName}
}
-
- )}
-
- {/* Username */}
-
-
Username
-
- {errors.username &&
{errors.username}
}
-
-
- {/* Password */}
-
-
Password
-
- {errors.password &&
{errors.password}
}
- {mode === 'signup' && formData.password && (
-
-
-
= 8 ? 'bg-green-400' : 'bg-white/30',
- )}
- />
- At least 8 characters
-
-
-
-
One uppercase letter
-
-
-
-
One lowercase letter
-
-
-
- )}
-
-
- {/* Confirm Password (Signup only) */}
- {mode === 'signup' && (
-
-
Confirm Password
-
- {errors.confirmPassword &&
{errors.confirmPassword}
}
-
- )}
-
- {/* Remember Me (Login only) */}
- {mode === 'login' && (
-
-
-
- Remember me for 7 days
-
-
- )}
-
- {/* Error Message */}
- {errors.general && (
-
- )}
-
- {/* Submit Button */}
-
- {loading ? (
- <>
-
- {mode === 'login' ? 'Signing in...' : 'Creating account...'}
- >
- ) : mode === 'login' ? (
- 'Sign In'
- ) : (
- 'Create Account'
- )}
-
-
-
-
- {/* Developer Credit */}
-
-
- Developed by Keoma Wright
-
-
-
- {/* Continue as Guest */}
-
-
navigate('/')}
- className="w-full py-2 text-center text-sm text-white/60 hover:text-white transition-colors"
- >
-
-
-
-
- Continue as Guest
-
-
-
-
-
-
-
- );
-}
diff --git a/package.json b/package.json
index def62cf5b2..a8c90b5fa7 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,6 @@
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-separator": "^1.1.0",
- "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.4",
@@ -98,7 +97,6 @@
"@remix-run/node": "^2.15.2",
"@remix-run/react": "^2.15.2",
"@tanstack/react-virtual": "^3.13.0",
- "@types/jszip": "^3.4.1",
"@types/react-beautiful-dnd": "^13.1.8",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
@@ -107,7 +105,6 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"ai": "4.3.16",
- "bcryptjs": "^3.0.2",
"chalk": "^5.4.1",
"chart.js": "^4.4.7",
"class-variance-authority": "^0.7.0",
@@ -126,7 +123,6 @@
"istextorbinary": "^9.5.0",
"jose": "^5.9.6",
"js-cookie": "^3.0.5",
- "jsonwebtoken": "^9.0.2",
"jspdf": "^2.5.2",
"jszip": "^3.10.1",
"lucide-react": "^0.485.0",
@@ -166,19 +162,16 @@
"@cloudflare/workers-types": "^4.20241127.0",
"@electron/notarize": "^2.5.0",
"@iconify-json/ph": "^1.2.1",
- "@iconify-json/simple-icons": "^1.2.49",
"@iconify/types": "^2.0.0",
"@remix-run/dev": "^2.15.2",
"@remix-run/serve": "^2.15.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
- "@types/bcryptjs": "^3.0.0",
"@types/diff": "^5.2.3",
"@types/dom-speech-recognition": "^0.0.4",
"@types/electron": "^1.6.12",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
- "@types/jsonwebtoken": "^9.0.10",
"@types/path-browserify": "^1.0.3",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2db0e603cb..468c93caad 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -146,9 +146,6 @@ importers:
'@radix-ui/react-separator':
specifier: ^1.1.0
version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-slider':
- specifier: ^1.3.6
- version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-switch':
specifier: ^1.1.1
version: 1.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -173,9 +170,6 @@ importers:
'@tanstack/react-virtual':
specifier: ^3.13.0
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@types/jszip':
- specifier: ^3.4.1
- version: 3.4.1
'@types/react-beautiful-dnd':
specifier: ^13.1.8
version: 13.1.8
@@ -200,9 +194,6 @@ importers:
ai:
specifier: 4.3.16
version: 4.3.16(react@18.3.1)(zod@3.25.76)
- bcryptjs:
- specifier: ^3.0.2
- version: 3.0.2
chalk:
specifier: ^5.4.1
version: 5.4.1
@@ -257,9 +248,6 @@ importers:
js-cookie:
specifier: ^3.0.5
version: 3.0.5
- jsonwebtoken:
- specifier: ^9.0.2
- version: 9.0.2
jspdf:
specifier: ^2.5.2
version: 2.5.2
@@ -372,9 +360,6 @@ importers:
'@iconify-json/ph':
specifier: ^1.2.1
version: 1.2.2
- '@iconify-json/simple-icons':
- specifier: ^1.2.49
- version: 1.2.49
'@iconify/types':
specifier: ^2.0.0
version: 2.0.0
@@ -390,9 +375,6 @@ importers:
'@testing-library/react':
specifier: ^16.2.0
version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@types/bcryptjs':
- specifier: ^3.0.0
- version: 3.0.0
'@types/diff':
specifier: ^5.2.3
version: 5.2.3
@@ -408,9 +390,6 @@ importers:
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
- '@types/jsonwebtoken':
- specifier: ^9.0.10
- version: 9.0.10
'@types/path-browserify':
specifier: ^1.0.3
version: 1.0.3
@@ -1974,9 +1953,6 @@ packages:
'@iconify-json/ph@1.2.2':
resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==}
- '@iconify-json/simple-icons@1.2.49':
- resolution: {integrity: sha512-nRLwrHzz+cTAQYBNQrcr4eWOmQIcHObTj/QSi7nj0SFwVh5MvBsgx8OhoDC/R8iGklNmMpmoE/NKU0cPXMlOZw==}
-
'@iconify-json/svg-spinners@1.2.2':
resolution: {integrity: sha512-DIErwfBWWzLfmAG2oQnbUOSqZhDxlXvr8941itMCrxQoMB0Hiv8Ww6Bln/zIgxwjDvSem2dKJtap+yKKwsB/2A==}
@@ -2321,9 +2297,6 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
- '@radix-ui/primitive@1.1.3':
- resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
-
'@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies:
@@ -2629,19 +2602,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-slider@1.3.6':
- resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
- peerDependencies:
- '@types/react': '*'
- '@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
@@ -3336,10 +3296,6 @@ packages:
'@types/babel__traverse@7.20.7':
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
- '@types/bcryptjs@3.0.0':
- resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
- deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
-
'@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
@@ -3394,13 +3350,6 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
- '@types/jsonwebtoken@9.0.10':
- resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
-
- '@types/jszip@3.4.1':
- resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==}
- deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed.
-
'@types/keyv@3.1.4':
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
@@ -3901,10 +3850,6 @@ packages:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
- bcryptjs@3.0.2:
- resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
- hasBin: true
-
before-after-hook@3.0.2:
resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
@@ -3998,9 +3943,6 @@ packages:
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
- buffer-equal-constant-time@1.0.1:
- resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
-
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -4562,9 +4504,6 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- ecdsa-sig-formatter@1.0.11:
- resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
-
editions@6.21.0:
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
engines: {node: '>=4'}
@@ -5647,22 +5586,12 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
- jsonwebtoken@9.0.2:
- resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
- engines: {node: '>=12', npm: '>=6'}
-
jspdf@2.5.2:
resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
- jwa@1.4.2:
- resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
-
- jws@3.2.2:
- resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
-
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -5712,34 +5641,13 @@ packages:
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
- lodash.includes@4.3.0:
- resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
-
- lodash.isboolean@3.0.3:
- resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
-
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
- lodash.isinteger@4.0.4:
- resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
-
- lodash.isnumber@3.0.3:
- resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
-
- lodash.isplainobject@4.0.6:
- resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
-
- lodash.isstring@4.0.1:
- resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
-
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
- lodash.once@4.1.1:
- resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
-
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -9955,10 +9863,6 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
- '@iconify-json/simple-icons@1.2.49':
- dependencies:
- '@iconify/types': 2.0.0
-
'@iconify-json/svg-spinners@1.2.2':
dependencies:
'@iconify/types': 2.0.0
@@ -10366,8 +10270,6 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
- '@radix-ui/primitive@1.1.3': {}
-
'@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -10685,25 +10587,6 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
- '@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- '@radix-ui/number': 1.1.1
- '@radix-ui/primitive': 1.1.3
- '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
- '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
- optionalDependencies:
- '@types/react': 18.3.23
- '@types/react-dom': 18.3.7(@types/react@18.3.23)
-
'@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
@@ -11568,15 +11451,11 @@ snapshots:
dependencies:
'@babel/types': 7.28.1
- '@types/bcryptjs@3.0.0':
- dependencies:
- bcryptjs: 3.0.2
-
'@types/cacheable-request@6.0.3':
dependencies:
'@types/http-cache-semantics': 4.0.4
'@types/keyv': 3.1.4
- '@types/node': 24.1.0
+ '@types/node': 20.19.9
'@types/responselike': 1.0.3
'@types/cookie@0.6.0': {}
@@ -11628,18 +11507,9 @@ snapshots:
'@types/json-schema@7.0.15': {}
- '@types/jsonwebtoken@9.0.10':
- dependencies:
- '@types/ms': 2.1.0
- '@types/node': 24.1.0
-
- '@types/jszip@3.4.1':
- dependencies:
- jszip: 3.10.1
-
'@types/keyv@3.1.4':
dependencies:
- '@types/node': 24.1.0
+ '@types/node': 20.19.9
'@types/mdast@3.0.15':
dependencies:
@@ -11700,7 +11570,7 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
- '@types/node': 24.1.0
+ '@types/node': 20.19.9
'@types/unist@2.0.11': {}
@@ -11713,7 +11583,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
- '@types/node': 24.1.0
+ '@types/node': 20.19.9
optional: true
'@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3)':
@@ -12319,8 +12189,6 @@ snapshots:
dependencies:
safe-buffer: 5.1.2
- bcryptjs@3.0.2: {}
-
before-after-hook@3.0.2: {}
binary-extensions@2.3.0: {}
@@ -12458,8 +12326,6 @@ snapshots:
buffer-crc32@0.2.13: {}
- buffer-equal-constant-time@1.0.1: {}
-
buffer-from@1.1.2: {}
buffer-xor@1.0.3: {}
@@ -13088,10 +12954,6 @@ snapshots:
eastasianwidth@0.2.0: {}
- ecdsa-sig-formatter@1.0.11:
- dependencies:
- safe-buffer: 5.2.1
-
editions@6.21.0:
dependencies:
version-range: 4.14.0
@@ -14535,19 +14397,6 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
- jsonwebtoken@9.0.2:
- dependencies:
- jws: 3.2.2
- lodash.includes: 4.3.0
- lodash.isboolean: 3.0.3
- lodash.isinteger: 4.0.4
- lodash.isnumber: 3.0.3
- lodash.isplainobject: 4.0.6
- lodash.isstring: 4.0.1
- lodash.once: 4.1.1
- ms: 2.1.3
- semver: 7.7.2
-
jspdf@2.5.2:
dependencies:
'@babel/runtime': 7.27.6
@@ -14567,17 +14416,6 @@ snapshots:
readable-stream: 2.3.8
setimmediate: 1.0.5
- jwa@1.4.2:
- dependencies:
- buffer-equal-constant-time: 1.0.1
- ecdsa-sig-formatter: 1.0.11
- safe-buffer: 5.2.1
-
- jws@3.2.2:
- dependencies:
- jwa: 1.4.2
- safe-buffer: 5.2.1
-
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -14619,24 +14457,10 @@ snapshots:
lodash.escaperegexp@4.1.2: {}
- lodash.includes@4.3.0: {}
-
- lodash.isboolean@3.0.3: {}
-
lodash.isequal@4.5.0: {}
- lodash.isinteger@4.0.4: {}
-
- lodash.isnumber@3.0.3: {}
-
- lodash.isplainobject@4.0.6: {}
-
- lodash.isstring@4.0.1: {}
-
lodash.merge@4.6.2: {}
- lodash.once@4.1.1: {}
-
lodash@4.17.21: {}
log-symbols@4.1.0:
diff --git a/public/boltgives.jpeg b/public/boltgives.jpeg
deleted file mode 100644
index df782c6d8a..0000000000
Binary files a/public/boltgives.jpeg and /dev/null differ
diff --git a/start-prod.sh b/start-prod.sh
deleted file mode 100755
index 6d019ad89c..0000000000
--- a/start-prod.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-
-# Set working directory
-cd /root/bolt
-
-# Kill any existing processes on port 5173
-lsof -ti:5173 | xargs -r kill -9 2>/dev/null
-
-# Set environment variables
-export NODE_OPTIONS="--max-old-space-size=3482"
-export HOST=0.0.0.0
-export PORT=5173
-# Don't set NODE_ENV for dev server
-unset NODE_ENV
-
-echo "Starting Bolt.gives Server..."
-echo "========================================="
-echo "Version: 3.0.1"
-echo "Port: 5173"
-echo "Host: 0.0.0.0"
-echo "URL: https://bolt.openweb.live"
-echo "========================================="
-
-# Use dev server with fixed dependencies
-exec pnpm run dev --host 0.0.0.0 --port 5173
\ No newline at end of file
diff --git a/test-ollama-fix.js b/test-ollama-fix.js
deleted file mode 100644
index 89a7eabc51..0000000000
--- a/test-ollama-fix.js
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Test script to verify Ollama provider is properly enabled
- * when configured via environment variables
- */
-
-// Simulate environment configuration
-process.env.OLLAMA_API_BASE_URL = 'http://127.0.0.1:11434';
-
-console.log('Testing Ollama Provider Auto-Enable Fix...\n');
-
-// Test 1: Check environment detection in loader
-console.log('Test 1: Environment Detection');
-const hasOllamaEnv = process.env.OLLAMA_API_BASE_URL;
-console.log(`✓ OLLAMA_API_BASE_URL detected: ${hasOllamaEnv}`);
-
-// Test 2: Verify provider configuration logic
-console.log('\nTest 2: Provider Configuration Logic');
-const configuredProviders = [];
-if (process.env.OLLAMA_API_BASE_URL) {
- configuredProviders.push('Ollama');
-}
-console.log(`✓ Ollama added to configured providers: ${configuredProviders.includes('Ollama')}`);
-
-// Test 3: Simulate provider enablement
-console.log('\nTest 3: Provider Enablement Simulation');
-const mockProviderStore = {
- 'Ollama': {
- name: 'Ollama',
- settings: { enabled: false }
- },
- 'OpenRouter': {
- name: 'OpenRouter',
- settings: { enabled: true }
- }
-};
-
-// Simulate the fix logic
-const savedSettings = null; // Simulate no saved settings
-if (!savedSettings && configuredProviders.length > 0) {
- configuredProviders.forEach((providerName) => {
- if (mockProviderStore[providerName]) {
- mockProviderStore[providerName].settings.enabled = true;
- console.log(`✓ ${providerName} provider enabled automatically`);
- }
- });
-}
-
-console.log(`✓ Ollama final state - enabled: ${mockProviderStore['Ollama'].settings.enabled}`);
-
-// Summary
-console.log('\n=== Test Summary ===');
-console.log('All tests passed! The fix properly:');
-console.log('1. Detects Ollama configuration in environment variables');
-console.log('2. Adds Ollama to the list of configured providers');
-console.log('3. Automatically enables Ollama when no saved settings exist');
-console.log('\nIssue #1881 should now be resolved.');
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 9519c9d7bd..7847a17cbd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -13,19 +13,6 @@ dotenv.config();
export default defineConfig((config) => {
return {
- server: {
- host: '0.0.0.0',
- port: 5173,
- hmr: {
- host: 'bolt.openweb.live',
- protocol: 'wss'
- },
- headers: {
- 'Cross-Origin-Embedder-Policy': 'credentialless',
- 'Cross-Origin-Opener-Policy': 'same-origin',
- },
- allowedHosts: ['bolt.openweb.live', 'localhost']
- },
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},