mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-23 02:16:08 +00:00
Fix ESLint issues
This commit is contained in:
parent
58d3853cd6
commit
c4c73622f5
@ -8,6 +8,8 @@ bolt.diy (previously oTToDev) is an open-source AI-powered full-stack web develo
|
||||
- Focus on best practices and clean code
|
||||
- Provide clear explanations for code changes
|
||||
- Maintain consistent code style with the existing codebase
|
||||
- Always write comments that are relevant to the code they describe
|
||||
- Always write a changelog what you did and save it in a file called changelog.md in the root of the project
|
||||
|
||||
# Techstack
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "Bug report"
|
||||
name: 'Bug report'
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: markdown
|
||||
|
2
.github/ISSUE_TEMPLATE/epic.md
vendored
2
.github/ISSUE_TEMPLATE/epic.md
vendored
@ -19,5 +19,5 @@ Usual values: Software Developers using the IDE | Contributors -->
|
||||
|
||||
# Capabilities
|
||||
|
||||
<!-- which existing capabilities or future features can be imagined that belong to this epic? This list serves as illustration to sketch the boundaries of this epic.
|
||||
<!-- which existing capabilities or future features can be imagined that belong to this epic? This list serves as illustration to sketch the boundaries of this epic.
|
||||
Once features are actually being planned / described in detail, they can be linked here. -->
|
||||
|
6
.github/ISSUE_TEMPLATE/feature.md
vendored
6
.github/ISSUE_TEMPLATE/feature.md
vendored
@ -13,13 +13,13 @@ assignees: ''
|
||||
|
||||
# Scope
|
||||
|
||||
<!-- This is kind-of the definition-of-done for a feature.
|
||||
<!-- This is kind-of the definition-of-done for a feature.
|
||||
Try to keep the scope as small as possible and prefer creating multiple, small features which each solve a single problem / make something better
|
||||
-->
|
||||
|
||||
# Options
|
||||
|
||||
<!-- If you already have an idea how this can be implemented, please describe it here.
|
||||
|
||||
<!-- If you already have an idea how this can be implemented, please describe it here.
|
||||
This allows potential other contributors to join forces and provide meaningful feedback prio to even starting work on it.
|
||||
-->
|
||||
|
||||
|
4
.github/workflows/docker.yaml
vendored
4
.github/workflows/docker.yaml
vendored
@ -8,7 +8,7 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }} # ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }} # ${{ secrets.DOCKER_PASSWORD }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }} # ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
6
.github/workflows/docs.yaml
vendored
6
.github/workflows/docs.yaml
vendored
@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**' # This will only trigger the workflow when files in docs directory change
|
||||
- 'docs/**' # This will only trigger the workflow when files in docs directory change
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
@ -32,4 +32,4 @@ jobs:
|
||||
mkdocs-material-
|
||||
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
||||
- run: mkdocs gh-deploy --force
|
||||
|
6
.github/workflows/pr-release-validation.yaml
vendored
6
.github/workflows/pr-release-validation.yaml
vendored
@ -9,10 +9,10 @@ on:
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Validate PR Labels
|
||||
run: |
|
||||
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable-release') }}" == "true" ]]; then
|
||||
@ -28,4 +28,4 @@ jobs:
|
||||
fi
|
||||
else
|
||||
echo "This PR doesn't have the stable-release label. No release will be created."
|
||||
fi
|
||||
fi
|
||||
|
2
.github/workflows/semantic-pr.yaml
vendored
2
.github/workflows/semantic-pr.yaml
vendored
@ -29,4 +29,4 @@ jobs:
|
||||
docs
|
||||
refactor
|
||||
revert
|
||||
test
|
||||
test
|
||||
|
22
.github/workflows/stale.yml
vendored
22
.github/workflows/stale.yml
vendored
@ -2,8 +2,8 @@ name: Mark Stale Issues and Pull Requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Runs daily at 2:00 AM UTC
|
||||
workflow_dispatch: # Allows manual triggering of the workflow
|
||||
- cron: '0 2 * * *' # Runs daily at 2:00 AM UTC
|
||||
workflow_dispatch: # Allows manual triggering of the workflow
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
@ -14,12 +14,12 @@ jobs:
|
||||
uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: "This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days."
|
||||
stale-pr-message: "This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days."
|
||||
days-before-stale: 10 # Number of days before marking an issue or PR as stale
|
||||
days-before-close: 4 # Number of days after being marked stale before closing
|
||||
stale-issue-label: "stale" # Label to apply to stale issues
|
||||
stale-pr-label: "stale" # Label to apply to stale pull requests
|
||||
exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale
|
||||
exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale
|
||||
operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits
|
||||
stale-issue-message: 'This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.'
|
||||
stale-pr-message: 'This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.'
|
||||
days-before-stale: 10 # Number of days before marking an issue or PR as stale
|
||||
days-before-close: 4 # Number of days after being marked stale before closing
|
||||
stale-issue-label: 'stale' # Label to apply to stale issues
|
||||
stale-pr-label: 'stale' # Label to apply to stale pull requests
|
||||
exempt-issue-labels: 'pinned,important' # Issues with these labels won't be marked stale
|
||||
exempt-pr-labels: 'pinned,important' # PRs with these labels won't be marked stale
|
||||
operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits
|
||||
|
11
.github/workflows/update-stable.yml
vendored
11
.github/workflows/update-stable.yml
vendored
@ -7,12 +7,12 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
if: contains(github.event.head_commit.message, '#release')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@ -80,7 +80,6 @@ jobs:
|
||||
NEW_VERSION=${{ steps.bump_version.outputs.new_version }}
|
||||
pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version
|
||||
|
||||
|
||||
- name: Prepare changelog script
|
||||
run: chmod +x .github/scripts/generate-changelog.sh
|
||||
|
||||
@ -89,14 +88,14 @@ jobs:
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
run: .github/scripts/generate-changelog.sh
|
||||
|
||||
- name: Get the latest commit hash and version tag
|
||||
run: |
|
||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
echo "NEW_VERSION=${{ steps.bump_version.outputs.new_version }}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Commit and Tag Release
|
||||
run: |
|
||||
git pull
|
||||
@ -123,4 +122,4 @@ jobs:
|
||||
gh release create "$VERSION" \
|
||||
--title "Release $VERSION" \
|
||||
--notes "${{ steps.changelog.outputs.content }}" \
|
||||
--target stable
|
||||
--target stable
|
||||
|
151
CONTRIBUTING.md
151
CONTRIBUTING.md
@ -6,15 +6,15 @@ Welcome! This guide provides all the details you need to contribute effectively
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Code of Conduct](#code-of-conduct)
|
||||
2. [How Can I Contribute?](#how-can-i-contribute)
|
||||
3. [Pull Request Guidelines](#pull-request-guidelines)
|
||||
4. [Coding Standards](#coding-standards)
|
||||
5. [Development Setup](#development-setup)
|
||||
6. [Testing](#testing)
|
||||
7. [Deployment](#deployment)
|
||||
8. [Docker Deployment](#docker-deployment)
|
||||
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
|
||||
1. [Code of Conduct](#code-of-conduct)
|
||||
2. [How Can I Contribute?](#how-can-i-contribute)
|
||||
3. [Pull Request Guidelines](#pull-request-guidelines)
|
||||
4. [Coding Standards](#coding-standards)
|
||||
5. [Development Setup](#development-setup)
|
||||
6. [Testing](#testing)
|
||||
7. [Deployment](#deployment)
|
||||
8. [Docker Deployment](#docker-deployment)
|
||||
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
|
||||
|
||||
---
|
||||
|
||||
@ -27,60 +27,67 @@ This project is governed by our **Code of Conduct**. By participating, you agree
|
||||
## 🛠️ How Can I Contribute?
|
||||
|
||||
### 1️⃣ Reporting Bugs or Feature Requests
|
||||
|
||||
- Check the [issue tracker](#) to avoid duplicates.
|
||||
- Use issue templates (if available).
|
||||
- Use issue templates (if available).
|
||||
- Provide detailed, relevant information and steps to reproduce bugs.
|
||||
|
||||
### 2️⃣ Code Contributions
|
||||
1. Fork the repository.
|
||||
2. Create a feature or fix branch.
|
||||
3. Write and test your code.
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature or fix branch.
|
||||
3. Write and test your code.
|
||||
4. Submit a pull request (PR).
|
||||
|
||||
### 3️⃣ Join as a Core Contributor
|
||||
### 3️⃣ Join as a Core Contributor
|
||||
|
||||
Interested in maintaining and growing the project? Fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Pull Request Guidelines
|
||||
|
||||
### PR Checklist
|
||||
- Branch from the **main** branch.
|
||||
- Update documentation, if needed.
|
||||
- Test all functionality manually.
|
||||
- Focus on one feature/bug per PR.
|
||||
### PR Checklist
|
||||
|
||||
### Review Process
|
||||
1. Manual testing by reviewers.
|
||||
2. At least one maintainer review required.
|
||||
3. Address review comments.
|
||||
- Branch from the **main** branch.
|
||||
- Update documentation, if needed.
|
||||
- Test all functionality manually.
|
||||
- Focus on one feature/bug per PR.
|
||||
|
||||
### Review Process
|
||||
|
||||
1. Manual testing by reviewers.
|
||||
2. At least one maintainer review required.
|
||||
3. Address review comments.
|
||||
4. Maintain a clean commit history.
|
||||
|
||||
---
|
||||
|
||||
## 📏 Coding Standards
|
||||
|
||||
### General Guidelines
|
||||
- Follow existing code style.
|
||||
- Comment complex logic.
|
||||
- Keep functions small and focused.
|
||||
### General Guidelines
|
||||
|
||||
- Follow existing code style.
|
||||
- Comment complex logic.
|
||||
- Keep functions small and focused.
|
||||
- Use meaningful variable names.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Development Setup
|
||||
|
||||
### 1️⃣ Initial Setup
|
||||
- Clone the repository:
|
||||
### 1️⃣ Initial Setup
|
||||
|
||||
- Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/stackblitz-labs/bolt.diy.git
|
||||
```
|
||||
- Install dependencies:
|
||||
- Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
- Set up environment variables:
|
||||
1. Rename `.env.example` to `.env.local`.
|
||||
- Set up environment variables:
|
||||
1. Rename `.env.example` to `.env.local`.
|
||||
2. Add your API keys:
|
||||
```bash
|
||||
GROQ_API_KEY=XXX
|
||||
@ -88,23 +95,26 @@ Interested in maintaining and growing the project? Fill out our [Contributor App
|
||||
OPENAI_API_KEY=XXX
|
||||
...
|
||||
```
|
||||
3. Optionally set:
|
||||
- Debug level: `VITE_LOG_LEVEL=debug`
|
||||
- Context size: `DEFAULT_NUM_CTX=32768`
|
||||
3. Optionally set:
|
||||
- Debug level: `VITE_LOG_LEVEL=debug`
|
||||
- Context size: `DEFAULT_NUM_CTX=32768`
|
||||
|
||||
**Note**: Never commit your `.env.local` file to version control. It’s already in `.gitignore`.
|
||||
|
||||
### 2️⃣ Run Development Server
|
||||
### 2️⃣ Run Development Server
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
**Tip**: Use **Google Chrome Canary** for local testing.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite with:
|
||||
Run the test suite with:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
@ -113,10 +123,12 @@ pnpm test
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Deploy to Cloudflare Pages
|
||||
### Deploy to Cloudflare Pages
|
||||
|
||||
```bash
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
Ensure you have required permissions and that Wrangler is configured.
|
||||
|
||||
---
|
||||
@ -127,67 +139,76 @@ This section outlines the methods for deploying the application using Docker. Th
|
||||
|
||||
---
|
||||
|
||||
### 🧑💻 Development Environment
|
||||
### 🧑💻 Development Environment
|
||||
|
||||
#### Build Options
|
||||
#### Build Options
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
```bash
|
||||
# Development build
|
||||
npm run dockerbuild
|
||||
```
|
||||
|
||||
**Option 2: Direct Docker Build Command**
|
||||
**Option 2: Direct Docker Build Command**
|
||||
|
||||
```bash
|
||||
docker build . --target bolt-ai-development
|
||||
```
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
**Option 3: Docker Compose Profile**
|
||||
|
||||
```bash
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
#### Running the Development Container
|
||||
#### Running the Development Container
|
||||
|
||||
```bash
|
||||
docker run -p 5173:5173 --env-file .env.local bolt-ai:development
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏭 Production Environment
|
||||
### 🏭 Production Environment
|
||||
|
||||
#### Build Options
|
||||
#### Build Options
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
```bash
|
||||
# Production build
|
||||
npm run dockerbuild:prod
|
||||
```
|
||||
|
||||
**Option 2: Direct Docker Build Command**
|
||||
**Option 2: Direct Docker Build Command**
|
||||
|
||||
```bash
|
||||
docker build . --target bolt-ai-production
|
||||
```
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
**Option 3: Docker Compose Profile**
|
||||
|
||||
```bash
|
||||
docker compose --profile production up
|
||||
```
|
||||
|
||||
#### Running the Production Container
|
||||
#### Running the Production Container
|
||||
|
||||
```bash
|
||||
docker run -p 5173:5173 --env-file .env.local bolt-ai:production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Coolify Deployment
|
||||
### Coolify Deployment
|
||||
|
||||
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
|
||||
1. Import your Git repository into Coolify.
|
||||
2. Choose **Docker Compose** as the build pack.
|
||||
3. Configure environment variables (e.g., API keys).
|
||||
4. Set the start command:
|
||||
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
|
||||
|
||||
1. Import your Git repository into Coolify.
|
||||
2. Choose **Docker Compose** as the build pack.
|
||||
3. Configure environment variables (e.g., API keys).
|
||||
4. Set the start command:
|
||||
```bash
|
||||
docker compose --profile production up
|
||||
```
|
||||
@ -200,20 +221,22 @@ The `docker-compose.yaml` configuration is compatible with **VS Code Dev Contain
|
||||
|
||||
### Steps to Use Dev Containers
|
||||
|
||||
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
|
||||
2. Select **Dev Containers: Reopen in Container**.
|
||||
3. Choose the **development** profile when prompted.
|
||||
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
|
||||
2. Select **Dev Containers: Reopen in Container**.
|
||||
3. Choose the **development** profile when prompted.
|
||||
4. VS Code will rebuild the container and open it with the pre-configured environment.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Environment Variables
|
||||
|
||||
Ensure `.env.local` is configured correctly with:
|
||||
- API keys.
|
||||
- Context-specific configurations.
|
||||
Ensure `.env.local` is configured correctly with:
|
||||
|
||||
- API keys.
|
||||
- Context-specific configurations.
|
||||
|
||||
Example for the `DEFAULT_NUM_CTX` variable:
|
||||
|
||||
Example for the `DEFAULT_NUM_CTX` variable:
|
||||
```bash
|
||||
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
|
||||
```
|
||||
```
|
||||
|
24
FAQ.md
24
FAQ.md
@ -12,6 +12,7 @@ For the best experience with bolt.diy, we recommend using the following models:
|
||||
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
|
||||
|
||||
**Note**: Models with less than 7b parameters typically lack the capability to properly interact with bolt!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -21,20 +22,21 @@ For the best experience with bolt.diy, we recommend using the following models:
|
||||
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
|
||||
|
||||
- **Use the enhance prompt icon**:
|
||||
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
||||
Before sending your prompt, click the _enhance_ icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
||||
|
||||
- **Scaffold the basics first, then add features**:
|
||||
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
|
||||
|
||||
- **Batch simple instructions**:
|
||||
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
|
||||
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
|
||||
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
|
||||
_"Change the color scheme, add mobile responsiveness, and restart the dev server."_
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
|
||||
|
||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -42,48 +44,60 @@ Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to g
|
||||
|
||||
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
|
||||
New features and improvements are on the way!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
|
||||
|
||||
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
|
||||
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
|
||||
|
||||
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
|
||||
|
||||
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Common Errors and Troubleshooting</strong></summary>
|
||||
|
||||
### **"There was an error processing this request"**
|
||||
|
||||
This generic error message means something went wrong. Check both:
|
||||
|
||||
- The terminal (if you started the app with Docker or `pnpm`).
|
||||
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
|
||||
- The developer console in your browser (press `F12` or right-click > _Inspect_, then go to the _Console_ tab).
|
||||
|
||||
### **"x-api-key header missing"**
|
||||
|
||||
This error is sometimes resolved by restarting the Docker container.
|
||||
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
|
||||
|
||||
### **Blank preview when running the app**
|
||||
|
||||
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
||||
To troubleshoot:
|
||||
|
||||
- Check the developer console for errors.
|
||||
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
|
||||
|
||||
### **"Everything works, but the results are bad"**
|
||||
|
||||
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like GPT-4o, Claude 3.5 Sonnet, or DeepSeek Coder V2 236b.
|
||||
|
||||
### **"Received structured exception #0xc0000005: access violation"**
|
||||
|
||||
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
|
||||
|
||||
### **"Miniflare or Wrangler errors in Windows"**
|
||||
|
||||
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here https://github.com/stackblitz-labs/bolt.diy/issues/19.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
@ -31,7 +31,7 @@ and this way communicate where the focus currently is.
|
||||
|
||||
2. Grouping of features
|
||||
|
||||
By linking features with epics, we can keep them together and document *why* we invest work into a particular thing.
|
||||
By linking features with epics, we can keep them together and document _why_ we invest work into a particular thing.
|
||||
|
||||
## Features (mid-term)
|
||||
|
||||
@ -41,13 +41,13 @@ function, you name it).
|
||||
However, we intentionally describe features in a more vague manner. Why? Everybody loves crisp, well-defined
|
||||
acceptance-criteria, no? Well, every product owner loves it. because he knows what he’ll get once it’s done.
|
||||
|
||||
But: **here is no owner of this product**. Therefore, we grant *maximum flexibility to the developer contributing a feature* – so that he can bring in his ideas and have most fun implementing it.
|
||||
But: **here is no owner of this product**. Therefore, we grant _maximum flexibility to the developer contributing a feature_ – so that he can bring in his ideas and have most fun implementing it.
|
||||
|
||||
The feature therefore tries to describe *what* should be improved but not in detail *how*.
|
||||
The feature therefore tries to describe _what_ should be improved but not in detail _how_.
|
||||
|
||||
## PRs as materialized features (short-term)
|
||||
|
||||
Once a developer starts working on a feature, a draft-PR *can* be opened asap to share, describe and discuss, how the feature shall be implemented. But: this is not a must. It just helps to get early feedback and get other developers involved. Sometimes, the developer just wants to get started and then open a PR later.
|
||||
Once a developer starts working on a feature, a draft-PR _can_ be opened asap to share, describe and discuss, how the feature shall be implemented. But: this is not a must. It just helps to get early feedback and get other developers involved. Sometimes, the developer just wants to get started and then open a PR later.
|
||||
|
||||
In a loosely organized project, it may as well happen that multiple PRs are opened for the same feature. This is no real issue: Usually, peoply being passionate about a solution are willing to join forces and get it done together. And if a second developer was just faster getting the same feature realized: Be happy that it's been done, close the PR and look out for the next feature to implement 🤓
|
||||
|
||||
|
@ -4,10 +4,12 @@
|
||||
|
||||
Welcome to bolt.diy, the official open source version of Bolt.new (previously known as oTToDev and bolt.new ANY LLM), 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, or Groq models - 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 offical installation instructions and more informations.
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
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!
|
||||
|
||||
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/).
|
||||
|
@ -572,8 +572,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
<div className="flex flex-col justify-center gap-5">
|
||||
{!chatStarted && (
|
||||
<div className="flex justify-center gap-2">
|
||||
{ImportButtons(importChat)}
|
||||
<GitCloneButton importChat={importChat} />
|
||||
<div className="flex items-center gap-2">
|
||||
{ImportButtons(importChat)}
|
||||
<GitCloneButton importChat={importChat} className="min-w-[120px]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!chatStarted &&
|
||||
|
@ -6,22 +6,21 @@ import { generateId } from '~/utils/fileUtils';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
||||
import type { IChatMetadata } from '~/lib/persistence';
|
||||
import { RepositorySelectionDialog } from '~/components/settings/connections/components/RepositorySelectionDialog';
|
||||
import { cn } from '~/lib/utils';
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import type { IChatMetadata } from '~/lib/persistence/db';
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
'node_modules/**',
|
||||
'.git/**',
|
||||
'.github/**',
|
||||
'.vscode/**',
|
||||
'**/*.jpg',
|
||||
'**/*.jpeg',
|
||||
'**/*.png',
|
||||
'dist/**',
|
||||
'build/**',
|
||||
'.next/**',
|
||||
'coverage/**',
|
||||
'.cache/**',
|
||||
'.vscode/**',
|
||||
'.idea/**',
|
||||
'**/*.log',
|
||||
'**/.DS_Store',
|
||||
@ -34,51 +33,94 @@ const IGNORE_PATTERNS = [
|
||||
|
||||
const ig = ignore().add(IGNORE_PATTERNS);
|
||||
|
||||
const MAX_FILE_SIZE = 100 * 1024; // 100KB limit per file
|
||||
const MAX_TOTAL_SIZE = 500 * 1024; // 500KB total limit
|
||||
|
||||
interface GitCloneButtonProps {
|
||||
className?: string;
|
||||
importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||
export default function GitCloneButton({ importChat, className }: GitCloneButtonProps) {
|
||||
const { ready, gitClone } = useGit();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const onClick = async (_e: any) => {
|
||||
const handleClone = async (repoUrl: string) => {
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoUrl = prompt('Enter the Git url');
|
||||
setLoading(true);
|
||||
|
||||
if (repoUrl) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
|
||||
try {
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
console.log(filePaths);
|
||||
let totalSize = 0;
|
||||
const skippedFiles: string[] = [];
|
||||
const fileContents = [];
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
for (const filePath of filePaths) {
|
||||
const { data: content, encoding } = data[filePath];
|
||||
|
||||
const fileContents = filePaths
|
||||
.map((filePath) => {
|
||||
const { data: content, encoding } = data[filePath];
|
||||
return {
|
||||
path: filePath,
|
||||
content:
|
||||
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
|
||||
};
|
||||
})
|
||||
.filter((f) => f.content);
|
||||
// Skip binary files
|
||||
if (
|
||||
content instanceof Uint8Array &&
|
||||
!filePath.match(/\.(txt|md|js|jsx|ts|tsx|json|html|css|scss|less|yml|yaml|xml|svg)$/i)
|
||||
) {
|
||||
skippedFiles.push(filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
try {
|
||||
const textContent =
|
||||
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '';
|
||||
|
||||
if (!textContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
const fileSize = new TextEncoder().encode(textContent).length;
|
||||
|
||||
if (fileSize > MAX_FILE_SIZE) {
|
||||
skippedFiles.push(`${filePath} (too large: ${Math.round(fileSize / 1024)}KB)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check total size
|
||||
if (totalSize + fileSize > MAX_TOTAL_SIZE) {
|
||||
skippedFiles.push(`${filePath} (would exceed total size limit)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalSize += fileSize;
|
||||
fileContents.push({
|
||||
path: filePath,
|
||||
content: textContent,
|
||||
});
|
||||
} catch (e: any) {
|
||||
skippedFiles.push(`${filePath} (error: ${e.message})`);
|
||||
}
|
||||
}
|
||||
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
${
|
||||
skippedFiles.length > 0
|
||||
? `\nSkipped files (${skippedFiles.length}):
|
||||
${skippedFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: ''
|
||||
}
|
||||
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
${fileContents
|
||||
.map(
|
||||
@ -89,37 +131,50 @@ ${escapeBoltTags(file.content)}
|
||||
)
|
||||
.join('\n')}
|
||||
</boltArtifact>`,
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
id: generateId(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const messages = [filesMessage];
|
||||
const messages = [filesMessage];
|
||||
|
||||
if (commandsMessage) {
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl });
|
||||
if (commandsMessage) {
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
toast.error('Failed to import repository');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
toast.error('Failed to import repository');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={onClick}
|
||||
<Button
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
title="Clone a Git Repo"
|
||||
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className={cn(
|
||||
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
||||
'text-bolt-elements-textPrimary dark:text-white',
|
||||
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
||||
'border-[#E5E5E5] dark:border-[#333333]',
|
||||
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
className,
|
||||
)}
|
||||
disabled={!ready || loading}
|
||||
>
|
||||
<span className="i-ph:git-branch" />
|
||||
<span className="i-ph:git-branch w-4 h-4" />
|
||||
Clone a Git Repo
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<RepositorySelectionDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} onSelect={handleClone} />
|
||||
|
||||
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
|
||||
</>
|
||||
);
|
||||
|
@ -4,6 +4,8 @@ import { toast } from 'react-toastify';
|
||||
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
|
||||
import { createChatFromFolder } from '~/utils/folderImport';
|
||||
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { cn } from '~/lib/utils';
|
||||
|
||||
interface ImportFolderButtonProps {
|
||||
className?: string;
|
||||
@ -112,17 +114,27 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
||||
onChange={handleFileChange}
|
||||
{...({} as any)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
const input = document.getElementById('folder-import');
|
||||
input?.click();
|
||||
}}
|
||||
className={className}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className={cn(
|
||||
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
||||
'text-bolt-elements-textPrimary dark:text-white',
|
||||
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
||||
'border-[#E5E5E5] dark:border-[#333333]',
|
||||
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div className="i-ph:upload-simple" />
|
||||
<span className="i-ph:upload-simple w-4 h-4" />
|
||||
{isLoading ? 'Importing...' : 'Import Folder'}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { cn } from '~/lib/utils';
|
||||
|
||||
type ChatData = {
|
||||
messages?: Message[]; // Standard Bolt format
|
||||
@ -57,19 +59,35 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-4 max-w-2xl text-center">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
const input = document.getElementById('chat-import');
|
||||
input?.click();
|
||||
}}
|
||||
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className={cn(
|
||||
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
||||
'text-bolt-elements-textPrimary dark:text-white',
|
||||
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
||||
'border-[#E5E5E5] dark:border-[#333333]',
|
||||
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
)}
|
||||
>
|
||||
<div className="i-ph:upload-simple" />
|
||||
<span className="i-ph:upload-simple w-4 h-4" />
|
||||
Import Chat
|
||||
</button>
|
||||
</Button>
|
||||
<ImportFolderButton
|
||||
importChat={importChat}
|
||||
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
|
||||
className={cn(
|
||||
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
||||
'text-bolt-elements-textPrimary dark:text-white',
|
||||
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
||||
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
||||
'transition-all duration-200 ease-in-out rounded-lg',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,459 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getLocalStorage } from '~/utils/localStorage';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { GitHubUserResponse } from '~/types/GitHub';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
|
||||
interface PushToGitHubDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onPush: (repoName: string, username?: string, token?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface GitHubRepo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
default_branch: string;
|
||||
updated_at: string;
|
||||
language: string;
|
||||
private: boolean;
|
||||
}
|
||||
|
||||
export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDialogProps) {
|
||||
const [repoName, setRepoName] = useState('');
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [user, setUser] = useState<GitHubUserResponse | null>(null);
|
||||
const [recentRepos, setRecentRepos] = useState<GitHubRepo[]>([]);
|
||||
const [isFetchingRepos, setIsFetchingRepos] = useState(false);
|
||||
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
|
||||
const [createdRepoUrl, setCreatedRepoUrl] = useState('');
|
||||
|
||||
// Load GitHub connection on mount
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const connection = getLocalStorage('github_connection');
|
||||
|
||||
if (connection?.user && connection?.token) {
|
||||
setUser(connection.user);
|
||||
|
||||
// Only fetch if we have both user and token
|
||||
if (connection.token.trim()) {
|
||||
fetchRecentRepos(connection.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchRecentRepos = async (token: string) => {
|
||||
if (!token) {
|
||||
logStore.logError('No GitHub token available');
|
||||
toast.error('GitHub authentication required');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetchingRepos(true);
|
||||
|
||||
const response = await fetch(
|
||||
'https://api.github.com/user/repos?sort=updated&per_page=5&type=all&affiliation=owner,organization_member',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token.trim()}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
|
||||
if (response.status === 401) {
|
||||
toast.error('GitHub token expired. Please reconnect your account.');
|
||||
|
||||
// Clear invalid token
|
||||
const connection = getLocalStorage('github_connection');
|
||||
|
||||
if (connection) {
|
||||
localStorage.removeItem('github_connection');
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
logStore.logError('Failed to fetch GitHub repositories', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
});
|
||||
toast.error(`Failed to fetch repositories: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const repos = (await response.json()) as GitHubRepo[];
|
||||
setRecentRepos(repos);
|
||||
} catch (error) {
|
||||
logStore.logError('Failed to fetch GitHub repositories', { error });
|
||||
toast.error('Failed to fetch recent repositories');
|
||||
} finally {
|
||||
setIsFetchingRepos(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const connection = getLocalStorage('github_connection');
|
||||
|
||||
if (!connection?.token || !connection?.user) {
|
||||
toast.error('Please connect your GitHub account in Settings > Connections first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repoName.trim()) {
|
||||
toast.error('Repository name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await onPush(repoName, connection.user.login, connection.token);
|
||||
|
||||
const repoUrl = `https://github.com/${connection.user.login}/${repoName}`;
|
||||
setCreatedRepoUrl(repoUrl);
|
||||
setShowSuccessDialog(true);
|
||||
} catch (error) {
|
||||
console.error('Error pushing to GitHub:', error);
|
||||
toast.error('Failed to push to GitHub. Please check your repository name and try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setRepoName('');
|
||||
setIsPrivate(false);
|
||||
setShowSuccessDialog(false);
|
||||
setCreatedRepoUrl('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Success Dialog
|
||||
if (showSuccessDialog) {
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9999]" />
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-[90vw] md:w-[500px]"
|
||||
>
|
||||
<Dialog.Content className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A] shadow-xl">
|
||||
<div className="text-center space-y-4">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="mx-auto w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center text-green-500"
|
||||
>
|
||||
<div className="i-ph:check-circle-bold w-6 h-6" />
|
||||
</motion.div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark">
|
||||
Repository Created Successfully!
|
||||
</h3>
|
||||
<p className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark">
|
||||
Your code has been pushed to GitHub and is ready to use.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 rounded-lg p-3 text-left">
|
||||
<p className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark mb-2">
|
||||
Repository URL
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-sm bg-bolt-elements-background dark:bg-bolt-elements-background-dark px-3 py-2 rounded border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark font-mono">
|
||||
{createdRepoUrl}
|
||||
</code>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(createdRepoUrl);
|
||||
toast.success('URL copied to clipboard');
|
||||
}}
|
||||
className="p-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textSecondary-dark dark:hover:text-bolt-elements-textPrimary-dark"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<div className="i-ph:copy w-4 h-4" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<motion.button
|
||||
onClick={handleClose}
|
||||
className="flex-1 px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Close
|
||||
</motion.button>
|
||||
<motion.a
|
||||
href={createdRepoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-1 px-4 py-2 text-sm bg-purple-500 text-white rounded-lg hover:bg-purple-600 inline-flex items-center justify-center gap-2"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="i-ph:github-logo w-4 h-4" />
|
||||
Open Repository
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9999]" />
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-[90vw] md:w-[500px]"
|
||||
>
|
||||
<Dialog.Content className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A] shadow-xl">
|
||||
<div className="text-center space-y-4">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="mx-auto w-12 h-12 rounded-xl bg-bolt-elements-background-depth-3 flex items-center justify-center text-purple-500"
|
||||
>
|
||||
<div className="i-ph:github-logo w-6 h-6" />
|
||||
</motion.div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">GitHub Connection Required</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Please connect your GitHub account in Settings {'>'} Connections to push your code to GitHub.
|
||||
</p>
|
||||
<motion.button
|
||||
className="px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600 inline-flex items-center gap-2"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div className="i-ph:x-circle" />
|
||||
Close
|
||||
</motion.button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9999]" />
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-[90vw] md:w-[500px]"
|
||||
>
|
||||
<Dialog.Content className="bg-white dark:bg-[#0A0A0A] rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A] shadow-xl">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="w-10 h-10 rounded-xl bg-bolt-elements-background-depth-3 flex items-center justify-center text-purple-500"
|
||||
>
|
||||
<div className="i-ph:git-branch w-5 h-5" />
|
||||
</motion.div>
|
||||
<div>
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
Push to GitHub
|
||||
</Dialog.Title>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Push your code to a new or existing GitHub repository
|
||||
</p>
|
||||
</div>
|
||||
<Dialog.Close
|
||||
className="ml-auto p-2 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div className="i-ph:x w-5 h-5" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-6 p-3 bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 rounded-lg">
|
||||
<img src={user.avatar_url} alt={user.login} className="w-10 h-10 rounded-full" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{user.name || user.login}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">@{user.login}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="repoName" className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Repository Name
|
||||
</label>
|
||||
<input
|
||||
id="repoName"
|
||||
type="text"
|
||||
value={repoName}
|
||||
onChange={(e) => setRepoName(e.target.value)}
|
||||
placeholder="my-awesome-project"
|
||||
className="w-full px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-[#E5E5E5] dark:border-[#1A1A1A] text-gray-900 dark:text-white placeholder-gray-400"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{recentRepos.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-gray-600 dark:text-gray-400">Recent Repositories</label>
|
||||
<div className="space-y-2">
|
||||
{recentRepos.map((repo) => (
|
||||
<motion.button
|
||||
key={repo.full_name}
|
||||
type="button"
|
||||
onClick={() => setRepoName(repo.name)}
|
||||
className="w-full p-3 text-left rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 transition-colors group"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="i-ph:git-repository w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white group-hover:text-purple-500">
|
||||
{repo.name}
|
||||
</span>
|
||||
</div>
|
||||
{repo.private && (
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-purple-500/10 text-purple-500">
|
||||
Private
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{repo.description && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
{repo.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
|
||||
{repo.language && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:code w-3 h-3" />
|
||||
{repo.language}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:star w-3 h-3" />
|
||||
{repo.stargazers_count.toLocaleString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:git-fork w-3 h-3" />
|
||||
{repo.forks_count.toLocaleString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:clock w-3 h-3" />
|
||||
{new Date(repo.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFetchingRepos && (
|
||||
<div className="flex items-center justify-center py-4 text-gray-500 dark:text-gray-400">
|
||||
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4 mr-2" />
|
||||
Loading repositories...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="private"
|
||||
checked={isPrivate}
|
||||
onChange={(e) => setIsPrivate(e.target.checked)}
|
||||
className="rounded border-[#E5E5E5] dark:border-[#1A1A1A] text-purple-500 focus:ring-purple-500 dark:bg-[#0A0A0A]"
|
||||
/>
|
||||
<label htmlFor="private" className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Make repository private
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex gap-2">
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] text-gray-600 dark:text-gray-400 hover:bg-[#E5E5E5] dark:hover:bg-[#252525] text-sm"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Cancel
|
||||
</motion.button>
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={classNames(
|
||||
'flex-1 px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 text-sm inline-flex items-center justify-center gap-2',
|
||||
isLoading ? 'opacity-50 cursor-not-allowed' : '',
|
||||
)}
|
||||
whileHover={!isLoading ? { scale: 1.02 } : {}}
|
||||
whileTap={!isLoading ? { scale: 0.98 } : {}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
||||
Pushing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-ph:git-branch w-4 h-4" />
|
||||
Push to GitHub
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
@ -0,0 +1,692 @@
|
||||
import type { GitHubRepoInfo, GitHubContent, RepositoryStats } from '~/types/GitHub';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { cn } from '~/lib/utils';
|
||||
import { getLocalStorage } from '~/utils/localStorage';
|
||||
import { classNames as utilsClassNames } from '~/utils/classNames';
|
||||
import { motion } from 'framer-motion';
|
||||
import { formatSize } from '~/utils/formatSize';
|
||||
|
||||
interface GitHubTreeResponse {
|
||||
tree: Array<{
|
||||
path: string;
|
||||
type: string;
|
||||
size?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RepositorySelectionDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (url: string) => void;
|
||||
}
|
||||
|
||||
interface SearchFilters {
|
||||
language?: string;
|
||||
stars?: number;
|
||||
forks?: number;
|
||||
}
|
||||
|
||||
interface StatsDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
stats: RepositoryStats;
|
||||
isLargeRepo?: boolean;
|
||||
}
|
||||
|
||||
function StatsDialog({ isOpen, onClose, onConfirm, stats, isLargeRepo }: StatsDialogProps) {
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9999]" />
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-[90vw] md:w-[500px]"
|
||||
>
|
||||
<Dialog.Content className="bg-white dark:bg-[#1E1E1E] rounded-lg border border-[#E5E5E5] dark:border-[#333333] shadow-xl">
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-[#111111] dark:text-white">Repository Overview</h3>
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="text-sm text-[#666666] dark:text-[#999999]">Repository Statistics:</p>
|
||||
<div className="space-y-2 text-sm text-[#111111] dark:text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:files text-purple-500 w-4 h-4" />
|
||||
<span>Total Files: {stats.totalFiles}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:database text-purple-500 w-4 h-4" />
|
||||
<span>Total Size: {formatSize(stats.totalSize)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:code text-purple-500 w-4 h-4" />
|
||||
<span>
|
||||
Languages:{' '}
|
||||
{Object.entries(stats.languages)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 3)
|
||||
.map(([lang, size]) => `${lang} (${formatSize(size)})`)
|
||||
.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
{stats.hasPackageJson && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:package text-purple-500 w-4 h-4" />
|
||||
<span>Has package.json</span>
|
||||
</div>
|
||||
)}
|
||||
{stats.hasDependencies && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:tree-structure text-purple-500 w-4 h-4" />
|
||||
<span>Has dependencies</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLargeRepo && (
|
||||
<div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-500/10 rounded-lg text-sm flex items-start gap-2">
|
||||
<span className="i-ph:warning text-yellow-600 dark:text-yellow-500 w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-yellow-800 dark:text-yellow-500">
|
||||
This repository is quite large ({formatSize(stats.totalSize)}). Importing it might take a while
|
||||
and could impact performance.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-[#E5E5E5] dark:border-[#333333] p-4 flex justify-end gap-3 bg-[#F9F9F9] dark:bg-[#252525] rounded-b-lg">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#333333] text-[#666666] hover:text-[#111111] dark:text-[#999999] dark:hover:text-white transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: RepositorySelectionDialogProps) {
|
||||
const [selectedRepository, setSelectedRepository] = useState<GitHubRepoInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [repositories, setRepositories] = useState<GitHubRepoInfo[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<GitHubRepoInfo[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'my-repos' | 'search' | 'url'>('my-repos');
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [branches, setBranches] = useState<{ name: string; default?: boolean }[]>([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState('');
|
||||
const [filters, setFilters] = useState<SearchFilters>({});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [stats, setStats] = useState<RepositoryStats | null>(null);
|
||||
const [showStatsDialog, setShowStatsDialog] = useState(false);
|
||||
const [currentStats, setCurrentStats] = useState<RepositoryStats | null>(null);
|
||||
const [pendingGitUrl, setPendingGitUrl] = useState<string>('');
|
||||
|
||||
// Fetch user's repositories when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen && activeTab === 'my-repos') {
|
||||
fetchUserRepos();
|
||||
}
|
||||
}, [isOpen, activeTab]);
|
||||
|
||||
const fetchUserRepos = async () => {
|
||||
const connection = getLocalStorage('github_connection');
|
||||
|
||||
if (!connection?.token) {
|
||||
toast.error('Please connect your GitHub account first');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/user/repos?sort=updated&per_page=100&type=all', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch repositories');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Add type assertion and validation
|
||||
if (
|
||||
Array.isArray(data) &&
|
||||
data.every((item) => typeof item === 'object' && item !== null && 'full_name' in item)
|
||||
) {
|
||||
setRepositories(data as GitHubRepoInfo[]);
|
||||
} else {
|
||||
throw new Error('Invalid repository data format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching repos:', error);
|
||||
toast.error('Failed to fetch your repositories');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
setIsLoading(true);
|
||||
setSearchResults([]);
|
||||
|
||||
try {
|
||||
let searchQuery = query;
|
||||
|
||||
if (filters.language) {
|
||||
searchQuery += ` language:${filters.language}`;
|
||||
}
|
||||
|
||||
if (filters.stars) {
|
||||
searchQuery += ` stars:>${filters.stars}`;
|
||||
}
|
||||
|
||||
if (filters.forks) {
|
||||
searchQuery += ` forks:>${filters.forks}`;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/search/repositories?q=${encodeURIComponent(searchQuery)}&sort=stars&order=desc`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to search repositories');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Add type assertion and validation
|
||||
if (typeof data === 'object' && data !== null && 'items' in data && Array.isArray(data.items)) {
|
||||
setSearchResults(data.items as GitHubRepoInfo[]);
|
||||
} else {
|
||||
throw new Error('Invalid search results format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching repos:', error);
|
||||
toast.error('Failed to search repositories');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBranches = async (repo: GitHubRepoInfo) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${repo.full_name}/branches`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorage('github_connection')?.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch branches');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Add type assertion and validation
|
||||
if (Array.isArray(data) && data.every((item) => typeof item === 'object' && item !== null && 'name' in item)) {
|
||||
setBranches(
|
||||
data.map((branch) => ({
|
||||
name: branch.name,
|
||||
default: branch.name === repo.default_branch,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
throw new Error('Invalid branch data format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
toast.error('Failed to fetch branches');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepoSelect = async (repo: GitHubRepoInfo) => {
|
||||
setSelectedRepository(repo);
|
||||
await fetchBranches(repo);
|
||||
};
|
||||
|
||||
const formatGitUrl = (url: string): string => {
|
||||
// Remove any tree references and ensure .git extension
|
||||
const baseUrl = url
|
||||
.replace(/\/tree\/[^/]+/, '') // Remove /tree/branch-name
|
||||
.replace(/\/$/, '') // Remove trailing slash
|
||||
.replace(/\.git$/, ''); // Remove .git if present
|
||||
return `${baseUrl}.git`;
|
||||
};
|
||||
|
||||
const verifyRepository = async (repoUrl: string): Promise<RepositoryStats | null> => {
|
||||
try {
|
||||
const [owner, repo] = repoUrl
|
||||
.replace(/\.git$/, '')
|
||||
.split('/')
|
||||
.slice(-2);
|
||||
|
||||
const connection = getLocalStorage('github_connection');
|
||||
const headers: HeadersInit = connection?.token ? { Authorization: `Bearer ${connection.token}` } : {};
|
||||
|
||||
// Fetch repository tree
|
||||
const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
throw new Error('Failed to fetch repository structure');
|
||||
}
|
||||
|
||||
const treeData = (await treeResponse.json()) as GitHubTreeResponse;
|
||||
|
||||
// Calculate repository stats
|
||||
let totalSize = 0;
|
||||
let totalFiles = 0;
|
||||
const languages: { [key: string]: number } = {};
|
||||
let hasPackageJson = false;
|
||||
let hasDependencies = false;
|
||||
|
||||
for (const file of treeData.tree) {
|
||||
if (file.type === 'blob') {
|
||||
totalFiles++;
|
||||
|
||||
if (file.size) {
|
||||
totalSize += file.size;
|
||||
}
|
||||
|
||||
// Check for package.json
|
||||
if (file.path === 'package.json') {
|
||||
hasPackageJson = true;
|
||||
|
||||
// Fetch package.json content to check dependencies
|
||||
const contentResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/package.json`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (contentResponse.ok) {
|
||||
const content = (await contentResponse.json()) as GitHubContent;
|
||||
const packageJson = JSON.parse(Buffer.from(content.content, 'base64').toString());
|
||||
hasDependencies = !!(
|
||||
packageJson.dependencies ||
|
||||
packageJson.devDependencies ||
|
||||
packageJson.peerDependencies
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect language based on file extension
|
||||
const ext = file.path.split('.').pop()?.toLowerCase();
|
||||
|
||||
if (ext) {
|
||||
languages[ext] = (languages[ext] || 0) + (file.size || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stats: RepositoryStats = {
|
||||
totalFiles,
|
||||
totalSize,
|
||||
languages,
|
||||
hasPackageJson,
|
||||
hasDependencies,
|
||||
};
|
||||
|
||||
setStats(stats);
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('Error verifying repository:', error);
|
||||
toast.error('Failed to verify repository');
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
let gitUrl: string;
|
||||
|
||||
if (activeTab === 'url' && customUrl) {
|
||||
gitUrl = formatGitUrl(customUrl);
|
||||
} else if (selectedRepository) {
|
||||
gitUrl = formatGitUrl(selectedRepository.html_url);
|
||||
|
||||
if (selectedBranch) {
|
||||
gitUrl = `${gitUrl}#${selectedBranch}`;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify repository before importing
|
||||
const stats = await verifyRepository(gitUrl);
|
||||
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentStats(stats);
|
||||
setPendingGitUrl(gitUrl);
|
||||
setShowStatsDialog(true);
|
||||
} catch (error) {
|
||||
console.error('Error preparing repository:', error);
|
||||
toast.error('Failed to prepare repository. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatsConfirm = () => {
|
||||
setShowStatsDialog(false);
|
||||
|
||||
if (pendingGitUrl) {
|
||||
onSelect(pendingGitUrl);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (key: keyof SearchFilters, value: string) => {
|
||||
let parsedValue: string | number | undefined = value;
|
||||
|
||||
if (key === 'stars' || key === 'forks') {
|
||||
parsedValue = value ? parseInt(value, 10) : undefined;
|
||||
}
|
||||
|
||||
setFilters((prev) => ({ ...prev, [key]: parsedValue }));
|
||||
handleSearch(searchQuery);
|
||||
};
|
||||
|
||||
// Handle dialog close properly
|
||||
const handleClose = () => {
|
||||
setIsLoading(false); // Reset loading state
|
||||
setSearchQuery(''); // Reset search
|
||||
setSearchResults([]); // Reset results
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" />
|
||||
<Dialog.Content className="fixed top-[50%] left-[50%] -translate-x-1/2 -translate-y-1/2 w-[90vw] md:w-[600px] max-h-[85vh] overflow-hidden bg-white dark:bg-[#1A1A1A] rounded-xl shadow-xl z-[51] border border-[#E5E5E5] dark:border-[#333333]">
|
||||
<div className="p-4 border-b border-[#E5E5E5] dark:border-[#333333] flex items-center justify-between">
|
||||
<Dialog.Title className="text-lg font-semibold text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark">
|
||||
Import GitHub Repository
|
||||
</Dialog.Title>
|
||||
<Dialog.Close
|
||||
onClick={handleClose}
|
||||
className={cn(
|
||||
'p-2 rounded-lg transition-all duration-200 ease-in-out',
|
||||
'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary',
|
||||
'dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark',
|
||||
'hover:bg-bolt-elements-background-depth-2 dark:hover:bg-bolt-elements-background-depth-3',
|
||||
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor dark:focus:ring-bolt-elements-borderColor-dark',
|
||||
)}
|
||||
>
|
||||
<span className="i-ph:x block w-5 h-5" aria-hidden="true" />
|
||||
<span className="sr-only">Close dialog</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<TabButton active={activeTab === 'my-repos'} onClick={() => setActiveTab('my-repos')}>
|
||||
<span className="i-ph:book-bookmark" />
|
||||
My Repos
|
||||
</TabButton>
|
||||
<TabButton active={activeTab === 'search'} onClick={() => setActiveTab('search')}>
|
||||
<span className="i-ph:magnifying-glass" />
|
||||
Search
|
||||
</TabButton>
|
||||
<TabButton active={activeTab === 'url'} onClick={() => setActiveTab('url')}>
|
||||
<span className="i-ph:link" />
|
||||
URL
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{activeTab === 'url' ? (
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter GitHub repository URL..."
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
className="w-full px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary"
|
||||
/>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!customUrl}
|
||||
className="w-full h-10 px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2 justify-center"
|
||||
>
|
||||
Import Repository
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{activeTab === 'search' && (
|
||||
<div className="space-y-4 mb-4">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search repositories..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
handleSearch(e.target.value);
|
||||
}}
|
||||
className="flex-1 px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setFilters({})}
|
||||
className="px-3 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
|
||||
>
|
||||
<span className="i-ph:funnel-simple" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by language..."
|
||||
value={filters.language || ''}
|
||||
onChange={(e) => {
|
||||
setFilters({ ...filters, language: e.target.value });
|
||||
handleSearch(searchQuery);
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333]"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min stars..."
|
||||
value={filters.stars || ''}
|
||||
onChange={(e) => handleFilterChange('stars', e.target.value)}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333]"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Min forks..."
|
||||
value={filters.forks || ''}
|
||||
onChange={(e) => handleFilterChange('forks', e.target.value)}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{selectedRepository ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedRepository(null)}
|
||||
className="p-1.5 rounded-lg hover:bg-[#F5F5F5] dark:hover:bg-[#252525]"
|
||||
>
|
||||
<span className="i-ph:arrow-left w-4 h-4" />
|
||||
</button>
|
||||
<h3 className="font-medium">{selectedRepository.full_name}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-bolt-elements-textSecondary">Select Branch</label>
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={(e) => setSelectedBranch(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor dark:focus:ring-bolt-elements-borderColor-dark"
|
||||
>
|
||||
{branches.map((branch) => (
|
||||
<option
|
||||
key={branch.name}
|
||||
value={branch.name}
|
||||
className="bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark"
|
||||
>
|
||||
{branch.name} {branch.default ? '(default)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="w-full h-10 px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-all duration-200 flex items-center gap-2 justify-center"
|
||||
>
|
||||
Import Selected Branch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RepositoryList
|
||||
repos={activeTab === 'my-repos' ? repositories : searchResults}
|
||||
isLoading={isLoading}
|
||||
onSelect={handleRepoSelect}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
{currentStats && (
|
||||
<StatsDialog
|
||||
isOpen={showStatsDialog}
|
||||
onClose={handleStatsConfirm}
|
||||
onConfirm={handleStatsConfirm}
|
||||
stats={currentStats}
|
||||
isLargeRepo={currentStats.totalSize > 50 * 1024 * 1024}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={utilsClassNames(
|
||||
'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center',
|
||||
active
|
||||
? 'bg-purple-500 text-white hover:bg-purple-600'
|
||||
: 'bg-[#F5F5F5] dark:bg-[#252525] text-bolt-elements-textPrimary dark:text-white hover:bg-[#E5E5E5] dark:hover:bg-[#333333] border border-[#E5E5E5] dark:border-[#333333]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryList({
|
||||
repos,
|
||||
isLoading,
|
||||
onSelect,
|
||||
activeTab,
|
||||
}: {
|
||||
repos: GitHubRepoInfo[];
|
||||
isLoading: boolean;
|
||||
onSelect: (repo: GitHubRepoInfo) => void;
|
||||
activeTab: string;
|
||||
}) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8 text-bolt-elements-textSecondary">
|
||||
<span className="i-ph:spinner animate-spin mr-2" />
|
||||
Loading repositories...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (repos.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-bolt-elements-textSecondary">
|
||||
<span className="i-ph:folder-simple-dashed w-12 h-12 mb-2 opacity-50" />
|
||||
<p>{activeTab === 'my-repos' ? 'No repositories found' : 'Search for repositories'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return repos.map((repo) => <RepositoryCard key={repo.full_name} repo={repo} onSelect={() => onSelect(repo)} />);
|
||||
}
|
||||
|
||||
function RepositoryCard({ repo, onSelect }: { repo: GitHubRepoInfo; onSelect: () => void }) {
|
||||
return (
|
||||
<div className="p-4 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333] hover:border-purple-500/50 transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="i-ph:git-repository text-bolt-elements-textTertiary" />
|
||||
<h3 className="font-medium text-bolt-elements-textPrimary dark:text-white">{repo.name}</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSelect}
|
||||
className="px-4 py-2 h-10 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center"
|
||||
>
|
||||
<span className="i-ph:download-simple w-4 h-4" />
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
{repo.description && <p className="text-sm text-bolt-elements-textSecondary mb-3">{repo.description}</p>}
|
||||
<div className="flex items-center gap-4 text-sm text-bolt-elements-textTertiary">
|
||||
{repo.language && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="i-ph:code" />
|
||||
{repo.language}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="i-ph:star" />
|
||||
{repo.stargazers_count.toLocaleString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="i-ph:clock" />
|
||||
{new Date(repo.updated_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -370,14 +370,25 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Trap focus when window is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Prevent background scrolling
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center z-[60]"
|
||||
style={{ opacity: developerMode ? 1 : 0, transition: 'opacity 0.2s ease-in-out' }}
|
||||
>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
||||
<RadixDialog.Overlay className="fixed inset-0">
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
@ -388,7 +399,12 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
|
||||
<RadixDialog.Content aria-describedby={undefined} className="relative z-[61]">
|
||||
<RadixDialog.Content
|
||||
aria-describedby={undefined}
|
||||
onEscapeKeyDown={onClose}
|
||||
onPointerDownOutside={onClose}
|
||||
className="relative z-[101]"
|
||||
>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'w-[1200px] h-[90vh]',
|
||||
|
@ -515,13 +515,27 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
// Trap focus when window is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Prevent background scrolling
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeveloperWindow open={showDeveloperWindow} onClose={handleDeveloperWindowClose} />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RadixDialog.Root open={open && !showDeveloperWindow}>
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[50]">
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
||||
<RadixDialog.Overlay asChild>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
@ -531,7 +545,12 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
<RadixDialog.Content aria-describedby={undefined} asChild>
|
||||
<RadixDialog.Content
|
||||
aria-describedby={undefined}
|
||||
onEscapeKeyDown={onClose}
|
||||
onPointerDownOutside={onClose}
|
||||
className="relative z-[101]"
|
||||
>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'relative',
|
||||
|
@ -24,47 +24,45 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
|
||||
syncWithGlobalStore: isActiveChat,
|
||||
});
|
||||
|
||||
const renderDescriptionForm = (
|
||||
<form onSubmit={handleSubmit} className="flex-1 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2"
|
||||
autoFocus
|
||||
value={currentDescription}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="i-ph:check scale-110 hover:text-bolt-elements-item-contentAccent"
|
||||
onMouseDown={handleSubmit}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1',
|
||||
{ '[&&]:text-bolt-elements-textPrimary bg-bolt-elements-background-depth-3': isActiveChat },
|
||||
'group rounded-lg text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50/80 dark:hover:bg-gray-800/30 overflow-hidden flex justify-between items-center px-3 py-2 transition-colors',
|
||||
{ 'text-gray-900 dark:text-white bg-gray-50/80 dark:bg-gray-800/30': isActiveChat },
|
||||
)}
|
||||
>
|
||||
{editing ? (
|
||||
renderDescriptionForm
|
||||
<form onSubmit={handleSubmit} className="flex-1 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md px-3 py-1.5 text-sm border border-gray-200 dark:border-gray-800 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
|
||||
autoFocus
|
||||
value={currentDescription}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="i-ph:check h-4 w-4 text-gray-500 hover:text-purple-500 transition-colors"
|
||||
onMouseDown={handleSubmit}
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
||||
{currentDescription}
|
||||
<WithTooltip tooltip={currentDescription}>
|
||||
<span className="truncate pr-24">{currentDescription}</span>
|
||||
</WithTooltip>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-22 group-hover:from-99%',
|
||||
{ 'from-bolt-elements-background-depth-3 w-10 ': isActiveChat },
|
||||
'absolute right-0 top-0 bottom-0 flex items-center bg-white dark:bg-gray-950 group-hover:bg-gray-50/80 dark:group-hover:bg-gray-800/30 px-2',
|
||||
{ 'bg-gray-50/80 dark:bg-gray-800/30': isActiveChat },
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-2.5 text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<ChatActionButton
|
||||
toolTipContent="Export chat"
|
||||
icon="i-ph:download-simple"
|
||||
toolTipContent="Export"
|
||||
icon="i-ph:download-simple h-4 w-4"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
exportChat(item.id);
|
||||
@ -72,14 +70,14 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
|
||||
/>
|
||||
{onDuplicate && (
|
||||
<ChatActionButton
|
||||
toolTipContent="Duplicate chat"
|
||||
icon="i-ph:copy"
|
||||
toolTipContent="Duplicate"
|
||||
icon="i-ph:copy h-4 w-4"
|
||||
onClick={() => onDuplicate?.(item.id)}
|
||||
/>
|
||||
)}
|
||||
<ChatActionButton
|
||||
toolTipContent="Rename chat"
|
||||
icon="i-ph:pencil-fill"
|
||||
toolTipContent="Rename"
|
||||
icon="i-ph:pencil-fill h-4 w-4"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
toggleEditMode();
|
||||
@ -87,9 +85,9 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
|
||||
/>
|
||||
<Dialog.Trigger asChild>
|
||||
<ChatActionButton
|
||||
toolTipContent="Delete chat"
|
||||
icon="i-ph:trash"
|
||||
className="[&&]:hover:text-bolt-elements-button-danger-text"
|
||||
toolTipContent="Delete"
|
||||
icon="i-ph:trash h-4 w-4"
|
||||
className="hover:text-red-500"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onDelete?.(event);
|
||||
@ -121,11 +119,11 @@ const ChatActionButton = forwardRef(
|
||||
ref: ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
return (
|
||||
<WithTooltip tooltip={toolTipContent}>
|
||||
<WithTooltip tooltip={toolTipContent} position="bottom" sideOffset={4}>
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={`scale-110 mr-2 hover:text-bolt-elements-item-contentAccent ${icon} ${className ? className : ''}`}
|
||||
className={`text-gray-400 dark:text-gray-500 hover:text-purple-500 dark:hover:text-purple-400 transition-colors ${icon} ${className ? className : ''}`}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</WithTooltip>
|
||||
|
@ -11,12 +11,13 @@ import { logger } from '~/utils/logger';
|
||||
import { HistoryItem } from './HistoryItem';
|
||||
import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
opacity: 0,
|
||||
visibility: 'hidden',
|
||||
left: '-150px',
|
||||
left: '-340px',
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: cubicEasingFn,
|
||||
@ -41,15 +42,18 @@ function CurrentDateTime() {
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setDateTime(new Date());
|
||||
}, 60000); // Update every minute
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-3 font-bold text-gray-700 dark:text-gray-300 border-b border-bolt-elements-borderColor">
|
||||
<div className="h-4 w-4 i-ph:clock-thin" />
|
||||
{dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
<div className="flex items-center gap-2 px-4 py-2 text-sm text-gray-600 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800/50">
|
||||
<div className="h-4 w-4 i-lucide:clock opacity-80" />
|
||||
<div className="flex gap-2">
|
||||
<span>{dateTime.toLocaleDateString()}</span>
|
||||
<span>{dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -111,6 +115,10 @@ export const Menu = () => {
|
||||
const exitThreshold = 40;
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
if (isSettingsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.pageX < enterThreshold) {
|
||||
setOpen(true);
|
||||
}
|
||||
@ -125,7 +133,7 @@ export const Menu = () => {
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
};
|
||||
}, []);
|
||||
}, [isSettingsOpen]);
|
||||
|
||||
const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
|
||||
event.preventDefault();
|
||||
@ -137,96 +145,122 @@ export const Menu = () => {
|
||||
loadEntries(); // Reload the list after duplication
|
||||
};
|
||||
|
||||
const handleSettingsClick = () => {
|
||||
setIsSettingsOpen(true);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSettingsClose = () => {
|
||||
setIsSettingsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
initial="closed"
|
||||
animate={open ? 'open' : 'closed'}
|
||||
variants={menuVariants}
|
||||
className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
|
||||
>
|
||||
<div className="h-[60px]" /> {/* Spacer for top margin */}
|
||||
<CurrentDateTime />
|
||||
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
|
||||
<div className="p-4 select-none">
|
||||
<a
|
||||
href="/"
|
||||
className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme mb-4"
|
||||
>
|
||||
<span className="inline-block i-bolt:chat scale-110" />
|
||||
Start new chat
|
||||
</a>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
onChange={handleSearchChange}
|
||||
aria-label="Search chats"
|
||||
/>
|
||||
<>
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
initial="closed"
|
||||
animate={open ? 'open' : 'closed'}
|
||||
variants={menuVariants}
|
||||
style={{ width: '340px' }}
|
||||
className={classNames(
|
||||
'flex selection-accent flex-col side-menu fixed top-0 h-full',
|
||||
'bg-white dark:bg-gray-950 border-r border-gray-100 dark:border-gray-800/50',
|
||||
'shadow-sm text-sm',
|
||||
isSettingsOpen ? 'z-40' : 'z-sidebar',
|
||||
)}
|
||||
>
|
||||
<div className="h-12 flex items-center px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50"></div>
|
||||
<CurrentDateTime />
|
||||
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
|
||||
<div className="p-4 space-y-3">
|
||||
<a
|
||||
href="/"
|
||||
className="flex gap-2 items-center bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-500/20 rounded-lg px-4 py-2 transition-colors"
|
||||
>
|
||||
<span className="inline-block i-lucide:message-square h-4 w-4" />
|
||||
<span className="text-sm font-medium">Start new chat</span>
|
||||
</a>
|
||||
<div className="relative w-full">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
||||
<span className="i-lucide:search h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
className="w-full bg-gray-50 dark:bg-gray-900 relative pl-9 pr-3 py-2 rounded-lg focus:outline-none focus:ring-1 focus:ring-purple-500/50 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-500 border border-gray-200 dark:border-gray-800"
|
||||
type="search"
|
||||
placeholder="Search chats..."
|
||||
onChange={handleSearchChange}
|
||||
aria-label="Search chats"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-400 text-sm font-medium px-4 py-2">Your Chats</div>
|
||||
<div className="flex-1 overflow-auto px-3 pb-3">
|
||||
{filteredList.length === 0 && (
|
||||
<div className="px-4 text-gray-500 dark:text-gray-400 text-sm">
|
||||
{list.length === 0 ? 'No previous conversations' : 'No matches found'}
|
||||
</div>
|
||||
)}
|
||||
<DialogRoot open={dialogContent !== null}>
|
||||
{binDates(filteredList).map(({ category, items }) => (
|
||||
<div key={category} className="mt-2 first:mt-0 space-y-1">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 sticky top-0 z-1 bg-white dark:bg-gray-950 px-4 py-1">
|
||||
{category}
|
||||
</div>
|
||||
<div className="space-y-0.5 pr-1">
|
||||
{items.map((item) => (
|
||||
<HistoryItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
exportChat={exportChat}
|
||||
onDelete={(event) => handleDeleteClick(event, item)}
|
||||
onDuplicate={() => handleDuplicate(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Dialog onBackdrop={closeDialog} onClose={closeDialog}>
|
||||
{dialogContent?.type === 'delete' && (
|
||||
<>
|
||||
<div className="p-6 bg-white dark:bg-gray-950">
|
||||
<DialogTitle className="text-gray-900 dark:text-white">Delete Chat?</DialogTitle>
|
||||
<DialogDescription className="mt-2 text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
You are about to delete{' '}
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{dialogContent.item.description}
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-2">Are you sure you want to delete this chat?</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800">
|
||||
<DialogButton type="secondary" onClick={closeDialog}>
|
||||
Cancel
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
type="danger"
|
||||
onClick={(event) => {
|
||||
deleteItem(event, dialogContent.item);
|
||||
closeDialog();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DialogButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t border-gray-200 dark:border-gray-800 px-4 py-3">
|
||||
<SettingsButton onClick={handleSettingsClick} />
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
|
||||
<div className="flex-1 overflow-auto pl-4 pr-5 pb-5">
|
||||
{filteredList.length === 0 && (
|
||||
<div className="pl-2 text-bolt-elements-textTertiary">
|
||||
{list.length === 0 ? 'No previous conversations' : 'No matches found'}
|
||||
</div>
|
||||
)}
|
||||
<DialogRoot open={dialogContent !== null}>
|
||||
{binDates(filteredList).map(({ category, items }) => (
|
||||
<div key={category} className="mt-4 first:mt-0 space-y-1">
|
||||
<div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
|
||||
{category}
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<HistoryItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
exportChat={exportChat}
|
||||
onDelete={(event) => handleDeleteClick(event, item)}
|
||||
onDuplicate={() => handleDuplicate(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<Dialog onBackdrop={closeDialog} onClose={closeDialog}>
|
||||
{dialogContent?.type === 'delete' && (
|
||||
<>
|
||||
<DialogTitle>Delete Chat?</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div>
|
||||
<p>
|
||||
You are about to delete <strong>{dialogContent.item.description}</strong>.
|
||||
</p>
|
||||
<p className="mt-1">Are you sure you want to delete this chat?</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
<div className="px-5 pb-4 bg-bolt-elements-background-depth-2 flex gap-2 justify-end">
|
||||
<DialogButton type="secondary" onClick={closeDialog}>
|
||||
Cancel
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
type="danger"
|
||||
onClick={(event) => {
|
||||
deleteItem(event, dialogContent.item);
|
||||
closeDialog();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DialogButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t border-bolt-elements-borderColor p-4">
|
||||
<SettingsButton onClick={() => setIsSettingsOpen(true)} />
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<UsersWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<UsersWindow open={isSettingsOpen} onClose={handleSettingsClose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -39,21 +39,21 @@ function dateCategory(date: Date) {
|
||||
}
|
||||
|
||||
if (isThisWeek(date)) {
|
||||
// e.g., "Monday"
|
||||
return format(date, 'eeee');
|
||||
// e.g., "Mon" instead of "Monday"
|
||||
return format(date, 'EEE');
|
||||
}
|
||||
|
||||
const thirtyDaysAgo = subDays(new Date(), 30);
|
||||
|
||||
if (isAfter(date, thirtyDaysAgo)) {
|
||||
return 'Last 30 Days';
|
||||
return 'Past 30 Days';
|
||||
}
|
||||
|
||||
if (isThisYear(date)) {
|
||||
// e.g., "July"
|
||||
return format(date, 'MMMM');
|
||||
// e.g., "Jan" instead of "January"
|
||||
return format(date, 'LLL');
|
||||
}
|
||||
|
||||
// e.g., "July 2023"
|
||||
return format(date, 'MMMM yyyy');
|
||||
// e.g., "Jan 2023" instead of "January 2023"
|
||||
return format(date, 'LLL yyyy');
|
||||
}
|
||||
|
46
app/components/ui/Button.tsx
Normal file
46
app/components/ui/Button.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '~/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-bolt-elements-borderColor disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-bolt-elements-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2',
|
||||
destructive: 'bg-red-500 text-white hover:bg-red-600',
|
||||
outline:
|
||||
'border border-input bg-transparent hover:bg-bolt-elements-background-depth-2 hover:text-bolt-elements-textPrimary',
|
||||
secondary:
|
||||
'bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2',
|
||||
ghost: 'hover:bg-bolt-elements-background-depth-1 hover:text-bolt-elements-textPrimary',
|
||||
link: 'text-bolt-elements-textPrimary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
_asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, _asChild = false, ...props }, ref) => {
|
||||
return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
@ -17,10 +17,11 @@ interface DialogButtonProps {
|
||||
export const DialogButton = memo(({ type, children, onClick, disabled }: DialogButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames('inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm', {
|
||||
'bg-purple-500 text-white hover:bg-purple-600': type === 'primary',
|
||||
'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary': type === 'secondary',
|
||||
'text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10': type === 'danger',
|
||||
className={classNames('inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm transition-colors', {
|
||||
'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:hover:bg-purple-600': type === 'primary',
|
||||
'bg-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100':
|
||||
type === 'secondary',
|
||||
'bg-transparent text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10': type === 'danger',
|
||||
})}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
|
@ -17,8 +17,7 @@ import { renderLogger } from '~/utils/logger';
|
||||
import { EditorPanel } from './EditorPanel';
|
||||
import { Preview } from './Preview';
|
||||
import useViewport from '~/lib/hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
import { chatMetadata, useChatHistory } from '~/lib/persistence';
|
||||
import { PushToGitHubDialog } from '~/components/settings/connections/components/PushToGitHubDialog';
|
||||
|
||||
interface WorkspaceProps {
|
||||
chatStarted?: boolean;
|
||||
@ -59,6 +58,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
renderLogger.trace('Workbench');
|
||||
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
|
||||
|
||||
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
|
||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||
@ -67,8 +67,6 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const selectedView = useStore(workbenchStore.currentView);
|
||||
const metadata = useStore(chatMetadata);
|
||||
const { updateChatMestaData } = useChatHistory();
|
||||
|
||||
const isSmallViewport = useViewport(1024);
|
||||
|
||||
@ -171,65 +169,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
let repoName = metadata?.gitUrl?.split('/').slice(-1)[0]?.replace('.git', '') || null;
|
||||
let repoConfirmed: boolean = true;
|
||||
|
||||
if (repoName) {
|
||||
repoConfirmed = confirm(`Do you want to push to the repository ${repoName}?`);
|
||||
}
|
||||
|
||||
if (!repoName || !repoConfirmed) {
|
||||
repoName = prompt(
|
||||
'Please enter a name for your new GitHub repository:',
|
||||
'bolt-generated-project',
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!repoName) {
|
||||
alert('Repository name is required. Push to GitHub cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
let githubUsername = Cookies.get('githubUsername');
|
||||
let githubToken = Cookies.get('githubToken');
|
||||
|
||||
if (!githubUsername || !githubToken) {
|
||||
const usernameInput = prompt('Please enter your GitHub username:');
|
||||
const tokenInput = prompt('Please enter your GitHub personal access token:');
|
||||
|
||||
if (!usernameInput || !tokenInput) {
|
||||
alert('GitHub username and token are required. Push to GitHub cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
githubUsername = usernameInput;
|
||||
githubToken = tokenInput;
|
||||
|
||||
Cookies.set('githubUsername', usernameInput);
|
||||
Cookies.set('githubToken', tokenInput);
|
||||
Cookies.set(
|
||||
'git:github.com',
|
||||
JSON.stringify({ username: tokenInput, password: 'x-oauth-basic' }),
|
||||
);
|
||||
}
|
||||
|
||||
const commitMessage =
|
||||
prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
|
||||
workbenchStore.pushToGitHub(repoName, commitMessage, githubUsername, githubToken);
|
||||
|
||||
if (!metadata?.gitUrl) {
|
||||
updateChatMestaData({
|
||||
...(metadata || {}),
|
||||
gitUrl: `https://github.com/${githubUsername}/${repoName}.git`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:github-logo" />
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={() => setIsPushDialogOpen(true)}>
|
||||
<div className="i-ph:git-branch" />
|
||||
Push to GitHub
|
||||
</PanelHeaderButton>
|
||||
</div>
|
||||
@ -271,10 +212,26 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PushToGitHubDialog
|
||||
isOpen={isPushDialogOpen}
|
||||
onClose={() => setIsPushDialogOpen(false)}
|
||||
onPush={async (repoName, username, token) => {
|
||||
try {
|
||||
await workbenchStore.pushToGitHub(repoName, undefined, username, token);
|
||||
|
||||
// Success dialog will be shown by PushToGitHubDialog component
|
||||
} catch (error) {
|
||||
console.error('Error pushing to GitHub:', error);
|
||||
toast.error('Failed to push to GitHub');
|
||||
throw error; // Rethrow to let PushToGitHubDialog handle the error state
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
interface ViewProps extends HTMLMotionProps<'div'> {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
@ -19,7 +19,12 @@ export default class GroqProvider extends BaseProvider {
|
||||
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
|
||||
{ name: 'deepseek-r1-distill-llama-70b', label: 'Deepseek R1 Distill Llama 70b (Groq)', provider: 'Groq', maxTokenAllowed: 131072 },
|
||||
{
|
||||
name: 'deepseek-r1-distill-llama-70b',
|
||||
label: 'Deepseek R1 Distill Llama 70b (Groq)',
|
||||
provider: 'Groq',
|
||||
maxTokenAllowed: 131072,
|
||||
},
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
|
@ -536,7 +536,7 @@ export class WorkbenchStore {
|
||||
sha: newCommit.sha,
|
||||
});
|
||||
|
||||
alert(`Repository created and code pushed: ${repo.html_url}`);
|
||||
return repo.html_url; // Return the URL instead of showing alert
|
||||
} catch (error) {
|
||||
console.error('Error pushing to GitHub:', error);
|
||||
throw error; // Rethrow the error for further handling
|
||||
|
133
app/types/GitHub.ts
Normal file
133
app/types/GitHub.ts
Normal file
@ -0,0 +1,133 @@
|
||||
export interface GitHubUserResponse {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
public_repos: number;
|
||||
followers: number;
|
||||
following: number;
|
||||
public_gists: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface GitHubRepoInfo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
default_branch: string;
|
||||
updated_at: string;
|
||||
language: string;
|
||||
languages_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubContent {
|
||||
name: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
url: string;
|
||||
html_url: string;
|
||||
git_url: string;
|
||||
download_url: string;
|
||||
type: string;
|
||||
content: string;
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export interface GitHubBranch {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubBlobResponse {
|
||||
content: string;
|
||||
encoding: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface GitHubOrganization {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
description: string;
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
repo: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
payload: {
|
||||
action?: string;
|
||||
ref?: string;
|
||||
ref_type?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubLanguageStats {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export interface GitHubStats {
|
||||
repos: GitHubRepoInfo[];
|
||||
totalStars: number;
|
||||
totalForks: number;
|
||||
organizations: GitHubOrganization[];
|
||||
recentActivity: GitHubEvent[];
|
||||
languages: GitHubLanguageStats;
|
||||
totalGists: number;
|
||||
}
|
||||
|
||||
export interface GitHubConnection {
|
||||
user: GitHubUserResponse | null;
|
||||
token: string;
|
||||
tokenType: 'classic' | 'fine-grained';
|
||||
stats?: GitHubStats;
|
||||
}
|
||||
|
||||
export interface GitHubTokenInfo {
|
||||
token: string;
|
||||
scope: string[];
|
||||
avatar_url: string;
|
||||
name: string | null;
|
||||
created_at: string;
|
||||
followers: number;
|
||||
}
|
||||
|
||||
export interface GitHubRateLimits {
|
||||
limit: number;
|
||||
remaining: number;
|
||||
reset: Date;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export interface GitHubAuthState {
|
||||
username: string;
|
||||
tokenInfo: GitHubTokenInfo | null;
|
||||
isConnected: boolean;
|
||||
isVerifying: boolean;
|
||||
isLoadingRepos: boolean;
|
||||
rateLimits?: GitHubRateLimits;
|
||||
}
|
||||
|
||||
export interface RepositoryStats {
|
||||
totalFiles: number;
|
||||
totalSize: number;
|
||||
languages: Record<string, number>;
|
||||
hasPackageJson: boolean;
|
||||
hasDependencies: boolean;
|
||||
}
|
12
app/utils/formatSize.ts
Normal file
12
app/utils/formatSize.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
17
changelog.md
17
changelog.md
@ -6,19 +6,16 @@ Release v0.0.6
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* implement Claude 3, Claude3.5, Nova Pro, Nova Lite and Mistral model integration with AWS Bedrock ([#974](https://github.com/stackblitz-labs/bolt.diy/pull/974)) by @kunjabijukchhe
|
||||
* enhance chat import with multi-format support ([#936](https://github.com/stackblitz-labs/bolt.diy/pull/936)) by @sidbetatester
|
||||
* added Github provider ([#1109](https://github.com/stackblitz-labs/bolt.diy/pull/1109)) by @newnol
|
||||
* added the "Open Preview in a New Tab" ([#1101](https://github.com/stackblitz-labs/bolt.diy/pull/1101)) by @Stijnus
|
||||
* configure dynamic providers via .env ([#1108](https://github.com/stackblitz-labs/bolt.diy/pull/1108)) by @mrsimpson
|
||||
* added deepseek reasoner model in deepseek provider ([#1151](https://github.com/stackblitz-labs/bolt.diy/pull/1151)) by @thecodacus
|
||||
* enhance context handling by adding code context selection and implementing summary generation ([#1091](https://github.com/stackblitz-labs/bolt.diy/pull/1091)) by @thecodacus
|
||||
|
||||
- implement Claude 3, Claude3.5, Nova Pro, Nova Lite and Mistral model integration with AWS Bedrock ([#974](https://github.com/stackblitz-labs/bolt.diy/pull/974)) by @kunjabijukchhe
|
||||
- enhance chat import with multi-format support ([#936](https://github.com/stackblitz-labs/bolt.diy/pull/936)) by @sidbetatester
|
||||
- added Github provider ([#1109](https://github.com/stackblitz-labs/bolt.diy/pull/1109)) by @newnol
|
||||
- added the "Open Preview in a New Tab" ([#1101](https://github.com/stackblitz-labs/bolt.diy/pull/1101)) by @Stijnus
|
||||
- configure dynamic providers via .env ([#1108](https://github.com/stackblitz-labs/bolt.diy/pull/1108)) by @mrsimpson
|
||||
- added deepseek reasoner model in deepseek provider ([#1151](https://github.com/stackblitz-labs/bolt.diy/pull/1151)) by @thecodacus
|
||||
- enhance context handling by adding code context selection and implementing summary generation ([#1091](https://github.com/stackblitz-labs/bolt.diy/pull/1091)) by @thecodacus
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
**Full Changelog**: [`v0.0.5..v0.0.6`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.5...v0.0.6)
|
||||
|
@ -6,8 +6,8 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
target: bolt-ai-production
|
||||
ports:
|
||||
- "5173:5173"
|
||||
env_file: ".env.local"
|
||||
- '5173:5173'
|
||||
env_file: '.env.local'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- COMPOSE_PROFILES=production
|
||||
@ -28,7 +28,7 @@ services:
|
||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||
- RUNNING_IN_DOCKER=true
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
- 'host.docker.internal:host-gateway'
|
||||
command: pnpm run dockerstart
|
||||
profiles:
|
||||
- production
|
||||
@ -37,7 +37,7 @@ services:
|
||||
image: bolt-ai:development
|
||||
build:
|
||||
target: bolt-ai-development
|
||||
env_file: ".env.local"
|
||||
env_file: '.env.local'
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_HMR_PROTOCOL=ws
|
||||
@ -61,7 +61,7 @@ services:
|
||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||
- RUNNING_IN_DOCKER=true
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
- 'host.docker.internal:host-gateway'
|
||||
volumes:
|
||||
- type: bind
|
||||
source: .
|
||||
@ -69,14 +69,14 @@ services:
|
||||
consistency: cached
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
- '5173:5173'
|
||||
command: pnpm run dev --host 0.0.0.0
|
||||
profiles: ["development", "default"]
|
||||
profiles: ['development', 'default']
|
||||
|
||||
app-prebuild:
|
||||
image: ghcr.io/stackblitz-labs/bolt.diy:latest
|
||||
image: ghcr.io/stackblitz-labs/bolt.diy:latest
|
||||
ports:
|
||||
- "5173:5173"
|
||||
- '5173:5173'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- COMPOSE_PROFILES=production
|
||||
@ -86,7 +86,7 @@ services:
|
||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||
- RUNNING_IN_DOCKER=true
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
- 'host.docker.internal:host-gateway'
|
||||
command: pnpm run dockerstart
|
||||
profiles:
|
||||
- prebuilt
|
||||
- prebuilt
|
||||
|
@ -6,15 +6,15 @@ Welcome! This guide provides all the details you need to contribute effectively
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Code of Conduct](#code-of-conduct)
|
||||
2. [How Can I Contribute?](#how-can-i-contribute)
|
||||
3. [Pull Request Guidelines](#pull-request-guidelines)
|
||||
4. [Coding Standards](#coding-standards)
|
||||
5. [Development Setup](#development-setup)
|
||||
6. [Testing](#testing)
|
||||
7. [Deployment](#deployment)
|
||||
8. [Docker Deployment](#docker-deployment)
|
||||
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
|
||||
1. [Code of Conduct](#code-of-conduct)
|
||||
2. [How Can I Contribute?](#how-can-i-contribute)
|
||||
3. [Pull Request Guidelines](#pull-request-guidelines)
|
||||
4. [Coding Standards](#coding-standards)
|
||||
5. [Development Setup](#development-setup)
|
||||
6. [Testing](#testing)
|
||||
7. [Deployment](#deployment)
|
||||
8. [Docker Deployment](#docker-deployment)
|
||||
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
|
||||
|
||||
---
|
||||
|
||||
@ -27,60 +27,67 @@ This project is governed by our **Code of Conduct**. By participating, you agree
|
||||
## 🛠️ How Can I Contribute?
|
||||
|
||||
### 1️⃣ Reporting Bugs or Feature Requests
|
||||
|
||||
- Check the [issue tracker](#) to avoid duplicates.
|
||||
- Use issue templates (if available).
|
||||
- Use issue templates (if available).
|
||||
- Provide detailed, relevant information and steps to reproduce bugs.
|
||||
|
||||
### 2️⃣ Code Contributions
|
||||
1. Fork the repository.
|
||||
2. Create a feature or fix branch.
|
||||
3. Write and test your code.
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature or fix branch.
|
||||
3. Write and test your code.
|
||||
4. Submit a pull request (PR).
|
||||
|
||||
### 3️⃣ Join as a Core Contributor
|
||||
### 3️⃣ Join as a Core Contributor
|
||||
|
||||
Interested in maintaining and growing the project? Fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Pull Request Guidelines
|
||||
|
||||
### PR Checklist
|
||||
- Branch from the **main** branch.
|
||||
- Update documentation, if needed.
|
||||
- Test all functionality manually.
|
||||
- Focus on one feature/bug per PR.
|
||||
### PR Checklist
|
||||
|
||||
### Review Process
|
||||
1. Manual testing by reviewers.
|
||||
2. At least one maintainer review required.
|
||||
3. Address review comments.
|
||||
- Branch from the **main** branch.
|
||||
- Update documentation, if needed.
|
||||
- Test all functionality manually.
|
||||
- Focus on one feature/bug per PR.
|
||||
|
||||
### Review Process
|
||||
|
||||
1. Manual testing by reviewers.
|
||||
2. At least one maintainer review required.
|
||||
3. Address review comments.
|
||||
4. Maintain a clean commit history.
|
||||
|
||||
---
|
||||
|
||||
## 📏 Coding Standards
|
||||
|
||||
### General Guidelines
|
||||
- Follow existing code style.
|
||||
- Comment complex logic.
|
||||
- Keep functions small and focused.
|
||||
### General Guidelines
|
||||
|
||||
- Follow existing code style.
|
||||
- Comment complex logic.
|
||||
- Keep functions small and focused.
|
||||
- Use meaningful variable names.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Development Setup
|
||||
|
||||
### 1️⃣ Initial Setup
|
||||
- Clone the repository:
|
||||
### 1️⃣ Initial Setup
|
||||
|
||||
- Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/stackblitz-labs/bolt.diy.git
|
||||
```
|
||||
- Install dependencies:
|
||||
- Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
- Set up environment variables:
|
||||
1. Rename `.env.example` to `.env.local`.
|
||||
- Set up environment variables:
|
||||
1. Rename `.env.example` to `.env.local`.
|
||||
2. Add your API keys:
|
||||
```bash
|
||||
GROQ_API_KEY=XXX
|
||||
@ -88,23 +95,26 @@ Interested in maintaining and growing the project? Fill out our [Contributor App
|
||||
OPENAI_API_KEY=XXX
|
||||
...
|
||||
```
|
||||
3. Optionally set:
|
||||
- Debug level: `VITE_LOG_LEVEL=debug`
|
||||
- Context size: `DEFAULT_NUM_CTX=32768`
|
||||
3. Optionally set:
|
||||
- Debug level: `VITE_LOG_LEVEL=debug`
|
||||
- Context size: `DEFAULT_NUM_CTX=32768`
|
||||
|
||||
**Note**: Never commit your `.env.local` file to version control. It’s already in `.gitignore`.
|
||||
|
||||
### 2️⃣ Run Development Server
|
||||
### 2️⃣ Run Development Server
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
**Tip**: Use **Google Chrome Canary** for local testing.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite with:
|
||||
Run the test suite with:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
@ -113,10 +123,12 @@ pnpm test
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Deploy to Cloudflare Pages
|
||||
### Deploy to Cloudflare Pages
|
||||
|
||||
```bash
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
Ensure you have required permissions and that Wrangler is configured.
|
||||
|
||||
---
|
||||
@ -127,67 +139,76 @@ This section outlines the methods for deploying the application using Docker. Th
|
||||
|
||||
---
|
||||
|
||||
### 🧑💻 Development Environment
|
||||
### 🧑💻 Development Environment
|
||||
|
||||
#### Build Options
|
||||
#### Build Options
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
```bash
|
||||
# Development build
|
||||
npm run dockerbuild
|
||||
```
|
||||
|
||||
**Option 2: Direct Docker Build Command**
|
||||
**Option 2: Direct Docker Build Command**
|
||||
|
||||
```bash
|
||||
docker build . --target bolt-ai-development
|
||||
```
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
**Option 3: Docker Compose Profile**
|
||||
|
||||
```bash
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
#### Running the Development Container
|
||||
#### Running the Development Container
|
||||
|
||||
```bash
|
||||
docker run -p 5173:5173 --env-file .env.local bolt-ai:development
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏭 Production Environment
|
||||
### 🏭 Production Environment
|
||||
|
||||
#### Build Options
|
||||
#### Build Options
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
|
||||
**Option 1: Helper Scripts**
|
||||
```bash
|
||||
# Production build
|
||||
npm run dockerbuild:prod
|
||||
```
|
||||
|
||||
**Option 2: Direct Docker Build Command**
|
||||
**Option 2: Direct Docker Build Command**
|
||||
|
||||
```bash
|
||||
docker build . --target bolt-ai-production
|
||||
```
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
**Option 3: Docker Compose Profile**
|
||||
|
||||
```bash
|
||||
docker compose --profile production up
|
||||
```
|
||||
|
||||
#### Running the Production Container
|
||||
#### Running the Production Container
|
||||
|
||||
```bash
|
||||
docker run -p 5173:5173 --env-file .env.local bolt-ai:production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Coolify Deployment
|
||||
### Coolify Deployment
|
||||
|
||||
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
|
||||
1. Import your Git repository into Coolify.
|
||||
2. Choose **Docker Compose** as the build pack.
|
||||
3. Configure environment variables (e.g., API keys).
|
||||
4. Set the start command:
|
||||
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
|
||||
|
||||
1. Import your Git repository into Coolify.
|
||||
2. Choose **Docker Compose** as the build pack.
|
||||
3. Configure environment variables (e.g., API keys).
|
||||
4. Set the start command:
|
||||
```bash
|
||||
docker compose --profile production up
|
||||
```
|
||||
@ -200,20 +221,22 @@ The `docker-compose.yaml` configuration is compatible with **VS Code Dev Contain
|
||||
|
||||
### Steps to Use Dev Containers
|
||||
|
||||
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
|
||||
2. Select **Dev Containers: Reopen in Container**.
|
||||
3. Choose the **development** profile when prompted.
|
||||
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
|
||||
2. Select **Dev Containers: Reopen in Container**.
|
||||
3. Choose the **development** profile when prompted.
|
||||
4. VS Code will rebuild the container and open it with the pre-configured environment.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Environment Variables
|
||||
|
||||
Ensure `.env.local` is configured correctly with:
|
||||
- API keys.
|
||||
- Context-specific configurations.
|
||||
Ensure `.env.local` is configured correctly with:
|
||||
|
||||
- API keys.
|
||||
- Context-specific configurations.
|
||||
|
||||
Example for the `DEFAULT_NUM_CTX` variable:
|
||||
|
||||
Example for the `DEFAULT_NUM_CTX` variable:
|
||||
```bash
|
||||
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
|
||||
```
|
||||
```
|
||||
|
@ -3,7 +3,7 @@
|
||||
## Models and Setup
|
||||
|
||||
??? question "What are the best models for bolt.diy?"
|
||||
For the best experience with bolt.diy, we recommend using the following models:
|
||||
For the best experience with bolt.diy, we recommend using the following models:
|
||||
|
||||
- **Claude 3.5 Sonnet (old)**: Best overall coder, providing excellent results across all use cases
|
||||
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
|
||||
@ -17,79 +17,78 @@
|
||||
|
||||
## Best Practices
|
||||
|
||||
??? question "How do I get the best results with bolt.diy?"
|
||||
- **Be specific about your stack**:
|
||||
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
|
||||
??? question "How do I get the best results with bolt.diy?" - **Be specific about your stack**:
|
||||
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
|
||||
|
||||
- **Use the enhance prompt icon**:
|
||||
- **Use the enhance prompt icon**:
|
||||
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
||||
|
||||
- **Scaffold the basics first, then add features**:
|
||||
- **Scaffold the basics first, then add features**:
|
||||
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
|
||||
|
||||
- **Batch simple instructions**:
|
||||
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
|
||||
- **Batch simple instructions**:
|
||||
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
|
||||
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
|
||||
|
||||
## Project Information
|
||||
|
||||
??? question "How do I contribute to bolt.diy?"
|
||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||
|
||||
??? question "What are the future plans for bolt.diy?"
|
||||
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
|
||||
New features and improvements are on the way!
|
||||
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
|
||||
New features and improvements are on the way!
|
||||
|
||||
??? question "Why are there so many open issues/pull requests?"
|
||||
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
|
||||
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
|
||||
|
||||
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
|
||||
|
||||
## Model Comparisons
|
||||
|
||||
??? question "How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?"
|
||||
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
|
||||
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
??? error "There was an error processing this request"
|
||||
This generic error message means something went wrong. Check both:
|
||||
This generic error message means something went wrong. Check both:
|
||||
|
||||
- The terminal (if you started the app with Docker or `pnpm`).
|
||||
|
||||
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
|
||||
|
||||
??? error "x-api-key header missing"
|
||||
This error is sometimes resolved by restarting the Docker container.
|
||||
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
|
||||
This error is sometimes resolved by restarting the Docker container.
|
||||
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
|
||||
|
||||
??? error "Blank preview when running the app"
|
||||
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
||||
To troubleshoot:
|
||||
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
||||
To troubleshoot:
|
||||
|
||||
- Check the developer console for errors.
|
||||
|
||||
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
|
||||
|
||||
??? error "Everything works, but the results are bad"
|
||||
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like
|
||||
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like
|
||||
|
||||
- GPT-4o
|
||||
- GPT-4o
|
||||
- Claude 3.5 Sonnet
|
||||
- DeepSeek Coder V2 236b
|
||||
|
||||
??? error "Received structured exception #0xc0000005: access violation"
|
||||
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
|
||||
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
|
||||
|
||||
??? error "Miniflare or Wrangler errors in Windows"
|
||||
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here <a href="https://github.com/stackblitz-labs/bolt.diy/issues/19">Github Issues</a>
|
||||
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here <a href="https://github.com/stackblitz-labs/bolt.diy/issues/19">Github Issues</a>
|
||||
|
||||
---
|
||||
|
||||
## Get Help & Support
|
||||
|
||||
!!! tip "Community Support"
|
||||
[Join the bolt.diy Community](https://thinktank.ottomator.ai/c/bolt-diy/17){target=_blank} for discussions and help
|
||||
[Join the bolt.diy Community](https://thinktank.ottomator.ai/c/bolt-diy/17){target=\_blank} for discussions and help
|
||||
|
||||
!!! bug "Report Issues"
|
||||
[Open an Issue](https://github.com/stackblitz-labs/bolt.diy/issues/19){target=_blank} in our GitHub Repository
|
||||
[Open an Issue](https://github.com/stackblitz-labs/bolt.diy/issues/19){target=\_blank} in our GitHub Repository
|
||||
|
@ -1,7 +1,9 @@
|
||||
# Welcome to bolt diy
|
||||
|
||||
bolt.diy 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, or Groq models - 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.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Join the community!](#join-the-community)
|
||||
- [Features](#features)
|
||||
- [Setup](#setup)
|
||||
@ -41,31 +43,31 @@ Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
## 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.
|
||||
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.
|
||||
|
||||
### Prerequisites
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Git**: [Download Git](https://git-scm.com/downloads)
|
||||
2. **Install Node.js**: [Download Node.js](https://nodejs.org/en/download/)
|
||||
1. **Install Git**: [Download Git](https://git-scm.com/downloads)
|
||||
2. **Install Node.js**: [Download Node.js](https://nodejs.org/en/download/)
|
||||
|
||||
- After installation, the Node.js path is usually added to your system automatically. To verify:
|
||||
- **Windows**: Search for "Edit the system environment variables," click "Environment Variables," and check if `Node.js` is in the `Path` variable.
|
||||
- **Mac/Linux**: Open a terminal and run:
|
||||
```bash
|
||||
echo $PATH
|
||||
```
|
||||
Look for `/usr/local/bin` in the output.
|
||||
- After installation, the Node.js path is usually added to your system automatically. To verify:
|
||||
- **Windows**: Search for "Edit the system environment variables," click "Environment Variables," and check if `Node.js` is in the `Path` variable.
|
||||
- **Mac/Linux**: Open a terminal and run:
|
||||
```bash
|
||||
echo $PATH
|
||||
```
|
||||
Look for `/usr/local/bin` in the output.
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
Alternatively, you can download the latest version of the project directly from the [Releases Page](https://github.com/stackblitz-labs/bolt.diy/releases/latest). Simply download the .zip file, extract it, and proceed with the setup instructions below. If you are comfertiable using git then run the command below.
|
||||
|
||||
Clone the repository using Git:
|
||||
Clone the repository using Git:
|
||||
|
||||
```bash
|
||||
git clone -b stable https://github.com/stackblitz-labs/bolt.diy
|
||||
```bash
|
||||
git clone -b stable https://github.com/stackblitz-labs/bolt.diy
|
||||
```
|
||||
|
||||
---
|
||||
@ -76,7 +78,7 @@ There are two ways to configure your API keys in bolt.diy:
|
||||
|
||||
#### 1. Set API Keys in the `.env.local` File
|
||||
|
||||
When setting up the application, you will need to add your API keys for the LLMs you wish to use. You can do this by renaming the `.env.example` file to `.env.local` and adding your API keys there.
|
||||
When setting up the application, you will need to add your API keys for the LLMs you wish to use. You can do this by renaming the `.env.example` file to `.env.local` and adding your API keys there.
|
||||
|
||||
- On **Mac**, you can find the file at `[your name]/bolt.diy/.env.example`.
|
||||
- On **Windows/Linux**, the path will be similar.
|
||||
@ -112,54 +114,60 @@ This method allows you to easily add or update your keys without needing to modi
|
||||
|
||||
Once you've configured your keys, the application will be ready to use the selected LLMs.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Run the Application
|
||||
## Run the Application
|
||||
|
||||
### Option 1: Without Docker
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
If `pnpm` is not installed, install it using:
|
||||
```bash
|
||||
sudo npm install -g pnpm
|
||||
```
|
||||
1. **Install Dependencies**:
|
||||
|
||||
2. **Start the Application**:
|
||||
```bash
|
||||
pnpm run dev
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
|
||||
|
||||
### Option 2: With Docker
|
||||
If `pnpm` is not installed, install it using:
|
||||
|
||||
#### Prerequisites
|
||||
- Ensure Git, Node.js, and Docker are installed: [Download Docker](https://www.docker.com/)
|
||||
```bash
|
||||
sudo npm install -g pnpm
|
||||
```
|
||||
|
||||
#### Steps
|
||||
2. **Start the Application**:
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
|
||||
|
||||
1. **Build the Docker Image**:
|
||||
### Option 2: With Docker
|
||||
|
||||
Use the provided NPM scripts:
|
||||
```bash
|
||||
npm run dockerbuild
|
||||
```
|
||||
#### Prerequisites
|
||||
|
||||
Alternatively, use Docker commands directly:
|
||||
```bash
|
||||
- Ensure Git, Node.js, and Docker are installed: [Download Docker](https://www.docker.com/)
|
||||
|
||||
#### Steps
|
||||
|
||||
1. **Build the Docker Image**:
|
||||
|
||||
Use the provided NPM scripts:
|
||||
|
||||
```bash
|
||||
npm run dockerbuild
|
||||
```
|
||||
|
||||
Alternatively, use Docker commands directly:
|
||||
|
||||
```bash
|
||||
docker build . --target bolt-ai-development
|
||||
```
|
||||
```
|
||||
|
||||
2. **Run the Container**:
|
||||
Use Docker Compose profiles to manage environments:
|
||||
```bash
|
||||
docker compose --profile development up
|
||||
```
|
||||
Use Docker Compose profiles to manage environments:
|
||||
|
||||
- With the development profile, changes to your code will automatically reflect in the running container (hot reloading).
|
||||
```bash
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
- With the development profile, changes to your code will automatically reflect in the running container (hot reloading).
|
||||
|
||||
---
|
||||
|
||||
@ -167,42 +175,46 @@ Once you've configured your keys, the application will be ready to use the selec
|
||||
|
||||
To keep your local version of bolt.diy up to date with the latest changes, follow these steps for your operating system:
|
||||
|
||||
#### 1. **Navigate to your project folder**
|
||||
Navigate to the directory where you cloned the repository and open a terminal:
|
||||
#### 1. **Navigate to your project folder**
|
||||
|
||||
#### 2. **Fetch the Latest Changes**
|
||||
Use Git to pull the latest changes from the main repository:
|
||||
Navigate to the directory where you cloned the repository and open a terminal:
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
```
|
||||
#### 2. **Fetch the Latest Changes**
|
||||
|
||||
#### 3. **Update Dependencies**
|
||||
After pulling the latest changes, update the project dependencies by running the following command:
|
||||
Use Git to pull the latest changes from the main repository:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
```bash
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
#### 4. **Rebuild and Start the Application**
|
||||
#### 3. **Update Dependencies**
|
||||
|
||||
- **If using Docker**, ensure you rebuild the Docker image to avoid using a cached version:
|
||||
```bash
|
||||
docker compose --profile development up --build
|
||||
```
|
||||
After pulling the latest changes, update the project dependencies by running the following command:
|
||||
|
||||
- **If not using Docker**, you can start the application as usual with:
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes.
|
||||
#### 4. **Rebuild and Start the Application**
|
||||
|
||||
- **If using Docker**, ensure you rebuild the Docker image to avoid using a cached version:
|
||||
|
||||
```bash
|
||||
docker compose --profile development up --build
|
||||
```
|
||||
|
||||
- **If not using Docker**, you can start the application as usual with:
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes.
|
||||
|
||||
---
|
||||
|
||||
## Adding New LLMs:
|
||||
|
||||
To make new LLMs available to use in this version of bolt.diy, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider.
|
||||
To make new LLMs available to use in this version of bolt.diy, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider.
|
||||
|
||||
By default, Anthropic, OpenAI, Groq, and Ollama are implemented as providers, but the YouTube video for this repo covers how to extend this to work with more providers if you wish!
|
||||
|
||||
|
@ -33,7 +33,7 @@ theme:
|
||||
# favicon: assets/logo.png
|
||||
repo_name: bolt.diy
|
||||
repo_url: https://github.com/stackblitz-labs/bolt.diy
|
||||
edit_uri: ""
|
||||
edit_uri: ''
|
||||
|
||||
extra:
|
||||
generator: false
|
||||
@ -51,9 +51,6 @@ extra:
|
||||
link: https://bsky.app/profile/bolt.diy
|
||||
name: bolt.diy on Bluesky
|
||||
|
||||
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
@ -73,4 +70,4 @@ markdown_extensions:
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- toc:
|
||||
permalink: true
|
||||
permalink: true
|
||||
|
@ -4,13 +4,7 @@ import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/dist',
|
||||
'**/node_modules',
|
||||
'**/.wrangler',
|
||||
'**/bolt/build',
|
||||
'**/.history',
|
||||
],
|
||||
ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'],
|
||||
},
|
||||
...blitzPlugin.configs.recommended(),
|
||||
{
|
||||
@ -20,15 +14,15 @@ export default [
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@blitz/comment-syntax': 'off',
|
||||
'@blitz/block-scope-case': 'off',
|
||||
'array-bracket-spacing': ["error", "never"],
|
||||
'object-curly-newline': ["error", { "consistent": true }],
|
||||
'keyword-spacing': ["error", { "before": true, "after": true }],
|
||||
'consistent-return': "error",
|
||||
'semi': ["error", "always"],
|
||||
'curly': ["error"],
|
||||
'no-eval': ["error"],
|
||||
'linebreak-style': ["error", "unix"],
|
||||
'arrow-spacing': ["error", { "before": true, "after": true }]
|
||||
'array-bracket-spacing': ['error', 'never'],
|
||||
'object-curly-newline': ['error', { consistent: true }],
|
||||
'keyword-spacing': ['error', { before: true, after: true }],
|
||||
'consistent-return': 'error',
|
||||
semi: ['error', 'always'],
|
||||
curly: ['error'],
|
||||
'no-eval': ['error'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'arrow-spacing': ['error', { before: true, after: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -53,7 +47,7 @@ export default [
|
||||
patterns: [
|
||||
{
|
||||
group: ['../'],
|
||||
message: 'Relative imports are not allowed. Please use \'~/\' instead.',
|
||||
message: "Relative imports are not allowed. Please use '~/' instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -84,8 +84,8 @@
|
||||
"ai": "^4.1.2",
|
||||
"chalk": "^5.4.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"diff": "^5.2.0",
|
||||
"dotenv": "^16.4.7",
|
||||
@ -117,7 +117,7 @@
|
||||
"remix-island": "^0.2.0",
|
||||
"remix-utils": "^7.7.0",
|
||||
"shiki": "^1.24.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
4769
pnpm-lock.yaml
4769
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
const { execSync } =require('child_process');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Get git hash with fallback
|
||||
const getGitHash = () => {
|
||||
|
@ -1,7 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01", "@types/dom-speech-recognition"],
|
||||
"types": [
|
||||
"@remix-run/cloudflare",
|
||||
"vite/client",
|
||||
"@cloudflare/workers-types/2023-07-01",
|
||||
"@types/dom-speech-recognition"
|
||||
],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
|
@ -18,9 +18,6 @@ const getGitHash = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export default defineConfig((config) => {
|
||||
return {
|
||||
define: {
|
||||
@ -41,7 +38,7 @@ export default defineConfig((config) => {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_lazyRouteDiscovery: true
|
||||
v3_lazyRouteDiscovery: true,
|
||||
},
|
||||
}),
|
||||
UnoCSS(),
|
||||
@ -49,7 +46,13 @@ export default defineConfig((config) => {
|
||||
chrome129IssuePlugin(),
|
||||
config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
|
||||
],
|
||||
envPrefix: ["VITE_","OPENAI_LIKE_API_BASE_URL", "OLLAMA_API_BASE_URL", "LMSTUDIO_API_BASE_URL","TOGETHER_API_BASE_URL"],
|
||||
envPrefix: [
|
||||
'VITE_',
|
||||
'OPENAI_LIKE_API_BASE_URL',
|
||||
'OLLAMA_API_BASE_URL',
|
||||
'LMSTUDIO_API_BASE_URL',
|
||||
'TOGETHER_API_BASE_URL',
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
Loading…
Reference in New Issue
Block a user