codacus commited on
Commit
9f5c01f
·
unverified ·
2 Parent(s): 6467995 825137a

Merge branch 'main' into system-prompt-variations

Browse files
.env.example CHANGED
@@ -70,6 +70,11 @@ LMSTUDIO_API_BASE_URL=
70
  # You only need this environment variable set if you want to use xAI models
71
  XAI_API_KEY=
72
 
 
 
 
 
 
73
  # Include this environment variable if you want more logging for debugging locally
74
  VITE_LOG_LEVEL=debug
75
 
 
70
  # You only need this environment variable set if you want to use xAI models
71
  XAI_API_KEY=
72
 
73
+ # Get your Perplexity API Key here -
74
+ # https://www.perplexity.ai/settings/api
75
+ # You only need this environment variable set if you want to use Perplexity models
76
+ PERPLEXITY_API_KEY=
77
+
78
  # Include this environment variable if you want more logging for debugging locally
79
  VITE_LOG_LEVEL=debug
80
 
CONTRIBUTING.md CHANGED
@@ -1,6 +1,6 @@
1
- # Contributing to oTToDev
2
 
3
- 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.
4
 
5
  ## 📋 Table of Contents
6
  - [Code of Conduct](#code-of-conduct)
 
1
+ # Contributing to bolt.diy
2
 
3
+ 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.
4
 
5
  ## 📋 Table of Contents
6
  - [Code of Conduct](#code-of-conduct)
FAQ.md CHANGED
@@ -1,12 +1,12 @@
1
- [![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)
2
 
3
- # Bolt.new Fork by Cole Medin - Bolt.diy
4
 
5
  ## FAQ
6
 
7
- ### How do I get the best results with Bolt.diy?
8
 
9
- - **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.
10
 
11
  - **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.
12
 
@@ -14,36 +14,29 @@
14
 
15
  - **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.
16
 
17
- ### Do you plan on merging Bolt.diy back into the official Bolt.new repo?
18
-
19
- More news coming on this coming early next month - stay tuned!
20
-
21
  ### Why are there so many open issues/pull requests?
22
 
23
- 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
24
- 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.
25
- 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
26
- 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!
27
 
28
- ### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for Bolt.diy/Bolt.new?
29
 
30
  As much as the gap is quickly closing between open source and massive close source models, you’re 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!
31
 
32
  ### I'm getting the error: "There was an error processing this request"
33
 
34
- 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.
35
 
36
  ### I'm getting the error: "x-api-key header missing"
37
 
38
- 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 didn’t run first. We are still on the hunt for why this happens once and a while!
39
 
40
- ### I'm getting a blank preview when Bolt.diy runs my app!
41
 
42
- 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 don’t 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.
43
 
44
  ### How to add a LLM:
45
 
46
- 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.
47
 
48
  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!
49
 
 
1
+ [![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
2
 
3
+ # bolt.diy
4
 
5
  ## FAQ
6
 
7
+ ### How do I get the best results with bolt.diy?
8
 
9
+ - **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.
10
 
11
  - **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.
12
 
 
14
 
15
  - **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.
16
 
 
 
 
 
17
  ### Why are there so many open issues/pull requests?
18
 
19
+ 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!
 
 
 
20
 
21
+ ### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for bolt.diy/bolt.new?
22
 
23
  As much as the gap is quickly closing between open source and massive close source models, you’re 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!
24
 
25
  ### I'm getting the error: "There was an error processing this request"
26
 
27
+ 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.
28
 
29
  ### I'm getting the error: "x-api-key header missing"
30
 
31
+ 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 didn’t run first. We are still on the hunt for why this happens once and a while!
32
 
33
+ ### I'm getting a blank preview when bolt.diy runs my app!
34
 
35
+ 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 don’t 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.
36
 
37
  ### How to add a LLM:
38
 
39
+ 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.
40
 
41
  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!
42
 
README.md CHANGED
@@ -1,14 +1,14 @@
1
- [![Bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
2
 
3
- # Bolt.diy (Previously oTToDev)
4
 
5
- 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.
6
 
7
- Check the [Bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more information. This documentation is still being updated after the transfer.
8
 
9
- 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!
10
 
11
- ## Join the community for Bolt.diy!
12
 
13
  https://thinktank.ottomator.ai
14
 
@@ -20,7 +20,7 @@ https://thinktank.ottomator.ai
20
  - ✅ Autogenerate Ollama models from what is downloaded (@yunatamos)
21
  - ✅ Filter models by provider (@jasonm23)
22
  - ✅ Download project as ZIP (@fabwaseem)
23
- - ✅ Improvements to the main Bolt.new prompt in `app\lib\.server\llm\prompts.ts` (@kofi-bhr)
24
  - ✅ DeepSeek API Integration (@zenith110)
25
  - ✅ Mistral API Integration (@ArulGandhi)
26
  - ✅ "Open AI Like" API Integration (@ZerxZ)
@@ -47,7 +47,8 @@ https://thinktank.ottomator.ai
47
  - ✅ Added Git Clone button (@thecodacus)
48
  - ✅ Git Import from url (@thecodacus)
49
  - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
50
- - **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
 
51
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
52
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
53
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
@@ -59,7 +60,7 @@ https://thinktank.ottomator.ai
59
  - ⬜ Perplexity Integration
60
  - ⬜ Vertex AI Integration
61
 
62
- ## Bolt.diy Features
63
 
64
  - **AI-powered full-stack web development** directly in your browser.
65
  - **Support for multiple LLMs** with an extensible architecture to integrate additional models.
@@ -69,7 +70,7 @@ https://thinktank.ottomator.ai
69
  - **Download projects as ZIP** for easy portability.
70
  - **Integration-ready Docker support** for a hassle-free setup.
71
 
72
- ## Setup Bolt.diy
73
 
74
  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.
75
 
@@ -177,7 +178,7 @@ DEFAULT_NUM_CTX=8192
177
 
178
  ### Update Your Local Version to the Latest
179
 
180
- To keep your local version of Bolt.diy up to date with the latest changes, follow these steps for your operating system:
181
 
182
  #### 1. **Navigate to your project folder**
183
  Navigate to the directory where you cloned the repository and open a terminal:
@@ -203,34 +204,36 @@ To keep your local version of Bolt.diy up to date with the latest changes, follo
203
  pnpm run dev
204
  ```
205
 
206
- This ensures that you're running the latest version of Bolt.diy and can take advantage of all the newest features and bug fixes.
207
 
208
  ---
209
 
210
- ## Available Scripts
211
 
212
- Here are the available commands for managing the application:
 
 
 
 
 
 
 
 
213
 
214
- - `pnpm run dev`: Start the development server.
215
- - `pnpm run build`: Build the project.
216
- - `pnpm run start`: Run the built application locally (uses Wrangler Pages).
217
- - `pnpm run preview`: Build and start the application locally for production testing.
218
- - `pnpm test`: Run the test suite using Vitest.
219
- - `pnpm run typecheck`: Perform TypeScript type checking.
220
- - `pnpm run typegen`: Generate TypeScript types using Wrangler.
221
- - `pnpm run deploy`: Build and deploy the project to Cloudflare Pages.
222
- - `pnpm lint:fix`: Run the linter and automatically fix issues.
223
 
224
- ## How do I contribute to Bolt.diy?
225
 
226
- [Please check out our dedicated page for contributing to Bolt.diy here!](CONTRIBUTING.md)
227
 
228
- ## What are the future plans for Bolt.diy?
229
 
230
- [Check out our Roadmap here!](https://roadmap.sh/r/ottodev-roadmap-2ovzo)
231
 
232
- Lot more updates to this roadmap coming soon!
233
 
234
  ## FAQ
235
 
236
- [Please check out our dedicated page for FAQ's related to Bolt.diy here!](FAQ.md)
 
1
+ [![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
2
 
3
+ # bolt.diy (Previously oTToDev)
4
 
5
+ 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.
6
 
7
+ Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more information. This documentation is still being updated after the transfer.
8
 
9
+ 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!
10
 
11
+ ## Join the community for bolt.diy!
12
 
13
  https://thinktank.ottomator.ai
14
 
 
20
  - ✅ Autogenerate Ollama models from what is downloaded (@yunatamos)
21
  - ✅ Filter models by provider (@jasonm23)
22
  - ✅ Download project as ZIP (@fabwaseem)
23
+ - ✅ Improvements to the main bolt.new prompt in `app\lib\.server\llm\prompts.ts` (@kofi-bhr)
24
  - ✅ DeepSeek API Integration (@zenith110)
25
  - ✅ Mistral API Integration (@ArulGandhi)
26
  - ✅ "Open AI Like" API Integration (@ZerxZ)
 
47
  - ✅ Added Git Clone button (@thecodacus)
48
  - ✅ Git Import from url (@thecodacus)
49
  - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
50
+ - Selection tool to target changes visually (@emcconnell)
51
+ - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
52
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
53
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
54
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
 
60
  - ⬜ Perplexity Integration
61
  - ⬜ Vertex AI Integration
62
 
63
+ ## bolt.diy Features
64
 
65
  - **AI-powered full-stack web development** directly in your browser.
66
  - **Support for multiple LLMs** with an extensible architecture to integrate additional models.
 
70
  - **Download projects as ZIP** for easy portability.
71
  - **Integration-ready Docker support** for a hassle-free setup.
72
 
73
+ ## Setup bolt.diy
74
 
75
  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.
76
 
 
178
 
179
  ### Update Your Local Version to the Latest
180
 
181
+ To keep your local version of bolt.diy up to date with the latest changes, follow these steps for your operating system:
182
 
183
  #### 1. **Navigate to your project folder**
184
  Navigate to the directory where you cloned the repository and open a terminal:
 
204
  pnpm run dev
205
  ```
206
 
207
+ This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes.
208
 
209
  ---
210
 
211
+ ## Available Scripts
212
 
213
+ - **`pnpm run dev`**: Starts the development server.
214
+ - **`pnpm run build`**: Builds the project.
215
+ - **`pnpm run start`**: Runs the built application locally using Wrangler Pages.
216
+ - **`pnpm run preview`**: Builds and runs the production build locally.
217
+ - **`pnpm test`**: Runs the test suite using Vitest.
218
+ - **`pnpm run typecheck`**: Runs TypeScript type checking.
219
+ - **`pnpm run typegen`**: Generates TypeScript types using Wrangler.
220
+ - **`pnpm run deploy`**: Deploys the project to Cloudflare Pages.
221
+ - **`pnpm run lint:fix`**: Automatically fixes linting issues.
222
 
223
+ ---
224
+
225
+ ## Contributing
 
 
 
 
 
 
226
 
227
+ We welcome contributions! Check out our [Contributing Guide](CONTRIBUTING.md) to get started.
228
 
229
+ ---
230
 
231
+ ## Roadmap
232
 
233
+ Explore upcoming features and priorities on our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo).
234
 
235
+ ---
236
 
237
  ## FAQ
238
 
239
+ For answers to common questions, visit our [FAQ Page](FAQ.md).
app/commit.json CHANGED
@@ -1 +1 @@
1
- { "commit": "2b8236f9880984508f2ae17575b587bb9d938e55" }
 
1
+ { "commit": "9cd9ee9088467882e1e4efdf491959619307cc9d" }
app/components/chat/BaseChat.tsx CHANGED
@@ -26,6 +26,8 @@ import FilePreview from './FilePreview';
26
  import { ModelSelector } from '~/components/chat/ModelSelector';
27
  import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
 
 
29
 
30
  const TEXTAREA_MIN_HEIGHT = 76;
31
 
@@ -376,6 +378,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
376
  setImageDataList?.(imageDataList.filter((_, i) => i !== index));
377
  }}
378
  />
 
 
 
 
 
 
 
 
 
 
379
  <div
380
  className={classNames(
381
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
@@ -431,6 +443,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
431
  return;
432
  }
433
 
 
 
 
 
 
434
  handleSendMessage?.(event);
435
  }
436
  }}
@@ -476,22 +493,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
476
  className={classNames(
477
  'transition-all',
478
  enhancingPrompt ? 'opacity-100' : '',
479
- promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
480
- promptEnhanced ? 'pr-1.5' : '',
481
- promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
482
  )}
483
- onClick={() => enhancePrompt?.()}
 
 
 
484
  >
485
  {enhancingPrompt ? (
486
- <>
487
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
488
- <div className="ml-1.5">Enhancing prompt...</div>
489
- </>
490
  ) : (
491
- <>
492
- <div className="i-bolt:stars text-xl"></div>
493
- {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
494
- </>
495
  )}
496
  </IconButton>
497
 
 
26
  import { ModelSelector } from '~/components/chat/ModelSelector';
27
  import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
29
+ import { ScreenshotStateManager } from './ScreenshotStateManager';
30
+ import { toast } from 'react-toastify';
31
 
32
  const TEXTAREA_MIN_HEIGHT = 76;
33
 
 
378
  setImageDataList?.(imageDataList.filter((_, i) => i !== index));
379
  }}
380
  />
381
+ <ClientOnly>
382
+ {() => (
383
+ <ScreenshotStateManager
384
+ setUploadedFiles={setUploadedFiles}
385
+ setImageDataList={setImageDataList}
386
+ uploadedFiles={uploadedFiles}
387
+ imageDataList={imageDataList}
388
+ />
389
+ )}
390
+ </ClientOnly>
391
  <div
392
  className={classNames(
393
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
 
443
  return;
444
  }
445
 
446
+ // ignore if using input method engine
447
+ if (event.nativeEvent.isComposing) {
448
+ return;
449
+ }
450
+
451
  handleSendMessage?.(event);
452
  }
453
  }}
 
