41 Commits

Author SHA1 Message Date
Leex
840dd59af8 docs: Update README.md 2025-01-18 21:24:02 +01:00
Leex
7f540b5d1e Update README.md
- Enhanced text for bolt.diy docs section and better visibility to guide people there instead using github readme which is more for devs
- added NodeJS based applications, as this is not clear and some people asked about in the community
2025-01-18 21:14:57 +01:00
Oliver Jägle
e19644268c feat: configure dynamic providers via .env (#1108)
* Use backend API route to fetch dynamic models

# Conflicts:
#	app/components/chat/BaseChat.tsx

* Override ApiKeys if provided in frontend

* Remove obsolete artifact

* Transport api keys from client to server in header

* Cache static provider information

* Restore reading provider settings from cookie

* Reload only a single provider on api key change

* Transport apiKeys and providerSettings via cookies.

While doing this, introduce a simple helper function for cookies
2025-01-18 03:39:19 +05:30
Stijnus
87ff81035f feat: added the "Open Preview in a New Tab" (#1101)
* added the "Open Preview in a New Tab"

* enhancement

[Open Preview] [▼]  // Two buttons side by side
    |
    +-- [Mobile (375x667)]    // Dropdown menu
    |-- [Tablet (768x1024)]
    |-- [Laptop (1366x768)]
    +-- [Desktop (1920x1080)]

* Update Preview.tsx

* Update Preview.tsx
2025-01-18 01:55:44 +05:30
Ngô Tấn Tài
c7738243ca feat: added Github provider (#1109) 2025-01-17 13:22:51 +05:30
lewis liu
41bb909f8d fix: fallback model name not working (#1095)
Co-authored-by: 刘一奇 <liuyiqi02@corp.netease.com>
2025-01-15 16:36:33 +05:30
Digitl Alchemyst
85d864f607 Merge pull request #809 from stackblitz-labs/807-transparency-about-development
docs: document how we work
2025-01-12 23:34:48 -06:00
Anirban Kar
fad41973e2 fix: api-key manager cleanup and log error on llm call (#1077)
* fix: api-key manager cleanup and log error on llm call

* log improved
2025-01-13 04:21:29 +05:30
Eduard Ruzga
3a298f1586 fix: cors issues from preview fixed by changing embedder policies (#1056) 2025-01-12 21:58:06 +05:30
GaryStimson
6aaff63ca7 fix: bugfix in fetching API Key on base llm provider. (#1063) 2025-01-12 21:54:45 +05:30
Digitl Alchemyst
12c6b7abe5 Update README.md
Add Todo
2025-01-10 16:38:37 -06:00
Digitl Alchemyst
42030e1e20 Merge pull request #1026 from dnielsen/patch-1
chore: Update README.md with new model integration todo
2025-01-10 11:51:25 -06:00
Adithyan K
49bb17886a fix: added ui indicator on how apikeys are set (UI/Env) for api-key-manager component (#732)
* fixed #333

* Added instruction in case api-key is not set.

* addressed some of the review changes:

1. moved function definiton to useCallback.
2. added a cache to store the env key status and the api call is made only on a cache miss.

* Manages the API-key entered via UI in a better way.

- Persist API keys in cookies when entered via UI
- Automatically load saved keys when switching between providers
- Preserve existing functionality for environment variable based keys

* Re-used map from utils/constants file.

* Code cleanup -  Removed redundant API key init in BaseChat as its already handled by APIKeyManager component.
2025-01-10 21:26:31 +05:30
Leex
6bf36a915c docs: bugfix/formatting faq docs (#1027)
* fixed formatting of faq and enhanced styling

* save

* did further improvements and fixing the sidebar

---------

Co-authored-by: Anirban Kar <thecodacus@gmail.com>
2025-01-10 21:17:32 +05:30
Oliver Jägle
05eca7cde6 Add feature issue template 2025-01-07 19:53:41 +01:00
Oliver Jägle
a0596bbad3 Fix typos in epic template 2025-01-07 19:53:31 +01:00
Oliver Jägle
c6254b7940 Remove bolt.new references from bug template 2025-01-07 19:53:23 +01:00
Oliver Jägle
8d1cc49140 Add epic template 2025-01-07 19:53:15 +01:00
Oliver Jägle
c78995fee2 Clarify PRs in project organization 2025-01-07 19:53:04 +01:00
Oliver Jägle
bc3274c77e Satisfy the linter 2025-01-07 19:52:50 +01:00
Oliver Jägle
f20fff1443 WIP: Add information about development process 2025-01-07 19:52:28 +01:00
Siddharth V
4fd5040355 feat: enhance chat import with multi-format support (#936)
* feat: enhance chat import with multi-format support

- Add support for importing chats from different formats:
  - Standard Bolt format
  - Chrome extension format
  - History array format
  - Bolt export format
- Add Import Chats button to Data Management
- Add proper error handling and logging
- Update README with backup/restore feature

* refactor: simplify chat import formats

- Remove multi-format support from DataTab
- Keep only standard Bolt export formats
- Simplify ImportButtons to handle standard format only
2025-01-07 16:46:42 +02:00
Dave Nielsen
3aed93aeed Update README.md
Added  Granite Integration
2025-01-06 11:21:13 -08:00
Anirban Kar
7004c897f7 updated docs (#1025) 2025-01-07 00:43:35 +05:30
Cole Medin
441b797f16 Adding resources page to index.md for docs. 2025-01-06 10:20:43 -06:00
Cole Medin
354f416559 Updating README with resources and small fixes. 2025-01-06 10:15:19 -06:00
Anirban Kar
6f524fdf27 ci: added arm64 platform for docker published images (#1021) 2025-01-06 19:19:40 +05:30
Anirban Kar
78eb3a5f34 fix: streaming issue fixed for build versions (#1006)
* fix: streaming issue fixed for build versions

* added keep-alive header
2025-01-06 19:18:52 +05:30
Anirban Kar
49c7129ded fix: ollama and lm studio url issue fix for docker and build (#1008)
* fix: ollama and lm studio url issue fix for docker and build

* vite config fix
2025-01-06 19:18:42 +05:30
kunjabijukchhe
3ecac25a35 feat: implement Claude 3, Claude3.5, Nova Pro, Nova Lite and Mistral model integration with AWS Bedrock (#974)
* feat: Integrate AWS Bedrock with Claude 3.5 Sonnet, Claude 3 Sonnet, and Claude 3.5 Haiku

* update Dockerfile for AWS Bedrock configuration

* feat: add new Bedrock model 'Mistral' and update Haiku to version 3

* feat: add new bedrock model Nova Lite and Nova Pro

* Update README documentation to reflect the latest changes

* Add the icon for aws bedrock

* add support for serialized AWS Bedrock configuration in api key
2025-01-06 17:49:16 +05:30
Anirban Kar
7e39e924e1 reverted visibility change (#1018) 2025-01-06 14:05:01 +05:30
Anirban Kar
f2546f12d8 ci: added visibility change to public for docker image publish (#1017) 2025-01-06 13:57:16 +05:30
twsl
77f8a368a2 ci: fix docker image workflow permissions (#1013)
* Update docker.yaml

* Fix indentation in docker workflow file
2025-01-06 10:34:31 +05:30
twsl
02a3abd892 ci: docker Image creation pipeline (#1011)
* Create docker.yaml

* Add build target

* Use build target var

* Use github token instead
2025-01-06 03:32:36 +05:30
Siddarth
6437ceb5dd fix: added XAI to docker config (#274)
* commit

* Create .env.example

* Update docker-compose.yaml

---------

Co-authored-by: Anirban Kar <thecodacus@gmail.com>
2025-01-06 03:18:07 +05:30
Cole Medin
481268b941 docs: updating copyright in LICENSE (#796) 2025-01-06 03:10:26 +05:30
Anirban Kar
59cae5733d fix: git private clone with custom proxy (#1010)
* cookie fix

* fix: git private clone with custom proxy

* list -fix
2025-01-06 01:54:55 +05:30
Eduard Ruzga
b1f9380c30 fix: introduce our own cors proxy for git import to fix 403 errors on isometric git cors proxy (#924)
* Exploration of improving git import

* Fix our own git proxy

* Clean out file counting for progress, does not seem to work well anyways
2025-01-05 17:26:02 +05:30
Eduard Ruzga
31e03ce99f Merge pull request #961 from Gaurav-Wankhede/main
fix: updated hyperbolic link
2025-01-01 19:43:33 +02:00
Gaurav-Wankhede
e9852bfb22 Update hyperbolic.ts
Changed updated Hyperbolic Settings link
2025-01-01 17:59:11 +05:30
Anirban Kar
d2ba8d3be3 fix: show warning on starter template failure and continue (#960) 2025-01-01 13:39:09 +05:30
52 changed files with 2819 additions and 482 deletions

View File

@@ -83,6 +83,17 @@ XAI_API_KEY=
# You only need this environment variable set if you want to use Perplexity models
PERPLEXITY_API_KEY=
# Get your AWS configuration
# https://console.aws.amazon.com/iam/home
# The JSON should include the following keys:
# - region: The AWS region where Bedrock is available.
# - accessKeyId: Your AWS access key ID.
# - secretAccessKey: Your AWS secret access key.
# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials.
# Example JSON:
# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"}
AWS_BEDROCK_CONFIG=
# Include this environment variable if you want more logging for debugging locally
VITE_LOG_LEVEL=debug

View File

@@ -6,8 +6,8 @@ body:
value: |
Thank you for reporting an issue :pray:.
This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new).
If you experience issues related to WebContainer, please file an issue in our [WebContainer repo](https://github.com/stackblitz/webcontainer-core), or file an issue in our [StackBlitz core repo](https://github.com/stackblitz/core) for issues with StackBlitz.
This issue tracker is for bugs and issues found with [Bolt.diy](https://bolt.diy).
If you experience issues related to WebContainer, please file an issue in the official [StackBlitz WebContainer repo](https://github.com/stackblitz/webcontainer-core).
The more information you fill in, the better we can help you.
- type: textarea

23
.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View 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
View File

@@ -0,0 +1,28 @@
---
name: Feature
about: A pretty vague description of how a capability of our software can be added or improved.
title: ''
labels:
- feature
assignees: ''
---
# Motivation
<!-- What capability should be either established or improved? How is life of the target audience better after it's been done? -->
# Scope
<!-- This is kind-of the definition-of-done for a feature.
Try to keep the scope as small as possible and prefer creating multiple, small features which each solve a single problem / make something better
-->
# Options
<!-- If you already have an idea how this can be implemented, please describe it here.
This allows potential other contributors to join forces and provide meaningful feedback prio to even starting work on it.
-->
# Related
<!-- Link to the epic or other issues or PRs which are related to this feature. -->

81
.github/workflows/docker.yaml vendored Normal file
View 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

View File

@@ -25,8 +25,10 @@ ARG ANTHROPIC_API_KEY
ARG OPEN_ROUTER_API_KEY
ARG GOOGLE_GENERATIVE_AI_API_KEY
ARG OLLAMA_API_BASE_URL
ARG XAI_API_KEY
ARG TOGETHER_API_KEY
ARG TOGETHER_API_BASE_URL
ARG AWS_BEDROCK_CONFIG
ARG VITE_LOG_LEVEL=debug
ARG DEFAULT_NUM_CTX
@@ -38,16 +40,19 @@ ENV WRANGLER_SEND_METRICS=false \
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
XAI_API_KEY=${XAI_API_KEY} \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
RUNNING_IN_DOCKER=true
# Pre-configure wrangler to disable metrics
RUN mkdir -p /root/.config/.wrangler && \
echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
RUN npm run build
RUN pnpm run build
CMD [ "pnpm", "run", "dockerstart"]
@@ -62,6 +67,7 @@ ARG ANTHROPIC_API_KEY
ARG OPEN_ROUTER_API_KEY
ARG GOOGLE_GENERATIVE_AI_API_KEY
ARG OLLAMA_API_BASE_URL
ARG XAI_API_KEY
ARG TOGETHER_API_KEY
ARG TOGETHER_API_BASE_URL
ARG VITE_LOG_LEVEL=debug
@@ -74,10 +80,13 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
XAI_API_KEY=${XAI_API_KEY} \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
RUNNING_IN_DOCKER=true
RUN mkdir -p ${WORKDIR}/run
CMD pnpm run dev --host

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 StackBlitz, Inc.
Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

57
PROJECT.md Normal file
View 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 dont 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 hell get once its 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 🙌

View File

@@ -1,9 +1,13 @@
# bolt.diy (Previously oTToDev)
# bolt.diy (Previously oTToDev)
[![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
Welcome to bolt.diy, the official open source version of Bolt.new (previously known as oTToDev and bolt.new ANY LLM), which allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more information.
-----
Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more offical installation instructions and more informations.
-----
Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos-tutorial-helpful-content/3243) has a bunch of incredible resources for running and deploying bolt.diy yourself!
We have also launched an experimental agent called the "bolt.diy Expert" that can answer common questions about bolt.diy. Find it here on the [oTTomator Live Agent Studio](https://studio.ottomator.ai/).
@@ -23,8 +27,15 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
## Join the community
[Join the bolt.diy community here, in the thinktank on ottomator.ai!](https://thinktank.ottomator.ai)
[Join the bolt.diy community here, in the oTTomator Think Tank!](https://thinktank.ottomator.ai)
## Project management
Bolt.diy is a community effort! Still, the core team of contributors aims at organizing the project in way that allows
you to understand where the current areas of focus are.
If you want to know what we are working on, what we are planning to work on, or if you want to contribute to the
project, please check the [project management guide](./PROJECT.md) to get started easily.
## Requested Additions
@@ -47,6 +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)
- ✅ Streaming of code output (@thecodacus)
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
- ✅ Chat history backup and restore functionality (@sidbetatester)
- ✅ Cohere Integration (@hasanraiyan)
- ✅ Dynamic model max token length (@hasanraiyan)
- ✅ Better prompt enhancing (@SujalXplores)
@@ -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 preview Errors and ask bolt to fix it (@wonderwhy-er)
- ✅ Add Starter Template Options (@thecodacus)
- ✅ Perplexity Integration (@meetpateltech)
- ✅ AWS Bedrock Integration (@kunjabijukchhe)
-**HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
-**HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
-**HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
@@ -73,12 +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.
- ⬜ Voice prompting
- ⬜ Azure Open AI API Integration
- ✅ Perplexity Integration (@meetpateltech)
- ⬜ Vertex AI Integration
- ⬜ Granite Integration
- ⬜ Popout Window for Web Container
## Features
- **AI-powered full-stack web development** directly in your browser.
- **AI-powered full-stack web development** for **NodeJS based applications** directly in your browser.
- **Support for multiple LLMs** with an extensible architecture to integrate additional models.
- **Attach images to prompts** for better contextual understanding.
- **Integrated terminal** to view output of LLM-run commands.

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { IconButton } from '~/components/ui/IconButton';
import type { ProviderInfo } from '~/types/model';
import Cookies from 'js-cookie';
@@ -11,11 +11,14 @@ interface APIKeyManagerProps {
labelForGetApiKey?: string;
}
// cache which stores whether the provider's API key is set via environment variable
const providerEnvKeyStatusCache: Record<string, boolean> = {};
const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
export function getApiKeysFromCookies() {
const storedApiKeys = Cookies.get('apiKeys');
let parsedKeys = {};
let parsedKeys: Record<string, string> = {};
if (storedApiKeys) {
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
@@ -32,54 +35,135 @@ export function getApiKeysFromCookies() {
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
const [isEditing, setIsEditing] = useState(false);
const [tempKey, setTempKey] = useState(apiKey);
const [isEnvKeySet, setIsEnvKeySet] = useState(false);
// Reset states and load saved key when provider changes
useEffect(() => {
// Load saved API key from cookies for this provider
const savedKeys = getApiKeysFromCookies();
const savedKey = savedKeys[provider.name] || '';
setTempKey(savedKey);
setApiKey(savedKey);
setIsEditing(false);
}, [provider.name]);
const checkEnvApiKey = useCallback(async () => {
// Check cache first
if (providerEnvKeyStatusCache[provider.name] !== undefined) {
setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]);
return;
}
try {
const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`);
const data = await response.json();
const isSet = (data as { isSet: boolean }).isSet;
// Cache the result
providerEnvKeyStatusCache[provider.name] = isSet;
setIsEnvKeySet(isSet);
} catch (error) {
console.error('Failed to check environment API key:', error);
setIsEnvKeySet(false);
}
}, [provider.name]);
useEffect(() => {
checkEnvApiKey();
}, [checkEnvApiKey]);
const handleSave = () => {
// Save to parent state
setApiKey(tempKey);
// Save to cookies
const currentKeys = getApiKeysFromCookies();
const newKeys = { ...currentKeys, [provider.name]: tempKey };
Cookies.set('apiKeys', JSON.stringify(newKeys));
setIsEditing(false);
};
return (
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
<div>
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
{!isEditing && (
<div className="flex items-center mb-4">
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
</span>
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
<div className="i-ph:pencil-simple" />
</IconButton>
</div>
)}
<div className="flex items-center justify-between py-3 px-1">
<div className="flex items-center gap-2 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
{!isEditing && (
<div className="flex items-center gap-2">
{apiKey ? (
<>
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
<span className="text-xs text-green-500">Set via UI</span>
</>
) : isEnvKeySet ? (
<>
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
<span className="text-xs text-green-500">Set via environment variable</span>
</>
) : (
<>
<div className="i-ph:x-circle-fill text-red-500 w-4 h-4" />
<span className="text-xs text-red-500">Not Set (Please set via UI or ENV_VAR)</span>
</>
)}
</div>
)}
</div>
</div>
{isEditing ? (
<div className="flex items-center gap-3 mt-2">
<input
type="password"
value={tempKey}
placeholder="Your API Key"
onChange={(e) => setTempKey(e.target.value)}
className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
/>
<IconButton onClick={handleSave} title="Save API Key">
<div className="i-ph:check" />
</IconButton>
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
<div className="i-ph:x" />
</IconButton>
</div>
) : (
<>
{provider?.getApiKeyLink && (
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
<div className={provider?.icon || 'i-ph:key'} />
<div className="flex items-center gap-2 shrink-0">
{isEditing ? (
<div className="flex items-center gap-2">
<input
type="password"
value={tempKey}
placeholder="Enter API Key"
onChange={(e) => setTempKey(e.target.value)}
className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor
bg-bolt-elements-prompt-background text-bolt-elements-textPrimary
focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
/>
<IconButton
onClick={handleSave}
title="Save API Key"
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
>
<div className="i-ph:check w-4 h-4" />
</IconButton>
)}
</>
)}
<IconButton
onClick={() => setIsEditing(false)}
title="Cancel"
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
>
<div className="i-ph:x w-4 h-4" />
</IconButton>
</div>
) : (
<>
{
<IconButton
onClick={() => setIsEditing(true)}
title="Edit API Key"
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
>
<div className="i-ph:pencil-simple w-4 h-4" />
</IconButton>
}
{provider?.getApiKeyLink && !apiKey && (
<IconButton
onClick={() => window.open(provider?.getApiKeyLink)}
title="Get API Key"
className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2"
>
<span className="text-xs whitespace-nowrap">{provider?.labelForGetApiKey || 'Get API Key'}</span>
<div className={`${provider?.icon || 'i-ph:key'} w-4 h-4`} />
</IconButton>
)}
</>
)}
</div>
</div>
);
};

View File

@@ -3,13 +3,13 @@
* Preventing TS checks with files presented in the video for a better presentation.
*/
import type { Message } from 'ai';
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
import React, { type RefCallback, useEffect, useState } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import { Menu } from '~/components/sidebar/Menu.client';
import { IconButton } from '~/components/ui/IconButton';
import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames';
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
import { PROVIDER_LIST } from '~/utils/constants';
import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client';
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
@@ -25,13 +25,13 @@ import GitCloneButton from './GitCloneButton';
import FilePreview from './FilePreview';
import { ModelSelector } from '~/components/chat/ModelSelector';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import type { ProviderInfo } from '~/types/model';
import { ScreenshotStateManager } from './ScreenshotStateManager';
import { toast } from 'react-toastify';
import StarterTemplates from './StarterTemplates';
import type { ActionAlert } from '~/types/actions';
import ChatAlert from './ChatAlert';
import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo } from '~/lib/modules/llm/types';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -102,35 +102,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
const [modelList, setModelList] = useState(MODEL_LIST);
const [modelList, setModelList] = useState<ModelInfo[]>([]);
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
const [isListening, setIsListening] = useState(false);
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
const [transcript, setTranscript] = useState('');
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
const getProviderSettings = useCallback(() => {
let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
try {
const savedProviderSettings = Cookies.get('providers');
if (savedProviderSettings) {
const parsedProviderSettings = JSON.parse(savedProviderSettings);
if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
providerSettings = parsedProviderSettings;
}
}
} catch (error) {
console.error('Error loading Provider Settings from cookies:', error);
// Clear invalid cookie data
Cookies.remove('providers');
}
return providerSettings;
}, []);
useEffect(() => {
console.log(transcript);
}, [transcript]);
@@ -169,7 +147,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
useEffect(() => {
if (typeof window !== 'undefined') {
const providerSettings = getProviderSettings();
let parsedApiKeys: Record<string, string> | undefined = {};
try {
@@ -177,53 +154,48 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setApiKeys(parsedApiKeys);
} catch (error) {
console.error('Error loading API keys from cookies:', error);
// Clear invalid cookie data
Cookies.remove('apiKeys');
}
setIsModelLoading('all');
initializeModelList({ apiKeys: parsedApiKeys, providerSettings })
.then((modelList) => {
// console.log('Model List: ', modelList);
setModelList(modelList);
fetch('/api/models')
.then((response) => response.json())
.then((data) => {
const typedData = data as { modelList: ModelInfo[] };
setModelList(typedData.modelList);
})
.catch((error) => {
console.error('Error initializing model list:', error);
console.error('Error fetching model list:', error);
})
.finally(() => {
setIsModelLoading(undefined);
});
}
}, [providerList]);
}, [providerList, provider]);
const onApiKeysChange = async (providerName: string, apiKey: string) => {
const newApiKeys = { ...apiKeys, [providerName]: apiKey };
setApiKeys(newApiKeys);
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
const provider = LLMManager.getInstance(import.meta.env || process.env || {}).getProvider(providerName);
setIsModelLoading(providerName);
if (provider && provider.getDynamicModels) {
setIsModelLoading(providerName);
let providerModels: ModelInfo[] = [];
try {
const providerSettings = getProviderSettings();
const staticModels = provider.staticModels;
const dynamicModels = await provider.getDynamicModels(
newApiKeys,
providerSettings,
import.meta.env || process.env || {},
);
setModelList((preModels) => {
const filteredOutPreModels = preModels.filter((x) => x.provider !== providerName);
return [...filteredOutPreModels, ...staticModels, ...dynamicModels];
});
} catch (error) {
console.error('Error loading dynamic models:', error);
}
setIsModelLoading(undefined);
try {
const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);
const data = await response.json();
providerModels = (data as { modelList: ModelInfo[] }).modelList;
} catch (error) {
console.error('Error loading dynamic models for:', providerName, error);
}
// Only update models for the specific provider
setModelList((prevModels) => {
const otherModels = prevModels.filter((model) => model.provider !== providerName);
return [...otherModels, ...providerModels];
});
setIsModelLoading(undefined);
};
const startListening = () => {

View File

@@ -137,35 +137,36 @@ export const ChatImpl = memo(
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
api: '/api/chat',
body: {
apiKeys,
files,
promptId,
contextOptimization: contextOptimizationEnabled,
},
sendExtraMessageFields: true,
onError: (error) => {
logger.error('Request failed\n\n', error);
toast.error(
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
);
},
onFinish: (message, response) => {
const usage = response.usage;
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
useChat({
api: '/api/chat',
body: {
apiKeys,
files,
promptId,
contextOptimization: contextOptimizationEnabled,
},
sendExtraMessageFields: true,
onError: (e) => {
logger.error('Request failed\n\n', e, error);
toast.error(
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
);
},
onFinish: (message, response) => {
const usage = response.usage;
if (usage) {
console.log('Token usage:', usage);
if (usage) {
console.log('Token usage:', usage);
// You can now use the usage data as needed
}
// You can now use the usage data as needed
}
logger.debug('Finished streaming');
},
initialMessages,
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
});
logger.debug('Finished streaming');
},
initialMessages,
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
});
useEffect(() => {
const prompt = searchParams.get('prompt');
@@ -263,6 +264,10 @@ export const ChatImpl = memo(
*/
await workbenchStore.saveAllFiles();
if (error != null) {
setMessages(messages.slice(0, -1));
}
const fileModifications = workbenchStore.getFileModifcations();
chatStore.setKey('aborted', false);
@@ -297,7 +302,15 @@ export const ChatImpl = memo(
});
if (template !== 'blank') {
const temResp = await getTemplates(template, title);
const temResp = await getTemplates(template, title).catch((e) => {
if (e.message.includes('rate limit')) {
toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
} else {
toast.warning('Failed to import starter template\n Continuing with blank template');
}
return null;
});
if (temResp) {
const { assistantMessage, userMessage } = temResp;

View File

@@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
import type { Message } from 'ai';
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
import { generateId } from '~/utils/fileUtils';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
const IGNORE_PATTERNS = [
'node_modules/**',
@@ -37,6 +40,8 @@ interface GitCloneButtonProps {
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const { ready, gitClone } = useGit();
const [loading, setLoading] = useState(false);
const onClick = async (_e: any) => {
if (!ready) {
return;
@@ -45,33 +50,34 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const repoUrl = prompt('Enter the Git url');
if (repoUrl) {
const { workdir, data } = await gitClone(repoUrl);
setLoading(true);
if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
console.log(filePaths);
try {
const { workdir, data } = await gitClone(repoUrl);
const textDecoder = new TextDecoder('utf-8');
if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
console.log(filePaths);
// Convert files to common format for command detection
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);
const textDecoder = new TextDecoder('utf-8');
// Detect and create commands message
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);
// Create files message
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
${fileContents
.map(
@@ -82,29 +88,38 @@ ${file.content}
)
.join('\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
id: generateId(),
createdAt: new Date(),
};
const messages = [filesMessage];
const messages = [filesMessage];
if (commandsMessage) {
messages.push(commandsMessage);
if (commandsMessage) {
messages.push(commandsMessage);
}
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
}
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
} finally {
setLoading(false);
}
}
};
return (
<button
onClick={onClick}
title="Clone a Git Repo"
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
>
<span className="i-ph:git-branch" />
Clone a Git Repo
</button>
<>
<button
onClick={onClick}
title="Clone a Git Repo"
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
>
<span className="i-ph:git-branch" />
Clone a Git Repo
</button>
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
</>
);
}

View File

@@ -2,6 +2,11 @@ import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
type ChatData = {
messages?: Message[]; // Standard Bolt format
description?: string; // Optional description
};
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
return (
<div className="flex flex-col items-center justify-center w-auto">
@@ -20,14 +25,17 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
reader.onload = async (e) => {
try {
const content = e.target?.result as string;
const data = JSON.parse(content);
const data = JSON.parse(content) as ChatData;
if (!Array.isArray(data.messages)) {
toast.error('Invalid chat file format');
// Standard format
if (Array.isArray(data.messages)) {
await importChat(data.description || 'Imported Chat', data.messages);
toast.success('Chat imported successfully');
return;
}
await importChat(data.description, data.messages);
toast.success('Chat imported successfully');
toast.error('Invalid chat file format');
} catch (error: unknown) {
if (error instanceof Error) {
toast.error('Failed to parse chat file: ' + error.message);

View File

@@ -49,33 +49,32 @@ export function GitUrlImport() {
if (repoUrl) {
const ig = ignore().add(IGNORE_PATTERNS);
const { workdir, data } = await gitClone(repoUrl);
if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
try {
const { workdir, data } = await gitClone(repoUrl);
const textDecoder = new TextDecoder('utf-8');
if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
const textDecoder = new TextDecoder('utf-8');
// Convert files to common format for command detection
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);
// Detect and create commands message
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);
// Create files message
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
${fileContents
.map(
(file) =>
@@ -85,17 +84,25 @@ ${file.content}
)
.join('\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
id: generateId(),
createdAt: new Date(),
};
const messages = [filesMessage];
const messages = [filesMessage];
if (commandsMessage) {
messages.push(commandsMessage);
if (commandsMessage) {
messages.push(commandsMessage);
}
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
}
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
setLoading(false);
window.location.href = '/';
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
return;
}
}
};

View File

@@ -2,9 +2,10 @@ import React, { useState } from 'react';
import { useNavigate } from '@remix-run/react';
import Cookies from 'js-cookie';
import { toast } from 'react-toastify';
import { db, deleteById, getAll } from '~/lib/persistence';
import { db, deleteById, getAll, setMessages } from '~/lib/persistence';
import { logStore } from '~/lib/stores/logs';
import { classNames } from '~/utils/classNames';
import type { Message } from 'ai';
// List of supported providers that can have API keys
const API_KEY_PROVIDERS = [
@@ -22,6 +23,7 @@ const API_KEY_PROVIDERS = [
'Perplexity',
'Cohere',
'AzureOpenAI',
'AmazonBedrock',
] as const;
interface ApiKeys {
@@ -231,6 +233,81 @@ export default function DataTab() {
event.target.value = '';
};
const processChatData = (
data: any,
): Array<{
id: string;
messages: Message[];
description: string;
urlId?: string;
}> => {
// Handle Bolt standard format (single chat)
if (data.messages && Array.isArray(data.messages)) {
const chatId = crypto.randomUUID();
return [
{
id: chatId,
messages: data.messages,
description: data.description || 'Imported Chat',
urlId: chatId,
},
];
}
// Handle Bolt export format (multiple chats)
if (data.chats && Array.isArray(data.chats)) {
return data.chats.map((chat: { id?: string; messages: Message[]; description?: string; urlId?: string }) => ({
id: chat.id || crypto.randomUUID(),
messages: chat.messages,
description: chat.description || 'Imported Chat',
urlId: chat.urlId,
}));
}
console.error('No matching format found for:', data);
throw new Error('Unsupported chat format');
};
const handleImportChats = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file || !db) {
toast.error('Something went wrong');
return;
}
try {
const content = await file.text();
const data = JSON.parse(content);
const chatsToImport = processChatData(data);
for (const chat of chatsToImport) {
await setMessages(db, chat.id, chat.messages, chat.urlId, chat.description);
}
logStore.logSystem('Chats imported successfully', { count: chatsToImport.length });
toast.success(`Successfully imported ${chatsToImport.length} chat${chatsToImport.length > 1 ? 's' : ''}`);
window.location.reload();
} catch (error) {
if (error instanceof Error) {
logStore.logError('Failed to import chats:', error);
toast.error('Failed to import chats: ' + error.message);
} else {
toast.error('Failed to import chats');
}
console.error(error);
}
};
input.click();
};
return (
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
<div className="mb-6">
@@ -247,6 +324,12 @@ export default function DataTab() {
>
Export All Chats
</button>
<button
onClick={handleImportChats}
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
>
Import Chats
</button>
<button
onClick={handleDeleteAllChats}
disabled={isDeleting}

View File

@@ -1,13 +1,31 @@
export const LoadingOverlay = ({ message = 'Loading...' }) => {
export const LoadingOverlay = ({
message = 'Loading...',
progress,
progressText,
}: {
message?: string;
progress?: number;
progressText?: string;
}) => {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
{/* Loading content */}
<div className="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
<div
className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
style={{ fontSize: '2rem' }}
></div>
<p className="text-lg text-bolt-elements-textTertiary">{message}</p>
{progress !== undefined && (
<div className="w-64 flex flex-col gap-2">
<div className="w-full h-2 bg-bolt-elements-background-depth-1 rounded-full overflow-hidden">
<div
className="h-full bg-bolt-elements-loader-progress transition-all duration-300 ease-out rounded-full"
style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
/>
</div>
{progressText && <p className="text-sm text-bolt-elements-textTertiary text-center">{progressText}</p>}
</div>
)}
</div>
</div>
);

View File

@@ -1,5 +1,5 @@
import { useStore } from '@nanostores/react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useStore } from '@nanostores/react';
import { IconButton } from '~/components/ui/IconButton';
import { workbenchStore } from '~/lib/stores/workbench';
import { PortDropdown } from './PortDropdown';
@@ -7,6 +7,19 @@ import { ScreenshotSelector } from './ScreenshotSelector';
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(() => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -15,6 +28,7 @@ export const Preview = memo(() => {
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
const hasSelectedPreview = useRef(false);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
@@ -27,7 +41,7 @@ export const Preview = memo(() => {
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
// Use percentage for width
const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially
const [widthPercent, setWidthPercent] = useState<number>(37.5);
const resizingState = useRef({
isResizing: false,
@@ -37,8 +51,10 @@ export const Preview = memo(() => {
windowWidth: window.innerWidth,
});
// Define the scaling factor
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
const SCALING_FACTOR = 2;
const [isWindowSizeDropdownOpen, setIsWindowSizeDropdownOpen] = useState(false);
const [selectedWindowSize, setSelectedWindowSize] = useState<WindowSize>(WINDOW_SIZES[0]);
useEffect(() => {
if (!activePreview) {
@@ -79,7 +95,6 @@ export const Preview = memo(() => {
[],
);
// When previews change, display the lowest port if user hasn't selected a preview
useEffect(() => {
if (previews.length > 1 && !hasSelectedPreview.current) {
const minPortIndex = previews.reduce(findMinPortIndex, 0);
@@ -122,7 +137,6 @@ export const Preview = memo(() => {
return;
}
// Prevent text selection
document.body.style.userSelect = 'none';
resizingState.current.isResizing = true;
@@ -134,7 +148,7 @@ export const Preview = memo(() => {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault(); // Prevent any text selection on mousedown
e.preventDefault();
};
const onMouseMove = (e: MouseEvent) => {
@@ -145,7 +159,6 @@ export const Preview = memo(() => {
const dx = e.clientX - resizingState.current.startX;
const windowWidth = resizingState.current.windowWidth;
// Apply scaling factor to increase sensitivity
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
let newWidthPercent = resizingState.current.startWidthPercent;
@@ -156,7 +169,6 @@ export const Preview = memo(() => {
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
}
// Clamp the width between 10% and 90%
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
setWidthPercent(newWidthPercent);
@@ -168,17 +180,12 @@ export const Preview = memo(() => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Restore text selection
document.body.style.userSelect = '';
};
// Handle window resize to ensure widthPercent remains valid
useEffect(() => {
const handleWindowResize = () => {
/*
* Optional: Adjust widthPercent if necessary
* For now, since widthPercent is relative, no action is needed
*/
// Optional: Adjust widthPercent if necessary
};
window.addEventListener('resize', handleWindowResize);
@@ -188,7 +195,6 @@ export const Preview = memo(() => {
};
}, []);
// A small helper component for the handle's "grip" icon
const GripIcon = () => (
<div
style={{
@@ -213,8 +219,33 @@ export const Preview = memo(() => {
</div>
);
const openInNewWindow = (size: WindowSize) => {
if (activePreview?.baseUrl) {
const match = activePreview.baseUrl.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
if (match) {
const previewId = match[1];
const previewUrl = `/webcontainer/preview/${previewId}`;
const newWindow = window.open(
previewUrl,
'_blank',
`noopener,noreferrer,width=${size.width},height=${size.height},menubar=no,toolbar=no,location=no,status=no`,
);
if (newWindow) {
newWindow.focus();
}
} else {
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
}
}
};
return (
<div ref={containerRef} className="w-full h-full flex flex-col relative">
<div
ref={containerRef}
className={`w-full h-full flex flex-col relative ${isPreviewOnly ? 'fixed inset-0 z-50 bg-white' : ''}`}
>
{isPortDropdownOpen && (
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
)}
@@ -225,10 +256,7 @@ export const Preview = memo(() => {
onClick={() => setIsSelectionMode(!isSelectionMode)}
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
/>
<div
className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
>
<div className="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
title="URL"
ref={inputRef}
@@ -261,26 +289,65 @@ export const Preview = memo(() => {
/>
)}
{/* Device mode toggle button */}
<IconButton
icon="i-ph:devices"
onClick={toggleDeviceMode}
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
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
onClick={toggleFullscreen}
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 className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
<div
style={{
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
height: '100%', // Always full height
height: '100%',
overflow: 'visible',
background: '#fff',
position: 'relative',
@@ -294,7 +361,8 @@ export const Preview = memo(() => {
title="preview"
className="border-none w-full h-full bg-white"
src={iframeUrl}
allowFullScreen
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
allow="cross-origin-isolated"
/>
<ScreenshotSelector
isSelectionMode={isSelectionMode}
@@ -308,7 +376,6 @@ export const Preview = memo(() => {
{isDeviceModeOn && (
<>
{/* Left handle */}
<div
onMouseDown={(e) => startResizing(e, 'left')}
style={{
@@ -333,7 +400,6 @@ export const Preview = memo(() => {
<GripIcon />
</div>
{/* Right handle */}
<div
onMouseDown={(e) => startResizing(e, 'right')}
style={{

View File

@@ -226,9 +226,9 @@ export async function streamText(props: {
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
return _streamText({
return await _streamText({
model: provider.getModelInstance({
model: currentModel,
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,

33
app/lib/api/cookies.ts Normal file
View 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) : {};
}

View File

@@ -3,10 +3,10 @@ import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import {
chatId as chatIdStore,
description as descriptionStore,
db,
updateChatDescription,
description as descriptionStore,
getMessages,
updateChatDescription,
} from '~/lib/persistence';
interface EditChatDescriptionOptions {

View File

@@ -49,50 +49,68 @@ export function useGit() {
}
fileData.current = {};
await git.clone({
fs,
http,
dir: webcontainer.workdir,
url,
depth: 1,
singleBranch: true,
corsProxy: 'https://cors.isomorphic-git.org',
onAuth: (url) => {
// let domain=url.split("/")[2]
let auth = lookupSavedPassword(url);
const headers: {
[x: string]: string;
} = {
'User-Agent': 'bolt.diy',
};
if (auth) {
return auth;
}
const auth = lookupSavedPassword(url);
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
auth = {
username: prompt('Enter username'),
password: prompt('Enter password'),
};
return auth;
} else {
return { cancel: true };
}
},
onAuthFailure: (url, _auth) => {
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
},
onAuthSuccess: (url, auth) => {
saveGitAuth(url, auth);
},
});
const data: Record<string, { data: any; encoding?: string }> = {};
for (const [key, value] of Object.entries(fileData.current)) {
data[key] = value;
if (auth) {
headers.Authorization = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`;
}
return { workdir: webcontainer.workdir, data };
try {
await git.clone({
fs,
http,
dir: webcontainer.workdir,
url,
depth: 1,
singleBranch: true,
corsProxy: '/api/git-proxy',
headers,
onAuth: (url) => {
let auth = lookupSavedPassword(url);
if (auth) {
return auth;
}
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
auth = {
username: prompt('Enter username'),
password: prompt('Enter password'),
};
return auth;
} else {
return { cancel: true };
}
},
onAuthFailure: (url, _auth) => {
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
},
onAuthSuccess: (url, auth) => {
saveGitAuth(url, auth);
},
});
const data: Record<string, { data: any; encoding?: string }> = {};
for (const [key, value] of Object.entries(fileData.current)) {
data[key] = value;
}
return { workdir: webcontainer.workdir, data };
} catch (error) {
console.error('Git clone error:', error);
throw error;
}
},
[webcontainer],
[webcontainer, fs, ready],
);
return { ready, gitClone };
@@ -104,55 +122,86 @@ const getFs = (
) => ({
promises: {
readFile: async (path: string, options: any) => {
const encoding = options.encoding;
const encoding = options?.encoding;
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('readFile', relativePath, encoding);
return await webcontainer.fs.readFile(relativePath, encoding);
try {
const result = await webcontainer.fs.readFile(relativePath, encoding);
return result;
} catch (error) {
throw error;
}
},
writeFile: async (path: string, data: any, options: any) => {
const encoding = options.encoding;
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('writeFile', { relativePath, data, encoding });
if (record.current) {
record.current[relativePath] = { data, encoding };
}
return await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
try {
const result = await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
return result;
} catch (error) {
throw error;
}
},
mkdir: async (path: string, options: any) => {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('mkdir', relativePath, options);
return await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
try {
const result = await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
return result;
} catch (error) {
throw error;
}
},
readdir: async (path: string, options: any) => {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('readdir', relativePath, options);
return await webcontainer.fs.readdir(relativePath, options);
try {
const result = await webcontainer.fs.readdir(relativePath, options);
return result;
} catch (error) {
throw error;
}
},
rm: async (path: string, options: any) => {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('rm', relativePath, options);
return await webcontainer.fs.rm(relativePath, { ...(options || {}) });
try {
const result = await webcontainer.fs.rm(relativePath, { ...(options || {}) });
return result;
} catch (error) {
throw error;
}
},
rmdir: async (path: string, options: any) => {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
console.log('rmdir', relativePath, options);
return await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
try {
const result = await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
return result;
} catch (error) {
throw error;
}
},
// Mock implementations for missing functions
unlink: async (path: string) => {
// unlink is just removing a single file
const relativePath = pathUtils.relative(webcontainer.workdir, path);
return await webcontainer.fs.rm(relativePath, { recursive: false });
},
try {
return await webcontainer.fs.rm(relativePath, { recursive: false });
} catch (error) {
throw error;
}
},
stat: async (path: string) => {
try {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
@@ -185,23 +234,12 @@ const getFs = (
throw err;
}
},
lstat: async (path: string) => {
/*
* For basic usage, lstat can return the same as stat
* since we're not handling symbolic links
*/
return await getFs(webcontainer, record).promises.stat(path);
},
readlink: async (path: string) => {
/*
* Since WebContainer doesn't support symlinks,
* we'll throw a "not a symbolic link" error
*/
throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
},
symlink: async (target: string, path: string) => {
/*
* Since WebContainer doesn't support symlinks,

View File

@@ -46,7 +46,7 @@ export abstract class BaseProvider implements ProviderInfo {
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
const apiKey =
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[baseUrlKey];
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[apiTokenKey];
return {
baseUrl,

View File

@@ -83,7 +83,7 @@ export class LLMManager {
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
if (providerSettings) {
if (providerSettings && Object.keys(providerSettings).length > 0) {
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
}

View 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);
}
}

View 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);
}
}

View File

@@ -6,7 +6,7 @@ import { createOpenAI } from '@ai-sdk/openai';
export default class HyperbolicProvider extends BaseProvider {
name = 'Hyperbolic';
getApiKeyLink = 'https://hyperbolic.xyz/settings';
getApiKeyLink = 'https://app.hyperbolic.xyz/settings';
config = {
apiTokenKey: 'HYPERBOLIC_API_KEY',

View File

@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import { createOpenAI } from '@ai-sdk/openai';
import type { LanguageModelV1 } from 'ai';
import { logger } from '~/utils/logger';
export default class LMStudioProvider extends BaseProvider {
name = 'LMStudio';
@@ -22,7 +23,7 @@ export default class LMStudioProvider extends BaseProvider {
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl } = this.getProviderBaseUrlAndKey({
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
@@ -31,7 +32,18 @@ export default class LMStudioProvider extends BaseProvider {
});
if (!baseUrl) {
return [];
throw new Error('No baseUrl found for LMStudio provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/v1/models`);
@@ -51,13 +63,26 @@ export default class LMStudioProvider extends BaseProvider {
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
const { baseUrl } = this.getProviderBaseUrlAndKey({
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for LMStudio provider');
}
if (typeof window === 'undefined') {
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
logger.debug('LMStudio Base Url used: ', baseUrl);
const lmstudio = createOpenAI({
baseUrl: `${baseUrl}/v1`,
apiKey: '',

View File

@@ -3,6 +3,7 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { ollama } from 'ollama-ai-provider';
import { logger } from '~/utils/logger';
interface OllamaModelDetails {
parent_model: string;
@@ -45,7 +46,7 @@ export default class OllamaProvider extends BaseProvider {
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl } = this.getProviderBaseUrlAndKey({
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
@@ -54,7 +55,18 @@ export default class OllamaProvider extends BaseProvider {
});
if (!baseUrl) {
return [];
throw new Error('No baseUrl found for OLLAMA provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/api/tags`);
@@ -78,18 +90,23 @@ export default class OllamaProvider extends BaseProvider {
const { apiKeys, providerSettings, serverEnv, model } = options;
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
// Backend: Check if we're running in Docker
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
if (!baseUrl) {
throw new Error('No baseUrl found for OLLAMA provider');
}
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
logger.debug('Ollama Base Url used: ', baseUrl);
const ollamaInstance = ollama(model, {
numCtx: DEFAULT_NUM_CTX,
}) as LanguageModelV1 & { config: any };

View File

@@ -14,6 +14,8 @@ import PerplexityProvider from './providers/perplexity';
import TogetherProvider from './providers/together';
import XAIProvider from './providers/xai';
import HyperbolicProvider from './providers/hyperbolic';
import AmazonBedrockProvider from './providers/amazon-bedrock';
import GithubProvider from './providers/github';
export {
AnthropicProvider,
@@ -32,4 +34,6 @@ export {
XAIProvider,
TogetherProvider,
LMStudioProvider,
AmazonBedrockProvider,
GithubProvider,
};

View File

@@ -1,27 +1,192 @@
import type { WebContainer } from '@webcontainer/api';
import { atom } from 'nanostores';
// Extend Window interface to include our custom property
declare global {
interface Window {
_tabId?: string;
}
}
export interface PreviewInfo {
port: number;
ready: boolean;
baseUrl: string;
}
// Create a broadcast channel for preview updates
const PREVIEW_CHANNEL = 'preview-updates';
export class PreviewsStore {
#availablePreviews = new Map<number, PreviewInfo>();
#webcontainer: Promise<WebContainer>;
#broadcastChannel: BroadcastChannel;
#lastUpdate = new Map<string, number>();
#watchedFiles = new Set<string>();
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
#REFRESH_DELAY = 300;
#storageChannel: BroadcastChannel;
previews = atom<PreviewInfo[]>([]);
constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL);
this.#storageChannel = new BroadcastChannel('storage-sync-channel');
// Listen for preview updates from other tabs
this.#broadcastChannel.onmessage = (event) => {
const { type, previewId } = event.data;
if (type === 'file-change') {
const timestamp = event.data.timestamp;
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
if (timestamp > lastUpdate) {
this.#lastUpdate.set(previewId, timestamp);
this.refreshPreview(previewId);
}
}
};
// Listen for storage sync messages
this.#storageChannel.onmessage = (event) => {
const { storage, source } = event.data;
if (storage && source !== this._getTabId()) {
this._syncStorage(storage);
}
};
// Override localStorage setItem to catch all changes
if (typeof window !== 'undefined') {
const originalSetItem = localStorage.setItem;
localStorage.setItem = (...args) => {
originalSetItem.apply(localStorage, args);
this._broadcastStorageSync();
};
}
this.#init();
}
// Generate a unique ID for this tab
private _getTabId(): string {
if (typeof window !== 'undefined') {
if (!window._tabId) {
window._tabId = Math.random().toString(36).substring(2, 15);
}
return window._tabId;
}
return '';
}
// Sync storage data between tabs
private _syncStorage(storage: Record<string, string>) {
if (typeof window !== 'undefined') {
Object.entries(storage).forEach(([key, value]) => {
try {
const originalSetItem = Object.getPrototypeOf(localStorage).setItem;
originalSetItem.call(localStorage, key, value);
} catch (error) {
console.error('[Preview] Error syncing storage:', error);
}
});
// Force a refresh after syncing storage
const previews = this.previews.get();
previews.forEach((preview) => {
const previewId = this.getPreviewId(preview.baseUrl);
if (previewId) {
this.refreshPreview(previewId);
}
});
// Reload the page content
if (typeof window !== 'undefined' && window.location) {
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.src = iframe.src;
}
}
}
}
// Broadcast storage state to other tabs
private _broadcastStorageSync() {
if (typeof window !== 'undefined') {
const storage: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
storage[key] = localStorage.getItem(key) || '';
}
}
this.#storageChannel.postMessage({
type: 'storage-sync',
storage,
source: this._getTabId(),
timestamp: Date.now(),
});
}
}
async #init() {
const webcontainer = await this.#webcontainer;
// Listen for server ready events
webcontainer.on('server-ready', (port, url) => {
console.log('[Preview] Server ready on port:', port, url);
this.broadcastUpdate(url);
// Initial storage sync when preview is ready
this._broadcastStorageSync();
});
try {
// Watch for file changes
const watcher = await webcontainer.fs.watch('**/*', { persistent: true });
// Use the native watch events
(watcher as any).addEventListener('change', async () => {
const previews = this.previews.get();
for (const preview of previews) {
const previewId = this.getPreviewId(preview.baseUrl);
if (previewId) {
this.broadcastFileChange(previewId);
}
}
});
// Watch for DOM changes that might affect storage
if (typeof window !== 'undefined') {
const observer = new MutationObserver((_mutations) => {
// Broadcast storage changes when DOM changes
this._broadcastStorageSync();
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
}
} catch (error) {
console.error('[Preview] Error setting up watchers:', error);
}
// Listen for port events
webcontainer.on('port', (port, type, url) => {
let previewInfo = this.#availablePreviews.get(port);
@@ -44,6 +209,101 @@ export class PreviewsStore {
previewInfo.baseUrl = url;
this.previews.set([...previews]);
if (type === 'open') {
this.broadcastUpdate(url);
}
});
}
// Helper to extract preview ID from URL
getPreviewId(url: string): string | null {
const match = url.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
return match ? match[1] : null;
}
// Broadcast state change to all tabs
broadcastStateChange(previewId: string) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'state-change',
previewId,
timestamp,
});
}
// Broadcast file change to all tabs
broadcastFileChange(previewId: string) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'file-change',
previewId,
timestamp,
});
}
// Broadcast update to all tabs
broadcastUpdate(url: string) {
const previewId = this.getPreviewId(url);
if (previewId) {
const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({
type: 'file-change',
previewId,
timestamp,
});
}
}
// Method to refresh a specific preview
refreshPreview(previewId: string) {
// Clear any pending refresh for this preview
const existingTimeout = this.#refreshTimeouts.get(previewId);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
// Set a new timeout for this refresh
const timeout = setTimeout(() => {
const previews = this.previews.get();
const preview = previews.find((p) => this.getPreviewId(p.baseUrl) === previewId);
if (preview) {
preview.ready = false;
this.previews.set([...previews]);
requestAnimationFrame(() => {
preview.ready = true;
this.previews.set([...previews]);
});
}
this.#refreshTimeouts.delete(previewId);
}, this.#REFRESH_DELAY);
this.#refreshTimeouts.set(previewId, timeout);
}
}
// Create a singleton instance
let previewsStore: PreviewsStore | null = null;
export function usePreviewStore() {
if (!previewsStore) {
/*
* Initialize with a Promise that resolves to WebContainer
* This should match how you're initializing WebContainer elsewhere
*/
previewsStore = new PreviewsStore(Promise.resolve({} as WebContainer));
}
return previewsStore;
}

View File

@@ -24,6 +24,7 @@ if (!import.meta.env.SSR) {
Promise.resolve()
.then(() => {
return WebContainer.boot({
coep: 'credentialless',
workdirName: WORK_DIR_NAME,
forwardPreviewErrors: true, // Enable error forwarding from iframes
});

View File

@@ -122,6 +122,8 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
return;
},
};
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`);
const result = await streamText({
messages,
@@ -134,12 +136,27 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
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());
// return createrespo
return new Response(stream.readable, {
status: 200,
headers: {
contentType: 'text/plain; charset=utf-8',
'Content-Type': 'text/event-stream; charset=utf-8',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Text-Encoding': 'chunked',
},
});
} catch (error: any) {

View 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 });
};

View File

@@ -1,34 +1,13 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
//import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text';
import { stripIndents } from '~/utils/stripIndent';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import type { ProviderInfo } from '~/types/model';
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
export async function action(args: ActionFunctionArgs) {
return enhancerAction(args);
}
function parseCookies(cookieHeader: string) {
const cookies: any = {};
// Split the cookie string by semicolons and spaces
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
items.forEach((item) => {
const [name, ...rest] = item.split('=');
if (name && rest) {
// Decode the name and value, and join value parts in case it contains '='
const decodedName = decodeURIComponent(name.trim());
const decodedValue = decodeURIComponent(rest.join('=').trim());
cookies[decodedName] = decodedValue;
}
});
return cookies;
}
async function enhancerAction({ context, request }: ActionFunctionArgs) {
const { message, model, provider } = await request.json<{
message: string;
@@ -55,12 +34,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
}
const cookieHeader = request.headers.get('Cookie');
// Parse the cookie's value (returns an object or null if no cookie exists)
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
parseCookies(cookieHeader || '').providers || '{}',
);
const apiKeys = getApiKeysFromCookie(cookieHeader);
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
try {
const result = await streamText({
@@ -107,7 +82,10 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
return new Response(result.textStream, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Text-Encoding': 'chunked',
},
});
} catch (error: unknown) {

View 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 });
}
}

View File

@@ -1,34 +1,24 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
//import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { generateText } from 'ai';
import { getModelList, PROVIDER_LIST } from '~/utils/constants';
import { PROVIDER_LIST } from '~/utils/constants';
import { MAX_TOKENS } from '~/lib/.server/llm/constants';
import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo } from '~/lib/modules/llm/types';
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
export async function action(args: ActionFunctionArgs) {
return llmCallAction(args);
}
function parseCookies(cookieHeader: string) {
const cookies: any = {};
// Split the cookie string by semicolons and spaces
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
items.forEach((item) => {
const [name, ...rest] = item.split('=');
if (name && rest) {
// Decode the name and value, and join value parts in case it contains '='
const decodedName = decodeURIComponent(name.trim());
const decodedValue = decodeURIComponent(rest.join('=').trim());
cookies[decodedName] = decodedValue;
}
});
return cookies;
async function getModelList(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}) {
const llmManager = LLMManager.getInstance(import.meta.env);
return llmManager.updateModelList(options);
}
async function llmCallAction({ context, request }: ActionFunctionArgs) {
@@ -58,12 +48,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
}
const cookieHeader = request.headers.get('Cookie');
// Parse the cookie's value (returns an object or null if no cookie exists)
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
parseCookies(cookieHeader || '').providers || '{}',
);
const apiKeys = getApiKeysFromCookie(cookieHeader);
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
if (streamOutput) {
try {
@@ -105,8 +91,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
}
} else {
try {
const MODEL_LIST = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
const modelDetails = MODEL_LIST.find((m) => m.name === model);
const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
const modelDetails = models.find((m: ModelInfo) => m.name === model);
if (!modelDetails) {
throw new Error('Model not found');

View File

@@ -0,0 +1,2 @@
import { loader } from './api.models';
export { loader };

View File

@@ -1,6 +1,84 @@
import { json } from '@remix-run/cloudflare';
import { MODEL_LIST } from '~/utils/constants';
import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { ProviderInfo } from '~/types/model';
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
export async function loader() {
return json(MODEL_LIST);
interface ModelsResponse {
modelList: ModelInfo[];
providers: ProviderInfo[];
defaultProvider: ProviderInfo;
}
let cachedProviders: ProviderInfo[] | null = null;
let cachedDefaultProvider: ProviderInfo | null = null;
function getProviderInfo(llmManager: LLMManager) {
if (!cachedProviders) {
cachedProviders = llmManager.getAllProviders().map((provider) => ({
name: provider.name,
staticModels: provider.staticModels,
getApiKeyLink: provider.getApiKeyLink,
labelForGetApiKey: provider.labelForGetApiKey,
icon: provider.icon,
}));
}
if (!cachedDefaultProvider) {
const defaultProvider = llmManager.getDefaultProvider();
cachedDefaultProvider = {
name: defaultProvider.name,
staticModels: defaultProvider.staticModels,
getApiKeyLink: defaultProvider.getApiKeyLink,
labelForGetApiKey: defaultProvider.labelForGetApiKey,
icon: defaultProvider.icon,
};
}
return { providers: cachedProviders, defaultProvider: cachedDefaultProvider };
}
export async function loader({
request,
params,
}: {
request: Request;
params: { provider?: string };
}): Promise<Response> {
const llmManager = LLMManager.getInstance(import.meta.env);
// Get client side maintained API keys and provider settings from cookies
const cookieHeader = request.headers.get('Cookie');
const apiKeys = getApiKeysFromCookie(cookieHeader);
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
const { providers, defaultProvider } = getProviderInfo(llmManager);
let modelList: ModelInfo[] = [];
if (params.provider) {
// Only update models for the specific provider
const provider = llmManager.getProvider(params.provider);
if (provider) {
const staticModels = provider.staticModels;
const dynamicModels = provider.getDynamicModels
? await provider.getDynamicModels(apiKeys, providerSettings, import.meta.env)
: [];
modelList = [...staticModels, ...dynamicModels];
}
} else {
// Update all models
modelList = await llmManager.updateModelList({
apiKeys,
providerSettings,
serverEnv: import.meta.env,
});
}
return json<ModelsResponse>({
modelList,
providers,
defaultProvider,
});
}

View 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>
);
}

View File

@@ -1,7 +1,4 @@
import type { IProviderSetting } from '~/types/model';
import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { Template } from '~/types/template';
export const WORK_DIR_NAME = 'project';
@@ -17,9 +14,7 @@ const llmManager = LLMManager.getInstance(import.meta.env);
export const PROVIDER_LIST = llmManager.getAllProviders();
export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
let MODEL_LIST = llmManager.getModelList();
const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
export const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
PROVIDER_LIST.forEach((provider) => {
providerBaseUrlEnvKeys[provider.name] = {
baseUrlKey: provider.config.baseUrlKey,
@@ -27,34 +22,6 @@ PROVIDER_LIST.forEach((provider) => {
};
});
// Export the getModelList function using the manager
export async function getModelList(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}) {
return await llmManager.updateModelList(options);
}
async function initializeModelList(options: {
env?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
apiKeys?: Record<string, string>;
}): Promise<ModelInfo[]> {
const { providerSettings, apiKeys, env } = options;
const list = await getModelList({
apiKeys,
providerSettings,
serverEnv: env,
});
MODEL_LIST = list || MODEL_LIST;
return list;
}
// initializeModelList({})
export { initializeModelList, providerBaseUrlEnvKeys, MODEL_LIST };
// starter Templates
export const STARTER_TEMPLATES: Template[] = [

View File

@@ -20,8 +20,10 @@ services:
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
- XAI_API_KEY=${XAI_API_KEY}
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
- RUNNING_IN_DOCKER=true
@@ -48,10 +50,12 @@ services:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
- XAI_API_KEY=${XAI_API_KEY}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
- RUNNING_IN_DOCKER=true

3
docs/.gitignore vendored
View File

@@ -1,2 +1,3 @@
.venv
site/
site/
.python-version

1
docs/.python-version Normal file
View File

@@ -0,0 +1 @@
3.12.0

View File

@@ -1,91 +1,95 @@
# Frequently Asked Questions (FAQ)
<details>
<summary><strong>What are the best models for bolt.diy?</strong></summary>
## Models and Setup
For the best experience with bolt.diy, we recommend using the following models:
??? question "What are the best models for bolt.diy?"
For the best experience with bolt.diy, we recommend using the following models:
- **Claude 3.5 Sonnet (old)**: Best overall coder, providing excellent results across all use cases
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
- **DeepSeekCoder V2 236b**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
- **Claude 3.5 Sonnet (old)**: Best overall coder, providing excellent results across all use cases
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
- **DeepSeekCoder V3**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
- **DeepSeekCoder V2 236b**: available through OpenRouter, DeepSeek API, or self-hosted
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
**Note**: Models with less than 7b parameters typically lack the capability to properly interact with bolt!
</details>
!!! warning
Models with less than 7b parameters typically lack the capability to properly interact with bolt!
<details>
<summary><strong>How do I get the best results with bolt.diy?</strong></summary>
## Best Practices
- **Be specific about your stack**:
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
??? question "How do I get the best results with bolt.diy?"
- **Be specific about your stack**:
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
- **Use the enhance prompt icon**:
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
- **Use the enhance prompt icon**:
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
- **Scaffold the basics first, then add features**:
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
- **Scaffold the basics first, then add features**:
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
- **Batch simple instructions**:
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
</details>
- **Batch simple instructions**:
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
<details>
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
## Project Information
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
</details>
??? question "How do I contribute to bolt.diy?"
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
<details>
<summary><strong>What are the future plans for bolt.diy?</strong></summary>
??? question "What are the future plans for bolt.diy?"
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
New features and improvements are on the way!
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
New features and improvements are on the way!
</details>
??? question "Why are there so many open issues/pull requests?"
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
<details>
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
## Model Comparisons
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
</details>
??? question "How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?"
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
<details>
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
## Troubleshooting
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
</details>
??? error "There was an error processing this request"
This generic error message means something went wrong. Check both:
<details>
<summary><strong>Common Errors and Troubleshooting</strong></summary>
- The terminal (if you started the app with Docker or `pnpm`).
### **"There was an error processing this request"**
This generic error message means something went wrong. Check both:
- The terminal (if you started the app with Docker or `pnpm`).
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
### **"x-api-key header missing"**
This error is sometimes resolved by restarting the Docker container.
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
??? error "x-api-key header missing"
This error is sometimes resolved by restarting the Docker container.
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
### **Blank preview when running the app**
A blank preview often occurs due to hallucinated bad code or incorrect commands.
To troubleshoot:
- Check the developer console for errors.
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
??? error "Blank preview when running the app"
A blank preview often occurs due to hallucinated bad code or incorrect commands.
To troubleshoot:
### **"Everything works, but the results are bad"**
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like GPT-4o, Claude 3.5 Sonnet, or DeepSeek Coder V2 236b.
- Check the developer console for errors.
### **"Received structured exception #0xc0000005: access violation"**
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
### **"Miniflare or Wrangler errors in Windows"**
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here https://github.com/stackblitz-labs/bolt.diy/issues/19.
</details>
??? error "Everything works, but the results are bad"
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like
- GPT-4o
- Claude 3.5 Sonnet
- DeepSeek Coder V2 236b
??? error "Received structured exception #0xc0000005: access violation"
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
??? error "Miniflare or Wrangler errors in Windows"
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here <a href="https://github.com/stackblitz-labs/bolt.diy/issues/19">Github Issues</a>
---
Got more questions? Feel free to reach out or open an issue in our GitHub repo!
## Get Help & Support
!!! tip "Community Support"
[Join the bolt.diy Community](https://thinktank.ottomator.ai/c/bolt-diy/17){target=_blank} for discussions and help
!!! bug "Report Issues"
[Open an Issue](https://github.com/stackblitz-labs/bolt.diy/issues/19){target=_blank} in our GitHub Repository

View File

@@ -25,6 +25,8 @@ bolt.diy allows you to choose the LLM that you use for each prompt! Currently, y
[Join the community!](https://thinktank.ottomator.ai)
Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos-tutorial-helpful-content/3243) has a bunch of incredible resources for running and deploying bolt.diy yourself!
---
## Features

View File

@@ -65,4 +65,12 @@ markdown_extensions:
- pymdownx.details
- pymdownx.superfences
- pymdownx.mark
- attr_list
- attr_list
- md_in_html
- tables
- def_list
- admonition
- pymdownx.tasklist:
custom_checkbox: true
- toc:
permalink: true

View File

@@ -35,6 +35,7 @@
"@ai-sdk/google": "^0.0.52",
"@ai-sdk/mistral": "^0.0.43",
"@ai-sdk/openai": "^0.0.66",
"@ai-sdk/amazon-bedrock": "1.0.6",
"@codemirror/autocomplete": "^6.18.3",
"@codemirror/commands": "^6.7.1",
"@codemirror/lang-cpp": "^6.0.2",

1075
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -4,9 +4,11 @@ import { defineConfig, type ViteDevServer } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
import tsconfigPaths from 'vite-tsconfig-paths';
import * as dotenv from 'dotenv';
import { execSync } from 'child_process';
dotenv.config();
// Get git hash with fallback
const getGitHash = () => {
try {
@@ -17,18 +19,21 @@ const getGitHash = () => {
};
export default defineConfig((config) => {
return {
define: {
__COMMIT_HASH: JSON.stringify(getGitHash()),
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
// 'process.env': JSON.stringify(process.env)
},
build: {
target: 'esnext',
},
plugins: [
nodePolyfills({
include: ['path', 'buffer'],
include: ['path', 'buffer', 'process'],
}),
config.mode !== 'test' && remixCloudflareDevProxy(),
remixVitePlugin({

View File

@@ -16,4 +16,5 @@ interface Env {
MISTRAL_API_KEY: string;
XAI_API_KEY: string;
PERPLEXITY_API_KEY: string;
AWS_BEDROCK_CONFIG: string;
}