164 Commits

Author SHA1 Message Date
github-actions[bot]
5a0489f3c3 chore: release version 0.0.6 2025-01-22 20:54:02 +00:00
Anirban Kar
3c56346e83 feat: enhance context handling by adding code context selection and implementing summary generation (#1091) #release
* feat: add context annotation types and enhance file handling in LLM processing

* feat: enhance context handling by adding chatId to annotations and implementing summary generation

* removed useless changes

* feat: updated token counts to include optimization requests

* prompt fix

* logging added

* useless logs removed
2025-01-22 22:48:13 +05:30
Leex
2ae897aae7 fix: get environment variables for docker #1120
fix: get environment variables for docker
2025-01-22 13:01:10 +01:00
Anirban Kar
46f15bdde6 fix: updated system prompt to have correct indentations (#1139)
* updated system prompt to have correct indentations

* removed a section
2025-01-22 01:59:07 +05:30
Anirban Kar
0ad4aa56d3 feat: added deepseek reasoner model in deepseek provider (#1151) 2025-01-22 01:58:31 +05:30
Anirban Kar
f29380e147 fix: auto select starter template bugfix (#1148) 2025-01-22 00:30:31 +05:30
Leex
d62e211d09 Merge pull request #1122 from Stijnus/FEAT_BoltDYI_PREVIEW_V3
fix: for Open preview in a new tab.
2025-01-19 00:17:13 +01:00
Leex
031e679aff Merge branch 'main' into FEAT_BoltDYI_PREVIEW_V3 2025-01-19 00:14:09 +01:00
Leex
6ae1ac25d2 Merge pull request #1094 from lewis617/patch-1
docs: replace docker-compose with docker compose
2025-01-19 00:11:39 +01:00
Leex
b842e0c920 Merge pull request #1124 from stackblitz-labs/leex279-patch-readme-changes-v1
docs: update README.md
2025-01-18 23:41:05 +01:00
Stijnus
48f4999f32 Update Preview.tsx 2025-01-18 21:45:29 +01:00
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
Stijnus
b732f20233 bug fix for Open preview in a new tab. 2025-01-18 19:25:01 +01:00
Ken Jenney
7341b1292b Get environment variables from .env.local
Environment variables are not being passed to the container in the development profile. Adding env_file to pass them so they can be used by the application.
2025-01-18 12:38:34 -05: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
lewis liu
9958496468 fix: replace docker-compose with docker compose 2025-01-15 17:47:48 +08:00
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
github-actions[bot]
608d9338a8 chore: release version 0.0.5 2024-12-31 22:28:11 +00:00
Anirban Kar
67d984c52c fix: hotfix auto select starter template works without github token #release (#959)
* fix: hotfix starter template fix, updated header link to use navigate

* template auth fix

* updated changelog script
2025-01-01 03:57:55 +05:30
github-actions[bot]
1cf83454ed chore: release version 0.0.4 2024-12-31 17:17:50 +00:00
Anirban Kar
6494f5ac2e fix: updated logger and model caching minor bugfix #release (#895)
* fix: updated logger and model caching

* usage token stream issue fix

* minor changes

* updated starter template change to fix the app title

* starter template bigfix

* fixed hydretion errors and raw logs

* removed raw log

* made auto select template false by default

* more cleaner logs and updated logic to call dynamicModels only if not found in static models

* updated starter template instructions

* browser console log improved for firefox

* provider icons fix icons
2024-12-31 22:47:32 +05:30
Anirban Kar
389eedcac4 fix: better model loading ui feedback and model list update (#954)
* fix: better model loading feedback and model list update

* added load on providersettings  update
2024-12-31 19:22:46 +05:30
Eduard Ruzga
55cfd5d4ee fix: refresh model list after api key changes (#944) 2024-12-30 23:35:08 +05:30
Arsalaan Ahmed
e00264236e feat: added hyperbolic llm models (#943)
* Added Hyperbolic Models

* Fix: Fixed problem in connecting with hyperbolic models

* added dynamic models for hyperbolic

* removed logs
2024-12-30 23:26:33 +05:30
Eduard Ruzga
93c2a6e81d Merge pull request #939 from juanmcampos/fix-project-import-button
fix: import folder filtering
2024-12-30 19:39:27 +02:00
Eduard Ruzga
4c81cb02e1 fix: add defaults for LMStudio to work out of the box (#928) 2024-12-30 17:50:13 +05:30
Juan Manuel Campos Olvera
6e3b5dcbd7 fix: import folder filtering 2024-12-29 20:21:49 -06:00
Anirban Kar
4c81e154a0 feat: added Automatic Code Template Detection And Import (#867)
* initial setup

* updated template list

* added optional switch to control this feature

* removed some logs
2024-12-29 15:52:37 +05:30
Anirban Kar
3a36a4469a feat: redact file contents from chat and put latest files into system prompt (#904) 2024-12-29 15:36:31 +05:30
Eduard Ruzga
aecd9b1b40 fix: detect and remove markdown block syntax that llms sometimes hallucinate for file actions (#886)
* Clean out markdown syntax

* Remove identation removal

* Improve for streaming
2024-12-29 15:23:20 +05:30
Anirban Kar
4844db82fa ci: improved change-log generation script and cleaner release ci action (#896)
* build: improved-changelog

* added a better change log script

* improved changelog script

* improved change log script
2024-12-26 00:58:42 +05:30
Eduard Ruzga
7eee0386ff feat: catch errors from web container preview and show in actionable alert so user can send them to AI for fixing (#856)
* Catch errors from web container

* Show fix error popup on errors in preview

* Remove unneeded action type

* PR comments

* Cleanup urls in stacktrace

---------

Co-authored-by: Anirban Kar <thecodacus@gmail.com>
2024-12-25 03:05:44 +05:30
Anirban Kar
fc4f89f806 feat: add Starter template menu in homepage (#884)
* added icons and component

* updated unocss to add dynamic icons

* removed temp logs

* updated readme
2024-12-24 22:37:28 +05:30
Anirban Kar
8185fd56ca fix: check for updates does not look for commit.json now (#861) 2024-12-24 13:34:46 +05:30
Alex Parker
d673206952 docs: updated env.example of OLLAMA & LMSTUDIO base url (#877)
* correct OLLAMA_API_BASE_URL

* correct OLLAMA_API_BASE_URL

* correct OLLAMA_API_BASE_URL
2024-12-24 13:32:04 +05:30
Eduard Ruzga
79e7e75228 Merge pull request #797 from thecodacus/terminal-error-detection
feat: enhanced Terminal Error Handling and Alert System
2024-12-23 14:25:39 +02:00
Dustin Loring
1f938fca40 feat; data tab added to the settings
added a data tab to the settings
removed chat history
2024-12-22 06:20:14 -05:00
Anirban Kar
8b58c7a0fb fix: ollama provider module base url hotfix for docker (#863)
* fix: ollama base url hotfix

* cleanup logic
2024-12-22 01:25:48 +05:30
Anirban Kar
13b208df46 better osc code cleanup 2024-12-22 01:01:06 +05:30
Anirban Kar
d8943f9474 fix: provider menu dropdown fix (ghost providers) (#862) 2024-12-21 23:31:19 +05:30
Anirban Kar
47471e05d9 updated read me 2024-12-21 18:22:17 +05:30
Anirban Kar
28f0c3629d ignored alert on project reload 2024-12-21 17:44:18 +05:30
Anirban Kar
975e7d83fd Merge branch 'main' into terminal-error-detection 2024-12-21 16:14:46 +05:30
Anirban Kar
7295352a98 refactor: refactored LLM Providers: Adapting Modular Approach (#832)
* refactor: Refactoring Providers to have providers as modules

* updated package and lock file

* added grok model back

* updated registry system
2024-12-21 11:45:17 +05:30
Anirban Kar
63abf52000 fix hotfix for version metadata issue (#853) 2024-12-21 02:24:57 +05:30
Anirban Kar
df6925ac07 fix: add Message Processing Throttling to Prevent Browser Crashes (#848) 2024-12-21 02:13:29 +05:30
Anirban Kar
80d9800aa0 docs: updated download link (#850) 2024-12-21 01:52:45 +05:30
Anirban Kar
dbd24142ae ci: updated the docs ci to only trigger if any files changed in the docs folder (#849) 2024-12-21 01:09:44 +05:30
Cole Medin
7afd654210 Merge pull request #845 from dustinwloring1988/docs/updated-docs
docs: updated Docs
2024-12-20 13:37:16 -06:00
Anirban Kar
d8a277f0ac refactor: updated vite config to inject add version metadata into the app on build (#841)
* refactor: removes commit.json and used vite.config to load these variables

* updated precommit hook

* updated the pre start script

* updated the workflows
2024-12-21 01:03:28 +05:30
Dustin Loring
6145c603a1 Merge branch 'main' into docs/updated-docs 2024-12-20 14:21:50 -05:00
github-actions[bot]
56edbc4699 chore: update commit hash to ab5cde30a1 2024-12-20 19:17:25 +00:00
Dustin Loring
ab5cde30a1 docs: setup updated
docs: updated setup guide to have more detailed instructions
2024-12-20 14:17:12 -05:00
Dustin Loring
f2662c1c71 Merge branch 'main' into docs-setup-updated 2024-12-20 14:15:46 -05:00
Dustin Loring
ed5bad36ba docs: added info on both ways to set api keys 2024-12-20 10:02:06 -05:00
Dustin Loring
0c07bc9105 docs: added info on the Releases Page
Added the option to download from the Releases Page instead of git clone in the README
2024-12-20 09:50:47 -05:00
Dustin Loring
58db67683e docs: added info on updating using docker
Added docker-compose --profile development up --build  to the update section
2024-12-20 09:42:54 -05:00
Dustin Loring
7e70dc5d8e docs: updated Contributing
updated Contributing in the docs
updated Contributing and FAQ in the GitHub part as well
2024-12-20 09:37:34 -05:00
github-actions[bot]
2638c1a704 chore: update commit hash to 636f87f568 2024-12-19 20:17:35 +00:00
Dustin Loring
636f87f568 docs: faq style change, toc added to index
docs: updated style in FAQ
added: TOC to the index page
2024-12-19 15:17:21 -05:00
Dustin Loring
0733813af8 docs: updated style in faq
updated style in FAQ docs to be an accordion like style
added a TOC to the index page in the docs
2024-12-19 15:12:21 -05:00
github-actions[bot]
01599caf38 chore: update commit hash to 75ec49beb8 2024-12-19 18:08:12 +00:00
Dustin Loring
75ec49beb8 feat: providers list is now 2 columns
The providers list in the settings modal is now 2 columns allowing you to see more at once.
2024-12-19 13:07:58 -05:00
Dustin Loring
bbd9a17d3b Update ProvidersTab.tsx 2024-12-19 13:04:26 -05:00
Dustin Loring
a7551ecfcc Update ProvidersTab.tsx 2024-12-19 12:57:59 -05:00
Dustin Loring
db769e0c90 Merge branch 'main' into feat/improved-providers-list 2024-12-19 12:43:51 -05:00
github-actions[bot]
d4400a5704 chore: update commit hash to a53b10ff39 2024-12-19 15:53:04 +00:00
Anirban Kar
a53b10ff39 Merge pull request #800 from gokulkgm/xai-update
feat: add xAI grok-2-1212 model
2024-12-19 21:22:52 +05:30
github-actions[bot]
7e344aaa5c chore: update commit hash to 381d490f1f 2024-12-19 12:33:56 +00:00
Anirban Kar
381d490f1f Merge pull request #714 from kris1803/fix/described_by
fix: fixed console error for SettingsWIndow & Removed ts-nocheck
2024-12-19 18:03:46 +05:30
Anirban Kar
3e2fc32a46 Merge branch 'main' into fix/described_by 2024-12-19 18:02:43 +05:30
github-actions[bot]
1d716c0925 chore: update commit hash to de64007697 2024-12-19 11:25:23 +00:00
Dustin Loring
de64007697 docs: toc for readme
docs: added a TOC for the README
2024-12-19 06:25:11 -05:00
github-actions[bot]
906cc38e79 chore: update commit hash to 95dbcf1eb7 2024-12-19 09:20:48 +00:00
Anirban Kar
95dbcf1eb7 Merge pull request #789 from thecodacus/bolt-shell-race-condition
fix: added wait till terminal prompt for bolt shell execution
2024-12-19 14:50:37 +05:30
Anirban Kar
69c58c1410 Merge branch 'main' into bolt-shell-race-condition 2024-12-19 14:50:24 +05:30
Anirban Kar
602f65a4dc updated some text 2024-12-18 23:41:59 +05:30
Anirban Kar
1398344c11 docs: updated setup guide to have more detailed instructions 2024-12-18 23:34:41 +05:30
github-actions[bot]
296e5d9f77 chore: update commit hash to 50e6778784 2024-12-18 16:28:44 +00:00
Anirban Kar
50e6778784 Merge pull request #816 from thecodacus/env-file-fix
fix: fixed model not loading/working, even after baseUrl set in .env file
2024-12-18 21:58:32 +05:30
Anirban Kar
3fba4f0b61 Merge branch 'main' into env-file-fix 2024-12-18 21:57:02 +05:30
Anirban Kar
b892d708ec removed logs and openAI fix 2024-12-18 21:55:17 +05:30
Anirban Kar
d37c3736d5 removed logs 2024-12-18 21:34:18 +05:30
Anirban Kar
6975083fb9 stopped apikeys set to envfile being exposed to UI 2024-12-18 21:31:50 +05:30
Anirban Kar
620d13ad8a updated title header 2024-12-18 21:29:54 +05:30
Anirban Kar
b25b8b98f9 updated styling wordings and animations icons 2024-12-18 21:09:36 +05:30
github-actions[bot]
90c9c9c760 chore: update commit hash to 6458211bed 2024-12-18 15:12:36 +00:00
Cole Medin
6458211bed Merge pull request #822 from stackblitz-labs/documentation-enhancements
chore: a few documentation enhancements for main README and FAQs
2024-12-18 09:12:24 -06:00
Anirban Kar
3d8abee511 Merge branch 'main' into terminal-error-detection 2024-12-18 20:19:59 +05:30
Anirban Kar
26a3bcf9b6 Merge branch 'env-file-fix-old' into env-file-fix 2024-12-18 20:06:29 +05:30
Anirban Kar
283eb22ae5 added indicator on settings menu 2024-12-18 20:04:43 +05:30
Dustin Loring
7458b22780 fixed API Key import 2024-12-18 09:06:58 -05:00
Cole Medin
b3b4d74824 chore: a few documentation enhancements for main README and FAQs 2024-12-18 07:01:58 -06:00
github-actions[bot]
a1e902fe79 chore: update commit hash to a9309161e9 2024-12-18 12:31:51 +00:00
Cole Medin
a9309161e9 Merge pull request #817 from dustinwloring1988/docs/more-beginner-friendly
docs: simplified setup
2024-12-18 06:31:33 -06:00
Dustin Loring
278bd242b1 docs: simplified setup
Removed the section about environment variables. In docker removed the production build. Both of these can still be found in the docs.
2024-12-18 06:35:33 -05:00
Anirban Kar
e74d6cafb5 Merge branch 'main' into env-file-fix 2024-12-18 16:41:49 +05:30
Anirban Kar
62ebfe51a6 fix: .env file baseUrl Issue 2024-12-18 16:34:18 +05:30
Dustin Loring
ba4e78809c feat: improved providers list style
made the list 2 columns wide and separate out the experimental providers
2024-12-17 17:40:16 -05:00
github-actions[bot]
6003c165ef chore: update commit hash to e064803955 2024-12-17 22:12:17 +00:00
Dustin Loring
e064803955 fix: chat title character restriction
added: characters to valid list for chat title
2024-12-17 17:12:01 -05:00
Dustin Loring
1a0ede6774 Update DataTab.tsx 2024-12-17 16:57:16 -05:00
Dustin Loring
a9b15e56c9 feat: Data Tab
Removed Chat History Tab
Added Data Tab
Data tab can export and delete chat history, import API keys, import and export settings
2024-12-17 16:44:56 -05:00
Anirban Kar
fd193ee7d7 Merge branch 'main' into terminal-error-detection 2024-12-18 02:36:38 +05:30
github-actions[bot]
fce8999f27 chore: release version 0.0.3 2024-12-17 21:00:04 +00:00
Anirban Kar
eb6d435356 Merge pull request #784 from dustinwloring1988/fix/prompt-enhance #release
fix: prompt enhance patch
2024-12-18 02:29:45 +05:30
Anirban Kar
d51b0dcadf Merge branch 'main' into fix/prompt-enhance 2024-12-18 02:28:46 +05:30
Anirban Kar
18d04ca065 simplified the fix 2024-12-18 02:20:14 +05:30
GK
8d0f0da04a add: xAI grok-2-1212 model 2024-12-17 17:31:10 +00:00
Anirban Kar
d327cfea29 feat: added terminal error capturing and automated fix prompt 2024-12-17 21:19:43 +05:30
Dustin Loring
0e7937b835 Update README.md 2024-12-17 10:27:04 -05:00
github-actions[bot]
a2330084eb chore: update commit hash to 282beb96e2 2024-12-17 15:25:41 +00:00
Dustin Loring
282beb96e2 docs: miniflare error knowledge
added miniflare and wrangler error on Windows FAQ to the docs
2024-12-17 10:25:30 -05:00
Dustin Loring
206a6d7816 add/toc-for-readme
added a TOC for the README file, renamed some headings to better suite the TOC
2024-12-17 10:19:40 -05:00
Dustin Loring
defe73367e add: miniflare and wrangler error
added information to FAQ in docs
2024-12-17 07:56:25 -05:00
Anirban Kar
42bde1cae4 Merge branch 'main' into terminal-error-detection 2024-12-17 16:52:51 +05:30
Anirban Kar
e9e7857675 removed logs 2024-12-17 15:01:00 +05:30
Anirban Kar
69c0bf5873 fix: added wait till terminal prompt for bolt shell execution 2024-12-17 14:57:16 +05:30
Dustin Loring
2d57bd2746 add: charactors to the vaild list for chat titles
added common punctuation to the charactersValid
2024-12-16 18:55:38 -05:00
Dustin Loring
05146c18d6 fix: Prompt Enhance
Prompt Enhance option stopped, this fixes it
2024-12-16 18:40:04 -05:00
github-actions[bot]
0ee3736297 chore: update commit hash to 1e72d52278 2024-12-16 22:04:55 +00:00
Anirban Kar
1e72d52278 Merge pull request #781 from thecodacus/semantic-pull-pr
chore: Reintroducing semantic pull pr validation
2024-12-17 03:34:42 +05:30
Anirban Kar
35f5aaaa76 chore: adding back semantic pull pr check for better changelog system 2024-12-17 03:32:08 +05:30
Anirban Kar
d238c9353c Merge remote-tracking branch 'upstream/main' 2024-12-17 03:30:54 +05:30
Anirban Kar
99cbde505d Merge branch 'main' into terminal-error-detection 2024-12-16 21:59:36 +05:30
kris1803
2064c83177 Fixed console error for SettingsWIndow & Removed ts-nocheck where not needed 2024-12-14 00:39:27 +01:00
134 changed files with 8851 additions and 3022 deletions

View File

@@ -32,7 +32,8 @@ OPEN_ROUTER_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY= GOOGLE_GENERATIVE_AI_API_KEY=
# You only need this environment variable set if you want to use oLLAMA models # You only need this environment variable set if you want to use oLLAMA models
# EXAMPLE http://localhost:11434 # DONT USE http://localhost:11434 due to IPV6 issues
# USE EXAMPLE http://127.0.0.1:11434
OLLAMA_API_BASE_URL= OLLAMA_API_BASE_URL=
# You only need this environment variable set if you want to use OpenAI Like models # You only need this environment variable set if you want to use OpenAI Like models
@@ -50,6 +51,12 @@ OPENAI_LIKE_API_KEY=
# Get your Together API Key # Get your Together API Key
TOGETHER_API_KEY= TOGETHER_API_KEY=
# You only need this environment variable set if you want to use Hyperbolic models
#Get your Hyperbolics API Key at https://app.hyperbolic.xyz/settings
#baseURL="https://api.hyperbolic.xyz/v1/chat/completions"
HYPERBOLIC_API_KEY=
HYPERBOLIC_API_BASE_URL=
# Get your Mistral API Key by following these instructions - # Get your Mistral API Key by following these instructions -
# https://console.mistral.ai/api-keys/ # https://console.mistral.ai/api-keys/
# You only need this environment variable set if you want to use Mistral models # You only need this environment variable set if you want to use Mistral models
@@ -62,7 +69,8 @@ COHERE_API_KEY=
# Get LMStudio Base URL from LM Studio Developer Console # Get LMStudio Base URL from LM Studio Developer Console
# Make sure to enable CORS # Make sure to enable CORS
# Example: http://localhost:1234 # DONT USE http://localhost:1234 due to IPV6 issues
# Example: http://127.0.0.1:1234
LMSTUDIO_API_BASE_URL= LMSTUDIO_API_BASE_URL=
# Get your xAI API key # Get your xAI API key
@@ -75,6 +83,17 @@ XAI_API_KEY=
# You only need this environment variable set if you want to use Perplexity models # You only need this environment variable set if you want to use Perplexity models
PERPLEXITY_API_KEY= PERPLEXITY_API_KEY=
# Get your AWS configuration
# https://console.aws.amazon.com/iam/home
# The JSON should include the following keys:
# - region: The AWS region where Bedrock is available.
# - accessKeyId: Your AWS access key ID.
# - secretAccessKey: Your AWS secret access key.
# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials.
# Example JSON:
# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"}
AWS_BEDROCK_CONFIG=
# Include this environment variable if you want more logging for debugging locally # Include this environment variable if you want more logging for debugging locally
VITE_LOG_LEVEL=debug VITE_LOG_LEVEL=debug

View File

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

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

261
.github/scripts/generate-changelog.sh vendored Executable file
View File

@@ -0,0 +1,261 @@
#!/usr/bin/env bash
# Ensure we're running in bash
if [ -z "$BASH_VERSION" ]; then
echo "This script requires bash. Please run with: bash $0" >&2
exit 1
fi
# Ensure we're using bash 4.0 or later for associative arrays
if ((BASH_VERSINFO[0] < 4)); then
echo "This script requires bash version 4 or later" >&2
echo "Current bash version: $BASH_VERSION" >&2
exit 1
fi
# Set default values for required environment variables if not in GitHub Actions
if [ -z "$GITHUB_ACTIONS" ]; then
: "${GITHUB_SERVER_URL:=https://github.com}"
: "${GITHUB_REPOSITORY:=stackblitz-labs/bolt.diy}"
: "${GITHUB_OUTPUT:=/tmp/github_output}"
touch "$GITHUB_OUTPUT"
# Running locally
echo "Running locally - checking for upstream remote..."
MAIN_REMOTE="origin"
if git remote -v | grep -q "upstream"; then
MAIN_REMOTE="upstream"
fi
MAIN_BRANCH="main" # or "master" depending on your repository
# Ensure we have latest tags
git fetch ${MAIN_REMOTE} --tags
# Use the remote reference for git log
GITLOG_REF="${MAIN_REMOTE}/${MAIN_BRANCH}"
else
# Running in GitHub Actions
GITLOG_REF="HEAD"
fi
# Get the latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
# Start changelog file
echo "# 🚀 Release v${NEW_VERSION}" > changelog.md
echo "" >> changelog.md
echo "## What's Changed 🌟" >> changelog.md
echo "" >> changelog.md
if [ -z "$LATEST_TAG" ]; then
echo "### 🎉 First Release" >> changelog.md
echo "" >> changelog.md
echo "Exciting times! This marks our first release. Thanks to everyone who contributed! 🙌" >> changelog.md
echo "" >> changelog.md
COMPARE_BASE="$(git rev-list --max-parents=0 HEAD)"
else
echo "### 🔄 Changes since $LATEST_TAG" >> changelog.md
echo "" >> changelog.md
COMPARE_BASE="$LATEST_TAG"
fi
# Function to extract conventional commit type and associated emoji
get_commit_type() {
local msg="$1"
if [[ $msg =~ ^feat(\(.+\))?:|^feature(\(.+\))?: ]]; then echo "✨ Features"
elif [[ $msg =~ ^fix(\(.+\))?: ]]; then echo "🐛 Bug Fixes"
elif [[ $msg =~ ^docs(\(.+\))?: ]]; then echo "📚 Documentation"
elif [[ $msg =~ ^style(\(.+\))?: ]]; then echo "💎 Styles"
elif [[ $msg =~ ^refactor(\(.+\))?: ]]; then echo "♻️ Code Refactoring"
elif [[ $msg =~ ^perf(\(.+\))?: ]]; then echo "⚡ Performance Improvements"
elif [[ $msg =~ ^test(\(.+\))?: ]]; then echo "🧪 Tests"
elif [[ $msg =~ ^build(\(.+\))?: ]]; then echo "🛠️ Build System"
elif [[ $msg =~ ^ci(\(.+\))?: ]]; then echo "⚙️ CI"
elif [[ $msg =~ ^chore(\(.+\))?: ]]; then echo "" # Skip chore commits
else echo "🔍 Other Changes" # Default category with emoji
fi
}
# Initialize associative arrays
declare -A CATEGORIES
declare -A COMMITS_BY_CATEGORY
declare -A ALL_AUTHORS
declare -A NEW_CONTRIBUTORS
# Get all historical authors before the compare base
while IFS= read -r author; do
ALL_AUTHORS["$author"]=1
done < <(git log "${COMPARE_BASE}" --pretty=format:"%ae" | sort -u)
# Process all commits since last tag
while IFS= read -r commit_line; do
if [[ ! $commit_line =~ ^[a-f0-9]+\| ]]; then
echo "WARNING: Skipping invalid commit line format: $commit_line" >&2
continue
fi
HASH=$(echo "$commit_line" | cut -d'|' -f1)
COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2)
BODY=$(echo "$commit_line" | cut -d'|' -f3)
# Skip if hash doesn't match the expected format
if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
continue
fi
HASH=$(echo "$commit_line" | cut -d'|' -f1)
COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2)
BODY=$(echo "$commit_line" | cut -d'|' -f3)
# Validate hash format
if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
echo "WARNING: Invalid commit hash format: $HASH" >&2
continue
fi
# Check if it's a merge commit
if [[ $COMMIT_MSG =~ Merge\ pull\ request\ #([0-9]+) ]]; then
# echo "Processing as merge commit" >&2
PR_NUM="${BASH_REMATCH[1]}"
# Extract the PR title from the merge commit body
PR_TITLE=$(echo "$BODY" | grep -v "^Merge pull request" | head -n 1)
# Only process if it follows conventional commit format
CATEGORY=$(get_commit_type "$PR_TITLE")
if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
# Get PR author's GitHub username
GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login')
if [ -n "$GITHUB_USERNAME" ]; then
# Check if this is a first-time contributor
AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
ALL_AUTHORS["$AUTHOR_EMAIL"]=1
fi
CATEGORIES["$CATEGORY"]=1
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
else
COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
fi
fi
# Check if it's a squash merge by looking for (#NUMBER) pattern
elif [[ $COMMIT_MSG =~ \(#([0-9]+)\) ]]; then
# echo "Processing as squash commit" >&2
PR_NUM="${BASH_REMATCH[1]}"
# Only process if it follows conventional commit format
CATEGORY=$(get_commit_type "$COMMIT_MSG")
if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
# Get PR author's GitHub username
GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login')
if [ -n "$GITHUB_USERNAME" ]; then
# Check if this is a first-time contributor
AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
ALL_AUTHORS["$AUTHOR_EMAIL"]=1
fi
CATEGORIES["$CATEGORY"]=1
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
else
COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
fi
fi
else
# echo "Processing as regular commit" >&2
# Process conventional commits without PR numbers
CATEGORY=$(get_commit_type "$COMMIT_MSG")
if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
# Get commit author info
AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
# Try to get GitHub username using gh api
if [ -n "$GITHUB_ACTIONS" ] || command -v gh >/dev/null 2>&1; then
GITHUB_USERNAME=$(gh api "/repos/${GITHUB_REPOSITORY}/commits/${HASH}" --jq '.author.login' 2>/dev/null)
fi
if [ -n "$GITHUB_USERNAME" ]; then
# If we got GitHub username, use it
if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
ALL_AUTHORS["$AUTHOR_EMAIL"]=1
fi
CATEGORIES["$CATEGORY"]=1
COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by @$GITHUB_USERNAME"$'\n'
else
# Fallback to git author name if no GitHub username found
AUTHOR_NAME=$(git show -s --format='%an' "$HASH")
if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
NEW_CONTRIBUTORS["$AUTHOR_NAME"]=1
ALL_AUTHORS["$AUTHOR_EMAIL"]=1
fi
CATEGORIES["$CATEGORY"]=1
COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by $AUTHOR_NAME"$'\n'
fi
fi
fi
done < <(git log "${COMPARE_BASE}..${GITLOG_REF}" --pretty=format:"%H|%s|%b" --reverse --first-parent)
# Write categorized commits to changelog with their emojis
for category in "✨ Features" "🐛 Bug Fixes" "📚 Documentation" "💎 Styles" "♻️ Code Refactoring" "⚡ Performance Improvements" "🧪 Tests" "🛠️ Build System" "⚙️ CI" "🔍 Other Changes"; do
if [ -n "${COMMITS_BY_CATEGORY[$category]}" ]; then
echo "### $category" >> changelog.md
echo "" >> changelog.md
echo "${COMMITS_BY_CATEGORY[$category]}" >> changelog.md
echo "" >> changelog.md
fi
done
# Add first-time contributors section if there are any
if [ ${#NEW_CONTRIBUTORS[@]} -gt 0 ]; then
echo "## ✨ First-time Contributors" >> changelog.md
echo "" >> changelog.md
echo "A huge thank you to our amazing new contributors! Your first contribution marks the start of an exciting journey! 🌟" >> changelog.md
echo "" >> changelog.md
# Use readarray to sort the keys
readarray -t sorted_contributors < <(printf '%s\n' "${!NEW_CONTRIBUTORS[@]}" | sort)
for github_username in "${sorted_contributors[@]}"; do
echo "* 🌟 [@$github_username](https://github.com/$github_username)" >> changelog.md
done
echo "" >> changelog.md
fi
# Add compare link if not first release
if [ -n "$LATEST_TAG" ]; then
echo "## 📈 Stats" >> changelog.md
echo "" >> changelog.md
echo "**Full Changelog**: [\`$LATEST_TAG..v${NEW_VERSION}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/$LATEST_TAG...v${NEW_VERSION})" >> changelog.md
fi
# Output the changelog content
CHANGELOG_CONTENT=$(cat changelog.md)
{
echo "content<<EOF"
echo "$CHANGELOG_CONTENT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# Also print to stdout for local testing
echo "Generated changelog:"
echo "==================="
cat changelog.md
echo "==================="

View File

@@ -1,39 +0,0 @@
name: Update Commit Hash File
on:
push:
branches:
- main
permissions:
contents: write
jobs:
update-commit:
if: contains(github.event.head_commit.message, '#release') != true
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Get the latest commit hash
run: |
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
echo "CURRENT_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Update commit file
run: |
echo "{ \"commit\": \"$COMMIT_HASH\", \"version\": \"$CURRENT_VERSION\" }" > app/commit.json
- name: Commit and push the update
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add app/commit.json
git commit -m "chore: update commit hash to $COMMIT_HASH"
git push

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

@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'docs/**' # This will only trigger the workflow when files in docs directory change
permissions: permissions:
contents: write contents: write
jobs: jobs:

32
.github/workflows/semantic-pr.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Semantic Pull Request
on:
pull_request_target:
types: [opened, reopened, edited, synchronize]
permissions:
pull-requests: read
jobs:
main:
name: Validate PR Title
runs-on: ubuntu-latest
steps:
# https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
types: |
fix
feat
chore
build
ci
perf
docs
refactor
revert
test

View File

@@ -80,83 +80,17 @@ jobs:
NEW_VERSION=${{ steps.bump_version.outputs.new_version }} NEW_VERSION=${{ steps.bump_version.outputs.new_version }}
pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version
- name: Prepare changelog script
run: chmod +x .github/scripts/generate-changelog.sh
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
run: | env:
# Get the latest tag NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Start changelog file run: .github/scripts/generate-changelog.sh
echo "# Release v${{ steps.bump_version.outputs.new_version }}" > changelog.md
echo "" >> changelog.md
if [ -z "$LATEST_TAG" ]; then
echo "### 🎉 First Release" >> changelog.md
echo "" >> changelog.md
COMPARE_BASE="$(git rev-list --max-parents=0 HEAD)"
else
echo "### 🔄 Changes since $LATEST_TAG" >> changelog.md
echo "" >> changelog.md
COMPARE_BASE="$LATEST_TAG"
fi
# Function to extract conventional commit type
get_commit_type() {
if [[ $1 =~ ^feat:|^feature: ]]; then echo "✨ Features";
elif [[ $1 =~ ^fix: ]]; then echo "🐛 Bug Fixes";
elif [[ $1 =~ ^docs: ]]; then echo "📚 Documentation";
elif [[ $1 =~ ^style: ]]; then echo "💎 Styles";
elif [[ $1 =~ ^refactor: ]]; then echo "♻️ Code Refactoring";
elif [[ $1 =~ ^perf: ]]; then echo "⚡️ Performance Improvements";
elif [[ $1 =~ ^test: ]]; then echo "✅ Tests";
elif [[ $1 =~ ^build: ]]; then echo "🛠️ Build System";
elif [[ $1 =~ ^ci: ]]; then echo "⚙️ CI";
elif [[ $1 =~ ^chore: ]]; then echo "🔧 Chores";
else echo "🔍 Other Changes";
fi
}
# Generate categorized changelog
declare -A CATEGORIES
declare -A COMMITS_BY_CATEGORY
# Get commits since last tag or all commits if no tag exists
while IFS= read -r commit_line; do
HASH=$(echo "$commit_line" | cut -d'|' -f1)
MSG=$(echo "$commit_line" | cut -d'|' -f2)
PR_NUM=$(echo "$commit_line" | cut -d'|' -f3)
CATEGORY=$(get_commit_type "$MSG")
CATEGORIES["$CATEGORY"]=1
# Format commit message with PR link if available
if [ -n "$PR_NUM" ]; then
COMMITS_BY_CATEGORY["$CATEGORY"]+="- ${MSG#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
else
COMMITS_BY_CATEGORY["$CATEGORY"]+="- ${MSG#*: }"$'\n'
fi
done < <(git log "${COMPARE_BASE}..HEAD" --pretty=format:"%H|%s|%(trailers:key=PR-Number,valueonly)" --reverse)
# Write categorized commits to changelog
for category in "✨ Features" "🐛 Bug Fixes" "📚 Documentation" "💎 Styles" "♻️ Code Refactoring" "⚡️ Performance Improvements" "✅ Tests" "🛠️ Build System" "⚙️ CI" "🔧 Chores" "🔍 Other Changes"; do
if [ -n "${COMMITS_BY_CATEGORY[$category]}" ]; then
echo "#### $category" >> changelog.md
echo "" >> changelog.md
echo "${COMMITS_BY_CATEGORY[$category]}" >> changelog.md
echo "" >> changelog.md
fi
done
# Add compare link if not first release
if [ -n "$LATEST_TAG" ]; then
echo "**Full Changelog**: [\`$LATEST_TAG..v${{ steps.bump_version.outputs.new_version }}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/$LATEST_TAG...v${{ steps.bump_version.outputs.new_version }})" >> changelog.md
fi
# Save changelog content for the release
CHANGELOG_CONTENT=$(cat changelog.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Get the latest commit hash and version tag - name: Get the latest commit hash and version tag
run: | run: |
@@ -166,8 +100,7 @@ jobs:
- name: Commit and Tag Release - name: Commit and Tag Release
run: | run: |
git pull git pull
echo "{ \"commit\": \"$COMMIT_HASH\", \"version\": \"$NEW_VERSION\" }" > app/commit.json git add package.json pnpm-lock.yaml changelog.md
git add package.json pnpm-lock.yaml changelog.md app/commit.json
git commit -m "chore: release version ${{ steps.bump_version.outputs.new_version }}" git commit -m "chore: release version ${{ steps.bump_version.outputs.new_version }}"
git tag "v${{ steps.bump_version.outputs.new_version }}" git tag "v${{ steps.bump_version.outputs.new_version }}"
git push git push

View File

@@ -29,15 +29,4 @@ if ! pnpm lint; then
exit 1 exit 1
fi fi
# Update commit.json with the latest commit hash
echo "Updating commit.json with the latest commit hash..."
COMMIT_HASH=$(git rev-parse HEAD)
if [ $? -ne 0 ]; then
echo "❌ Failed to get commit hash. Ensure you are in a git repository."
exit 1
fi
echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json
git add app/commit.json
echo "👍 All checks passed! Committing changes..." echo "👍 All checks passed! Committing changes..."

View File

@@ -1,217 +1,219 @@
# Contributing to bolt.diy # Contribution Guidelines
First off, thank you for considering contributing to bolt.diy! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make bolt.diy a better tool for developers worldwide. Welcome! This guide provides all the details you need to contribute effectively to the project. Thank you for helping us make **bolt.diy** a better tool for developers worldwide. 💡
---
## 📋 Table of Contents ## 📋 Table of Contents
- [Code of Conduct](#code-of-conduct)
- [How Can I Contribute?](#how-can-i-contribute)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Coding Standards](#coding-standards)
- [Development Setup](#development-setup)
- [Deploymnt with Docker](#docker-deployment-documentation)
- [Project Structure](#project-structure)
## Code of Conduct 1. [Code of Conduct](#code-of-conduct)
2. [How Can I Contribute?](#how-can-i-contribute)
3. [Pull Request Guidelines](#pull-request-guidelines)
4. [Coding Standards](#coding-standards)
5. [Development Setup](#development-setup)
6. [Testing](#testing)
7. [Deployment](#deployment)
8. [Docker Deployment](#docker-deployment)
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. ---
## How Can I Contribute? ## 🛡️ Code of Conduct
### 🐞 Reporting Bugs and Feature Requests This project is governed by our **Code of Conduct**. By participating, you agree to uphold this code. Report unacceptable behavior to the project maintainers.
- Check the issue tracker to avoid duplicates
- Use the issue templates when available
- Include as much relevant information as possible
- For bugs, add steps to reproduce the issue
### 🔧 Code Contributions ---
1. Fork the repository
2. Create a new branch for your feature/fix
3. Write your code
4. Submit a pull request
### ✨ Becoming a Core Contributor ## 🛠️ How Can I Contribute?
We're looking for dedicated contributors to help maintain and grow this project. If you're interested in becoming a core contributor, please fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
## Pull Request Guidelines ### 1⃣ Reporting Bugs or Feature Requests
- Check the [issue tracker](#) to avoid duplicates.
- Use issue templates (if available).
- Provide detailed, relevant information and steps to reproduce bugs.
### 📝 PR Checklist ### 2⃣ Code Contributions
- [ ] Branch from the main branch 1. Fork the repository.
- [ ] Update documentation if needed 2. Create a feature or fix branch.
- [ ] Manually verify all new functionality works as expected 3. Write and test your code.
- [ ] Keep PRs focused and atomic 4. Submit a pull request (PR).
### 👀 Review Process ### 3⃣ Join as a Core Contributor
1. Manually test the changes Interested in maintaining and growing the project? Fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
2. At least one maintainer review required
3. Address all review comments
4. Maintain clean commit history
## Coding Standards ---
### 💻 General Guidelines ## ✅ Pull Request Guidelines
- Follow existing code style
- Comment complex logic
- Keep functions focused and small
- Use meaningful variable names
- Lint your code. This repo contains a pre-commit-hook that will verify your code is linted properly,
so set up your IDE to do that for you!
## Development Setup ### PR Checklist
- Branch from the **main** branch.
- Update documentation, if needed.
- Test all functionality manually.
- Focus on one feature/bug per PR.
### 🔄 Initial Setup ### Review Process
1. Clone the repository: 1. Manual testing by reviewers.
```bash 2. At least one maintainer review required.
git clone https://github.com/coleam00/bolt.new-any-llm.git 3. Address review comments.
``` 4. Maintain a clean commit history.
2. Install dependencies: ---
```bash
pnpm install
```
3. Set up environment variables: ## 📏 Coding Standards
- Rename `.env.example` to `.env.local`
- Add your LLM API keys (only set the ones you plan to use):
```bash
GROQ_API_KEY=XXX
HuggingFace_API_KEY=XXX
OPENAI_API_KEY=XXX
ANTHROPIC_API_KEY=XXX
...
```
- Optionally set debug level:
```bash
VITE_LOG_LEVEL=debug
```
- Optionally set context size: ### General Guidelines
```bash - Follow existing code style.
DEFAULT_NUM_CTX=32768 - Comment complex logic.
``` - Keep functions small and focused.
- Use meaningful variable names.
Some Example Context Values for the qwen2.5-coder:32b models are. ---
* DEFAULT_NUM_CTX=32768 - Consumes 36GB of VRAM ## 🖥️ Development Setup
* DEFAULT_NUM_CTX=24576 - Consumes 32GB of VRAM
* DEFAULT_NUM_CTX=12288 - Consumes 26GB of VRAM
* DEFAULT_NUM_CTX=6144 - Consumes 24GB of VRAM
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore. ### 1⃣ Initial Setup
- Clone the repository:
```bash
git clone https://github.com/stackblitz-labs/bolt.diy.git
```
- Install dependencies:
```bash
pnpm install
```
- Set up environment variables:
1. Rename `.env.example` to `.env.local`.
2. Add your API keys:
```bash
GROQ_API_KEY=XXX
HuggingFace_API_KEY=XXX
OPENAI_API_KEY=XXX
...
```
3. Optionally set:
- Debug level: `VITE_LOG_LEVEL=debug`
- Context size: `DEFAULT_NUM_CTX=32768`
### 🚀 Running the Development Server **Note**: Never commit your `.env.local` file to version control. Its already in `.gitignore`.
### 2⃣ Run Development Server
```bash ```bash
pnpm run dev pnpm run dev
``` ```
**Tip**: Use **Google Chrome Canary** for local testing.
**Note**: You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway. ---
## Testing ## 🧪 Testing
Run the test suite with: Run the test suite with:
```bash ```bash
pnpm test pnpm test
``` ```
## Deployment ---
To deploy the application to Cloudflare Pages: ## 🚀 Deployment
### Deploy to Cloudflare Pages
```bash ```bash
pnpm run deploy pnpm run deploy
``` ```
Ensure you have required permissions and that Wrangler is configured.
Make sure you have the necessary permissions and Wrangler is correctly configured for your Cloudflare account. ---
# Docker Deployment Documentation ## 🐳 Docker Deployment
This guide outlines various methods for building and deploying the application using Docker. This section outlines the methods for deploying the application using Docker. The processes for **Development** and **Production** are provided separately for clarity.
## Build Methods ---
### 1. Using Helper Scripts ### 🧑‍💻 Development Environment
NPM scripts are provided for convenient building: #### Build Options
**Option 1: Helper Scripts**
```bash ```bash
# Development build # Development build
npm run dockerbuild npm run dockerbuild
```
**Option 2: Direct Docker Build Command**
```bash
docker build . --target bolt-ai-development
```
**Option 3: Docker Compose Profile**
```bash
docker compose --profile development up
```
#### Running the Development Container
```bash
docker run -p 5173:5173 --env-file .env.local bolt-ai:development
```
---
### 🏭 Production Environment
#### Build Options
**Option 1: Helper Scripts**
```bash
# Production build # Production build
npm run dockerbuild:prod npm run dockerbuild:prod
``` ```
### 2. Direct Docker Build Commands **Option 2: Direct Docker Build Command**
You can use Docker's target feature to specify the build environment:
```bash ```bash
# Development build
docker build . --target bolt-ai-development
# Production build
docker build . --target bolt-ai-production docker build . --target bolt-ai-production
``` ```
### 3. Docker Compose with Profiles **Option 3: Docker Compose Profile**
Use Docker Compose profiles to manage different environments:
```bash ```bash
# Development environment docker compose --profile production up
docker-compose --profile development up
# Production environment
docker-compose --profile production up
``` ```
## Running the Application #### Running the Production Container
After building using any of the methods above, run the container with:
```bash ```bash
# Development
docker run -p 5173:5173 --env-file .env.local bolt-ai:development
# Production
docker run -p 5173:5173 --env-file .env.local bolt-ai:production docker run -p 5173:5173 --env-file .env.local bolt-ai:production
``` ```
## Deployment with Coolify ---
[Coolify](https://github.com/coollabsio/coolify) provides a straightforward deployment process: ### Coolify Deployment
1. Import your Git repository as a new project For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
2. Select your target environment (development/production) 1. Import your Git repository into Coolify.
3. Choose "Docker Compose" as the Build Pack 2. Choose **Docker Compose** as the build pack.
4. Configure deployment domains 3. Configure environment variables (e.g., API keys).
5. Set the custom start command: 4. Set the start command:
```bash ```bash
docker compose --profile production up docker compose --profile production up
``` ```
6. Configure environment variables
- Add necessary AI API keys
- Adjust other environment variables as needed
7. Deploy the application
## VS Code Integration ---
The `docker-compose.yaml` configuration is compatible with VS Code dev containers: ## 🛠️ VS Code Dev Containers Integration
1. Open the command palette in VS Code The `docker-compose.yaml` configuration is compatible with **VS Code Dev Containers**, making it easy to set up a development environment directly in Visual Studio Code.
2. Select the dev container configuration
3. Choose the "development" profile from the context menu
## Environment Files ### Steps to Use Dev Containers
Ensure you have the appropriate `.env.local` file configured before running the containers. This file should contain: 1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
- API keys 2. Select **Dev Containers: Reopen in Container**.
- Environment-specific configurations 3. Choose the **development** profile when prompted.
- Other required environment variables 4. VS Code will rebuild the container and open it with the pre-configured environment.
## Notes ---
- Port 5173 is exposed and mapped for both development and production environments ## 🔑 Environment Variables
- Environment variables are loaded from `.env.local`
- Different profiles (development/production) can be used for different deployment scenarios Ensure `.env.local` is configured correctly with:
- The configuration supports both local development and production deployment - API keys.
- Context-specific configurations.
Example for the `DEFAULT_NUM_CTX` variable:
```bash
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
```

View File

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

92
FAQ.md
View File

@@ -1,47 +1,91 @@
[![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy) # Frequently Asked Questions (FAQ)
# bolt.diy <details>
<summary><strong>What are the best models for bolt.diy?</strong></summary>
## FAQ For the best experience with bolt.diy, we recommend using the following models:
### How do I get the best results with bolt.diy? - **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
- **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure bolt scaffolds the project accordingly. **Note**: Models with less than 7b parameters typically lack the capability to properly interact with bolt!
</details>
- **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting. <details>
<summary><strong>How do I get the best results with bolt.diy?</strong></summary>
- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt.diy understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality. - **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.
- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt.diy to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly. - **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.
### Why are there so many open issues/pull requests? - **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.
bolt.diy was started simply to showcase how to edit an open source project and to do something cool with local LLMs on my (@ColeMedin) YouTube channel! However, it quickly grew into a massive community project that I am working hard to keep up with the demand of by forming a team of maintainers and getting as many people involved as I can. That effort is going well and all of our maintainers are ABSOLUTE rockstars, but it still takes time to organize everything so we can efficiently get through all the issues and PRs. But rest assured, we are working hard and even working on some partnerships behind the scenes to really help this project take off! - **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>
### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for bolt.diy/bolt.new? <details>
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
As much as the gap is quickly closing between open source and massive close source models, youre still going to get the best results with the very large models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. This is one of the big tasks we have at hand - figuring out how to prompt better, use agents, and improve the platform as a whole to make it work better for even the smaller local LLMs! Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
</details>
### I'm getting the error: "There was an error processing this request" <details>
<summary><strong>What are the future plans for bolt.diy?</strong></summary>
If you see this error within bolt.diy, that is just the application telling you there is a problem at a high level, and this could mean a number of different things. To find the actual error, please check BOTH the terminal where you started the application (with Docker or pnpm) and the developer console in the browser. For most browsers, you can access the developer console by pressing F12 or right clicking anywhere in the browser and selecting “Inspect”. Then go to the “console” tab in the top right. Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
New features and improvements are on the way!
</details>
### I'm getting the error: "x-api-key header missing" <details>
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
We have seen this error a couple times and for some reason just restarting the Docker container has fixed it. This seems to be Ollama specific. Another thing to try is try to run bolt.diy with Docker or pnpm, whichever you didnt run first. We are still on the hunt for why this happens once and a while! 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!
### I'm getting a blank preview when bolt.diy runs my app! 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>
We promise you that we are constantly testing new PRs coming into bolt.diy and the preview is core functionality, so the application is not broken! When you get a blank preview or dont get a preview, this is generally because the LLM hallucinated bad code or incorrect commands. We are working on making this more transparent so it is obvious. Sometimes the error will appear in developer console too so check that as well. <details>
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
### How to add a LLM: 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>
To make new LLMs available to use in this version of bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider. <details>
<summary><strong>Common Errors and Troubleshooting</strong></summary>
By default, Anthropic, OpenAI, Groq, and Ollama are implemented as providers, but the YouTube video for this repo covers how to extend this to work with more providers if you wish! ### **"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).
When you add a new model to the MODEL_LIST array, it will immediately be available to use when you run the app locally or reload it. For Ollama models, make sure you have the model installed already before trying to use it here! ### **"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.
### Everything works but the results are bad ### **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.
This goes to the point above about how local LLMs are getting very powerful but you still are going to see better (sometimes much better) results with the largest LLMs like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. If you are using smaller LLMs like Qwen-2.5-Coder, consider it more experimental and educational at this point. It can build smaller applications really well, which is super impressive for a local LLM, but for larger scale applications you want to use the larger LLMs still! ### **"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.
### **"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)
### **"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>
---
Got more questions? Feel free to reach out or open an issue in our GitHub repo!

View File

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

57
PROJECT.md Normal file
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 🙌

297
README.md
View File

@@ -1,19 +1,44 @@
[![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
# 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. 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. This documentation is still being updated after the transfer. -----
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/).
bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMedin) but has quickly grown into a massive community effort to build the BEST open source AI coding assistant! bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMedin) but has quickly grown into a massive community effort to build the BEST open source AI coding assistant!
## Join the community for bolt.diy! ## Table of Contents
https://thinktank.ottomator.ai - [Join the Community](#join-the-community)
- [Requested Additions](#requested-additions)
- [Features](#features)
- [Setup](#setup)
- [Run the Application](#run-the-application)
- [Available Scripts](#available-scripts)
- [Contributing](#contributing)
- [Roadmap](#roadmap)
- [FAQ](#faq)
## Join the community
## Requested Additions - Feel Free to Contribute! [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
- ✅ OpenRouter Integration (@coleam00) - ✅ OpenRouter Integration (@coleam00)
- ✅ Gemini Integration (@jonathands) - ✅ Gemini Integration (@jonathands)
@@ -34,6 +59,7 @@ https://thinktank.ottomator.ai
- ✅ Bolt terminal to see the output of LLM run commands (@thecodacus) - ✅ Bolt terminal to see the output of LLM run commands (@thecodacus)
- ✅ Streaming of code output (@thecodacus) - ✅ Streaming of code output (@thecodacus)
- ✅ Ability to revert code to earlier version (@wonderwhy-er) - ✅ Ability to revert code to earlier version (@wonderwhy-er)
- ✅ Chat history backup and restore functionality (@sidbetatester)
- ✅ Cohere Integration (@hasanraiyan) - ✅ Cohere Integration (@hasanraiyan)
- ✅ Dynamic model max token length (@hasanraiyan) - ✅ Dynamic model max token length (@hasanraiyan)
- ✅ Better prompt enhancing (@SujalXplores) - ✅ Better prompt enhancing (@SujalXplores)
@@ -42,12 +68,17 @@ https://thinktank.ottomator.ai
- ✅ Together Integration (@mouimet-infinisoft) - ✅ Together Integration (@mouimet-infinisoft)
- ✅ Mobile friendly (@qwikode) - ✅ Mobile friendly (@qwikode)
- ✅ Better prompt enhancing (@SujalXplores) - ✅ Better prompt enhancing (@SujalXplores)
- ✅ Attach images to prompts (@atrokhym) - ✅ Attach images to prompts (@atrokhym)(@stijnus)
- ✅ Added Git Clone button (@thecodacus) - ✅ Added Git Clone button (@thecodacus)
- ✅ Git Import from url (@thecodacus) - ✅ Git Import from url (@thecodacus)
- ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus) - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
- ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er) - ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
- ✅ Selection tool to target changes visually (@emcconnell) - ✅ Selection tool to target changes visually (@emcconnell)
- ✅ 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** - Prevent bolt from rewriting files as often (file locking and diffs)
-**HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start) -**HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
-**HIGH PRIORITY** - Run agents in the backend as opposed to a single model call -**HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
@@ -57,12 +88,14 @@ https://thinktank.ottomator.ai
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc. - ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
- ⬜ Voice prompting - ⬜ Voice prompting
- ⬜ Azure Open AI API Integration - ⬜ Azure Open AI API Integration
- ⬜ Perplexity Integration
- ⬜ Vertex AI Integration - ⬜ Vertex AI Integration
- ⬜ Granite Integration
- ✅ Popout Window for Web Container(@stijnus)
- ✅ Ability to change Popout window size (@stijnus)
## bolt.diy Features ## Features
- **AI-powered full-stack web development** directly in your browser. - **AI-powered full-stack web development** for **NodeJS based applications** directly in your browser.
- **Support for multiple LLMs** with an extensible architecture to integrate additional models. - **Support for multiple LLMs** with an extensible architecture to integrate additional models.
- **Attach images to prompts** for better contextual understanding. - **Attach images to prompts** for better contextual understanding.
- **Integrated terminal** to view output of LLM-run commands. - **Integrated terminal** to view output of LLM-run commands.
@@ -70,141 +103,221 @@ https://thinktank.ottomator.ai
- **Download projects as ZIP** for easy portability. - **Download projects as ZIP** for easy portability.
- **Integration-ready Docker support** for a hassle-free setup. - **Integration-ready Docker support** for a hassle-free setup.
## Setup bolt.diy ## 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.
### Prerequisites Let's get you up and running with the stable version of Bolt.DIY!
1. **Install Git**: [Download Git](https://git-scm.com/downloads) ## Quick Download
2. **Install Node.js**: [Download Node.js](https://nodejs.org/en/download/)
- After installation, the Node.js path is usually added to your system automatically. To verify: [![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!
- **Windows**: Search for "Edit the system environment variables," click "Environment Variables," and check if `Node.js` is in the `Path` variable.
- **Mac/Linux**: Open a terminal and run: - Next **click source.zip**
## Prerequisites
Before you begin, you'll need to install two important pieces of software:
### Install Node.js
Node.js is required to run the application.
1. Visit the [Node.js Download Page](https://nodejs.org/en/download/)
2. Download the "LTS" (Long Term Support) version for your operating system
3. Run the installer, accepting the default settings
4. Verify Node.js is properly installed:
- **For Windows Users**:
1. Press `Windows + R`
2. Type "sysdm.cpl" and press Enter
3. Go to "Advanced" tab → "Environment Variables"
4. Check if `Node.js` appears in the "Path" variable
- **For Mac/Linux Users**:
1. Open Terminal
2. Type this command:
```bash ```bash
echo $PATH echo $PATH
``` ```
Look for `/usr/local/bin` in the output. 3. Look for `/usr/local/bin` in the output
### Clone the Repository ## Running the Application
Clone the repository using Git: You have two options for running Bolt.DIY: directly on your machine or using Docker.
```bash ### Option 1: Direct Installation (Recommended for Beginners)
git clone -b stable https://github.com/stackblitz-labs/bolt.diy
```
### (Optional) Configure Environment Variables 1. **Install Package Manager (pnpm)**:
Most environment variables can be configured directly through the settings menu of the application. However, if you need to manually configure them: ```bash
npm install -g pnpm
```
1. Rename `.env.example` to `.env.local`. 2. **Install Project Dependencies**:
2. Add your LLM API keys. For example:
```env
GROQ_API_KEY=YOUR_GROQ_API_KEY
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
```
**Note**: Ollama does not require an API key as it runs locally.
3. Optionally, set additional configurations:
```env
# Debugging
VITE_LOG_LEVEL=debug
# Ollama settings (example: 8K context, localhost port 11434)
OLLAMA_API_BASE_URL=http://localhost:11434
DEFAULT_NUM_CTX=8192
```
**Important**: Do not commit your `.env.local` file to version control. This file is already included in `.gitignore`.
---
## Run the Application
### Option 1: Without Docker
1. **Install Dependencies**:
```bash ```bash
pnpm install pnpm install
``` ```
If `pnpm` is not installed, install it using:
```bash
sudo npm install -g pnpm
```
2. **Start the Application**: 3. **Start the Application**:
```bash ```bash
pnpm run dev pnpm run dev
``` ```
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
### Option 2: With Docker **Important Note**: If you're using Google Chrome, you'll need Chrome Canary for local development. [Download it here](https://www.google.com/chrome/canary/)
#### Prerequisites ### Option 2: Using Docker
- Ensure Git, Node.js, and Docker are installed: [Download Docker](https://www.docker.com/)
#### Steps 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**: 1. **Build the Docker Image**:
Use the provided NPM scripts:
```bash ```bash
npm run dockerbuild # Development build # Using npm script:
npm run dockerbuild:prod # Production build npm run dockerbuild
```
Alternatively, use Docker commands directly: # OR using direct Docker command:
```bash docker build . --target bolt-ai-development
docker build . --target bolt-ai-development # Development build
docker build . --target bolt-ai-production # Production build
``` ```
2. **Run the Container**: 2. **Run the Container**:
Use Docker Compose profiles to manage environments:
```bash ```bash
docker-compose --profile development up # Development docker compose --profile development up
docker-compose --profile production up # Production
``` ```
- With the development profile, changes to your code will automatically reflect in the running container (hot reloading). ## Configuring API Keys and Providers
--- ### Adding Your API Keys
### Update Your Local Version to the Latest Setting up your API keys in Bolt.DIY is straightforward:
To keep your local version of bolt.diy up to date with the latest changes, follow these steps for your operating system: 1. Open the home page (main interface)
2. Select your desired provider from the dropdown menu
3. Click the pencil (edit) icon
4. Enter your API key in the secure input field
#### 1. **Navigate to your project folder** ![API Key Configuration Interface](./docs/images/api-key-ui-section.png)
Navigate to the directory where you cloned the repository and open a terminal:
#### 2. **Fetch the Latest Changes** ### Configuring Custom Base URLs
Use Git to pull the latest changes from the main repository:
For providers that support custom base URLs (such as Ollama or LM Studio), follow these steps:
1. Click the settings icon in the sidebar to open the settings menu
![Settings Button Location](./docs/images/bolt-settings-button.png)
2. Navigate to the "Providers" tab
3. Search for your provider using the search bar
4. Enter your custom base URL in the designated field
![Provider Base URL Configuration](./docs/images/provider-base-url.png)
> **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
## 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
```
3. **Switch to the Main Branch**:
```bash
git checkout main
```
4. **Install Dependencies**:
```bash
pnpm install
```
5. **Start the Development Server**:
```bash
pnpm run dev
```
#### Staying Updated
To get the latest changes from the repository:
1. **Save Your Local Changes** (if any):
```bash
git stash
```
2. **Pull Latest Updates**:
```bash ```bash
git pull origin main git pull origin main
``` ```
#### 3. **Update Dependencies** 3. **Update Dependencies**:
After pulling the latest changes, update the project dependencies by running the following command:
```bash ```bash
pnpm install pnpm install
``` ```
#### 4. **Run the Application** 4. **Restore Your Local Changes** (if any):
Once the updates are complete, you can start the application again with:
```bash ```bash
pnpm run dev git stash pop
``` ```
This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes. #### Troubleshooting Git Setup
If you encounter issues:
1. **Clean Installation**:
```bash
# Remove node modules and lock files
rm -rf node_modules pnpm-lock.yaml
# Clear pnpm cache
pnpm store prune
# Reinstall dependencies
pnpm install
```
2. **Reset Local Changes**:
```bash
# Discard all local changes
git reset --hard origin/main
```
Remember to always commit your local changes or stash them before pulling updates to avoid conflicts.
--- ---
@@ -236,4 +349,4 @@ Explore upcoming features and priorities on our [Roadmap](https://roadmap.sh/r/o
## FAQ ## FAQ
For answers to common questions, visit our [FAQ Page](FAQ.md). For answers to common questions, issues, and to see a list of recommended models, visit our [FAQ Page](FAQ.md).

View File

@@ -1 +0,0 @@
{ "commit": "89eac1ba8b2ada92dd76da9671d17e08de61a8bf", "version": "0.0.2" }

View File

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

View File

@@ -1,6 +1,8 @@
import { memo } from 'react'; import { memo } from 'react';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import type { JSONValue } from 'ai'; import type { JSONValue } from 'ai';
import type { ProgressAnnotation } from '~/types/context';
import Popover from '~/components/ui/Popover';
interface AssistantMessageProps { interface AssistantMessageProps {
content: string; content: string;
@@ -10,7 +12,12 @@ interface AssistantMessageProps {
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => { export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
const filteredAnnotations = (annotations?.filter( const filteredAnnotations = (annotations?.filter(
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), (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: { const usage: {
completionTokens: number; completionTokens: number;
@@ -20,11 +27,18 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
return ( return (
<div className="overflow-hidden w-full"> <div className="overflow-hidden w-full">
<>
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
{progressAnnotation.length > 0 && (
<Popover trigger={<div className="i-ph:info" />}>{progressAnnotation[0].message}</Popover>
)}
{usage && ( {usage && (
<div className="text-sm text-bolt-elements-textSecondary mb-2"> <div>
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
</div> </div>
)} )}
</div>
</>
<Markdown html>{content}</Markdown> <Markdown html>{content}</Markdown>
</div> </div>
); );

View File

@@ -9,10 +9,10 @@ import { Menu } from '~/components/sidebar/Menu.client';
import { IconButton } from '~/components/ui/IconButton'; import { IconButton } from '~/components/ui/IconButton';
import { Workbench } from '~/components/workbench/Workbench.client'; import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants'; import { PROVIDER_LIST } from '~/utils/constants';
import { Messages } from './Messages.client'; import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client'; import { SendButton } from './SendButton.client';
import { APIKeyManager } from './APIKeyManager'; import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Tooltip from '@radix-ui/react-tooltip'; import * as Tooltip from '@radix-ui/react-tooltip';
@@ -25,9 +25,13 @@ import GitCloneButton from './GitCloneButton';
import FilePreview from './FilePreview'; import FilePreview from './FilePreview';
import { ModelSelector } from '~/components/chat/ModelSelector'; import { ModelSelector } from '~/components/chat/ModelSelector';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition'; import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import type { IProviderSetting, ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import { ScreenshotStateManager } from './ScreenshotStateManager'; import { ScreenshotStateManager } from './ScreenshotStateManager';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import StarterTemplates from './StarterTemplates';
import type { ActionAlert } from '~/types/actions';
import ChatAlert from './ChatAlert';
import type { ModelInfo } from '~/lib/modules/llm/types';
const TEXTAREA_MIN_HEIGHT = 76; const TEXTAREA_MIN_HEIGHT = 76;
@@ -58,6 +62,8 @@ interface BaseChatProps {
setUploadedFiles?: (files: File[]) => void; setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[]; imageDataList?: string[];
setImageDataList?: (dataList: string[]) => void; setImageDataList?: (dataList: string[]) => void;
actionAlert?: ActionAlert;
clearAlert?: () => void;
} }
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>( export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -89,76 +95,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
imageDataList = [], imageDataList = [],
setImageDataList, setImageDataList,
messages, messages,
actionAlert,
clearAlert,
}, },
ref, ref,
) => { ) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>(() => { const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
const savedKeys = Cookies.get('apiKeys'); const [modelList, setModelList] = useState<ModelInfo[]>([]);
if (savedKeys) {
try {
return JSON.parse(savedKeys);
} catch (error) {
console.error('Failed to parse API keys from cookies:', error);
return {};
}
}
return {};
});
const [modelList, setModelList] = useState(MODEL_LIST);
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false); const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null); const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
const [transcript, setTranscript] = useState(''); const [transcript, setTranscript] = useState('');
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
useEffect(() => { useEffect(() => {
console.log(transcript); console.log(transcript);
}, [transcript]); }, [transcript]);
useEffect(() => { useEffect(() => {
// Load API keys from cookies on component mount
try {
const storedApiKeys = Cookies.get('apiKeys');
if (storedApiKeys) {
const parsedKeys = JSON.parse(storedApiKeys);
if (typeof parsedKeys === 'object' && parsedKeys !== null) {
setApiKeys(parsedKeys);
}
}
} catch (error) {
console.error('Error loading API keys from cookies:', error);
// Clear invalid cookie data
Cookies.remove('apiKeys');
}
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');
}
initializeModelList(providerSettings).then((modelList) => {
setModelList(modelList);
});
if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) { if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition(); const recognition = new SpeechRecognition();
@@ -190,6 +145,59 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
} }
}, []); }, []);
useEffect(() => {
if (typeof window !== 'undefined') {
let parsedApiKeys: Record<string, string> | undefined = {};
try {
parsedApiKeys = getApiKeysFromCookies();
setApiKeys(parsedApiKeys);
} catch (error) {
console.error('Error loading API keys from cookies:', error);
Cookies.remove('apiKeys');
}
setIsModelLoading('all');
fetch('/api/models')
.then((response) => response.json())
.then((data) => {
const typedData = data as { modelList: ModelInfo[] };
setModelList(typedData.modelList);
})
.catch((error) => {
console.error('Error fetching model list:', error);
})
.finally(() => {
setIsModelLoading(undefined);
});
}
}, [providerList, provider]);
const onApiKeysChange = async (providerName: string, apiKey: string) => {
const newApiKeys = { ...apiKeys, [providerName]: apiKey };
setApiKeys(newApiKeys);
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
setIsModelLoading(providerName);
let providerModels: ModelInfo[] = [];
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 = () => { const startListening = () => {
if (recognition) { if (recognition) {
recognition.start(); recognition.start();
@@ -313,11 +321,31 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}} }}
</ClientOnly> </ClientOnly>
<div <div
className={classNames( className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
{
'sticky bottom-2': chatStarted, 'sticky bottom-2': chatStarted,
}, })}
>
<div className="bg-bolt-elements-background-depth-2">
{actionAlert && (
<ChatAlert
alert={actionAlert}
clearAlert={() => clearAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearAlert?.();
}}
/>
)}
</div>
<div
className={classNames(
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
/*
* {
* 'sticky bottom-2': chatStarted,
* },
*/
)} )}
> >
<svg className={classNames(styles.PromptEffectContainer)}> <svg className={classNames(styles.PromptEffectContainer)}>
@@ -347,6 +375,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect> <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
</svg> </svg>
<div> <div>
<ClientOnly>
{() => (
<div className={isModelSettingsCollapsed ? 'hidden' : ''}> <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
<ModelSelector <ModelSelector
key={provider?.name + ':' + modelList.length} key={provider?.name + ':' + modelList.length}
@@ -355,21 +385,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
modelList={modelList} modelList={modelList}
provider={provider} provider={provider}
setProvider={setProvider} setProvider={setProvider}
providerList={providerList || PROVIDER_LIST} providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
apiKeys={apiKeys} apiKeys={apiKeys}
modelLoading={isModelLoading}
/> />
{(providerList || []).length > 0 && provider && ( {(providerList || []).length > 0 && provider && (
<APIKeyManager <APIKeyManager
provider={provider} provider={provider}
apiKey={apiKeys[provider.name] || ''} apiKey={apiKeys[provider.name] || ''}
setApiKey={(key) => { setApiKey={(key) => {
const newApiKeys = { ...apiKeys, [provider.name]: key }; onApiKeysChange(provider.name, key);
setApiKeys(newApiKeys);
Cookies.set('apiKeys', JSON.stringify(newApiKeys));
}} }}
/> />
)} )}
</div> </div>
)}
</ClientOnly>
</div> </div>
<FilePreview <FilePreview
files={uploadedFiles} files={uploadedFiles}
@@ -528,15 +559,17 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div> </div>
{input.length > 3 ? ( {input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary"> <div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '} Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
new line a new line
</div> </div>
) : null} ) : null}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="flex flex-col justify-center gap-5">
{!chatStarted && ( {!chatStarted && (
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{ImportButtons(importChat)} {ImportButtons(importChat)}
@@ -552,6 +585,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleSendMessage?.(event, messageInput); handleSendMessage?.(event, messageInput);
})} })}
{!chatStarted && <StarterTemplates />}
</div>
</div> </div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly> <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
</div> </div>

View File

@@ -21,6 +21,8 @@ import { debounce } from '~/utils/debounce';
import { useSettings } from '~/lib/hooks/useSettings'; import { useSettings } from '~/lib/hooks/useSettings';
import type { ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import { useSearchParams } from '@remix-run/react'; import { useSearchParams } from '@remix-run/react';
import { createSampler } from '~/utils/sampler';
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
const toastAnimation = cssTransition({ const toastAnimation = cssTransition({
enter: 'animated fadeInRight', enter: 'animated fadeInRight',
@@ -34,6 +36,9 @@ export function Chat() {
const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory(); const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
const title = useStore(description); const title = useStore(description);
useEffect(() => {
workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
}, [initialMessages]);
return ( return (
<> <>
@@ -77,6 +82,24 @@ export function Chat() {
); );
} }
const processSampledMessages = createSampler(
(options: {
messages: Message[];
initialMessages: Message[];
isLoading: boolean;
parseMessages: (messages: Message[], isLoading: boolean) => void;
storeMessageHistory: (messages: Message[]) => Promise<void>;
}) => {
const { messages, initialMessages, isLoading, parseMessages, storeMessageHistory } = options;
parseMessages(messages, isLoading);
if (messages.length > initialMessages.length) {
storeMessageHistory(messages).catch((error) => toast.error(error.message));
}
},
50,
);
interface ChatProps { interface ChatProps {
initialMessages: Message[]; initialMessages: Message[];
storeMessageHistory: (messages: Message[]) => Promise<void>; storeMessageHistory: (messages: Message[]) => Promise<void>;
@@ -94,8 +117,10 @@ export const ChatImpl = memo(
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [fakeLoading, setFakeLoading] = useState(false);
const files = useStore(workbenchStore.files); const files = useStore(workbenchStore.files);
const { activeProviders, promptId } = useSettings(); const actionAlert = useStore(workbenchStore.alert);
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
const [model, setModel] = useState(() => { const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel'); const savedModel = Cookies.get('selectedModel');
@@ -103,7 +128,7 @@ export const ChatImpl = memo(
}); });
const [provider, setProvider] = useState(() => { const [provider, setProvider] = useState(() => {
const savedProvider = Cookies.get('selectedProvider'); const savedProvider = Cookies.get('selectedProvider');
return PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER; return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo;
}); });
const { showChat } = useStore(chatStore); const { showChat } = useStore(chatStore);
@@ -112,18 +137,20 @@ export const ChatImpl = memo(
const [apiKeys, setApiKeys] = useState<Record<string, string>>({}); const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({ const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
useChat({
api: '/api/chat', api: '/api/chat',
body: { body: {
apiKeys, apiKeys,
files, files,
promptId, promptId,
contextOptimization: contextOptimizationEnabled,
}, },
sendExtraMessageFields: true, sendExtraMessageFields: true,
onError: (error) => { onError: (e) => {
logger.error('Request failed\n\n', error); logger.error('Request failed\n\n', e, error);
toast.error( toast.error(
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'), 'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
); );
}, },
onFinish: (message, response) => { onFinish: (message, response) => {
@@ -142,7 +169,8 @@ export const ChatImpl = memo(
}); });
useEffect(() => { useEffect(() => {
const prompt = searchParams.get('prompt'); const prompt = searchParams.get('prompt');
console.log(prompt, searchParams, model, provider);
// console.log(prompt, searchParams, model, provider);
if (prompt) { if (prompt) {
setSearchParams({}); setSearchParams({});
@@ -169,11 +197,13 @@ export const ChatImpl = memo(
}, []); }, []);
useEffect(() => { useEffect(() => {
parseMessages(messages, isLoading); processSampledMessages({
messages,
if (messages.length > initialMessages.length) { initialMessages,
storeMessageHistory(messages).catch((error) => toast.error(error.message)); isLoading,
} parseMessages,
storeMessageHistory,
});
}, [messages, isLoading, parseMessages]); }, [messages, isLoading, parseMessages]);
const scrollTextArea = () => { const scrollTextArea = () => {
@@ -234,12 +264,128 @@ export const ChatImpl = memo(
*/ */
await workbenchStore.saveAllFiles(); await workbenchStore.saveAllFiles();
if (error != null) {
setMessages(messages.slice(0, -1));
}
const fileModifications = workbenchStore.getFileModifcations(); const fileModifications = workbenchStore.getFileModifcations();
chatStore.setKey('aborted', false); chatStore.setKey('aborted', false);
runAnimation(); runAnimation();
if (!chatStarted && _input && autoSelectTemplate) {
setFakeLoading(true);
setMessages([
{
id: `${new Date().getTime()}`,
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any, // Type assertion to bypass compiler check
},
]);
// reload();
const { template, title } = await selectStarterTemplate({
message: _input,
model,
provider,
});
if (template !== 'blank') {
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;
setMessages([
{
id: `${new Date().getTime()}`,
role: 'user',
content: _input,
// annotations: ['hidden'],
},
{
id: `${new Date().getTime()}`,
role: 'assistant',
content: assistantMessage,
},
{
id: `${new Date().getTime()}`,
role: 'user',
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
annotations: ['hidden'],
},
]);
reload();
setFakeLoading(false);
return;
} else {
setMessages([
{
id: `${new Date().getTime()}`,
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any, // Type assertion to bypass compiler check
},
]);
reload();
setFakeLoading(false);
return;
}
} else {
setMessages([
{
id: `${new Date().getTime()}`,
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any, // Type assertion to bypass compiler check
},
]);
reload();
setFakeLoading(false);
return;
}
}
if (fileModifications !== undefined) { if (fileModifications !== undefined) {
/** /**
* If we have file modifications we append a new user message manually since we have to prefix * If we have file modifications we append a new user message manually since we have to prefix
@@ -342,7 +488,7 @@ export const ChatImpl = memo(
input={input} input={input}
showChat={showChat} showChat={showChat}
chatStarted={chatStarted} chatStarted={chatStarted}
isStreaming={isLoading} isStreaming={isLoading || fakeLoading}
enhancingPrompt={enhancingPrompt} enhancingPrompt={enhancingPrompt}
promptEnhanced={promptEnhanced} promptEnhanced={promptEnhanced}
sendMessage={sendMessage} sendMessage={sendMessage}
@@ -387,6 +533,8 @@ export const ChatImpl = memo(
setUploadedFiles={setUploadedFiles} setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList} imageDataList={imageDataList}
setImageDataList={setImageDataList} setImageDataList={setImageDataList}
actionAlert={actionAlert}
clearAlert={() => workbenchStore.clearAlert()}
/> />
); );
}, },

View File

@@ -0,0 +1,108 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { ActionAlert } from '~/types/actions';
import { classNames } from '~/utils/classNames';
interface Props {
alert: ActionAlert;
clearAlert: () => void;
postMessage: (message: string) => void;
}
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
const { description, content, source } = alert;
const isPreview = source === 'preview';
const title = isPreview ? 'Preview Error' : 'Terminal Error';
const message = isPreview
? 'We encountered an error while running the preview. Would you like Bolt to analyze and help resolve this issue?'
: 'We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve this issue?';
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}
>
<div className="flex items-start">
{/* Icon */}
<motion.div
className="flex-shrink-0"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className={`i-ph:warning-duotone text-xl text-bolt-elements-button-danger-text`}></div>
</motion.div>
{/* Content */}
<div className="ml-3 flex-1">
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className={`text-sm font-medium text-bolt-elements-textPrimary`}
>
{title}
</motion.h3>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className={`mt-2 text-sm text-bolt-elements-textSecondary`}
>
<p>{message}</p>
{description && (
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
Error: {description}
</div>
)}
</motion.div>
{/* Actions */}
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className={classNames(' flex gap-2')}>
<button
onClick={() =>
postMessage(
`*Fix this ${isPreview ? 'preview' : 'terminal'} error* \n\`\`\`${isPreview ? 'js' : 'sh'}\n${content}\n\`\`\`\n`,
)
}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-primary-background',
'hover:bg-bolt-elements-button-primary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
'text-bolt-elements-button-primary-text',
'flex items-center gap-1.5',
)}
>
<div className="i-ph:chat-circle-duotone"></div>
Ask Bolt
</button>
<button
onClick={clearAlert}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-secondary-background',
'hover:bg-bolt-elements-button-secondary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
'text-bolt-elements-button-secondary-text',
)}
>
Dismiss
</button>
</div>
</motion.div>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}

View File

@@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
import type { Message } from 'ai'; import type { Message } from 'ai';
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands'; import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
import { generateId } from '~/utils/fileUtils'; import { generateId } from '~/utils/fileUtils';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
const IGNORE_PATTERNS = [ const IGNORE_PATTERNS = [
'node_modules/**', 'node_modules/**',
@@ -37,6 +40,8 @@ interface GitCloneButtonProps {
export default function GitCloneButton({ importChat }: GitCloneButtonProps) { export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const { ready, gitClone } = useGit(); const { ready, gitClone } = useGit();
const [loading, setLoading] = useState(false);
const onClick = async (_e: any) => { const onClick = async (_e: any) => {
if (!ready) { if (!ready) {
return; return;
@@ -45,6 +50,9 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const repoUrl = prompt('Enter the Git url'); const repoUrl = prompt('Enter the Git url');
if (repoUrl) { if (repoUrl) {
setLoading(true);
try {
const { workdir, data } = await gitClone(repoUrl); const { workdir, data } = await gitClone(repoUrl);
if (importChat) { if (importChat) {
@@ -53,22 +61,20 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const textDecoder = new TextDecoder('utf-8'); const textDecoder = new TextDecoder('utf-8');
// Convert files to common format for command detection
const fileContents = filePaths const fileContents = filePaths
.map((filePath) => { .map((filePath) => {
const { data: content, encoding } = data[filePath]; const { data: content, encoding } = data[filePath];
return { return {
path: filePath, path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '', content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
}; };
}) })
.filter((f) => f.content); .filter((f) => f.content);
// Detect and create commands message
const commands = await detectProjectCommands(fileContents); const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands); const commandsMessage = createCommandsMessage(commands);
// Create files message
const filesMessage: Message = { const filesMessage: Message = {
role: 'assistant', role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir} content: `Cloning the repo ${repoUrl} into ${workdir}
@@ -94,10 +100,17 @@ ${file.content}
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
} }
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
} finally {
setLoading(false);
}
} }
}; };
return ( return (
<>
<button <button
onClick={onClick} onClick={onClick}
title="Clone a Git Repo" title="Clone a Git Repo"
@@ -106,5 +119,7 @@ ${file.content}
<span className="i-ph:git-branch" /> <span className="i-ph:git-branch" />
Clone a Git Repo Clone a Git Repo
</button> </button>
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
</>
); );
} }

View File

@@ -16,35 +16,40 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const allFiles = Array.from(e.target.files || []); const allFiles = Array.from(e.target.files || []);
if (allFiles.length > MAX_FILES) { const filteredFiles = allFiles.filter((file) => {
const error = new Error(`Too many files: ${allFiles.length}`); const path = file.webkitRelativePath.split('/').slice(1).join('/');
logStore.logError('File import failed - too many files', error, { const include = shouldIncludeFile(path);
fileCount: allFiles.length,
maxFiles: MAX_FILES, return include;
}); });
toast.error(
`This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
);
return;
}
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
setIsLoading(true);
const loadingToast = toast.loading(`Importing ${folderName}...`);
try {
const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
if (filteredFiles.length === 0) { if (filteredFiles.length === 0) {
const error = new Error('No valid files found'); const error = new Error('No valid files found');
logStore.logError('File import failed - no valid files', error, { folderName }); logStore.logError('File import failed - no valid files', error, { folderName: 'Unknown Folder' });
toast.error('No files found in the selected folder'); toast.error('No files found in the selected folder');
return; return;
} }
if (filteredFiles.length > MAX_FILES) {
const error = new Error(`Too many files: ${filteredFiles.length}`);
logStore.logError('File import failed - too many files', error, {
fileCount: filteredFiles.length,
maxFiles: MAX_FILES,
});
toast.error(
`This folder contains ${filteredFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
);
return;
}
const folderName = filteredFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
setIsLoading(true);
const loadingToast = toast.loading(`Importing ${folderName}...`);
try {
const fileChecks = await Promise.all( const fileChecks = await Promise.all(
filteredFiles.map(async (file) => ({ filteredFiles.map(async (file) => ({
file, file,

View File

@@ -1,5 +1,5 @@
import type { Message } from 'ai'; import type { Message } from 'ai';
import React from 'react'; import React, { Fragment } from 'react';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { AssistantMessage } from './AssistantMessage'; import { AssistantMessage } from './AssistantMessage';
import { UserMessage } from './UserMessage'; import { UserMessage } from './UserMessage';
@@ -44,10 +44,15 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
<div id={id} ref={ref} className={props.className}> <div id={id} ref={ref} className={props.className}>
{messages.length > 0 {messages.length > 0
? messages.map((message, index) => { ? messages.map((message, index) => {
const { role, content, id: messageId } = message; const { role, content, id: messageId, annotations } = message;
const isUserMessage = role === 'user'; const isUserMessage = role === 'user';
const isFirst = index === 0; const isFirst = index === 0;
const isLast = index === messages.length - 1; const isLast = index === messages.length - 1;
const isHidden = annotations?.includes('hidden');
if (isHidden) {
return <Fragment key={index} />;
}
return ( return (
<div <div

View File

@@ -1,6 +1,6 @@
import type { ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import type { ModelInfo } from '~/utils/types';
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { ModelInfo } from '~/lib/modules/llm/types';
interface ModelSelectorProps { interface ModelSelectorProps {
model?: string; model?: string;
@@ -10,6 +10,7 @@ interface ModelSelectorProps {
modelList: ModelInfo[]; modelList: ModelInfo[];
providerList: ProviderInfo[]; providerList: ProviderInfo[];
apiKeys: Record<string, string>; apiKeys: Record<string, string>;
modelLoading?: string;
} }
export const ModelSelector = ({ export const ModelSelector = ({
@@ -19,6 +20,7 @@ export const ModelSelector = ({
setProvider, setProvider,
modelList, modelList,
providerList, providerList,
modelLoading,
}: ModelSelectorProps) => { }: ModelSelectorProps) => {
// Load enabled providers from cookies // Load enabled providers from cookies
@@ -83,14 +85,21 @@ export const ModelSelector = ({
value={model} value={model}
onChange={(e) => setModel?.(e.target.value)} onChange={(e) => setModel?.(e.target.value)}
className="flex-1 p-2 rounded-lg 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 transition-all lg:max-w-[70%]" className="flex-1 p-2 rounded-lg 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 transition-all lg:max-w-[70%]"
disabled={modelLoading === 'all' || modelLoading === provider?.name}
> >
{[...modelList] {modelLoading == 'all' || modelLoading == provider?.name ? (
<option key={0} value="">
Loading...
</option>
) : (
[...modelList]
.filter((e) => e.provider == provider?.name && e.name) .filter((e) => e.provider == provider?.name && e.name)
.map((modelOption, index) => ( .map((modelOption, index) => (
<option key={index} value={modelOption.name}> <option key={index} value={modelOption.name}>
{modelOption.label} {modelOption.label}
</option> </option>
))} ))
)}
</select> </select>
</div> </div>
); );

View File

@@ -0,0 +1,37 @@
import React from 'react';
import type { Template } from '~/types/template';
import { STARTER_TEMPLATES } from '~/utils/constants';
interface FrameworkLinkProps {
template: Template;
}
const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
<a
href={`/git?url=https://github.com/${template.githubRepo}.git`}
data-state="closed"
data-discover="true"
className="items-center justify-center "
>
<div
className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-75 transition-all`}
/>
</a>
);
const StarterTemplates: React.FC = () => {
return (
<div className="flex flex-col items-center gap-4">
<span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>
<div className="flex justify-center">
<div className="flex w-70 flex-wrap items-center justify-center gap-4">
{STARTER_TEMPLATES.map((template) => (
<FrameworkLink key={template.name} template={template} />
))}
</div>
</div>
</div>
);
};
export default StarterTemplates;

View File

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

View File

@@ -49,29 +49,28 @@ export function GitUrlImport() {
if (repoUrl) { if (repoUrl) {
const ig = ignore().add(IGNORE_PATTERNS); const ig = ignore().add(IGNORE_PATTERNS);
try {
const { workdir, data } = await gitClone(repoUrl); const { workdir, data } = await gitClone(repoUrl);
if (importChat) { if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
const textDecoder = new TextDecoder('utf-8'); const textDecoder = new TextDecoder('utf-8');
// Convert files to common format for command detection
const fileContents = filePaths const fileContents = filePaths
.map((filePath) => { .map((filePath) => {
const { data: content, encoding } = data[filePath]; const { data: content, encoding } = data[filePath];
return { return {
path: filePath, path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '', content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
}; };
}) })
.filter((f) => f.content); .filter((f) => f.content);
// Detect and create commands message
const commands = await detectProjectCommands(fileContents); const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands); const commandsMessage = createCommandsMessage(commands);
// Create files message
const filesMessage: Message = { const filesMessage: Message = {
role: 'assistant', role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir} content: `Cloning the repo ${repoUrl} into ${workdir}
@@ -97,6 +96,14 @@ ${file.content}
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
} }
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
setLoading(false);
window.location.href = '/';
return;
}
} }
}; };

View File

@@ -5,27 +5,27 @@ import { classNames } from '~/utils/classNames';
import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog'; import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog';
import { IconButton } from '~/components/ui/IconButton'; import { IconButton } from '~/components/ui/IconButton';
import styles from './Settings.module.scss'; import styles from './Settings.module.scss';
import ChatHistoryTab from './chat-history/ChatHistoryTab';
import ProvidersTab from './providers/ProvidersTab'; import ProvidersTab from './providers/ProvidersTab';
import { useSettings } from '~/lib/hooks/useSettings'; import { useSettings } from '~/lib/hooks/useSettings';
import FeaturesTab from './features/FeaturesTab'; import FeaturesTab from './features/FeaturesTab';
import DebugTab from './debug/DebugTab'; import DebugTab from './debug/DebugTab';
import EventLogsTab from './event-logs/EventLogsTab'; import EventLogsTab from './event-logs/EventLogsTab';
import ConnectionsTab from './connections/ConnectionsTab'; import ConnectionsTab from './connections/ConnectionsTab';
import DataTab from './data/DataTab';
interface SettingsProps { interface SettingsProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }
type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection'; type TabType = 'data' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';
export const SettingsWindow = ({ open, onClose }: SettingsProps) => { export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
const { debug, eventLogs } = useSettings(); const { debug, eventLogs } = useSettings();
const [activeTab, setActiveTab] = useState<TabType>('chat-history'); const [activeTab, setActiveTab] = useState<TabType>('data');
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [ const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
{ id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: <ChatHistoryTab /> }, { id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
{ id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> }, { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> }, { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> }, { id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
@@ -63,7 +63,7 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
variants={dialogBackdropVariants} variants={dialogBackdropVariants}
/> />
</RadixDialog.Overlay> </RadixDialog.Overlay>
<RadixDialog.Content asChild> <RadixDialog.Content aria-describedby={undefined} asChild>
<motion.div <motion.div
className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden" className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden"
initial="closed" initial="closed"

View File

@@ -1,119 +0,0 @@
import { useNavigate } from '@remix-run/react';
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { db, deleteById, getAll } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import styles from '~/components/settings/Settings.module.scss';
import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging
export default function ChatHistoryTab() {
const navigate = useNavigate();
const [isDeleting, setIsDeleting] = useState(false);
const downloadAsJson = (data: any, filename: string) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleDeleteAllChats = async () => {
const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.');
if (!confirmDelete) {
return; // Exit if the user cancels
}
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
setIsDeleting(true);
const allChats = await getAll(db);
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
toast.success('All chats deleted successfully');
navigate('/', { replace: true });
} catch (error) {
logStore.logError('Failed to delete chats', error);
toast.error('Failed to delete chats');
console.error(error);
} finally {
setIsDeleting(false);
}
};
const handleExportAllChats = async () => {
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
const allChats = await getAll(db);
const exportData = {
chats: allChats,
exportDate: new Date().toISOString(),
};
downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
logStore.logSystem('Chats exported successfully', { count: allChats.length });
toast.success('Chats exported successfully');
} catch (error) {
logStore.logError('Failed to export chats', error);
toast.error('Failed to export chats');
console.error(error);
}
};
return (
<>
<div className="p-4">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Chat History</h3>
<button
onClick={handleExportAllChats}
className={classNames(
'bg-bolt-elements-button-primary-background',
'rounded-lg px-4 py-2 mb-4 transition-colors duration-200',
'hover:bg-bolt-elements-button-primary-backgroundHover',
'text-bolt-elements-button-primary-text',
)}
>
Export All Chats
</button>
<div
className={classNames('text-bolt-elements-textPrimary rounded-lg py-4 mb-4', styles['settings-danger-area'])}
>
<h4 className="font-semibold">Danger Area</h4>
<p className="mb-2">This action cannot be undone!</p>
<button
onClick={handleDeleteAllChats}
disabled={isDeleting}
className={classNames(
'bg-bolt-elements-button-danger-background',
'rounded-lg px-4 py-2 transition-colors duration-200',
isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-bolt-elements-button-danger-backgroundHover',
'text-bolt-elements-button-danger-text',
)}
>
{isDeleting ? 'Deleting...' : 'Delete All Chats'}
</button>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,388 @@
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, 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 = [
'Anthropic',
'OpenAI',
'Google',
'Groq',
'HuggingFace',
'OpenRouter',
'Deepseek',
'Mistral',
'OpenAILike',
'Together',
'xAI',
'Perplexity',
'Cohere',
'AzureOpenAI',
'AmazonBedrock',
] as const;
interface ApiKeys {
[key: string]: string;
}
export default function DataTab() {
const navigate = useNavigate();
const [isDeleting, setIsDeleting] = useState(false);
const downloadAsJson = (data: any, filename: string) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleExportAllChats = async () => {
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
const allChats = await getAll(db);
const exportData = {
chats: allChats,
exportDate: new Date().toISOString(),
};
downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
logStore.logSystem('Chats exported successfully', { count: allChats.length });
toast.success('Chats exported successfully');
} catch (error) {
logStore.logError('Failed to export chats', error);
toast.error('Failed to export chats');
console.error(error);
}
};
const handleDeleteAllChats = async () => {
const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.');
if (!confirmDelete) {
return;
}
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
setIsDeleting(true);
const allChats = await getAll(db);
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
toast.success('All chats deleted successfully');
navigate('/', { replace: true });
} catch (error) {
logStore.logError('Failed to delete chats', error);
toast.error('Failed to delete chats');
console.error(error);
} finally {
setIsDeleting(false);
}
};
const handleExportSettings = () => {
const settings = {
providers: Cookies.get('providers'),
isDebugEnabled: Cookies.get('isDebugEnabled'),
isEventLogsEnabled: Cookies.get('isEventLogsEnabled'),
isLocalModelsEnabled: Cookies.get('isLocalModelsEnabled'),
promptId: Cookies.get('promptId'),
isLatestBranch: Cookies.get('isLatestBranch'),
commitHash: Cookies.get('commitHash'),
eventLogs: Cookies.get('eventLogs'),
selectedModel: Cookies.get('selectedModel'),
selectedProvider: Cookies.get('selectedProvider'),
githubUsername: Cookies.get('githubUsername'),
githubToken: Cookies.get('githubToken'),
bolt_theme: localStorage.getItem('bolt_theme'),
};
downloadAsJson(settings, 'bolt-settings.json');
toast.success('Settings exported successfully');
};
const handleImportSettings = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const settings = JSON.parse(e.target?.result as string);
Object.entries(settings).forEach(([key, value]) => {
if (key === 'bolt_theme') {
if (value) {
localStorage.setItem(key, value as string);
}
} else if (value) {
Cookies.set(key, value as string);
}
});
toast.success('Settings imported successfully. Please refresh the page for changes to take effect.');
} catch (error) {
toast.error('Failed to import settings. Make sure the file is a valid JSON file.');
console.error('Failed to import settings:', error);
}
};
reader.readAsText(file);
event.target.value = '';
};
const handleExportApiKeyTemplate = () => {
const template: ApiKeys = {};
API_KEY_PROVIDERS.forEach((provider) => {
template[`${provider}_API_KEY`] = '';
});
template.OPENAI_LIKE_API_BASE_URL = '';
template.LMSTUDIO_API_BASE_URL = '';
template.OLLAMA_API_BASE_URL = '';
template.TOGETHER_API_BASE_URL = '';
downloadAsJson(template, 'api-keys-template.json');
toast.success('API keys template exported successfully');
};
const handleImportApiKeys = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const apiKeys = JSON.parse(e.target?.result as string);
let importedCount = 0;
const consolidatedKeys: Record<string, string> = {};
API_KEY_PROVIDERS.forEach((provider) => {
const keyName = `${provider}_API_KEY`;
if (apiKeys[keyName]) {
consolidatedKeys[provider] = apiKeys[keyName];
importedCount++;
}
});
if (importedCount > 0) {
// Store all API keys in a single cookie as JSON
Cookies.set('apiKeys', JSON.stringify(consolidatedKeys));
// Also set individual cookies for backward compatibility
Object.entries(consolidatedKeys).forEach(([provider, key]) => {
Cookies.set(`${provider}_API_KEY`, key);
});
toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`);
// Reload the page after a short delay to allow the toast to be seen
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
toast.warn('No valid API keys found in the file');
}
// Set base URLs if they exist
['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(
(baseUrl) => {
if (apiKeys[baseUrl]) {
Cookies.set(baseUrl, apiKeys[baseUrl]);
}
},
);
} catch (error) {
toast.error('Failed to import API keys. Make sure the file is a valid JSON file.');
console.error('Failed to import API keys:', error);
}
};
reader.readAsText(file);
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">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Data Management</h3>
<div className="space-y-8">
<div className="flex flex-col gap-4">
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">Chat History</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
<div className="flex gap-4">
<button
onClick={handleExportAllChats}
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"
>
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}
className={classNames(
'px-4 py-2 bg-bolt-elements-button-danger-background hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text rounded-lg transition-colors',
isDeleting ? 'opacity-50 cursor-not-allowed' : '',
)}
>
{isDeleting ? 'Deleting...' : 'Delete All Chats'}
</button>
</div>
</div>
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">Settings Backup</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Export your settings to a JSON file or import settings from a previously exported file.
</p>
<div className="flex gap-4">
<button
onClick={handleExportSettings}
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"
>
Export Settings
</button>
<label 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 cursor-pointer">
Import Settings
<input type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
</label>
</div>
</div>
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">API Keys Management</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Import API keys from a JSON file or download a template to fill in your keys.
</p>
<div className="flex gap-4">
<button
onClick={handleExportApiKeyTemplate}
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"
>
Download Template
</button>
<label 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 cursor-pointer">
Import API Keys
<input type="file" accept=".json" onChange={handleImportApiKeys} className="hidden" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useSettings } from '~/lib/hooks/useSettings'; import { useSettings } from '~/lib/hooks/useSettings';
import commit from '~/commit.json';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { providerBaseUrlEnvKeys } from '~/utils/constants';
interface ProviderStatus { interface ProviderStatus {
name: string; name: string;
@@ -43,16 +43,38 @@ interface CommitData {
version?: string; version?: string;
} }
const connitJson: CommitData = commit; const connitJson: CommitData = {
commit: __COMMIT_HASH,
version: __APP_VERSION,
};
const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
const versionHash = connitJson.commit; const versionHash = connitJson.commit;
const versionTag = connitJson.version; const versionTag = connitJson.version;
const GITHUB_URLS = { const GITHUB_URLS = {
original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main', original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main',
fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main', fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main',
commitJson: (branch: string) => commitJson: async (branch: string) => {
`https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/app/commit.json`, try {
const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
const data: { sha: string } = await response.json();
const packageJsonResp = await fetch(
`https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/package.json`,
);
const packageJson: { version: string } = await packageJsonResp.json();
return {
commit: data.sha.slice(0, 7),
version: packageJson.version,
};
} catch (error) {
console.log('Failed to fetch local commit info:', error);
throw new Error('Failed to fetch local commit info');
}
},
}; };
function getSystemInfo(): SystemInfo { function getSystemInfo(): SystemInfo {
@@ -236,7 +258,7 @@ const checkProviderStatus = async (url: string | null, providerName: string): Pr
} }
// Try different endpoints based on provider // Try different endpoints based on provider
const checkUrls = [`${url}/api/health`, `${url}/v1/models`]; const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`];
console.log(`[Debug] Checking additional endpoints:`, checkUrls); console.log(`[Debug] Checking additional endpoints:`, checkUrls);
const results = await Promise.all( const results = await Promise.all(
@@ -321,14 +343,16 @@ export default function DebugTab() {
.filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name)) .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name))
.map(async ([, provider]) => { .map(async ([, provider]) => {
const envVarName = const envVarName =
provider.name.toLowerCase() === 'ollama' providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`;
? 'OLLAMA_API_BASE_URL'
: provider.name.toLowerCase() === 'lmstudio'
? 'LMSTUDIO_API_BASE_URL'
: `REACT_APP_${provider.name.toUpperCase()}_URL`;
// Access environment variables through import.meta.env // Access environment variables through import.meta.env
const url = import.meta.env[envVarName] || provider.settings.baseUrl || null; // Ensure baseUrl is used let settingsUrl = provider.settings.baseUrl;
if (settingsUrl && settingsUrl.trim().length === 0) {
settingsUrl = undefined;
}
const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used
console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`);
const status = await checkProviderStatus(url, provider.name); const status = await checkProviderStatus(url, provider.name);
@@ -366,14 +390,9 @@ export default function DebugTab() {
const branchToCheck = isLatestBranch ? 'main' : 'stable'; const branchToCheck = isLatestBranch ? 'main' : 'stable';
console.log(`[Debug] Checking for updates against ${branchToCheck} branch`); console.log(`[Debug] Checking for updates against ${branchToCheck} branch`);
const localCommitResponse = await fetch(GITHUB_URLS.commitJson(branchToCheck)); const latestCommitResp = await GITHUB_URLS.commitJson(branchToCheck);
if (!localCommitResponse.ok) { const remoteCommitHash = latestCommitResp.commit;
throw new Error('Failed to fetch local commit info');
}
const localCommitData = (await localCommitResponse.json()) as CommitData;
const remoteCommitHash = localCommitData.commit;
const currentCommitHash = versionHash; const currentCommitHash = versionHash;
if (remoteCommitHash !== currentCommitHash) { if (remoteCommitHash !== currentCommitHash) {
@@ -521,7 +540,7 @@ export default function DebugTab() {
<div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover"> <div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover">
<p className="text-xs text-bolt-elements-textSecondary">Version</p> <p className="text-xs text-bolt-elements-textSecondary">Version</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary font-mono"> <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
{versionHash.slice(0, 7)} {connitJson.commit.slice(0, 7)}
<span className="ml-2 text-xs text-bolt-elements-textSecondary"> <span className="ml-2 text-xs text-bolt-elements-textSecondary">
(v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'} (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'}
</span> </span>

View File

@@ -14,6 +14,10 @@ export default function FeaturesTab() {
enableLatestBranch, enableLatestBranch,
promptId, promptId,
setPromptId, setPromptId,
autoSelectTemplate,
setAutoSelectTemplate,
enableContextOptimization,
contextOptimizationEnabled,
} = useSettings(); } = useSettings();
const handleToggle = (enabled: boolean) => { const handleToggle = (enabled: boolean) => {
@@ -33,29 +37,55 @@ export default function FeaturesTab() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<span className="text-bolt-elements-textPrimary">Use Main Branch</span> <span className="text-bolt-elements-textPrimary">Use Main Branch</span>
<p className="text-sm text-bolt-elements-textSecondary"> <p className="text-xs text-bolt-elements-textTertiary">
Check for updates against the main branch instead of stable Check for updates against the main branch instead of stable
</p> </p>
</div> </div>
<Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} /> <Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
</div> </div>
<div className="flex items-center justify-between">
<div>
<span className="text-bolt-elements-textPrimary">Auto Select Code Template</span>
<p className="text-xs text-bolt-elements-textTertiary">
Let Bolt select the best starter template for your project.
</p>
</div>
<Switch className="ml-auto" checked={autoSelectTemplate} onCheckedChange={setAutoSelectTemplate} />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-bolt-elements-textPrimary">Use Context Optimization</span>
<p className="text-sm text-bolt-elements-textSecondary">
redact file contents form chat and puts the latest file contents on the system prompt
</p>
</div>
<Switch
className="ml-auto"
checked={contextOptimizationEnabled}
onCheckedChange={enableContextOptimization}
/>
</div>
</div> </div>
</div> </div>
<div className="mb-6 border-t border-bolt-elements-borderColor pt-4"> <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3> <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
<p className="text-sm text-bolt-elements-textSecondary mb-4"> <p className="text-sm text-bolt-elements-textSecondary mb-10">
Disclaimer: Experimental features may be unstable and are subject to change. Disclaimer: Experimental features may be unstable and are subject to change.
</p> </p>
<div className="flex flex-col">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-bolt-elements-textPrimary">Experimental Providers</span> <span className="text-bolt-elements-textPrimary">Experimental Providers</span>
<Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} /> <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
</div> </div>
<p className="text-xs text-bolt-elements-textTertiary mb-4">
Enable experimental providers such as Ollama, LMStudio, and OpenAILike.
</p>
</div>
<div className="flex items-start justify-between pt-4 mb-2 gap-2"> <div className="flex items-start justify-between pt-4 mb-2 gap-2">
<div className="flex-1 max-w-[200px]"> <div className="flex-1 max-w-[200px]">
<span className="text-bolt-elements-textPrimary">Prompt Library</span> <span className="text-bolt-elements-textPrimary">Prompt Library</span>
<p className="text-sm text-bolt-elements-textSecondary mb-4"> <p className="text-xs text-bolt-elements-textTertiary mb-4">
Choose a prompt from the library to use as the system prompt. Choose a prompt from the library to use as the system prompt.
</p> </p>
</div> </div>

View File

@@ -6,7 +6,9 @@ import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs'; import { logStore } from '~/lib/stores/logs';
// Import a default fallback icon // Import a default fallback icon
import DefaultIcon from '/icons/Default.svg'; // Adjust the path as necessary import { providerBaseUrlEnvKeys } from '~/utils/constants';
const DefaultIcon = '/icons/Default.svg'; // Adjust the path as necessary
export default function ProvidersTab() { export default function ProvidersTab() {
const { providers, updateProviderSettings, isLocalModel } = useSettings(); const { providers, updateProviderSettings, isLocalModel } = useSettings();
@@ -33,31 +35,28 @@ export default function ProvidersTab() {
newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name)); newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
setFilteredProviders(newFilteredProviders); // Split providers into regular and URL-configurable
const regular = newFilteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
setFilteredProviders([...regular, ...urlConfigurable]);
}, [providers, searchTerm, isLocalModel]); }, [providers, searchTerm, isLocalModel]);
const renderProviderCard = (provider: IProviderConfig) => {
const envBaseUrlKey = providerBaseUrlEnvKeys[provider.name].baseUrlKey;
const envBaseUrl = envBaseUrlKey ? import.meta.env[envBaseUrlKey] : undefined;
const isUrlConfigurable = URL_CONFIGURABLE_PROVIDERS.includes(provider.name);
return ( return (
<div className="p-4">
<div className="flex mb-4">
<input
type="text"
placeholder="Search providers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
{filteredProviders.map((provider) => (
<div <div
key={provider.name} key={provider.name}
className="flex flex-col mb-2 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor " className="flex flex-col provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor"
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<img <img
src={`/icons/${provider.name}.svg`} // Attempt to load the specific icon src={`/icons/${provider.name}.svg`}
onError={(e) => { onError={(e) => {
// Fallback to default icon on error
e.currentTarget.src = DefaultIcon; e.currentTarget.src = DefaultIcon;
}} }}
alt={`${provider.name} icon`} alt={`${provider.name} icon`}
@@ -79,15 +78,26 @@ export default function ProvidersTab() {
}} }}
/> />
</div> </div>
{/* Base URL input for configurable providers */} {isUrlConfigurable && provider.settings.enabled && (
{URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && (
<div className="mt-2"> <div className="mt-2">
<label className="block text-sm text-bolt-elements-textSecondary mb-1">Base URL:</label> {envBaseUrl && (
<label className="block text-xs text-bolt-elements-textSecondary text-green-300 mb-2">
Set On (.env) : {envBaseUrl}
</label>
)}
<label className="block text-sm text-bolt-elements-textSecondary mb-2">
{envBaseUrl ? 'Override Base Url' : 'Base URL '}:{' '}
</label>
<input <input
type="text" type="text"
value={provider.settings.baseUrl || ''} value={provider.settings.baseUrl || ''}
onChange={(e) => { onChange={(e) => {
const newBaseUrl = e.target.value; let newBaseUrl: string | undefined = e.target.value;
if (newBaseUrl && newBaseUrl.trim().length === 0) {
newBaseUrl = undefined;
}
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
logStore.logProvider(`Base URL updated for ${provider.name}`, { logStore.logProvider(`Base URL updated for ${provider.name}`, {
provider: provider.name, provider: provider.name,
@@ -100,7 +110,38 @@ export default function ProvidersTab() {
</div> </div>
)} )}
</div> </div>
))} );
};
const regularProviders = filteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
const urlConfigurableProviders = filteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
return (
<div className="p-4">
<div className="flex mb-4">
<input
type="text"
placeholder="Search providers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
{/* Regular Providers Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">{regularProviders.map(renderProviderCard)}</div>
{/* URL Configurable Providers Section */}
{urlConfigurableProviders.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-semibold mb-2 text-bolt-elements-textPrimary">Experimental Providers</h3>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
These providers are experimental and allow you to run AI models locally or connect to your own
infrastructure. They require additional setup but offer more flexibility.
</p>
<div className="space-y-4">{urlConfigurableProviders.map(renderProviderCard)}</div>
</div>
)}
</div> </div>
); );
} }

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

View File

@@ -0,0 +1,20 @@
import * as Popover from '@radix-ui/react-popover';
import type { PropsWithChildren, ReactNode } from 'react';
export default ({ children, trigger }: PropsWithChildren<{ trigger: ReactNode }>) => (
<Popover.Root>
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
<Popover.Anchor />
<Popover.Portal>
<Popover.Content
sideOffset={10}
side="top"
align="center"
className="bg-bolt-elements-background-depth-2 text-bolt-elements-item-contentAccent p-2 rounded-md shadow-xl z-workbench"
>
{children}
<Popover.Arrow className="bg-bolt-elements-item-background-depth-2" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);

View File

@@ -1,5 +1,5 @@
import { useStore } from '@nanostores/react';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useStore } from '@nanostores/react';
import { IconButton } from '~/components/ui/IconButton'; import { IconButton } from '~/components/ui/IconButton';
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { PortDropdown } from './PortDropdown'; import { PortDropdown } from './PortDropdown';
@@ -7,6 +7,20 @@ import { ScreenshotSelector } from './ScreenshotSelector';
type ResizeSide = 'left' | 'right' | null; type ResizeSide = 'left' | 'right' | null;
interface WindowSize {
name: string;
width: number;
height: number;
icon: string;
}
const WINDOW_SIZES: WindowSize[] = [
{ name: 'Mobile', width: 375, height: 667, icon: 'i-ph:device-mobile' },
{ name: 'Tablet', width: 768, height: 1024, icon: 'i-ph:device-tablet' },
{ name: 'Laptop', width: 1366, height: 768, icon: 'i-ph:laptop' },
{ name: 'Desktop', width: 1920, height: 1080, icon: 'i-ph:monitor' },
];
export const Preview = memo(() => { export const Preview = memo(() => {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -15,6 +29,7 @@ export const Preview = memo(() => {
const [activePreviewIndex, setActivePreviewIndex] = useState(0); const [activePreviewIndex, setActivePreviewIndex] = useState(0);
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false); const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
const hasSelectedPreview = useRef(false); const hasSelectedPreview = useRef(false);
const previews = useStore(workbenchStore.previews); const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex]; const activePreview = previews[activePreviewIndex];
@@ -27,7 +42,7 @@ export const Preview = memo(() => {
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false); const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
// Use percentage for width // Use percentage for width
const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially const [widthPercent, setWidthPercent] = useState<number>(37.5);
const resizingState = useRef({ const resizingState = useRef({
isResizing: false, isResizing: false,
@@ -37,8 +52,10 @@ export const Preview = memo(() => {
windowWidth: window.innerWidth, windowWidth: window.innerWidth,
}); });
// Define the scaling factor const SCALING_FACTOR = 2;
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
const [isWindowSizeDropdownOpen, setIsWindowSizeDropdownOpen] = useState(false);
const [selectedWindowSize, setSelectedWindowSize] = useState<WindowSize>(WINDOW_SIZES[0]);
useEffect(() => { useEffect(() => {
if (!activePreview) { if (!activePreview) {
@@ -79,7 +96,6 @@ export const Preview = memo(() => {
[], [],
); );
// When previews change, display the lowest port if user hasn't selected a preview
useEffect(() => { useEffect(() => {
if (previews.length > 1 && !hasSelectedPreview.current) { if (previews.length > 1 && !hasSelectedPreview.current) {
const minPortIndex = previews.reduce(findMinPortIndex, 0); const minPortIndex = previews.reduce(findMinPortIndex, 0);
@@ -122,7 +138,6 @@ export const Preview = memo(() => {
return; return;
} }
// Prevent text selection
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
resizingState.current.isResizing = true; resizingState.current.isResizing = true;
@@ -134,7 +149,7 @@ export const Preview = memo(() => {
document.addEventListener('mousemove', onMouseMove); document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp); document.addEventListener('mouseup', onMouseUp);
e.preventDefault(); // Prevent any text selection on mousedown e.preventDefault();
}; };
const onMouseMove = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => {
@@ -145,7 +160,6 @@ export const Preview = memo(() => {
const dx = e.clientX - resizingState.current.startX; const dx = e.clientX - resizingState.current.startX;
const windowWidth = resizingState.current.windowWidth; const windowWidth = resizingState.current.windowWidth;
// Apply scaling factor to increase sensitivity
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR; const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
let newWidthPercent = resizingState.current.startWidthPercent; let newWidthPercent = resizingState.current.startWidthPercent;
@@ -156,7 +170,6 @@ export const Preview = memo(() => {
newWidthPercent = resizingState.current.startWidthPercent - dxPercent; newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
} }
// Clamp the width between 10% and 90%
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90)); newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
setWidthPercent(newWidthPercent); setWidthPercent(newWidthPercent);
@@ -168,17 +181,12 @@ export const Preview = memo(() => {
document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('mouseup', onMouseUp);
// Restore text selection
document.body.style.userSelect = ''; document.body.style.userSelect = '';
}; };
// Handle window resize to ensure widthPercent remains valid
useEffect(() => { useEffect(() => {
const handleWindowResize = () => { const handleWindowResize = () => {
/* // Optional: Adjust widthPercent if necessary
* Optional: Adjust widthPercent if necessary
* For now, since widthPercent is relative, no action is needed
*/
}; };
window.addEventListener('resize', handleWindowResize); window.addEventListener('resize', handleWindowResize);
@@ -188,7 +196,6 @@ export const Preview = memo(() => {
}; };
}, []); }, []);
// A small helper component for the handle's "grip" icon
const GripIcon = () => ( const GripIcon = () => (
<div <div
style={{ style={{
@@ -213,22 +220,47 @@ export const Preview = memo(() => {
</div> </div>
); );
const openInNewWindow = (size: WindowSize) => {
if (activePreview?.baseUrl) {
const match = activePreview.baseUrl.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
if (match) {
const previewId = match[1];
const previewUrl = `/webcontainer/preview/${previewId}`;
const newWindow = window.open(
previewUrl,
'_blank',
`noopener,noreferrer,width=${size.width},height=${size.height},menubar=no,toolbar=no,location=no,status=no`,
);
if (newWindow) {
newWindow.focus();
}
} else {
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
}
}
};
return ( return (
<div ref={containerRef} className="w-full h-full flex flex-col relative"> <div
ref={containerRef}
className={`w-full h-full flex flex-col relative ${isPreviewOnly ? 'fixed inset-0 z-50 bg-white' : ''}`}
>
{isPortDropdownOpen && ( {isPortDropdownOpen && (
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} /> <div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
)} )}
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5"> <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-2">
<div className="flex items-center gap-2">
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} /> <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
<IconButton <IconButton
icon="i-ph:selection" icon="i-ph:selection"
onClick={() => setIsSelectionMode(!isSelectionMode)} onClick={() => setIsSelectionMode(!isSelectionMode)}
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''} className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
/> />
<div </div>
className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive" <div className="flex-grow flex items-center gap-1 bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
>
<input <input
title="URL" title="URL"
ref={inputRef} ref={inputRef}
@@ -250,6 +282,7 @@ export const Preview = memo(() => {
/> />
</div> </div>
<div className="flex items-center gap-2">
{previews.length > 1 && ( {previews.length > 1 && (
<PortDropdown <PortDropdown
activePreviewIndex={activePreviewIndex} activePreviewIndex={activePreviewIndex}
@@ -261,28 +294,78 @@ export const Preview = memo(() => {
/> />
)} )}
{/* Device mode toggle button */}
<IconButton <IconButton
icon="i-ph:devices" icon="i-ph:devices"
onClick={toggleDeviceMode} onClick={toggleDeviceMode}
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'} title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
/> />
{/* Fullscreen toggle button */} <IconButton
icon="i-ph:layout-light"
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
/>
<IconButton <IconButton
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'} icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
onClick={toggleFullscreen} onClick={toggleFullscreen}
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'} title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
/> />
<div className="flex items-center relative">
<IconButton
icon="i-ph:arrow-square-out"
onClick={() => openInNewWindow(selectedWindowSize)}
title={`Open Preview in ${selectedWindowSize.name} Window`}
/>
<IconButton
icon="i-ph:caret-down"
onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
className="ml-1"
title="Select Window Size"
/>
{isWindowSizeDropdownOpen && (
<>
<div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
<div className="absolute right-0 top-full mt-2 z-50 min-w-[240px] bg-white dark:bg-black rounded-xl shadow-2xl border border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)] overflow-hidden">
{WINDOW_SIZES.map((size) => (
<button
key={size.name}
className="w-full px-4 py-3.5 text-left text-[#111827] dark:text-gray-300 text-sm whitespace-nowrap flex items-center gap-3 group hover:bg-[#F5EEFF] dark:hover:bg-gray-900 bg-white dark:bg-black"
onClick={() => {
setSelectedWindowSize(size);
setIsWindowSizeDropdownOpen(false);
openInNewWindow(size);
}}
>
<div
className={`${size.icon} w-5 h-5 text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200`}
/>
<div className="flex flex-col">
<span className="font-medium group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
{size.name}
</span>
<span className="text-xs text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
{size.width} × {size.height}
</span>
</div>
</button>
))}
</div>
</>
)}
</div>
</div>
</div> </div>
<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto"> <div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
<div <div
style={{ style={{
width: isDeviceModeOn ? `${widthPercent}%` : '100%', width: isDeviceModeOn ? `${widthPercent}%` : '100%',
height: '100%', // Always full height height: '100%',
overflow: 'visible', overflow: 'visible',
background: '#fff', background: 'var(--bolt-elements-background-depth-1)',
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
}} }}
@@ -292,9 +375,10 @@ export const Preview = memo(() => {
<iframe <iframe
ref={iframeRef} ref={iframeRef}
title="preview" title="preview"
className="border-none w-full h-full bg-white" className="border-none w-full h-full bg-bolt-elements-background-depth-1"
src={iframeUrl} src={iframeUrl}
allowFullScreen sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
allow="cross-origin-isolated"
/> />
<ScreenshotSelector <ScreenshotSelector
isSelectionMode={isSelectionMode} isSelectionMode={isSelectionMode}
@@ -303,12 +387,13 @@ export const Preview = memo(() => {
/> />
</> </>
) : ( ) : (
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div> <div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary">
No preview available
</div>
)} )}
{isDeviceModeOn && ( {isDeviceModeOn && (
<> <>
{/* Left handle */}
<div <div
onMouseDown={(e) => startResizing(e, 'left')} onMouseDown={(e) => startResizing(e, 'left')}
style={{ style={{
@@ -333,7 +418,6 @@ export const Preview = memo(() => {
<GripIcon /> <GripIcon />
</div> </div>
{/* Right handle */}
<div <div
onMouseDown={(e) => startResizing(e, 'right')} onMouseDown={(e) => startResizing(e, 'right')}
style={{ style={{

View File

@@ -1,20 +1,19 @@
import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'; import type { AppLoadContext } from '@remix-run/cloudflare';
import { RemixServer } from '@remix-run/react'; import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot'; import { isbot } from 'isbot';
import { renderToReadableStream } from 'react-dom/server'; import { renderToReadableStream } from 'react-dom/server';
import { renderHeadToString } from 'remix-island'; import { renderHeadToString } from 'remix-island';
import { Head } from './root'; import { Head } from './root';
import { themeStore } from '~/lib/stores/theme'; import { themeStore } from '~/lib/stores/theme';
import { initializeModelList } from '~/utils/constants';
export default async function handleRequest( export default async function handleRequest(
request: Request, request: Request,
responseStatusCode: number, responseStatusCode: number,
responseHeaders: Headers, responseHeaders: Headers,
remixContext: EntryContext, remixContext: any,
_loadContext: AppLoadContext, _loadContext: AppLoadContext,
) { ) {
await initializeModelList(); // await initializeModelList({});
const readable = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, { const readable = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
signal: request.signal, signal: request.signal,

View File

@@ -1,73 +0,0 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { env } from 'node:process';
export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
/**
* The `cloudflareEnv` is only used when deployed or when previewing locally.
* In development the environment variables are available through `env`.
*/
// First check user-provided API keys
if (userApiKeys?.[provider]) {
return userApiKeys[provider];
}
// Fall back to environment variables
switch (provider) {
case 'Anthropic':
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
case 'OpenAI':
return env.OPENAI_API_KEY || cloudflareEnv.OPENAI_API_KEY;
case 'Google':
return env.GOOGLE_GENERATIVE_AI_API_KEY || cloudflareEnv.GOOGLE_GENERATIVE_AI_API_KEY;
case 'Groq':
return env.GROQ_API_KEY || cloudflareEnv.GROQ_API_KEY;
case 'HuggingFace':
return env.HuggingFace_API_KEY || cloudflareEnv.HuggingFace_API_KEY;
case 'OpenRouter':
return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
case 'Deepseek':
return env.DEEPSEEK_API_KEY || cloudflareEnv.DEEPSEEK_API_KEY;
case 'Mistral':
return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
case 'OpenAILike':
return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
case 'Together':
return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
case 'xAI':
return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
case 'Perplexity':
return env.PERPLEXITY_API_KEY || cloudflareEnv.PERPLEXITY_API_KEY;
case 'Cohere':
return env.COHERE_API_KEY;
case 'AzureOpenAI':
return env.AZURE_OPENAI_API_KEY;
default:
return '';
}
}
export function getBaseURL(cloudflareEnv: Env, provider: string) {
switch (provider) {
case 'Together':
return env.TOGETHER_API_BASE_URL || cloudflareEnv.TOGETHER_API_BASE_URL || 'https://api.together.xyz/v1';
case 'OpenAILike':
return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
case 'LMStudio':
return env.LMSTUDIO_API_BASE_URL || cloudflareEnv.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
case 'Ollama': {
let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || 'http://localhost:11434';
if (env.RUNNING_IN_DOCKER === 'true') {
baseUrl = baseUrl.replace('localhost', 'host.docker.internal');
}
return baseUrl;
}
default:
return '';
}
}

View File

@@ -3,3 +3,36 @@ export const MAX_TOKENS = 8000;
// limits the number of model responses that can be returned in a single request // limits the number of model responses that can be returned in a single request
export const MAX_RESPONSE_SEGMENTS = 2; export const MAX_RESPONSE_SEGMENTS = 2;
export interface File {
type: 'file';
content: string;
isBinary: boolean;
}
export interface Folder {
type: 'folder';
}
type Dirent = File | Folder;
export type FileMap = Record<string, Dirent | undefined>;
export const IGNORE_PATTERNS = [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'.cache/**',
'.vscode/**',
'.idea/**',
'**/*.log',
'**/.DS_Store',
'**/npm-debug.log*',
'**/yarn-debug.log*',
'**/yarn-error.log*',
'**/*lock.json',
'**/*lock.yml',
];

View File

@@ -0,0 +1,138 @@
import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai';
import type { IProviderSetting } from '~/types/model';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
import { extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
import { createScopedLogger } from '~/utils/logger';
import { LLMManager } from '~/lib/modules/llm/manager';
const logger = createScopedLogger('create-summary');
export async function createSummary(props: {
messages: Message[];
env?: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
promptId?: string;
contextOptimization?: boolean;
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
}) {
const { messages, env: serverEnv, apiKeys, providerSettings, contextOptimization, onFinish } = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
const processedMessages = messages.map((message) => {
if (message.role === 'user') {
const { model, provider, content } = extractPropertiesFromMessage(message);
currentModel = model;
currentProvider = provider;
return { ...message, content };
} else if (message.role == 'assistant') {
let content = message.content;
if (contextOptimization) {
content = simplifyBoltActions(content);
}
return { ...message, content };
}
return message;
});
const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER;
const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider);
let modelDetails = staticModels.find((m) => m.name === currentModel);
if (!modelDetails) {
const modelsList = [
...(provider.staticModels || []),
...(await LLMManager.getInstance().getModelListFromProvider(provider, {
apiKeys,
providerSettings,
serverEnv: serverEnv as any,
})),
];
if (!modelsList.length) {
throw new Error(`No models found for provider ${provider.name}`);
}
modelDetails = modelsList.find((m) => m.name === currentModel);
if (!modelDetails) {
// Fallback to first model
logger.warn(
`MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`,
);
modelDetails = modelsList[0];
}
}
let slicedMessages = processedMessages;
const { summary } = extractCurrentContext(processedMessages);
let summaryText: string | undefined = undefined;
let chatId: string | undefined = undefined;
if (summary && summary.type === 'chatSummary') {
chatId = summary.chatId;
summaryText = `Below is the Chat Summary till now, this is chat summary before the conversation provided by the user
you should also use this as historical message while providing the response to the user.
${summary.summary}`;
if (chatId) {
let index = 0;
for (let i = 0; i < processedMessages.length; i++) {
if (processedMessages[i].id === chatId) {
index = i;
break;
}
}
slicedMessages = processedMessages.slice(index + 1);
}
}
const extractTextContent = (message: Message) =>
Array.isArray(message.content)
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
: message.content;
// select files from the list of code file from the project that might be useful for the current request from the user
const resp = await generateText({
system: `
You are a software engineer. You are working on a project. tou need to summarize the work till now and provide a summary of the chat till now.
${summaryText}
RULES:
* Only provide the summary of the chat till now.
* Do not provide any new information.
`,
prompt: `
please provide a summary of the chat till now.
below is the latest chat:
---
${slicedMessages
.map((x) => {
return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
})
.join('\n')}
---
`,
model: provider.getModelInstance({
model: currentModel,
serverEnv,
apiKeys,
providerSettings,
}),
});
const response = resp.text;
if (onFinish) {
onFinish(resp);
}
return response;
}

View File

@@ -1,187 +0,0 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { getAPIKey, getBaseURL } from '~/lib/.server/llm/api-key';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { ollama } from 'ollama-ai-provider';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { createMistral } from '@ai-sdk/mistral';
import { createCohere } from '@ai-sdk/cohere';
import type { LanguageModelV1 } from 'ai';
import type { IProviderSetting } from '~/types/model';
export const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
type OptionalApiKey = string | undefined;
export function getAnthropicModel(apiKey: OptionalApiKey, model: string) {
const anthropic = createAnthropic({
apiKey,
});
return anthropic(model);
}
export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL,
apiKey,
});
return openai(model);
}
export function getCohereAIModel(apiKey: OptionalApiKey, model: string) {
const cohere = createCohere({
apiKey,
});
return cohere(model);
}
export function getOpenAIModel(apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
apiKey,
});
return openai(model);
}
export function getMistralModel(apiKey: OptionalApiKey, model: string) {
const mistral = createMistral({
apiKey,
});
return mistral(model);
}
export function getGoogleModel(apiKey: OptionalApiKey, model: string) {
const google = createGoogleGenerativeAI({
apiKey,
});
return google(model);
}
export function getGroqModel(apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL: 'https://api.groq.com/openai/v1',
apiKey,
});
return openai(model);
}
export function getHuggingFaceModel(apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL: 'https://api-inference.huggingface.co/v1/',
apiKey,
});
return openai(model);
}
export function getOllamaModel(baseURL: string, model: string) {
const ollamaInstance = ollama(model, {
numCtx: DEFAULT_NUM_CTX,
}) as LanguageModelV1 & { config: any };
ollamaInstance.config.baseURL = `${baseURL}/api`;
return ollamaInstance;
}
export function getDeepseekModel(apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL: 'https://api.deepseek.com/beta',
apiKey,
});
return openai(model);
}
export function getOpenRouterModel(apiKey: OptionalApiKey, model: string) {
const openRouter = createOpenRouter({
apiKey,
});
return openRouter.chat(model);
}
export function getLMStudioModel(baseURL: string, model: string) {
const lmstudio = createOpenAI({
baseUrl: `${baseURL}/v1`,
apiKey: '',
});
return lmstudio(model);
}
export function getXAIModel(apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL: 'https://api.x.ai/v1',
apiKey,
});
return openai(model);
}
export function getPerplexityModel(apiKey: OptionalApiKey, model: string) {
const perplexity = createOpenAI({
baseURL: 'https://api.perplexity.ai/',
apiKey,
});
return perplexity(model);
}
export function getModel(
provider: string,
model: string,
env: Env,
apiKeys?: Record<string, string>,
providerSettings?: Record<string, IProviderSetting>,
) {
/*
* let apiKey; // Declare first
* let baseURL;
*/
const apiKey = getAPIKey(env, provider, apiKeys); // Then assign
const baseURL = providerSettings?.[provider].baseUrl || getBaseURL(env, provider);
switch (provider) {
case 'Anthropic':
return getAnthropicModel(apiKey, model);
case 'OpenAI':
return getOpenAIModel(apiKey, model);
case 'Groq':
return getGroqModel(apiKey, model);
case 'HuggingFace':
return getHuggingFaceModel(apiKey, model);
case 'OpenRouter':
return getOpenRouterModel(apiKey, model);
case 'Google':
return getGoogleModel(apiKey, model);
case 'OpenAILike':
return getOpenAILikeModel(baseURL, apiKey, model);
case 'Together':
return getOpenAILikeModel(baseURL, apiKey, model);
case 'Deepseek':
return getDeepseekModel(apiKey, model);
case 'Mistral':
return getMistralModel(apiKey, model);
case 'LMStudio':
return getLMStudioModel(baseURL, model);
case 'xAI':
return getXAIModel(apiKey, model);
case 'Cohere':
return getCohereAIModel(apiKey, model);
case 'Perplexity':
return getPerplexityModel(apiKey, model);
default:
return getOllamaModel(baseURL, model);
}
}

View File

@@ -0,0 +1,233 @@
import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai';
import ignore from 'ignore';
import type { IProviderSetting } from '~/types/model';
import { IGNORE_PATTERNS, type FileMap } from './constants';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
import { createFilesContext, extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
import { createScopedLogger } from '~/utils/logger';
import { LLMManager } from '~/lib/modules/llm/manager';
// Common patterns to ignore, similar to .gitignore
const ig = ignore().add(IGNORE_PATTERNS);
const logger = createScopedLogger('select-context');
export async function selectContext(props: {
messages: Message[];
env?: Env;
apiKeys?: Record<string, string>;
files: FileMap;
providerSettings?: Record<string, IProviderSetting>;
promptId?: string;
contextOptimization?: boolean;
summary: string;
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
}) {
const { messages, env: serverEnv, apiKeys, files, providerSettings, contextOptimization, summary, onFinish } = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
const processedMessages = messages.map((message) => {
if (message.role === 'user') {
const { model, provider, content } = extractPropertiesFromMessage(message);
currentModel = model;
currentProvider = provider;
return { ...message, content };
} else if (message.role == 'assistant') {
let content = message.content;
if (contextOptimization) {
content = simplifyBoltActions(content);
}
return { ...message, content };
}
return message;
});
const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER;
const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider);
let modelDetails = staticModels.find((m) => m.name === currentModel);
if (!modelDetails) {
const modelsList = [
...(provider.staticModels || []),
...(await LLMManager.getInstance().getModelListFromProvider(provider, {
apiKeys,
providerSettings,
serverEnv: serverEnv as any,
})),
];
if (!modelsList.length) {
throw new Error(`No models found for provider ${provider.name}`);
}
modelDetails = modelsList.find((m) => m.name === currentModel);
if (!modelDetails) {
// Fallback to first model
logger.warn(
`MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`,
);
modelDetails = modelsList[0];
}
}
const { codeContext } = extractCurrentContext(processedMessages);
let filePaths = getFilePaths(files || {});
filePaths = filePaths.filter((x) => {
const relPath = x.replace('/home/project/', '');
return !ig.ignores(relPath);
});
let context = '';
const currrentFiles: string[] = [];
const contextFiles: FileMap = {};
if (codeContext?.type === 'codeContext') {
const codeContextFiles: string[] = codeContext.files;
Object.keys(files || {}).forEach((path) => {
let relativePath = path;
if (path.startsWith('/home/project/')) {
relativePath = path.replace('/home/project/', '');
}
if (codeContextFiles.includes(relativePath)) {
contextFiles[relativePath] = files[path];
currrentFiles.push(relativePath);
}
});
context = createFilesContext(contextFiles);
}
const summaryText = `Here is the summary of the chat till now: ${summary}`;
const extractTextContent = (message: Message) =>
Array.isArray(message.content)
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
: message.content;
const lastUserMessage = processedMessages.filter((x) => x.role == 'user').pop();
if (!lastUserMessage) {
throw new Error('No user message found');
}
// select files from the list of code file from the project that might be useful for the current request from the user
const resp = await generateText({
system: `
You are a software engineer. You are working on a project. You have access to the following files:
AVAILABLE FILES PATHS
---
${filePaths.map((path) => `- ${path}`).join('\n')}
---
You have following code loaded in the context buffer that you can refer to:
CURRENT CONTEXT BUFFER
---
${context}
---
Now, you are given a task. You need to select the files that are relevant to the task from the list of files above.
RESPONSE FORMAT:
your response shoudl be in following format:
---
<updateContextBuffer>
<includeFile path="path/to/file"/>
<excludeFile path="path/to/file"/>
</updateContextBuffer>
---
* Your should start with <updateContextBuffer> and end with </updateContextBuffer>.
* You can include multiple <includeFile> and <excludeFile> tags in the response.
* You should not include any other text in the response.
* You should not include any file that is not in the list of files above.
* You should not include any file that is already in the context buffer.
* If no changes are needed, you can leave the response empty updateContextBuffer tag.
`,
prompt: `
${summaryText}
Users Question: ${extractTextContent(lastUserMessage)}
update the context buffer with the files that are relevant to the task from the list of files above.
CRITICAL RULES:
* Only include relevant files in the context buffer.
* context buffer should not include any file that is not in the list of files above.
* context buffer is extremlly expensive, so only include files that are absolutely necessary.
* If no changes are needed, you can leave the response empty updateContextBuffer tag.
* Only 5 files can be placed in the context buffer at a time.
* if the buffer is full, you need to exclude files that is not needed and include files that is relevent.
`,
model: provider.getModelInstance({
model: currentModel,
serverEnv,
apiKeys,
providerSettings,
}),
});
const response = resp.text;
const updateContextBuffer = response.match(/<updateContextBuffer>([\s\S]*?)<\/updateContextBuffer>/);
if (!updateContextBuffer) {
throw new Error('Invalid response. Please follow the response format');
}
const includeFiles =
updateContextBuffer[1]
.match(/<includeFile path="(.*?)"/gm)
?.map((x) => x.replace('<includeFile path="', '').replace('"', '')) || [];
const excludeFiles =
updateContextBuffer[1]
.match(/<excludeFile path="(.*?)"/gm)
?.map((x) => x.replace('<excludeFile path="', '').replace('"', '')) || [];
const filteredFiles: FileMap = {};
excludeFiles.forEach((path) => {
delete contextFiles[path];
});
includeFiles.forEach((path) => {
let fullPath = path;
if (!path.startsWith('/home/project/')) {
fullPath = `/home/project/${path}`;
}
if (!filePaths.includes(fullPath)) {
throw new Error(`File ${path} is not in the list of files above.`);
}
if (currrentFiles.includes(path)) {
return;
}
filteredFiles[path] = files[fullPath];
});
if (onFinish) {
onFinish(resp);
}
return filteredFiles;
// generateText({
}
export function getFilePaths(files: FileMap) {
let filePaths = Object.keys(files);
filePaths = filePaths.filter((x) => {
const relPath = x.replace('/home/project/', '');
return !ig.ignores(relPath);
});
return filePaths;
}

View File

@@ -1,175 +1,60 @@
import { convertToCoreMessages, streamText as _streamText } from 'ai'; import { convertToCoreMessages, streamText as _streamText, type Message } from 'ai';
import { getModel } from '~/lib/.server/llm/model'; import { MAX_TOKENS, type FileMap } from './constants';
import { MAX_TOKENS } from './constants';
import { getSystemPrompt } from '~/lib/common/prompts/prompts'; import { getSystemPrompt } from '~/lib/common/prompts/prompts';
import { import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODIFICATIONS_TAG_NAME, PROVIDER_LIST, WORK_DIR } from '~/utils/constants';
DEFAULT_MODEL,
DEFAULT_PROVIDER,
getModelList,
MODEL_REGEX,
MODIFICATIONS_TAG_NAME,
PROVIDER_REGEX,
WORK_DIR,
} from '~/utils/constants';
import ignore from 'ignore';
import type { IProviderSetting } from '~/types/model'; import type { IProviderSetting } from '~/types/model';
import { PromptLibrary } from '~/lib/common/prompt-library'; import { PromptLibrary } from '~/lib/common/prompt-library';
import { allowedHTMLElements } from '~/utils/markdown'; import { allowedHTMLElements } from '~/utils/markdown';
import { LLMManager } from '~/lib/modules/llm/manager';
interface ToolResult<Name extends string, Args, Result> { import { createScopedLogger } from '~/utils/logger';
toolCallId: string; import { createFilesContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
toolName: Name; import { getFilePaths } from './select-context';
args: Args;
result: Result;
}
interface Message {
role: 'user' | 'assistant';
content: string;
toolInvocations?: ToolResult<string, unknown, unknown>[];
model?: string;
}
export type Messages = Message[]; export type Messages = Message[];
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>; export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
export interface File { const logger = createScopedLogger('stream-text');
type: 'file';
content: string;
isBinary: boolean;
}
export interface Folder {
type: 'folder';
}
type Dirent = File | Folder;
export type FileMap = Record<string, Dirent | undefined>;
export function simplifyBoltActions(input: string): string {
// Using regex to match boltAction tags that have type="file"
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
// Replace each matching occurrence
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
return `${openingTag}\n ...\n ${closingTag}`;
});
}
// Common patterns to ignore, similar to .gitignore
const IGNORE_PATTERNS = [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'.cache/**',
'.vscode/**',
'.idea/**',
'**/*.log',
'**/.DS_Store',
'**/npm-debug.log*',
'**/yarn-debug.log*',
'**/yarn-error.log*',
'**/*lock.json',
'**/*lock.yml',
];
const ig = ignore().add(IGNORE_PATTERNS);
function createFilesContext(files: FileMap) {
let filePaths = Object.keys(files);
filePaths = filePaths.filter((x) => {
const relPath = x.replace('/home/project/', '');
return !ig.ignores(relPath);
});
const fileContexts = filePaths
.filter((x) => files[x] && files[x].type == 'file')
.map((path) => {
const dirent = files[path];
if (!dirent || dirent.type == 'folder') {
return '';
}
const codeWithLinesNumbers = dirent.content
.split('\n')
.map((v, i) => `${i + 1}|${v}`)
.join('\n');
return `<file path="${path}">\n${codeWithLinesNumbers}\n</file>`;
});
return `Below are the code files present in the webcontainer:\ncode format:\n<line number>|<line content>\n <codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
}
function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
const textContent = Array.isArray(message.content)
? message.content.find((item) => item.type === 'text')?.text || ''
: message.content;
const modelMatch = textContent.match(MODEL_REGEX);
const providerMatch = textContent.match(PROVIDER_REGEX);
/*
* Extract model
* const modelMatch = message.content.match(MODEL_REGEX);
*/
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
/*
* Extract provider
* const providerMatch = message.content.match(PROVIDER_REGEX);
*/
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
const cleanedContent = Array.isArray(message.content)
? message.content.map((item) => {
if (item.type === 'text') {
return {
type: 'text',
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
};
}
return item; // Preserve image_url and other types as is
})
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
return { model, provider, content: cleanedContent };
}
export async function streamText(props: { export async function streamText(props: {
messages: Messages; messages: Omit<Message, 'id'>[];
env: Env; env?: Env;
options?: StreamingOptions; options?: StreamingOptions;
apiKeys?: Record<string, string>; apiKeys?: Record<string, string>;
files?: FileMap; files?: FileMap;
providerSettings?: Record<string, IProviderSetting>; providerSettings?: Record<string, IProviderSetting>;
promptId?: string; promptId?: string;
contextOptimization?: boolean;
contextFiles?: FileMap;
summary?: string;
}) { }) {
const { messages, env, options, apiKeys, files, providerSettings, promptId } = props; const {
messages,
env: serverEnv,
options,
apiKeys,
files,
providerSettings,
promptId,
contextOptimization,
contextFiles,
summary,
} = props;
let currentModel = DEFAULT_MODEL; let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name; let currentProvider = DEFAULT_PROVIDER.name;
const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings); let processedMessages = messages.map((message) => {
const processedMessages = messages.map((message) => {
if (message.role === 'user') { if (message.role === 'user') {
const { model, provider, content } = extractPropertiesFromMessage(message); const { model, provider, content } = extractPropertiesFromMessage(message);
if (MODEL_LIST.find((m) => m.name === model)) {
currentModel = model; currentModel = model;
}
currentProvider = provider; currentProvider = provider;
return { ...message, content }; return { ...message, content };
} else if (message.role == 'assistant') { } else if (message.role == 'assistant') {
const content = message.content; let content = message.content;
// content = simplifyBoltActions(content); if (contextOptimization) {
content = simplifyBoltActions(content);
}
return { ...message, content }; return { ...message, content };
} }
@@ -177,7 +62,34 @@ export async function streamText(props: {
return message; return message;
}); });
const modelDetails = MODEL_LIST.find((m) => m.name === currentModel); const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER;
const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider);
let modelDetails = staticModels.find((m) => m.name === currentModel);
if (!modelDetails) {
const modelsList = [
...(provider.staticModels || []),
...(await LLMManager.getInstance().getModelListFromProvider(provider, {
apiKeys,
providerSettings,
serverEnv: serverEnv as any,
})),
];
if (!modelsList.length) {
throw new Error(`No models found for provider ${provider.name}`);
}
modelDetails = modelsList.find((m) => m.name === currentModel);
if (!modelDetails) {
// Fallback to first model
logger.warn(
`MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`,
);
modelDetails = modelsList[0];
}
}
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS; const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
@@ -187,16 +99,52 @@ export async function streamText(props: {
allowedHtmlElements: allowedHTMLElements, allowedHtmlElements: allowedHTMLElements,
modificationTagName: MODIFICATIONS_TAG_NAME, modificationTagName: MODIFICATIONS_TAG_NAME,
}) ?? getSystemPrompt(); }) ?? getSystemPrompt();
let codeContext = '';
if (files) { if (files && contextFiles && contextOptimization) {
codeContext = createFilesContext(files); const codeContext = createFilesContext(contextFiles, true);
codeContext = ''; const filePaths = getFilePaths(files);
systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
systemPrompt = `${systemPrompt}
Below are all the files present in the project:
---
${filePaths.join('\n')}
---
Below is the context loaded into context buffer for you to have knowledge of and might need changes to fullfill current user request.
CONTEXT BUFFER:
---
${codeContext}
---
`;
if (summary) {
systemPrompt = `${systemPrompt}
below is the chat history till now
CHAT SUMMARY:
---
${props.summary}
---
`;
const lastMessage = processedMessages.pop();
if (lastMessage) {
processedMessages = [lastMessage];
}
}
} }
return _streamText({ logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
// console.log(systemPrompt,processedMessages);
return await _streamText({
model: provider.getModelInstance({
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,
}),
system: systemPrompt, system: systemPrompt,
maxTokens: dynamicMaxTokens, maxTokens: dynamicMaxTokens,
messages: convertToCoreMessages(processedMessages as any), messages: convertToCoreMessages(processedMessages as any),

View File

@@ -0,0 +1,128 @@
import { type Message } from 'ai';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
import { IGNORE_PATTERNS, type FileMap } from './constants';
import ignore from 'ignore';
import type { ContextAnnotation } from '~/types/context';
export function extractPropertiesFromMessage(message: Omit<Message, 'id'>): {
model: string;
provider: string;
content: string;
} {
const textContent = Array.isArray(message.content)
? message.content.find((item) => item.type === 'text')?.text || ''
: message.content;
const modelMatch = textContent.match(MODEL_REGEX);
const providerMatch = textContent.match(PROVIDER_REGEX);
/*
* Extract model
* const modelMatch = message.content.match(MODEL_REGEX);
*/
const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
/*
* Extract provider
* const providerMatch = message.content.match(PROVIDER_REGEX);
*/
const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name;
const cleanedContent = Array.isArray(message.content)
? message.content.map((item) => {
if (item.type === 'text') {
return {
type: 'text',
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
};
}
return item; // Preserve image_url and other types as is
})
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
return { model, provider, content: cleanedContent };
}
export function simplifyBoltActions(input: string): string {
// Using regex to match boltAction tags that have type="file"
const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
// Replace each matching occurrence
return input.replace(regex, (_0, openingTag, _2, closingTag) => {
return `${openingTag}\n ...\n ${closingTag}`;
});
}
export function createFilesContext(files: FileMap, useRelativePath?: boolean) {
const ig = ignore().add(IGNORE_PATTERNS);
let filePaths = Object.keys(files);
filePaths = filePaths.filter((x) => {
const relPath = x.replace('/home/project/', '');
return !ig.ignores(relPath);
});
const fileContexts = filePaths
.filter((x) => files[x] && files[x].type == 'file')
.map((path) => {
const dirent = files[path];
if (!dirent || dirent.type == 'folder') {
return '';
}
const codeWithLinesNumbers = dirent.content
.split('\n')
// .map((v, i) => `${i + 1}|${v}`)
.join('\n');
let filePath = path;
if (useRelativePath) {
filePath = path.replace('/home/project/', '');
}
return `<file path="${filePath}">\n${codeWithLinesNumbers}\n</file>`;
});
return `<codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
}
export function extractCurrentContext(messages: Message[]) {
const lastAssistantMessage = messages.filter((x) => x.role == 'assistant').slice(-1)[0];
if (!lastAssistantMessage) {
return { summary: undefined, codeContext: undefined };
}
let summary: ContextAnnotation | undefined;
let codeContext: ContextAnnotation | undefined;
if (!lastAssistantMessage.annotations?.length) {
return { summary: undefined, codeContext: undefined };
}
for (let i = 0; i < lastAssistantMessage.annotations.length; i++) {
const annotation = lastAssistantMessage.annotations[i];
if (!annotation || typeof annotation !== 'object') {
continue;
}
if (!(annotation as any).type) {
continue;
}
const annotationObject = annotation as any;
if (annotationObject.type === 'codeContext') {
codeContext = annotationObject;
break;
} else if (annotationObject.type === 'chatSummary') {
summary = annotationObject;
break;
}
}
return { summary, codeContext };
}

33
app/lib/api/cookies.ts Normal file
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

@@ -99,16 +99,12 @@ Examples:
Certainly, I can help you create a JavaScript function to calculate the factorial of a number. Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
<boltArtifact id="factorial-function" title="JavaScript Factorial Function"> <boltArtifact id="factorial-function" title="JavaScript Factorial Function">
<boltAction type="file" filePath="index.js"> <boltAction type="file" filePath="index.js">function factorial(n) {
function factorial(n) {
... ...
} }
... ...</boltAction>
</boltAction> <boltAction type="shell">node index.js</boltAction>
<boltAction type="shell">
node index.js
</boltAction>
</boltArtifact> </boltArtifact>
</assistant_response> </assistant_response>
</example> </example>
@@ -119,24 +115,16 @@ node index.js
Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step. Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
<boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript"> <boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
<boltAction type="file" filePath="package.json"> <boltAction type="file" filePath="package.json">{
{
"name": "snake", "name": "snake",
"scripts": { "scripts": {
"dev": "vite" "dev": "vite"
} }
... ...
} }</boltAction>
</boltAction> <boltAction type="shell">npm install --save-dev vite</boltAction>
<boltAction type="shell"> <boltAction type="file" filePath="index.html">...</boltAction>
npm install --save-dev vite <boltAction type="start">npm run dev</boltAction>
</boltAction>
<boltAction type="file" filePath="index.html">
...
</boltAction>
<boltAction type="start">
npm run dev
</boltAction>
</boltArtifact> </boltArtifact>
Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail. Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
@@ -149,8 +137,7 @@ npm run dev
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations. Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
<boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React"> <boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
<boltAction type="file" filePath="package.json"> <boltAction type="file" filePath="package.json">{
{
"name": "bouncing-ball", "name": "bouncing-ball",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
@@ -171,23 +158,12 @@ npm run dev
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"vite": "^4.2.0" "vite": "^4.2.0"
} }
} }</boltAction>
</boltAction> <boltAction type="file" filePath="index.html">...</boltAction>
<boltAction type="file" filePath="index.html"> <boltAction type="file" filePath="src/main.jsx">...</boltAction>
... <boltAction type="file" filePath="src/index.css">...</boltAction>
</boltAction> <boltAction type="file" filePath="src/App.jsx">...</boltAction>
<boltAction type="file" filePath="src/main.jsx"> <boltAction type="start">npm run dev</boltAction>
...
</boltAction>
<boltAction type="file" filePath="src/index.css">
...
</boltAction>
<boltAction type="file" filePath="src/App.jsx">
...
</boltAction>
<boltAction type="start">
npm run dev
</boltAction>
</boltArtifact> </boltArtifact>
You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom. You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.

View File

@@ -231,17 +231,12 @@ Here are some examples of correct usage of artifacts:
Certainly, I can help you create a JavaScript function to calculate the factorial of a number. Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
<boltArtifact id="factorial-function" title="JavaScript Factorial Function"> <boltArtifact id="factorial-function" title="JavaScript Factorial Function">
<boltAction type="file" filePath="index.js"> <boltAction type="file" filePath="index.js">function factorial(n) {
function factorial(n) {
... ...
} }
...</boltAction>
... <boltAction type="shell">node index.js</boltAction>
</boltAction>
<boltAction type="shell">
node index.js
</boltAction>
</boltArtifact> </boltArtifact>
</assistant_response> </assistant_response>
</example> </example>
@@ -253,27 +248,19 @@ Here are some examples of correct usage of artifacts:
Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step. Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
<boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript"> <boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
<boltAction type="file" filePath="package.json"> <boltAction type="file" filePath="package.json">{
{
"name": "snake", "name": "snake",
"scripts": { "scripts": {
"dev": "vite" "dev": "vite"
} }
... ...
} }</boltAction>
</boltAction>
<boltAction type="shell"> <boltAction type="shell">npm install --save-dev vite</boltAction>
npm install --save-dev vite
</boltAction>
<boltAction type="file" filePath="index.html"> <boltAction type="file" filePath="index.html">...</boltAction>
...
</boltAction>
<boltAction type="start"> <boltAction type="start">npm run dev</boltAction>
npm run dev
</boltAction>
</boltArtifact> </boltArtifact>
Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail. Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
@@ -287,8 +274,7 @@ Here are some examples of correct usage of artifacts:
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations. Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
<boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React"> <boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
<boltAction type="file" filePath="package.json"> <boltAction type="file" filePath="package.json">{
{
"name": "bouncing-ball", "name": "bouncing-ball",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
@@ -309,28 +295,17 @@ Here are some examples of correct usage of artifacts:
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"vite": "^4.2.0" "vite": "^4.2.0"
} }
} }</boltAction>
</boltAction>
<boltAction type="file" filePath="index.html"> <boltAction type="file" filePath="index.html">...</boltAction>
...
</boltAction>
<boltAction type="file" filePath="src/main.jsx"> <boltAction type="file" filePath="src/main.jsx">...</boltAction>
...
</boltAction>
<boltAction type="file" filePath="src/index.css"> <boltAction type="file" filePath="src/index.css">...</boltAction>
...
</boltAction>
<boltAction type="file" filePath="src/App.jsx"> <boltAction type="file" filePath="src/App.jsx">...</boltAction>
...
</boltAction>
<boltAction type="start"> <boltAction type="start">npm run dev</boltAction>
npm run dev
</boltAction>
</boltArtifact> </boltArtifact>
You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom. You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.

View File

@@ -3,10 +3,10 @@ import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { import {
chatId as chatIdStore, chatId as chatIdStore,
description as descriptionStore,
db, db,
updateChatDescription, description as descriptionStore,
getMessages, getMessages,
updateChatDescription,
} from '~/lib/persistence'; } from '~/lib/persistence';
interface EditChatDescriptionOptions { interface EditChatDescriptionOptions {
@@ -92,7 +92,9 @@ export function useEditChatDescription({
} }
const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100; const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100;
const characterValid = /^[a-zA-Z0-9\s]+$/.test(trimmedDesc);
// Allow letters, numbers, spaces, and common punctuation but exclude characters that could cause issues
const characterValid = /^[a-zA-Z0-9\s\-_.,!?()[\]{}'"]+$/.test(trimmedDesc);
if (!lengthValid) { if (!lengthValid) {
toast.error('Description must be between 1 and 100 characters.'); toast.error('Description must be between 1 and 100 characters.');
@@ -100,7 +102,7 @@ export function useEditChatDescription({
} }
if (!characterValid) { if (!characterValid) {
toast.error('Description can only contain alphanumeric characters and spaces.'); toast.error('Description can only contain letters, numbers, spaces, and basic punctuation.');
return false; return false;
} }

View File

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

View File

@@ -7,19 +7,23 @@ import {
promptStore, promptStore,
providersStore, providersStore,
latestBranchStore, latestBranchStore,
autoSelectStarterTemplate,
enableContextOptimizationStore,
} from '~/lib/stores/settings'; } from '~/lib/stores/settings';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import type { IProviderSetting, ProviderInfo } from '~/types/model'; import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
import commit from '~/commit.json';
interface CommitData { interface CommitData {
commit: string; commit: string;
version?: string; version?: string;
} }
const commitJson: CommitData = commit; const versionData: CommitData = {
commit: __COMMIT_HASH,
version: __APP_VERSION,
};
export function useSettings() { export function useSettings() {
const providers = useStore(providersStore); const providers = useStore(providersStore);
@@ -28,23 +32,19 @@ export function useSettings() {
const promptId = useStore(promptStore); const promptId = useStore(promptStore);
const isLocalModel = useStore(isLocalModelsEnabled); const isLocalModel = useStore(isLocalModelsEnabled);
const isLatestBranch = useStore(latestBranchStore); const isLatestBranch = useStore(latestBranchStore);
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]); const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
// Function to check if we're on stable version // Function to check if we're on stable version
const checkIsStableVersion = async () => { const checkIsStableVersion = async () => {
try { try {
const stableResponse = await fetch( const response = await fetch(
`https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/refs/tags/v${commitJson.version}/app/commit.json`, `https://api.github.com/repos/stackblitz-labs/bolt.diy/git/refs/tags/v${versionData.version}`,
); );
const data: { object: { sha: string } } = await response.json();
if (!stableResponse.ok) { return versionData.commit.slice(0, 7) === data.object.sha.slice(0, 7);
console.warn('Failed to fetch stable commit info');
return false;
}
const stableData = (await stableResponse.json()) as CommitData;
return commit.commit === stableData.commit;
} catch (error) { } catch (error) {
console.warn('Error checking stable version:', error); console.warn('Error checking stable version:', error);
return false; return false;
@@ -58,15 +58,18 @@ export function useSettings() {
if (savedProviders) { if (savedProviders) {
try { try {
const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders); const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
Object.keys(parsedProviders).forEach((provider) => { Object.keys(providers).forEach((provider) => {
const currentProvider = providers[provider]; const currentProviderSettings = parsedProviders[provider];
if (currentProviderSettings) {
providersStore.setKey(provider, { providersStore.setKey(provider, {
...currentProvider, ...providers[provider],
settings: { settings: {
...parsedProviders[provider], ...currentProviderSettings,
enabled: parsedProviders[provider].enabled ?? true, enabled: currentProviderSettings.enabled ?? true,
}, },
}); });
}
}); });
} catch (error) { } catch (error) {
console.error('Failed to parse providers from cookies:', error); console.error('Failed to parse providers from cookies:', error);
@@ -105,20 +108,32 @@ export function useSettings() {
let checkCommit = Cookies.get('commitHash'); let checkCommit = Cookies.get('commitHash');
if (checkCommit === undefined) { if (checkCommit === undefined) {
checkCommit = commit.commit; checkCommit = versionData.commit;
} }
if (savedLatestBranch === undefined || checkCommit !== commit.commit) { if (savedLatestBranch === undefined || checkCommit !== versionData.commit) {
// If setting hasn't been set by user, check version // If setting hasn't been set by user, check version
checkIsStableVersion().then((isStable) => { checkIsStableVersion().then((isStable) => {
const shouldUseLatest = !isStable; const shouldUseLatest = !isStable;
latestBranchStore.set(shouldUseLatest); latestBranchStore.set(shouldUseLatest);
Cookies.set('isLatestBranch', String(shouldUseLatest)); Cookies.set('isLatestBranch', String(shouldUseLatest));
Cookies.set('commitHash', String(commit.commit)); Cookies.set('commitHash', String(versionData.commit));
}); });
} else { } else {
latestBranchStore.set(savedLatestBranch === 'true'); latestBranchStore.set(savedLatestBranch === 'true');
} }
const autoSelectTemplate = Cookies.get('autoSelectTemplate');
if (autoSelectTemplate) {
autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
}
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
if (savedContextOptimizationEnabled) {
enableContextOptimizationStore.set(savedContextOptimizationEnabled === 'true');
}
}, []); }, []);
// writing values to cookies on change // writing values to cookies on change
@@ -180,6 +195,18 @@ export function useSettings() {
Cookies.set('isLatestBranch', String(enabled)); Cookies.set('isLatestBranch', String(enabled));
}, []); }, []);
const setAutoSelectTemplate = useCallback((enabled: boolean) => {
autoSelectStarterTemplate.set(enabled);
logStore.logSystem(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('autoSelectTemplate', String(enabled));
}, []);
const enableContextOptimization = useCallback((enabled: boolean) => {
enableContextOptimizationStore.set(enabled);
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('contextOptimizationEnabled', String(enabled));
}, []);
return { return {
providers, providers,
activeProviders, activeProviders,
@@ -194,5 +221,9 @@ export function useSettings() {
setPromptId, setPromptId,
isLatestBranch, isLatestBranch,
enableLatestBranch, enableLatestBranch,
autoSelectTemplate,
setAutoSelectTemplate,
contextOptimizationEnabled,
enableContextOptimization,
}; };
} }

View File

@@ -0,0 +1,129 @@
import type { LanguageModelV1 } from 'ai';
import type { ProviderInfo, ProviderConfig, ModelInfo } from './types';
import type { IProviderSetting } from '~/types/model';
import { createOpenAI } from '@ai-sdk/openai';
import { LLMManager } from './manager';
export abstract class BaseProvider implements ProviderInfo {
abstract name: string;
abstract staticModels: ModelInfo[];
abstract config: ProviderConfig;
cachedDynamicModels?: {
cacheId: string;
models: ModelInfo[];
};
getApiKeyLink?: string;
labelForGetApiKey?: string;
icon?: string;
getProviderBaseUrlAndKey(options: {
apiKeys?: Record<string, string>;
providerSettings?: IProviderSetting;
serverEnv?: Record<string, string>;
defaultBaseUrlKey: string;
defaultApiTokenKey: string;
}) {
const { apiKeys, providerSettings, serverEnv, defaultBaseUrlKey, defaultApiTokenKey } = options;
let settingsBaseUrl = providerSettings?.baseUrl;
const manager = LLMManager.getInstance();
if (settingsBaseUrl && settingsBaseUrl.length == 0) {
settingsBaseUrl = undefined;
}
const baseUrlKey = this.config.baseUrlKey || defaultBaseUrlKey;
let baseUrl =
settingsBaseUrl ||
serverEnv?.[baseUrlKey] ||
process?.env?.[baseUrlKey] ||
manager.env?.[baseUrlKey] ||
this.config.baseUrl;
if (baseUrl && baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
const apiKey =
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[apiTokenKey];
return {
baseUrl,
apiKey,
};
}
getModelsFromCache(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}): ModelInfo[] | null {
if (!this.cachedDynamicModels) {
// console.log('no dynamic models',this.name);
return null;
}
const cacheKey = this.cachedDynamicModels.cacheId;
const generatedCacheKey = this.getDynamicModelsCacheKey(options);
if (cacheKey !== generatedCacheKey) {
// console.log('cache key mismatch',this.name,cacheKey,generatedCacheKey);
this.cachedDynamicModels = undefined;
return null;
}
return this.cachedDynamicModels.models;
}
getDynamicModelsCacheKey(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}) {
return JSON.stringify({
apiKeys: options.apiKeys?.[this.name],
providerSettings: options.providerSettings?.[this.name],
serverEnv: options.serverEnv,
});
}
storeDynamicModels(
options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
},
models: ModelInfo[],
) {
const cacheId = this.getDynamicModelsCacheKey(options);
// console.log('caching dynamic models',this.name,cacheId);
this.cachedDynamicModels = {
cacheId,
models,
};
}
// Declare the optional getDynamicModels method
getDynamicModels?(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv?: Record<string, string>,
): Promise<ModelInfo[]>;
abstract getModelInstance(options: {
model: string;
serverEnv?: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1;
}
type OptionalApiKey = string | undefined;
export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL,
apiKey,
});
return openai(model);
}

View File

@@ -0,0 +1,203 @@
import type { IProviderSetting } from '~/types/model';
import { BaseProvider } from './base-provider';
import type { ModelInfo, ProviderInfo } from './types';
import * as providers from './registry';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('LLMManager');
export class LLMManager {
private static _instance: LLMManager;
private _providers: Map<string, BaseProvider> = new Map();
private _modelList: ModelInfo[] = [];
private readonly _env: any = {};
private constructor(_env: Record<string, string>) {
this._registerProvidersFromDirectory();
this._env = _env;
}
static getInstance(env: Record<string, string> = {}): LLMManager {
if (!LLMManager._instance) {
LLMManager._instance = new LLMManager(env);
}
return LLMManager._instance;
}
get env() {
return this._env;
}
private async _registerProvidersFromDirectory() {
try {
/*
* Dynamically import all files from the providers directory
* const providerModules = import.meta.glob('./providers/*.ts', { eager: true });
*/
// Look for exported classes that extend BaseProvider
for (const exportedItem of Object.values(providers)) {
if (typeof exportedItem === 'function' && exportedItem.prototype instanceof BaseProvider) {
const provider = new exportedItem();
try {
this.registerProvider(provider);
} catch (error: any) {
logger.warn('Failed To Register Provider: ', provider.name, 'error:', error.message);
}
}
}
} catch (error) {
logger.error('Error registering providers:', error);
}
}
registerProvider(provider: BaseProvider) {
if (this._providers.has(provider.name)) {
logger.warn(`Provider ${provider.name} is already registered. Skipping.`);
return;
}
logger.info('Registering Provider: ', provider.name);
this._providers.set(provider.name, provider);
this._modelList = [...this._modelList, ...provider.staticModels];
}
getProvider(name: string): BaseProvider | undefined {
return this._providers.get(name);
}
getAllProviders(): BaseProvider[] {
return Array.from(this._providers.values());
}
getModelList(): ModelInfo[] {
return this._modelList;
}
async updateModelList(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}): Promise<ModelInfo[]> {
const { apiKeys, providerSettings, serverEnv } = options;
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
if (providerSettings && Object.keys(providerSettings).length > 0) {
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
}
// Get dynamic models from all providers that support them
const dynamicModels = await Promise.all(
Array.from(this._providers.values())
.filter((provider) => enabledProviders.includes(provider.name))
.filter(
(provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
!!provider.getDynamicModels,
)
.map(async (provider) => {
const cachedModels = provider.getModelsFromCache(options);
if (cachedModels) {
return cachedModels;
}
const dynamicModels = await provider
.getDynamicModels(apiKeys, providerSettings?.[provider.name], serverEnv)
.then((models) => {
logger.info(`Caching ${models.length} dynamic models for ${provider.name}`);
provider.storeDynamicModels(options, models);
return models;
})
.catch((err) => {
logger.error(`Error getting dynamic models ${provider.name} :`, err);
return [];
});
return dynamicModels;
}),
);
// Combine static and dynamic models
const modelList = [
...dynamicModels.flat(),
...Array.from(this._providers.values()).flatMap((p) => p.staticModels || []),
];
this._modelList = modelList;
return modelList;
}
getStaticModelList() {
return [...this._providers.values()].flatMap((p) => p.staticModels || []);
}
async getModelListFromProvider(
providerArg: BaseProvider,
options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
},
): Promise<ModelInfo[]> {
const provider = this._providers.get(providerArg.name);
if (!provider) {
throw new Error(`Provider ${providerArg.name} not found`);
}
const staticModels = provider.staticModels || [];
if (!provider.getDynamicModels) {
return staticModels;
}
const { apiKeys, providerSettings, serverEnv } = options;
const cachedModels = provider.getModelsFromCache({
apiKeys,
providerSettings,
serverEnv,
});
if (cachedModels) {
logger.info(`Found ${cachedModels.length} cached models for ${provider.name}`);
return [...cachedModels, ...staticModels];
}
logger.info(`Getting dynamic models for ${provider.name}`);
const dynamicModels = await provider
.getDynamicModels?.(apiKeys, providerSettings?.[provider.name], serverEnv)
.then((models) => {
logger.info(`Got ${models.length} dynamic models for ${provider.name}`);
provider.storeDynamicModels(options, models);
return models;
})
.catch((err) => {
logger.error(`Error getting dynamic models ${provider.name} :`, err);
return [];
});
return [...dynamicModels, ...staticModels];
}
getStaticModelListFromProvider(providerArg: BaseProvider) {
const provider = this._providers.get(providerArg.name);
if (!provider) {
throw new Error(`Provider ${providerArg.name} not found`);
}
return [...(provider.staticModels || [])];
}
getDefaultProvider(): BaseProvider {
const firstProvider = this._providers.values().next().value;
if (!firstProvider) {
throw new Error('No providers registered');
}
return firstProvider;
}
}

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,58 @@
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 { createAnthropic } from '@ai-sdk/anthropic';
export default class AnthropicProvider extends BaseProvider {
name = 'Anthropic';
getApiKeyLink = 'https://console.anthropic.com/settings/keys';
config = {
apiTokenKey: 'ANTHROPIC_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'claude-3-5-sonnet-latest',
label: 'Claude 3.5 Sonnet (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-sonnet-20240620',
label: 'Claude 3.5 Sonnet (old)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-haiku-latest',
label: 'Claude 3.5 Haiku (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{ name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 },
];
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings,
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'ANTHROPIC_API_KEY',
});
const anthropic = createAnthropic({
apiKey,
});
return anthropic(model);
};
}

View File

@@ -0,0 +1,54 @@
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 { createCohere } from '@ai-sdk/cohere';
export default class CohereProvider extends BaseProvider {
name = 'Cohere';
getApiKeyLink = 'https://dashboard.cohere.com/api-keys';
config = {
apiTokenKey: 'COHERE_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'command-r-plus-08-2024', label: 'Command R plus Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-08-2024', label: 'Command R Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-plus', label: 'Command R plus', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r', label: 'Command R', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command', label: 'Command', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-nightly', label: 'Command Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light', label: 'Command Light', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light-nightly', label: 'Command Light Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 },
];
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: 'COHERE_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const cohere = createCohere({
apiKey,
});
return cohere(model);
}
}

View File

@@ -0,0 +1,48 @@
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 DeepseekProvider extends BaseProvider {
name = 'Deepseek';
getApiKeyLink = 'https://platform.deepseek.com/apiKeys';
config = {
apiTokenKey: 'DEEPSEEK_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
{ name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 },
{ name: 'deepseek-reasoner', label: 'Deepseek-Reasoner', provider: 'Deepseek', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
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: 'DEEPSEEK_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.deepseek.com/beta',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,53 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class GithubProvider extends BaseProvider {
name = 'Github';
getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
config = {
apiTokenKey: 'GITHUB_API_KEY',
};
// find more in https://github.com/marketplace?type=models
staticModels: ModelInfo[] = [
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
{ name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'GITHUB_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://models.inference.ai.azure.com',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,51 @@
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 { createGoogleGenerativeAI } from '@ai-sdk/google';
export default class GoogleProvider extends BaseProvider {
name = 'Google';
getApiKeyLink = 'https://aistudio.google.com/app/apikey';
config = {
apiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-exp-1206', label: 'Gemini exp-1206', provider: 'Google', maxTokenAllowed: 8192 },
];
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: 'GOOGLE_GENERATIVE_AI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const google = createGoogleGenerativeAI({
apiKey,
});
return google(model);
}
}

View File

@@ -0,0 +1,51 @@
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 GroqProvider extends BaseProvider {
name = 'Groq';
getApiKeyLink = 'https://console.groq.com/keys';
config = {
apiTokenKey: 'GROQ_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', 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: 'GROQ_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.groq.com/openai/v1',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,111 @@
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 HuggingFaceProvider extends BaseProvider {
name = 'HuggingFace';
getApiKeyLink = 'https://huggingface.co/settings/tokens';
config = {
apiTokenKey: 'HuggingFace_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-72B-Instruct',
label: 'Qwen2.5-72B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-70B-Instruct',
label: 'Llama-3.1-70B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-405B',
label: 'Llama-3.1-405B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
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: 'HuggingFace_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api-inference.huggingface.co/v1/',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,111 @@
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 HyperbolicProvider extends BaseProvider {
name = 'Hyperbolic';
getApiKeyLink = 'https://app.hyperbolic.xyz/settings';
config = {
apiTokenKey: 'HYPERBOLIC_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen 2.5 Coder 32B Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/Qwen2.5-72B-Instruct',
label: 'Qwen2.5-72B-Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'deepseek-ai/DeepSeek-V2.5',
label: 'DeepSeek-V2.5',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/QwQ-32B-Preview',
label: 'QwQ-32B-Preview',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/Qwen2-VL-72B-Instruct',
label: 'Qwen2-VL-72B-Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl: fetchBaseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'HYPERBOLIC_API_KEY',
});
const baseUrl = fetchBaseUrl || 'https://api.hyperbolic.xyz/v1';
if (!apiKey) {
throw `Missing Api Key configuration for ${this.name} provider`;
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
const data = res.data.filter((model: any) => model.object === 'model' && model.supports_chat);
return data.map((m: any) => ({
name: m.id,
label: `${m.id} - context ${m.context_length ? Math.floor(m.context_length / 1000) + 'k' : 'N/A'}`,
provider: this.name,
maxTokenAllowed: m.context_length || 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: 'HYPERBOLIC_API_KEY',
});
if (!apiKey) {
throw `Missing Api Key configuration for ${this.name} provider`;
}
const openai = createOpenAI({
baseURL: 'https://api.hyperbolic.xyz/v1/',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,93 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
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';
getApiKeyLink = 'https://lmstudio.ai/';
labelForGetApiKey = 'Get LMStudio';
icon = 'i-ph:cloud-arrow-down';
config = {
baseUrlKey: 'LMSTUDIO_API_BASE_URL',
baseUrl: 'http://localhost:1234/',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for LMStudio provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/v1/models`);
const data = (await response.json()) as { data: Array<{ id: string }> };
return data.data.map((model) => ({
name: model.id,
label: model.id,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
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: '',
});
return lmstudio(model);
};
}

View File

@@ -0,0 +1,53 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createMistral } from '@ai-sdk/mistral';
export default class MistralProvider extends BaseProvider {
name = 'Mistral';
getApiKeyLink = 'https://console.mistral.ai/api-keys/';
config = {
apiTokenKey: 'MISTRAL_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', 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: 'MISTRAL_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const mistral = createMistral({
apiKey,
});
return mistral(model);
}
}

View File

@@ -0,0 +1,118 @@
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 { ollama } from 'ollama-ai-provider';
import { logger } from '~/utils/logger';
interface OllamaModelDetails {
parent_model: string;
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
}
export interface OllamaModel {
name: string;
model: string;
modified_at: string;
size: number;
digest: string;
details: OllamaModelDetails;
}
export interface OllamaApiResponse {
models: OllamaModel[];
}
export const DEFAULT_NUM_CTX = process?.env?.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
export default class OllamaProvider extends BaseProvider {
name = 'Ollama';
getApiKeyLink = 'https://ollama.com/download';
labelForGetApiKey = 'Download Ollama';
icon = 'i-ph:cloud-arrow-down';
config = {
baseUrlKey: 'OLLAMA_API_BASE_URL',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for OLLAMA provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/api/tags`);
const data = (await response.json()) as OllamaApiResponse;
// console.log({ ollamamodels: data.models });
return data.models.map((model: OllamaModel) => ({
name: model.name,
label: `${model.name} (${model.details.parameter_size})`,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
// Backend: Check if we're running in Docker
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 };
ollamaInstance.config.baseURL = `${baseUrl}/api`;
return ollamaInstance;
};
}

View File

@@ -0,0 +1,131 @@
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 { createOpenRouter } from '@openrouter/ai-sdk-provider';
interface OpenRouterModel {
name: string;
id: string;
context_length: number;
pricing: {
prompt: number;
completion: number;
};
}
interface OpenRouterModelsResponse {
data: OpenRouterModel[];
}
export default class OpenRouterProvider extends BaseProvider {
name = 'OpenRouter';
getApiKeyLink = 'https://openrouter.ai/settings/keys';
config = {
apiTokenKey: 'OPEN_ROUTER_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'anthropic/claude-3.5-sonnet',
label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'anthropic/claude-3-haiku',
label: 'Anthropic: Claude 3 Haiku (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'deepseek/deepseek-coder',
label: 'Deepseek-Coder V2 236B (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-flash-1.5',
label: 'Google Gemini Flash 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-pro-1.5',
label: 'Google Gemini Pro 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
{
name: 'mistralai/mistral-nemo',
label: 'OpenRouter Mistral Nemo (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'qwen/qwen-110b-chat',
label: 'OpenRouter Qwen 110b Chat (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 4096 },
];
async getDynamicModels(
_apiKeys?: Record<string, string>,
_settings?: IProviderSetting,
_serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
try {
const response = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Content-Type': 'application/json',
},
});
const data = (await response.json()) as OpenRouterModelsResponse;
return data.data
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => ({
name: m.id,
label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(2)} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider: this.name,
maxTokenAllowed: 8000,
}));
} catch (error) {
console.error('Error getting OpenRouter models:', error);
return [];
}
}
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: 'OPEN_ROUTER_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openRouter = createOpenRouter({
apiKey,
});
const instance = openRouter.chat(model) as LanguageModelV1;
return instance;
}
}

View File

@@ -0,0 +1,72 @@
import { BaseProvider, getOpenAILikeModel } 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';
export default class OpenAILikeProvider extends BaseProvider {
name = 'OpenAILike';
getApiKeyLink = undefined;
config = {
baseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
apiTokenKey: 'OPENAI_LIKE_API_KEY',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
return [];
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
return res.data.map((model: any) => ({
name: model.id,
label: model.id,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
throw new Error(`Missing configuration for ${this.name} provider`);
}
return getOpenAILikeModel(baseUrl, apiKey, model);
}
}

View File

@@ -0,0 +1,49 @@
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 OpenAIProvider extends BaseProvider {
name = 'OpenAI';
getApiKeyLink = 'https://platform.openai.com/api-keys';
config = {
apiTokenKey: 'OPENAI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', 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: 'OPENAI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,63 @@
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 PerplexityProvider extends BaseProvider {
name = 'Perplexity';
getApiKeyLink = 'https://www.perplexity.ai/settings/api';
config = {
apiTokenKey: 'PERPLEXITY_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'llama-3.1-sonar-small-128k-online',
label: 'Sonar Small Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-large-128k-online',
label: 'Sonar Large Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-huge-128k-online',
label: 'Sonar Huge Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
];
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: 'PERPLEXITY_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const perplexity = createOpenAI({
baseURL: 'https://api.perplexity.ai/',
apiKey,
});
return perplexity(model);
}
}

View File

@@ -0,0 +1,95 @@
import { BaseProvider, getOpenAILikeModel } 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';
export default class TogetherProvider extends BaseProvider {
name = 'Together';
getApiKeyLink = 'https://api.together.xyz/settings/api-keys';
config = {
baseUrlKey: 'TOGETHER_API_BASE_URL',
apiTokenKey: 'TOGETHER_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen/Qwen2.5-Coder-32B-Instruct',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
label: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
label: 'Mixtral 8x7B Instruct',
provider: 'Together',
maxTokenAllowed: 8192,
},
];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl: fetchBaseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'TOGETHER_API_BASE_URL',
defaultApiTokenKey: 'TOGETHER_API_KEY',
});
const baseUrl = fetchBaseUrl || 'https://api.together.xyz/v1';
if (!baseUrl || !apiKey) {
return [];
}
// console.log({ baseUrl, apiKey });
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
const data = (res || []).filter((model: any) => model.type === 'chat');
return data.map((m: any) => ({
name: m.id,
label: `${m.display_name} - in:$${m.pricing.input.toFixed(2)} out:$${m.pricing.output.toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'TOGETHER_API_BASE_URL',
defaultApiTokenKey: 'TOGETHER_API_KEY',
});
if (!baseUrl || !apiKey) {
throw new Error(`Missing configuration for ${this.name} provider`);
}
return getOpenAILikeModel(baseUrl, apiKey, model);
}
}

View File

@@ -0,0 +1,47 @@
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 XAIProvider extends BaseProvider {
name = 'xAI';
getApiKeyLink = 'https://docs.x.ai/docs/quickstart#creating-an-api-key';
config = {
apiTokenKey: 'XAI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 },
{ name: 'grok-2-1212', label: 'xAI Grok2 1212', provider: 'xAI', 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: 'XAI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.x.ai/v1',
apiKey,
});
return openai(model);
}
}

View File

@@ -0,0 +1,39 @@
import AnthropicProvider from './providers/anthropic';
import CohereProvider from './providers/cohere';
import DeepseekProvider from './providers/deepseek';
import GoogleProvider from './providers/google';
import GroqProvider from './providers/groq';
import HuggingFaceProvider from './providers/huggingface';
import LMStudioProvider from './providers/lmstudio';
import MistralProvider from './providers/mistral';
import OllamaProvider from './providers/ollama';
import OpenRouterProvider from './providers/open-router';
import OpenAILikeProvider from './providers/openai-like';
import OpenAIProvider from './providers/openai';
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,
CohereProvider,
DeepseekProvider,
GoogleProvider,
GroqProvider,
HuggingFaceProvider,
HyperbolicProvider,
MistralProvider,
OllamaProvider,
OpenAIProvider,
OpenRouterProvider,
OpenAILikeProvider,
PerplexityProvider,
XAIProvider,
TogetherProvider,
LMStudioProvider,
AmazonBedrockProvider,
GithubProvider,
};

View File

@@ -0,0 +1,33 @@
import type { LanguageModelV1 } from 'ai';
import type { IProviderSetting } from '~/types/model';
export interface ModelInfo {
name: string;
label: string;
provider: string;
maxTokenAllowed: number;
}
export interface ProviderInfo {
name: string;
staticModels: ModelInfo[];
getDynamicModels?: (
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv?: Record<string, string>,
) => Promise<ModelInfo[]>;
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1;
getApiKeyLink?: string;
labelForGetApiKey?: string;
icon?: string;
}
export interface ProviderConfig {
baseUrlKey?: string;
baseUrl?: string;
apiTokenKey?: string;
}

View File

@@ -1,7 +1,7 @@
import { WebContainer } from '@webcontainer/api'; import { WebContainer } from '@webcontainer/api';
import { atom, map, type MapStore } from 'nanostores'; import { atom, map, type MapStore } from 'nanostores';
import * as nodePath from 'node:path'; import * as nodePath from 'node:path';
import type { BoltAction } from '~/types/actions'; import type { ActionAlert, BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger'; import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable'; import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser'; import type { ActionCallbackData } from './message-parser';
@@ -34,16 +34,51 @@ export type ActionStateUpdate =
type ActionsMap = MapStore<Record<string, ActionState>>; type ActionsMap = MapStore<Record<string, ActionState>>;
class ActionCommandError extends Error {
readonly _output: string;
readonly _header: string;
constructor(message: string, output: string) {
// Create a formatted message that includes both the error message and output
const formattedMessage = `Failed To Execute Shell Command: ${message}\n\nOutput:\n${output}`;
super(formattedMessage);
// Set the output separately so it can be accessed programmatically
this._header = message;
this._output = output;
// Maintain proper prototype chain
Object.setPrototypeOf(this, ActionCommandError.prototype);
// Set the name of the error for better debugging
this.name = 'ActionCommandError';
}
// Optional: Add a method to get just the terminal output
get output() {
return this._output;
}
get header() {
return this._header;
}
}
export class ActionRunner { export class ActionRunner {
#webcontainer: Promise<WebContainer>; #webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve(); #currentExecutionPromise: Promise<void> = Promise.resolve();
#shellTerminal: () => BoltShell; #shellTerminal: () => BoltShell;
runnerId = atom<string>(`${Date.now()}`); runnerId = atom<string>(`${Date.now()}`);
actions: ActionsMap = map({}); actions: ActionsMap = map({});
onAlert?: (alert: ActionAlert) => void;
constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) { constructor(
webcontainerPromise: Promise<WebContainer>,
getShellTerminal: () => BoltShell,
onAlert?: (alert: ActionAlert) => void,
) {
this.#webcontainer = webcontainerPromise; this.#webcontainer = webcontainerPromise;
this.#shellTerminal = getShellTerminal; this.#shellTerminal = getShellTerminal;
this.onAlert = onAlert;
} }
addAction(data: ActionCallbackData) { addAction(data: ActionCallbackData) {
@@ -126,7 +161,25 @@ export class ActionRunner {
this.#runStartAction(action) this.#runStartAction(action)
.then(() => this.#updateAction(actionId, { status: 'complete' })) .then(() => this.#updateAction(actionId, { status: 'complete' }))
.catch(() => this.#updateAction(actionId, { status: 'failed', error: 'Action failed' })); .catch((err: Error) => {
if (action.abortSignal.aborted) {
return;
}
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
logger.error(`[${action.type}]:Action failed\n\n`, err);
if (!(err instanceof ActionCommandError)) {
return;
}
this.onAlert?.({
type: 'error',
title: 'Dev Server Failed',
description: err.header,
content: err.output,
});
});
/* /*
* adding a delay to avoid any race condition between 2 start actions * adding a delay to avoid any race condition between 2 start actions
@@ -142,9 +195,24 @@ export class ActionRunner {
status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete', status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
}); });
} catch (error) { } catch (error) {
if (action.abortSignal.aborted) {
return;
}
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }); this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
logger.error(`[${action.type}]:Action failed\n\n`, error); logger.error(`[${action.type}]:Action failed\n\n`, error);
if (!(error instanceof ActionCommandError)) {
return;
}
this.onAlert?.({
type: 'error',
title: 'Dev Server Failed',
description: error.header,
content: error.output,
});
// re-throw the error to be caught in the promise chain // re-throw the error to be caught in the promise chain
throw error; throw error;
} }
@@ -162,11 +230,14 @@ export class ActionRunner {
unreachable('Shell terminal not found'); unreachable('Shell terminal not found');
} }
const resp = await shell.executeCommand(this.runnerId.get(), action.content); const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
action.abort();
});
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`); logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
if (resp?.exitCode != 0) { if (resp?.exitCode != 0) {
throw new Error('Failed To Execute Shell Command'); throw new ActionCommandError(`Failed To Execute Shell Command`, resp?.output || 'No Output Available');
} }
} }
@@ -186,11 +257,14 @@ export class ActionRunner {
unreachable('Shell terminal not found'); unreachable('Shell terminal not found');
} }
const resp = await shell.executeCommand(this.runnerId.get(), action.content); const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
action.abort();
});
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`); logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
if (resp?.exitCode != 0) { if (resp?.exitCode != 0) {
throw new Error('Failed To Start Application'); throw new ActionCommandError('Failed To Start Application', resp?.output || 'No Output Available');
} }
return resp; return resp;

View File

@@ -52,6 +52,18 @@ interface MessageState {
actionId: number; actionId: number;
} }
function cleanoutMarkdownSyntax(content: string) {
const codeBlockRegex = /^\s*```\w*\n([\s\S]*?)\n\s*```\s*$/;
const match = content.match(codeBlockRegex);
// console.log('matching', !!match, content);
if (match) {
return match[1]; // Remove common leading 4-space indent
} else {
return content;
}
}
export class StreamingMessageParser { export class StreamingMessageParser {
#messages = new Map<string, MessageState>(); #messages = new Map<string, MessageState>();
@@ -95,6 +107,11 @@ export class StreamingMessageParser {
let content = currentAction.content.trim(); let content = currentAction.content.trim();
if ('type' in currentAction && currentAction.type === 'file') { if ('type' in currentAction && currentAction.type === 'file') {
// Remove markdown code block syntax if present and file is not markdown
if (!currentAction.filePath.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
}
content += '\n'; content += '\n';
} }
@@ -120,7 +137,11 @@ export class StreamingMessageParser {
i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length; i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
} else { } else {
if ('type' in currentAction && currentAction.type === 'file') { if ('type' in currentAction && currentAction.type === 'file') {
const content = input.slice(i); let content = input.slice(i);
if (!currentAction.filePath.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
}
this._options.callbacks?.onActionStream?.({ this._options.callbacks?.onActionStream?.({
artifactId: currentArtifact.id, artifactId: currentArtifact.id,

View File

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

View File

@@ -39,6 +39,9 @@ PROVIDER_LIST.forEach((provider) => {
}, },
}; };
}); });
//TODO: need to create one single map for all these flags
export const providersStore = map<ProviderSetting>(initialProviderSettings); export const providersStore = map<ProviderSetting>(initialProviderSettings);
export const isDebugMode = atom(false); export const isDebugMode = atom(false);
@@ -50,3 +53,6 @@ export const isLocalModelsEnabled = atom(true);
export const promptStore = atom<string>('default'); export const promptStore = atom<string>('default');
export const latestBranchStore = atom(false); export const latestBranchStore = atom(false);
export const autoSelectStarterTemplate = atom(false);
export const enableContextOptimizationStore = atom(false);

View File

@@ -17,6 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence'; import { description } from '~/lib/persistence';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { createSampler } from '~/utils/sampler'; import { createSampler } from '~/utils/sampler';
import type { ActionAlert } from '~/types/actions';
export interface ArtifactState { export interface ArtifactState {
id: string; id: string;
@@ -38,11 +39,15 @@ export class WorkbenchStore {
#editorStore = new EditorStore(this.#filesStore); #editorStore = new EditorStore(this.#filesStore);
#terminalStore = new TerminalStore(webcontainer); #terminalStore = new TerminalStore(webcontainer);
#reloadedMessages = new Set<string>();
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({}); artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false); showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code'); currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>()); unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
actionAlert: WritableAtom<ActionAlert | undefined> =
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
modifiedFiles = new Set<string>(); modifiedFiles = new Set<string>();
artifactIdList: string[] = []; artifactIdList: string[] = [];
#globalExecutionQueue = Promise.resolve(); #globalExecutionQueue = Promise.resolve();
@@ -52,6 +57,7 @@ export class WorkbenchStore {
import.meta.hot.data.unsavedFiles = this.unsavedFiles; import.meta.hot.data.unsavedFiles = this.unsavedFiles;
import.meta.hot.data.showWorkbench = this.showWorkbench; import.meta.hot.data.showWorkbench = this.showWorkbench;
import.meta.hot.data.currentView = this.currentView; import.meta.hot.data.currentView = this.currentView;
import.meta.hot.data.actionAlert = this.actionAlert;
} }
} }
@@ -89,6 +95,12 @@ export class WorkbenchStore {
get boltTerminal() { get boltTerminal() {
return this.#terminalStore.boltTerminal; return this.#terminalStore.boltTerminal;
} }
get alert() {
return this.actionAlert;
}
clearAlert() {
this.actionAlert.set(undefined);
}
toggleTerminal(value?: boolean) { toggleTerminal(value?: boolean) {
this.#terminalStore.toggleTerminal(value); this.#terminalStore.toggleTerminal(value);
@@ -233,6 +245,10 @@ export class WorkbenchStore {
// TODO: what do we wanna do and how do we wanna recover from this? // TODO: what do we wanna do and how do we wanna recover from this?
} }
setReloadedMessages(messages: string[]) {
this.#reloadedMessages = new Set(messages);
}
addArtifact({ messageId, title, id, type }: ArtifactCallbackData) { addArtifact({ messageId, title, id, type }: ArtifactCallbackData) {
const artifact = this.#getArtifact(messageId); const artifact = this.#getArtifact(messageId);
@@ -249,7 +265,17 @@ export class WorkbenchStore {
title, title,
closed: false, closed: false,
type, type,
runner: new ActionRunner(webcontainer, () => this.boltTerminal), runner: new ActionRunner(
webcontainer,
() => this.boltTerminal,
(alert) => {
if (this.#reloadedMessages.has(messageId)) {
return;
}
this.actionAlert.set(alert);
},
),
}); });
} }

View File

@@ -1,5 +1,6 @@
import { WebContainer } from '@webcontainer/api'; import { WebContainer } from '@webcontainer/api';
import { WORK_DIR_NAME } from '~/utils/constants'; import { WORK_DIR_NAME } from '~/utils/constants';
import { cleanStackTrace } from '~/utils/stacktrace';
interface WebContainerContext { interface WebContainerContext {
loaded: boolean; loaded: boolean;
@@ -22,10 +23,34 @@ if (!import.meta.env.SSR) {
import.meta.hot?.data.webcontainer ?? import.meta.hot?.data.webcontainer ??
Promise.resolve() Promise.resolve()
.then(() => { .then(() => {
return WebContainer.boot({ workdirName: WORK_DIR_NAME }); return WebContainer.boot({
coep: 'credentialless',
workdirName: WORK_DIR_NAME,
forwardPreviewErrors: true, // Enable error forwarding from iframes
});
}) })
.then((webcontainer) => { .then(async (webcontainer) => {
webcontainerContext.loaded = true; webcontainerContext.loaded = true;
const { workbenchStore } = await import('~/lib/stores/workbench');
// Listen for preview errors
webcontainer.on('preview-message', (message) => {
console.log('WebContainer preview message:', message);
// Handle both uncaught exceptions and unhandled promise rejections
if (message.type === 'PREVIEW_UNCAUGHT_EXCEPTION' || message.type === 'PREVIEW_UNHANDLED_REJECTION') {
const isPromise = message.type === 'PREVIEW_UNHANDLED_REJECTION';
workbenchStore.actionAlert.set({
type: 'preview',
title: isPromise ? 'Unhandled Promise Rejection' : 'Uncaught Exception',
description: message.message,
content: `Error occurred at ${message.pathname}${message.search}${message.hash}\nPort: ${message.port}\n\nStack trace:\n${cleanStackTrace(message.stack || '')}`,
source: 'preview',
});
}
});
return webcontainer; return webcontainer;
}); });

View File

@@ -1,15 +1,22 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'; import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { createDataStream } from 'ai'; import { createDataStream, generateId } from 'ai';
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants'; import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS, type FileMap } from '~/lib/.server/llm/constants';
import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts'; import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts';
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text'; import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
import SwitchableStream from '~/lib/.server/llm/switchable-stream'; import SwitchableStream from '~/lib/.server/llm/switchable-stream';
import type { IProviderSetting } from '~/types/model'; import type { IProviderSetting } from '~/types/model';
import { createScopedLogger } from '~/utils/logger';
import { getFilePaths, selectContext } from '~/lib/.server/llm/select-context';
import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
import { WORK_DIR } from '~/utils/constants';
import { createSummary } from '~/lib/.server/llm/create-summary';
export async function action(args: ActionFunctionArgs) { export async function action(args: ActionFunctionArgs) {
return chatAction(args); return chatAction(args);
} }
const logger = createScopedLogger('api.chat');
function parseCookies(cookieHeader: string): Record<string, string> { function parseCookies(cookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {}; const cookies: Record<string, string> = {};
@@ -29,10 +36,11 @@ function parseCookies(cookieHeader: string): Record<string, string> {
} }
async function chatAction({ context, request }: ActionFunctionArgs) { async function chatAction({ context, request }: ActionFunctionArgs) {
const { messages, files, promptId } = await request.json<{ const { messages, files, promptId, contextOptimization } = await request.json<{
messages: Messages; messages: Messages;
files: any; files: any;
promptId?: string; promptId?: string;
contextOptimization: boolean;
}>(); }>();
const cookieHeader = request.headers.get('Cookie'); const cookieHeader = request.headers.get('Cookie');
@@ -48,12 +56,113 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
promptTokens: 0, promptTokens: 0,
totalTokens: 0, totalTokens: 0,
}; };
const encoder: TextEncoder = new TextEncoder();
let progressCounter: number = 1;
try { try {
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`);
const dataStream = createDataStream({
async execute(dataStream) {
const filePaths = getFilePaths(files || {});
let filteredFiles: FileMap | undefined = undefined;
let summary: string | undefined = undefined;
if (filePaths.length > 0 && contextOptimization) {
dataStream.writeData('HI ');
logger.debug('Generating Chat Summary');
dataStream.writeMessageAnnotation({
type: 'progress',
value: progressCounter++,
message: 'Generating Chat Summary',
} as ProgressAnnotation);
// Create a summary of the chat
console.log(`Messages count: ${messages.length}`);
summary = await createSummary({
messages: [...messages],
env: context.cloudflare?.env,
apiKeys,
providerSettings,
promptId,
contextOptimization,
onFinish(resp) {
if (resp.usage) {
logger.debug('createSummary token usage', JSON.stringify(resp.usage));
cumulativeUsage.completionTokens += resp.usage.completionTokens || 0;
cumulativeUsage.promptTokens += resp.usage.promptTokens || 0;
cumulativeUsage.totalTokens += resp.usage.totalTokens || 0;
}
},
});
dataStream.writeMessageAnnotation({
type: 'chatSummary',
summary,
chatId: messages.slice(-1)?.[0]?.id,
} as ContextAnnotation);
// Update context buffer
logger.debug('Updating Context Buffer');
dataStream.writeMessageAnnotation({
type: 'progress',
value: progressCounter++,
message: 'Updating Context Buffer',
} as ProgressAnnotation);
// Select context files
console.log(`Messages count: ${messages.length}`);
filteredFiles = await selectContext({
messages: [...messages],
env: context.cloudflare?.env,
apiKeys,
files,
providerSettings,
promptId,
contextOptimization,
summary,
onFinish(resp) {
if (resp.usage) {
logger.debug('selectContext token usage', JSON.stringify(resp.usage));
cumulativeUsage.completionTokens += resp.usage.completionTokens || 0;
cumulativeUsage.promptTokens += resp.usage.promptTokens || 0;
cumulativeUsage.totalTokens += resp.usage.totalTokens || 0;
}
},
});
if (filteredFiles) {
logger.debug(`files in context : ${JSON.stringify(Object.keys(filteredFiles))}`);
}
dataStream.writeMessageAnnotation({
type: 'codeContext',
files: Object.keys(filteredFiles).map((key) => {
let path = key;
if (path.startsWith(WORK_DIR)) {
path = path.replace(WORK_DIR, '');
}
return path;
}),
} as ContextAnnotation);
dataStream.writeMessageAnnotation({
type: 'progress',
value: progressCounter++,
message: 'Context Buffer Updated',
} as ProgressAnnotation);
logger.debug('Context Buffer Updated');
}
// Stream the text
const options: StreamingOptions = { const options: StreamingOptions = {
toolChoice: 'none', toolChoice: 'none',
onFinish: async ({ text: content, finishReason, usage }) => { onFinish: async ({ text: content, finishReason, usage }) => {
console.log('usage', usage); logger.debug('usage', JSON.stringify(usage));
if (usage) { if (usage) {
cumulativeUsage.completionTokens += usage.completionTokens || 0; cumulativeUsage.completionTokens += usage.completionTokens || 0;
@@ -62,10 +171,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
} }
if (finishReason !== 'length') { if (finishReason !== 'length') {
return stream
.switchSource(
createDataStream({
async execute(dataStream) {
dataStream.writeMessageAnnotation({ dataStream.writeMessageAnnotation({
type: 'usage', type: 'usage',
value: { value: {
@@ -74,11 +179,10 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
totalTokens: cumulativeUsage.totalTokens, totalTokens: cumulativeUsage.totalTokens,
}, },
}); });
}, await new Promise((resolve) => setTimeout(resolve, 0));
onError: (error: any) => `Custom error: ${error.message}`,
}), // stream.close();
) return;
.then(() => stream.close());
} }
if (stream.switches >= MAX_RESPONSE_SEGMENTS) { if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
@@ -87,45 +191,87 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches; const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;
console.log(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`); logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
messages.push({ role: 'assistant', content }); messages.push({ id: generateId(), role: 'assistant', content });
messages.push({ role: 'user', content: CONTINUE_PROMPT }); messages.push({ id: generateId(), role: 'user', content: CONTINUE_PROMPT });
const result = await streamText({ const result = await streamText({
messages, messages,
env: context.cloudflare.env, env: context.cloudflare?.env,
options, options,
apiKeys, apiKeys,
files, files,
providerSettings, providerSettings,
promptId, promptId,
contextOptimization,
}); });
return stream.switchSource(result.toDataStream()); result.mergeIntoDataStream(dataStream);
(async () => {
for await (const part of result.fullStream) {
if (part.type === 'error') {
const error: any = part.error;
logger.error(`${error}`);
return;
}
}
})();
return;
}, },
}; };
const result = await streamText({ const result = await streamText({
messages, messages,
env: context.cloudflare.env, env: context.cloudflare?.env,
options, options,
apiKeys, apiKeys,
files, files,
providerSettings, providerSettings,
promptId, promptId,
contextOptimization,
contextFiles: filteredFiles,
summary,
}); });
stream.switchSource(result.toDataStream()); (async () => {
for await (const part of result.fullStream) {
if (part.type === 'error') {
const error: any = part.error;
logger.error(`${error}`);
return new Response(stream.readable, { return;
}
}
})();
result.mergeIntoDataStream(dataStream);
},
onError: (error: any) => `Custom error: ${error.message}`,
}).pipeThrough(
new TransformStream({
transform: (chunk, controller) => {
// Convert the string stream to a byte stream
const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
controller.enqueue(encoder.encode(str));
},
}),
);
return new Response(dataStream, {
status: 200, status: 200,
headers: { headers: {
contentType: 'text/plain; charset=utf-8', 'Content-Type': 'text/event-stream; charset=utf-8',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Text-Encoding': 'chunked',
}, },
}); });
} catch (error: any) { } catch (error: any) {
console.error(error); logger.error(error);
if (error.message?.includes('API key')) { if (error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', { throw new Response('Invalid or missing API key', {

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,37 +1,13 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'; import { type ActionFunctionArgs } from '@remix-run/cloudflare';
//import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text'; import { streamText } from '~/lib/.server/llm/stream-text';
import { stripIndents } from '~/utils/stripIndent'; import { stripIndents } from '~/utils/stripIndent';
import type { IProviderSetting, ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export async function action(args: ActionFunctionArgs) { export async function action(args: ActionFunctionArgs) {
return enhancerAction(args); return enhancerAction(args);
} }
function parseCookies(cookieHeader: string) {
const cookies: any = {};
// Split the cookie string by semicolons and spaces
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
items.forEach((item) => {
const [name, ...rest] = item.split('=');
if (name && rest) {
// Decode the name and value, and join value parts in case it contains '='
const decodedName = decodeURIComponent(name.trim());
const decodedValue = decodeURIComponent(rest.join('=').trim());
cookies[decodedName] = decodedValue;
}
});
return cookies;
}
async function enhancerAction({ context, request }: ActionFunctionArgs) { async function enhancerAction({ context, request }: ActionFunctionArgs) {
const { message, model, provider } = await request.json<{ const { message, model, provider } = await request.json<{
message: string; message: string;
@@ -58,12 +34,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
} }
const cookieHeader = request.headers.get('Cookie'); const cookieHeader = request.headers.get('Cookie');
const apiKeys = getApiKeysFromCookie(cookieHeader);
// Parse the cookie's value (returns an object or null if no cookie exists) const providerSettings = getProviderSettingsFromCookie(cookieHeader);
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
parseCookies(cookieHeader || '').providers || '{}',
);
try { try {
const result = await streamText({ const result = await streamText({
@@ -102,37 +74,18 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
`, `,
}, },
], ],
env: context.cloudflare.env, env: context.cloudflare?.env as any,
apiKeys, apiKeys,
providerSettings, providerSettings,
}); });
const transformStream = new TransformStream({ return new Response(result.textStream, {
transform(chunk, controller) {
const text = decoder.decode(chunk);
const lines = text.split('\n').filter((line) => line.trim() !== '');
for (const line of lines) {
try {
const parsed = JSON.parse(line);
if (parsed.type === 'text') {
controller.enqueue(encoder.encode(parsed.value));
}
} catch (e) {
// skip invalid JSON lines
console.warn('Failed to parse stream part:', line, e);
}
}
},
});
const transformedStream = result.toDataStream().pipeThrough(transformStream);
return new Response(transformedStream, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'text/plain; charset=utf-8', 'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Text-Encoding': 'chunked',
}, },
}); });
} catch (error: unknown) { } catch (error: unknown) {

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

149
app/routes/api.llmcall.ts Normal file
View File

@@ -0,0 +1,149 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { streamText } from '~/lib/.server/llm/stream-text';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { generateText } from 'ai';
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);
}
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) {
const { system, message, model, provider, streamOutput } = await request.json<{
system: string;
message: string;
model: string;
provider: ProviderInfo;
streamOutput?: boolean;
}>();
const { name: providerName } = provider;
// validate 'model' and 'provider' fields
if (!model || typeof model !== 'string') {
throw new Response('Invalid or missing model', {
status: 400,
statusText: 'Bad Request',
});
}
if (!providerName || typeof providerName !== 'string') {
throw new Response('Invalid or missing provider', {
status: 400,
statusText: 'Bad Request',
});
}
const cookieHeader = request.headers.get('Cookie');
const apiKeys = getApiKeysFromCookie(cookieHeader);
const providerSettings = getProviderSettingsFromCookie(cookieHeader);
if (streamOutput) {
try {
const result = await streamText({
options: {
system,
},
messages: [
{
role: 'user',
content: `${message}`,
},
],
env: context.cloudflare?.env as any,
apiKeys,
providerSettings,
});
return new Response(result.textStream, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error: unknown) {
console.log(error);
if (error instanceof Error && error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', {
status: 401,
statusText: 'Unauthorized',
});
}
throw new Response(null, {
status: 500,
statusText: 'Internal Server Error',
});
}
} else {
try {
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');
}
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
const providerInfo = PROVIDER_LIST.find((p) => p.name === provider.name);
if (!providerInfo) {
throw new Error('Provider not found');
}
const result = await generateText({
system,
messages: [
{
role: 'user',
content: `${message}`,
},
],
model: providerInfo.getModelInstance({
model: modelDetails.name,
serverEnv: context.cloudflare?.env as any,
apiKeys,
providerSettings,
}),
maxTokens: dynamicMaxTokens,
toolChoice: 'none',
});
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error: unknown) {
console.log(error);
if (error instanceof Error && error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', {
status: 401,
statusText: 'Unauthorized',
});
}
throw new Response(null, {
status: 500,
statusText: 'Internal Server Error',
});
}
}
}

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

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

@@ -219,7 +219,7 @@
--header-height: 54px; --header-height: 54px;
--chat-max-width: 37rem; --chat-max-width: 37rem;
--chat-min-width: 640px; --chat-min-width: 640px;
--workbench-width: min(calc(100% - var(--chat-min-width)), 1536px); --workbench-width: min(calc(100% - var(--chat-min-width)), 2536px);
--workbench-inner-width: var(--workbench-width); --workbench-inner-width: var(--workbench-width);
--workbench-left: calc(100% - var(--workbench-width)); --workbench-left: calc(100% - var(--workbench-width));

View File

@@ -20,3 +20,11 @@ export interface StartAction extends BaseAction {
export type BoltAction = FileAction | ShellAction | StartAction; export type BoltAction = FileAction | ShellAction | StartAction;
export type BoltActionData = BoltAction | BaseAction; export type BoltActionData = BoltAction | BaseAction;
export interface ActionAlert {
type: string;
title: string;
description: string;
content: string;
source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors
}

16
app/types/context.ts Normal file
View File

@@ -0,0 +1,16 @@
export type ContextAnnotation =
| {
type: 'codeContext';
files: string[];
}
| {
type: 'chatSummary';
summary: string;
chatId: string;
};
export type ProgressAnnotation = {
type: 'progress';
value: number;
message: string;
};

View File

@@ -1,9 +1,14 @@
import type { ModelInfo } from '~/utils/types'; import type { ModelInfo } from '~/lib/modules/llm/types';
export type ProviderInfo = { export type ProviderInfo = {
staticModels: ModelInfo[]; staticModels: ModelInfo[];
name: string; name: string;
getDynamicModels?: (apiKeys?: Record<string, string>, providerSettings?: IProviderSetting) => Promise<ModelInfo[]>; getDynamicModels?: (
providerName: string,
apiKeys?: Record<string, string>,
providerSettings?: IProviderSetting,
serverEnv?: Record<string, string>,
) => Promise<ModelInfo[]>;
getApiKeyLink?: string; getApiKeyLink?: string;
labelForGetApiKey?: string; labelForGetApiKey?: string;
icon?: string; icon?: string;

8
app/types/template.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface Template {
name: string;
label: string;
description: string;
githubRepo: string;
tags?: string[];
icon?: string;
}

View File

@@ -1,8 +1,5 @@
import Cookies from 'js-cookie'; import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types'; import type { Template } from '~/types/template';
import type { ProviderInfo, IProviderSetting } from '~/types/model';
import { createScopedLogger } from './logger';
import { logStore } from '~/lib/stores/logs';
export const WORK_DIR_NAME = 'project'; export const WORK_DIR_NAME = 'project';
export const WORK_DIR = `/home/${WORK_DIR_NAME}`; export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
@@ -12,533 +9,108 @@ export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest'; export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
export const PROMPT_COOKIE_KEY = 'cachedPrompt'; export const PROMPT_COOKIE_KEY = 'cachedPrompt';
const logger = createScopedLogger('Constants'); const llmManager = LLMManager.getInstance(import.meta.env);
const PROVIDER_LIST: ProviderInfo[] = [ export const PROVIDER_LIST = llmManager.getAllProviders();
{ export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
name: 'Anthropic',
staticModels: [
{
name: 'claude-3-5-sonnet-latest',
label: 'Claude 3.5 Sonnet (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-sonnet-20240620',
label: 'Claude 3.5 Sonnet (old)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-haiku-latest',
label: 'Claude 3.5 Haiku (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{ name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 },
],
getApiKeyLink: 'https://console.anthropic.com/settings/keys',
},
{
name: 'Ollama',
staticModels: [],
getDynamicModels: getOllamaModels,
getApiKeyLink: 'https://ollama.com/download',
labelForGetApiKey: 'Download Ollama',
icon: 'i-ph:cloud-arrow-down',
},
{
name: 'OpenAILike',
staticModels: [],
getDynamicModels: getOpenAILikeModels,
},
{
name: 'Cohere',
staticModels: [
{ name: 'command-r-plus-08-2024', label: 'Command R plus Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-08-2024', label: 'Command R Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-plus', label: 'Command R plus', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r', label: 'Command R', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command', label: 'Command', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-nightly', label: 'Command Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light', label: 'Command Light', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light-nightly', label: 'Command Light Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 },
],
getApiKeyLink: 'https://dashboard.cohere.com/api-keys',
},
{
name: 'OpenRouter',
staticModels: [
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI', maxTokenAllowed: 8000 },
{
name: 'anthropic/claude-3.5-sonnet',
label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'anthropic/claude-3-haiku',
label: 'Anthropic: Claude 3 Haiku (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'deepseek/deepseek-coder',
label: 'Deepseek-Coder V2 236B (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-flash-1.5',
label: 'Google Gemini Flash 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-pro-1.5',
label: 'Google Gemini Pro 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
{
name: 'mistralai/mistral-nemo',
label: 'OpenRouter Mistral Nemo (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'qwen/qwen-110b-chat',
label: 'OpenRouter Qwen 110b Chat (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 4096 },
],
getDynamicModels: getOpenRouterModels,
getApiKeyLink: 'https://openrouter.ai/settings/keys',
},
{
name: 'Google',
staticModels: [
{ name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-exp-1206', label: 'Gemini exp-1206', provider: 'Google', maxTokenAllowed: 8192 },
],
getApiKeyLink: 'https://aistudio.google.com/app/apikey',
},
{
name: 'Groq',
staticModels: [
{ name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
],
getApiKeyLink: 'https://console.groq.com/keys',
},
{
name: 'HuggingFace',
staticModels: [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-72B-Instruct',
label: 'Qwen2.5-72B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-70B-Instruct',
label: 'Llama-3.1-70B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-405B',
label: 'Llama-3.1-405B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
],
getApiKeyLink: 'https://huggingface.co/settings/tokens',
},
{ export const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
name: 'OpenAI', PROVIDER_LIST.forEach((provider) => {
staticModels: [ providerBaseUrlEnvKeys[provider.name] = {
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 }, baseUrlKey: provider.config.baseUrlKey,
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 }, apiTokenKey: provider.config.apiTokenKey,
{ name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 }, };
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 }, });
],
getApiKeyLink: 'https://platform.openai.com/api-keys',
},
{
name: 'xAI',
staticModels: [{ name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 }],
getApiKeyLink: 'https://docs.x.ai/docs/quickstart#creating-an-api-key',
},
{
name: 'Deepseek',
staticModels: [
{ name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
{ name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 },
],
getApiKeyLink: 'https://platform.deepseek.com/apiKeys',
},
{
name: 'Mistral',
staticModels: [
{ name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', maxTokenAllowed: 8000 },
],
getApiKeyLink: 'https://console.mistral.ai/api-keys/',
},
{
name: 'LMStudio',
staticModels: [],
getDynamicModels: getLMStudioModels,
getApiKeyLink: 'https://lmstudio.ai/',
labelForGetApiKey: 'Get LMStudio',
icon: 'i-ph:cloud-arrow-down',
},
{
name: 'Together',
getDynamicModels: getTogetherModels,
staticModels: [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen/Qwen2.5-Coder-32B-Instruct',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
label: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'Together',
maxTokenAllowed: 8000,
},
// starter Templates
export const STARTER_TEMPLATES: Template[] = [
{ {
name: 'mistralai/Mixtral-8x7B-Instruct-v0.1', name: 'bolt-astro-basic',
label: 'Mixtral 8x7B Instruct', label: 'Astro Basic',
provider: 'Together', description: 'Lightweight Astro starter template for building fast static websites',
maxTokenAllowed: 8192, githubRepo: 'thecodacus/bolt-astro-basic-template',
}, tags: ['astro', 'blog', 'performance'],
], icon: 'i-bolt:astro',
getApiKeyLink: 'https://api.together.xyz/settings/api-keys',
}, },
{ {
name: 'Perplexity', name: 'bolt-nextjs-shadcn',
staticModels: [ label: 'Next.js with shadcn/ui',
{ description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system',
name: 'llama-3.1-sonar-small-128k-online', githubRepo: 'thecodacus/bolt-nextjs-shadcn-template',
label: 'Sonar Small Online', tags: ['nextjs', 'react', 'typescript', 'shadcn', 'tailwind'],
provider: 'Perplexity', icon: 'i-bolt:nextjs',
maxTokenAllowed: 8192,
}, },
{ {
name: 'llama-3.1-sonar-large-128k-online', name: 'bolt-qwik-ts',
label: 'Sonar Large Online', label: 'Qwik TypeScript',
provider: 'Perplexity', description: 'Qwik framework starter with TypeScript for building resumable applications',
maxTokenAllowed: 8192, githubRepo: 'thecodacus/bolt-qwik-ts-template',
tags: ['qwik', 'typescript', 'performance', 'resumable'],
icon: 'i-bolt:qwik',
}, },
{ {
name: 'llama-3.1-sonar-huge-128k-online', name: 'bolt-remix-ts',
label: 'Sonar Huge Online', label: 'Remix TypeScript',
provider: 'Perplexity', description: 'Remix framework starter with TypeScript for full-stack web applications',
maxTokenAllowed: 8192, githubRepo: 'thecodacus/bolt-remix-ts-template',
tags: ['remix', 'typescript', 'fullstack', 'react'],
icon: 'i-bolt:remix',
}, },
], {
getApiKeyLink: 'https://www.perplexity.ai/settings/api', name: 'bolt-slidev',
label: 'Slidev Presentation',
description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
githubRepo: 'thecodacus/bolt-slidev-template',
tags: ['slidev', 'presentation', 'markdown'],
icon: 'i-bolt:slidev',
},
{
name: 'bolt-sveltekit',
label: 'SvelteKit',
description: 'SvelteKit starter template for building fast, efficient web applications',
githubRepo: 'bolt-sveltekit-template',
tags: ['svelte', 'sveltekit', 'typescript'],
icon: 'i-bolt:svelte',
},
{
name: 'vanilla-vite',
label: 'Vanilla + Vite',
description: 'Minimal Vite starter template for vanilla JavaScript projects',
githubRepo: 'thecodacus/vanilla-vite-template',
tags: ['vite', 'vanilla-js', 'minimal'],
icon: 'i-bolt:vite',
},
{
name: 'bolt-vite-react',
label: 'React + Vite + typescript',
description: 'React starter template powered by Vite for fast development experience',
githubRepo: 'thecodacus/bolt-vite-react-ts-template',
tags: ['react', 'vite', 'frontend'],
icon: 'i-bolt:react',
},
{
name: 'bolt-vite-ts',
label: 'Vite + TypeScript',
description: 'Vite starter template with TypeScript configuration for type-safe development',
githubRepo: 'thecodacus/bolt-vite-ts-template',
tags: ['vite', 'typescript', 'minimal'],
icon: 'i-bolt:typescript',
},
{
name: 'bolt-vue',
label: 'Vue.js',
description: 'Vue.js starter template with modern tooling and best practices',
githubRepo: 'thecodacus/bolt-vue-template',
tags: ['vue', 'typescript', 'frontend'],
icon: 'i-bolt:vue',
},
{
name: 'bolt-angular',
label: 'Angular Starter',
description: 'A modern Angular starter template with TypeScript support and best practices configuration',
githubRepo: 'thecodacus/bolt-angular-template',
tags: ['angular', 'typescript', 'frontend', 'spa'],
icon: 'i-bolt:angular',
}, },
]; ];
export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat();
export let MODEL_LIST: ModelInfo[] = [...staticModels];
export async function getModelList(
apiKeys: Record<string, string>,
providerSettings?: Record<string, IProviderSetting>,
) {
MODEL_LIST = [
...(
await Promise.all(
PROVIDER_LIST.filter(
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name])),
)
).flat(),
...staticModels,
];
return MODEL_LIST;
}
async function getTogetherModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
try {
const baseUrl = settings?.baseUrl || import.meta.env.TOGETHER_API_BASE_URL || '';
const provider = 'Together';
if (!baseUrl) {
return [];
}
let apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
if (apiKeys && apiKeys[provider]) {
apiKey = apiKeys[provider];
}
if (!apiKey) {
return [];
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
const data: any[] = (res || []).filter((model: any) => model.type == 'chat');
return data.map((m: any) => ({
name: m.id,
label: `${m.display_name} - in:$${m.pricing.input.toFixed(
2,
)} out:$${m.pricing.output.toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider,
maxTokenAllowed: 8000,
}));
} catch (e) {
console.error('Error getting OpenAILike models:', e);
return [];
}
}
const getOllamaBaseUrl = (settings?: IProviderSetting) => {
const defaultBaseUrl = settings?.baseUrl || import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
// Check if we're in the browser
if (typeof window !== 'undefined') {
// Frontend always uses localhost
return defaultBaseUrl;
}
// Backend: Check if we're running in Docker
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
return isDocker ? defaultBaseUrl.replace('localhost', 'host.docker.internal') : defaultBaseUrl;
};
async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
try {
const baseUrl = getOllamaBaseUrl(settings);
const response = await fetch(`${baseUrl}/api/tags`);
const data = (await response.json()) as OllamaApiResponse;
return data.models.map((model: OllamaModel) => ({
name: model.name,
label: `${model.name} (${model.details.parameter_size})`,
provider: 'Ollama',
maxTokenAllowed: 8000,
}));
} catch (e: any) {
logStore.logError('Failed to get Ollama models', e, { baseUrl: settings?.baseUrl });
logger.warn('Failed to get Ollama models: ', e.message || '');
return [];
}
}
async function getOpenAILikeModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
): Promise<ModelInfo[]> {
try {
const baseUrl = settings?.baseUrl || import.meta.env.OPENAI_LIKE_API_BASE_URL || '';
if (!baseUrl) {
return [];
}
let apiKey = '';
if (apiKeys && apiKeys.OpenAILike) {
apiKey = apiKeys.OpenAILike;
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
return res.data.map((model: any) => ({
name: model.id,
label: model.id,
provider: 'OpenAILike',
}));
} catch (e) {
console.error('Error getting OpenAILike models:', e);
return [];
}
}
type OpenRouterModelsResponse = {
data: {
name: string;
id: string;
context_length: number;
pricing: {
prompt: number;
completion: number;
};
}[];
};
async function getOpenRouterModels(): Promise<ModelInfo[]> {
const data: OpenRouterModelsResponse = await (
await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Content-Type': 'application/json',
},
})
).json();
return data.data
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => ({
name: m.id,
label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(
2,
)} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider: 'OpenRouter',
maxTokenAllowed: 8000,
}));
}
async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
try {
const baseUrl = settings?.baseUrl || import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
const response = await fetch(`${baseUrl}/v1/models`);
const data = (await response.json()) as any;
return data.data.map((model: any) => ({
name: model.id,
label: model.id,
provider: 'LMStudio',
}));
} catch (e: any) {
logStore.logError('Failed to get LMStudio models', e, { baseUrl: settings?.baseUrl });
return [];
}
}
async function initializeModelList(providerSettings?: Record<string, IProviderSetting>): Promise<ModelInfo[]> {
let apiKeys: Record<string, string> = {};
try {
const storedApiKeys = Cookies.get('apiKeys');
if (storedApiKeys) {
const parsedKeys = JSON.parse(storedApiKeys);
if (typeof parsedKeys === 'object' && parsedKeys !== null) {
apiKeys = parsedKeys;
}
}
} catch (error: any) {
logStore.logError('Failed to fetch API keys from cookies', error);
logger.warn(`Failed to fetch apikeys from cookies: ${error?.message}`);
}
MODEL_LIST = [
...(
await Promise.all(
PROVIDER_LIST.filter(
(p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name])),
)
).flat(),
...staticModels,
];
return MODEL_LIST;
}
export {
getOllamaModels,
getOpenAILikeModels,
getLMStudioModels,
initializeModelList,
getOpenRouterModels,
PROVIDER_LIST,
};

View File

@@ -1,4 +1,7 @@
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'; export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
import { Chalk } from 'chalk';
const chalk = new Chalk({ level: 3 });
type LoggerFunction = (...messages: any[]) => void; type LoggerFunction = (...messages: any[]) => void;
@@ -13,9 +16,6 @@ interface Logger {
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info'; let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
const isWorker = 'HTMLRewriter' in globalThis;
const supportsColor = !isWorker;
export const logger: Logger = { export const logger: Logger = {
trace: (...messages: any[]) => log('trace', undefined, messages), trace: (...messages: any[]) => log('trace', undefined, messages),
debug: (...messages: any[]) => log('debug', undefined, messages), debug: (...messages: any[]) => log('debug', undefined, messages),
@@ -63,14 +63,8 @@ function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
return `${acc} ${current}`; return `${acc} ${current}`;
}, ''); }, '');
if (!supportsColor) {
console.log(`[${level.toUpperCase()}]`, allMessages);
return;
}
const labelBackgroundColor = getColorForLevel(level); const labelBackgroundColor = getColorForLevel(level);
const labelTextColor = level === 'warn' ? 'black' : 'white'; const labelTextColor = level === 'warn' ? '#000000' : '#FFFFFF';
const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor); const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor);
const scopeStyles = getLabelStyles('#77828D', 'white'); const scopeStyles = getLabelStyles('#77828D', 'white');
@@ -81,7 +75,21 @@ function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
styles.push('', scopeStyles); styles.push('', scopeStyles);
} }
let labelText = formatText(` ${level.toUpperCase()} `, labelTextColor, labelBackgroundColor);
if (scope) {
labelText = `${labelText} ${formatText(` ${scope} `, '#FFFFFF', '77828D')}`;
}
if (typeof window !== 'undefined') {
console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, allMessages); console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, allMessages);
} else {
console.log(`${labelText}`, allMessages);
}
}
function formatText(text: string, color: string, bg: string) {
return chalk.bgHex(bg)(chalk.hex(color)(text));
} }
function getLabelStyles(color: string, textColor: string) { function getLabelStyles(color: string, textColor: string) {
@@ -104,7 +112,7 @@ function getColorForLevel(level: DebugLevel): string {
return '#EE4744'; return '#EE4744';
} }
default: { default: {
return 'black'; return '#000000';
} }
} }
} }

View File

@@ -0,0 +1,304 @@
import ignore from 'ignore';
import type { ProviderInfo } from '~/types/model';
import type { Template } from '~/types/template';
import { STARTER_TEMPLATES } from './constants';
import Cookies from 'js-cookie';
const starterTemplateSelectionPrompt = (templates: Template[]) => `
You are an experienced developer who helps people choose the best starter template for their projects.
Available templates:
<template>
<name>blank</name>
<description>Empty starter for simple scripts and trivial tasks that don't require a full template setup</description>
<tags>basic, script</tags>
</template>
${templates
.map(
(template) => `
<template>
<name>${template.name}</name>
<description>${template.description}</description>
${template.tags ? `<tags>${template.tags.join(', ')}</tags>` : ''}
</template>
`,
)
.join('\n')}
Response Format:
<selection>
<templateName>{selected template name}</templateName>
<title>{a proper title for the project}</title>
</selection>
Examples:
<example>
User: I need to build a todo app
Response:
<selection>
<templateName>react-basic-starter</templateName>
<title>Simple React todo application</title>
</selection>
</example>
<example>
User: Write a script to generate numbers from 1 to 100
Response:
<selection>
<templateName>blank</templateName>
<title>script to generate numbers from 1 to 100</title>
</selection>
</example>
Instructions:
1. For trivial tasks and simple scripts, always recommend the blank template
2. For more complex projects, recommend templates from the provided list
3. Follow the exact XML format
4. Consider both technical requirements and tags
5. If no perfect match exists, recommend the closest option
Important: Provide only the selection tags in your response, no additional text.
`;
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn'));
const parseSelectedTemplate = (llmOutput: string): { template: string; title: string } | null => {
try {
// Extract content between <templateName> tags
const templateNameMatch = llmOutput.match(/<templateName>(.*?)<\/templateName>/);
const titleMatch = llmOutput.match(/<title>(.*?)<\/title>/);
if (!templateNameMatch) {
return null;
}
return { template: templateNameMatch[1].trim(), title: titleMatch?.[1].trim() || 'Untitled Project' };
} catch (error) {
console.error('Error parsing template selection:', error);
return null;
}
};
export const selectStarterTemplate = async (options: { message: string; model: string; provider: ProviderInfo }) => {
const { message, model, provider } = options;
const requestBody = {
message,
model,
provider,
system: starterTemplateSelectionPrompt(templates),
};
const response = await fetch('/api/llmcall', {
method: 'POST',
body: JSON.stringify(requestBody),
});
const respJson: { text: string } = await response.json();
console.log(respJson);
const { text } = respJson;
const selectedTemplate = parseSelectedTemplate(text);
if (selectedTemplate) {
return selectedTemplate;
} else {
console.log('No template selected, using blank template');
return {
template: 'blank',
title: '',
};
}
};
const getGitHubRepoContent = async (
repoName: string,
path: string = '',
): Promise<{ name: string; path: string; content: string }[]> => {
const baseUrl = 'https://api.github.com';
try {
const token = Cookies.get('githubToken') || import.meta.env.VITE_GITHUB_ACCESS_TOKEN;
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3+json',
};
// Add your GitHub token if needed
if (token) {
headers.Authorization = 'token ' + token;
}
// Fetch contents of the path
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
headers,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: any = await response.json();
// If it's a single file, return its content
if (!Array.isArray(data)) {
if (data.type === 'file') {
// If it's a file, get its content
const content = atob(data.content); // Decode base64 content
return [
{
name: data.name,
path: data.path,
content,
},
];
}
}
// Process directory contents recursively
const contents = await Promise.all(
data.map(async (item: any) => {
if (item.type === 'dir') {
// Recursively get contents of subdirectories
return await getGitHubRepoContent(repoName, item.path);
} else if (item.type === 'file') {
// Fetch file content
const fileResponse = await fetch(item.url, {
headers,
});
const fileData: any = await fileResponse.json();
const content = atob(fileData.content); // Decode base64 content
return [
{
name: item.name,
path: item.path,
content,
},
];
}
return [];
}),
);
// Flatten the array of contents
return contents.flat();
} catch (error) {
console.error('Error fetching repo contents:', error);
throw error;
}
};
export async function getTemplates(templateName: string, title?: string) {
const template = STARTER_TEMPLATES.find((t) => t.name == templateName);
if (!template) {
return null;
}
const githubRepo = template.githubRepo;
const files = await getGitHubRepoContent(githubRepo);
let filteredFiles = files;
/*
* ignoring common unwanted files
* exclude .git
*/
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.git') == false);
// exclude lock files
const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false);
// exclude .bolt
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.bolt') == false);
// check for ignore file in .bolt folder
const templateIgnoreFile = files.find((x) => x.path.startsWith('.bolt') && x.name == 'ignore');
const filesToImport = {
files: filteredFiles,
ignoreFile: [] as typeof filteredFiles,
};
if (templateIgnoreFile) {
// redacting files specified in ignore file
const ignorepatterns = templateIgnoreFile.content.split('\n').map((x) => x.trim());
const ig = ignore().add(ignorepatterns);
// filteredFiles = filteredFiles.filter(x => !ig.ignores(x.path))
const ignoredFiles = filteredFiles.filter((x) => ig.ignores(x.path));
filesToImport.files = filteredFiles;
filesToImport.ignoreFile = ignoredFiles;
}
const assistantMessage = `
<boltArtifact id="imported-files" title="${title || 'Importing Starter Files'}" type="bundled">
${filesToImport.files
.map(
(file) =>
`<boltAction type="file" filePath="${file.path}">
${file.content}
</boltAction>`,
)
.join('\n')}
</boltArtifact>
`;
let userMessage = ``;
const templatePromptFile = files.filter((x) => x.path.startsWith('.bolt')).find((x) => x.name == 'prompt');
if (templatePromptFile) {
userMessage = `
TEMPLATE INSTRUCTIONS:
${templatePromptFile.content}
IMPORTANT: Dont Forget to install the dependencies before running the app
---
`;
}
if (filesToImport.ignoreFile.length > 0) {
userMessage =
userMessage +
`
STRICT FILE ACCESS RULES - READ CAREFULLY:
The following files are READ-ONLY and must never be modified:
${filesToImport.ignoreFile.map((file) => `- ${file.path}`).join('\n')}
Permitted actions:
✓ Import these files as dependencies
✓ Read from these files
✓ Reference these files
Strictly forbidden actions:
❌ Modify any content within these files
❌ Delete these files
❌ Rename these files
❌ Move these files
❌ Create new versions of these files
❌ Suggest changes to these files
Any attempt to modify these protected files will result in immediate termination of the operation.
If you need to make changes to functionality, create new files instead of modifying the protected ones listed above.
---
`;
}
userMessage += `
---
template import is done, and you can now use the imported files,
edit only the files that need to be changed, and you can create new files as needed.
NO NOT EDIT/WRITE ANY FILES THAT ALREADY EXIST IN THE PROJECT AND DOES NOT NEED TO BE MODIFIED
---
Now that the Template is imported please continue with my original request
`;
return {
assistantMessage,
userMessage,
};
}

View File

@@ -60,7 +60,9 @@ export class BoltShell {
#webcontainer: WebContainer | undefined; #webcontainer: WebContainer | undefined;
#terminal: ITerminal | undefined; #terminal: ITerminal | undefined;
#process: WebContainerProcess | undefined; #process: WebContainerProcess | undefined;
executionState = atom<{ sessionId: string; active: boolean; executionPrms?: Promise<any> } | undefined>(); executionState = atom<
{ sessionId: string; active: boolean; executionPrms?: Promise<any>; abort?: () => void } | undefined
>();
#outputStream: ReadableStreamDefaultReader<string> | undefined; #outputStream: ReadableStreamDefaultReader<string> | undefined;
#shellInputStream: WritableStreamDefaultWriter<string> | undefined; #shellInputStream: WritableStreamDefaultWriter<string> | undefined;
@@ -93,18 +95,23 @@ export class BoltShell {
return this.#process; return this.#process;
} }
async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> { async executeCommand(sessionId: string, command: string, abort?: () => void): Promise<ExecutionResult> {
if (!this.process || !this.terminal) { if (!this.process || !this.terminal) {
return undefined; return undefined;
} }
const state = this.executionState.get(); const state = this.executionState.get();
if (state?.active && state.abort) {
state.abort();
}
/* /*
* interrupt the current execution * interrupt the current execution
* this.#shellInputStream?.write('\x03'); * this.#shellInputStream?.write('\x03');
*/ */
this.terminal.input('\x03'); this.terminal.input('\x03');
await this.waitTillOscCode('prompt');
if (state && state.executionPrms) { if (state && state.executionPrms) {
await state.executionPrms; await state.executionPrms;
@@ -115,11 +122,19 @@ export class BoltShell {
//wait for the execution to finish //wait for the execution to finish
const executionPromise = this.getCurrentExecutionResult(); const executionPromise = this.getCurrentExecutionResult();
this.executionState.set({ sessionId, active: true, executionPrms: executionPromise }); this.executionState.set({ sessionId, active: true, executionPrms: executionPromise, abort });
const resp = await executionPromise; const resp = await executionPromise;
this.executionState.set({ sessionId, active: false }); this.executionState.set({ sessionId, active: false });
if (resp) {
try {
resp.output = cleanTerminalOutput(resp.output);
} catch (error) {
console.log('failed to format terminal output', error);
}
}
return resp; return resp;
} }
@@ -215,6 +230,65 @@ export class BoltShell {
} }
} }
/**
* Cleans and formats terminal output while preserving structure and paths
* Handles ANSI, OSC, and various terminal control sequences
*/
export function cleanTerminalOutput(input: string): string {
// Step 1: Remove OSC sequences (including those with parameters)
const removeOsc = input
.replace(/\x1b\](\d+;[^\x07\x1b]*|\d+[^\x07\x1b]*)\x07/g, '')
.replace(/\](\d+;[^\n]*|\d+[^\n]*)/g, '');
// Step 2: Remove ANSI escape sequences and color codes more thoroughly
const removeAnsi = removeOsc
// Remove all escape sequences with parameters
.replace(/\u001b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
.replace(/\x1b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
// Remove color codes
.replace(/\u001b\[[0-9;]*m/g, '')
.replace(/\x1b\[[0-9;]*m/g, '')
// Clean up any remaining escape characters
.replace(/\u001b/g, '')
.replace(/\x1b/g, '');
// Step 3: Clean up carriage returns and newlines
const cleanNewlines = removeAnsi
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n{3,}/g, '\n\n');
// Step 4: Add newlines at key breakpoints while preserving paths
const formatOutput = cleanNewlines
// Preserve prompt line
.replace(/^([~\/][^\n]+)/m, '$1\n')
// Add newline before command output indicators
.replace(/(?<!^|\n)>/g, '\n>')
// Add newline before error keywords without breaking paths
.replace(/(?<!^|\n|\w)(error|failed|warning|Error|Failed|Warning):/g, '\n$1:')
// Add newline before 'at' in stack traces without breaking paths
.replace(/(?<!^|\n|\/)(at\s+(?!async|sync))/g, '\nat ')
// Ensure 'at async' stays on same line
.replace(/\bat\s+async/g, 'at async')
// Add newline before npm error indicators
.replace(/(?<!^|\n)(npm ERR!)/g, '\n$1');
// Step 5: Clean up whitespace while preserving intentional spacing
const cleanSpaces = formatOutput
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
// Step 6: Final cleanup
return cleanSpaces
.replace(/\n{3,}/g, '\n\n') // Replace multiple newlines with double newlines
.replace(/:\s+/g, ': ') // Normalize spacing after colons
.replace(/\s{2,}/g, ' ') // Remove multiple spaces
.replace(/^\s+|\s+$/g, '') // Trim start and end
.replace(/\u0000/g, ''); // Remove null characters
}
export function newBoltShellProcess() { export function newBoltShellProcess() {
return new BoltShell(); return new BoltShell();
} }

27
app/utils/stacktrace.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* Cleans webcontainer URLs from stack traces to show relative paths instead
*/
export function cleanStackTrace(stackTrace: string): string {
// Function to clean a single URL
const cleanUrl = (url: string): string => {
const regex = /^https?:\/\/[^\/]+\.webcontainer-api\.io(\/.*)?$/;
if (!regex.test(url)) {
return url;
}
const pathRegex = /^https?:\/\/[^\/]+\.webcontainer-api\.io\/(.*?)$/;
const match = url.match(pathRegex);
return match?.[1] || '';
};
// Split the stack trace into lines and process each line
return stackTrace
.split('\n')
.map((line) => {
// Match any URL in the line that contains webcontainer-api.io
return line.replace(/(https?:\/\/[^\/]+\.webcontainer-api\.io\/[^\s\)]+)/g, (match) => cleanUrl(match));
})
.join('\n');
}

View File

@@ -19,10 +19,3 @@ export interface OllamaModel {
export interface OllamaApiResponse { export interface OllamaApiResponse {
models: OllamaModel[]; models: OllamaModel[];
} }
export interface ModelInfo {
name: string;
label: string;
provider: string;
maxTokenAllowed: number;
}

Some files were not shown because too many files have changed in this diff Show More