493
  className={classNames(
494
  'transition-all',
495
  enhancingPrompt ? 'opacity-100' : '',
 
 
 
496
  )}
497
+ onClick={() => {
498
+ enhancePrompt?.();
499
+ toast.success('Prompt enhanced!');
500
+ }}
501
  >
502
  {enhancingPrompt ? (
503
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
 
 
 
504
  ) : (
505
+ <div className="i-bolt:stars text-xl"></div>
 
 
 
506
  )}
507
  </IconButton>
508
 
app/components/chat/ScreenshotStateManager.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react';
2
+
3
+ interface ScreenshotStateManagerProps {
4
+ setUploadedFiles?: (files: File[]) => void;
5
+ setImageDataList?: (dataList: string[]) => void;
6
+ uploadedFiles: File[];
7
+ imageDataList: string[];
8
+ }
9
+
10
+ export const ScreenshotStateManager = ({
11
+ setUploadedFiles,
12
+ setImageDataList,
13
+ uploadedFiles,
14
+ imageDataList,
15
+ }: ScreenshotStateManagerProps) => {
16
+ useEffect(() => {
17
+ if (setUploadedFiles && setImageDataList) {
18
+ (window as any).__BOLT_SET_UPLOADED_FILES__ = setUploadedFiles;
19
+ (window as any).__BOLT_SET_IMAGE_DATA_LIST__ = setImageDataList;
20
+ (window as any).__BOLT_UPLOADED_FILES__ = uploadedFiles;
21
+ (window as any).__BOLT_IMAGE_DATA_LIST__ = imageDataList;
22
+ }
23
+
24
+ return () => {
25
+ delete (window as any).__BOLT_SET_UPLOADED_FILES__;
26
+ delete (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
27
+ delete (window as any).__BOLT_UPLOADED_FILES__;
28
+ delete (window as any).__BOLT_IMAGE_DATA_LIST__;
29
+ };
30
+ }, [setUploadedFiles, setImageDataList, uploadedFiles, imageDataList]);
31
+
32
+ return null;
33
+ };
app/components/settings/SettingsWindow.tsx CHANGED
@@ -27,8 +27,8 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
27
  const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
28
  { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: <ChatHistoryTab /> },
29
  { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
30
- { id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
31
  { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
 
32
  ...(debug
33
  ? [
34
  {
 
27
  const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
28
  { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: <ChatHistoryTab /> },
29
  { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
 
30
  { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
31
+ { id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
32
  ...(debug
33
  ? [
34
  {
app/components/settings/chat-history/ChatHistoryTab.tsx CHANGED
@@ -22,17 +22,20 @@ export default function ChatHistoryTab() {
22
  };
23
 
24
  const handleDeleteAllChats = async () => {
 
 
 
 
 
25
  if (!db) {
26
  const error = new Error('Database is not available');
27
  logStore.logError('Failed to delete chats - DB unavailable', error);
28
  toast.error('Database is not available');
29
-
30
  return;
31
  }
32
 
33
  try {
34
  setIsDeleting(true);
35
-
36
  const allChats = await getAll(db);
37
  await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
38
  logStore.logSystem('All chats deleted successfully', { count: allChats.length });
@@ -52,7 +55,6 @@ export default function ChatHistoryTab() {
52
  const error = new Error('Database is not available');
53
  logStore.logError('Failed to export chats - DB unavailable', error);
54
  toast.error('Database is not available');
55
-
56
  return;
57
  }
58
 
 
22
  };
23
 
24
  const handleDeleteAllChats = async () => {
25
+ const confirmDelete = window.confirm("Are you sure you want to delete all chats? This action cannot be undone.");
26
+ if (!confirmDelete) {
27
+ return; // Exit if the user cancels
28
+ }
29
+
30
  if (!db) {
31
  const error = new Error('Database is not available');
32
  logStore.logError('Failed to delete chats - DB unavailable', error);
33
  toast.error('Database is not available');
 
34
  return;
35
  }
36
 
37
  try {
38
  setIsDeleting(true);
 
39
  const allChats = await getAll(db);
40
  await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
41
  logStore.logSystem('All chats deleted successfully', { count: allChats.length });
 
55
  const error = new Error('Database is not available');
56
  logStore.logError('Failed to export chats - DB unavailable', error);
57
  toast.error('Database is not available');
 
58
  return;
59
  }
60
 
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSettings } from '~/lib/hooks/useSettings';
3
  import commit from '~/commit.json';
 
4
 
5
  interface ProviderStatus {
6
  name: string;
@@ -308,8 +309,9 @@ export default function DebugTab() {
308
  Version: versionHash,
309
  Timestamp: new Date().toISOString(),
310
  };
 
311
  navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
312
- alert('Debug information copied to clipboard!');
313
  });
314
  }, [activeProviders, systemInfo]);
315
 
 
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSettings } from '~/lib/hooks/useSettings';
3
  import commit from '~/commit.json';
4
+ import { toast } from 'react-toastify';
5
 
6
  interface ProviderStatus {
7
  name: string;
 
309
  Version: versionHash,
310
  Timestamp: new Date().toISOString(),
311
  };
312
+
313
  navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
314
+ toast.success('Debug information copied to clipboard!');
315
  });
316
  }, [activeProviders, systemInfo]);
317
 
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -4,19 +4,21 @@ import { PromptLibrary } from '~/lib/common/prompt-library';
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
 
6
  export default function FeaturesTab() {
 
7
  const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs, promptId, setPromptId } =
8
  useSettings();
 
 
 
 
 
9
  return (
10
  <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
11
  <div className="mb-6">
12
  <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
13
- <div className="flex items-center justify-between mb-6">
14
- <span className="text-bolt-elements-textPrimary">Debug Info</span>
15
- <Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
16
- </div>
17
- <div className="flex items-center justify-between mb-6">
18
- <span className="text-bolt-elements-textPrimary">Event Logs</span>
19
- <Switch className="ml-auto" checked={eventLogs} onCheckedChange={enableEventLogs} />
20
  </div>
21
  </div>
22
 
@@ -25,8 +27,9 @@ export default function FeaturesTab() {
25
  <p className="text-sm text-bolt-elements-textSecondary mb-4">
26
  Disclaimer: Experimental features may be unstable and are subject to change.
27
  </p>
28
- <div className="flex items-center justify-between pt-4 mb-4">
29
- <span className="text-bolt-elements-textPrimary">Enable Local Models</span>
 
30
  <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
31
  </div>
32
  <div className="flex items-start justify-between pt-4 mb-2 gap-2">
 
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
 
6
  export default function FeaturesTab() {
7
+
8
  const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs, promptId, setPromptId } =
9
  useSettings();
10
+ const handleToggle = (enabled: boolean) => {
11
+ enableDebugMode(enabled);
12
+ enableEventLogs(enabled);
13
+ };
14
+
15
  return (
16
  <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
17
  <div className="mb-6">
18
  <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
19
+ <div className="flex items-center justify-between mb-2">
20
+ <span className="text-bolt-elements-textPrimary">Debug Features</span>
21
+ <Switch className="ml-auto" checked={debug} onCheckedChange={handleToggle} />
 
 
 
 
22
  </div>
23
  </div>
24
 
 
27
  <p className="text-sm text-bolt-elements-textSecondary mb-4">
28
  Disclaimer: Experimental features may be unstable and are subject to change.
29
  </p>
30
+
31
+ <div className="flex items-center justify-between mb-2">
32
+ <span className="text-bolt-elements-textPrimary">Experimental Providers</span>
33
  <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
34
  </div>
35
  <div className="flex items-start justify-between pt-4 mb-2 gap-2">
app/components/settings/providers/ProvidersTab.tsx CHANGED
@@ -5,6 +5,9 @@ import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settin
5
  import type { IProviderConfig } from '~/types/model';
6
  import { logStore } from '~/lib/stores/logs';
7
 
 
 
 
8
  export default function ProvidersTab() {
9
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
10
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
@@ -51,7 +54,14 @@ export default function ProvidersTab() {
51
  >
52
  <div className="flex items-center justify-between mb-2">
53
  <div className="flex items-center gap-2">
54
- <img src={`/icons/${provider.name}.svg`} alt={`${provider.name} icon`} className="w-6 h-6 dark:invert" />
 
 
 
 
 
 
 
55
  <span className="text-bolt-elements-textPrimary">{provider.name}</span>
56
  </div>
57
  <Switch
 
5
  import type { IProviderConfig } from '~/types/model';
6
  import { logStore } from '~/lib/stores/logs';
7
 
8
+ // Import a default fallback icon
9
+ import DefaultIcon from '/icons/Ollama.svg'; // Adjust the path as necessary
10
+
11
  export default function ProvidersTab() {
12
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
13
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
 
54
  >
55
  <div className="flex items-center justify-between mb-2">
56
  <div className="flex items-center gap-2">
57
+ <img
58
+ src={`/icons/${provider.name}.svg`} // Attempt to load the specific icon
59
+ onError={(e) => { // Fallback to default icon on error
60
+ e.currentTarget.src = DefaultIcon;
61
+ }}
62
+ alt={`${provider.name} icon`}
63
+ className="w-6 h-6 dark:invert"
64
+ />
65
  <span className="text-bolt-elements-textPrimary">{provider.name}</span>
66
  </div>
67
  <Switch
app/components/workbench/FileTree.tsx CHANGED
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
2
  import type { FileMap } from '~/lib/stores/files';
3
  import { classNames } from '~/utils/classNames';
4
  import { createScopedLogger, renderLogger } from '~/utils/logger';
 
5
 
6
  const logger = createScopedLogger('FileTree');
7
 
@@ -110,6 +111,22 @@ export const FileTree = memo(
110
  });
111
  };
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  return (
114
  <div className={classNames('text-sm', className, 'overflow-y-auto')}>
115
  {filteredFileList.map((fileOrFolder) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
121
  selected={selectedFile === fileOrFolder.fullPath}
122
  file={fileOrFolder}
123
  unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
 
 
 
 
 
 
124
  onClick={() => {
125
  onFileSelect?.(fileOrFolder.fullPath);
126
  }}
@@ -134,6 +157,12 @@ export const FileTree = memo(
134
  folder={fileOrFolder}
135
  selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
136
  collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
 
 
 
 
 
 
137
  onClick={() => {
138
  toggleCollapseState(fileOrFolder.fullPath);
139
  }}
@@ -156,26 +185,67 @@ interface FolderProps {
156
  folder: FolderNode;
157
  collapsed: boolean;
158
  selected?: boolean;
 
 
159
  onClick: () => void;
160
  }
161
 
162
- function Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {
 
 
 
 
 
 
163
  return (
164
- <NodeButton
165
- className={classNames('group', {
166
- 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
167
- !selected,
168
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
169
- })}
170
- depth={depth}
171
- iconClasses={classNames({
172
- 'i-ph:caret-right scale-98': collapsed,
173
- 'i-ph:caret-down scale-98': !collapsed,
174
- })}
175
- onClick={onClick}
176
  >
177
- {name}
178
- </NodeButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  );
180
  }
181
 
@@ -183,31 +253,43 @@ interface FileProps {
183
  file: FileNode;
184
  selected: boolean;
185
  unsavedChanges?: boolean;
 
 
186
  onClick: () => void;
187
  }
188
 
189
- function File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {
 
 
 
 
 
 
 
190
  return (
191
- <NodeButton
192
- className={classNames('group', {
193
- 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,
194
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
195
- })}
196
- depth={depth}
197
- iconClasses={classNames('i-ph:file-duotone scale-98', {
198
- 'group-hover:text-bolt-elements-item-contentActive': !selected,
199
- })}
200
- onClick={onClick}
201
- >
202
- <div
203
- className={classNames('flex items-center', {
204
  'group-hover:text-bolt-elements-item-contentActive': !selected,
205
  })}
 
206
  >
207
- <div className="flex-1 truncate pr-2">{name}</div>
208
- {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
209
- </div>
210
- </NodeButton>
 
 
 
 
 
 
211
  );
212
  }
213
 
 
2
  import type { FileMap } from '~/lib/stores/files';
3
  import { classNames } from '~/utils/classNames';
4
  import { createScopedLogger, renderLogger } from '~/utils/logger';
5
+ import * as ContextMenu from '@radix-ui/react-context-menu';
6
 
7
  const logger = createScopedLogger('FileTree');
8
 
 
111
  });
112
  };
113
 
114
+ const onCopyPath = (fileOrFolder: FileNode | FolderNode) => {
115
+ try {
116
+ navigator.clipboard.writeText(fileOrFolder.fullPath);
117
+ } catch (error) {
118
+ logger.error(error);
119
+ }
120
+ };
121
+
122
+ const onCopyRelativePath = (fileOrFolder: FileNode | FolderNode) => {
123
+ try {
124
+ navigator.clipboard.writeText(fileOrFolder.fullPath.substring((rootFolder || '').length));
125
+ } catch (error) {
126
+ logger.error(error);
127
+ }
128
+ };
129
+
130
  return (
131
  <div className={classNames('text-sm', className, 'overflow-y-auto')}>
132
  {filteredFileList.map((fileOrFolder) => {
 
138
  selected={selectedFile === fileOrFolder.fullPath}
139
  file={fileOrFolder}
140
  unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
141
+ onCopyPath={() => {
142
+ onCopyPath(fileOrFolder);
143
+ }}
144
+ onCopyRelativePath={() => {
145
+ onCopyRelativePath(fileOrFolder);
146
+ }}
147
  onClick={() => {
148
  onFileSelect?.(fileOrFolder.fullPath);
149
  }}
 
157
  folder={fileOrFolder}
158
  selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
159
  collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
160
+ onCopyPath={() => {
161
+ onCopyPath(fileOrFolder);
162
+ }}
163
+ onCopyRelativePath={() => {
164
+ onCopyRelativePath(fileOrFolder);
165
+ }}
166
  onClick={() => {
167
  toggleCollapseState(fileOrFolder.fullPath);
168
  }}
 
185
  folder: FolderNode;
186
  collapsed: boolean;
187
  selected?: boolean;
188
+ onCopyPath: () => void;
189
+ onCopyRelativePath: () => void;
190
  onClick: () => void;
191
  }
192
 
193
+ interface FolderContextMenuProps {
194
+ onCopyPath?: () => void;
195
+ onCopyRelativePath?: () => void;
196
+ children: ReactNode;
197
+ }
198
+
199
+ function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; children: ReactNode }) {
200
  return (
201
+ <ContextMenu.Item
202
+ onSelect={onSelect}
203
+ className="flex items-center gap-2 px-2 py-1.5 outline-0 text-sm text-bolt-elements-textPrimary cursor-pointer ws-nowrap text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive rounded-md"
 
 
 
 
 
 
 
 
 
204
  >
205
+ <span className="size-4 shrink-0"></span>
206
+ <span>{children}</span>
207
+ </ContextMenu.Item>
208
+ );
209
+ }
210
+
211
+ function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) {
212
+ return (
213
+ <ContextMenu.Root>
214
+ <ContextMenu.Trigger>{children}</ContextMenu.Trigger>
215
+ <ContextMenu.Portal>
216
+ <ContextMenu.Content
217
+ style={{ zIndex: 998 }}
218
+ className="border border-bolt-elements-borderColor rounded-md z-context-menu bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2 data-[state=open]:animate-in animate-duration-100 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-98 w-56"
219
+ >
220
+ <ContextMenu.Group className="p-1 border-b-px border-solid border-bolt-elements-borderColor">
221
+ <ContextMenuItem onSelect={onCopyPath}>Copy path</ContextMenuItem>
222
+ <ContextMenuItem onSelect={onCopyRelativePath}>Copy relative path</ContextMenuItem>
223
+ </ContextMenu.Group>
224
+ </ContextMenu.Content>
225
+ </ContextMenu.Portal>
226
+ </ContextMenu.Root>
227
+ );
228
+ }
229
+
230
+ function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) {
231
+ return (
232
+ <FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
233
+ <NodeButton
234
+ className={classNames('group', {
235
+ 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
236
+ !selected,
237
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
238
+ })}
239
+ depth={folder.depth}
240
+ iconClasses={classNames({
241
+ 'i-ph:caret-right scale-98': collapsed,
242
+ 'i-ph:caret-down scale-98': !collapsed,
243
+ })}
244
+ onClick={onClick}
245
+ >
246
+ {folder.name}
247
+ </NodeButton>
248
+ </FileContextMenu>
249
  );
250
  }
251
 
 
253
  file: FileNode;
254
  selected: boolean;
255
  unsavedChanges?: boolean;
256
+ onCopyPath: () => void;
257
+ onCopyRelativePath: () => void;
258
  onClick: () => void;
259
  }
260
 
261
+ function File({
262
+ file: { depth, name },
263
+ onClick,
264
+ onCopyPath,
265
+ onCopyRelativePath,
266
+ selected,
267
+ unsavedChanges = false,
268
+ }: FileProps) {
269
  return (
270
+ <FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
271
+ <NodeButton
272
+ className={classNames('group', {
273
+ 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault':
274
+ !selected,
275
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
276
+ })}
277
+ depth={depth}
278
+ iconClasses={classNames('i-ph:file-duotone scale-98', {
 
 
 
 
279
  'group-hover:text-bolt-elements-item-contentActive': !selected,
280
  })}
281
+ onClick={onClick}
282
  >
283
+ <div
284
+ className={classNames('flex items-center', {
285
+ 'group-hover:text-bolt-elements-item-contentActive': !selected,
286
+ })}
287
+ >
288
+ <div className="flex-1 truncate pr-2">{name}</div>
289
+ {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
290
+ </div>
291
+ </NodeButton>
292
+ </FileContextMenu>
293
  );
294
  }
295
 
app/components/workbench/Preview.tsx CHANGED
@@ -3,6 +3,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
3
  import { IconButton } from '~/components/ui/IconButton';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
 
6
 
7
  type ResizeSide = 'left' | 'right' | null;
8
 
@@ -20,6 +21,7 @@ export const Preview = memo(() => {
20
 
21
  const [url, setUrl] = useState('');
22
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
 
23
 
24
  // Toggle between responsive mode and device mode
25
  const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
@@ -218,12 +220,17 @@ export const Preview = memo(() => {
218
  )}
219
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
220
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
221
-
 
 
 
 
222
  <div
223
  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
224
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
225
  >
226
  <input
 
227
  ref={inputRef}
228
  className="w-full bg-transparent outline-none"
229
  type="text"
@@ -281,7 +288,20 @@ export const Preview = memo(() => {
281
  }}
282
  >
283
  {activePreview ? (
284
- <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} allowFullScreen />
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  ) : (
286
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
287
  )}
 
3
  import { IconButton } from '~/components/ui/IconButton';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
6
+ import { ScreenshotSelector } from './ScreenshotSelector';
7
 
8
  type ResizeSide = 'left' | 'right' | null;
9
 
 
21
 
22
  const [url, setUrl] = useState('');
23
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
24
+ const [isSelectionMode, setIsSelectionMode] = useState(false);
25
 
26
  // Toggle between responsive mode and device mode
27
  const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
 
220
  )}
221
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
222
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
223
+ <IconButton
224
+ icon="i-ph:selection"
225
+ onClick={() => setIsSelectionMode(!isSelectionMode)}
226
+ className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
227
+ />
228
  <div
229
  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
230
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
231
  >
232
  <input
233
+ title="URL"
234
  ref={inputRef}
235
  className="w-full bg-transparent outline-none"
236
  type="text"
 
288
  }}
289
  >
290
  {activePreview ? (
291
+ <>
292
+ <iframe
293
+ ref={iframeRef}
294
+ title="preview"
295
+ className="border-none w-full h-full bg-white"
296
+ src={iframeUrl}
297
+ allowFullScreen
298
+ />
299
+ <ScreenshotSelector
300
+ isSelectionMode={isSelectionMode}
301
+ setIsSelectionMode={setIsSelectionMode}
302
+ containerRef={iframeRef}
303
+ />
304
+ </>
305
  ) : (
306
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
307
  )}
app/components/workbench/ScreenshotSelector.tsx ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+
4
+ interface ScreenshotSelectorProps {
5
+ isSelectionMode: boolean;
6
+ setIsSelectionMode: (mode: boolean) => void;
7
+ containerRef: React.RefObject<HTMLElement>;
8
+ }
9
+
10
+ export const ScreenshotSelector = memo(
11
+ ({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => {
12
+ const [isCapturing, setIsCapturing] = useState(false);
13
+ const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
14
+ const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
15
+ const mediaStreamRef = useRef<MediaStream | null>(null);
16
+ const videoRef = useRef<HTMLVideoElement | null>(null);
17
+
18
+ useEffect(() => {
19
+ // Cleanup function to stop all tracks when component unmounts
20
+ return () => {
21
+ if (videoRef.current) {
22
+ videoRef.current.pause();
23
+ videoRef.current.srcObject = null;
24
+ videoRef.current.remove();
25
+ videoRef.current = null;
26
+ }
27
+
28
+ if (mediaStreamRef.current) {
29
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
30
+ mediaStreamRef.current = null;
31
+ }
32
+ };
33
+ }, []);
34
+
35
+ const initializeStream = async () => {
36
+ if (!mediaStreamRef.current) {
37
+ try {
38
+ const stream = await navigator.mediaDevices.getDisplayMedia({
39
+ audio: false,
40
+ video: {
41
+ displaySurface: 'window',
42
+ preferCurrentTab: true,
43
+ surfaceSwitching: 'include',
44
+ systemAudio: 'exclude',
45
+ },
46
+ } as MediaStreamConstraints);
47
+
48
+ // Add handler for when sharing stops
49
+ stream.addEventListener('inactive', () => {
50
+ if (videoRef.current) {
51
+ videoRef.current.pause();
52
+ videoRef.current.srcObject = null;
53
+ videoRef.current.remove();
54
+ videoRef.current = null;
55
+ }
56
+
57
+ if (mediaStreamRef.current) {
58
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
59
+ mediaStreamRef.current = null;
60
+ }
61
+
62
+ setIsSelectionMode(false);
63
+ setSelectionStart(null);
64
+ setSelectionEnd(null);
65
+ setIsCapturing(false);
66
+ });
67
+
68
+ mediaStreamRef.current = stream;
69
+
70
+ // Initialize video element if needed
71
+ if (!videoRef.current) {
72
+ const video = document.createElement('video');
73
+ video.style.opacity = '0';
74
+ video.style.position = 'fixed';
75
+ video.style.pointerEvents = 'none';
76
+ video.style.zIndex = '-1';
77
+ document.body.appendChild(video);
78
+ videoRef.current = video;
79
+ }
80
+
81
+ // Set up video with the stream
82
+ videoRef.current.srcObject = stream;
83
+ await videoRef.current.play();
84
+ } catch (error) {
85
+ console.error('Failed to initialize stream:', error);
86
+ setIsSelectionMode(false);
87
+ toast.error('Failed to initialize screen capture');
88
+ }
89
+ }
90
+
91
+ return mediaStreamRef.current;
92
+ };
93
+
94
+ const handleCopySelection = useCallback(async () => {
95
+ if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) {
96
+ return;
97
+ }
98
+
99
+ setIsCapturing(true);
100
+
101
+ try {
102
+ const stream = await initializeStream();
103
+
104
+ if (!stream || !videoRef.current) {
105
+ return;
106
+ }
107
+
108
+ // Wait for video to be ready
109
+ await new Promise((resolve) => setTimeout(resolve, 300));
110
+
111
+ // Create temporary canvas for full screenshot
112
+ const tempCanvas = document.createElement('canvas');
113
+ tempCanvas.width = videoRef.current.videoWidth;
114
+ tempCanvas.height = videoRef.current.videoHeight;
115
+
116
+ const tempCtx = tempCanvas.getContext('2d');
117
+
118
+ if (!tempCtx) {
119
+ throw new Error('Failed to get temporary canvas context');
120
+ }
121
+
122
+ // Draw the full video frame
123
+ tempCtx.drawImage(videoRef.current, 0, 0);
124
+
125
+ // Calculate scale factor between video and screen
126
+ const scaleX = videoRef.current.videoWidth / window.innerWidth;
127
+ const scaleY = videoRef.current.videoHeight / window.innerHeight;
128
+
129
+ // Get window scroll position
130
+ const scrollX = window.scrollX;
131
+ const scrollY = window.scrollY + 40;
132
+
133
+ // Get the container's position in the page
134
+ const containerRect = containerRef.current.getBoundingClientRect();
135
+
136
+ // Offset adjustments for more accurate clipping
137
+ const leftOffset = -9; // Adjust left position
138
+ const bottomOffset = -14; // Adjust bottom position
139
+
140
+ // Calculate the scaled coordinates with scroll offset and adjustments
141
+ const scaledX = Math.round(
142
+ (containerRect.left + Math.min(selectionStart.x, selectionEnd.x) + scrollX + leftOffset) * scaleX,
143
+ );
144
+ const scaledY = Math.round(
145
+ (containerRect.top + Math.min(selectionStart.y, selectionEnd.y) + scrollY + bottomOffset) * scaleY,
146
+ );
147
+ const scaledWidth = Math.round(Math.abs(selectionEnd.x - selectionStart.x) * scaleX);
148
+ const scaledHeight = Math.round(Math.abs(selectionEnd.y - selectionStart.y) * scaleY);
149
+
150
+ // Create final canvas for the cropped area
151
+ const canvas = document.createElement('canvas');
152
+ canvas.width = Math.round(Math.abs(selectionEnd.x - selectionStart.x));
153
+ canvas.height = Math.round(Math.abs(selectionEnd.y - selectionStart.y));
154
+
155
+ const ctx = canvas.getContext('2d');
156
+
157
+ if (!ctx) {
158
+ throw new Error('Failed to get canvas context');
159
+ }
160
+
161
+ // Draw the cropped area
162
+ ctx.drawImage(tempCanvas, scaledX, scaledY, scaledWidth, scaledHeight, 0, 0, canvas.width, canvas.height);
163
+
164
+ // Convert to blob
165
+ const blob = await new Promise<Blob>((resolve, reject) => {
166
+ canvas.toBlob((blob) => {
167
+ if (blob) {
168
+ resolve(blob);
169
+ } else {
170
+ reject(new Error('Failed to create blob'));
171
+ }
172
+ }, 'image/png');
173
+ });
174
+
175
+ // Create a FileReader to convert blob to base64
176
+ const reader = new FileReader();
177
+
178
+ reader.onload = (e) => {
179
+ const base64Image = e.target?.result as string;
180
+
181
+ // Find the textarea element
182
+ const textarea = document.querySelector('textarea');
183
+
184
+ if (textarea) {
185
+ // Get the setters from the BaseChat component
186
+ const setUploadedFiles = (window as any).__BOLT_SET_UPLOADED_FILES__;
187
+ const setImageDataList = (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
188
+ const uploadedFiles = (window as any).__BOLT_UPLOADED_FILES__ || [];
189
+ const imageDataList = (window as any).__BOLT_IMAGE_DATA_LIST__ || [];
190
+
191
+ if (setUploadedFiles && setImageDataList) {
192
+ // Update the files and image data
193
+ const file = new File([blob], 'screenshot.png', { type: 'image/png' });
194
+ setUploadedFiles([...uploadedFiles, file]);
195
+ setImageDataList([...imageDataList, base64Image]);
196
+ toast.success('Screenshot captured and added to chat');
197
+ } else {
198
+ toast.error('Could not add screenshot to chat');
199
+ }
200
+ }
201
+ };
202
+ reader.readAsDataURL(blob);
203
+ } catch (error) {
204
+ console.error('Failed to capture screenshot:', error);
205
+ toast.error('Failed to capture screenshot');
206
+
207
+ if (mediaStreamRef.current) {
208
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
209
+ mediaStreamRef.current = null;
210
+ }
211
+ } finally {
212
+ setIsCapturing(false);
213
+ setSelectionStart(null);
214
+ setSelectionEnd(null);
215
+ setIsSelectionMode(false); // Turn off selection mode after capture
216
+ }
217
+ }, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode]);
218
+
219
+ const handleSelectionStart = useCallback(
220
+ (e: React.MouseEvent) => {
221
+ e.preventDefault();
222
+ e.stopPropagation();
223
+
224
+ if (!isSelectionMode) {
225
+ return;
226
+ }
227
+
228
+ const rect = e.currentTarget.getBoundingClientRect();
229
+ const x = e.clientX - rect.left;
230
+ const y = e.clientY - rect.top;
231
+ setSelectionStart({ x, y });
232
+ setSelectionEnd({ x, y });
233
+ },
234
+ [isSelectionMode],
235
+ );
236
+
237
+ const handleSelectionMove = useCallback(
238
+ (e: React.MouseEvent) => {
239
+ e.preventDefault();
240
+ e.stopPropagation();
241
+
242
+ if (!isSelectionMode || !selectionStart) {
243
+ return;
244
+ }
245
+
246
+ const rect = e.currentTarget.getBoundingClientRect();
247
+ const x = e.clientX - rect.left;
248
+ const y = e.clientY - rect.top;
249
+ setSelectionEnd({ x, y });
250
+ },
251
+ [isSelectionMode, selectionStart],
252
+ );
253
+
254
+ if (!isSelectionMode) {
255
+ return null;
256
+ }
257
+
258
+ return (
259
+ <div
260
+ className="absolute inset-0 cursor-crosshair"
261
+ onMouseDown={handleSelectionStart}
262
+ onMouseMove={handleSelectionMove}
263
+ onMouseUp={handleCopySelection}
264
+ onMouseLeave={() => {
265
+ if (selectionStart) {
266
+ setSelectionStart(null);
267
+ }
268
+ }}
269
+ style={{
270
+ backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
271
+ userSelect: 'none',
272
+ WebkitUserSelect: 'none',
273
+ pointerEvents: 'all',
274
+ opacity: isCapturing ? 0 : 1,
275
+ zIndex: 50,
276
+ transition: 'opacity 0.1s ease-in-out',
277
+ }}
278
+ >
279
+ {selectionStart && selectionEnd && !isCapturing && (
280
+ <div
281
+ className="absolute border-2 border-blue-500 bg-blue-200 bg-opacity-20"
282
+ style={{
283
+ left: Math.min(selectionStart.x, selectionEnd.x),
284
+ top: Math.min(selectionStart.y, selectionEnd.y),
285
+ width: Math.abs(selectionEnd.x - selectionStart.x),
286
+ height: Math.abs(selectionEnd.y - selectionStart.y),
287
+ }}
288
+ />
289
+ )}
290
+ </div>
291
+ );
292
+ },
293
+ );
app/lib/.server/llm/api-key.ts CHANGED
@@ -39,6 +39,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
39
  return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
40
  case 'xAI':
41
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
 
 
42
  case 'Cohere':
43
  return env.COHERE_API_KEY;
44
  case 'AzureOpenAI':
 
39
  return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
40
  case 'xAI':
41
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
42
+ case 'Perplexity':
43
+ return env.PERPLEXITY_API_KEY || cloudflareEnv.PERPLEXITY_API_KEY;
44
  case 'Cohere':
45
  return env.COHERE_API_KEY;
46
  case 'AzureOpenAI':
app/lib/.server/llm/model.ts CHANGED
@@ -128,6 +128,15 @@ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
128
  return openai(model);
129
  }
130
 
 
 
 
 
 
 
 
 
 
131
  export function getModel(
132
  provider: string,
133
  model: string,
@@ -170,6 +179,8 @@ export function getModel(
170
  return getXAIModel(apiKey, model);
171
  case 'Cohere':
172
  return getCohereAIModel(apiKey, model);
 
 
173
  default:
174
  return getOllamaModel(baseURL, model);
175
  }
 
128
  return openai(model);
129
  }
130
 
131
+ export function getPerplexityModel(apiKey: OptionalApiKey, model: string) {
132
+ const perplexity = createOpenAI({
133
+ baseURL: 'https://api.perplexity.ai/',
134
+ apiKey,
135
+ });
136
+
137
+ return perplexity(model);
138
+ }
139
+
140
  export function getModel(
141
  provider: string,
142
  model: string,
 
179
  return getXAIModel(apiKey, model);
180
  case 'Cohere':
181
  return getCohereAIModel(apiKey, model);
182
+ case 'Perplexity':
183
+ return getPerplexityModel(apiKey, model);
184
  default:
185
  return getOllamaModel(baseURL, model);
186
  }
app/utils/constants.ts CHANGED
@@ -139,11 +139,12 @@ const PROVIDER_LIST: ProviderInfo[] = [
139
  {
140
  name: 'Groq',
141
  staticModels: [
142
- { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
143
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
144
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
 
145
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
146
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
 
147
  ],
148
  getApiKeyLink: 'https://console.groq.com/keys',
149
  },
@@ -292,6 +293,30 @@ const PROVIDER_LIST: ProviderInfo[] = [
292
  ],
293
  getApiKeyLink: 'https://api.together.xyz/settings/api-keys',
294
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  ];
296
 
297
  export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
 
139
  {
140
  name: 'Groq',
141
  staticModels: [
 
142
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
143
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
144
+ { name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
145
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
146
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
147
+ { name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
148
  ],
149
  getApiKeyLink: 'https://console.groq.com/keys',
150
  },
 
293
  ],
294
  getApiKeyLink: 'https://api.together.xyz/settings/api-keys',
295
  },
296
+ {
297
+ name: 'Perplexity',
298
+ staticModels: [
299
+ {
300
+ name: 'llama-3.1-sonar-small-128k-online',
301
+ label: 'Sonar Small Online',
302
+ provider: 'Perplexity',
303
+ maxTokenAllowed: 8192,
304
+ },
305
+ {
306
+ name: 'llama-3.1-sonar-large-128k-online',
307
+ label: 'Sonar Large Online',
308
+ provider: 'Perplexity',
309
+ maxTokenAllowed: 8192,
310
+ },
311
+ {
312
+ name: 'llama-3.1-sonar-huge-128k-online',
313
+ label: 'Sonar Huge Online',
314
+ provider: 'Perplexity',
315
+ maxTokenAllowed: 8192,
316
+ },
317
+ ],
318
+ getApiKeyLink: 'https://www.perplexity.ai/settings/api',
319
+ },
320
  ];
321
 
322
  export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
package.json CHANGED
@@ -58,6 +58,7 @@
58
  "@octokit/rest": "^21.0.2",
59
  "@octokit/types": "^13.6.2",
60
  "@openrouter/ai-sdk-provider": "^0.0.5",
 
61
  "@radix-ui/react-dialog": "^1.1.2",
62
  "@radix-ui/react-dropdown-menu": "^2.1.2",
63
  "@radix-ui/react-separator": "^1.1.0",
 
58
  "@octokit/rest": "^21.0.2",
59
  "@octokit/types": "^13.6.2",
60
  "@openrouter/ai-sdk-provider": "^0.0.5",
61
+ "@radix-ui/react-context-menu": "^2.2.2",
62
  "@radix-ui/react-dialog": "^1.1.2",
63
  "@radix-ui/react-dropdown-menu": "^2.1.2",
64
  "@radix-ui/react-separator": "^1.1.0",
pnpm-lock.yaml CHANGED
@@ -95,6 +95,9 @@ importers:
95
  '@openrouter/ai-sdk-provider':
96
  specifier: ^0.0.5
97
  version: 0.0.5([email protected])
 
 
 
98
  '@radix-ui/react-dialog':
99
  specifier: ^1.1.2
100
@@ -1557,6 +1560,19 @@ packages:
1557
  '@types/react':
1558
  optional: true
1559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  '@radix-ui/[email protected]':
1561
  resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
1562
  peerDependencies:
@@ -7032,6 +7048,20 @@ snapshots:
7032
  optionalDependencies:
7033
  '@types/react': 18.3.12
7034
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7035
7036
  dependencies:
7037
  react: 18.3.1
 
95
  '@openrouter/ai-sdk-provider':
96
  specifier: ^0.0.5
97
  version: 0.0.5([email protected])
98
+ '@radix-ui/react-context-menu':
99
+ specifier: ^2.2.2
100
101
  '@radix-ui/react-dialog':
102
  specifier: ^1.1.2
103
 
1560
  '@types/react':
1561
  optional: true
1562
 
1563
+ '@radix-ui/[email protected]':
1564
+ resolution: {integrity: sha512-99EatSTpW+hRYHt7m8wdDlLtkmTovEe8Z/hnxUPV+SKuuNL5HWNhQI4QSdjZqNSgXHay2z4M3Dym73j9p2Gx5Q==}
1565
+ peerDependencies:
1566
+ '@types/react': '*'
1567
+ '@types/react-dom': '*'
1568
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1569
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1570
+ peerDependenciesMeta:
1571
+ '@types/react':
1572
+ optional: true
1573
+ '@types/react-dom':
1574
+ optional: true
1575
+
1576
  '@radix-ui/[email protected]':
1577
  resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
1578
  peerDependencies:
 
7048
  optionalDependencies:
7049
  '@types/react': 18.3.12
7050
 
7051
7052
+ dependencies:
7053
+ '@radix-ui/primitive': 1.1.0
7054
+ '@radix-ui/react-context': 1.1.1(@types/[email protected])([email protected])
7055
7056
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7057
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
7058
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
7059
+ react: 18.3.1
7060
+ react-dom: 18.3.1([email protected])
7061
+ optionalDependencies:
7062
+ '@types/react': 18.3.12
7063
+ '@types/react-dom': 18.3.1
7064
+
7065
7066
  dependencies:
7067
  react: 18.3.1
public/icons/Perplexity.svg ADDED
worker-configuration.d.ts CHANGED
@@ -14,4 +14,5 @@ interface Env {
14
  GOOGLE_GENERATIVE_AI_API_KEY: string;
15
  MISTRAL_API_KEY: string;
16
  XAI_API_KEY: string;
 
17
  }
 
14
  GOOGLE_GENERATIVE_AI_API_KEY: string;
15
  MISTRAL_API_KEY: string;
16
  XAI_API_KEY: string;
17
+ PERPLEXITY_API_KEY: string;
18
  }