mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
chore: release version 0.0.6
This commit is contained in:
11
.env.example
11
.env.example
@@ -83,6 +83,17 @@ XAI_API_KEY=
|
||||
# You only need this environment variable set if you want to use Perplexity models
|
||||
PERPLEXITY_API_KEY=
|
||||
|
||||
# Get your AWS configuration
|
||||
# https://console.aws.amazon.com/iam/home
|
||||
# The JSON should include the following keys:
|
||||
# - region: The AWS region where Bedrock is available.
|
||||
# - accessKeyId: Your AWS access key ID.
|
||||
# - secretAccessKey: Your AWS secret access key.
|
||||
# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials.
|
||||
# Example JSON:
|
||||
# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"}
|
||||
AWS_BEDROCK_CONFIG=
|
||||
|
||||
# Include this environment variable if you want more logging for debugging locally
|
||||
VITE_LOG_LEVEL=debug
|
||||
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -6,8 +6,8 @@ body:
|
||||
value: |
|
||||
Thank you for reporting an issue :pray:.
|
||||
|
||||
This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new).
|
||||
If you experience issues related to WebContainer, please file an issue in our [WebContainer repo](https://github.com/stackblitz/webcontainer-core), or file an issue in our [StackBlitz core repo](https://github.com/stackblitz/core) for issues with StackBlitz.
|
||||
This issue tracker is for bugs and issues found with [Bolt.diy](https://bolt.diy).
|
||||
If you experience issues related to WebContainer, please file an issue in the official [StackBlitz WebContainer repo](https://github.com/stackblitz/webcontainer-core).
|
||||
|
||||
The more information you fill in, the better we can help you.
|
||||
- type: textarea
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/epic.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/epic.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Epic
|
||||
about: Epics define long-term vision and capabilities of the software. They will never be finished but serve as umbrella for features.
|
||||
title: ''
|
||||
labels:
|
||||
- epic
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Strategic Impact
|
||||
|
||||
<!-- Why does this area matter? How is it integrated into the product or the development process? What would happen if we ignore it? -->
|
||||
|
||||
# Target Audience
|
||||
|
||||
<!-- Who benefits most from improvements in this area?
|
||||
|
||||
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.
|
||||
Once features are actually being planned / described in detail, they can be linked here. -->
|
||||
28
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Feature
|
||||
about: A pretty vague description of how a capability of our software can be added or improved.
|
||||
title: ''
|
||||
labels:
|
||||
- feature
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Motivation
|
||||
|
||||
<!-- What capability should be either established or improved? How is life of the target audience better after it's been done? -->
|
||||
|
||||
# Scope
|
||||
|
||||
<!-- 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.
|
||||
This allows potential other contributors to join forces and provide meaningful feedback prio to even starting work on it.
|
||||
-->
|
||||
|
||||
# Related
|
||||
|
||||
<!-- Link to the epic or other issues or PRs which are related to this feature. -->
|
||||
81
.github/workflows/docker.yaml
vendored
Normal file
81
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: Docker Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
DOCKER_IMAGE: ghcr.io/${{ github.repository }}
|
||||
BUILD_TARGET: bolt-ai-production # bolt-ai-development
|
||||
|
||||
jobs:
|
||||
docker-build-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- id: string
|
||||
uses: ASzc/change-string-case-action@v6
|
||||
with:
|
||||
string: ${{ env.DOCKER_IMAGE }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v5
|
||||
with:
|
||||
images: ${{ steps.string.outputs.lowercase }}
|
||||
flavor: |
|
||||
latest=true
|
||||
prefix=
|
||||
suffix=
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=pep440,pattern={{version}}
|
||||
type=ref,event=tag
|
||||
type=raw,value={{sha}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }} # ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }} # ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: ${{ env.BUILD_TARGET }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ steps.string.outputs.lowercase }}:latest
|
||||
cache-to: type=inline
|
||||
|
||||
- name: Check manifest
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ steps.string.outputs.lowercase }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Dump context
|
||||
if: always()
|
||||
uses: crazy-max/ghaction-dump-context@v2
|
||||
@@ -144,7 +144,7 @@ docker build . --target bolt-ai-development
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
```bash
|
||||
docker-compose --profile development up
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
#### Running the Development Container
|
||||
@@ -171,7 +171,7 @@ docker build . --target bolt-ai-production
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
```bash
|
||||
docker-compose --profile production up
|
||||
docker compose --profile production up
|
||||
```
|
||||
|
||||
#### Running the Production Container
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -25,8 +25,10 @@ ARG ANTHROPIC_API_KEY
|
||||
ARG OPEN_ROUTER_API_KEY
|
||||
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
||||
ARG OLLAMA_API_BASE_URL
|
||||
ARG XAI_API_KEY
|
||||
ARG TOGETHER_API_KEY
|
||||
ARG TOGETHER_API_BASE_URL
|
||||
ARG AWS_BEDROCK_CONFIG
|
||||
ARG VITE_LOG_LEVEL=debug
|
||||
ARG DEFAULT_NUM_CTX
|
||||
|
||||
@@ -38,16 +40,19 @@ ENV WRANGLER_SEND_METRICS=false \
|
||||
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
||||
XAI_API_KEY=${XAI_API_KEY} \
|
||||
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
||||
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
||||
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
|
||||
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
|
||||
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
|
||||
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
|
||||
RUNNING_IN_DOCKER=true
|
||||
|
||||
# Pre-configure wrangler to disable metrics
|
||||
RUN mkdir -p /root/.config/.wrangler && \
|
||||
echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
|
||||
|
||||
RUN npm run build
|
||||
RUN pnpm run build
|
||||
|
||||
CMD [ "pnpm", "run", "dockerstart"]
|
||||
|
||||
@@ -62,6 +67,7 @@ ARG ANTHROPIC_API_KEY
|
||||
ARG OPEN_ROUTER_API_KEY
|
||||
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
||||
ARG OLLAMA_API_BASE_URL
|
||||
ARG XAI_API_KEY
|
||||
ARG TOGETHER_API_KEY
|
||||
ARG TOGETHER_API_BASE_URL
|
||||
ARG VITE_LOG_LEVEL=debug
|
||||
@@ -74,10 +80,13 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \
|
||||
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
||||
XAI_API_KEY=${XAI_API_KEY} \
|
||||
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
||||
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
||||
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
|
||||
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
|
||||
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
|
||||
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
|
||||
RUNNING_IN_DOCKER=true
|
||||
|
||||
RUN mkdir -p ${WORKDIR}/run
|
||||
CMD pnpm run dev --host
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 StackBlitz, Inc.
|
||||
Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
57
PROJECT.md
Normal file
57
PROJECT.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Project management of bolt.diy
|
||||
|
||||
First off: this sounds funny, we know. "Project management" comes from a world of enterprise stuff and this project is
|
||||
far from being enterprisy- it's still anarchy all over the place 😉
|
||||
|
||||
But we need to organize ourselves somehow, right?
|
||||
|
||||
> tl;dr: We've got a project board with epics and features. We use PRs as change log and as materialized features. Find it [here](https://github.com/orgs/stackblitz-labs/projects/4).
|
||||
|
||||
Here's how we structure long-term vision, mid-term capabilities of the software and short term improvements.
|
||||
|
||||
## Strategic epics (long-term)
|
||||
|
||||
Strategic epics define areas in which the product evolves. Usually, these epics don’t overlap. They shall allow the core
|
||||
team to define what they believe is most important and should be worked on with the highest priority.
|
||||
|
||||
You can find the [epics as issues](https://github.com/stackblitz-labs/bolt.diy/labels/epic) which are probably never
|
||||
going to be closed.
|
||||
|
||||
What's the benefit / purpose of epics?
|
||||
|
||||
1. Prioritization
|
||||
|
||||
E. g. we could say “managing files is currently more important that quality”. Then, we could thing about which features
|
||||
would bring “managing files” forward. It may be different features, such as “upload local files”, “import from a repo”
|
||||
or also undo/redo/commit.
|
||||
|
||||
In a more-or-less regular meeting dedicated for that, the core team discusses which epics matter most, sketch features
|
||||
and then check who can work on them. After the meeting, they update the roadmap (at least for the next development turn)
|
||||
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.
|
||||
|
||||
## Features (mid-term)
|
||||
|
||||
We all know probably a dozen of methodologies following which features are being described (User story, business
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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 🤓
|
||||
|
||||
## PRs as change log
|
||||
|
||||
Once a PR is merged, a squashed commit contains the whole PR description which allows for a good change log.
|
||||
All authors of commits in the PR are mentioned in the squashed commit message and become contributors 🙌
|
||||
56
README.md
56
README.md
@@ -1,9 +1,14 @@
|
||||
# bolt.diy (Previously oTToDev)
|
||||
|
||||
[](https://bolt.diy)
|
||||
|
||||
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 information.
|
||||
-----
|
||||
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/).
|
||||
|
||||
@@ -23,8 +28,15 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
|
||||
## Join the community
|
||||
|
||||
[Join the bolt.diy community here, in the thinktank on ottomator.ai!](https://thinktank.ottomator.ai)
|
||||
[Join the bolt.diy community here, in the oTTomator Think Tank!](https://thinktank.ottomator.ai)
|
||||
|
||||
## Project management
|
||||
|
||||
Bolt.diy is a community effort! Still, the core team of contributors aims at organizing the project in way that allows
|
||||
you to understand where the current areas of focus are.
|
||||
|
||||
If you want to know what we are working on, what we are planning to work on, or if you want to contribute to the
|
||||
project, please check the [project management guide](./PROJECT.md) to get started easily.
|
||||
|
||||
## Requested Additions
|
||||
|
||||
@@ -47,6 +59,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
- ✅ Bolt terminal to see the output of LLM run commands (@thecodacus)
|
||||
- ✅ Streaming of code output (@thecodacus)
|
||||
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
||||
- ✅ Chat history backup and restore functionality (@sidbetatester)
|
||||
- ✅ Cohere Integration (@hasanraiyan)
|
||||
- ✅ Dynamic model max token length (@hasanraiyan)
|
||||
- ✅ Better prompt enhancing (@SujalXplores)
|
||||
@@ -55,7 +68,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
- ✅ Together Integration (@mouimet-infinisoft)
|
||||
- ✅ Mobile friendly (@qwikode)
|
||||
- ✅ Better prompt enhancing (@SujalXplores)
|
||||
- ✅ Attach images to prompts (@atrokhym)
|
||||
- ✅ Attach images to prompts (@atrokhym)(@stijnus)
|
||||
- ✅ Added Git Clone button (@thecodacus)
|
||||
- ✅ Git Import from url (@thecodacus)
|
||||
- ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
|
||||
@@ -64,6 +77,8 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
- ✅ Detect terminal Errors and ask bolt to fix it (@thecodacus)
|
||||
- ✅ Detect preview Errors and ask bolt to fix it (@wonderwhy-er)
|
||||
- ✅ Add Starter Template Options (@thecodacus)
|
||||
- ✅ Perplexity Integration (@meetpateltech)
|
||||
- ✅ AWS Bedrock Integration (@kunjabijukchhe)
|
||||
- ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
|
||||
- ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
|
||||
- ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
|
||||
@@ -73,12 +88,14 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
|
||||
- ⬜ Voice prompting
|
||||
- ⬜ Azure Open AI API Integration
|
||||
- ✅ Perplexity Integration (@meetpateltech)
|
||||
- ⬜ Vertex AI Integration
|
||||
- ⬜ Granite Integration
|
||||
- ✅ Popout Window for Web Container(@stijnus)
|
||||
- ✅ Ability to change Popout window size (@stijnus)
|
||||
|
||||
## Features
|
||||
|
||||
- **AI-powered full-stack web development** directly in your browser.
|
||||
- **AI-powered full-stack web development** for **NodeJS based applications** directly in your browser.
|
||||
- **Support for multiple LLMs** with an extensible architecture to integrate additional models.
|
||||
- **Attach images to prompts** for better contextual understanding.
|
||||
- **Integrated terminal** to view output of LLM-run commands.
|
||||
@@ -86,21 +103,18 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
||||
- **Download projects as ZIP** for easy portability.
|
||||
- **Integration-ready Docker support** for a hassle-free setup.
|
||||
|
||||
## 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.
|
||||
|
||||
Let's get you up and running with the stable version of Bolt.DIY!
|
||||
|
||||
## Quick Download
|
||||
|
||||
[](https://github.com/stackblitz-labs/bolt.diy/releases/latest) ← Click here to go the the latest release version!
|
||||
[](https://github.com/stackblitz-labs/bolt.diy/releases/latest) ← Click here to go the the latest release version!
|
||||
|
||||
- Next **click source.zip**
|
||||
|
||||
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, you'll need to install two important pieces of software:
|
||||
@@ -133,16 +147,19 @@ You have two options for running Bolt.DIY: directly on your machine or using Doc
|
||||
### Option 1: Direct Installation (Recommended for Beginners)
|
||||
|
||||
1. **Install Package Manager (pnpm)**:
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
2. **Install Project Dependencies**:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Start the Application**:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
@@ -154,11 +171,13 @@ You have two options for running Bolt.DIY: directly on your machine or using Doc
|
||||
This option requires some familiarity with Docker but provides a more isolated environment.
|
||||
|
||||
#### Additional Prerequisite
|
||||
|
||||
- Install Docker: [Download Docker](https://www.docker.com/)
|
||||
|
||||
#### Steps:
|
||||
|
||||
1. **Build the Docker Image**:
|
||||
|
||||
```bash
|
||||
# Using npm script:
|
||||
npm run dockerbuild
|
||||
@@ -169,12 +188,9 @@ This option requires some familiarity with Docker but provides a more isolated e
|
||||
|
||||
2. **Run the Container**:
|
||||
```bash
|
||||
docker-compose --profile development up
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Configuring API Keys and Providers
|
||||
|
||||
### Adding Your API Keys
|
||||
@@ -203,6 +219,7 @@ For providers that support custom base URLs (such as Ollama or LM Studio), follo
|
||||
> **Note**: Custom base URLs are particularly useful when running local instances of AI models or using custom API endpoints.
|
||||
|
||||
### Supported Providers
|
||||
|
||||
- Ollama
|
||||
- LM Studio
|
||||
- OpenAILike
|
||||
@@ -210,23 +227,27 @@ For providers that support custom base URLs (such as Ollama or LM Studio), follo
|
||||
## Setup Using Git (For Developers only)
|
||||
|
||||
This method is recommended for developers who want to:
|
||||
|
||||
- Contribute to the project
|
||||
- Stay updated with the latest changes
|
||||
- Switch between different versions
|
||||
- Create custom modifications
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. Install Git: [Download Git](https://git-scm.com/downloads)
|
||||
|
||||
#### Initial Setup
|
||||
|
||||
1. **Clone the Repository**:
|
||||
|
||||
```bash
|
||||
# Using HTTPS
|
||||
git clone https://github.com/stackblitz-labs/bolt.diy.git
|
||||
```
|
||||
|
||||
2. **Navigate to Project Directory**:
|
||||
|
||||
```bash
|
||||
cd bolt.diy
|
||||
```
|
||||
@@ -236,6 +257,7 @@ This method is recommended for developers who want to:
|
||||
git checkout main
|
||||
```
|
||||
4. **Install Dependencies**:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
@@ -250,16 +272,19 @@ This method is recommended for developers who want to:
|
||||
To get the latest changes from the repository:
|
||||
|
||||
1. **Save Your Local Changes** (if any):
|
||||
|
||||
```bash
|
||||
git stash
|
||||
```
|
||||
|
||||
2. **Pull Latest Updates**:
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
3. **Update Dependencies**:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
@@ -274,6 +299,7 @@ To get the latest changes from the repository:
|
||||
If you encounter issues:
|
||||
|
||||
1. **Clean Installation**:
|
||||
|
||||
```bash
|
||||
# Remove node modules and lock files
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -11,11 +11,14 @@ interface APIKeyManagerProps {
|
||||
labelForGetApiKey?: string;
|
||||
}
|
||||
|
||||
// cache which stores whether the provider's API key is set via environment variable
|
||||
const providerEnvKeyStatusCache: Record<string, boolean> = {};
|
||||
|
||||
const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
|
||||
|
||||
export function getApiKeysFromCookies() {
|
||||
const storedApiKeys = Cookies.get('apiKeys');
|
||||
let parsedKeys = {};
|
||||
let parsedKeys: Record<string, string> = {};
|
||||
|
||||
if (storedApiKeys) {
|
||||
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
|
||||
@@ -32,54 +35,135 @@ export function getApiKeysFromCookies() {
|
||||
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [tempKey, setTempKey] = useState(apiKey);
|
||||
const [isEnvKeySet, setIsEnvKeySet] = useState(false);
|
||||
|
||||
// Reset states and load saved key when provider changes
|
||||
useEffect(() => {
|
||||
// Load saved API key from cookies for this provider
|
||||
const savedKeys = getApiKeysFromCookies();
|
||||
const savedKey = savedKeys[provider.name] || '';
|
||||
|
||||
setTempKey(savedKey);
|
||||
setApiKey(savedKey);
|
||||
setIsEditing(false);
|
||||
}, [provider.name]);
|
||||
|
||||
const checkEnvApiKey = useCallback(async () => {
|
||||
// Check cache first
|
||||
if (providerEnvKeyStatusCache[provider.name] !== undefined) {
|
||||
setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`);
|
||||
const data = await response.json();
|
||||
const isSet = (data as { isSet: boolean }).isSet;
|
||||
|
||||
// Cache the result
|
||||
providerEnvKeyStatusCache[provider.name] = isSet;
|
||||
setIsEnvKeySet(isSet);
|
||||
} catch (error) {
|
||||
console.error('Failed to check environment API key:', error);
|
||||
setIsEnvKeySet(false);
|
||||
}
|
||||
}, [provider.name]);
|
||||
|
||||
useEffect(() => {
|
||||
checkEnvApiKey();
|
||||
}, [checkEnvApiKey]);
|
||||
|
||||
const handleSave = () => {
|
||||
// Save to parent state
|
||||
setApiKey(tempKey);
|
||||
|
||||
// Save to cookies
|
||||
const currentKeys = getApiKeysFromCookies();
|
||||
const newKeys = { ...currentKeys, [provider.name]: tempKey };
|
||||
Cookies.set('apiKeys', JSON.stringify(newKeys));
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
|
||||
<div>
|
||||
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
||||
{!isEditing && (
|
||||
<div className="flex items-center mb-4">
|
||||
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
|
||||
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
|
||||
</span>
|
||||
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
|
||||
<div className="i-ph:pencil-simple" />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between py-3 px-1">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
||||
{!isEditing && (
|
||||
<div className="flex items-center gap-2">
|
||||
{apiKey ? (
|
||||
<>
|
||||
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
|
||||
<span className="text-xs text-green-500">Set via UI</span>
|
||||
</>
|
||||
) : isEnvKeySet ? (
|
||||
<>
|
||||
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
|
||||
<span className="text-xs text-green-500">Set via environment variable</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-ph:x-circle-fill text-red-500 w-4 h-4" />
|
||||
<span className="text-xs text-red-500">Not Set (Please set via UI or ENV_VAR)</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<input
|
||||
type="password"
|
||||
value={tempKey}
|
||||
placeholder="Your API Key"
|
||||
onChange={(e) => setTempKey(e.target.value)}
|
||||
className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
|
||||
/>
|
||||
<IconButton onClick={handleSave} title="Save API Key">
|
||||
<div className="i-ph:check" />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
||||
<div className="i-ph:x" />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{provider?.getApiKeyLink && (
|
||||
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
||||
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
||||
<div className={provider?.icon || 'i-ph:key'} />
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={tempKey}
|
||||
placeholder="Enter API Key"
|
||||
onChange={(e) => setTempKey(e.target.value)}
|
||||
className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor
|
||||
bg-bolt-elements-prompt-background text-bolt-elements-textPrimary
|
||||
focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={handleSave}
|
||||
title="Save API Key"
|
||||
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
|
||||
>
|
||||
<div className="i-ph:check w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => setIsEditing(false)}
|
||||
title="Cancel"
|
||||
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
|
||||
>
|
||||
<div className="i-ph:x w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
<IconButton
|
||||
onClick={() => setIsEditing(true)}
|
||||
title="Edit API Key"
|
||||
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
|
||||
>
|
||||
<div className="i-ph:pencil-simple w-4 h-4" />
|
||||
</IconButton>
|
||||
}
|
||||
{provider?.getApiKeyLink && !apiKey && (
|
||||
<IconButton
|
||||
onClick={() => window.open(provider?.getApiKeyLink)}
|
||||
title="Get API Key"
|
||||
className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2"
|
||||
>
|
||||
<span className="text-xs whitespace-nowrap">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
||||
<div className={`${provider?.icon || 'i-ph:key'} w-4 h-4`} />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { memo } from 'react';
|
||||
import { Markdown } from './Markdown';
|
||||
import type { JSONValue } from 'ai';
|
||||
import type { ProgressAnnotation } from '~/types/context';
|
||||
import Popover from '~/components/ui/Popover';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
@@ -10,7 +12,12 @@ interface AssistantMessageProps {
|
||||
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
|
||||
const filteredAnnotations = (annotations?.filter(
|
||||
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||
) || []) as { type: string; value: any }[];
|
||||
) || []) as { type: string; value: any } & { [key: string]: any }[];
|
||||
|
||||
let progressAnnotation: ProgressAnnotation[] = filteredAnnotations.filter(
|
||||
(annotation) => annotation.type === 'progress',
|
||||
) as ProgressAnnotation[];
|
||||
progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value);
|
||||
|
||||
const usage: {
|
||||
completionTokens: number;
|
||||
@@ -20,11 +27,18 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
{usage && (
|
||||
<div className="text-sm text-bolt-elements-textSecondary mb-2">
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
<>
|
||||
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
|
||||
{progressAnnotation.length > 0 && (
|
||||
<Popover trigger={<div className="i-ph:info" />}>{progressAnnotation[0].message}</Popover>
|
||||
)}
|
||||
{usage && (
|
||||
<div>
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<Markdown html>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* Preventing TS checks with files presented in the video for a better presentation.
|
||||
*/
|
||||
import type { Message } from 'ai';
|
||||
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
|
||||
import React, { type RefCallback, useEffect, useState } from 'react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
|
||||
import { PROVIDER_LIST } from '~/utils/constants';
|
||||
import { Messages } from './Messages.client';
|
||||
import { SendButton } from './SendButton.client';
|
||||
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
|
||||
@@ -25,13 +25,13 @@ import GitCloneButton from './GitCloneButton';
|
||||
import FilePreview from './FilePreview';
|
||||
import { ModelSelector } from '~/components/chat/ModelSelector';
|
||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import StarterTemplates from './StarterTemplates';
|
||||
import type { ActionAlert } from '~/types/actions';
|
||||
import ChatAlert from './ChatAlert';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
|
||||
const TEXTAREA_MIN_HEIGHT = 76;
|
||||
|
||||
@@ -102,35 +102,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
) => {
|
||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
|
||||
const [modelList, setModelList] = useState(MODEL_LIST);
|
||||
const [modelList, setModelList] = useState<ModelInfo[]>([]);
|
||||
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
|
||||
|
||||
const getProviderSettings = useCallback(() => {
|
||||
let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
|
||||
|
||||
try {
|
||||
const savedProviderSettings = Cookies.get('providers');
|
||||
|
||||
if (savedProviderSettings) {
|
||||
const parsedProviderSettings = JSON.parse(savedProviderSettings);
|
||||
|
||||
if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
|
||||
providerSettings = parsedProviderSettings;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading Provider Settings from cookies:', error);
|
||||
|
||||
// Clear invalid cookie data
|
||||
Cookies.remove('providers');
|
||||
}
|
||||
|
||||
return providerSettings;
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
console.log(transcript);
|
||||
}, [transcript]);
|
||||
@@ -169,7 +147,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const providerSettings = getProviderSettings();
|
||||
let parsedApiKeys: Record<string, string> | undefined = {};
|
||||
|
||||
try {
|
||||
@@ -177,53 +154,48 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
setApiKeys(parsedApiKeys);
|
||||
} catch (error) {
|
||||
console.error('Error loading API keys from cookies:', error);
|
||||
|
||||
// Clear invalid cookie data
|
||||
Cookies.remove('apiKeys');
|
||||
}
|
||||
|
||||
setIsModelLoading('all');
|
||||
initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
|
||||
.then((modelList) => {
|
||||
// console.log('Model List: ', modelList);
|
||||
setModelList(modelList);
|
||||
fetch('/api/models')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const typedData = data as { modelList: ModelInfo[] };
|
||||
setModelList(typedData.modelList);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error initializing model list:', error);
|
||||
console.error('Error fetching model list:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsModelLoading(undefined);
|
||||
});
|
||||
}
|
||||
}, [providerList]);
|
||||
}, [providerList, provider]);
|
||||
|
||||
const onApiKeysChange = async (providerName: string, apiKey: string) => {
|
||||
const newApiKeys = { ...apiKeys, [providerName]: apiKey };
|
||||
setApiKeys(newApiKeys);
|
||||
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
|
||||
|
||||
const provider = LLMManager.getInstance(import.meta.env || process.env || {}).getProvider(providerName);
|
||||
setIsModelLoading(providerName);
|
||||
|
||||
if (provider && provider.getDynamicModels) {
|
||||
setIsModelLoading(providerName);
|
||||
let providerModels: ModelInfo[] = [];
|
||||
|
||||
try {
|
||||
const providerSettings = getProviderSettings();
|
||||
const staticModels = provider.staticModels;
|
||||
const dynamicModels = await provider.getDynamicModels(
|
||||
newApiKeys,
|
||||
providerSettings,
|
||||
import.meta.env || process.env || {},
|
||||
);
|
||||
|
||||
setModelList((preModels) => {
|
||||
const filteredOutPreModels = preModels.filter((x) => x.provider !== providerName);
|
||||
return [...filteredOutPreModels, ...staticModels, ...dynamicModels];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading dynamic models:', error);
|
||||
}
|
||||
setIsModelLoading(undefined);
|
||||
try {
|
||||
const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);
|
||||
const data = await response.json();
|
||||
providerModels = (data as { modelList: ModelInfo[] }).modelList;
|
||||
} catch (error) {
|
||||
console.error('Error loading dynamic models for:', providerName, error);
|
||||
}
|
||||
|
||||
// Only update models for the specific provider
|
||||
setModelList((prevModels) => {
|
||||
const otherModels = prevModels.filter((model) => model.provider !== providerName);
|
||||
return [...otherModels, ...providerModels];
|
||||
});
|
||||
setIsModelLoading(undefined);
|
||||
};
|
||||
|
||||
const startListening = () => {
|
||||
|
||||
@@ -137,35 +137,36 @@ export const ChatImpl = memo(
|
||||
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||
|
||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
|
||||
api: '/api/chat',
|
||||
body: {
|
||||
apiKeys,
|
||||
files,
|
||||
promptId,
|
||||
contextOptimization: contextOptimizationEnabled,
|
||||
},
|
||||
sendExtraMessageFields: true,
|
||||
onError: (error) => {
|
||||
logger.error('Request failed\n\n', error);
|
||||
toast.error(
|
||||
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
|
||||
);
|
||||
},
|
||||
onFinish: (message, response) => {
|
||||
const usage = response.usage;
|
||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
|
||||
useChat({
|
||||
api: '/api/chat',
|
||||
body: {
|
||||
apiKeys,
|
||||
files,
|
||||
promptId,
|
||||
contextOptimization: contextOptimizationEnabled,
|
||||
},
|
||||
sendExtraMessageFields: true,
|
||||
onError: (e) => {
|
||||
logger.error('Request failed\n\n', e, error);
|
||||
toast.error(
|
||||
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
||||
);
|
||||
},
|
||||
onFinish: (message, response) => {
|
||||
const usage = response.usage;
|
||||
|
||||
if (usage) {
|
||||
console.log('Token usage:', usage);
|
||||
if (usage) {
|
||||
console.log('Token usage:', usage);
|
||||
|
||||
// You can now use the usage data as needed
|
||||
}
|
||||
// You can now use the usage data as needed
|
||||
}
|
||||
|
||||
logger.debug('Finished streaming');
|
||||
},
|
||||
initialMessages,
|
||||
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
|
||||
});
|
||||
logger.debug('Finished streaming');
|
||||
},
|
||||
initialMessages,
|
||||
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
|
||||
});
|
||||
useEffect(() => {
|
||||
const prompt = searchParams.get('prompt');
|
||||
|
||||
@@ -263,13 +264,17 @@ export const ChatImpl = memo(
|
||||
*/
|
||||
await workbenchStore.saveAllFiles();
|
||||
|
||||
if (error != null) {
|
||||
setMessages(messages.slice(0, -1));
|
||||
}
|
||||
|
||||
const fileModifications = workbenchStore.getFileModifcations();
|
||||
|
||||
chatStore.setKey('aborted', false);
|
||||
|
||||
runAnimation();
|
||||
|
||||
if (!chatStarted && messageInput && autoSelectTemplate) {
|
||||
if (!chatStarted && _input && autoSelectTemplate) {
|
||||
setFakeLoading(true);
|
||||
setMessages([
|
||||
{
|
||||
@@ -291,13 +296,21 @@ export const ChatImpl = memo(
|
||||
// reload();
|
||||
|
||||
const { template, title } = await selectStarterTemplate({
|
||||
message: messageInput,
|
||||
message: _input,
|
||||
model,
|
||||
provider,
|
||||
});
|
||||
|
||||
if (template !== 'blank') {
|
||||
const temResp = await getTemplates(template, title);
|
||||
const temResp = await getTemplates(template, title).catch((e) => {
|
||||
if (e.message.includes('rate limit')) {
|
||||
toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
|
||||
} else {
|
||||
toast.warning('Failed to import starter template\n Continuing with blank template');
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (temResp) {
|
||||
const { assistantMessage, userMessage } = temResp;
|
||||
@@ -306,7 +319,7 @@ export const ChatImpl = memo(
|
||||
{
|
||||
id: `${new Date().getTime()}`,
|
||||
role: 'user',
|
||||
content: messageInput,
|
||||
content: _input,
|
||||
|
||||
// annotations: ['hidden'],
|
||||
},
|
||||
|
||||
@@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
|
||||
import type { Message } from 'ai';
|
||||
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
||||
import { generateId } from '~/utils/fileUtils';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
'node_modules/**',
|
||||
@@ -37,6 +40,8 @@ interface GitCloneButtonProps {
|
||||
|
||||
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||
const { ready, gitClone } = useGit();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClick = async (_e: any) => {
|
||||
if (!ready) {
|
||||
return;
|
||||
@@ -45,33 +50,34 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||
const repoUrl = prompt('Enter the Git url');
|
||||
|
||||
if (repoUrl) {
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
setLoading(true);
|
||||
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
console.log(filePaths);
|
||||
try {
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
console.log(filePaths);
|
||||
|
||||
// Convert files to common format for command detection
|
||||
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);
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
|
||||
// Detect and create commands message
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
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);
|
||||
|
||||
// Create files message
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
${fileContents
|
||||
.map(
|
||||
@@ -82,29 +88,38 @@ ${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);
|
||||
if (commandsMessage) {
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
}
|
||||
|
||||
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}
|
||||
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"
|
||||
>
|
||||
<span className="i-ph:git-branch" />
|
||||
Clone a Git Repo
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={onClick}
|
||||
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"
|
||||
>
|
||||
<span className="i-ph:git-branch" />
|
||||
Clone a Git Repo
|
||||
</button>
|
||||
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
||||
|
||||
type ChatData = {
|
||||
messages?: Message[]; // Standard Bolt format
|
||||
description?: string; // Optional description
|
||||
};
|
||||
|
||||
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-auto">
|
||||
@@ -20,14 +25,17 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content = e.target?.result as string;
|
||||
const data = JSON.parse(content);
|
||||
const data = JSON.parse(content) as ChatData;
|
||||
|
||||
if (!Array.isArray(data.messages)) {
|
||||
toast.error('Invalid chat file format');
|
||||
// Standard format
|
||||
if (Array.isArray(data.messages)) {
|
||||
await importChat(data.description || 'Imported Chat', data.messages);
|
||||
toast.success('Chat imported successfully');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await importChat(data.description, data.messages);
|
||||
toast.success('Chat imported successfully');
|
||||
toast.error('Invalid chat file format');
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error('Failed to parse chat file: ' + error.message);
|
||||
|
||||
@@ -49,33 +49,32 @@ export function GitUrlImport() {
|
||||
|
||||
if (repoUrl) {
|
||||
const ig = ignore().add(IGNORE_PATTERNS);
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
try {
|
||||
const { workdir, data } = await gitClone(repoUrl);
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
if (importChat) {
|
||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
|
||||
// Convert files to common format for command detection
|
||||
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);
|
||||
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);
|
||||
|
||||
// Detect and create commands message
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
const commands = await detectProjectCommands(fileContents);
|
||||
const commandsMessage = createCommandsMessage(commands);
|
||||
|
||||
// Create files message
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
${fileContents
|
||||
.map(
|
||||
(file) =>
|
||||
@@ -85,17 +84,25 @@ ${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);
|
||||
if (commandsMessage) {
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
toast.error('Failed to import repository');
|
||||
setLoading(false);
|
||||
window.location.href = '/';
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ import React, { useState } from 'react';
|
||||
import { useNavigate } from '@remix-run/react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { toast } from 'react-toastify';
|
||||
import { db, deleteById, getAll } from '~/lib/persistence';
|
||||
import { db, deleteById, getAll, setMessages } from '~/lib/persistence';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { Message } from 'ai';
|
||||
|
||||
// List of supported providers that can have API keys
|
||||
const API_KEY_PROVIDERS = [
|
||||
@@ -22,6 +23,7 @@ const API_KEY_PROVIDERS = [
|
||||
'Perplexity',
|
||||
'Cohere',
|
||||
'AzureOpenAI',
|
||||
'AmazonBedrock',
|
||||
] as const;
|
||||
|
||||
interface ApiKeys {
|
||||
@@ -231,6 +233,81 @@ export default function DataTab() {
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const processChatData = (
|
||||
data: any,
|
||||
): Array<{
|
||||
id: string;
|
||||
messages: Message[];
|
||||
description: string;
|
||||
urlId?: string;
|
||||
}> => {
|
||||
// Handle Bolt standard format (single chat)
|
||||
if (data.messages && Array.isArray(data.messages)) {
|
||||
const chatId = crypto.randomUUID();
|
||||
return [
|
||||
{
|
||||
id: chatId,
|
||||
messages: data.messages,
|
||||
description: data.description || 'Imported Chat',
|
||||
urlId: chatId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Handle Bolt export format (multiple chats)
|
||||
if (data.chats && Array.isArray(data.chats)) {
|
||||
return data.chats.map((chat: { id?: string; messages: Message[]; description?: string; urlId?: string }) => ({
|
||||
id: chat.id || crypto.randomUUID(),
|
||||
messages: chat.messages,
|
||||
description: chat.description || 'Imported Chat',
|
||||
urlId: chat.urlId,
|
||||
}));
|
||||
}
|
||||
|
||||
console.error('No matching format found for:', data);
|
||||
throw new Error('Unsupported chat format');
|
||||
};
|
||||
|
||||
const handleImportChats = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
|
||||
if (!file || !db) {
|
||||
toast.error('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await file.text();
|
||||
const data = JSON.parse(content);
|
||||
const chatsToImport = processChatData(data);
|
||||
|
||||
for (const chat of chatsToImport) {
|
||||
await setMessages(db, chat.id, chat.messages, chat.urlId, chat.description);
|
||||
}
|
||||
|
||||
logStore.logSystem('Chats imported successfully', { count: chatsToImport.length });
|
||||
toast.success(`Successfully imported ${chatsToImport.length} chat${chatsToImport.length > 1 ? 's' : ''}`);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logStore.logError('Failed to import chats:', error);
|
||||
toast.error('Failed to import chats: ' + error.message);
|
||||
} else {
|
||||
toast.error('Failed to import chats');
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
|
||||
<div className="mb-6">
|
||||
@@ -247,6 +324,12 @@ export default function DataTab() {
|
||||
>
|
||||
Export All Chats
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImportChats}
|
||||
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
|
||||
>
|
||||
Import Chats
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteAllChats}
|
||||
disabled={isDeleting}
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
export const LoadingOverlay = ({ message = 'Loading...' }) => {
|
||||
export const LoadingOverlay = ({
|
||||
message = 'Loading...',
|
||||
progress,
|
||||
progressText,
|
||||
}: {
|
||||
message?: string;
|
||||
progress?: number;
|
||||
progressText?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
|
||||
{/* Loading content */}
|
||||
<div className="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
|
||||
<div
|
||||
className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
|
||||
style={{ fontSize: '2rem' }}
|
||||
></div>
|
||||
<p className="text-lg text-bolt-elements-textTertiary">{message}</p>
|
||||
{progress !== undefined && (
|
||||
<div className="w-64 flex flex-col gap-2">
|
||||
<div className="w-full h-2 bg-bolt-elements-background-depth-1 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-bolt-elements-loader-progress transition-all duration-300 ease-out rounded-full"
|
||||
style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
{progressText && <p className="text-sm text-bolt-elements-textTertiary text-center">{progressText}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
20
app/components/ui/Popover.tsx
Normal file
20
app/components/ui/Popover.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
export default ({ children, trigger }: PropsWithChildren<{ trigger: ReactNode }>) => (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
|
||||
<Popover.Anchor />
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
sideOffset={10}
|
||||
side="top"
|
||||
align="center"
|
||||
className="bg-bolt-elements-background-depth-2 text-bolt-elements-item-contentAccent p-2 rounded-md shadow-xl z-workbench"
|
||||
>
|
||||
{children}
|
||||
<Popover.Arrow className="bg-bolt-elements-item-background-depth-2" />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { PortDropdown } from './PortDropdown';
|
||||
@@ -7,6 +7,20 @@ import { ScreenshotSelector } from './ScreenshotSelector';
|
||||
|
||||
type ResizeSide = 'left' | 'right' | null;
|
||||
|
||||
interface WindowSize {
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const WINDOW_SIZES: WindowSize[] = [
|
||||
{ name: 'Mobile', width: 375, height: 667, icon: 'i-ph:device-mobile' },
|
||||
{ name: 'Tablet', width: 768, height: 1024, icon: 'i-ph:device-tablet' },
|
||||
{ name: 'Laptop', width: 1366, height: 768, icon: 'i-ph:laptop' },
|
||||
{ name: 'Desktop', width: 1920, height: 1080, icon: 'i-ph:monitor' },
|
||||
];
|
||||
|
||||
export const Preview = memo(() => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -15,6 +29,7 @@ export const Preview = memo(() => {
|
||||
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
|
||||
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
|
||||
const hasSelectedPreview = useRef(false);
|
||||
const previews = useStore(workbenchStore.previews);
|
||||
const activePreview = previews[activePreviewIndex];
|
||||
@@ -27,7 +42,7 @@ export const Preview = memo(() => {
|
||||
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
|
||||
|
||||
// Use percentage for width
|
||||
const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially
|
||||
const [widthPercent, setWidthPercent] = useState<number>(37.5);
|
||||
|
||||
const resizingState = useRef({
|
||||
isResizing: false,
|
||||
@@ -37,8 +52,10 @@ export const Preview = memo(() => {
|
||||
windowWidth: window.innerWidth,
|
||||
});
|
||||
|
||||
// Define the scaling factor
|
||||
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
|
||||
const SCALING_FACTOR = 2;
|
||||
|
||||
const [isWindowSizeDropdownOpen, setIsWindowSizeDropdownOpen] = useState(false);
|
||||
const [selectedWindowSize, setSelectedWindowSize] = useState<WindowSize>(WINDOW_SIZES[0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePreview) {
|
||||
@@ -79,7 +96,6 @@ export const Preview = memo(() => {
|
||||
[],
|
||||
);
|
||||
|
||||
// When previews change, display the lowest port if user hasn't selected a preview
|
||||
useEffect(() => {
|
||||
if (previews.length > 1 && !hasSelectedPreview.current) {
|
||||
const minPortIndex = previews.reduce(findMinPortIndex, 0);
|
||||
@@ -122,7 +138,6 @@ export const Preview = memo(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent text selection
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
resizingState.current.isResizing = true;
|
||||
@@ -134,7 +149,7 @@ export const Preview = memo(() => {
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
e.preventDefault(); // Prevent any text selection on mousedown
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
@@ -145,7 +160,6 @@ export const Preview = memo(() => {
|
||||
const dx = e.clientX - resizingState.current.startX;
|
||||
const windowWidth = resizingState.current.windowWidth;
|
||||
|
||||
// Apply scaling factor to increase sensitivity
|
||||
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
|
||||
|
||||
let newWidthPercent = resizingState.current.startWidthPercent;
|
||||
@@ -156,7 +170,6 @@ export const Preview = memo(() => {
|
||||
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
|
||||
}
|
||||
|
||||
// Clamp the width between 10% and 90%
|
||||
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
|
||||
|
||||
setWidthPercent(newWidthPercent);
|
||||
@@ -168,17 +181,12 @@ export const Preview = memo(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
// Restore text selection
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
// Handle window resize to ensure widthPercent remains valid
|
||||
useEffect(() => {
|
||||
const handleWindowResize = () => {
|
||||
/*
|
||||
* Optional: Adjust widthPercent if necessary
|
||||
* For now, since widthPercent is relative, no action is needed
|
||||
*/
|
||||
// Optional: Adjust widthPercent if necessary
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
@@ -188,7 +196,6 @@ export const Preview = memo(() => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// A small helper component for the handle's "grip" icon
|
||||
const GripIcon = () => (
|
||||
<div
|
||||
style={{
|
||||
@@ -213,22 +220,47 @@ export const Preview = memo(() => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const openInNewWindow = (size: WindowSize) => {
|
||||
if (activePreview?.baseUrl) {
|
||||
const match = activePreview.baseUrl.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
|
||||
|
||||
if (match) {
|
||||
const previewId = match[1];
|
||||
const previewUrl = `/webcontainer/preview/${previewId}`;
|
||||
const newWindow = window.open(
|
||||
previewUrl,
|
||||
'_blank',
|
||||
`noopener,noreferrer,width=${size.width},height=${size.height},menubar=no,toolbar=no,location=no,status=no`,
|
||||
);
|
||||
|
||||
if (newWindow) {
|
||||
newWindow.focus();
|
||||
}
|
||||
} else {
|
||||
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex flex-col relative">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`w-full h-full flex flex-col relative ${isPreviewOnly ? 'fixed inset-0 z-50 bg-white' : ''}`}
|
||||
>
|
||||
{isPortDropdownOpen && (
|
||||
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
|
||||
)}
|
||||
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
|
||||
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
|
||||
<IconButton
|
||||
icon="i-ph:selection"
|
||||
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
||||
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
|
||||
/>
|
||||
<div
|
||||
className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
|
||||
focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
|
||||
>
|
||||
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
|
||||
<IconButton
|
||||
icon="i-ph:selection"
|
||||
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
||||
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex items-center gap-1 bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
|
||||
<input
|
||||
title="URL"
|
||||
ref={inputRef}
|
||||
@@ -250,39 +282,90 @@ export const Preview = memo(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{previews.length > 1 && (
|
||||
<PortDropdown
|
||||
activePreviewIndex={activePreviewIndex}
|
||||
setActivePreviewIndex={setActivePreviewIndex}
|
||||
isDropdownOpen={isPortDropdownOpen}
|
||||
setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}
|
||||
setIsDropdownOpen={setIsPortDropdownOpen}
|
||||
previews={previews}
|
||||
<div className="flex items-center gap-2">
|
||||
{previews.length > 1 && (
|
||||
<PortDropdown
|
||||
activePreviewIndex={activePreviewIndex}
|
||||
setActivePreviewIndex={setActivePreviewIndex}
|
||||
isDropdownOpen={isPortDropdownOpen}
|
||||
setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}
|
||||
setIsDropdownOpen={setIsPortDropdownOpen}
|
||||
previews={previews}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
icon="i-ph:devices"
|
||||
onClick={toggleDeviceMode}
|
||||
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Device mode toggle button */}
|
||||
<IconButton
|
||||
icon="i-ph:devices"
|
||||
onClick={toggleDeviceMode}
|
||||
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
||||
/>
|
||||
<IconButton
|
||||
icon="i-ph:layout-light"
|
||||
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
|
||||
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
|
||||
/>
|
||||
|
||||
{/* Fullscreen toggle button */}
|
||||
<IconButton
|
||||
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
||||
/>
|
||||
|
||||
<div className="flex items-center relative">
|
||||
<IconButton
|
||||
icon="i-ph:arrow-square-out"
|
||||
onClick={() => openInNewWindow(selectedWindowSize)}
|
||||
title={`Open Preview in ${selectedWindowSize.name} Window`}
|
||||
/>
|
||||
<IconButton
|
||||
icon="i-ph:caret-down"
|
||||
onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
|
||||
className="ml-1"
|
||||
title="Select Window Size"
|
||||
/>
|
||||
|
||||
{isWindowSizeDropdownOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
|
||||
<div className="absolute right-0 top-full mt-2 z-50 min-w-[240px] bg-white dark:bg-black rounded-xl shadow-2xl border border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)] overflow-hidden">
|
||||
{WINDOW_SIZES.map((size) => (
|
||||
<button
|
||||
key={size.name}
|
||||
className="w-full px-4 py-3.5 text-left text-[#111827] dark:text-gray-300 text-sm whitespace-nowrap flex items-center gap-3 group hover:bg-[#F5EEFF] dark:hover:bg-gray-900 bg-white dark:bg-black"
|
||||
onClick={() => {
|
||||
setSelectedWindowSize(size);
|
||||
setIsWindowSizeDropdownOpen(false);
|
||||
openInNewWindow(size);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${size.icon} w-5 h-5 text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200`}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
|
||||
{size.name}
|
||||
</span>
|
||||
<span className="text-xs text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
|
||||
{size.width} × {size.height}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
|
||||
<div
|
||||
style={{
|
||||
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
|
||||
height: '100%', // Always full height
|
||||
height: '100%',
|
||||
overflow: 'visible',
|
||||
background: '#fff',
|
||||
background: 'var(--bolt-elements-background-depth-1)',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
}}
|
||||
@@ -292,9 +375,10 @@ export const Preview = memo(() => {
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="preview"
|
||||
className="border-none w-full h-full bg-white"
|
||||
className="border-none w-full h-full bg-bolt-elements-background-depth-1"
|
||||
src={iframeUrl}
|
||||
allowFullScreen
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
|
||||
allow="cross-origin-isolated"
|
||||
/>
|
||||
<ScreenshotSelector
|
||||
isSelectionMode={isSelectionMode}
|
||||
@@ -303,12 +387,13 @@ export const Preview = memo(() => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
|
||||
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary">
|
||||
No preview available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDeviceModeOn && (
|
||||
<>
|
||||
{/* Left handle */}
|
||||
<div
|
||||
onMouseDown={(e) => startResizing(e, 'left')}
|
||||
style={{
|
||||
@@ -333,7 +418,6 @@ export const Preview = memo(() => {
|
||||
<GripIcon />
|
||||
</div>
|
||||
|
||||
{/* Right handle */}
|
||||
<div
|
||||
onMouseDown={(e) => startResizing(e, 'right')}
|
||||
style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare';
|
||||
import type { AppLoadContext } from '@remix-run/cloudflare';
|
||||
import { RemixServer } from '@remix-run/react';
|
||||
import { isbot } from 'isbot';
|
||||
import { renderToReadableStream } from 'react-dom/server';
|
||||
@@ -10,7 +10,7 @@ export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
remixContext: any,
|
||||
_loadContext: AppLoadContext,
|
||||
) {
|
||||
// await initializeModelList({});
|
||||
|
||||
@@ -3,3 +3,36 @@ export const MAX_TOKENS = 8000;
|
||||
|
||||
// limits the number of model responses that can be returned in a single request
|
||||
export const MAX_RESPONSE_SEGMENTS = 2;
|
||||
|
||||
export interface File {
|
||||
type: 'file';
|
||||
content: string;
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
type: 'folder';
|
||||
}
|
||||
|
||||
type Dirent = File | Folder;
|
||||
|
||||
export type FileMap = Record<string, Dirent | undefined>;
|
||||
|
||||
export const IGNORE_PATTERNS = [
|
||||
'node_modules/**',
|
||||
'.git/**',
|
||||
'dist/**',
|
||||
'build/**',
|
||||
'.next/**',
|
||||
'coverage/**',
|
||||
'.cache/**',
|
||||
'.vscode/**',
|
||||
'.idea/**',
|
||||
'**/*.log',
|
||||
'**/.DS_Store',
|
||||
'**/npm-debug.log*',
|
||||
'**/yarn-debug.log*',
|
||||
'**/yarn-error.log*',
|
||||
'**/*lock.json',
|
||||
'**/*lock.yml',
|
||||
];
|
||||
|
||||
138
app/lib/.server/llm/create-summary.ts
Normal file
138
app/lib/.server/llm/create-summary.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
|
||||
import { extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
|
||||
const logger = createScopedLogger('create-summary');
|
||||
|
||||
export async function createSummary(props: {
|
||||
messages: Message[];
|
||||
env?: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
promptId?: string;
|
||||
contextOptimization?: boolean;
|
||||
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
|
||||
}) {
|
||||
const { messages, env: serverEnv, apiKeys, providerSettings, contextOptimization, onFinish } = props;
|
||||
let currentModel = DEFAULT_MODEL;
|
||||
let currentProvider = DEFAULT_PROVIDER.name;
|
||||
const processedMessages = messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
const { model, provider, content } = extractPropertiesFromMessage(message);
|
||||
currentModel = model;
|
||||
currentProvider = provider;
|
||||
|
||||
return { ...message, content };
|
||||
} else if (message.role == 'assistant') {
|
||||
let content = message.content;
|
||||
|
||||
if (contextOptimization) {
|
||||
content = simplifyBoltActions(content);
|
||||
}
|
||||
|
||||
return { ...message, content };
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER;
|
||||
const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider);
|
||||
let modelDetails = staticModels.find((m) => m.name === currentModel);
|
||||
|
||||
if (!modelDetails) {
|
||||
const modelsList = [
|
||||
...(provider.staticModels || []),
|
||||
...(await LLMManager.getInstance().getModelListFromProvider(provider, {
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
serverEnv: serverEnv as any,
|
||||
})),
|
||||
];
|
||||
|
||||
if (!modelsList.length) {
|
||||
throw new Error(`No models found for provider ${provider.name}`);
|
||||
}
|
||||
|
||||
modelDetails = modelsList.find((m) => m.name === currentModel);
|
||||
|
||||
if (!modelDetails) {
|
||||
// Fallback to first model
|
||||
logger.warn(
|
||||
`MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`,
|
||||
);
|
||||
modelDetails = modelsList[0];
|
||||
}
|
||||
}
|
||||
|
||||
let slicedMessages = processedMessages;
|
||||
const { summary } = extractCurrentContext(processedMessages);
|
||||
let summaryText: string | undefined = undefined;
|
||||
let chatId: string | undefined = undefined;
|
||||
|
||||
if (summary && summary.type === 'chatSummary') {
|
||||
chatId = summary.chatId;
|
||||
summaryText = `Below is the Chat Summary till now, this is chat summary before the conversation provided by the user
|
||||
you should also use this as historical message while providing the response to the user.
|
||||
${summary.summary}`;
|
||||
|
||||
if (chatId) {
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < processedMessages.length; i++) {
|
||||
if (processedMessages[i].id === chatId) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
slicedMessages = processedMessages.slice(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const extractTextContent = (message: Message) =>
|
||||
Array.isArray(message.content)
|
||||
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
|
||||
: message.content;
|
||||
|
||||
// select files from the list of code file from the project that might be useful for the current request from the user
|
||||
const resp = await generateText({
|
||||
system: `
|
||||
You are a software engineer. You are working on a project. tou need to summarize the work till now and provide a summary of the chat till now.
|
||||
|
||||
${summaryText}
|
||||
|
||||
RULES:
|
||||
* Only provide the summary of the chat till now.
|
||||
* Do not provide any new information.
|
||||
`,
|
||||
prompt: `
|
||||
please provide a summary of the chat till now.
|
||||
below is the latest chat:
|
||||
|
||||
---
|
||||
${slicedMessages
|
||||
.map((x) => {
|
||||
return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
|
||||
})
|
||||
.join('\n')}
|
||||
---
|
||||
`,
|
||||
model: provider.getModelInstance({
|
||||
model: currentModel,
|
||||
serverEnv,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = resp.text;
|
||||
|
||||
if (onFinish) {
|
||||
onFinish(resp);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
233
app/lib/.server/llm/select-context.ts
Normal file
233
app/lib/.server/llm/select-context.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai';
|
||||
import ignore from 'ignore';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { IGNORE_PATTERNS, type FileMap } from './constants';
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
|
||||
import { createFilesContext, extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
|
||||
// Common patterns to ignore, similar to .gitignore
|
||||
|
||||
const ig = ignore().add(IGNORE_PATTERNS);
|
||||
const logger = createScopedLogger('select-context');
|
||||
|
||||
export async function selectContext(props: {
|
||||
messages: Message[];
|
||||
env?: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
files: FileMap;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
promptId?: string;
|
||||
contextOptimization?: boolean;
|
||||
summary: string;
|
||||
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
|
||||
}) {
|
||||
const { messages, env: serverEnv, apiKeys, files, providerSettings, contextOptimization, summary, onFinish } = props;
|
||||
let currentModel = DEFAULT_MODEL;
|
||||
let currentProvider = DEFAULT_PROVIDER.name;
|
||||
const processedMessages = messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
const { model, provider, content } = extractPropertiesFromMessage(message);
|
||||
currentModel = model;
|
||||
currentProvider = provider;
|
||||
|
||||
return { ...message, content };
|
||||
} else if (message.role == 'assistant') {
|
||||
let content = message.content;
|
||||
|
||||
if (contextOptimization) {
|
||||
content = simplifyBoltActions(content);
|
||||
}
|
||||
|
||||
return { ...message, content };
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER;
|
||||
const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider);
|
||||
let modelDetails = staticModels.find((m) => m.name === currentModel);
|
||||
|
||||
if (!modelDetails) {
|
||||
const modelsList = [
|
||||
...(provider.staticModels || []),
|
||||
...(await LLMManager.getInstance().getModelListFromProvider(provider, {
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
serverEnv: serverEnv as any,
|
||||
})),
|
||||
];
|
||||
|
||||
if (!modelsList.length) {
|
||||
throw new Error(`No models found for provider ${provider.name}`);
|
||||
}
|
||||
|
||||
modelDetails = modelsList.find((m) => m.name === currentModel);
|
||||
|
||||
if (!modelDetails) {
|
||||
// Fallback to first model
|
||||
logger.warn(
|
||||
`MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`,
|
||||
);
|
||||
modelDetails = modelsList[0];
|
||||
}
|
||||
}
|
||||
|
||||
const { codeContext } = extractCurrentContext(processedMessages);
|
||||
|
||||
let filePaths = getFilePaths(files || {});
|
||||
filePaths = filePaths.filter((x) => {
|
||||
const relPath = x.replace('/home/project/', '');
|
||||
return !ig.ignores(relPath);
|
||||
});
|
||||
|
||||
let context = '';
|
||||
const currrentFiles: string[] = [];
|
||||
const contextFiles: FileMap = {};
|
||||
|
||||
if (codeContext?.type === 'codeContext') {
|
||||
const codeContextFiles: string[] = codeContext.files;
|
||||
Object.keys(files || {}).forEach((path) => {
|
||||
let relativePath = path;
|
||||
|
||||
if (path.startsWith('/home/project/')) {
|
||||
relativePath = path.replace('/home/project/', '');
|
||||
}
|
||||
|
||||
if (codeContextFiles.includes(relativePath)) {
|
||||
contextFiles[relativePath] = files[path];
|
||||
currrentFiles.push(relativePath);
|
||||
}
|
||||
});
|
||||
context = createFilesContext(contextFiles);
|
||||
}
|
||||
|
||||
const summaryText = `Here is the summary of the chat till now: ${summary}`;
|
||||
|
||||
const extractTextContent = (message: Message) =>
|
||||
Array.isArray(message.content)
|
||||
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
|
||||
: message.content;
|
||||
|
||||
const lastUserMessage = processedMessages.filter((x) => x.role == 'user').pop();
|
||||
|
||||
if (!lastUserMessage) {
|
||||
throw new Error('No user message found');
|
||||
}
|
||||
|
||||
// select files from the list of code file from the project that might be useful for the current request from the user
|
||||
const resp = await generateText({
|
||||
system: `
|
||||
You are a software engineer. You are working on a project. You have access to the following files:
|
||||
|
||||
AVAILABLE FILES PATHS
|
||||
---
|
||||
${filePaths.map((path) => `- ${path}`).join('\n')}
|
||||
---
|
||||
|
||||
You have following code loaded in the context buffer that you can refer to:
|
||||
|
||||
CURRENT CONTEXT BUFFER
|
||||
---
|
||||
${context}
|
||||
---
|
||||
|
||||
Now, you are given a task. You need to select the files that are relevant to the task from the list of files above.
|
||||
|
||||
RESPONSE FORMAT:
|
||||
your response shoudl be in following format:
|
||||
---
|
||||
<updateContextBuffer>
|
||||
<includeFile path="path/to/file"/>
|
||||
<excludeFile path="path/to/file"/>
|
||||
</updateContextBuffer>
|
||||
---
|
||||
* Your should start with <updateContextBuffer> and end with </updateContextBuffer>.
|
||||
* You can include multiple <includeFile> and <excludeFile> tags in the response.
|
||||
* You should not include any other text in the response.
|
||||
* You should not include any file that is not in the list of files above.
|
||||
* You should not include any file that is already in the context buffer.
|
||||
* If no changes are needed, you can leave the response empty updateContextBuffer tag.
|
||||
`,
|
||||
prompt: `
|
||||
${summaryText}
|
||||
|
||||
Users Question: ${extractTextContent(lastUserMessage)}
|
||||
|
||||
update the context buffer with the files that are relevant to the task from the list of files above.
|
||||
|
||||
CRITICAL RULES:
|
||||
* Only include relevant files in the context buffer.
|
||||
* context buffer should not include any file that is not in the list of files above.
|
||||
* context buffer is extremlly expensive, so only include files that are absolutely necessary.
|
||||
* If no changes are needed, you can leave the response empty updateContextBuffer tag.
|
||||
* Only 5 files can be placed in the context buffer at a time.
|
||||
* if the buffer is full, you need to exclude files that is not needed and include files that is relevent.
|
||||
|
||||
`,
|
||||
model: provider.getModelInstance({
|
||||
model: currentModel,
|
||||
serverEnv,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = resp.text;
|
||||
const updateContextBuffer = response.match(/<updateContextBuffer>([\s\S]*?)<\/updateContextBuffer>/);
|
||||
|
||||
if (!updateContextBuffer) {
|
||||
throw new Error('Invalid response. Please follow the response format');
|
||||
}
|
||||
|
||||
const includeFiles =
|
||||
updateContextBuffer[1]
|
||||
.match(/<includeFile path="(.*?)"/gm)
|
||||
?.map((x) => x.replace('<includeFile path="', '').replace('"', '')) || [];
|
||||
const excludeFiles =
|
||||
updateContextBuffer[1]
|
||||
.match(/<excludeFile path="(.*?)"/gm)
|
||||
?.map((x) => x.replace('<excludeFile path="', '').replace('"', '')) || [];
|
||||
|
||||
const filteredFiles: FileMap = {};
|
||||
excludeFiles.forEach((path) => {
|
||||
delete contextFiles[path];
|
||||
});
|
||||
includeFiles.forEach((path) => {
|
||||
let fullPath = path;
|
||||
|
||||
if (!path.startsWith('/home/project/')) {
|
||||
fullPath = `/home/project/${path}`;
|
||||
}
|
||||
|
||||
if (!filePaths.includes(fullPath)) {
|
||||
throw new Error(`File ${path} is not in the list of files above.`);
|
||||
}
|
||||
|
||||
if (currrentFiles.includes(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
filteredFiles[path] = files[fullPath];
|
||||
});
|
||||
|
||||
if (onFinish) {
|
||||
onFinish(resp);
|
||||
}
|
||||
|
||||
return filteredFiles;
|
||||
|
||||
// generateText({
|
||||
}
|
||||
|
||||
export function getFilePaths(files: FileMap) {
|
||||
let filePaths = Object.keys(files);
|
||||
filePaths = filePaths.filter((x) => {
|
||||
const relPath = x.replace('/home/project/', '');
|
||||
return !ig.ignores(relPath);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
@@ -1,167 +1,48 @@
|
||||
import { convertToCoreMessages, streamText as _streamText } from 'ai';
|
||||
import { MAX_TOKENS } from './constants';
|
||||
import { convertToCoreMessages, streamText as _streamText, type Message } from 'ai';
|
||||
import { MAX_TOKENS, type FileMap } from './constants';
|
||||
import { getSystemPrompt } from '~/lib/common/prompts/prompts';
|
||||
import {
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_PROVIDER,
|
||||
MODEL_REGEX,
|
||||
MODIFICATIONS_TAG_NAME,
|
||||
PROVIDER_LIST,
|
||||
PROVIDER_REGEX,
|
||||
WORK_DIR,
|
||||
} from '~/utils/constants';
|
||||
import ignore from 'ignore';
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODIFICATIONS_TAG_NAME, PROVIDER_LIST, WORK_DIR } from '~/utils/constants';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { PromptLibrary } from '~/lib/common/prompt-library';
|
||||
import { allowedHTMLElements } from '~/utils/markdown';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
interface ToolResult<Name extends string, Args, Result> {
|
||||
toolCallId: string;
|
||||
toolName: Name;
|
||||
args: Args;
|
||||
result: Result;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
toolInvocations?: ToolResult<string, unknown, unknown>[];
|
||||
model?: string;
|
||||
}
|
||||
import { createFilesContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
|
||||
import { getFilePaths } from './select-context';
|
||||
|
||||
export type Messages = Message[];
|
||||
|
||||
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
|
||||
|
||||
export interface File {
|
||||
type: 'file';
|
||||
content: string;
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
type: 'folder';
|
||||
}
|
||||
|
||||
type Dirent = File | Folder;
|
||||
|
||||
export type FileMap = Record<string, Dirent | undefined>;
|
||||
|
||||
export function simplifyBoltActions(input: string): string {
|
||||
// Using regex to match boltAction tags that have type="file"
|
||||
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
|
||||
|
||||
// Replace each matching occurrence
|
||||
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
|
||||
return `${openingTag}\n ...\n ${closingTag}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Common patterns to ignore, similar to .gitignore
|
||||
const IGNORE_PATTERNS = [
|
||||
'node_modules/**',
|
||||
'.git/**',
|
||||
'dist/**',
|
||||
'build/**',
|
||||
'.next/**',
|
||||
'coverage/**',
|
||||
'.cache/**',
|
||||
'.vscode/**',
|
||||
'.idea/**',
|
||||
'**/*.log',
|
||||
'**/.DS_Store',
|
||||
'**/npm-debug.log*',
|
||||
'**/yarn-debug.log*',
|
||||
'**/yarn-error.log*',
|
||||
'**/*lock.json',
|
||||
'**/*lock.yml',
|
||||
];
|
||||
const ig = ignore().add(IGNORE_PATTERNS);
|
||||
|
||||
function createFilesContext(files: FileMap) {
|
||||
let filePaths = Object.keys(files);
|
||||
filePaths = filePaths.filter((x) => {
|
||||
const relPath = x.replace('/home/project/', '');
|
||||
return !ig.ignores(relPath);
|
||||
});
|
||||
|
||||
const fileContexts = filePaths
|
||||
.filter((x) => files[x] && files[x].type == 'file')
|
||||
.map((path) => {
|
||||
const dirent = files[path];
|
||||
|
||||
if (!dirent || dirent.type == 'folder') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const codeWithLinesNumbers = dirent.content
|
||||
.split('\n')
|
||||
.map((v, i) => `${i + 1}|${v}`)
|
||||
.join('\n');
|
||||
|
||||
return `<file path="${path}">\n${codeWithLinesNumbers}\n</file>`;
|
||||
});
|
||||
|
||||
return `Below are the code files present in the webcontainer:\ncode format:\n<line number>|<line content>\n <codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
|
||||
}
|
||||
|
||||
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
|
||||
const textContent = Array.isArray(message.content)
|
||||
? message.content.find((item) => item.type === 'text')?.text || ''
|
||||
: message.content;
|
||||
|
||||
const modelMatch = textContent.match(MODEL_REGEX);
|
||||
const providerMatch = textContent.match(PROVIDER_REGEX);
|
||||
|
||||
/*
|
||||
* Extract model
|
||||
* const modelMatch = message.content.match(MODEL_REGEX);
|
||||
*/
|
||||
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
|
||||
|
||||
/*
|
||||
* Extract provider
|
||||
* const providerMatch = message.content.match(PROVIDER_REGEX);
|
||||
*/
|
||||
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
|
||||
|
||||
const cleanedContent = Array.isArray(message.content)
|
||||
? message.content.map((item) => {
|
||||
if (item.type === 'text') {
|
||||
return {
|
||||
type: 'text',
|
||||
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
|
||||
};
|
||||
}
|
||||
|
||||
return item; // Preserve image_url and other types as is
|
||||
})
|
||||
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
||||
|
||||
return { model, provider, content: cleanedContent };
|
||||
}
|
||||
|
||||
const logger = createScopedLogger('stream-text');
|
||||
|
||||
export async function streamText(props: {
|
||||
messages: Messages;
|
||||
env: Env;
|
||||
messages: Omit<Message, 'id'>[];
|
||||
env?: Env;
|
||||
options?: StreamingOptions;
|
||||
apiKeys?: Record<string, string>;
|
||||
files?: FileMap;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
promptId?: string;
|
||||
contextOptimization?: boolean;
|
||||
contextFiles?: FileMap;
|
||||
summary?: string;
|
||||
}) {
|
||||
const { messages, env: serverEnv, options, apiKeys, files, providerSettings, promptId, contextOptimization } = props;
|
||||
|
||||
// console.log({serverEnv});
|
||||
|
||||
const {
|
||||
messages,
|
||||
env: serverEnv,
|
||||
options,
|
||||
apiKeys,
|
||||
files,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
contextFiles,
|
||||
summary,
|
||||
} = props;
|
||||
let currentModel = DEFAULT_MODEL;
|
||||
let currentProvider = DEFAULT_PROVIDER.name;
|
||||
const processedMessages = messages.map((message) => {
|
||||
let processedMessages = messages.map((message) => {
|
||||
if (message.role === 'user') {
|
||||
const { model, provider, content } = extractPropertiesFromMessage(message);
|
||||
currentModel = model;
|
||||
@@ -219,16 +100,47 @@ export async function streamText(props: {
|
||||
modificationTagName: MODIFICATIONS_TAG_NAME,
|
||||
}) ?? getSystemPrompt();
|
||||
|
||||
if (files && contextOptimization) {
|
||||
const codeContext = createFilesContext(files);
|
||||
systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
|
||||
if (files && contextFiles && contextOptimization) {
|
||||
const codeContext = createFilesContext(contextFiles, true);
|
||||
const filePaths = getFilePaths(files);
|
||||
|
||||
systemPrompt = `${systemPrompt}
|
||||
Below are all the files present in the project:
|
||||
---
|
||||
${filePaths.join('\n')}
|
||||
---
|
||||
|
||||
Below is the context loaded into context buffer for you to have knowledge of and might need changes to fullfill current user request.
|
||||
CONTEXT BUFFER:
|
||||
---
|
||||
${codeContext}
|
||||
---
|
||||
`;
|
||||
|
||||
if (summary) {
|
||||
systemPrompt = `${systemPrompt}
|
||||
below is the chat history till now
|
||||
CHAT SUMMARY:
|
||||
---
|
||||
${props.summary}
|
||||
---
|
||||
`;
|
||||
|
||||
const lastMessage = processedMessages.pop();
|
||||
|
||||
if (lastMessage) {
|
||||
processedMessages = [lastMessage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
|
||||
|
||||
return _streamText({
|
||||
// console.log(systemPrompt,processedMessages);
|
||||
|
||||
return await _streamText({
|
||||
model: provider.getModelInstance({
|
||||
model: currentModel,
|
||||
model: modelDetails.name,
|
||||
serverEnv,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
|
||||
128
app/lib/.server/llm/utils.ts
Normal file
128
app/lib/.server/llm/utils.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { type Message } from 'ai';
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
||||
import { IGNORE_PATTERNS, type FileMap } from './constants';
|
||||
import ignore from 'ignore';
|
||||
import type { ContextAnnotation } from '~/types/context';
|
||||
|
||||
export function extractPropertiesFromMessage(message: Omit<Message, 'id'>): {
|
||||
model: string;
|
||||
provider: string;
|
||||
content: string;
|
||||
} {
|
||||
const textContent = Array.isArray(message.content)
|
||||
? message.content.find((item) => item.type === 'text')?.text || ''
|
||||
: message.content;
|
||||
|
||||
const modelMatch = textContent.match(MODEL_REGEX);
|
||||
const providerMatch = textContent.match(PROVIDER_REGEX);
|
||||
|
||||
/*
|
||||
* Extract model
|
||||
* const modelMatch = message.content.match(MODEL_REGEX);
|
||||
*/
|
||||
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
|
||||
|
||||
/*
|
||||
* Extract provider
|
||||
* const providerMatch = message.content.match(PROVIDER_REGEX);
|
||||
*/
|
||||
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
|
||||
|
||||
const cleanedContent = Array.isArray(message.content)
|
||||
? message.content.map((item) => {
|
||||
if (item.type === 'text') {
|
||||
return {
|
||||
type: 'text',
|
||||
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
|
||||
};
|
||||
}
|
||||
|
||||
return item; // Preserve image_url and other types as is
|
||||
})
|
||||
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
||||
|
||||
return { model, provider, content: cleanedContent };
|
||||
}
|
||||
|
||||
export function simplifyBoltActions(input: string): string {
|
||||
// Using regex to match boltAction tags that have type="file"
|
||||
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
|
||||
|
||||
// Replace each matching occurrence
|
||||
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
|
||||
return `${openingTag}\n ...\n ${closingTag}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function createFilesContext(files: FileMap, useRelativePath?: boolean) {
|
||||
const ig = ignore().add(IGNORE_PATTERNS);
|
||||
let filePaths = Object.keys(files);
|
||||
filePaths = filePaths.filter((x) => {
|
||||
const relPath = x.replace('/home/project/', '');
|
||||
return !ig.ignores(relPath);
|
||||
});
|
||||
|
||||
const fileContexts = filePaths
|
||||
.filter((x) => files[x] && files[x].type == 'file')
|
||||
.map((path) => {
|
||||
const dirent = files[path];
|
||||
|
||||
if (!dirent || dirent.type == 'folder') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const codeWithLinesNumbers = dirent.content
|
||||
.split('\n')
|
||||
// .map((v, i) => `${i + 1}|${v}`)
|
||||
.join('\n');
|
||||
|
||||
let filePath = path;
|
||||
|
||||
if (useRelativePath) {
|
||||
filePath = path.replace('/home/project/', '');
|
||||
}
|
||||
|
||||
return `<file path="${filePath}">\n${codeWithLinesNumbers}\n</file>`;
|
||||
});
|
||||
|
||||
return `<codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
|
||||
}
|
||||
|
||||
export function extractCurrentContext(messages: Message[]) {
|
||||
const lastAssistantMessage = messages.filter((x) => x.role == 'assistant').slice(-1)[0];
|
||||
|
||||
if (!lastAssistantMessage) {
|
||||
return { summary: undefined, codeContext: undefined };
|
||||
}
|
||||
|
||||
let summary: ContextAnnotation | undefined;
|
||||
let codeContext: ContextAnnotation | undefined;
|
||||
|
||||
if (!lastAssistantMessage.annotations?.length) {
|
||||
return { summary: undefined, codeContext: undefined };
|
||||
}
|
||||
|
||||
for (let i = 0; i < lastAssistantMessage.annotations.length; i++) {
|
||||
const annotation = lastAssistantMessage.annotations[i];
|
||||
|
||||
if (!annotation || typeof annotation !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(annotation as any).type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const annotationObject = annotation as any;
|
||||
|
||||
if (annotationObject.type === 'codeContext') {
|
||||
codeContext = annotationObject;
|
||||
break;
|
||||
} else if (annotationObject.type === 'chatSummary') {
|
||||
summary = annotationObject;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { summary, codeContext };
|
||||
}
|
||||
33
app/lib/api/cookies.ts
Normal file
33
app/lib/api/cookies.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export function parseCookies(cookieHeader: string | null) {
|
||||
const cookies: Record<string, string> = {};
|
||||
|
||||
if (!cookieHeader) {
|
||||
return cookies;
|
||||
}
|
||||
|
||||
// Split the cookie string by semicolons and spaces
|
||||
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
|
||||
|
||||
items.forEach((item) => {
|
||||
const [name, ...rest] = item.split('=');
|
||||
|
||||
if (name && rest.length > 0) {
|
||||
// Decode the name and value, and join value parts in case it contains '='
|
||||
const decodedName = decodeURIComponent(name.trim());
|
||||
const decodedValue = decodeURIComponent(rest.join('=').trim());
|
||||
cookies[decodedName] = decodedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
export function getApiKeysFromCookie(cookieHeader: string | null): Record<string, string> {
|
||||
const cookies = parseCookies(cookieHeader);
|
||||
return cookies.apiKeys ? JSON.parse(cookies.apiKeys) : {};
|
||||
}
|
||||
|
||||
export function getProviderSettingsFromCookie(cookieHeader: string | null): Record<string, any> {
|
||||
const cookies = parseCookies(cookieHeader);
|
||||
return cookies.providers ? JSON.parse(cookies.providers) : {};
|
||||
}
|
||||
@@ -99,16 +99,12 @@ Examples:
|
||||
Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
|
||||
|
||||
<boltArtifact id="factorial-function" title="JavaScript Factorial Function">
|
||||
<boltAction type="file" filePath="index.js">
|
||||
function factorial(n) {
|
||||
<boltAction type="file" filePath="index.js">function factorial(n) {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="shell">
|
||||
node index.js
|
||||
</boltAction>
|
||||
...</boltAction>
|
||||
<boltAction type="shell">node index.js</boltAction>
|
||||
</boltArtifact>
|
||||
</assistant_response>
|
||||
</example>
|
||||
@@ -119,24 +115,16 @@ node index.js
|
||||
Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
|
||||
|
||||
<boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
|
||||
<boltAction type="file" filePath="package.json">
|
||||
{
|
||||
<boltAction type="file" filePath="package.json">{
|
||||
"name": "snake",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
...
|
||||
}
|
||||
</boltAction>
|
||||
<boltAction type="shell">
|
||||
npm install --save-dev vite
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
}</boltAction>
|
||||
<boltAction type="shell">npm install --save-dev vite</boltAction>
|
||||
<boltAction type="file" filePath="index.html">...</boltAction>
|
||||
<boltAction type="start">npm run dev</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
|
||||
@@ -149,8 +137,7 @@ npm run dev
|
||||
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
|
||||
|
||||
<boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
|
||||
<boltAction type="file" filePath="package.json">
|
||||
{
|
||||
<boltAction type="file" filePath="package.json">{
|
||||
"name": "bouncing-ball",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
@@ -171,23 +158,12 @@ npm run dev
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/main.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/index.css">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/App.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
}</boltAction>
|
||||
<boltAction type="file" filePath="index.html">...</boltAction>
|
||||
<boltAction type="file" filePath="src/main.jsx">...</boltAction>
|
||||
<boltAction type="file" filePath="src/index.css">...</boltAction>
|
||||
<boltAction type="file" filePath="src/App.jsx">...</boltAction>
|
||||
<boltAction type="start">npm run dev</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
|
||||
|
||||
@@ -231,17 +231,12 @@ Here are some examples of correct usage of artifacts:
|
||||
Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
|
||||
|
||||
<boltArtifact id="factorial-function" title="JavaScript Factorial Function">
|
||||
<boltAction type="file" filePath="index.js">
|
||||
function factorial(n) {
|
||||
...
|
||||
}
|
||||
<boltAction type="file" filePath="index.js">function factorial(n) {
|
||||
...
|
||||
}
|
||||
...</boltAction>
|
||||
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
node index.js
|
||||
</boltAction>
|
||||
<boltAction type="shell">node index.js</boltAction>
|
||||
</boltArtifact>
|
||||
</assistant_response>
|
||||
</example>
|
||||
@@ -253,27 +248,19 @@ Here are some examples of correct usage of artifacts:
|
||||
Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
|
||||
|
||||
<boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
|
||||
<boltAction type="file" filePath="package.json">
|
||||
{
|
||||
"name": "snake",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
...
|
||||
}
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="package.json">{
|
||||
"name": "snake",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
...
|
||||
}</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
npm install --save-dev vite
|
||||
</boltAction>
|
||||
<boltAction type="shell">npm install --save-dev vite</boltAction>
|
||||
|
||||
<boltAction type="file" filePath="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="index.html">...</boltAction>
|
||||
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
<boltAction type="start">npm run dev</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
|
||||
@@ -287,50 +274,38 @@ Here are some examples of correct usage of artifacts:
|
||||
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
|
||||
|
||||
<boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
|
||||
<boltAction type="file" filePath="package.json">
|
||||
{
|
||||
"name": "bouncing-ball",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-spring": "^9.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="package.json">{
|
||||
"name": "bouncing-ball",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-spring": "^9.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}</boltAction>
|
||||
|
||||
<boltAction type="file" filePath="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="index.html">...</boltAction>
|
||||
|
||||
<boltAction type="file" filePath="src/main.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/main.jsx">...</boltAction>
|
||||
|
||||
<boltAction type="file" filePath="src/index.css">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/index.css">...</boltAction>
|
||||
|
||||
<boltAction type="file" filePath="src/App.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
<boltAction type="file" filePath="src/App.jsx">...</boltAction>
|
||||
|
||||
<boltAction type="start">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
<boltAction type="start">npm run dev</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
chatId as chatIdStore,
|
||||
description as descriptionStore,
|
||||
db,
|
||||
updateChatDescription,
|
||||
description as descriptionStore,
|
||||
getMessages,
|
||||
updateChatDescription,
|
||||
} from '~/lib/persistence';
|
||||
|
||||
interface EditChatDescriptionOptions {
|
||||
|
||||
@@ -49,50 +49,68 @@ export function useGit() {
|
||||
}
|
||||
|
||||
fileData.current = {};
|
||||
await git.clone({
|
||||
fs,
|
||||
http,
|
||||
dir: webcontainer.workdir,
|
||||
url,
|
||||
depth: 1,
|
||||
singleBranch: true,
|
||||
corsProxy: 'https://cors.isomorphic-git.org',
|
||||
onAuth: (url) => {
|
||||
// let domain=url.split("/")[2]
|
||||
|
||||
let auth = lookupSavedPassword(url);
|
||||
const headers: {
|
||||
[x: string]: string;
|
||||
} = {
|
||||
'User-Agent': 'bolt.diy',
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
return auth;
|
||||
}
|
||||
const auth = lookupSavedPassword(url);
|
||||
|
||||
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
|
||||
auth = {
|
||||
username: prompt('Enter username'),
|
||||
password: prompt('Enter password'),
|
||||
};
|
||||
return auth;
|
||||
} else {
|
||||
return { cancel: true };
|
||||
}
|
||||
},
|
||||
onAuthFailure: (url, _auth) => {
|
||||
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
|
||||
},
|
||||
onAuthSuccess: (url, auth) => {
|
||||
saveGitAuth(url, auth);
|
||||
},
|
||||
});
|
||||
|
||||
const data: Record<string, { data: any; encoding?: string }> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(fileData.current)) {
|
||||
data[key] = value;
|
||||
if (auth) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
return { workdir: webcontainer.workdir, data };
|
||||
try {
|
||||
await git.clone({
|
||||
fs,
|
||||
http,
|
||||
dir: webcontainer.workdir,
|
||||
url,
|
||||
depth: 1,
|
||||
singleBranch: true,
|
||||
corsProxy: '/api/git-proxy',
|
||||
headers,
|
||||
|
||||
onAuth: (url) => {
|
||||
let auth = lookupSavedPassword(url);
|
||||
|
||||
if (auth) {
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
|
||||
auth = {
|
||||
username: prompt('Enter username'),
|
||||
password: prompt('Enter password'),
|
||||
};
|
||||
return auth;
|
||||
} else {
|
||||
return { cancel: true };
|
||||
}
|
||||
},
|
||||
onAuthFailure: (url, _auth) => {
|
||||
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
|
||||
},
|
||||
onAuthSuccess: (url, auth) => {
|
||||
saveGitAuth(url, auth);
|
||||
},
|
||||
});
|
||||
|
||||
const data: Record<string, { data: any; encoding?: string }> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(fileData.current)) {
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
return { workdir: webcontainer.workdir, data };
|
||||
} catch (error) {
|
||||
console.error('Git clone error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[webcontainer],
|
||||
[webcontainer, fs, ready],
|
||||
);
|
||||
|
||||
return { ready, gitClone };
|
||||
@@ -104,55 +122,86 @@ const getFs = (
|
||||
) => ({
|
||||
promises: {
|
||||
readFile: async (path: string, options: any) => {
|
||||
const encoding = options.encoding;
|
||||
const encoding = options?.encoding;
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('readFile', relativePath, encoding);
|
||||
|
||||
return await webcontainer.fs.readFile(relativePath, encoding);
|
||||
try {
|
||||
const result = await webcontainer.fs.readFile(relativePath, encoding);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
writeFile: async (path: string, data: any, options: any) => {
|
||||
const encoding = options.encoding;
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('writeFile', { relativePath, data, encoding });
|
||||
|
||||
if (record.current) {
|
||||
record.current[relativePath] = { data, encoding };
|
||||
}
|
||||
|
||||
return await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
|
||||
try {
|
||||
const result = await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
mkdir: async (path: string, options: any) => {
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('mkdir', relativePath, options);
|
||||
|
||||
return await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
|
||||
try {
|
||||
const result = await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
readdir: async (path: string, options: any) => {
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('readdir', relativePath, options);
|
||||
|
||||
return await webcontainer.fs.readdir(relativePath, options);
|
||||
try {
|
||||
const result = await webcontainer.fs.readdir(relativePath, options);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
rm: async (path: string, options: any) => {
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('rm', relativePath, options);
|
||||
|
||||
return await webcontainer.fs.rm(relativePath, { ...(options || {}) });
|
||||
try {
|
||||
const result = await webcontainer.fs.rm(relativePath, { ...(options || {}) });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
rmdir: async (path: string, options: any) => {
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
console.log('rmdir', relativePath, options);
|
||||
|
||||
return await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
|
||||
try {
|
||||
const result = await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Mock implementations for missing functions
|
||||
unlink: async (path: string) => {
|
||||
// unlink is just removing a single file
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
return await webcontainer.fs.rm(relativePath, { recursive: false });
|
||||
},
|
||||
|
||||
try {
|
||||
return await webcontainer.fs.rm(relativePath, { recursive: false });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
stat: async (path: string) => {
|
||||
try {
|
||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||
@@ -185,23 +234,12 @@ const getFs = (
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
lstat: async (path: string) => {
|
||||
/*
|
||||
* For basic usage, lstat can return the same as stat
|
||||
* since we're not handling symbolic links
|
||||
*/
|
||||
return await getFs(webcontainer, record).promises.stat(path);
|
||||
},
|
||||
|
||||
readlink: async (path: string) => {
|
||||
/*
|
||||
* Since WebContainer doesn't support symlinks,
|
||||
* we'll throw a "not a symbolic link" error
|
||||
*/
|
||||
throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
|
||||
},
|
||||
|
||||
symlink: async (target: string, path: string) => {
|
||||
/*
|
||||
* Since WebContainer doesn't support symlinks,
|
||||
|
||||
@@ -46,7 +46,7 @@ export abstract class BaseProvider implements ProviderInfo {
|
||||
|
||||
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
|
||||
const apiKey =
|
||||
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[baseUrlKey];
|
||||
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[apiTokenKey];
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
@@ -111,7 +111,7 @@ export abstract class BaseProvider implements ProviderInfo {
|
||||
|
||||
abstract getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
serverEnv?: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1;
|
||||
|
||||
@@ -83,7 +83,7 @@ export class LLMManager {
|
||||
|
||||
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
|
||||
|
||||
if (providerSettings) {
|
||||
if (providerSettings && Object.keys(providerSettings).length > 0) {
|
||||
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
|
||||
}
|
||||
|
||||
|
||||
113
app/lib/modules/llm/providers/amazon-bedrock.ts
Normal file
113
app/lib/modules/llm/providers/amazon-bedrock.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { BaseProvider } from '~/lib/modules/llm/base-provider';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
||||
|
||||
interface AWSBedRockConfig {
|
||||
region: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken?: string;
|
||||
}
|
||||
|
||||
export default class AmazonBedrockProvider extends BaseProvider {
|
||||
name = 'AmazonBedrock';
|
||||
getApiKeyLink = 'https://console.aws.amazon.com/iam/home';
|
||||
|
||||
config = {
|
||||
apiTokenKey: 'AWS_BEDROCK_CONFIG',
|
||||
};
|
||||
|
||||
staticModels: ModelInfo[] = [
|
||||
{
|
||||
name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
|
||||
label: 'Claude 3.5 Sonnet (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 4096,
|
||||
},
|
||||
{
|
||||
name: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
label: 'Claude 3 Sonnet (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 4096,
|
||||
},
|
||||
{
|
||||
name: 'anthropic.claude-3-haiku-20240307-v1:0',
|
||||
label: 'Claude 3 Haiku (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 4096,
|
||||
},
|
||||
{
|
||||
name: 'amazon.nova-pro-v1:0',
|
||||
label: 'Amazon Nova Pro (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 5120,
|
||||
},
|
||||
{
|
||||
name: 'amazon.nova-lite-v1:0',
|
||||
label: 'Amazon Nova Lite (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 5120,
|
||||
},
|
||||
{
|
||||
name: 'mistral.mistral-large-2402-v1:0',
|
||||
label: 'Mistral Large 24.02 (Bedrock)',
|
||||
provider: 'AmazonBedrock',
|
||||
maxTokenAllowed: 8192,
|
||||
},
|
||||
];
|
||||
|
||||
private _parseAndValidateConfig(apiKey: string): AWSBedRockConfig {
|
||||
let parsedConfig: AWSBedRockConfig;
|
||||
|
||||
try {
|
||||
parsedConfig = JSON.parse(apiKey);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Invalid AWS Bedrock configuration format. Please provide a valid JSON string containing region, accessKeyId, and secretAccessKey.',
|
||||
);
|
||||
}
|
||||
|
||||
const { region, accessKeyId, secretAccessKey, sessionToken } = parsedConfig;
|
||||
|
||||
if (!region || !accessKeyId || !secretAccessKey) {
|
||||
throw new Error(
|
||||
'Missing required AWS credentials. Configuration must include region, accessKeyId, and secretAccessKey.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
region,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
...(sessionToken && { sessionToken }),
|
||||
};
|
||||
}
|
||||
|
||||
getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: any;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1 {
|
||||
const { model, serverEnv, apiKeys, providerSettings } = options;
|
||||
|
||||
const { apiKey } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: '',
|
||||
defaultApiTokenKey: 'AWS_BEDROCK_CONFIG',
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`Missing API key for ${this.name} provider`);
|
||||
}
|
||||
|
||||
const config = this._parseAndValidateConfig(apiKey);
|
||||
const bedrock = createAmazonBedrock(config);
|
||||
|
||||
return bedrock(model);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default class DeepseekProvider extends BaseProvider {
|
||||
staticModels: ModelInfo[] = [
|
||||
{ name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
|
||||
{ name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 },
|
||||
{ name: 'deepseek-reasoner', label: 'Deepseek-Reasoner', provider: 'Deepseek', maxTokenAllowed: 8000 },
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
|
||||
53
app/lib/modules/llm/providers/github.ts
Normal file
53
app/lib/modules/llm/providers/github.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { BaseProvider } from '~/lib/modules/llm/base-provider';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
|
||||
export default class GithubProvider extends BaseProvider {
|
||||
name = 'Github';
|
||||
getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
|
||||
|
||||
config = {
|
||||
apiTokenKey: 'GITHUB_API_KEY',
|
||||
};
|
||||
|
||||
// find more in https://github.com/marketplace?type=models
|
||||
staticModels: ModelInfo[] = [
|
||||
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
|
||||
{ name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1 {
|
||||
const { model, serverEnv, apiKeys, providerSettings } = options;
|
||||
|
||||
const { apiKey } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: '',
|
||||
defaultApiTokenKey: 'GITHUB_API_KEY',
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`Missing API key for ${this.name} provider`);
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
baseURL: 'https://models.inference.ai.azure.com',
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return openai(model);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { createOpenAI } from '@ai-sdk/openai';
|
||||
|
||||
export default class HyperbolicProvider extends BaseProvider {
|
||||
name = 'Hyperbolic';
|
||||
getApiKeyLink = 'https://hyperbolic.xyz/settings';
|
||||
getApiKeyLink = 'https://app.hyperbolic.xyz/settings';
|
||||
|
||||
config = {
|
||||
apiTokenKey: 'HYPERBOLIC_API_KEY',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { logger } from '~/utils/logger';
|
||||
|
||||
export default class LMStudioProvider extends BaseProvider {
|
||||
name = 'LMStudio';
|
||||
@@ -22,7 +23,7 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
settings?: IProviderSetting,
|
||||
serverEnv: Record<string, string> = {},
|
||||
): Promise<ModelInfo[]> {
|
||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: settings,
|
||||
serverEnv,
|
||||
@@ -31,7 +32,18 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
});
|
||||
|
||||
if (!baseUrl) {
|
||||
return [];
|
||||
throw new Error('No baseUrl found for LMStudio provider');
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
/*
|
||||
* Running in Server
|
||||
* Backend: Check if we're running in Docker
|
||||
*/
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/models`);
|
||||
@@ -51,13 +63,26 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}) => LanguageModelV1 = (options) => {
|
||||
const { apiKeys, providerSettings, serverEnv, model } = options;
|
||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
|
||||
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
|
||||
defaultApiTokenKey: '',
|
||||
});
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('No baseUrl found for LMStudio provider');
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
}
|
||||
|
||||
logger.debug('LMStudio Base Url used: ', baseUrl);
|
||||
|
||||
const lmstudio = createOpenAI({
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
apiKey: '',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { ollama } from 'ollama-ai-provider';
|
||||
import { logger } from '~/utils/logger';
|
||||
|
||||
interface OllamaModelDetails {
|
||||
parent_model: string;
|
||||
@@ -45,7 +46,7 @@ export default class OllamaProvider extends BaseProvider {
|
||||
settings?: IProviderSetting,
|
||||
serverEnv: Record<string, string> = {},
|
||||
): Promise<ModelInfo[]> {
|
||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: settings,
|
||||
serverEnv,
|
||||
@@ -54,7 +55,18 @@ export default class OllamaProvider extends BaseProvider {
|
||||
});
|
||||
|
||||
if (!baseUrl) {
|
||||
return [];
|
||||
throw new Error('No baseUrl found for OLLAMA provider');
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
/*
|
||||
* Running in Server
|
||||
* Backend: Check if we're running in Docker
|
||||
*/
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
@@ -78,18 +90,23 @@ export default class OllamaProvider extends BaseProvider {
|
||||
const { apiKeys, providerSettings, serverEnv, model } = options;
|
||||
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
|
||||
defaultApiTokenKey: '',
|
||||
});
|
||||
|
||||
// Backend: Check if we're running in Docker
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
if (!baseUrl) {
|
||||
throw new Error('No baseUrl found for OLLAMA provider');
|
||||
}
|
||||
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
|
||||
logger.debug('Ollama Base Url used: ', baseUrl);
|
||||
|
||||
const ollamaInstance = ollama(model, {
|
||||
numCtx: DEFAULT_NUM_CTX,
|
||||
}) as LanguageModelV1 & { config: any };
|
||||
|
||||
@@ -14,6 +14,8 @@ import PerplexityProvider from './providers/perplexity';
|
||||
import TogetherProvider from './providers/together';
|
||||
import XAIProvider from './providers/xai';
|
||||
import HyperbolicProvider from './providers/hyperbolic';
|
||||
import AmazonBedrockProvider from './providers/amazon-bedrock';
|
||||
import GithubProvider from './providers/github';
|
||||
|
||||
export {
|
||||
AnthropicProvider,
|
||||
@@ -32,4 +34,6 @@ export {
|
||||
XAIProvider,
|
||||
TogetherProvider,
|
||||
LMStudioProvider,
|
||||
AmazonBedrockProvider,
|
||||
GithubProvider,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,192 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
// Extend Window interface to include our custom property
|
||||
declare global {
|
||||
interface Window {
|
||||
_tabId?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PreviewInfo {
|
||||
port: number;
|
||||
ready: boolean;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
// Create a broadcast channel for preview updates
|
||||
const PREVIEW_CHANNEL = 'preview-updates';
|
||||
|
||||
export class PreviewsStore {
|
||||
#availablePreviews = new Map<number, PreviewInfo>();
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#broadcastChannel: BroadcastChannel;
|
||||
#lastUpdate = new Map<string, number>();
|
||||
#watchedFiles = new Set<string>();
|
||||
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
#REFRESH_DELAY = 300;
|
||||
#storageChannel: BroadcastChannel;
|
||||
|
||||
previews = atom<PreviewInfo[]>([]);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL);
|
||||
this.#storageChannel = new BroadcastChannel('storage-sync-channel');
|
||||
|
||||
// Listen for preview updates from other tabs
|
||||
this.#broadcastChannel.onmessage = (event) => {
|
||||
const { type, previewId } = event.data;
|
||||
|
||||
if (type === 'file-change') {
|
||||
const timestamp = event.data.timestamp;
|
||||
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
|
||||
|
||||
if (timestamp > lastUpdate) {
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
this.refreshPreview(previewId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for storage sync messages
|
||||
this.#storageChannel.onmessage = (event) => {
|
||||
const { storage, source } = event.data;
|
||||
|
||||
if (storage && source !== this._getTabId()) {
|
||||
this._syncStorage(storage);
|
||||
}
|
||||
};
|
||||
|
||||
// Override localStorage setItem to catch all changes
|
||||
if (typeof window !== 'undefined') {
|
||||
const originalSetItem = localStorage.setItem;
|
||||
|
||||
localStorage.setItem = (...args) => {
|
||||
originalSetItem.apply(localStorage, args);
|
||||
this._broadcastStorageSync();
|
||||
};
|
||||
}
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
// Generate a unique ID for this tab
|
||||
private _getTabId(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window._tabId) {
|
||||
window._tabId = Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
return window._tabId;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sync storage data between tabs
|
||||
private _syncStorage(storage: Record<string, string>) {
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.entries(storage).forEach(([key, value]) => {
|
||||
try {
|
||||
const originalSetItem = Object.getPrototypeOf(localStorage).setItem;
|
||||
originalSetItem.call(localStorage, key, value);
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error syncing storage:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Force a refresh after syncing storage
|
||||
const previews = this.previews.get();
|
||||
previews.forEach((preview) => {
|
||||
const previewId = this.getPreviewId(preview.baseUrl);
|
||||
|
||||
if (previewId) {
|
||||
this.refreshPreview(previewId);
|
||||
}
|
||||
});
|
||||
|
||||
// Reload the page content
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
|
||||
if (iframe) {
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast storage state to other tabs
|
||||
private _broadcastStorageSync() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storage: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
|
||||
if (key) {
|
||||
storage[key] = localStorage.getItem(key) || '';
|
||||
}
|
||||
}
|
||||
|
||||
this.#storageChannel.postMessage({
|
||||
type: 'storage-sync',
|
||||
storage,
|
||||
source: this._getTabId(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
// Listen for server ready events
|
||||
webcontainer.on('server-ready', (port, url) => {
|
||||
console.log('[Preview] Server ready on port:', port, url);
|
||||
this.broadcastUpdate(url);
|
||||
|
||||
// Initial storage sync when preview is ready
|
||||
this._broadcastStorageSync();
|
||||
});
|
||||
|
||||
try {
|
||||
// Watch for file changes
|
||||
const watcher = await webcontainer.fs.watch('**/*', { persistent: true });
|
||||
|
||||
// Use the native watch events
|
||||
(watcher as any).addEventListener('change', async () => {
|
||||
const previews = this.previews.get();
|
||||
|
||||
for (const preview of previews) {
|
||||
const previewId = this.getPreviewId(preview.baseUrl);
|
||||
|
||||
if (previewId) {
|
||||
this.broadcastFileChange(previewId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for DOM changes that might affect storage
|
||||
if (typeof window !== 'undefined') {
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
// Broadcast storage changes when DOM changes
|
||||
this._broadcastStorageSync();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error setting up watchers:', error);
|
||||
}
|
||||
|
||||
// Listen for port events
|
||||
webcontainer.on('port', (port, type, url) => {
|
||||
let previewInfo = this.#availablePreviews.get(port);
|
||||
|
||||
@@ -44,6 +209,101 @@ export class PreviewsStore {
|
||||
previewInfo.baseUrl = url;
|
||||
|
||||
this.previews.set([...previews]);
|
||||
|
||||
if (type === 'open') {
|
||||
this.broadcastUpdate(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to extract preview ID from URL
|
||||
getPreviewId(url: string): string | null {
|
||||
const match = url.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Broadcast state change to all tabs
|
||||
broadcastStateChange(previewId: string) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'state-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast file change to all tabs
|
||||
broadcastFileChange(previewId: string) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'file-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast update to all tabs
|
||||
broadcastUpdate(url: string) {
|
||||
const previewId = this.getPreviewId(url);
|
||||
|
||||
if (previewId) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'file-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method to refresh a specific preview
|
||||
refreshPreview(previewId: string) {
|
||||
// Clear any pending refresh for this preview
|
||||
const existingTimeout = this.#refreshTimeouts.get(previewId);
|
||||
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
|
||||
// Set a new timeout for this refresh
|
||||
const timeout = setTimeout(() => {
|
||||
const previews = this.previews.get();
|
||||
const preview = previews.find((p) => this.getPreviewId(p.baseUrl) === previewId);
|
||||
|
||||
if (preview) {
|
||||
preview.ready = false;
|
||||
this.previews.set([...previews]);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
preview.ready = true;
|
||||
this.previews.set([...previews]);
|
||||
});
|
||||
}
|
||||
|
||||
this.#refreshTimeouts.delete(previewId);
|
||||
}, this.#REFRESH_DELAY);
|
||||
|
||||
this.#refreshTimeouts.set(previewId, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
let previewsStore: PreviewsStore | null = null;
|
||||
|
||||
export function usePreviewStore() {
|
||||
if (!previewsStore) {
|
||||
/*
|
||||
* Initialize with a Promise that resolves to WebContainer
|
||||
* This should match how you're initializing WebContainer elsewhere
|
||||
*/
|
||||
previewsStore = new PreviewsStore(Promise.resolve({} as WebContainer));
|
||||
}
|
||||
|
||||
return previewsStore;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ if (!import.meta.env.SSR) {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return WebContainer.boot({
|
||||
coep: 'credentialless',
|
||||
workdirName: WORK_DIR_NAME,
|
||||
forwardPreviewErrors: true, // Enable error forwarding from iframes
|
||||
});
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { createDataStream } from 'ai';
|
||||
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
|
||||
import { createDataStream, generateId } from 'ai';
|
||||
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS, type FileMap } from '~/lib/.server/llm/constants';
|
||||
import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts';
|
||||
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
|
||||
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { getFilePaths, selectContext } from '~/lib/.server/llm/select-context';
|
||||
import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import { createSummary } from '~/lib/.server/llm/create-summary';
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return chatAction(args);
|
||||
@@ -52,23 +56,121 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
promptTokens: 0,
|
||||
totalTokens: 0,
|
||||
};
|
||||
const encoder: TextEncoder = new TextEncoder();
|
||||
let progressCounter: number = 1;
|
||||
|
||||
try {
|
||||
const options: StreamingOptions = {
|
||||
toolChoice: 'none',
|
||||
onFinish: async ({ text: content, finishReason, usage }) => {
|
||||
logger.debug('usage', JSON.stringify(usage));
|
||||
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
|
||||
logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`);
|
||||
|
||||
if (usage) {
|
||||
cumulativeUsage.completionTokens += usage.completionTokens || 0;
|
||||
cumulativeUsage.promptTokens += usage.promptTokens || 0;
|
||||
cumulativeUsage.totalTokens += usage.totalTokens || 0;
|
||||
const dataStream = createDataStream({
|
||||
async execute(dataStream) {
|
||||
const filePaths = getFilePaths(files || {});
|
||||
let filteredFiles: FileMap | undefined = undefined;
|
||||
let summary: string | undefined = undefined;
|
||||
|
||||
if (filePaths.length > 0 && contextOptimization) {
|
||||
dataStream.writeData('HI ');
|
||||
logger.debug('Generating Chat Summary');
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'progress',
|
||||
value: progressCounter++,
|
||||
message: 'Generating Chat Summary',
|
||||
} as ProgressAnnotation);
|
||||
|
||||
// Create a summary of the chat
|
||||
console.log(`Messages count: ${messages.length}`);
|
||||
|
||||
summary = await createSummary({
|
||||
messages: [...messages],
|
||||
env: context.cloudflare?.env,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
onFinish(resp) {
|
||||
if (resp.usage) {
|
||||
logger.debug('createSummary token usage', JSON.stringify(resp.usage));
|
||||
cumulativeUsage.completionTokens += resp.usage.completionTokens || 0;
|
||||
cumulativeUsage.promptTokens += resp.usage.promptTokens || 0;
|
||||
cumulativeUsage.totalTokens += resp.usage.totalTokens || 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'chatSummary',
|
||||
summary,
|
||||
chatId: messages.slice(-1)?.[0]?.id,
|
||||
} as ContextAnnotation);
|
||||
|
||||
// Update context buffer
|
||||
logger.debug('Updating Context Buffer');
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'progress',
|
||||
value: progressCounter++,
|
||||
message: 'Updating Context Buffer',
|
||||
} as ProgressAnnotation);
|
||||
|
||||
// Select context files
|
||||
console.log(`Messages count: ${messages.length}`);
|
||||
filteredFiles = await selectContext({
|
||||
messages: [...messages],
|
||||
env: context.cloudflare?.env,
|
||||
apiKeys,
|
||||
files,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
summary,
|
||||
onFinish(resp) {
|
||||
if (resp.usage) {
|
||||
logger.debug('selectContext token usage', JSON.stringify(resp.usage));
|
||||
cumulativeUsage.completionTokens += resp.usage.completionTokens || 0;
|
||||
cumulativeUsage.promptTokens += resp.usage.promptTokens || 0;
|
||||
cumulativeUsage.totalTokens += resp.usage.totalTokens || 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (filteredFiles) {
|
||||
logger.debug(`files in context : ${JSON.stringify(Object.keys(filteredFiles))}`);
|
||||
}
|
||||
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'codeContext',
|
||||
files: Object.keys(filteredFiles).map((key) => {
|
||||
let path = key;
|
||||
|
||||
if (path.startsWith(WORK_DIR)) {
|
||||
path = path.replace(WORK_DIR, '');
|
||||
}
|
||||
|
||||
return path;
|
||||
}),
|
||||
} as ContextAnnotation);
|
||||
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'progress',
|
||||
value: progressCounter++,
|
||||
message: 'Context Buffer Updated',
|
||||
} as ProgressAnnotation);
|
||||
logger.debug('Context Buffer Updated');
|
||||
}
|
||||
|
||||
if (finishReason !== 'length') {
|
||||
const encoder = new TextEncoder();
|
||||
const usageStream = createDataStream({
|
||||
async execute(dataStream) {
|
||||
// Stream the text
|
||||
const options: StreamingOptions = {
|
||||
toolChoice: 'none',
|
||||
onFinish: async ({ text: content, finishReason, usage }) => {
|
||||
logger.debug('usage', JSON.stringify(usage));
|
||||
|
||||
if (usage) {
|
||||
cumulativeUsage.completionTokens += usage.completionTokens || 0;
|
||||
cumulativeUsage.promptTokens += usage.promptTokens || 0;
|
||||
cumulativeUsage.totalTokens += usage.totalTokens || 0;
|
||||
}
|
||||
|
||||
if (finishReason !== 'length') {
|
||||
dataStream.writeMessageAnnotation({
|
||||
type: 'usage',
|
||||
value: {
|
||||
@@ -77,69 +179,95 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
totalTokens: cumulativeUsage.totalTokens,
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: (error: any) => `Custom error: ${error.message}`,
|
||||
}).pipeThrough(
|
||||
new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
// Convert the string stream to a byte stream
|
||||
const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
|
||||
controller.enqueue(encoder.encode(str));
|
||||
},
|
||||
}),
|
||||
);
|
||||
await stream.switchSource(usageStream);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
stream.close();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
return;
|
||||
}
|
||||
// stream.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
|
||||
throw Error('Cannot continue message: Maximum segments reached');
|
||||
}
|
||||
if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
|
||||
throw Error('Cannot continue message: Maximum segments reached');
|
||||
}
|
||||
|
||||
const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;
|
||||
const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;
|
||||
|
||||
logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
|
||||
logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
|
||||
|
||||
messages.push({ role: 'assistant', content });
|
||||
messages.push({ role: 'user', content: CONTINUE_PROMPT });
|
||||
messages.push({ id: generateId(), role: 'assistant', content });
|
||||
messages.push({ id: generateId(), role: 'user', content: CONTINUE_PROMPT });
|
||||
|
||||
const result = await streamText({
|
||||
messages,
|
||||
env: context.cloudflare?.env,
|
||||
options,
|
||||
apiKeys,
|
||||
files,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
});
|
||||
|
||||
result.mergeIntoDataStream(dataStream);
|
||||
|
||||
(async () => {
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === 'error') {
|
||||
const error: any = part.error;
|
||||
logger.error(`${error}`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
const result = await streamText({
|
||||
messages,
|
||||
env: context.cloudflare.env,
|
||||
env: context.cloudflare?.env,
|
||||
options,
|
||||
apiKeys,
|
||||
files,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
contextFiles: filteredFiles,
|
||||
summary,
|
||||
});
|
||||
|
||||
stream.switchSource(result.toDataStream());
|
||||
(async () => {
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === 'error') {
|
||||
const error: any = part.error;
|
||||
logger.error(`${error}`);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
result.mergeIntoDataStream(dataStream);
|
||||
},
|
||||
};
|
||||
onError: (error: any) => `Custom error: ${error.message}`,
|
||||
}).pipeThrough(
|
||||
new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
// Convert the string stream to a byte stream
|
||||
const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
|
||||
controller.enqueue(encoder.encode(str));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await streamText({
|
||||
messages,
|
||||
env: context.cloudflare.env,
|
||||
options,
|
||||
apiKeys,
|
||||
files,
|
||||
providerSettings,
|
||||
promptId,
|
||||
contextOptimization,
|
||||
});
|
||||
|
||||
stream.switchSource(result.toDataStream());
|
||||
|
||||
return new Response(stream.readable, {
|
||||
return new Response(dataStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
contentType: 'text/plain; charset=utf-8',
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Text-Encoding': 'chunked',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
||||
16
app/routes/api.check-env-key.ts
Normal file
16
app/routes/api.check-env-key.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
||||
|
||||
export const loader: LoaderFunction = async ({ context, request }) => {
|
||||
const url = new URL(request.url);
|
||||
const provider = url.searchParams.get('provider');
|
||||
|
||||
if (!provider || !providerBaseUrlEnvKeys[provider].apiTokenKey) {
|
||||
return Response.json({ isSet: false });
|
||||
}
|
||||
|
||||
const envVarName = providerBaseUrlEnvKeys[provider].apiTokenKey;
|
||||
const isSet = !!(process.env[envVarName] || (context?.cloudflare?.env as Record<string, any>)?.[envVarName]);
|
||||
|
||||
return Response.json({ isSet });
|
||||
};
|
||||
@@ -1,34 +1,13 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
//import { StreamingTextResponse, parseStreamPart } from 'ai';
|
||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||
import { stripIndents } from '~/utils/stripIndent';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return enhancerAction(args);
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader: string) {
|
||||
const cookies: any = {};
|
||||
|
||||
// Split the cookie string by semicolons and spaces
|
||||
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
|
||||
|
||||
items.forEach((item) => {
|
||||
const [name, ...rest] = item.split('=');
|
||||
|
||||
if (name && rest) {
|
||||
// Decode the name and value, and join value parts in case it contains '='
|
||||
const decodedName = decodeURIComponent(name.trim());
|
||||
const decodedValue = decodeURIComponent(rest.join('=').trim());
|
||||
cookies[decodedName] = decodedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
const { message, model, provider } = await request.json<{
|
||||
message: string;
|
||||
@@ -55,12 +34,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
|
||||
// Parse the cookie's value (returns an object or null if no cookie exists)
|
||||
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
|
||||
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
|
||||
parseCookies(cookieHeader || '').providers || '{}',
|
||||
);
|
||||
const apiKeys = getApiKeysFromCookie(cookieHeader);
|
||||
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
|
||||
|
||||
try {
|
||||
const result = await streamText({
|
||||
@@ -99,7 +74,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
`,
|
||||
},
|
||||
],
|
||||
env: context.cloudflare.env,
|
||||
env: context.cloudflare?.env as any,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
});
|
||||
@@ -107,7 +82,10 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||
return new Response(result.textStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Text-Encoding': 'chunked',
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
|
||||
65
app/routes/api.git-proxy.$.ts
Normal file
65
app/routes/api.git-proxy.$.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
// Handle all HTTP methods
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return handleProxyRequest(request, params['*']);
|
||||
}
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
return handleProxyRequest(request, params['*']);
|
||||
}
|
||||
|
||||
async function handleProxyRequest(request: Request, path: string | undefined) {
|
||||
try {
|
||||
if (!path) {
|
||||
return json({ error: 'Invalid proxy URL format' }, { status: 400 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Reconstruct the target URL
|
||||
const targetURL = `https://${path}${url.search}`;
|
||||
|
||||
// Forward the request to the target URL
|
||||
const response = await fetch(targetURL, {
|
||||
method: request.method,
|
||||
headers: {
|
||||
...Object.fromEntries(request.headers),
|
||||
|
||||
// Override host header with the target host
|
||||
host: new URL(targetURL).host,
|
||||
},
|
||||
body: ['GET', 'HEAD'].includes(request.method) ? null : await request.arrayBuffer(),
|
||||
});
|
||||
|
||||
// Create response with CORS headers
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
};
|
||||
|
||||
// Handle preflight requests
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: corsHeaders,
|
||||
status: 204,
|
||||
});
|
||||
}
|
||||
|
||||
// Forward the response with CORS headers
|
||||
const responseHeaders = new Headers(response.headers);
|
||||
Object.entries(corsHeaders).forEach(([key, value]) => {
|
||||
responseHeaders.set(key, value);
|
||||
});
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Git proxy error:', error);
|
||||
return json({ error: 'Proxy error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,24 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
//import { StreamingTextResponse, parseStreamPart } from 'ai';
|
||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import { generateText } from 'ai';
|
||||
import { getModelList, PROVIDER_LIST } from '~/utils/constants';
|
||||
import { PROVIDER_LIST } from '~/utils/constants';
|
||||
import { MAX_TOKENS } from '~/lib/.server/llm/constants';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
return llmCallAction(args);
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader: string) {
|
||||
const cookies: any = {};
|
||||
|
||||
// Split the cookie string by semicolons and spaces
|
||||
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
|
||||
|
||||
items.forEach((item) => {
|
||||
const [name, ...rest] = item.split('=');
|
||||
|
||||
if (name && rest) {
|
||||
// Decode the name and value, and join value parts in case it contains '='
|
||||
const decodedName = decodeURIComponent(name.trim());
|
||||
const decodedValue = decodeURIComponent(rest.join('=').trim());
|
||||
cookies[decodedName] = decodedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return cookies;
|
||||
async function getModelList(options: {
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
serverEnv?: Record<string, string>;
|
||||
}) {
|
||||
const llmManager = LLMManager.getInstance(import.meta.env);
|
||||
return llmManager.updateModelList(options);
|
||||
}
|
||||
|
||||
async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
@@ -58,12 +48,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
|
||||
// Parse the cookie's value (returns an object or null if no cookie exists)
|
||||
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
|
||||
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
|
||||
parseCookies(cookieHeader || '').providers || '{}',
|
||||
);
|
||||
const apiKeys = getApiKeysFromCookie(cookieHeader);
|
||||
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
|
||||
|
||||
if (streamOutput) {
|
||||
try {
|
||||
@@ -77,7 +63,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
content: `${message}`,
|
||||
},
|
||||
],
|
||||
env: context.cloudflare.env,
|
||||
env: context.cloudflare?.env as any,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
});
|
||||
@@ -105,8 +91,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const MODEL_LIST = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
|
||||
const modelDetails = MODEL_LIST.find((m) => m.name === model);
|
||||
const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare?.env as any });
|
||||
const modelDetails = models.find((m: ModelInfo) => m.name === model);
|
||||
|
||||
if (!modelDetails) {
|
||||
throw new Error('Model not found');
|
||||
@@ -130,7 +116,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||
],
|
||||
model: providerInfo.getModelInstance({
|
||||
model: modelDetails.name,
|
||||
serverEnv: context.cloudflare.env as any,
|
||||
serverEnv: context.cloudflare?.env as any,
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
}),
|
||||
|
||||
2
app/routes/api.models.$provider.ts
Normal file
2
app/routes/api.models.$provider.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { loader } from './api.models';
|
||||
export { loader };
|
||||
@@ -1,6 +1,84 @@
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import { MODEL_LIST } from '~/utils/constants';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
||||
|
||||
export async function loader() {
|
||||
return json(MODEL_LIST);
|
||||
interface ModelsResponse {
|
||||
modelList: ModelInfo[];
|
||||
providers: ProviderInfo[];
|
||||
defaultProvider: ProviderInfo;
|
||||
}
|
||||
|
||||
let cachedProviders: ProviderInfo[] | null = null;
|
||||
let cachedDefaultProvider: ProviderInfo | null = null;
|
||||
|
||||
function getProviderInfo(llmManager: LLMManager) {
|
||||
if (!cachedProviders) {
|
||||
cachedProviders = llmManager.getAllProviders().map((provider) => ({
|
||||
name: provider.name,
|
||||
staticModels: provider.staticModels,
|
||||
getApiKeyLink: provider.getApiKeyLink,
|
||||
labelForGetApiKey: provider.labelForGetApiKey,
|
||||
icon: provider.icon,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!cachedDefaultProvider) {
|
||||
const defaultProvider = llmManager.getDefaultProvider();
|
||||
cachedDefaultProvider = {
|
||||
name: defaultProvider.name,
|
||||
staticModels: defaultProvider.staticModels,
|
||||
getApiKeyLink: defaultProvider.getApiKeyLink,
|
||||
labelForGetApiKey: defaultProvider.labelForGetApiKey,
|
||||
icon: defaultProvider.icon,
|
||||
};
|
||||
}
|
||||
|
||||
return { providers: cachedProviders, defaultProvider: cachedDefaultProvider };
|
||||
}
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
params,
|
||||
}: {
|
||||
request: Request;
|
||||
params: { provider?: string };
|
||||
}): Promise<Response> {
|
||||
const llmManager = LLMManager.getInstance(import.meta.env);
|
||||
|
||||
// Get client side maintained API keys and provider settings from cookies
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
const apiKeys = getApiKeysFromCookie(cookieHeader);
|
||||
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
|
||||
|
||||
const { providers, defaultProvider } = getProviderInfo(llmManager);
|
||||
|
||||
let modelList: ModelInfo[] = [];
|
||||
|
||||
if (params.provider) {
|
||||
// Only update models for the specific provider
|
||||
const provider = llmManager.getProvider(params.provider);
|
||||
|
||||
if (provider) {
|
||||
const staticModels = provider.staticModels;
|
||||
const dynamicModels = provider.getDynamicModels
|
||||
? await provider.getDynamicModels(apiKeys, providerSettings, import.meta.env)
|
||||
: [];
|
||||
modelList = [...staticModels, ...dynamicModels];
|
||||
}
|
||||
} else {
|
||||
// Update all models
|
||||
modelList = await llmManager.updateModelList({
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
serverEnv: import.meta.env,
|
||||
});
|
||||
}
|
||||
|
||||
return json<ModelsResponse>({
|
||||
modelList,
|
||||
providers,
|
||||
defaultProvider,
|
||||
});
|
||||
}
|
||||
|
||||
92
app/routes/webcontainer.preview.$id.tsx
Normal file
92
app/routes/webcontainer.preview.$id.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { useLoaderData } from '@remix-run/react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const PREVIEW_CHANNEL = 'preview-updates';
|
||||
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const previewId = params.id;
|
||||
|
||||
if (!previewId) {
|
||||
throw new Response('Preview ID is required', { status: 400 });
|
||||
}
|
||||
|
||||
return json({ previewId });
|
||||
}
|
||||
|
||||
export default function WebContainerPreview() {
|
||||
const { previewId } = useLoaderData<typeof loader>();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const broadcastChannelRef = useRef<BroadcastChannel>();
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
|
||||
// Handle preview refresh
|
||||
const handleRefresh = useCallback(() => {
|
||||
if (iframeRef.current && previewUrl) {
|
||||
// Force a clean reload
|
||||
iframeRef.current.src = '';
|
||||
requestAnimationFrame(() => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = previewUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [previewUrl]);
|
||||
|
||||
// Notify other tabs that this preview is ready
|
||||
const notifyPreviewReady = useCallback(() => {
|
||||
if (broadcastChannelRef.current && previewUrl) {
|
||||
broadcastChannelRef.current.postMessage({
|
||||
type: 'preview-ready',
|
||||
previewId,
|
||||
url: previewUrl,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}, [previewId, previewUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize broadcast channel
|
||||
broadcastChannelRef.current = new BroadcastChannel(PREVIEW_CHANNEL);
|
||||
|
||||
// Listen for preview updates
|
||||
broadcastChannelRef.current.onmessage = (event) => {
|
||||
if (event.data.previewId === previewId) {
|
||||
if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') {
|
||||
handleRefresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Construct the WebContainer preview URL
|
||||
const url = `https://${previewId}.local-credentialless.webcontainer-api.io`;
|
||||
setPreviewUrl(url);
|
||||
|
||||
// Set the iframe src
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = url;
|
||||
}
|
||||
|
||||
// Notify other tabs that this preview is ready
|
||||
notifyPreviewReady();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
broadcastChannelRef.current?.close();
|
||||
};
|
||||
}, [previewId, handleRefresh, notifyPreviewReady]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="WebContainer Preview"
|
||||
className="w-full h-full border-none"
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
|
||||
allow="cross-origin-isolated"
|
||||
loading="eager"
|
||||
onLoad={notifyPreviewReady}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -219,7 +219,7 @@
|
||||
--header-height: 54px;
|
||||
--chat-max-width: 37rem;
|
||||
--chat-min-width: 640px;
|
||||
--workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
|
||||
--workbench-width: min(calc(100% - var(--chat-min-width)), 2536px);
|
||||
--workbench-inner-width: var(--workbench-width);
|
||||
--workbench-left: calc(100% - var(--workbench-width));
|
||||
|
||||
|
||||
16
app/types/context.ts
Normal file
16
app/types/context.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type ContextAnnotation =
|
||||
| {
|
||||
type: 'codeContext';
|
||||
files: string[];
|
||||
}
|
||||
| {
|
||||
type: 'chatSummary';
|
||||
summary: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
export type ProgressAnnotation = {
|
||||
type: 'progress';
|
||||
value: number;
|
||||
message: string;
|
||||
};
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { Template } from '~/types/template';
|
||||
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
@@ -17,9 +14,7 @@ const llmManager = LLMManager.getInstance(import.meta.env);
|
||||
export const PROVIDER_LIST = llmManager.getAllProviders();
|
||||
export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
|
||||
|
||||
let MODEL_LIST = llmManager.getModelList();
|
||||
|
||||
const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
|
||||
export const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
|
||||
PROVIDER_LIST.forEach((provider) => {
|
||||
providerBaseUrlEnvKeys[provider.name] = {
|
||||
baseUrlKey: provider.config.baseUrlKey,
|
||||
@@ -27,34 +22,6 @@ PROVIDER_LIST.forEach((provider) => {
|
||||
};
|
||||
});
|
||||
|
||||
// Export the getModelList function using the manager
|
||||
export async function getModelList(options: {
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
serverEnv?: Record<string, string>;
|
||||
}) {
|
||||
return await llmManager.updateModelList(options);
|
||||
}
|
||||
|
||||
async function initializeModelList(options: {
|
||||
env?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
apiKeys?: Record<string, string>;
|
||||
}): Promise<ModelInfo[]> {
|
||||
const { providerSettings, apiKeys, env } = options;
|
||||
const list = await getModelList({
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
serverEnv: env,
|
||||
});
|
||||
MODEL_LIST = list || MODEL_LIST;
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// initializeModelList({})
|
||||
export { initializeModelList, providerBaseUrlEnvKeys, MODEL_LIST };
|
||||
|
||||
// starter Templates
|
||||
|
||||
export const STARTER_TEMPLATES: Template[] = [
|
||||
|
||||
75
changelog.md
75
changelog.md
@@ -1,14 +1,81 @@
|
||||
# 🚀 Release v0.0.5
|
||||
# 🚀 Release v0.0.6
|
||||
|
||||
## What's Changed 🌟
|
||||
|
||||
### 🔄 Changes since v0.0.4
|
||||
### 🔄 Changes since v0.0.5
|
||||
|
||||
### ✨ 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
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* hotfix auto select starter template works without github token #release ([#959](https://github.com/stackblitz-labs/bolt.diy/pull/959)) by @thecodacus
|
||||
* show warning on starter template failure and continue ([#960](https://github.com/stackblitz-labs/bolt.diy/pull/960)) by @thecodacus
|
||||
* updated hyperbolic link ([#961](https://github.com/stackblitz-labs/bolt.diy/pull/961)) by @Gaurav-Wankhede
|
||||
* introduce our own cors proxy for git import to fix 403 errors on isometric git cors proxy ([#924](https://github.com/stackblitz-labs/bolt.diy/pull/924)) by @wonderwhy-er
|
||||
* git private clone with custom proxy ([#1010](https://github.com/stackblitz-labs/bolt.diy/pull/1010)) by @thecodacus
|
||||
* added XAI to docker config ([#274](https://github.com/stackblitz-labs/bolt.diy/pull/274)) by @siddartha-10
|
||||
* ollama and lm studio url issue fix for docker and build ([#1008](https://github.com/stackblitz-labs/bolt.diy/pull/1008)) by @thecodacus
|
||||
* streaming issue fixed for build versions ([#1006](https://github.com/stackblitz-labs/bolt.diy/pull/1006)) by @thecodacus
|
||||
* added ui indicator on how apikeys are set (UI/Env) for api-key-manager component ([#732](https://github.com/stackblitz-labs/bolt.diy/pull/732)) by @Adithyan777
|
||||
* bugfix in fetching API Key on base llm provider. ([#1063](https://github.com/stackblitz-labs/bolt.diy/pull/1063)) by @GaryStimson
|
||||
* cors issues from preview fixed by changing embedder policies ([#1056](https://github.com/stackblitz-labs/bolt.diy/pull/1056)) by @wonderwhy-er
|
||||
* api-key manager cleanup and log error on llm call ([#1077](https://github.com/stackblitz-labs/bolt.diy/pull/1077)) by @thecodacus
|
||||
* fallback model name not working ([#1095](https://github.com/stackblitz-labs/bolt.diy/pull/1095)) by @lewis617
|
||||
* for Open preview in a new tab. ([#1122](https://github.com/stackblitz-labs/bolt.diy/pull/1122)) by @Stijnus
|
||||
* auto select starter template bugfix ([#1148](https://github.com/stackblitz-labs/bolt.diy/pull/1148)) by @thecodacus
|
||||
* updated system prompt to have correct indentations ([#1139](https://github.com/stackblitz-labs/bolt.diy/pull/1139)) by @thecodacus
|
||||
* get environment variables for docker #1120 (2ae897a) by @leex279
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* updating copyright in LICENSE ([#796](https://github.com/stackblitz-labs/bolt.diy/pull/796)) by @coleam00
|
||||
* bugfix/formatting faq docs ([#1027](https://github.com/stackblitz-labs/bolt.diy/pull/1027)) by @leex279
|
||||
* document how we work ([#809](https://github.com/stackblitz-labs/bolt.diy/pull/809)) by @mrsimpson
|
||||
* update README.md ([#1124](https://github.com/stackblitz-labs/bolt.diy/pull/1124)) by @leex279
|
||||
* replace docker-compose with docker compose ([#1094](https://github.com/stackblitz-labs/bolt.diy/pull/1094)) by @lewis617
|
||||
|
||||
|
||||
### ⚙️ CI
|
||||
|
||||
* docker Image creation pipeline ([#1011](https://github.com/stackblitz-labs/bolt.diy/pull/1011)) by @twsl
|
||||
* fix docker image workflow permissions ([#1013](https://github.com/stackblitz-labs/bolt.diy/pull/1013)) by @twsl
|
||||
* added visibility change to public for docker image publish ([#1017](https://github.com/stackblitz-labs/bolt.diy/pull/1017)) by @thecodacus
|
||||
* added arm64 platform for docker published images ([#1021](https://github.com/stackblitz-labs/bolt.diy/pull/1021)) by @thecodacus
|
||||
|
||||
|
||||
### 🔍 Other Changes
|
||||
|
||||
* reverted visibility change ([#1018](https://github.com/stackblitz-labs/bolt.diy/pull/1018)) by @thecodacus
|
||||
* Updating README with resources and small fixes. (354f416) by @coleam00
|
||||
* Adding resources page to index.md for docs. (441b797) by @coleam00
|
||||
* updated docs ([#1025](https://github.com/stackblitz-labs/bolt.diy/pull/1025)) by @thecodacus
|
||||
* Update README.md (12c6b7a) by @Digitl-Alchemyst
|
||||
|
||||
|
||||
## ✨ First-time Contributors
|
||||
|
||||
A huge thank you to our amazing new contributors! Your first contribution marks the start of an exciting journey! 🌟
|
||||
|
||||
* 🌟 [@Adithyan777](https://github.com/Adithyan777)
|
||||
* 🌟 [@Digitl-Alchemyst](https://github.com/Digitl-Alchemyst)
|
||||
* 🌟 [@GaryStimson](https://github.com/GaryStimson)
|
||||
* 🌟 [@kunjabijukchhe](https://github.com/kunjabijukchhe)
|
||||
* 🌟 [@leex279](https://github.com/leex279)
|
||||
* 🌟 [@lewis617](https://github.com/lewis617)
|
||||
* 🌟 [@newnol](https://github.com/newnol)
|
||||
* 🌟 [@sidbetatester](https://github.com/sidbetatester)
|
||||
* 🌟 [@siddartha-10](https://github.com/siddartha-10)
|
||||
* 🌟 [@twsl](https://github.com/twsl)
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
**Full Changelog**: [`v0.0.4..v0.0.5`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.4...v0.0.5)
|
||||
**Full Changelog**: [`v0.0.5..v0.0.6`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.5...v0.0.6)
|
||||
|
||||
@@ -20,8 +20,10 @@ services:
|
||||
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
|
||||
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
||||
- XAI_API_KEY=${XAI_API_KEY}
|
||||
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
||||
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
||||
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
|
||||
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||
- RUNNING_IN_DOCKER=true
|
||||
@@ -35,6 +37,7 @@ services:
|
||||
image: bolt-ai:development
|
||||
build:
|
||||
target: bolt-ai-development
|
||||
env_file: ".env.local"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_HMR_PROTOCOL=ws
|
||||
@@ -48,10 +51,12 @@ services:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
|
||||
- XAI_API_KEY=${XAI_API_KEY}
|
||||
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
||||
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
||||
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
||||
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
|
||||
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||
- RUNNING_IN_DOCKER=true
|
||||
|
||||
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.venv
|
||||
site/
|
||||
site/
|
||||
.python-version
|
||||
1
docs/.python-version
Normal file
1
docs/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12.0
|
||||
@@ -144,7 +144,7 @@ docker build . --target bolt-ai-development
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
```bash
|
||||
docker-compose --profile development up
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
#### Running the Development Container
|
||||
@@ -171,7 +171,7 @@ docker build . --target bolt-ai-production
|
||||
|
||||
**Option 3: Docker Compose Profile**
|
||||
```bash
|
||||
docker-compose --profile production up
|
||||
docker compose --profile production up
|
||||
```
|
||||
|
||||
#### Running the Production Container
|
||||
|
||||
128
docs/docs/FAQ.md
128
docs/docs/FAQ.md
@@ -1,91 +1,95 @@
|
||||
# Frequently Asked Questions (FAQ)
|
||||
|
||||
<details>
|
||||
<summary><strong>What are the best models for bolt.diy?</strong></summary>
|
||||
## Models and Setup
|
||||
|
||||
For the best experience with bolt.diy, we recommend using the following models:
|
||||
??? question "What are the best models for bolt.diy?"
|
||||
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
|
||||
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
|
||||
- **DeepSeekCoder V2 236b**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
|
||||
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
|
||||
- **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
|
||||
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
|
||||
- **DeepSeekCoder V3**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
|
||||
- **DeepSeekCoder V2 236b**: available through OpenRouter, DeepSeek API, or self-hosted
|
||||
- **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>
|
||||
!!! warning
|
||||
Models with less than 7b parameters typically lack the capability to properly interact with bolt!
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I get the best results with bolt.diy?</strong></summary>
|
||||
## Best Practices
|
||||
|
||||
- **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**:
|
||||
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
||||
- **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**:
|
||||
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.
|
||||
- **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."*
|
||||
</details>
|
||||
- **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."*
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
|
||||
## Project Information
|
||||
|
||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||
</details>
|
||||
??? question "How do I contribute to bolt.diy?"
|
||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||
|
||||
<details>
|
||||
<summary><strong>What are the future plans for bolt.diy?</strong></summary>
|
||||
??? 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!
|
||||
</details>
|
||||
??? 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!
|
||||
|
||||
<details>
|
||||
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
|
||||
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.
|
||||
|
||||
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!
|
||||
## Model Comparisons
|
||||
|
||||
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>
|
||||
??? 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.
|
||||
|
||||
<details>
|
||||
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
|
||||
## Troubleshooting
|
||||
|
||||
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>
|
||||
??? error "There was an error processing this request"
|
||||
This generic error message means something went wrong. Check both:
|
||||
|
||||
<details>
|
||||
<summary><strong>Common Errors and Troubleshooting</strong></summary>
|
||||
- The terminal (if you started the app with Docker or `pnpm`).
|
||||
|
||||
### **"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.
|
||||
??? 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.
|
||||
|
||||
### **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.
|
||||
??? error "Blank preview when running the app"
|
||||
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
||||
To troubleshoot:
|
||||
|
||||
### **"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.
|
||||
- Check the developer console for errors.
|
||||
|
||||
### **"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)
|
||||
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
|
||||
|
||||
### **"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>
|
||||
??? 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
|
||||
|
||||
- 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)
|
||||
|
||||
??? 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>
|
||||
|
||||
---
|
||||
|
||||
Got more questions? Feel free to reach out or open an issue in our GitHub repo!
|
||||
## 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
|
||||
|
||||
!!! bug "Report Issues"
|
||||
[Open an Issue](https://github.com/stackblitz-labs/bolt.diy/issues/19){target=_blank} in our GitHub Repository
|
||||
|
||||
@@ -25,6 +25,8 @@ bolt.diy allows you to choose the LLM that you use for each prompt! Currently, y
|
||||
|
||||
[Join the community!](https://thinktank.ottomator.ai)
|
||||
|
||||
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!
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
@@ -154,7 +156,7 @@ Once you've configured your keys, the application will be ready to use the selec
|
||||
2. **Run the Container**:
|
||||
Use Docker Compose profiles to manage environments:
|
||||
```bash
|
||||
docker-compose --profile development up
|
||||
docker compose --profile development up
|
||||
```
|
||||
|
||||
- With the development profile, changes to your code will automatically reflect in the running container (hot reloading).
|
||||
@@ -186,7 +188,7 @@ To keep your local version of bolt.diy up to date with the latest changes, follo
|
||||
|
||||
- **If using Docker**, ensure you rebuild the Docker image to avoid using a cached version:
|
||||
```bash
|
||||
docker-compose --profile development up --build
|
||||
docker compose --profile development up --build
|
||||
```
|
||||
|
||||
- **If not using Docker**, you can start the application as usual with:
|
||||
|
||||
@@ -65,4 +65,12 @@ markdown_extensions:
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.mark
|
||||
- attr_list
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- tables
|
||||
- def_list
|
||||
- admonition
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- toc:
|
||||
permalink: true
|
||||
@@ -5,7 +5,7 @@
|
||||
"license": "MIT",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"scripts": {
|
||||
"deploy": "npm run build && wrangler pages deploy",
|
||||
"build": "remix vite:build",
|
||||
@@ -30,6 +30,7 @@
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "1.0.6",
|
||||
"@ai-sdk/anthropic": "^0.0.39",
|
||||
"@ai-sdk/cohere": "^1.0.3",
|
||||
"@ai-sdk/google": "^0.0.52",
|
||||
@@ -61,6 +62,7 @@
|
||||
"@radix-ui/react-context-menu": "^2.2.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
|
||||
1395
pnpm-lock.yaml
generated
1395
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
public/icons/AmazonBedrock.svg
Normal file
1
public/icons/AmazonBedrock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -4,9 +4,11 @@ import { defineConfig, type ViteDevServer } from 'vite';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Get git hash with fallback
|
||||
const getGitHash = () => {
|
||||
try {
|
||||
@@ -17,18 +19,21 @@ const getGitHash = () => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export default defineConfig((config) => {
|
||||
return {
|
||||
define: {
|
||||
__COMMIT_HASH: JSON.stringify(getGitHash()),
|
||||
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
// 'process.env': JSON.stringify(process.env)
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
plugins: [
|
||||
nodePolyfills({
|
||||
include: ['path', 'buffer'],
|
||||
include: ['path', 'buffer', 'process'],
|
||||
}),
|
||||
config.mode !== 'test' && remixCloudflareDevProxy(),
|
||||
remixVitePlugin({
|
||||
|
||||
1
worker-configuration.d.ts
vendored
1
worker-configuration.d.ts
vendored
@@ -16,4 +16,5 @@ interface Env {
|
||||
MISTRAL_API_KEY: string;
|
||||
XAI_API_KEY: string;
|
||||
PERPLEXITY_API_KEY: string;
|
||||
AWS_BEDROCK_CONFIG: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user