diff --git a/.env.example b/.env.example index 52b7284e76e0d0834a7488f8cd29debe6fe2bc08..22c26c4b14c5d506717fc18499813222b529dedc 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 797c61b99dfa93826b7d423de8111c1cbb819f33..c2f0c8bb2ee85bf02fe7fc7ecd9b2772cf94e96b 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 0000000000000000000000000000000000000000..2727594f4bbf7a902f0dfee2658c608e567241f5 --- /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 0000000000000000000000000000000000000000..8df8c3217d5c2c27e30ed940986175bcd77845df --- /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 index 449834a3a69253ffc9fcda008062f617870f39cb..7ea6e9b4455a9936f21efe3e2d4609e41b8d1b15 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -10,6 +10,10 @@ on: - v* - "*" +permissions: + packages: write + contents: read + env: REGISTRY: ghcr.io DOCKER_IMAGE: ghcr.io/${{ github.repository }} @@ -61,6 +65,7 @@ jobs: context: . file: ./Dockerfile target: ${{ env.BUILD_TARGET }} + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1248a0b4cbf5d02178fbe20e6cfbd0367aa14dd..1133d2e5e1fb203430bd853ae76850a43aec074c 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/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000000000000000000000000000000000000..33e697ef6202fe3fcd75cc9c80767ac290f26f48 --- /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 2774999ea444dfc63c67c28b80fb30e4d9c6c480..555e699c918d7a3c348d6f2ea3ecc7b4e2ecdbd3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,14 @@ ---- -title: bolt.diy -emoji: 📉 -colorFrom: red -colorTo: red -sdk: docker -app_port: 5173 -pinned: false ---- - # 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/). @@ -33,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 @@ -57,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) @@ -65,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) @@ -74,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 @@ -84,12 +89,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. @@ -97,21 +104,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: @@ -144,16 +148,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 ``` @@ -165,11 +172,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 @@ -180,12 +189,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 @@ -214,6 +220,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 @@ -221,23 +228,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 ``` @@ -247,6 +258,7 @@ This method is recommended for developers who want to: git checkout main ``` 4. **Install Dependencies**: + ```bash pnpm install ``` @@ -261,16 +273,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 ``` @@ -285,6 +300,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 3bd845244d32043d292214e8a9fda2069befdf88..9c10b6fc097bb1c3dfb617f0aa30f323f9fbbae5 100644 --- a/app/components/chat/APIKeyManager.tsx +++ b/app/components/chat/APIKeyManager.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { IconButton } from '~/components/ui/IconButton'; import { Switch } from '~/components/ui/Switch'; import type { ProviderInfo } from '~/types/model'; import Cookies from 'js-cookie'; - interface APIKeyManagerProps { provider: ProviderInfo; apiKey: string; @@ -12,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,99 +34,151 @@ export function getApiKeysFromCookies() { // eslint-disable-next-line @typescript-eslint/naming-convention export const APIKeyManager: React.FC = ({ provider, apiKey, setApiKey }) => { const [isEditing, setIsEditing] = useState(false); - const [tempKey, setTempKey] = useState(apiKey || cachedApiKeysOps().get()); + const [tempKey, setTempKey] = useState(apiKey); const [isPromptCachingEnabled, setIsPromptCachingEnabled] = useState(() => { // Read initial state from localStorage, defaulting to true const savedState = localStorage.getItem('PROMPT_CACHING_ENABLED'); return savedState !== null ? JSON.parse(savedState) : true; }); + const [isEnvKeySet, setIsEnvKeySet] = useState(false); - function cachedApiKeysOps() { - const STORAGE_KEY = 'PROVIDER_API_KEYS'; + useEffect(() => { + // Update localStorage whenever the prompt caching state changes + localStorage.setItem('PROMPT_CACHING_ENABLED', JSON.stringify(isPromptCachingEnabled)); + }, [isPromptCachingEnabled]); - const providerName = provider?.name; - const getStoredData = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); + // 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] || ''; - const get = () => { - const savedData = getStoredData(); - return providerName ? savedData[providerName] || '' : savedData; - }; + setTempKey(savedKey); + setApiKey(savedKey); + setIsEditing(false); + }, [provider.name]); - const save = (value: string) => { - const savedData = getStoredData(); - savedData[providerName] = value; - localStorage.setItem(STORAGE_KEY, JSON.stringify(savedData)); + const checkEnvApiKey = useCallback(async () => { + // Check cache first + if (providerEnvKeyStatusCache[provider.name] !== undefined) { + setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]); + return; + } - return value; - }; + 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]); - return { get, save }; - } + useEffect(() => { + checkEnvApiKey(); + }, [checkEnvApiKey]); const handleSave = () => { + // Save to parent state setApiKey(tempKey); - setIsEditing(false); - cachedApiKeysOps().save(tempKey); - }; - useEffect(() => { - if (tempKey) { - setApiKey(tempKey); - } - }, [tempKey]); + // Save to cookies + const currentKeys = getApiKeysFromCookies(); + const newKeys = { ...currentKeys, [provider.name]: tempKey }; + Cookies.set('apiKeys', JSON.stringify(newKeys)); - useEffect(() => { - // Update localStorage whenever the prompt caching state changes - localStorage.setItem('PROMPT_CACHING_ENABLED', JSON.stringify(isPromptCachingEnabled)); - }, [isPromptCachingEnabled]); + setIsEditing(false); + }; return ( -
-
-
- {provider?.name} API Key: - {!isEditing && ( -
- - {apiKey ? '••••••••' : API Key Required} - - 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="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'} +
+ + )} + )}
- - {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'} -
- - )} - - )}
{provider?.name === 'Anthropic' && ( -
+

- When enabled, allows caching of prompts for 10x cheaper responses. Recommended for Claude models. + When enabled, generates 10x cheaper responses if re-prompted within 5 mins (Recommended)

)} diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 7e82b3581f87f662cb0e136601c3ce57f4cfb95e..4bfc038c526e94283a9c929d78e9928bdb1fb14f 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 cbb4af37030ea5554a5245c02f113b0fe61c14b9..be337deb6eeedeae06a413da066193d3682a0b33 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -156,36 +156,37 @@ 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, - isPromptCachingEnabled: provider.name === 'Anthropic' && isPromptCachingEnabled(), - }, - 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; - - if (usage) { - console.log('Token usage:', usage); - - // You can now use the usage data as needed - } + const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } = + useChat({ + api: '/api/chat', + body: { + apiKeys, + files, + promptId, + contextOptimization: contextOptimizationEnabled, + isPromptCachingEnabled: provider.name === 'Anthropic' && isPromptCachingEnabled(), + }, + 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; - logger.debug('Finished streaming'); - }, - initialMessages, - initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '', - }); + if (usage) { + console.log('Token usage:', usage); + + // You can now use the usage data as needed + } + + logger.debug('Finished streaming'); + }, + initialMessages, + initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '', + }); useEffect(() => { const prompt = searchParams.get('prompt'); @@ -283,6 +284,10 @@ export const ChatImpl = memo( */ await workbenchStore.saveAllFiles(); + if (error != null) { + setMessages(messages.slice(0, -1)); + } + const fileModifications = workbenchStore.getFileModifcations(); chatStore.setKey('aborted', false); diff --git a/app/components/chat/chatExportAndImport/ImportButtons.tsx b/app/components/chat/chatExportAndImport/ImportButtons.tsx index 208fd02b9e9e4fbc091bef2408fa662c5c79279c..5ad8bb561c8869d88dd7ec92559aeea78d8aa825 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; + + // Standard format + if (Array.isArray(data.messages)) { + await importChat(data.description || 'Imported Chat', data.messages); + toast.success('Chat imported successfully'); - if (!Array.isArray(data.messages)) { - toast.error('Invalid chat file format'); + 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 bc59899bee5134ec37515b70a60caaca95d63c03..fe8b346bf7f78b3bac952fc88051cbf495761529 100644 --- a/app/components/git/GitUrlImport.client.tsx +++ b/app/components/git/GitUrlImport.client.tsx @@ -1,141 +1,141 @@ -import { useSearchParams } from '@remix-run/react'; -import { generateId, type Message } from 'ai'; -import ignore from 'ignore'; -import { useEffect, useState } from 'react'; -import { ClientOnly } from 'remix-utils/client-only'; -import { BaseChat } from '~/components/chat/BaseChat'; -import { Chat } from '~/components/chat/Chat.client'; -import { useGit } from '~/lib/hooks/useGit'; -import { useChatHistory } from '~/lib/persistence'; -import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands'; -import { LoadingOverlay } from '~/components/ui/LoadingOverlay'; -import { toast } from 'react-toastify'; - -const IGNORE_PATTERNS = [ - 'node_modules/**', - '.git/**', - '.github/**', - '.vscode/**', - '**/*.jpg', - '**/*.jpeg', - '**/*.png', - 'dist/**', - 'build/**', - '.next/**', - 'coverage/**', - '.cache/**', - '.vscode/**', - '.idea/**', - '**/*.log', - '**/.DS_Store', - '**/npm-debug.log*', - '**/yarn-debug.log*', - '**/yarn-error.log*', - '**/*lock.json', - '**/*lock.yaml', -]; - -export function GitUrlImport() { - const [searchParams] = useSearchParams(); - const { ready: historyReady, importChat } = useChatHistory(); - const { ready: gitReady, gitClone } = useGit(); - const [imported, setImported] = useState(false); - const [loading, setLoading] = useState(true); - - const importRepo = async (repoUrl?: string) => { - if (!gitReady && !historyReady) { - return; - } - - if (repoUrl) { - const ig = ignore().add(IGNORE_PATTERNS); - - try { - const { workdir, data } = await gitClone(repoUrl); - - if (importChat) { - const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); - const textDecoder = new TextDecoder('utf-8'); - - 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 commands = await detectProjectCommands(fileContents); - const commandsMessage = createCommandsMessage(commands); - - const filesMessage: Message = { - role: 'assistant', - content: `Cloning the repo ${repoUrl} into ${workdir} - -${fileContents - .map( - (file) => - ` -${file.content} -`, - ) - .join('\n')} -`, - id: generateId(), - createdAt: new Date(), - }; - - const messages = [filesMessage]; - - 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 = '/'; - - return; - } - } - }; - - useEffect(() => { - if (!historyReady || !gitReady || imported) { - return; - } - - const url = searchParams.get('url'); - - if (!url) { - window.location.href = '/'; - return; - } - - importRepo(url).catch((error) => { - console.error('Error importing repo:', error); - toast.error('Failed to import repository'); - setLoading(false); - window.location.href = '/'; - }); - setImported(true); - }, [searchParams, historyReady, gitReady, imported]); - - return ( - }> - {() => ( - <> - - {loading && } - - )} - - ); -} +import { useSearchParams } from '@remix-run/react'; +import { generateId, type Message } from 'ai'; +import ignore from 'ignore'; +import { useEffect, useState } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { BaseChat } from '~/components/chat/BaseChat'; +import { Chat } from '~/components/chat/Chat.client'; +import { useGit } from '~/lib/hooks/useGit'; +import { useChatHistory } from '~/lib/persistence'; +import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands'; +import { LoadingOverlay } from '~/components/ui/LoadingOverlay'; +import { toast } from 'react-toastify'; + +const IGNORE_PATTERNS = [ + 'node_modules/**', + '.git/**', + '.github/**', + '.vscode/**', + '**/*.jpg', + '**/*.jpeg', + '**/*.png', + 'dist/**', + 'build/**', + '.next/**', + 'coverage/**', + '.cache/**', + '.vscode/**', + '.idea/**', + '**/*.log', + '**/.DS_Store', + '**/npm-debug.log*', + '**/yarn-debug.log*', + '**/yarn-error.log*', + '**/*lock.json', + '**/*lock.yaml', +]; + +export function GitUrlImport() { + const [searchParams] = useSearchParams(); + const { ready: historyReady, importChat } = useChatHistory(); + const { ready: gitReady, gitClone } = useGit(); + const [imported, setImported] = useState(false); + const [loading, setLoading] = useState(true); + + const importRepo = async (repoUrl?: string) => { + if (!gitReady && !historyReady) { + return; + } + + if (repoUrl) { + const ig = ignore().add(IGNORE_PATTERNS); + + try { + const { workdir, data } = await gitClone(repoUrl); + + if (importChat) { + const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); + const textDecoder = new TextDecoder('utf-8'); + + 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 commands = await detectProjectCommands(fileContents); + const commandsMessage = createCommandsMessage(commands); + + const filesMessage: Message = { + role: 'assistant', + content: `Cloning the repo ${repoUrl} into ${workdir} + +${fileContents + .map( + (file) => + ` +${file.content} +`, + ) + .join('\n')} +`, + id: generateId(), + createdAt: new Date(), + }; + + const messages = [filesMessage]; + + 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 = '/'; + + return; + } + } + }; + + useEffect(() => { + if (!historyReady || !gitReady || imported) { + return; + } + + const url = searchParams.get('url'); + + if (!url) { + window.location.href = '/'; + return; + } + + importRepo(url).catch((error) => { + console.error('Error importing repo:', error); + toast.error('Failed to import repository'); + setLoading(false); + window.location.href = '/'; + }); + setImported(true); + }, [searchParams, historyReady, gitReady, imported]); + + return ( + }> + {() => ( + <> + + {loading && } + + )} + + ); +} diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx index aac2fe0fb5b1c8479533a3a596b795bead001ba1..9219d01557d3afa7e8861b1f54eac0368133c1c3 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 + + ))} +
+ + )} +
+
{