diff --git a/.env.example b/.env.example index 6f2f5f5a..2d736a72 100644 --- a/.env.example +++ b/.env.example @@ -83,6 +83,17 @@ XAI_API_KEY= # You only need this environment variable set if you want to use Perplexity models PERPLEXITY_API_KEY= +# Get your AWS configuration +# https://console.aws.amazon.com/iam/home +# The JSON should include the following keys: +# - region: The AWS region where Bedrock is available. +# - accessKeyId: Your AWS access key ID. +# - secretAccessKey: Your AWS secret access key. +# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials. +# Example JSON: +# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"} +AWS_BEDROCK_CONFIG= + # Include this environment variable if you want more logging for debugging locally VITE_LOG_LEVEL=debug diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 37ebae5a..8b66eb1b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md new file mode 100644 index 00000000..2727594f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -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 + + + +# Target Audience + + + +# Capabilities + + diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..8df8c321 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -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 + + + +# Scope + + + +# Options + + + +# Related + + diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..d3bd2f1b --- /dev/null +++ b/.github/workflows/docker.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a8d5be8..0310b6d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,7 +144,7 @@ docker build . --target bolt-ai-development **Option 3: Docker Compose Profile** ```bash -docker-compose --profile development up +docker compose --profile development up ``` #### Running the Development Container @@ -171,7 +171,7 @@ docker build . --target bolt-ai-production **Option 3: Docker Compose Profile** ```bash -docker-compose --profile production up +docker compose --profile production up ``` #### Running the Production Container diff --git a/Dockerfile b/Dockerfile index 06541d30..d287d407 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,8 +25,10 @@ ARG ANTHROPIC_API_KEY ARG OPEN_ROUTER_API_KEY ARG GOOGLE_GENERATIVE_AI_API_KEY ARG OLLAMA_API_BASE_URL +ARG XAI_API_KEY ARG TOGETHER_API_KEY ARG TOGETHER_API_BASE_URL +ARG AWS_BEDROCK_CONFIG ARG VITE_LOG_LEVEL=debug ARG DEFAULT_NUM_CTX @@ -38,16 +40,19 @@ ENV WRANGLER_SEND_METRICS=false \ OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \ GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \ OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ + XAI_API_KEY=${XAI_API_KEY} \ TOGETHER_API_KEY=${TOGETHER_API_KEY} \ TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ + AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \ VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} + DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\ + RUNNING_IN_DOCKER=true # Pre-configure wrangler to disable metrics RUN mkdir -p /root/.config/.wrangler && \ echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json -RUN npm run build +RUN pnpm run build CMD [ "pnpm", "run", "dockerstart"] @@ -62,6 +67,7 @@ ARG ANTHROPIC_API_KEY ARG OPEN_ROUTER_API_KEY ARG GOOGLE_GENERATIVE_AI_API_KEY ARG OLLAMA_API_BASE_URL +ARG XAI_API_KEY ARG TOGETHER_API_KEY ARG TOGETHER_API_BASE_URL ARG VITE_LOG_LEVEL=debug @@ -74,10 +80,13 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \ OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \ GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \ OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ + XAI_API_KEY=${XAI_API_KEY} \ TOGETHER_API_KEY=${TOGETHER_API_KEY} \ TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ + AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \ VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} + DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\ + RUNNING_IN_DOCKER=true RUN mkdir -p ${WORKDIR}/run CMD pnpm run dev --host diff --git a/LICENSE b/LICENSE index 79290241..8fb312e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 StackBlitz, Inc. +Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 00000000..33e697ef --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,57 @@ +# Project management of bolt.diy + +First off: this sounds funny, we know. "Project management" comes from a world of enterprise stuff and this project is +far from being enterprisy- it's still anarchy all over the place 😉 + +But we need to organize ourselves somehow, right? + +> tl;dr: We've got a project board with epics and features. We use PRs as change log and as materialized features. Find it [here](https://github.com/orgs/stackblitz-labs/projects/4). + +Here's how we structure long-term vision, mid-term capabilities of the software and short term improvements. + +## Strategic epics (long-term) + +Strategic epics define areas in which the product evolves. Usually, these epics don’t overlap. They shall allow the core +team to define what they believe is most important and should be worked on with the highest priority. + +You can find the [epics as issues](https://github.com/stackblitz-labs/bolt.diy/labels/epic) which are probably never +going to be closed. + +What's the benefit / purpose of epics? + +1. Prioritization + +E. g. we could say “managing files is currently more important that quality”. Then, we could thing about which features +would bring “managing files” forward. It may be different features, such as “upload local files”, “import from a repo” +or also undo/redo/commit. + +In a more-or-less regular meeting dedicated for that, the core team discusses which epics matter most, sketch features +and then check who can work on them. After the meeting, they update the roadmap (at least for the next development turn) +and this way communicate where the focus currently is. + +2. Grouping of features + +By linking features with epics, we can keep them together and document *why* we invest work into a particular thing. + +## Features (mid-term) + +We all know probably a dozen of methodologies following which features are being described (User story, business +function, you name it). + +However, we intentionally describe features in a more vague manner. Why? Everybody loves crisp, well-defined +acceptance-criteria, no? Well, every product owner loves it. because he knows what he’ll get once it’s done. + +But: **here is no owner of this product**. Therefore, we grant *maximum flexibility to the developer contributing a feature* – so that he can bring in his ideas and have most fun implementing it. + +The feature therefore tries to describe *what* should be improved but not in detail *how*. + +## PRs as materialized features (short-term) + +Once a developer starts working on a feature, a draft-PR *can* be opened asap to share, describe and discuss, how the feature shall be implemented. But: this is not a must. It just helps to get early feedback and get other developers involved. Sometimes, the developer just wants to get started and then open a PR later. + +In a loosely organized project, it may as well happen that multiple PRs are opened for the same feature. This is no real issue: Usually, peoply being passionate about a solution are willing to join forces and get it done together. And if a second developer was just faster getting the same feature realized: Be happy that it's been done, close the PR and look out for the next feature to implement 🤓 + +## PRs as change log + +Once a PR is merged, a squashed commit contains the whole PR description which allows for a good change log. +All authors of commits in the PR are mentioned in the squashed commit message and become contributors 🙌 diff --git a/README.md b/README.md index c50f4277..4da9c4d1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # 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 +28,15 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed ## Join the community -[Join the bolt.diy community here, in the thinktank on ottomator.ai!](https://thinktank.ottomator.ai) +[Join the bolt.diy community here, in the oTTomator Think Tank!](https://thinktank.ottomator.ai) +## Project management + +Bolt.diy is a community effort! Still, the core team of contributors aims at organizing the project in way that allows +you to understand where the current areas of focus are. + +If you want to know what we are working on, what we are planning to work on, or if you want to contribute to the +project, please check the [project management guide](./PROJECT.md) to get started easily. ## Requested Additions @@ -47,6 +59,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed - ✅ Bolt terminal to see the output of LLM run commands (@thecodacus) - ✅ Streaming of code output (@thecodacus) - ✅ Ability to revert code to earlier version (@wonderwhy-er) +- ✅ Chat history backup and restore functionality (@sidbetatester) - ✅ Cohere Integration (@hasanraiyan) - ✅ Dynamic model max token length (@hasanraiyan) - ✅ Better prompt enhancing (@SujalXplores) @@ -55,7 +68,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed - ✅ Together Integration (@mouimet-infinisoft) - ✅ Mobile friendly (@qwikode) - ✅ Better prompt enhancing (@SujalXplores) -- ✅ Attach images to prompts (@atrokhym) +- ✅ Attach images to prompts (@atrokhym)(@stijnus) - ✅ Added Git Clone button (@thecodacus) - ✅ Git Import from url (@thecodacus) - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus) @@ -64,6 +77,8 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed - ✅ Detect terminal Errors and ask bolt to fix it (@thecodacus) - ✅ Detect preview Errors and ask bolt to fix it (@wonderwhy-er) - ✅ Add Starter Template Options (@thecodacus) +- ✅ Perplexity Integration (@meetpateltech) +- ✅ AWS Bedrock Integration (@kunjabijukchhe) - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs) - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start) - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call @@ -73,12 +88,14 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed - ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc. - ⬜ Voice prompting - ⬜ Azure Open AI API Integration -- ✅ Perplexity Integration (@meetpateltech) - ⬜ Vertex AI Integration +- ⬜ Granite Integration +- ✅ Popout Window for Web Container(@stijnus) +- ✅ Ability to change Popout window size (@stijnus) ## Features -- **AI-powered full-stack web development** directly in your browser. +- **AI-powered full-stack web development** for **NodeJS based applications** directly in your browser. - **Support for multiple LLMs** with an extensible architecture to integrate additional models. - **Attach images to prompts** for better contextual understanding. - **Integrated terminal** to view output of LLM-run commands. @@ -86,21 +103,18 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed - **Download projects as ZIP** for easy portability. - **Integration-ready Docker support** for a hassle-free setup. -## Setup +## Setup -If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time. +If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time. Let's get you up and running with the stable version of Bolt.DIY! ## Quick Download -[![Download Latest Release](https://img.shields.io/github/v/release/stackblitz-labs/bolt.diy?label=Download%20Bolt&sort=semver)](https://github.com/stackblitz-labs/bolt.diy/releases/latest) ← Click here to go the the latest release version! +[![Download Latest Release](https://img.shields.io/github/v/release/stackblitz-labs/bolt.diy?label=Download%20Bolt&sort=semver)](https://github.com/stackblitz-labs/bolt.diy/releases/latest) ← Click here to go the the latest release version! - Next **click source.zip** - - - ## Prerequisites Before you begin, you'll need to install two important pieces of software: @@ -133,16 +147,19 @@ You have two options for running Bolt.DIY: directly on your machine or using Doc ### Option 1: Direct Installation (Recommended for Beginners) 1. **Install Package Manager (pnpm)**: + ```bash npm install -g pnpm ``` 2. **Install Project Dependencies**: + ```bash pnpm install ``` 3. **Start the Application**: + ```bash pnpm run dev ``` @@ -154,11 +171,13 @@ You have two options for running Bolt.DIY: directly on your machine or using Doc This option requires some familiarity with Docker but provides a more isolated environment. #### Additional Prerequisite + - Install Docker: [Download Docker](https://www.docker.com/) #### Steps: 1. **Build the Docker Image**: + ```bash # Using npm script: npm run dockerbuild @@ -169,12 +188,9 @@ This option requires some familiarity with Docker but provides a more isolated e 2. **Run the Container**: ```bash - docker-compose --profile development up + docker compose --profile development up ``` - - - ## Configuring API Keys and Providers ### Adding Your API Keys @@ -203,6 +219,7 @@ For providers that support custom base URLs (such as Ollama or LM Studio), follo > **Note**: Custom base URLs are particularly useful when running local instances of AI models or using custom API endpoints. ### Supported Providers + - Ollama - LM Studio - OpenAILike @@ -210,23 +227,27 @@ For providers that support custom base URLs (such as Ollama or LM Studio), follo ## Setup Using Git (For Developers only) This method is recommended for developers who want to: + - Contribute to the project - Stay updated with the latest changes - Switch between different versions - Create custom modifications #### Prerequisites + 1. Install Git: [Download Git](https://git-scm.com/downloads) #### Initial Setup 1. **Clone the Repository**: + ```bash # Using HTTPS git clone https://github.com/stackblitz-labs/bolt.diy.git ``` 2. **Navigate to Project Directory**: + ```bash cd bolt.diy ``` @@ -236,6 +257,7 @@ This method is recommended for developers who want to: git checkout main ``` 4. **Install Dependencies**: + ```bash pnpm install ``` @@ -250,16 +272,19 @@ This method is recommended for developers who want to: To get the latest changes from the repository: 1. **Save Your Local Changes** (if any): + ```bash git stash ``` 2. **Pull Latest Updates**: + ```bash git pull origin main ``` 3. **Update Dependencies**: + ```bash pnpm install ``` @@ -274,6 +299,7 @@ To get the latest changes from the repository: If you encounter issues: 1. **Clean Installation**: + ```bash # Remove node modules and lock files rm -rf node_modules pnpm-lock.yaml diff --git a/app/components/chat/APIKeyManager.tsx b/app/components/chat/APIKeyManager.tsx index d4020486..92263363 100644 --- a/app/components/chat/APIKeyManager.tsx +++ b/app/components/chat/APIKeyManager.tsx @@ -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 = {}; + const apiKeyMemoizeCache: { [k: string]: Record } = {}; export function getApiKeysFromCookies() { const storedApiKeys = Cookies.get('apiKeys'); - let parsedKeys = {}; + let parsedKeys: Record = {}; if (storedApiKeys) { parsedKeys = apiKeyMemoizeCache[storedApiKeys]; @@ -32,54 +35,135 @@ export function getApiKeysFromCookies() { export const APIKeyManager: React.FC = ({ 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 ( -
-
- {provider?.name} API Key: - {!isEditing && ( -
- - {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'} - - setIsEditing(true)} title="Edit API Key"> -
- -
- )} +
+
+
+ {provider?.name} API Key: + {!isEditing && ( +
+ {apiKey ? ( + <> +
+ Set via UI + + ) : isEnvKeySet ? ( + <> +
+ Set via environment variable + + ) : ( + <> +
+ Not Set (Please set via UI or ENV_VAR) + + )} +
+ )} +
- {isEditing ? ( -
- 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" - /> - -
- - setIsEditing(false)} title="Cancel"> -
- -
- ) : ( - <> - {provider?.getApiKeyLink && ( - window.open(provider?.getApiKeyLink)} title="Edit API Key"> - {provider?.labelForGetApiKey || 'Get API Key'} -
+
+ {isEditing ? ( +
+ 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" + /> + +
- )} - - )} + setIsEditing(false)} + title="Cancel" + className="bg-red-500/10 hover:bg-red-500/20 text-red-500" + > +
+ +
+ ) : ( + <> + { + setIsEditing(true)} + title="Edit API Key" + className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500" + > +
+ + } + {provider?.getApiKeyLink && !apiKey && ( + 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" + > + {provider?.labelForGetApiKey || 'Get API Key'} +
+ + )} + + )} +
); }; diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index be304c7b..60076bd8 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -1,6 +1,8 @@ import { memo } from 'react'; import { Markdown } from './Markdown'; import type { JSONValue } from 'ai'; +import type { ProgressAnnotation } from '~/types/context'; +import Popover from '~/components/ui/Popover'; interface AssistantMessageProps { content: string; @@ -10,7 +12,12 @@ interface AssistantMessageProps { export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => { const filteredAnnotations = (annotations?.filter( (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), - ) || []) as { type: string; value: any }[]; + ) || []) as { type: string; value: any } & { [key: string]: any }[]; + + let progressAnnotation: ProgressAnnotation[] = filteredAnnotations.filter( + (annotation) => annotation.type === 'progress', + ) as ProgressAnnotation[]; + progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value); const usage: { completionTokens: number; @@ -20,11 +27,18 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage return (
- {usage && ( -
- Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) + <> +
+ {progressAnnotation.length > 0 && ( + }>{progressAnnotation[0].message} + )} + {usage && ( +
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) +
+ )}
- )} + {content}
); diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 7e82b358..4bfc038c 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -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( ) => { const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const [apiKeys, setApiKeys] = useState>(getApiKeysFromCookies()); - const [modelList, setModelList] = useState(MODEL_LIST); + const [modelList, setModelList] = useState([]); const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false); const [isListening, setIsListening] = useState(false); const [recognition, setRecognition] = useState(null); const [transcript, setTranscript] = useState(''); const [isModelLoading, setIsModelLoading] = useState('all'); - const getProviderSettings = useCallback(() => { - let providerSettings: Record | 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( useEffect(() => { if (typeof window !== 'undefined') { - const providerSettings = getProviderSettings(); let parsedApiKeys: Record | undefined = {}; try { @@ -177,53 +154,48 @@ export const BaseChat = React.forwardRef( 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 = () => { diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index aa89e8c8..40dbe15d 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -137,35 +137,36 @@ export const ChatImpl = memo( const [apiKeys, setApiKeys] = useState>({}); - const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({ - api: '/api/chat', - body: { - apiKeys, - files, - promptId, - contextOptimization: contextOptimizationEnabled, - }, - sendExtraMessageFields: true, - onError: (error) => { - logger.error('Request failed\n\n', error); - toast.error( - 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'), - ); - }, - onFinish: (message, response) => { - const usage = response.usage; + const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } = + useChat({ + api: '/api/chat', + body: { + apiKeys, + files, + promptId, + contextOptimization: contextOptimizationEnabled, + }, + sendExtraMessageFields: true, + onError: (e) => { + logger.error('Request failed\n\n', e, error); + toast.error( + 'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'), + ); + }, + onFinish: (message, response) => { + const usage = response.usage; - if (usage) { - console.log('Token usage:', usage); + if (usage) { + console.log('Token usage:', usage); - // You can now use the usage data as needed - } + // You can now use the usage data as needed + } - logger.debug('Finished streaming'); - }, - initialMessages, - initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '', - }); + logger.debug('Finished streaming'); + }, + initialMessages, + initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '', + }); useEffect(() => { const prompt = searchParams.get('prompt'); @@ -263,13 +264,17 @@ export const ChatImpl = memo( */ await workbenchStore.saveAllFiles(); + if (error != null) { + setMessages(messages.slice(0, -1)); + } + const fileModifications = workbenchStore.getFileModifcations(); chatStore.setKey('aborted', false); runAnimation(); - if (!chatStarted && messageInput && autoSelectTemplate) { + if (!chatStarted && _input && autoSelectTemplate) { setFakeLoading(true); setMessages([ { @@ -291,13 +296,21 @@ export const ChatImpl = memo( // reload(); const { template, title } = await selectStarterTemplate({ - message: messageInput, + message: _input, model, provider, }); if (template !== 'blank') { - const temResp = await getTemplates(template, title); + const temResp = await getTemplates(template, title).catch((e) => { + if (e.message.includes('rate limit')) { + toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template'); + } else { + toast.warning('Failed to import starter template\n Continuing with blank template'); + } + + return null; + }); if (temResp) { const { assistantMessage, userMessage } = temResp; @@ -306,7 +319,7 @@ export const ChatImpl = memo( { id: `${new Date().getTime()}`, role: 'user', - content: messageInput, + content: _input, // annotations: ['hidden'], }, diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index 4fe4c55e..376d59d6 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -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} ${fileContents .map( @@ -82,29 +88,38 @@ ${file.content} ) .join('\n')} `, - 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 ( - + <> + + {loading && } + ); } diff --git a/app/components/chat/chatExportAndImport/ImportButtons.tsx b/app/components/chat/chatExportAndImport/ImportButtons.tsx index 208fd02b..5ad8bb56 100644 --- a/app/components/chat/chatExportAndImport/ImportButtons.tsx +++ b/app/components/chat/chatExportAndImport/ImportButtons.tsx @@ -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) | undefined) { return (
@@ -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); diff --git a/app/components/git/GitUrlImport.client.tsx b/app/components/git/GitUrlImport.client.tsx index c2c949ec..fe8b346b 100644 --- a/app/components/git/GitUrlImport.client.tsx +++ b/app/components/git/GitUrlImport.client.tsx @@ -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} - + const filesMessage: Message = { + role: 'assistant', + content: `Cloning the repo ${repoUrl} into ${workdir} + ${fileContents .map( (file) => @@ -85,17 +84,25 @@ ${file.content} ) .join('\n')} `, - 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; } } }; diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx index aac2fe0f..9219d015 100644 --- a/app/components/settings/data/DataTab.tsx +++ b/app/components/settings/data/DataTab.tsx @@ -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 (
@@ -247,6 +324,12 @@ export default function DataTab() { > Export All Chats + + ))} +
+ + )} +
+
{