Compare commits
43 Commits
v0.0.4
...
leex279-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840dd59af8 | ||
|
|
7f540b5d1e | ||
|
|
e19644268c | ||
|
|
87ff81035f | ||
|
|
c7738243ca | ||
|
|
41bb909f8d | ||
|
|
85d864f607 | ||
|
|
fad41973e2 | ||
|
|
3a298f1586 | ||
|
|
6aaff63ca7 | ||
|
|
12c6b7abe5 | ||
|
|
42030e1e20 | ||
|
|
49bb17886a | ||
|
|
6bf36a915c | ||
|
|
05eca7cde6 | ||
|
|
a0596bbad3 | ||
|
|
c6254b7940 | ||
|
|
8d1cc49140 | ||
|
|
c78995fee2 | ||
|
|
bc3274c77e | ||
|
|
f20fff1443 | ||
|
|
4fd5040355 | ||
|
|
3aed93aeed | ||
|
|
7004c897f7 | ||
|
|
441b797f16 | ||
|
|
354f416559 | ||
|
|
6f524fdf27 | ||
|
|
78eb3a5f34 | ||
|
|
49c7129ded | ||
|
|
3ecac25a35 | ||
|
|
7e39e924e1 | ||
|
|
f2546f12d8 | ||
|
|
77f8a368a2 | ||
|
|
02a3abd892 | ||
|
|
6437ceb5dd | ||
|
|
481268b941 | ||
|
|
59cae5733d | ||
|
|
b1f9380c30 | ||
|
|
31e03ce99f | ||
|
|
e9852bfb22 | ||
|
|
d2ba8d3be3 | ||
|
|
608d9338a8 | ||
|
|
67d984c52c |
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
|
# You only need this environment variable set if you want to use Perplexity models
|
||||||
PERPLEXITY_API_KEY=
|
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
|
# Include this environment variable if you want more logging for debugging locally
|
||||||
VITE_LOG_LEVEL=debug
|
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: |
|
value: |
|
||||||
Thank you for reporting an issue :pray:.
|
Thank you for reporting an issue :pray:.
|
||||||
|
|
||||||
This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new).
|
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 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.
|
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.
|
The more information you fill in, the better we can help you.
|
||||||
- type: textarea
|
- 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. -->
|
||||||
6
.github/scripts/generate-changelog.sh
vendored
6
.github/scripts/generate-changelog.sh
vendored
@@ -137,7 +137,7 @@ while IFS= read -r commit_line; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
CATEGORIES["$CATEGORY"]=1
|
CATEGORIES["$CATEGORY"]=1
|
||||||
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by [@$GITHUB_USERNAME](https://github.com/$GITHUB_USERNAME)"$'\n'
|
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
|
||||||
else
|
else
|
||||||
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
|
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
|
||||||
fi
|
fi
|
||||||
@@ -165,7 +165,7 @@ while IFS= read -r commit_line; do
|
|||||||
CATEGORIES["$CATEGORY"]=1
|
CATEGORIES["$CATEGORY"]=1
|
||||||
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
|
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
|
||||||
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
|
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
|
||||||
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by [@$GITHUB_USERNAME](https://github.com/$GITHUB_USERNAME)"$'\n'
|
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
|
||||||
else
|
else
|
||||||
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
|
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
|
||||||
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
|
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
|
||||||
@@ -196,7 +196,7 @@ while IFS= read -r commit_line; do
|
|||||||
|
|
||||||
CATEGORIES["$CATEGORY"]=1
|
CATEGORIES["$CATEGORY"]=1
|
||||||
COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
|
COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
|
||||||
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by [@$GITHUB_USERNAME](https://github.com/$GITHUB_USERNAME)"$'\n'
|
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by @$GITHUB_USERNAME"$'\n'
|
||||||
else
|
else
|
||||||
# Fallback to git author name if no GitHub username found
|
# Fallback to git author name if no GitHub username found
|
||||||
AUTHOR_NAME=$(git show -s --format='%an' "$HASH")
|
AUTHOR_NAME=$(git show -s --format='%an' "$HASH")
|
||||||
|
|||||||
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
|
||||||
2
.github/workflows/update-stable.yml
vendored
2
.github/workflows/update-stable.yml
vendored
@@ -88,6 +88,8 @@ jobs:
|
|||||||
id: changelog
|
id: changelog
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
|
NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
run: .github/scripts/generate-changelog.sh
|
run: .github/scripts/generate-changelog.sh
|
||||||
|
|
||||||
- name: Get the latest commit hash and version tag
|
- name: Get the latest commit hash and version tag
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -25,8 +25,10 @@ ARG ANTHROPIC_API_KEY
|
|||||||
ARG OPEN_ROUTER_API_KEY
|
ARG OPEN_ROUTER_API_KEY
|
||||||
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
||||||
ARG OLLAMA_API_BASE_URL
|
ARG OLLAMA_API_BASE_URL
|
||||||
|
ARG XAI_API_KEY
|
||||||
ARG TOGETHER_API_KEY
|
ARG TOGETHER_API_KEY
|
||||||
ARG TOGETHER_API_BASE_URL
|
ARG TOGETHER_API_BASE_URL
|
||||||
|
ARG AWS_BEDROCK_CONFIG
|
||||||
ARG VITE_LOG_LEVEL=debug
|
ARG VITE_LOG_LEVEL=debug
|
||||||
ARG DEFAULT_NUM_CTX
|
ARG DEFAULT_NUM_CTX
|
||||||
|
|
||||||
@@ -38,16 +40,19 @@ ENV WRANGLER_SEND_METRICS=false \
|
|||||||
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||||
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
||||||
|
XAI_API_KEY=${XAI_API_KEY} \
|
||||||
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
||||||
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
||||||
|
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
|
||||||
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
|
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
|
# Pre-configure wrangler to disable metrics
|
||||||
RUN mkdir -p /root/.config/.wrangler && \
|
RUN mkdir -p /root/.config/.wrangler && \
|
||||||
echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
|
echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
|
||||||
|
|
||||||
RUN npm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
CMD [ "pnpm", "run", "dockerstart"]
|
CMD [ "pnpm", "run", "dockerstart"]
|
||||||
|
|
||||||
@@ -62,6 +67,7 @@ ARG ANTHROPIC_API_KEY
|
|||||||
ARG OPEN_ROUTER_API_KEY
|
ARG OPEN_ROUTER_API_KEY
|
||||||
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
ARG GOOGLE_GENERATIVE_AI_API_KEY
|
||||||
ARG OLLAMA_API_BASE_URL
|
ARG OLLAMA_API_BASE_URL
|
||||||
|
ARG XAI_API_KEY
|
||||||
ARG TOGETHER_API_KEY
|
ARG TOGETHER_API_KEY
|
||||||
ARG TOGETHER_API_BASE_URL
|
ARG TOGETHER_API_BASE_URL
|
||||||
ARG VITE_LOG_LEVEL=debug
|
ARG VITE_LOG_LEVEL=debug
|
||||||
@@ -74,10 +80,13 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \
|
|||||||
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||||
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
|
||||||
|
XAI_API_KEY=${XAI_API_KEY} \
|
||||||
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
|
||||||
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
|
||||||
|
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
|
||||||
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
|
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
|
RUN mkdir -p ${WORKDIR}/run
|
||||||
CMD pnpm run dev --host
|
CMD pnpm run dev --host
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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 🙌
|
||||||
23
README.md
23
README.md
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
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.
|
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/).
|
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 +27,15 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
|||||||
|
|
||||||
## Join the community
|
## 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
|
## Requested Additions
|
||||||
|
|
||||||
@@ -47,6 +58,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)
|
- ✅ Bolt terminal to see the output of LLM run commands (@thecodacus)
|
||||||
- ✅ Streaming of code output (@thecodacus)
|
- ✅ Streaming of code output (@thecodacus)
|
||||||
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
||||||
|
- ✅ Chat history backup and restore functionality (@sidbetatester)
|
||||||
- ✅ Cohere Integration (@hasanraiyan)
|
- ✅ Cohere Integration (@hasanraiyan)
|
||||||
- ✅ Dynamic model max token length (@hasanraiyan)
|
- ✅ Dynamic model max token length (@hasanraiyan)
|
||||||
- ✅ Better prompt enhancing (@SujalXplores)
|
- ✅ Better prompt enhancing (@SujalXplores)
|
||||||
@@ -64,6 +76,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 terminal Errors and ask bolt to fix it (@thecodacus)
|
||||||
- ✅ Detect preview Errors and ask bolt to fix it (@wonderwhy-er)
|
- ✅ Detect preview Errors and ask bolt to fix it (@wonderwhy-er)
|
||||||
- ✅ Add Starter Template Options (@thecodacus)
|
- ✅ 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** - 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** - 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
|
- ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
|
||||||
@@ -73,12 +87,13 @@ 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.
|
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
|
||||||
- ⬜ Voice prompting
|
- ⬜ Voice prompting
|
||||||
- ⬜ Azure Open AI API Integration
|
- ⬜ Azure Open AI API Integration
|
||||||
- ✅ Perplexity Integration (@meetpateltech)
|
|
||||||
- ⬜ Vertex AI Integration
|
- ⬜ Vertex AI Integration
|
||||||
|
- ⬜ Granite Integration
|
||||||
|
- ⬜ Popout Window for Web Container
|
||||||
|
|
||||||
## Features
|
## 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.
|
- **Support for multiple LLMs** with an extensible architecture to integrate additional models.
|
||||||
- **Attach images to prompts** for better contextual understanding.
|
- **Attach images to prompts** for better contextual understanding.
|
||||||
- **Integrated terminal** to view output of LLM-run commands.
|
- **Integrated terminal** to view output of LLM-run commands.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { IconButton } from '~/components/ui/IconButton';
|
import { IconButton } from '~/components/ui/IconButton';
|
||||||
import type { ProviderInfo } from '~/types/model';
|
import type { ProviderInfo } from '~/types/model';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
@@ -11,11 +11,14 @@ interface APIKeyManagerProps {
|
|||||||
labelForGetApiKey?: string;
|
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> } = {};
|
const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
|
||||||
|
|
||||||
export function getApiKeysFromCookies() {
|
export function getApiKeysFromCookies() {
|
||||||
const storedApiKeys = Cookies.get('apiKeys');
|
const storedApiKeys = Cookies.get('apiKeys');
|
||||||
let parsedKeys = {};
|
let parsedKeys: Record<string, string> = {};
|
||||||
|
|
||||||
if (storedApiKeys) {
|
if (storedApiKeys) {
|
||||||
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
|
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
|
||||||
@@ -32,54 +35,135 @@ export function getApiKeysFromCookies() {
|
|||||||
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
|
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [tempKey, setTempKey] = useState(apiKey);
|
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 = () => {
|
const handleSave = () => {
|
||||||
|
// Save to parent state
|
||||||
setApiKey(tempKey);
|
setApiKey(tempKey);
|
||||||
|
|
||||||
|
// Save to cookies
|
||||||
|
const currentKeys = getApiKeysFromCookies();
|
||||||
|
const newKeys = { ...currentKeys, [provider.name]: tempKey };
|
||||||
|
Cookies.set('apiKeys', JSON.stringify(newKeys));
|
||||||
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
|
<div className="flex items-center justify-between py-3 px-1">
|
||||||
<div>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center gap-2">
|
||||||
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
|
{apiKey ? (
|
||||||
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
|
<>
|
||||||
</span>
|
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
|
||||||
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
|
<span className="text-xs text-green-500">Set via UI</span>
|
||||||
<div className="i-ph:pencil-simple" />
|
</>
|
||||||
</IconButton>
|
) : 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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="flex items-center gap-3 mt-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={tempKey}
|
value={tempKey}
|
||||||
placeholder="Your API Key"
|
placeholder="Enter API Key"
|
||||||
onChange={(e) => setTempKey(e.target.value)}
|
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"
|
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">
|
<IconButton
|
||||||
<div className="i-ph:check" />
|
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>
|
||||||
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
<IconButton
|
||||||
<div className="i-ph:x" />
|
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>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{provider?.getApiKeyLink && (
|
{
|
||||||
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
<IconButton
|
||||||
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
onClick={() => setIsEditing(true)}
|
||||||
<div className={provider?.icon || 'i-ph:key'} />
|
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>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
* Preventing TS checks with files presented in the video for a better presentation.
|
* Preventing TS checks with files presented in the video for a better presentation.
|
||||||
*/
|
*/
|
||||||
import type { Message } from 'ai';
|
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 { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { Menu } from '~/components/sidebar/Menu.client';
|
import { Menu } from '~/components/sidebar/Menu.client';
|
||||||
import { IconButton } from '~/components/ui/IconButton';
|
import { IconButton } from '~/components/ui/IconButton';
|
||||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||||
import { classNames } from '~/utils/classNames';
|
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 { Messages } from './Messages.client';
|
||||||
import { SendButton } from './SendButton.client';
|
import { SendButton } from './SendButton.client';
|
||||||
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
|
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
|
||||||
@@ -25,13 +25,13 @@ import GitCloneButton from './GitCloneButton';
|
|||||||
import FilePreview from './FilePreview';
|
import FilePreview from './FilePreview';
|
||||||
import { ModelSelector } from '~/components/chat/ModelSelector';
|
import { ModelSelector } from '~/components/chat/ModelSelector';
|
||||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
import type { ProviderInfo } from '~/types/model';
|
||||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import StarterTemplates from './StarterTemplates';
|
import StarterTemplates from './StarterTemplates';
|
||||||
import type { ActionAlert } from '~/types/actions';
|
import type { ActionAlert } from '~/types/actions';
|
||||||
import ChatAlert from './ChatAlert';
|
import ChatAlert from './ChatAlert';
|
||||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||||
|
|
||||||
const TEXTAREA_MIN_HEIGHT = 76;
|
const TEXTAREA_MIN_HEIGHT = 76;
|
||||||
|
|
||||||
@@ -102,35 +102,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
) => {
|
) => {
|
||||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
|
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 [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
|
||||||
const [isListening, setIsListening] = useState(false);
|
const [isListening, setIsListening] = useState(false);
|
||||||
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
||||||
const [transcript, setTranscript] = useState('');
|
const [transcript, setTranscript] = useState('');
|
||||||
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
|
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(() => {
|
useEffect(() => {
|
||||||
console.log(transcript);
|
console.log(transcript);
|
||||||
}, [transcript]);
|
}, [transcript]);
|
||||||
@@ -169,7 +147,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const providerSettings = getProviderSettings();
|
|
||||||
let parsedApiKeys: Record<string, string> | undefined = {};
|
let parsedApiKeys: Record<string, string> | undefined = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -177,53 +154,48 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
setApiKeys(parsedApiKeys);
|
setApiKeys(parsedApiKeys);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading API keys from cookies:', error);
|
console.error('Error loading API keys from cookies:', error);
|
||||||
|
|
||||||
// Clear invalid cookie data
|
|
||||||
Cookies.remove('apiKeys');
|
Cookies.remove('apiKeys');
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsModelLoading('all');
|
setIsModelLoading('all');
|
||||||
initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
|
fetch('/api/models')
|
||||||
.then((modelList) => {
|
.then((response) => response.json())
|
||||||
// console.log('Model List: ', modelList);
|
.then((data) => {
|
||||||
setModelList(modelList);
|
const typedData = data as { modelList: ModelInfo[] };
|
||||||
|
setModelList(typedData.modelList);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error initializing model list:', error);
|
console.error('Error fetching model list:', error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsModelLoading(undefined);
|
setIsModelLoading(undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [providerList]);
|
}, [providerList, provider]);
|
||||||
|
|
||||||
const onApiKeysChange = async (providerName: string, apiKey: string) => {
|
const onApiKeysChange = async (providerName: string, apiKey: string) => {
|
||||||
const newApiKeys = { ...apiKeys, [providerName]: apiKey };
|
const newApiKeys = { ...apiKeys, [providerName]: apiKey };
|
||||||
setApiKeys(newApiKeys);
|
setApiKeys(newApiKeys);
|
||||||
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
|
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
|
||||||
|
|
||||||
const provider = LLMManager.getInstance(import.meta.env || process.env || {}).getProvider(providerName);
|
|
||||||
|
|
||||||
if (provider && provider.getDynamicModels) {
|
|
||||||
setIsModelLoading(providerName);
|
setIsModelLoading(providerName);
|
||||||
|
|
||||||
try {
|
let providerModels: ModelInfo[] = [];
|
||||||
const providerSettings = getProviderSettings();
|
|
||||||
const staticModels = provider.staticModels;
|
|
||||||
const dynamicModels = await provider.getDynamicModels(
|
|
||||||
newApiKeys,
|
|
||||||
providerSettings,
|
|
||||||
import.meta.env || process.env || {},
|
|
||||||
);
|
|
||||||
|
|
||||||
setModelList((preModels) => {
|
try {
|
||||||
const filteredOutPreModels = preModels.filter((x) => x.provider !== providerName);
|
const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);
|
||||||
return [...filteredOutPreModels, ...staticModels, ...dynamicModels];
|
const data = await response.json();
|
||||||
});
|
providerModels = (data as { modelList: ModelInfo[] }).modelList;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading dynamic models:', 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);
|
setIsModelLoading(undefined);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startListening = () => {
|
const startListening = () => {
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ export const ChatImpl = memo(
|
|||||||
|
|
||||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
|
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
|
||||||
|
useChat({
|
||||||
api: '/api/chat',
|
api: '/api/chat',
|
||||||
body: {
|
body: {
|
||||||
apiKeys,
|
apiKeys,
|
||||||
@@ -146,10 +147,10 @@ export const ChatImpl = memo(
|
|||||||
contextOptimization: contextOptimizationEnabled,
|
contextOptimization: contextOptimizationEnabled,
|
||||||
},
|
},
|
||||||
sendExtraMessageFields: true,
|
sendExtraMessageFields: true,
|
||||||
onError: (error) => {
|
onError: (e) => {
|
||||||
logger.error('Request failed\n\n', error);
|
logger.error('Request failed\n\n', e, error);
|
||||||
toast.error(
|
toast.error(
|
||||||
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
|
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onFinish: (message, response) => {
|
onFinish: (message, response) => {
|
||||||
@@ -263,6 +264,10 @@ export const ChatImpl = memo(
|
|||||||
*/
|
*/
|
||||||
await workbenchStore.saveAllFiles();
|
await workbenchStore.saveAllFiles();
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
setMessages(messages.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
const fileModifications = workbenchStore.getFileModifcations();
|
const fileModifications = workbenchStore.getFileModifcations();
|
||||||
|
|
||||||
chatStore.setKey('aborted', false);
|
chatStore.setKey('aborted', false);
|
||||||
@@ -297,7 +302,15 @@ export const ChatImpl = memo(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (template !== 'blank') {
|
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) {
|
if (temResp) {
|
||||||
const { assistantMessage, userMessage } = temResp;
|
const { assistantMessage, userMessage } = temResp;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
|
|||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
||||||
import { generateId } from '~/utils/fileUtils';
|
import { generateId } from '~/utils/fileUtils';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
||||||
|
|
||||||
const IGNORE_PATTERNS = [
|
const IGNORE_PATTERNS = [
|
||||||
'node_modules/**',
|
'node_modules/**',
|
||||||
@@ -37,6 +40,8 @@ interface GitCloneButtonProps {
|
|||||||
|
|
||||||
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||||
const { ready, gitClone } = useGit();
|
const { ready, gitClone } = useGit();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const onClick = async (_e: any) => {
|
const onClick = async (_e: any) => {
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return;
|
return;
|
||||||
@@ -45,6 +50,9 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
|||||||
const repoUrl = prompt('Enter the Git url');
|
const repoUrl = prompt('Enter the Git url');
|
||||||
|
|
||||||
if (repoUrl) {
|
if (repoUrl) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
const { workdir, data } = await gitClone(repoUrl);
|
const { workdir, data } = await gitClone(repoUrl);
|
||||||
|
|
||||||
if (importChat) {
|
if (importChat) {
|
||||||
@@ -53,22 +61,20 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
|||||||
|
|
||||||
const textDecoder = new TextDecoder('utf-8');
|
const textDecoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
// Convert files to common format for command detection
|
|
||||||
const fileContents = filePaths
|
const fileContents = filePaths
|
||||||
.map((filePath) => {
|
.map((filePath) => {
|
||||||
const { data: content, encoding } = data[filePath];
|
const { data: content, encoding } = data[filePath];
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
|
content:
|
||||||
|
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((f) => f.content);
|
.filter((f) => f.content);
|
||||||
|
|
||||||
// Detect and create commands message
|
|
||||||
const commands = await detectProjectCommands(fileContents);
|
const commands = await detectProjectCommands(fileContents);
|
||||||
const commandsMessage = createCommandsMessage(commands);
|
const commandsMessage = createCommandsMessage(commands);
|
||||||
|
|
||||||
// Create files message
|
|
||||||
const filesMessage: Message = {
|
const filesMessage: Message = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||||
@@ -94,10 +100,17 @@ ${file.content}
|
|||||||
|
|
||||||
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 (
|
return (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title="Clone a Git Repo"
|
title="Clone a Git Repo"
|
||||||
@@ -106,5 +119,7 @@ ${file.content}
|
|||||||
<span className="i-ph:git-branch" />
|
<span className="i-ph:git-branch" />
|
||||||
Clone a Git Repo
|
Clone a Git Repo
|
||||||
</button>
|
</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 { toast } from 'react-toastify';
|
||||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
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) {
|
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center w-auto">
|
<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) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
const content = e.target?.result as string;
|
const content = e.target?.result as string;
|
||||||
const data = JSON.parse(content);
|
const data = JSON.parse(content) as ChatData;
|
||||||
|
|
||||||
if (!Array.isArray(data.messages)) {
|
// Standard format
|
||||||
toast.error('Invalid chat file 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.error('Invalid chat file format');
|
||||||
toast.success('Chat imported successfully');
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast.error('Failed to parse chat file: ' + error.message);
|
toast.error('Failed to parse chat file: ' + error.message);
|
||||||
|
|||||||
@@ -49,29 +49,28 @@ export function GitUrlImport() {
|
|||||||
|
|
||||||
if (repoUrl) {
|
if (repoUrl) {
|
||||||
const ig = ignore().add(IGNORE_PATTERNS);
|
const ig = ignore().add(IGNORE_PATTERNS);
|
||||||
|
|
||||||
|
try {
|
||||||
const { workdir, data } = await gitClone(repoUrl);
|
const { workdir, data } = await gitClone(repoUrl);
|
||||||
|
|
||||||
if (importChat) {
|
if (importChat) {
|
||||||
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
||||||
|
|
||||||
const textDecoder = new TextDecoder('utf-8');
|
const textDecoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
// Convert files to common format for command detection
|
|
||||||
const fileContents = filePaths
|
const fileContents = filePaths
|
||||||
.map((filePath) => {
|
.map((filePath) => {
|
||||||
const { data: content, encoding } = data[filePath];
|
const { data: content, encoding } = data[filePath];
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
|
content:
|
||||||
|
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((f) => f.content);
|
.filter((f) => f.content);
|
||||||
|
|
||||||
// Detect and create commands message
|
|
||||||
const commands = await detectProjectCommands(fileContents);
|
const commands = await detectProjectCommands(fileContents);
|
||||||
const commandsMessage = createCommandsMessage(commands);
|
const commandsMessage = createCommandsMessage(commands);
|
||||||
|
|
||||||
// Create files message
|
|
||||||
const filesMessage: Message = {
|
const filesMessage: Message = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||||
@@ -97,6 +96,14 @@ ${file.content}
|
|||||||
|
|
||||||
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');
|
||||||
|
setLoading(false);
|
||||||
|
window.location.href = '/';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import React, { useState } from 'react';
|
|||||||
import { useNavigate } from '@remix-run/react';
|
import { useNavigate } from '@remix-run/react';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { toast } from 'react-toastify';
|
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 { logStore } from '~/lib/stores/logs';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import type { Message } from 'ai';
|
||||||
|
|
||||||
// List of supported providers that can have API keys
|
// List of supported providers that can have API keys
|
||||||
const API_KEY_PROVIDERS = [
|
const API_KEY_PROVIDERS = [
|
||||||
@@ -22,6 +23,7 @@ const API_KEY_PROVIDERS = [
|
|||||||
'Perplexity',
|
'Perplexity',
|
||||||
'Cohere',
|
'Cohere',
|
||||||
'AzureOpenAI',
|
'AzureOpenAI',
|
||||||
|
'AmazonBedrock',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
interface ApiKeys {
|
interface ApiKeys {
|
||||||
@@ -231,6 +233,81 @@ export default function DataTab() {
|
|||||||
event.target.value = '';
|
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 (
|
return (
|
||||||
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
|
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -247,6 +324,12 @@ export default function DataTab() {
|
|||||||
>
|
>
|
||||||
Export All Chats
|
Export All Chats
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={handleDeleteAllChats}
|
onClick={handleDeleteAllChats}
|
||||||
disabled={isDeleting}
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
|
<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="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
|
||||||
<div
|
<div
|
||||||
className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
|
className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
|
||||||
style={{ fontSize: '2rem' }}
|
style={{ fontSize: '2rem' }}
|
||||||
></div>
|
></div>
|
||||||
<p className="text-lg text-bolt-elements-textTertiary">{message}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { IconButton } from '~/components/ui/IconButton';
|
import { IconButton } from '~/components/ui/IconButton';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { PortDropdown } from './PortDropdown';
|
import { PortDropdown } from './PortDropdown';
|
||||||
@@ -7,6 +7,19 @@ import { ScreenshotSelector } from './ScreenshotSelector';
|
|||||||
|
|
||||||
type ResizeSide = 'left' | 'right' | null;
|
type ResizeSide = 'left' | 'right' | null;
|
||||||
|
|
||||||
|
interface WindowSize {
|
||||||
|
name: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOW_SIZES: WindowSize[] = [
|
||||||
|
{ name: 'Mobile (375x667)', width: 375, height: 667 },
|
||||||
|
{ name: 'Tablet (768x1024)', width: 768, height: 1024 },
|
||||||
|
{ name: 'Laptop (1366x768)', width: 1366, height: 768 },
|
||||||
|
{ name: 'Desktop (1920x1080)', width: 1920, height: 1080 },
|
||||||
|
];
|
||||||
|
|
||||||
export const Preview = memo(() => {
|
export const Preview = memo(() => {
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -15,6 +28,7 @@ export const Preview = memo(() => {
|
|||||||
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
|
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
|
||||||
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
|
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
|
||||||
const hasSelectedPreview = useRef(false);
|
const hasSelectedPreview = useRef(false);
|
||||||
const previews = useStore(workbenchStore.previews);
|
const previews = useStore(workbenchStore.previews);
|
||||||
const activePreview = previews[activePreviewIndex];
|
const activePreview = previews[activePreviewIndex];
|
||||||
@@ -27,7 +41,7 @@ export const Preview = memo(() => {
|
|||||||
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
|
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
|
||||||
|
|
||||||
// Use percentage for width
|
// 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({
|
const resizingState = useRef({
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
@@ -37,8 +51,10 @@ export const Preview = memo(() => {
|
|||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the scaling factor
|
const SCALING_FACTOR = 2;
|
||||||
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
|
|
||||||
|
const [isWindowSizeDropdownOpen, setIsWindowSizeDropdownOpen] = useState(false);
|
||||||
|
const [selectedWindowSize, setSelectedWindowSize] = useState<WindowSize>(WINDOW_SIZES[0]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activePreview) {
|
if (!activePreview) {
|
||||||
@@ -79,7 +95,6 @@ export const Preview = memo(() => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// When previews change, display the lowest port if user hasn't selected a preview
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previews.length > 1 && !hasSelectedPreview.current) {
|
if (previews.length > 1 && !hasSelectedPreview.current) {
|
||||||
const minPortIndex = previews.reduce(findMinPortIndex, 0);
|
const minPortIndex = previews.reduce(findMinPortIndex, 0);
|
||||||
@@ -122,7 +137,6 @@ export const Preview = memo(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent text selection
|
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
resizingState.current.isResizing = true;
|
resizingState.current.isResizing = true;
|
||||||
@@ -134,7 +148,7 @@ export const Preview = memo(() => {
|
|||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
e.preventDefault(); // Prevent any text selection on mousedown
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
@@ -145,7 +159,6 @@ export const Preview = memo(() => {
|
|||||||
const dx = e.clientX - resizingState.current.startX;
|
const dx = e.clientX - resizingState.current.startX;
|
||||||
const windowWidth = resizingState.current.windowWidth;
|
const windowWidth = resizingState.current.windowWidth;
|
||||||
|
|
||||||
// Apply scaling factor to increase sensitivity
|
|
||||||
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
|
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
|
||||||
|
|
||||||
let newWidthPercent = resizingState.current.startWidthPercent;
|
let newWidthPercent = resizingState.current.startWidthPercent;
|
||||||
@@ -156,7 +169,6 @@ export const Preview = memo(() => {
|
|||||||
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
|
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp the width between 10% and 90%
|
|
||||||
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
|
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
|
||||||
|
|
||||||
setWidthPercent(newWidthPercent);
|
setWidthPercent(newWidthPercent);
|
||||||
@@ -168,17 +180,12 @@ export const Preview = memo(() => {
|
|||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
// Restore text selection
|
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle window resize to ensure widthPercent remains valid
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWindowResize = () => {
|
const handleWindowResize = () => {
|
||||||
/*
|
// Optional: Adjust widthPercent if necessary
|
||||||
* Optional: Adjust widthPercent if necessary
|
|
||||||
* For now, since widthPercent is relative, no action is needed
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
window.addEventListener('resize', handleWindowResize);
|
||||||
@@ -188,7 +195,6 @@ export const Preview = memo(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// A small helper component for the handle's "grip" icon
|
|
||||||
const GripIcon = () => (
|
const GripIcon = () => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -213,8 +219,33 @@ export const Preview = memo(() => {
|
|||||||
</div>
|
</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 (
|
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 && (
|
{isPortDropdownOpen && (
|
||||||
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
|
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
|
||||||
)}
|
)}
|
||||||
@@ -225,10 +256,7 @@ export const Preview = memo(() => {
|
|||||||
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
||||||
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
|
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
|
||||||
/>
|
/>
|
||||||
<div
|
<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">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
title="URL"
|
title="URL"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -261,26 +289,65 @@ export const Preview = memo(() => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Device mode toggle button */}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="i-ph:devices"
|
icon="i-ph:devices"
|
||||||
onClick={toggleDeviceMode}
|
onClick={toggleDeviceMode}
|
||||||
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Fullscreen toggle button */}
|
<IconButton
|
||||||
|
icon="i-ph:layout-light"
|
||||||
|
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
|
||||||
|
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
|
||||||
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="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-1 z-50 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg border border-bolt-elements-borderColor overflow-hidden">
|
||||||
|
{WINDOW_SIZES.map((size) => (
|
||||||
|
<button
|
||||||
|
key={size.name}
|
||||||
|
className="w-full px-4 py-2 text-left hover:bg-bolt-elements-background-depth-3 text-sm whitespace-nowrap"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedWindowSize(size);
|
||||||
|
setIsWindowSizeDropdownOpen(false);
|
||||||
|
openInNewWindow(size);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{size.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
|
<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
|
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
|
||||||
height: '100%', // Always full height
|
height: '100%',
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -294,7 +361,8 @@ export const Preview = memo(() => {
|
|||||||
title="preview"
|
title="preview"
|
||||||
className="border-none w-full h-full bg-white"
|
className="border-none w-full h-full bg-white"
|
||||||
src={iframeUrl}
|
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
|
<ScreenshotSelector
|
||||||
isSelectionMode={isSelectionMode}
|
isSelectionMode={isSelectionMode}
|
||||||
@@ -308,7 +376,6 @@ export const Preview = memo(() => {
|
|||||||
|
|
||||||
{isDeviceModeOn && (
|
{isDeviceModeOn && (
|
||||||
<>
|
<>
|
||||||
{/* Left handle */}
|
|
||||||
<div
|
<div
|
||||||
onMouseDown={(e) => startResizing(e, 'left')}
|
onMouseDown={(e) => startResizing(e, 'left')}
|
||||||
style={{
|
style={{
|
||||||
@@ -333,7 +400,6 @@ export const Preview = memo(() => {
|
|||||||
<GripIcon />
|
<GripIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right handle */}
|
|
||||||
<div
|
<div
|
||||||
onMouseDown={(e) => startResizing(e, 'right')}
|
onMouseDown={(e) => startResizing(e, 'right')}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -226,9 +226,9 @@ export async function streamText(props: {
|
|||||||
|
|
||||||
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
|
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
|
||||||
|
|
||||||
return _streamText({
|
return await _streamText({
|
||||||
model: provider.getModelInstance({
|
model: provider.getModelInstance({
|
||||||
model: currentModel,
|
model: modelDetails.name,
|
||||||
serverEnv,
|
serverEnv,
|
||||||
apiKeys,
|
apiKeys,
|
||||||
providerSettings,
|
providerSettings,
|
||||||
|
|||||||
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) : {};
|
||||||
|
}
|
||||||
@@ -3,10 +3,10 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import {
|
import {
|
||||||
chatId as chatIdStore,
|
chatId as chatIdStore,
|
||||||
description as descriptionStore,
|
|
||||||
db,
|
db,
|
||||||
updateChatDescription,
|
description as descriptionStore,
|
||||||
getMessages,
|
getMessages,
|
||||||
|
updateChatDescription,
|
||||||
} from '~/lib/persistence';
|
} from '~/lib/persistence';
|
||||||
|
|
||||||
interface EditChatDescriptionOptions {
|
interface EditChatDescriptionOptions {
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ export function useGit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileData.current = {};
|
fileData.current = {};
|
||||||
|
|
||||||
|
const headers: {
|
||||||
|
[x: string]: string;
|
||||||
|
} = {
|
||||||
|
'User-Agent': 'bolt.diy',
|
||||||
|
};
|
||||||
|
|
||||||
|
const auth = lookupSavedPassword(url);
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
headers.Authorization = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await git.clone({
|
await git.clone({
|
||||||
fs,
|
fs,
|
||||||
http,
|
http,
|
||||||
@@ -56,10 +70,10 @@ export function useGit() {
|
|||||||
url,
|
url,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
singleBranch: true,
|
singleBranch: true,
|
||||||
corsProxy: 'https://cors.isomorphic-git.org',
|
corsProxy: '/api/git-proxy',
|
||||||
onAuth: (url) => {
|
headers,
|
||||||
// let domain=url.split("/")[2]
|
|
||||||
|
|
||||||
|
onAuth: (url) => {
|
||||||
let auth = lookupSavedPassword(url);
|
let auth = lookupSavedPassword(url);
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
@@ -91,8 +105,12 @@ export function useGit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { workdir: webcontainer.workdir, data };
|
return { workdir: webcontainer.workdir, data };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Git clone error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[webcontainer],
|
[webcontainer, fs, ready],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ready, gitClone };
|
return { ready, gitClone };
|
||||||
@@ -104,55 +122,86 @@ const getFs = (
|
|||||||
) => ({
|
) => ({
|
||||||
promises: {
|
promises: {
|
||||||
readFile: async (path: string, options: any) => {
|
readFile: async (path: string, options: any) => {
|
||||||
const encoding = options.encoding;
|
const encoding = options?.encoding;
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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) => {
|
writeFile: async (path: string, data: any, options: any) => {
|
||||||
const encoding = options.encoding;
|
const encoding = options.encoding;
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||||
console.log('writeFile', { relativePath, data, encoding });
|
|
||||||
|
|
||||||
if (record.current) {
|
if (record.current) {
|
||||||
record.current[relativePath] = { data, encoding };
|
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) => {
|
mkdir: async (path: string, options: any) => {
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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) => {
|
readdir: async (path: string, options: any) => {
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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) => {
|
rm: async (path: string, options: any) => {
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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) => {
|
rmdir: async (path: string, options: any) => {
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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: async (path: string) => {
|
||||||
// unlink is just removing a single file
|
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
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) => {
|
stat: async (path: string) => {
|
||||||
try {
|
try {
|
||||||
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
const relativePath = pathUtils.relative(webcontainer.workdir, path);
|
||||||
@@ -185,23 +234,12 @@ const getFs = (
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
lstat: async (path: string) => {
|
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);
|
return await getFs(webcontainer, record).promises.stat(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
readlink: async (path: string) => {
|
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}'`);
|
throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
|
||||||
},
|
},
|
||||||
|
|
||||||
symlink: async (target: string, path: string) => {
|
symlink: async (target: string, path: string) => {
|
||||||
/*
|
/*
|
||||||
* Since WebContainer doesn't support symlinks,
|
* Since WebContainer doesn't support symlinks,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export abstract class BaseProvider implements ProviderInfo {
|
|||||||
|
|
||||||
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
|
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
|
||||||
const apiKey =
|
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 {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class LLMManager {
|
|||||||
|
|
||||||
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/lib/modules/llm/providers/github.ts
Normal file
52
app/lib/modules/llm/providers/github.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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 {
|
export default class HyperbolicProvider extends BaseProvider {
|
||||||
name = 'Hyperbolic';
|
name = 'Hyperbolic';
|
||||||
getApiKeyLink = 'https://hyperbolic.xyz/settings';
|
getApiKeyLink = 'https://app.hyperbolic.xyz/settings';
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
apiTokenKey: 'HYPERBOLIC_API_KEY',
|
apiTokenKey: 'HYPERBOLIC_API_KEY',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
|
|||||||
import type { IProviderSetting } from '~/types/model';
|
import type { IProviderSetting } from '~/types/model';
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
import type { LanguageModelV1 } from 'ai';
|
import type { LanguageModelV1 } from 'ai';
|
||||||
|
import { logger } from '~/utils/logger';
|
||||||
|
|
||||||
export default class LMStudioProvider extends BaseProvider {
|
export default class LMStudioProvider extends BaseProvider {
|
||||||
name = 'LMStudio';
|
name = 'LMStudio';
|
||||||
@@ -22,7 +23,7 @@ export default class LMStudioProvider extends BaseProvider {
|
|||||||
settings?: IProviderSetting,
|
settings?: IProviderSetting,
|
||||||
serverEnv: Record<string, string> = {},
|
serverEnv: Record<string, string> = {},
|
||||||
): Promise<ModelInfo[]> {
|
): Promise<ModelInfo[]> {
|
||||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||||
apiKeys,
|
apiKeys,
|
||||||
providerSettings: settings,
|
providerSettings: settings,
|
||||||
serverEnv,
|
serverEnv,
|
||||||
@@ -31,7 +32,18 @@ export default class LMStudioProvider extends BaseProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!baseUrl) {
|
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`);
|
const response = await fetch(`${baseUrl}/v1/models`);
|
||||||
@@ -51,13 +63,26 @@ export default class LMStudioProvider extends BaseProvider {
|
|||||||
providerSettings?: Record<string, IProviderSetting>;
|
providerSettings?: Record<string, IProviderSetting>;
|
||||||
}) => LanguageModelV1 = (options) => {
|
}) => LanguageModelV1 = (options) => {
|
||||||
const { apiKeys, providerSettings, serverEnv, model } = options;
|
const { apiKeys, providerSettings, serverEnv, model } = options;
|
||||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||||
apiKeys,
|
apiKeys,
|
||||||
providerSettings,
|
providerSettings: providerSettings?.[this.name],
|
||||||
serverEnv: serverEnv as any,
|
serverEnv: serverEnv as any,
|
||||||
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
|
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
|
||||||
defaultApiTokenKey: '',
|
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({
|
const lmstudio = createOpenAI({
|
||||||
baseUrl: `${baseUrl}/v1`,
|
baseUrl: `${baseUrl}/v1`,
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
|
|||||||
import type { IProviderSetting } from '~/types/model';
|
import type { IProviderSetting } from '~/types/model';
|
||||||
import type { LanguageModelV1 } from 'ai';
|
import type { LanguageModelV1 } from 'ai';
|
||||||
import { ollama } from 'ollama-ai-provider';
|
import { ollama } from 'ollama-ai-provider';
|
||||||
|
import { logger } from '~/utils/logger';
|
||||||
|
|
||||||
interface OllamaModelDetails {
|
interface OllamaModelDetails {
|
||||||
parent_model: string;
|
parent_model: string;
|
||||||
@@ -45,7 +46,7 @@ export default class OllamaProvider extends BaseProvider {
|
|||||||
settings?: IProviderSetting,
|
settings?: IProviderSetting,
|
||||||
serverEnv: Record<string, string> = {},
|
serverEnv: Record<string, string> = {},
|
||||||
): Promise<ModelInfo[]> {
|
): Promise<ModelInfo[]> {
|
||||||
const { baseUrl } = this.getProviderBaseUrlAndKey({
|
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||||
apiKeys,
|
apiKeys,
|
||||||
providerSettings: settings,
|
providerSettings: settings,
|
||||||
serverEnv,
|
serverEnv,
|
||||||
@@ -54,7 +55,18 @@ export default class OllamaProvider extends BaseProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!baseUrl) {
|
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`);
|
const response = await fetch(`${baseUrl}/api/tags`);
|
||||||
@@ -78,18 +90,23 @@ export default class OllamaProvider extends BaseProvider {
|
|||||||
const { apiKeys, providerSettings, serverEnv, model } = options;
|
const { apiKeys, providerSettings, serverEnv, model } = options;
|
||||||
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
let { baseUrl } = this.getProviderBaseUrlAndKey({
|
||||||
apiKeys,
|
apiKeys,
|
||||||
providerSettings,
|
providerSettings: providerSettings?.[this.name],
|
||||||
serverEnv: serverEnv as any,
|
serverEnv: serverEnv as any,
|
||||||
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
|
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
|
||||||
defaultApiTokenKey: '',
|
defaultApiTokenKey: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backend: Check if we're running in Docker
|
// 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('localhost', 'host.docker.internal') : baseUrl;
|
||||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', '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, {
|
const ollamaInstance = ollama(model, {
|
||||||
numCtx: DEFAULT_NUM_CTX,
|
numCtx: DEFAULT_NUM_CTX,
|
||||||
}) as LanguageModelV1 & { config: any };
|
}) as LanguageModelV1 & { config: any };
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import PerplexityProvider from './providers/perplexity';
|
|||||||
import TogetherProvider from './providers/together';
|
import TogetherProvider from './providers/together';
|
||||||
import XAIProvider from './providers/xai';
|
import XAIProvider from './providers/xai';
|
||||||
import HyperbolicProvider from './providers/hyperbolic';
|
import HyperbolicProvider from './providers/hyperbolic';
|
||||||
|
import AmazonBedrockProvider from './providers/amazon-bedrock';
|
||||||
|
import GithubProvider from './providers/github';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AnthropicProvider,
|
AnthropicProvider,
|
||||||
@@ -32,4 +34,6 @@ export {
|
|||||||
XAIProvider,
|
XAIProvider,
|
||||||
TogetherProvider,
|
TogetherProvider,
|
||||||
LMStudioProvider,
|
LMStudioProvider,
|
||||||
|
AmazonBedrockProvider,
|
||||||
|
GithubProvider,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,192 @@
|
|||||||
import type { WebContainer } from '@webcontainer/api';
|
import type { WebContainer } from '@webcontainer/api';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
|
|
||||||
|
// Extend Window interface to include our custom property
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
_tabId?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface PreviewInfo {
|
export interface PreviewInfo {
|
||||||
port: number;
|
port: number;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a broadcast channel for preview updates
|
||||||
|
const PREVIEW_CHANNEL = 'preview-updates';
|
||||||
|
|
||||||
export class PreviewsStore {
|
export class PreviewsStore {
|
||||||
#availablePreviews = new Map<number, PreviewInfo>();
|
#availablePreviews = new Map<number, PreviewInfo>();
|
||||||
#webcontainer: Promise<WebContainer>;
|
#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[]>([]);
|
previews = atom<PreviewInfo[]>([]);
|
||||||
|
|
||||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||||
this.#webcontainer = webcontainerPromise;
|
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();
|
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() {
|
async #init() {
|
||||||
const webcontainer = await this.#webcontainer;
|
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) => {
|
webcontainer.on('port', (port, type, url) => {
|
||||||
let previewInfo = this.#availablePreviews.get(port);
|
let previewInfo = this.#availablePreviews.get(port);
|
||||||
|
|
||||||
@@ -44,6 +209,101 @@ export class PreviewsStore {
|
|||||||
previewInfo.baseUrl = url;
|
previewInfo.baseUrl = url;
|
||||||
|
|
||||||
this.previews.set([...previews]);
|
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()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return WebContainer.boot({
|
return WebContainer.boot({
|
||||||
|
coep: 'credentialless',
|
||||||
workdirName: WORK_DIR_NAME,
|
workdirName: WORK_DIR_NAME,
|
||||||
forwardPreviewErrors: true, // Enable error forwarding from iframes
|
forwardPreviewErrors: true, // Enable error forwarding from iframes
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
|
||||||
|
logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`);
|
||||||
|
|
||||||
const result = await streamText({
|
const result = await streamText({
|
||||||
messages,
|
messages,
|
||||||
@@ -134,12 +136,27 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
contextOptimization,
|
contextOptimization,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for await (const part of result.fullStream) {
|
||||||
|
if (part.type === 'error') {
|
||||||
|
const error: any = part.error;
|
||||||
|
logger.error(`${error}`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
stream.switchSource(result.toDataStream());
|
stream.switchSource(result.toDataStream());
|
||||||
|
|
||||||
|
// return createrespo
|
||||||
return new Response(stream.readable, {
|
return new Response(stream.readable, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
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) {
|
} 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 { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||||
|
|
||||||
//import { StreamingTextResponse, parseStreamPart } from 'ai';
|
|
||||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
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) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
return enhancerAction(args);
|
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) {
|
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
||||||
const { message, model, provider } = await request.json<{
|
const { message, model, provider } = await request.json<{
|
||||||
message: string;
|
message: string;
|
||||||
@@ -55,12 +34,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cookieHeader = request.headers.get('Cookie');
|
const cookieHeader = request.headers.get('Cookie');
|
||||||
|
const apiKeys = getApiKeysFromCookie(cookieHeader);
|
||||||
// Parse the cookie's value (returns an object or null if no cookie exists)
|
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
|
||||||
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
|
|
||||||
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
|
|
||||||
parseCookies(cookieHeader || '').providers || '{}',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await streamText({
|
const result = await streamText({
|
||||||
@@ -107,7 +82,10 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
|||||||
return new Response(result.textStream, {
|
return new Response(result.textStream, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
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) {
|
} 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 { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||||
|
|
||||||
//import { StreamingTextResponse, parseStreamPart } from 'ai';
|
|
||||||
import { streamText } from '~/lib/.server/llm/stream-text';
|
import { streamText } from '~/lib/.server/llm/stream-text';
|
||||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||||
import { generateText } from 'ai';
|
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 { 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) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
return llmCallAction(args);
|
return llmCallAction(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCookies(cookieHeader: string) {
|
async function getModelList(options: {
|
||||||
const cookies: any = {};
|
apiKeys?: Record<string, string>;
|
||||||
|
providerSettings?: Record<string, IProviderSetting>;
|
||||||
// Split the cookie string by semicolons and spaces
|
serverEnv?: Record<string, string>;
|
||||||
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
|
}) {
|
||||||
|
const llmManager = LLMManager.getInstance(import.meta.env);
|
||||||
items.forEach((item) => {
|
return llmManager.updateModelList(options);
|
||||||
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 llmCallAction({ context, request }: ActionFunctionArgs) {
|
async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||||
@@ -58,12 +48,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cookieHeader = request.headers.get('Cookie');
|
const cookieHeader = request.headers.get('Cookie');
|
||||||
|
const apiKeys = getApiKeysFromCookie(cookieHeader);
|
||||||
// Parse the cookie's value (returns an object or null if no cookie exists)
|
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
|
||||||
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
|
|
||||||
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
|
|
||||||
parseCookies(cookieHeader || '').providers || '{}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (streamOutput) {
|
if (streamOutput) {
|
||||||
try {
|
try {
|
||||||
@@ -105,8 +91,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const MODEL_LIST = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
|
const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
|
||||||
const modelDetails = MODEL_LIST.find((m) => m.name === model);
|
const modelDetails = models.find((m: ModelInfo) => m.name === model);
|
||||||
|
|
||||||
if (!modelDetails) {
|
if (!modelDetails) {
|
||||||
throw new Error('Model not found');
|
throw new Error('Model not found');
|
||||||
|
|||||||
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 { 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() {
|
interface ModelsResponse {
|
||||||
return json(MODEL_LIST);
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import type { IProviderSetting } from '~/types/model';
|
|
||||||
|
|
||||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
|
||||||
import type { Template } from '~/types/template';
|
import type { Template } from '~/types/template';
|
||||||
|
|
||||||
export const WORK_DIR_NAME = 'project';
|
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 PROVIDER_LIST = llmManager.getAllProviders();
|
||||||
export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
|
export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
|
||||||
|
|
||||||
let MODEL_LIST = llmManager.getModelList();
|
export const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
|
||||||
|
|
||||||
const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
|
|
||||||
PROVIDER_LIST.forEach((provider) => {
|
PROVIDER_LIST.forEach((provider) => {
|
||||||
providerBaseUrlEnvKeys[provider.name] = {
|
providerBaseUrlEnvKeys[provider.name] = {
|
||||||
baseUrlKey: provider.config.baseUrlKey,
|
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
|
// starter Templates
|
||||||
|
|
||||||
export const STARTER_TEMPLATES: Template[] = [
|
export const STARTER_TEMPLATES: Template[] = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ignore from 'ignore';
|
|||||||
import type { ProviderInfo } from '~/types/model';
|
import type { ProviderInfo } from '~/types/model';
|
||||||
import type { Template } from '~/types/template';
|
import type { Template } from '~/types/template';
|
||||||
import { STARTER_TEMPLATES } from './constants';
|
import { STARTER_TEMPLATES } from './constants';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
const starterTemplateSelectionPrompt = (templates: Template[]) => `
|
const starterTemplateSelectionPrompt = (templates: Template[]) => `
|
||||||
You are an experienced developer who helps people choose the best starter template for their projects.
|
You are an experienced developer who helps people choose the best starter template for their projects.
|
||||||
@@ -116,14 +117,20 @@ const getGitHubRepoContent = async (
|
|||||||
const baseUrl = 'https://api.github.com';
|
const baseUrl = 'https://api.github.com';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch contents of the path
|
const token = Cookies.get('githubToken') || import.meta.env.VITE_GITHUB_ACCESS_TOKEN;
|
||||||
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
|
|
||||||
headers: {
|
const headers: HeadersInit = {
|
||||||
Accept: 'application/vnd.github.v3+json',
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
};
|
||||||
|
|
||||||
// Add your GitHub token if needed
|
// Add your GitHub token if needed
|
||||||
Authorization: 'token ' + import.meta.env.VITE_GITHUB_ACCESS_TOKEN,
|
if (token) {
|
||||||
},
|
headers.Authorization = 'token ' + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch contents of the path
|
||||||
|
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -156,10 +163,7 @@ const getGitHubRepoContent = async (
|
|||||||
} else if (item.type === 'file') {
|
} else if (item.type === 'file') {
|
||||||
// Fetch file content
|
// Fetch file content
|
||||||
const fileResponse = await fetch(item.url, {
|
const fileResponse = await fetch(item.url, {
|
||||||
headers: {
|
headers,
|
||||||
Accept: 'application/vnd.github.v3+json',
|
|
||||||
Authorization: 'token ' + import.meta.env.VITE_GITHUB_ACCESS_TOKEN,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const fileData: any = await fileResponse.json();
|
const fileData: any = await fileResponse.json();
|
||||||
const content = atob(fileData.content); // Decode base64 content
|
const content = atob(fileData.content); // Decode base64 content
|
||||||
|
|||||||
62
changelog.md
62
changelog.md
@@ -1,68 +1,14 @@
|
|||||||
# 🚀 Release v0.0.4
|
# 🚀 Release v0.0.5
|
||||||
|
|
||||||
## What's Changed 🌟
|
## What's Changed 🌟
|
||||||
|
|
||||||
### 🔄 Changes since v0.0.3
|
### 🔄 Changes since v0.0.4
|
||||||
|
|
||||||
### ✨ Features
|
|
||||||
|
|
||||||
* add xAI grok-2-1212 model ([#800](https://github.com/stackblitz-labs/bolt.diy/pull/800))
|
|
||||||
* providers list is now 2 columns (75ec49b) by Dustin Loring
|
|
||||||
* enhanced Terminal Error Handling and Alert System ([#797](https://github.com/stackblitz-labs/bolt.diy/pull/797))
|
|
||||||
* add Starter template menu in homepage ([#884](https://github.com/stackblitz-labs/bolt.diy/pull/884))
|
|
||||||
* catch errors from web container preview and show in actionable alert so user can send them to AI for fixing ([#856](https://github.com/stackblitz-labs/bolt.diy/pull/856))
|
|
||||||
* redact file contents from chat and put latest files into system prompt ([#904](https://github.com/stackblitz-labs/bolt.diy/pull/904))
|
|
||||||
* added Automatic Code Template Detection And Import ([#867](https://github.com/stackblitz-labs/bolt.diy/pull/867))
|
|
||||||
* added hyperbolic llm models ([#943](https://github.com/stackblitz-labs/bolt.diy/pull/943))
|
|
||||||
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
* chat title character restriction (e064803) by Dustin Loring
|
* hotfix auto select starter template works without github token #release ([#959](https://github.com/stackblitz-labs/bolt.diy/pull/959)) by @thecodacus
|
||||||
* fixed model not loading/working, even after baseUrl set in .env file ([#816](https://github.com/stackblitz-labs/bolt.diy/pull/816))
|
|
||||||
* added wait till terminal prompt for bolt shell execution ([#789](https://github.com/stackblitz-labs/bolt.diy/pull/789))
|
|
||||||
* fixed console error for SettingsWIndow & Removed ts-nocheck ([#714](https://github.com/stackblitz-labs/bolt.diy/pull/714))
|
|
||||||
* add Message Processing Throttling to Prevent Browser Crashes ([#848](https://github.com/stackblitz-labs/bolt.diy/pull/848))
|
|
||||||
* provider menu dropdown fix (ghost providers) ([#862](https://github.com/stackblitz-labs/bolt.diy/pull/862))
|
|
||||||
* ollama provider module base url hotfix for docker ([#863](https://github.com/stackblitz-labs/bolt.diy/pull/863))
|
|
||||||
* check for updates does not look for commit.json now ([#861](https://github.com/stackblitz-labs/bolt.diy/pull/861))
|
|
||||||
* detect and remove markdown block syntax that llms sometimes hallucinate for file actions ([#886](https://github.com/stackblitz-labs/bolt.diy/pull/886))
|
|
||||||
* add defaults for LMStudio to work out of the box ([#928](https://github.com/stackblitz-labs/bolt.diy/pull/928))
|
|
||||||
* import folder filtering ([#939](https://github.com/stackblitz-labs/bolt.diy/pull/939))
|
|
||||||
* refresh model list after api key changes ([#944](https://github.com/stackblitz-labs/bolt.diy/pull/944))
|
|
||||||
* better model loading ui feedback and model list update ([#954](https://github.com/stackblitz-labs/bolt.diy/pull/954))
|
|
||||||
* updated logger and model caching minor bugfix #release ([#895](https://github.com/stackblitz-labs/bolt.diy/pull/895))
|
|
||||||
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
|
|
||||||
* simplified setup ([#817](https://github.com/stackblitz-labs/bolt.diy/pull/817))
|
|
||||||
* toc for readme (de64007) by Dustin Loring
|
|
||||||
* faq style change, toc added to index (636f87f) by Dustin Loring
|
|
||||||
* setup updated (ab5cde3) by Dustin Loring
|
|
||||||
* updated Docs ([#845](https://github.com/stackblitz-labs/bolt.diy/pull/845))
|
|
||||||
* updated download link ([#850](https://github.com/stackblitz-labs/bolt.diy/pull/850))
|
|
||||||
* updated env.example of OLLAMA & LMSTUDIO base url ([#877](https://github.com/stackblitz-labs/bolt.diy/pull/877))
|
|
||||||
|
|
||||||
|
|
||||||
### ♻️ Code Refactoring
|
|
||||||
|
|
||||||
* updated vite config to inject add version metadata into the app on build ([#841](https://github.com/stackblitz-labs/bolt.diy/pull/841))
|
|
||||||
* refactored LLM Providers: Adapting Modular Approach ([#832](https://github.com/stackblitz-labs/bolt.diy/pull/832))
|
|
||||||
|
|
||||||
|
|
||||||
### ⚙️ CI
|
|
||||||
|
|
||||||
* updated the docs ci to only trigger if any files changed in the docs folder ([#849](https://github.com/stackblitz-labs/bolt.diy/pull/849))
|
|
||||||
* improved change-log generation script and cleaner release ci action ([#896](https://github.com/stackblitz-labs/bolt.diy/pull/896))
|
|
||||||
|
|
||||||
|
|
||||||
### 🔍 Other Changes
|
|
||||||
|
|
||||||
* fix hotfix for version metadata issue ([#853](https://github.com/stackblitz-labs/bolt.diy/pull/853))
|
|
||||||
* feat; data tab added to the settings (1f938fc) by Dustin Loring
|
|
||||||
|
|
||||||
|
|
||||||
## 📈 Stats
|
## 📈 Stats
|
||||||
|
|
||||||
**Full Changelog**: [`v0.0.3..v0.0.4`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.3...v0.0.4)
|
**Full Changelog**: [`v0.0.4..v0.0.5`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.4...v0.0.5)
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ services:
|
|||||||
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
|
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
|
||||||
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||||
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
||||||
|
- XAI_API_KEY=${XAI_API_KEY}
|
||||||
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
||||||
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
||||||
|
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
|
||||||
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
||||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||||
- RUNNING_IN_DOCKER=true
|
- RUNNING_IN_DOCKER=true
|
||||||
@@ -48,10 +50,12 @@ services:
|
|||||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||||
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_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}
|
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||||
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
|
||||||
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
||||||
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
|
||||||
|
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
|
||||||
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
|
||||||
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
|
||||||
- RUNNING_IN_DOCKER=true
|
- RUNNING_IN_DOCKER=true
|
||||||
|
|||||||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.venv
|
.venv
|
||||||
site/
|
site/
|
||||||
|
.python-version
|
||||||
1
docs/.python-version
Normal file
1
docs/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12.0
|
||||||
118
docs/docs/FAQ.md
118
docs/docs/FAQ.md
@@ -1,91 +1,95 @@
|
|||||||
# Frequently Asked Questions (FAQ)
|
# Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
<details>
|
## Models and Setup
|
||||||
<summary><strong>What are the best models for bolt.diy?</strong></summary>
|
|
||||||
|
|
||||||
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
|
- **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
|
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
|
||||||
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
|
- **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)
|
- **DeepSeekCoder V3**: 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
|
- **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!
|
!!! warning
|
||||||
</details>
|
Models with less than 7b parameters typically lack the capability to properly interact with bolt!
|
||||||
|
|
||||||
<details>
|
## Best Practices
|
||||||
<summary><strong>How do I get the best results with bolt.diy?</strong></summary>
|
|
||||||
|
|
||||||
- **Be specific about your stack**:
|
??? 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.
|
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
|
||||||
|
|
||||||
- **Use the enhance prompt icon**:
|
- **Use the enhance prompt icon**:
|
||||||
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
|
||||||
|
|
||||||
- **Scaffold the basics first, then add features**:
|
- **Scaffold the basics first, then add features**:
|
||||||
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
|
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**:
|
- **Batch simple instructions**:
|
||||||
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
|
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."*
|
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
## Project Information
|
||||||
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
|
|
||||||
|
|
||||||
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
??? question "How do I contribute to bolt.diy?"
|
||||||
</details>
|
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
|
||||||
|
|
||||||
<details>
|
??? question "What are the future plans for bolt.diy?"
|
||||||
<summary><strong>What are the future plans for bolt.diy?</strong></summary>
|
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.
|
??? question "Why are there so many open issues/pull requests?"
|
||||||
New features and improvements are on the way!
|
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>
|
|
||||||
|
|
||||||
<details>
|
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.
|
||||||
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
|
|
||||||
|
|
||||||
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
|
## 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.
|
??? question "How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?"
|
||||||
</details>
|
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>
|
## Troubleshooting
|
||||||
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
|
|
||||||
|
|
||||||
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
|
??? error "There was an error processing this request"
|
||||||
</details>
|
This generic error message means something went wrong. Check both:
|
||||||
|
|
||||||
<details>
|
- The terminal (if you started the app with Docker or `pnpm`).
|
||||||
<summary><strong>Common Errors and Troubleshooting</strong></summary>
|
|
||||||
|
|
||||||
### **"There was an error processing this request"**
|
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
|
||||||
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).
|
|
||||||
|
|
||||||
### **"x-api-key header missing"**
|
??? error "x-api-key header missing"
|
||||||
This error is sometimes resolved by restarting the Docker container.
|
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.
|
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**
|
??? error "Blank preview when running the app"
|
||||||
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
A blank preview often occurs due to hallucinated bad code or incorrect commands.
|
||||||
To troubleshoot:
|
To troubleshoot:
|
||||||
- Check the developer console for errors.
|
|
||||||
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
|
|
||||||
|
|
||||||
### **"Everything works, but the results are bad"**
|
- Check the developer console for errors.
|
||||||
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like GPT-4o, Claude 3.5 Sonnet, or DeepSeek Coder V2 236b.
|
|
||||||
|
|
||||||
### **"Received structured exception #0xc0000005: access violation"**
|
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
|
||||||
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
|
|
||||||
|
|
||||||
### **"Miniflare or Wrangler errors in Windows"**
|
??? error "Everything works, but the results are bad"
|
||||||
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.
|
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
|
||||||
</details>
|
|
||||||
|
- 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)
|
[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
|
## Features
|
||||||
|
|||||||
@@ -66,3 +66,11 @@ markdown_extensions:
|
|||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
- pymdownx.mark
|
- 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",
|
"license": "MIT",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "npm run build && wrangler pages deploy",
|
"deploy": "npm run build && wrangler pages deploy",
|
||||||
"build": "remix vite:build",
|
"build": "remix vite:build",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"@ai-sdk/google": "^0.0.52",
|
"@ai-sdk/google": "^0.0.52",
|
||||||
"@ai-sdk/mistral": "^0.0.43",
|
"@ai-sdk/mistral": "^0.0.43",
|
||||||
"@ai-sdk/openai": "^0.0.66",
|
"@ai-sdk/openai": "^0.0.66",
|
||||||
|
"@ai-sdk/amazon-bedrock": "1.0.6",
|
||||||
"@codemirror/autocomplete": "^6.18.3",
|
"@codemirror/autocomplete": "^6.18.3",
|
||||||
"@codemirror/commands": "^6.7.1",
|
"@codemirror/commands": "^6.7.1",
|
||||||
"@codemirror/lang-cpp": "^6.0.2",
|
"@codemirror/lang-cpp": "^6.0.2",
|
||||||
|
|||||||
1075
pnpm-lock.yaml
generated
1075
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 { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
// Get git hash with fallback
|
// Get git hash with fallback
|
||||||
const getGitHash = () => {
|
const getGitHash = () => {
|
||||||
try {
|
try {
|
||||||
@@ -17,18 +19,21 @@ const getGitHash = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default defineConfig((config) => {
|
export default defineConfig((config) => {
|
||||||
return {
|
return {
|
||||||
define: {
|
define: {
|
||||||
__COMMIT_HASH: JSON.stringify(getGitHash()),
|
__COMMIT_HASH: JSON.stringify(getGitHash()),
|
||||||
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||||
|
// 'process.env': JSON.stringify(process.env)
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
nodePolyfills({
|
nodePolyfills({
|
||||||
include: ['path', 'buffer'],
|
include: ['path', 'buffer', 'process'],
|
||||||
}),
|
}),
|
||||||
config.mode !== 'test' && remixCloudflareDevProxy(),
|
config.mode !== 'test' && remixCloudflareDevProxy(),
|
||||||
remixVitePlugin({
|
remixVitePlugin({
|
||||||
|
|||||||
1
worker-configuration.d.ts
vendored
1
worker-configuration.d.ts
vendored
@@ -16,4 +16,5 @@ interface Env {
|
|||||||
MISTRAL_API_KEY: string;
|
MISTRAL_API_KEY: string;
|
||||||
XAI_API_KEY: string;
|
XAI_API_KEY: string;
|
||||||
PERPLEXITY_API_KEY: string;
|
PERPLEXITY_API_KEY: string;
|
||||||
|
AWS_BEDROCK_CONFIG: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user