codacus commited on
Commit
2d270e7
·
unverified ·
2 Parent(s): 54351cd f1c5fbf

Merge branch 'main' into code-streaming

Browse files
.env.example CHANGED
@@ -43,6 +43,12 @@ OPENAI_LIKE_API_KEY=
43
  # You only need this environment variable set if you want to use Mistral models
44
  MISTRAL_API_KEY=
45
 
 
 
 
 
 
 
46
  # Get your xAI API key
47
  # https://x.ai/api
48
  # You only need this environment variable set if you want to use xAI models
 
43
  # You only need this environment variable set if you want to use Mistral models
44
  MISTRAL_API_KEY=
45
 
46
+
47
+ # Get LMStudio Base URL from LM Studio Developer Console
48
+ # Make sure to enable CORS
49
+ # Example: http://localhost:1234
50
+ LMSTUDIO_API_BASE_URL=
51
+
52
  # Get your xAI API key
53
  # https://x.ai/api
54
  # You only need this environment variable set if you want to use xAI models
.github/workflows/github-build-push.yml DELETED
@@ -1,39 +0,0 @@
1
- name: Build and Push Container
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- # paths:
8
- # - 'Dockerfile'
9
- workflow_dispatch:
10
- jobs:
11
- build-and-push:
12
- runs-on: [ubuntu-latest]
13
- steps:
14
- - name: Checkout code
15
- uses: actions/checkout@v4
16
-
17
- - name: Set up QEMU
18
- uses: docker/setup-qemu-action@v1
19
-
20
- - name: Set up Docker Buildx
21
- uses: docker/setup-buildx-action@v1
22
-
23
- - name: Login to GitHub Container Registry
24
- uses: docker/login-action@v1
25
- with:
26
- registry: ghcr.io
27
- username: ${{ github.actor }}
28
- password: ${{ secrets.GITHUB_TOKEN }}
29
-
30
- - name: Build and Push Containers
31
- uses: docker/build-push-action@v2
32
- with:
33
- context: .
34
- file: Dockerfile
35
- platforms: linux/amd64,linux/arm64
36
- push: true
37
- tags: |
38
- ghcr.io/${{ github.repository }}:latest
39
- ghcr.io/${{ github.repository }}:${{ github.sha }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/semantic-pr.yaml DELETED
@@ -1,32 +0,0 @@
1
- name: Semantic Pull Request
2
- on:
3
- pull_request_target:
4
- types: [opened, reopened, edited, synchronize]
5
- permissions:
6
- pull-requests: read
7
- jobs:
8
- main:
9
- name: Validate PR Title
10
- runs-on: ubuntu-latest
11
- steps:
12
- # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3
13
- - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017
14
- env:
15
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16
- with:
17
- subjectPattern: ^(?![A-Z]).+$
18
- subjectPatternError: |
19
- The subject "{subject}" found in the pull request title "{title}"
20
- didn't match the configured pattern. Please ensure that the subject
21
- doesn't start with an uppercase character.
22
- types: |
23
- fix
24
- feat
25
- chore
26
- build
27
- ci
28
- perf
29
- docs
30
- refactor
31
- revert
32
- test
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -31,3 +31,4 @@ dist-ssr
31
  _worker.bundle
32
 
33
  Modelfile
 
 
31
  _worker.bundle
32
 
33
  Modelfile
34
+ modelfiles
README.md CHANGED
@@ -18,7 +18,9 @@ This fork of Bolt.new allows you to choose the LLM that you use for each prompt!
18
  - ✅ Ability to sync files (one way sync) to local folder (@muzafferkadir)
19
  - ✅ Containerize the application with Docker for easy installation (@aaronbolton)
20
  - ✅ Publish projects directly to GitHub (@goncaloalves)
21
- - Prevent Bolt from rewriting files as often (Done but need to review PR still)
 
 
22
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
23
  - ⬜ **HIGH PRIORITY** Load local projects into the app
24
  - ⬜ **HIGH PRIORITY** - Attach images to prompts
@@ -34,7 +36,6 @@ This fork of Bolt.new allows you to choose the LLM that you use for each prompt!
34
  - ⬜ Ability to revert code to earlier version
35
  - ⬜ Prompt caching
36
  - ⬜ Better prompt enhancing
37
- - ⬜ Ability to enter API keys in the UI
38
  - ⬜ Have LLM plan the project in a MD file for better results/transparency
39
  - ⬜ VSCode Integration with git-like confirmations
40
  - ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
@@ -85,7 +86,7 @@ If you see usr/local/bin in the output then you're good to go.
85
  git clone https://github.com/coleam00/bolt.new-any-llm.git
86
  ```
87
 
88
- 3. Rename .env.example to .env and add your LLM API keys. You will find this file on a Mac at "[your name]/bold.new-any-llm/.env.example". For Windows and Linux the path will be similar.
89
 
90
  ![image](https://github.com/user-attachments/assets/7e6a532c-2268-401f-8310-e8d20c731328)
91
 
@@ -115,7 +116,7 @@ Optionally, you can set the debug level:
115
  VITE_LOG_LEVEL=debug
116
  ```
117
 
118
- **Important**: Never commit your `.env` file to version control. It's already included in .gitignore.
119
 
120
  ## Run with Docker
121
 
 
18
  - ✅ Ability to sync files (one way sync) to local folder (@muzafferkadir)
19
  - ✅ Containerize the application with Docker for easy installation (@aaronbolton)
20
  - ✅ Publish projects directly to GitHub (@goncaloalves)
21
+ - Ability to enter API keys in the UI (@ali00209)
22
+ - ✅ xAI Grok Beta Integration (@milutinke)
23
+ - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
24
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
25
  - ⬜ **HIGH PRIORITY** Load local projects into the app
26
  - ⬜ **HIGH PRIORITY** - Attach images to prompts
 
36
  - ⬜ Ability to revert code to earlier version
37
  - ⬜ Prompt caching
38
  - ⬜ Better prompt enhancing
 
39
  - ⬜ Have LLM plan the project in a MD file for better results/transparency
40
  - ⬜ VSCode Integration with git-like confirmations
41
  - ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
 
86
  git clone https://github.com/coleam00/bolt.new-any-llm.git
87
  ```
88
 
89
+ 3. Rename .env.example to .env.local and add your LLM API keys. You will find this file on a Mac at "[your name]/bold.new-any-llm/.env.example". For Windows and Linux the path will be similar.
90
 
91
  ![image](https://github.com/user-attachments/assets/7e6a532c-2268-401f-8310-e8d20c731328)
92
 
 
116
  VITE_LOG_LEVEL=debug
117
  ```
118
 
119
+ **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
120
 
121
  ## Run with Docker
122
 
app/components/chat/APIKeyManager.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+
4
+ interface APIKeyManagerProps {
5
+ provider: string;
6
+ apiKey: string;
7
+ setApiKey: (key: string) => void;
8
+ }
9
+
10
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
11
+ const [isEditing, setIsEditing] = useState(false);
12
+ const [tempKey, setTempKey] = useState(apiKey);
13
+
14
+ const handleSave = () => {
15
+ setApiKey(tempKey);
16
+ setIsEditing(false);
17
+ };
18
+
19
+ return (
20
+ <div className="flex items-center gap-2 mt-2 mb-2">
21
+ <span className="text-sm text-bolt-elements-textSecondary">{provider} API Key:</span>
22
+ {isEditing ? (
23
+ <>
24
+ <input
25
+ type="password"
26
+ value={tempKey}
27
+ onChange={(e) => setTempKey(e.target.value)}
28
+ className="flex-1 p-1 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"
29
+ />
30
+ <IconButton onClick={handleSave} title="Save API Key">
31
+ <div className="i-ph:check" />
32
+ </IconButton>
33
+ <IconButton onClick={() => setIsEditing(false)} title="Cancel">
34
+ <div className="i-ph:x" />
35
+ </IconButton>
36
+ </>
37
+ ) : (
38
+ <>
39
+ <span className="flex-1 text-sm text-bolt-elements-textPrimary">
40
+ {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
41
+ </span>
42
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
43
+ <div className="i-ph:pencil-simple" />
44
+ </IconButton>
45
+ </>
46
+ )}
47
+ </div>
48
+ );
49
+ };
app/components/chat/Artifact.tsx CHANGED
@@ -151,7 +151,13 @@ const ActionList = memo(({ actions }: ActionListProps) => {
151
  <div className="flex items-center gap-1.5 text-sm">
152
  <div className={classNames('text-lg', getIconColor(action.status))}>
153
  {status === 'running' ? (
154
- <div className="i-svg-spinners:90-ring-with-bg"></div>
 
 
 
 
 
 
155
  ) : status === 'pending' ? (
156
  <div className="i-ph:circle-duotone"></div>
157
  ) : status === 'complete' ? (
@@ -171,9 +177,19 @@ const ActionList = memo(({ actions }: ActionListProps) => {
171
  <div className="flex items-center w-full min-h-[28px]">
172
  <span className="flex-1">Run command</span>
173
  </div>
 
 
 
 
 
 
 
 
 
 
174
  ) : null}
175
  </div>
176
- {type === 'shell' && (
177
  <ShellCodeBlock
178
  classsName={classNames('mt-1', {
179
  'mb-3.5': !isLast,
 
151
  <div className="flex items-center gap-1.5 text-sm">
152
  <div className={classNames('text-lg', getIconColor(action.status))}>
153
  {status === 'running' ? (
154
+ <>
155
+ {type !== 'start' ? (
156
+ <div className="i-svg-spinners:90-ring-with-bg"></div>
157
+ ) : (
158
+ <div className="i-ph:terminal-window-duotone"></div>
159
+ )}
160
+ </>
161
  ) : status === 'pending' ? (
162
  <div className="i-ph:circle-duotone"></div>
163
  ) : status === 'complete' ? (
 
177
  <div className="flex items-center w-full min-h-[28px]">
178
  <span className="flex-1">Run command</span>
179
  </div>
180
+ ) : type === 'start' ? (
181
+ <a
182
+ onClick={(e) => {
183
+ e.preventDefault();
184
+ workbenchStore.currentView.set('preview');
185
+ }}
186
+ className="flex items-center w-full min-h-[28px]"
187
+ >
188
+ <span className="flex-1">Start Application</span>
189
+ </a>
190
  ) : null}
191
  </div>
192
+ {(type === 'shell' || type === 'start') && (
193
  <ShellCodeBlock
194
  classsName={classNames('mt-1', {
195
  'mb-3.5': !isLast,
app/components/chat/BaseChat.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
- import React, { type RefCallback } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
@@ -11,6 +11,8 @@ import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
 
 
14
 
15
  import styles from './BaseChat.module.scss';
16
 
@@ -24,18 +26,17 @@ const EXAMPLE_PROMPTS = [
24
 
25
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
26
 
27
- const ModelSelector = ({ model, setModel, modelList, providerList }) => {
28
- const [provider, setProvider] = useState(DEFAULT_PROVIDER);
29
  return (
30
- <div className="mb-2">
31
- <select
32
  value={provider}
33
  onChange={(e) => {
34
  setProvider(e.target.value);
35
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
36
  setModel(firstModel ? firstModel.name : '');
37
  }}
38
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
39
  >
40
  {providerList.map((provider) => (
41
  <option key={provider} value={provider}>
@@ -48,11 +49,14 @@ const ModelSelector = ({ model, setModel, modelList, providerList }) => {
48
  <option key="OpenAILike" value="OpenAILike">
49
  OpenAILike
50
  </option>
 
 
 
51
  </select>
52
  <select
53
  value={model}
54
  onChange={(e) => setModel(e.target.value)}
55
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
56
  >
57
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
58
  <option key={modelOption.name} value={modelOption.name}>
@@ -79,6 +83,8 @@ interface BaseChatProps {
79
  input?: string;
80
  model: string;
81
  setModel: (model: string) => void;
 
 
82
  handleStop?: () => void;
83
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
84
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
@@ -100,6 +106,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
100
  input = '',
101
  model,
102
  setModel,
 
 
103
  sendMessage,
104
  handleInputChange,
105
  enhancePrompt,
@@ -108,6 +116,40 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
108
  ref,
109
  ) => {
110
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  return (
113
  <div
@@ -122,11 +164,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
122
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
123
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
124
  {!chatStarted && (
125
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
126
- <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
127
  Where ideas begin
128
  </h1>
129
- <p className="mb-4 text-center text-bolt-elements-textSecondary">
130
  Bring ideas to life in seconds or get help on existing projects.
131
  </p>
132
  </div>
@@ -157,16 +199,23 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
157
  model={model}
158
  setModel={setModel}
159
  modelList={MODEL_LIST}
 
 
160
  providerList={providerList}
161
  />
 
 
 
 
 
162
  <div
163
  className={classNames(
164
- 'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
165
  )}
166
  >
167
  <textarea
168
  ref={textareaRef}
169
- className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
170
  onKeyDown={(event) => {
171
  if (event.key === 'Enter') {
172
  if (event.shiftKey) {
@@ -205,12 +254,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
205
  />
206
  )}
207
  </ClientOnly>
208
- <div className="flex justify-between text-sm p-4 pt-2">
209
  <div className="flex gap-1 items-center">
210
  <IconButton
211
  title="Enhance prompt"
212
  disabled={input.length === 0 || enhancingPrompt}
213
- className={classNames({
214
  'opacity-100!': enhancingPrompt,
215
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
216
  promptEnhanced,
@@ -219,7 +268,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
219
  >
220
  {enhancingPrompt ? (
221
  <>
222
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
223
  <div className="ml-1.5">Enhancing prompt...</div>
224
  </>
225
  ) : (
@@ -232,7 +281,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
232
  </div>
233
  {input.length > 3 ? (
234
  <div className="text-xs text-bolt-elements-textTertiary">
235
- Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
236
  </div>
237
  ) : null}
238
  </div>
@@ -266,4 +315,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
266
  </div>
267
  );
268
  },
269
- );
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
+ import React, { type RefCallback, useEffect } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
 
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
14
+ import { APIKeyManager } from './APIKeyManager';
15
+ import Cookies from 'js-cookie';
16
 
17
  import styles from './BaseChat.module.scss';
18
 
 
26
 
27
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
28
 
29
+ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
 
30
  return (
31
+ <div className="mb-2 flex gap-2">
32
+ <select
33
  value={provider}
34
  onChange={(e) => {
35
  setProvider(e.target.value);
36
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
+ 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"
40
  >
41
  {providerList.map((provider) => (
42
  <option key={provider} value={provider}>
 
49
  <option key="OpenAILike" value="OpenAILike">
50
  OpenAILike
51
  </option>
52
+ <option key="LMStudio" value="LMStudio">
53
+ LMStudio
54
+ </option>
55
  </select>
56
  <select
57
  value={model}
58
  onChange={(e) => setModel(e.target.value)}
59
+ 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"
60
  >
61
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
62
  <option key={modelOption.name} value={modelOption.name}>
 
83
  input?: string;
84
  model: string;
85
  setModel: (model: string) => void;
86
+ provider: string;
87
+ setProvider: (provider: string) => void;
88
  handleStop?: () => void;
89
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
90
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
 
106
  input = '',
107
  model,
108
  setModel,
109
+ provider,
110
+ setProvider,
111
  sendMessage,
112
  handleInputChange,
113
  enhancePrompt,
 
116
  ref,
117
  ) => {
118
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
119
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
120
+
121
+ useEffect(() => {
122
+ // Load API keys from cookies on component mount
123
+ try {
124
+ const storedApiKeys = Cookies.get('apiKeys');
125
+ if (storedApiKeys) {
126
+ const parsedKeys = JSON.parse(storedApiKeys);
127
+ if (typeof parsedKeys === 'object' && parsedKeys !== null) {
128
+ setApiKeys(parsedKeys);
129
+ }
130
+ }
131
+ } catch (error) {
132
+ console.error('Error loading API keys from cookies:', error);
133
+ // Clear invalid cookie data
134
+ Cookies.remove('apiKeys');
135
+ }
136
+ }, []);
137
+
138
+ const updateApiKey = (provider: string, key: string) => {
139
+ try {
140
+ const updatedApiKeys = { ...apiKeys, [provider]: key };
141
+ setApiKeys(updatedApiKeys);
142
+ // Save updated API keys to cookies with 30 day expiry and secure settings
143
+ Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
144
+ expires: 30, // 30 days
145
+ secure: true, // Only send over HTTPS
146
+ sameSite: 'strict', // Protect against CSRF
147
+ path: '/' // Accessible across the site
148
+ });
149
+ } catch (error) {
150
+ console.error('Error saving API keys to cookies:', error);
151
+ }
152
+ };
153
 
154
  return (
155
  <div
 
164
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
165
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
166
  {!chatStarted && (
167
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
168
+ <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
169
  Where ideas begin
170
  </h1>
171
+ <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
172
  Bring ideas to life in seconds or get help on existing projects.
173
  </p>
174
  </div>
 
199
  model={model}
200
  setModel={setModel}
201
  modelList={MODEL_LIST}
202
+ provider={provider}
203
+ setProvider={setProvider}
204
  providerList={providerList}
205
  />
206
+ <APIKeyManager
207
+ provider={provider}
208
+ apiKey={apiKeys[provider] || ''}
209
+ setApiKey={(key) => updateApiKey(provider, key)}
210
+ />
211
  <div
212
  className={classNames(
213
+ 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
214
  )}
215
  >
216
  <textarea
217
  ref={textareaRef}
218
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
219
  onKeyDown={(event) => {
220
  if (event.key === 'Enter') {
221
  if (event.shiftKey) {
 
254
  />
255
  )}
256
  </ClientOnly>
257
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
258
  <div className="flex gap-1 items-center">
259
  <IconButton
260
  title="Enhance prompt"
261
  disabled={input.length === 0 || enhancingPrompt}
262
+ className={classNames('transition-all', {
263
  'opacity-100!': enhancingPrompt,
264
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
265
  promptEnhanced,
 
268
  >
269
  {enhancingPrompt ? (
270
  <>
271
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
272
  <div className="ml-1.5">Enhancing prompt...</div>
273
  </>
274
  ) : (
 
281
  </div>
282
  {input.length > 3 ? (
283
  <div className="text-xs text-bolt-elements-textTertiary">
284
+ 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> for a new line
285
  </div>
286
  ) : null}
287
  </div>
 
315
  </div>
316
  );
317
  },
318
+ );
app/components/chat/Chat.client.tsx CHANGED
@@ -11,10 +11,11 @@ import { useChatHistory } from '~/lib/persistence';
11
  import { chatStore } from '~/lib/stores/chat';
12
  import { workbenchStore } from '~/lib/stores/workbench';
13
  import { fileModificationsToHTML } from '~/utils/diff';
14
- import { DEFAULT_MODEL } from '~/utils/constants';
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
 
18
 
19
  const toastAnimation = cssTransition({
20
  enter: 'animated fadeInRight',
@@ -73,14 +74,26 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
73
  const textareaRef = useRef<HTMLTextAreaElement>(null);
74
 
75
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
76
- const [model, setModel] = useState(DEFAULT_MODEL);
 
 
 
 
 
 
 
77
 
78
  const { showChat } = useStore(chatStore);
79
 
80
  const [animationScope, animate] = useAnimate();
81
 
 
 
82
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
83
  api: '/api/chat',
 
 
 
84
  onError: (error) => {
85
  logger.error('Request failed\n\n', error);
86
  toast.error('There was an error processing your request');
@@ -182,7 +195,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
182
  * manually reset the input and we'd have to manually pass in file attachments. However, those
183
  * aren't relevant here.
184
  */
185
- append({ role: 'user', content: `[Model: ${model}]\n\n${diff}\n\n${_input}` });
186
 
187
  /**
188
  * After sending a new message we reset all modifications since the model
@@ -190,7 +203,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
190
  */
191
  workbenchStore.resetAllFileModifications();
192
  } else {
193
- append({ role: 'user', content: `[Model: ${model}]\n\n${_input}` });
194
  }
195
 
196
  setInput('');
@@ -202,6 +215,23 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
202
 
203
  const [messageRef, scrollRef] = useSnapScroll();
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  return (
206
  <BaseChat
207
  ref={animationScope}
@@ -214,7 +244,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
214
  promptEnhanced={promptEnhanced}
215
  sendMessage={sendMessage}
216
  model={model}
217
- setModel={setModel}
 
 
218
  messageRef={messageRef}
219
  scrollRef={scrollRef}
220
  handleInputChange={handleInputChange}
@@ -230,10 +262,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
230
  };
231
  })}
232
  enhancePrompt={() => {
233
- enhancePrompt(input, (input) => {
234
- setInput(input);
235
- scrollTextArea();
236
- });
 
 
 
 
 
 
237
  }}
238
  />
239
  );
 
11
  import { chatStore } from '~/lib/stores/chat';
12
  import { workbenchStore } from '~/lib/stores/workbench';
13
  import { fileModificationsToHTML } from '~/utils/diff';
14
+ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
18
+ import Cookies from 'js-cookie';
19
 
20
  const toastAnimation = cssTransition({
21
  enter: 'animated fadeInRight',
 
74
  const textareaRef = useRef<HTMLTextAreaElement>(null);
75
 
76
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
77
+ const [model, setModel] = useState(() => {
78
+ const savedModel = Cookies.get('selectedModel');
79
+ return savedModel || DEFAULT_MODEL;
80
+ });
81
+ const [provider, setProvider] = useState(() => {
82
+ const savedProvider = Cookies.get('selectedProvider');
83
+ return savedProvider || DEFAULT_PROVIDER;
84
+ });
85
 
86
  const { showChat } = useStore(chatStore);
87
 
88
  const [animationScope, animate] = useAnimate();
89
 
90
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
91
+
92
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
93
  api: '/api/chat',
94
+ body: {
95
+ apiKeys
96
+ },
97
  onError: (error) => {
98
  logger.error('Request failed\n\n', error);
99
  toast.error('There was an error processing your request');
 
195
  * manually reset the input and we'd have to manually pass in file attachments. However, those
196
  * aren't relevant here.
197
  */
198
+ append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${diff}\n\n${_input}` });
199
 
200
  /**
201
  * After sending a new message we reset all modifications since the model
 
203
  */
204
  workbenchStore.resetAllFileModifications();
205
  } else {
206
+ append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${_input}` });
207
  }
208
 
209
  setInput('');
 
215
 
216
  const [messageRef, scrollRef] = useSnapScroll();
217
 
218
+ useEffect(() => {
219
+ const storedApiKeys = Cookies.get('apiKeys');
220
+ if (storedApiKeys) {
221
+ setApiKeys(JSON.parse(storedApiKeys));
222
+ }
223
+ }, []);
224
+
225
+ const handleModelChange = (newModel: string) => {
226
+ setModel(newModel);
227
+ Cookies.set('selectedModel', newModel, { expires: 30 });
228
+ };
229
+
230
+ const handleProviderChange = (newProvider: string) => {
231
+ setProvider(newProvider);
232
+ Cookies.set('selectedProvider', newProvider, { expires: 30 });
233
+ };
234
+
235
  return (
236
  <BaseChat
237
  ref={animationScope}
 
244
  promptEnhanced={promptEnhanced}
245
  sendMessage={sendMessage}
246
  model={model}
247
+ setModel={handleModelChange}
248
+ provider={provider}
249
+ setProvider={handleProviderChange}
250
  messageRef={messageRef}
251
  scrollRef={scrollRef}
252
  handleInputChange={handleInputChange}
 
262
  };
263
  })}
264
  enhancePrompt={() => {
265
+ enhancePrompt(
266
+ input,
267
+ (input) => {
268
+ setInput(input);
269
+ scrollTextArea();
270
+ },
271
+ model,
272
+ provider,
273
+ apiKeys
274
+ );
275
  }}
276
  />
277
  );
app/components/chat/UserMessage.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { modificationsRegex } from '~/utils/diff';
4
- import { MODEL_REGEX } from '~/utils/constants';
5
  import { Markdown } from './Markdown';
6
 
7
  interface UserMessageProps {
@@ -17,5 +17,5 @@ export function UserMessage({ content }: UserMessageProps) {
17
  }
18
 
19
  function sanitizeUserMessage(content: string) {
20
- return content.replace(modificationsRegex, '').replace(MODEL_REGEX, '').trim();
21
  }
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { modificationsRegex } from '~/utils/diff';
4
+ import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
5
  import { Markdown } from './Markdown';
6
 
7
  interface UserMessageProps {
 
17
  }
18
 
19
  function sanitizeUserMessage(content: string) {
20
+ return content.replace(modificationsRegex, '').replace(MODEL_REGEX, 'Using: $1').replace(PROVIDER_REGEX, ' ($1)\n\n').trim();
21
  }
app/components/workbench/EditorPanel.tsx CHANGED
@@ -18,7 +18,7 @@ import { themeStore } from '~/lib/stores/theme';
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
20
  import { WORK_DIR } from '~/utils/constants';
21
- import { renderLogger } from '~/utils/logger';
22
  import { isMobile } from '~/utils/mobile';
23
  import { FileBreadcrumb } from './FileBreadcrumb';
24
  import { FileTree } from './FileTree';
@@ -199,25 +199,48 @@ export const EditorPanel = memo(
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
- {Array.from({ length: terminalCount }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
- <button
207
- key={index}
208
- className={classNames(
209
- 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
210
- {
211
- 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
212
- 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
213
- !isActive,
214
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  )}
216
- onClick={() => setActiveTerminal(index)}
217
- >
218
- <div className="i-ph:terminal-window-duotone text-lg" />
219
- Terminal {terminalCount > 1 && index + 1}
220
- </button>
221
  );
222
  })}
223
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
@@ -229,9 +252,26 @@ export const EditorPanel = memo(
229
  onClick={() => workbenchStore.toggleTerminal(false)}
230
  />
231
  </div>
232
- {Array.from({ length: terminalCount }, (_, index) => {
233
  const isActive = activeTerminal === index;
 
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  return (
236
  <Terminal
237
  key={index}
 
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
20
  import { WORK_DIR } from '~/utils/constants';
21
+ import { logger, renderLogger } from '~/utils/logger';
22
  import { isMobile } from '~/utils/mobile';
23
  import { FileBreadcrumb } from './FileBreadcrumb';
24
  import { FileTree } from './FileTree';
 
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
+ <>
207
+ {index == 0 ? (
208
+ <button
209
+ key={index}
210
+ className={classNames(
211
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
212
+ {
213
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
214
+ isActive,
215
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
216
+ !isActive,
217
+ },
218
+ )}
219
+ onClick={() => setActiveTerminal(index)}
220
+ >
221
+ <div className="i-ph:terminal-window-duotone text-lg" />
222
+ Bolt Terminal
223
+ </button>
224
+ ) : (
225
+ <>
226
+ <button
227
+ key={index}
228
+ className={classNames(
229
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
230
+ {
231
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
232
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
233
+ !isActive,
234
+ },
235
+ )}
236
+ onClick={() => setActiveTerminal(index)}
237
+ >
238
+ <div className="i-ph:terminal-window-duotone text-lg" />
239
+ Terminal {terminalCount > 1 && index}
240
+ </button>
241
+ </>
242
  )}
243
+ </>
 
 
 
 
244
  );
245
  })}
246
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
 
252
  onClick={() => workbenchStore.toggleTerminal(false)}
253
  />
254
  </div>
255
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
256
  const isActive = activeTerminal === index;
257
+ if (index == 0) {
258
+ logger.info('Starting bolt terminal');
259
 
260
+ return (
261
+ <Terminal
262
+ key={index}
263
+ className={classNames('h-full overflow-hidden', {
264
+ hidden: !isActive,
265
+ })}
266
+ ref={(ref) => {
267
+ terminalRefs.current.push(ref);
268
+ }}
269
+ onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
270
+ onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
271
+ theme={theme}
272
+ />
273
+ );
274
+ }
275
  return (
276
  <Terminal
277
  key={index}
app/lib/.server/llm/api-key.ts CHANGED
@@ -2,12 +2,18 @@
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
- export function getAPIKey(cloudflareEnv: Env, provider: string) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
 
 
 
 
 
 
11
  switch (provider) {
12
  case 'Anthropic':
13
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
@@ -36,6 +42,8 @@ export function getBaseURL(cloudflareEnv: Env, provider: string) {
36
  switch (provider) {
37
  case 'OpenAILike':
38
  return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
 
 
39
  case 'Ollama':
40
  let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || "http://localhost:11434";
41
  if (env.RUNNING_IN_DOCKER === 'true') {
 
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
+ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
11
+ // First check user-provided API keys
12
+ if (userApiKeys?.[provider]) {
13
+ return userApiKeys[provider];
14
+ }
15
+
16
+ // Fall back to environment variables
17
  switch (provider) {
18
  case 'Anthropic':
19
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
 
42
  switch (provider) {
43
  case 'OpenAILike':
44
  return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
45
+ case 'LMStudio':
46
+ return env.LMSTUDIO_API_BASE_URL || cloudflareEnv.LMSTUDIO_API_BASE_URL || "http://localhost:1234";
47
  case 'Ollama':
48
  let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || "http://localhost:11434";
49
  if (env.RUNNING_IN_DOCKER === 'true') {
app/lib/.server/llm/model.ts CHANGED
@@ -83,6 +83,15 @@ export function getOpenRouterModel(apiKey: string, model: string) {
83
  return openRouter.chat(model);
84
  }
85
 
 
 
 
 
 
 
 
 
 
86
  export function getXAIModel(apiKey: string, model: string) {
87
  const openai = createOpenAI({
88
  baseURL: 'https://api.x.ai/v1',
@@ -91,9 +100,8 @@ export function getXAIModel(apiKey: string, model: string) {
91
 
92
  return openai(model);
93
  }
94
-
95
- export function getModel(provider: string, model: string, env: Env) {
96
- const apiKey = getAPIKey(env, provider);
97
  const baseURL = getBaseURL(env, provider);
98
 
99
  switch (provider) {
@@ -106,13 +114,15 @@ export function getModel(provider: string, model: string, env: Env) {
106
  case 'OpenRouter':
107
  return getOpenRouterModel(apiKey, model);
108
  case 'Google':
109
- return getGoogleModel(apiKey, model)
110
  case 'OpenAILike':
111
  return getOpenAILikeModel(baseURL,apiKey, model);
112
  case 'Deepseek':
113
- return getDeepseekModel(apiKey, model)
114
  case 'Mistral':
115
  return getMistralModel(apiKey, model);
 
 
116
  case 'xAI':
117
  return getXAIModel(apiKey, model);
118
  default:
 
83
  return openRouter.chat(model);
84
  }
85
 
86
+ export function getLMStudioModel(baseURL: string, model: string) {
87
+ const lmstudio = createOpenAI({
88
+ baseUrl: `${baseURL}/v1`,
89
+ apiKey: "",
90
+ });
91
+
92
+ return lmstudio(model);
93
+ }
94
+
95
  export function getXAIModel(apiKey: string, model: string) {
96
  const openai = createOpenAI({
97
  baseURL: 'https://api.x.ai/v1',
 
100
 
101
  return openai(model);
102
  }
103
+ export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
104
+ const apiKey = getAPIKey(env, provider, apiKeys);
 
105
  const baseURL = getBaseURL(env, provider);
106
 
107
  switch (provider) {
 
114
  case 'OpenRouter':
115
  return getOpenRouterModel(apiKey, model);
116
  case 'Google':
117
+ return getGoogleModel(apiKey, model);
118
  case 'OpenAILike':
119
  return getOpenAILikeModel(baseURL,apiKey, model);
120
  case 'Deepseek':
121
+ return getDeepseekModel(apiKey, model);
122
  case 'Mistral':
123
  return getMistralModel(apiKey, model);
124
+ case 'LMStudio':
125
+ return getLMStudioModel(baseURL, model);
126
  case 'xAI':
127
  return getXAIModel(apiKey, model);
128
  default:
app/lib/.server/llm/prompts.ts CHANGED
@@ -174,10 +174,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
- - ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
180
 
 
 
 
 
 
 
181
  9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
182
 
183
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
@@ -265,7 +271,7 @@ Here are some examples of correct usage of artifacts:
265
  ...
266
  </boltAction>
267
 
268
- <boltAction type="shell">
269
  npm run dev
270
  </boltAction>
271
  </boltArtifact>
@@ -322,7 +328,7 @@ Here are some examples of correct usage of artifacts:
322
  ...
323
  </boltAction>
324
 
325
- <boltAction type="shell">
326
  npm run dev
327
  </boltAction>
328
  </boltArtifact>
 
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
+ - ULTRA IMPORTANT: Do NOT re-run a dev command with shell action use dev action to run dev commands
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
180
 
181
+ - start: For starting development server.
182
+ - Use to start application if not already started or NEW dependencies added
183
+ - Only use this action when you need to run a dev server or start the application
184
+ - ULTRA IMORTANT: do NOT re-run a dev server if files updated, existing dev server can autometically detect changes and executes the file changes
185
+
186
+
187
  9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
188
 
189
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
 
271
  ...
272
  </boltAction>
273
 
274
+ <boltAction type="start">
275
  npm run dev
276
  </boltAction>
277
  </boltArtifact>
 
328
  ...
329
  </boltAction>
330
 
331
+ <boltAction type="start">
332
  npm run dev
333
  </boltAction>
334
  </boltArtifact>
app/lib/.server/llm/stream-text.ts CHANGED
@@ -4,7 +4,7 @@ import { streamText as _streamText, convertToCoreMessages } from 'ai';
4
  import { getModel } from '~/lib/.server/llm/model';
5
  import { MAX_TOKENS } from './constants';
6
  import { getSystemPrompt } from './prompts';
7
- import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
8
 
9
  interface ToolResult<Name extends string, Args, Result> {
10
  toolCallId: string;
@@ -24,42 +24,53 @@ export type Messages = Message[];
24
 
25
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
26
 
27
- function extractModelFromMessage(message: Message): { model: string; content: string } {
28
- const modelRegex = /^\[Model: (.*?)\]\n\n/;
29
- const match = message.content.match(modelRegex);
 
30
 
31
- if (match) {
32
- const model = match[1];
33
- const content = message.content.replace(modelRegex, '');
34
- return { model, content };
35
- }
36
 
37
- // Default model if not specified
38
- return { model: DEFAULT_MODEL, content: message.content };
 
 
 
 
 
39
  }
40
 
41
- export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
 
 
 
 
 
42
  let currentModel = DEFAULT_MODEL;
 
 
43
  const processedMessages = messages.map((message) => {
44
  if (message.role === 'user') {
45
- const { model, content } = extractModelFromMessage(message);
46
- if (model && MODEL_LIST.find((m) => m.name === model)) {
47
- currentModel = model; // Update the current model
 
48
  }
 
 
 
49
  return { ...message, content };
50
  }
51
- return message;
52
- });
53
 
54
- const provider = MODEL_LIST.find((model) => model.name === currentModel)?.provider || DEFAULT_PROVIDER;
 
55
 
56
  return _streamText({
57
- model: getModel(provider, currentModel, env),
58
  system: getSystemPrompt(),
59
  maxTokens: MAX_TOKENS,
60
- // headers: {
61
- // 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
62
- // },
63
  messages: convertToCoreMessages(processedMessages),
64
  ...options,
65
  });
 
4
  import { getModel } from '~/lib/.server/llm/model';
5
  import { MAX_TOKENS } from './constants';
6
  import { getSystemPrompt } from './prompts';
7
+ import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
8
 
9
  interface ToolResult<Name extends string, Args, Result> {
10
  toolCallId: string;
 
24
 
25
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
26
 
27
+ function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
28
+ // Extract model
29
+ const modelMatch = message.content.match(MODEL_REGEX);
30
+ const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
31
 
32
+ // Extract provider
33
+ const providerMatch = message.content.match(PROVIDER_REGEX);
34
+ const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER;
 
 
35
 
36
+ // Remove model and provider lines from content
37
+ const cleanedContent = message.content
38
+ .replace(MODEL_REGEX, '')
39
+ .replace(PROVIDER_REGEX, '')
40
+ .trim();
41
+
42
+ return { model, provider, content: cleanedContent };
43
  }
44
 
45
+ export function streamText(
46
+ messages: Messages,
47
+ env: Env,
48
+ options?: StreamingOptions,
49
+ apiKeys?: Record<string, string>
50
+ ) {
51
  let currentModel = DEFAULT_MODEL;
52
+ let currentProvider = DEFAULT_PROVIDER;
53
+
54
  const processedMessages = messages.map((message) => {
55
  if (message.role === 'user') {
56
+ const { model, provider, content } = extractPropertiesFromMessage(message);
57
+
58
+ if (MODEL_LIST.find((m) => m.name === model)) {
59
+ currentModel = model;
60
  }
61
+
62
+ currentProvider = provider;
63
+
64
  return { ...message, content };
65
  }
 
 
66
 
67
+ return message; // No changes for non-user messages
68
+ });
69
 
70
  return _streamText({
71
+ model: getModel(currentProvider, currentModel, env, apiKeys),
72
  system: getSystemPrompt(),
73
  maxTokens: MAX_TOKENS,
 
 
 
74
  messages: convertToCoreMessages(processedMessages),
75
  ...options,
76
  });
app/lib/hooks/usePromptEnhancer.ts CHANGED
@@ -12,41 +12,55 @@ export function usePromptEnhancer() {
12
  setPromptEnhanced(false);
13
  };
14
 
15
- const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
 
 
 
 
 
 
16
  setEnhancingPrompt(true);
17
  setPromptEnhanced(false);
18
-
 
 
 
 
 
 
 
 
 
 
19
  const response = await fetch('/api/enhancer', {
20
  method: 'POST',
21
- body: JSON.stringify({
22
- message: input,
23
- }),
24
  });
25
-
26
  const reader = response.body?.getReader();
27
-
28
  const originalInput = input;
29
-
30
  if (reader) {
31
  const decoder = new TextDecoder();
32
-
33
  let _input = '';
34
  let _error;
35
-
36
  try {
37
  setInput('');
38
-
39
  while (true) {
40
  const { value, done } = await reader.read();
41
-
42
  if (done) {
43
  break;
44
  }
45
-
46
  _input += decoder.decode(value);
47
-
48
  logger.trace('Set input', _input);
49
-
50
  setInput(_input);
51
  }
52
  } catch (error) {
@@ -56,10 +70,10 @@ export function usePromptEnhancer() {
56
  if (_error) {
57
  logger.error(_error);
58
  }
59
-
60
  setEnhancingPrompt(false);
61
  setPromptEnhanced(true);
62
-
63
  setTimeout(() => {
64
  setInput(_input);
65
  });
 
12
  setPromptEnhanced(false);
13
  };
14
 
15
+ const enhancePrompt = async (
16
+ input: string,
17
+ setInput: (value: string) => void,
18
+ model: string,
19
+ provider: string,
20
+ apiKeys?: Record<string, string>
21
+ ) => {
22
  setEnhancingPrompt(true);
23
  setPromptEnhanced(false);
24
+
25
+ const requestBody: any = {
26
+ message: input,
27
+ model,
28
+ provider,
29
+ };
30
+
31
+ if (apiKeys) {
32
+ requestBody.apiKeys = apiKeys;
33
+ }
34
+
35
  const response = await fetch('/api/enhancer', {
36
  method: 'POST',
37
+ body: JSON.stringify(requestBody),
 
 
38
  });
39
+
40
  const reader = response.body?.getReader();
41
+
42
  const originalInput = input;
43
+
44
  if (reader) {
45
  const decoder = new TextDecoder();
46
+
47
  let _input = '';
48
  let _error;
49
+
50
  try {
51
  setInput('');
52
+
53
  while (true) {
54
  const { value, done } = await reader.read();
55
+
56
  if (done) {
57
  break;
58
  }
59
+
60
  _input += decoder.decode(value);
61
+
62
  logger.trace('Set input', _input);
63
+
64
  setInput(_input);
65
  }
66
  } catch (error) {
 
70
  if (_error) {
71
  logger.error(_error);
72
  }
73
+
74
  setEnhancingPrompt(false);
75
  setPromptEnhanced(true);
76
+
77
  setTimeout(() => {
78
  setInput(_input);
79
  });
app/lib/runtime/action-runner.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { WebContainer } from '@webcontainer/api';
2
- import { map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
 
 
8
 
9
  const logger = createScopedLogger('ActionRunner');
10
 
@@ -36,11 +38,14 @@ type ActionsMap = MapStore<Record<string, ActionState>>;
36
  export class ActionRunner {
37
  #webcontainer: Promise<WebContainer>;
38
  #currentExecutionPromise: Promise<void> = Promise.resolve();
39
-
 
40
  actions: ActionsMap = map({});
41
 
42
- constructor(webcontainerPromise: Promise<WebContainer>) {
43
  this.#webcontainer = webcontainerPromise;
 
 
44
  }
45
 
46
  addAction(data: ActionCallbackData) {
@@ -113,11 +118,16 @@ export class ActionRunner {
113
  await this.#runFileAction(action);
114
  break;
115
  }
 
 
 
 
116
  }
117
 
118
  this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
119
  } catch (error) {
120
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
 
121
 
122
  // re-throw the error to be caught in the promise chain
123
  throw error;
@@ -128,28 +138,38 @@ export class ActionRunner {
128
  if (action.type !== 'shell') {
129
  unreachable('Expected shell action');
130
  }
 
 
 
 
 
 
 
 
 
131
 
132
- const webcontainer = await this.#webcontainer;
133
-
134
- const process = await webcontainer.spawn('jsh', ['-c', action.content], {
135
- env: { npm_config_yes: true },
136
- });
137
-
138
- action.abortSignal.addEventListener('abort', () => {
139
- process.kill();
140
- });
141
-
142
- process.output.pipeTo(
143
- new WritableStream({
144
- write(data) {
145
- console.log(data);
146
- },
147
- }),
148
- );
149
 
150
- const exitCode = await process.exit;
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- logger.debug(`Process terminated with code ${exitCode}`);
 
 
 
153
  }
154
 
155
  async #runFileAction(action: ActionState) {
@@ -180,7 +200,6 @@ export class ActionRunner {
180
  logger.error('Failed to write file\n\n', error);
181
  }
182
  }
183
-
184
  #updateAction(id: string, newState: ActionStateUpdate) {
185
  const actions = this.actions.get();
186
 
 
1
+ import { WebContainer, type WebContainerProcess } from '@webcontainer/api';
2
+ import { atom, map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
8
+ import type { ITerminal } from '~/types/terminal';
9
+ import type { BoltShell } from '~/utils/shell';
10
 
11
  const logger = createScopedLogger('ActionRunner');
12
 
 
38
  export class ActionRunner {
39
  #webcontainer: Promise<WebContainer>;
40
  #currentExecutionPromise: Promise<void> = Promise.resolve();
41
+ #shellTerminal: () => BoltShell;
42
+ runnerId = atom<string>(`${Date.now()}`);
43
  actions: ActionsMap = map({});
44
 
45
+ constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
46
  this.#webcontainer = webcontainerPromise;
47
+ this.#shellTerminal = getShellTerminal;
48
+
49
  }
50
 
51
  addAction(data: ActionCallbackData) {
 
118
  await this.#runFileAction(action);
119
  break;
120
  }
121
+ case 'start': {
122
+ await this.#runStartAction(action)
123
+ break;
124
+ }
125
  }
126
 
127
  this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
128
  } catch (error) {
129
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
130
+ logger.error(`[${action.type}]:Action failed\n\n`, error);
131
 
132
  // re-throw the error to be caught in the promise chain
133
  throw error;
 
138
  if (action.type !== 'shell') {
139
  unreachable('Expected shell action');
140
  }
141
+ const shell = this.#shellTerminal()
142
+ await shell.ready()
143
+ if (!shell || !shell.terminal || !shell.process) {
144
+ unreachable('Shell terminal not found');
145
+ }
146
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
147
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
148
+ if (resp?.exitCode != 0) {
149
+ throw new Error("Failed To Execute Shell Command");
150
 
151
+ }
152
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ async #runStartAction(action: ActionState) {
155
+ if (action.type !== 'start') {
156
+ unreachable('Expected shell action');
157
+ }
158
+ if (!this.#shellTerminal) {
159
+ unreachable('Shell terminal not found');
160
+ }
161
+ const shell = this.#shellTerminal()
162
+ await shell.ready()
163
+ if (!shell || !shell.terminal || !shell.process) {
164
+ unreachable('Shell terminal not found');
165
+ }
166
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
167
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
168
 
169
+ if (resp?.exitCode != 0) {
170
+ throw new Error("Failed To Start Application");
171
+ }
172
+ return resp
173
  }
174
 
175
  async #runFileAction(action: ActionState) {
 
200
  logger.error('Failed to write file\n\n', error);
201
  }
202
  }
 
203
  #updateAction(id: string, newState: ActionStateUpdate) {
204
  const actions = this.actions.get();
205
 
app/lib/runtime/message-parser.ts CHANGED
@@ -272,7 +272,7 @@ export class StreamingMessageParser {
272
  }
273
 
274
  (actionAttributes as FileAction).filePath = filePath;
275
- } else if (actionType !== 'shell') {
276
  logger.warn(`Unknown action type '${actionType}'`);
277
  }
278
 
 
272
  }
273
 
274
  (actionAttributes as FileAction).filePath = filePath;
275
+ } else if (!(['shell', 'start'].includes(actionType))) {
276
  logger.warn(`Unknown action type '${actionType}'`);
277
  }
278
 
app/lib/stores/terminal.ts CHANGED
@@ -1,14 +1,15 @@
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
- import { newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
 
10
 
11
- showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
12
 
13
  constructor(webcontainerPromise: Promise<WebContainer>) {
14
  this.#webcontainer = webcontainerPromise;
@@ -17,10 +18,22 @@ export class TerminalStore {
17
  import.meta.hot.data.showTerminal = this.showTerminal;
18
  }
19
  }
 
 
 
20
 
21
  toggleTerminal(value?: boolean) {
22
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
23
  }
 
 
 
 
 
 
 
 
 
24
 
25
  async attachTerminal(terminal: ITerminal) {
26
  try {
 
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
+ import { newBoltShellProcess, newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
10
+ #boltTerminal = newBoltShellProcess()
11
 
12
+ showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(true);
13
 
14
  constructor(webcontainerPromise: Promise<WebContainer>) {
15
  this.#webcontainer = webcontainerPromise;
 
18
  import.meta.hot.data.showTerminal = this.showTerminal;
19
  }
20
  }
21
+ get boltTerminal() {
22
+ return this.#boltTerminal;
23
+ }
24
 
25
  toggleTerminal(value?: boolean) {
26
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
27
  }
28
+ async attachBoltTerminal(terminal: ITerminal) {
29
+ try {
30
+ let wc = await this.#webcontainer
31
+ await this.#boltTerminal.init(wc, terminal)
32
+ } catch (error: any) {
33
+ terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message);
34
+ return;
35
+ }
36
+ }
37
 
38
  async attachTerminal(terminal: ITerminal) {
39
  try {
app/lib/stores/workbench.ts CHANGED
@@ -13,6 +13,7 @@ import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
15
  import * as nodePath from 'node:path';
 
16
 
17
  export interface ArtifactState {
18
  id: string;
@@ -40,6 +41,7 @@ export class WorkbenchStore {
40
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
41
  modifiedFiles = new Set<string>();
42
  artifactIdList: string[] = [];
 
43
 
44
  constructor() {
45
  if (import.meta.hot) {
@@ -77,6 +79,9 @@ export class WorkbenchStore {
77
  get showTerminal() {
78
  return this.#terminalStore.showTerminal;
79
  }
 
 
 
80
 
81
  toggleTerminal(value?: boolean) {
82
  this.#terminalStore.toggleTerminal(value);
@@ -85,6 +90,10 @@ export class WorkbenchStore {
85
  attachTerminal(terminal: ITerminal) {
86
  this.#terminalStore.attachTerminal(terminal);
87
  }
 
 
 
 
88
 
89
  onTerminalResize(cols: number, rows: number) {
90
  this.#terminalStore.onTerminalResize(cols, rows);
@@ -233,7 +242,7 @@ export class WorkbenchStore {
233
  id,
234
  title,
235
  closed: false,
236
- runner: new ActionRunner(webcontainer),
237
  });
238
  }
239
 
 
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
15
  import * as nodePath from 'node:path';
16
+ import type { WebContainerProcess } from '@webcontainer/api';
17
 
18
  export interface ArtifactState {
19
  id: string;
 
41
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
42
  modifiedFiles = new Set<string>();
43
  artifactIdList: string[] = [];
44
+ #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
45
 
46
  constructor() {
47
  if (import.meta.hot) {
 
79
  get showTerminal() {
80
  return this.#terminalStore.showTerminal;
81
  }
82
+ get boltTerminal() {
83
+ return this.#terminalStore.boltTerminal;
84
+ }
85
 
86
  toggleTerminal(value?: boolean) {
87
  this.#terminalStore.toggleTerminal(value);
 
90
  attachTerminal(terminal: ITerminal) {
91
  this.#terminalStore.attachTerminal(terminal);
92
  }
93
+ attachBoltTerminal(terminal: ITerminal) {
94
+
95
+ this.#terminalStore.attachBoltTerminal(terminal);
96
+ }
97
 
98
  onTerminalResize(cols: number, rows: number) {
99
  this.#terminalStore.onTerminalResize(cols, rows);
 
242
  id,
243
  title,
244
  closed: false,
245
+ runner: new ActionRunner(webcontainer, () => this.boltTerminal),
246
  });
247
  }
248
 
app/routes/api.chat.ts CHANGED
@@ -11,13 +11,17 @@ export async function action(args: ActionFunctionArgs) {
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
- const { messages } = await request.json<{ messages: Messages }>();
 
 
 
15
 
16
  const stream = new SwitchableStream();
17
 
18
  try {
19
  const options: StreamingOptions = {
20
  toolChoice: 'none',
 
21
  onFinish: async ({ text: content, finishReason }) => {
22
  if (finishReason !== 'length') {
23
  return stream.close();
@@ -40,7 +44,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
40
  },
41
  };
42
 
43
- const result = await streamText(messages, context.cloudflare.env, options);
44
 
45
  stream.switchSource(result.toAIStream());
46
 
@@ -52,6 +56,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
52
  });
53
  } catch (error) {
54
  console.log(error);
 
 
 
 
 
 
 
55
 
56
  throw new Response(null, {
57
  status: 500,
 
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
+ const { messages, apiKeys } = await request.json<{
15
+ messages: Messages,
16
+ apiKeys: Record<string, string>
17
+ }>();
18
 
19
  const stream = new SwitchableStream();
20
 
21
  try {
22
  const options: StreamingOptions = {
23
  toolChoice: 'none',
24
+ apiKeys,
25
  onFinish: async ({ text: content, finishReason }) => {
26
  if (finishReason !== 'length') {
27
  return stream.close();
 
44
  },
45
  };
46
 
47
+ const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
48
 
49
  stream.switchSource(result.toAIStream());
50
 
 
56
  });
57
  } catch (error) {
58
  console.log(error);
59
+
60
+ if (error.message?.includes('API key')) {
61
+ throw new Response('Invalid or missing API key', {
62
+ status: 401,
63
+ statusText: 'Unauthorized'
64
+ });
65
+ }
66
 
67
  throw new Response(null, {
68
  status: 500,
app/routes/api.enhancer.ts CHANGED
@@ -2,6 +2,7 @@ import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
 
5
 
6
  const encoder = new TextEncoder();
7
  const decoder = new TextDecoder();
@@ -11,14 +12,34 @@ export async function action(args: ActionFunctionArgs) {
11
  }
12
 
13
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
14
- const { message } = await request.json<{ message: string }>();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  try {
17
  const result = await streamText(
18
  [
19
  {
20
  role: 'user',
21
- content: stripIndents`
22
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
23
 
24
  IMPORTANT: Only respond with the improved prompt and nothing else!
@@ -30,28 +51,42 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
30
  },
31
  ],
32
  context.cloudflare.env,
 
 
33
  );
34
 
35
  const transformStream = new TransformStream({
36
  transform(chunk, controller) {
37
- const processedChunk = decoder
38
- .decode(chunk)
39
- .split('\n')
40
- .filter((line) => line !== '')
41
- .map(parseStreamPart)
42
- .map((part) => part.value)
43
- .join('');
44
-
45
- controller.enqueue(encoder.encode(processedChunk));
 
 
 
 
 
46
  },
47
  });
48
 
49
  const transformedStream = result.toAIStream().pipeThrough(transformStream);
50
 
51
  return new StreamingTextResponse(transformedStream);
52
- } catch (error) {
53
  console.log(error);
54
 
 
 
 
 
 
 
 
55
  throw new Response(null, {
56
  status: 500,
57
  statusText: 'Internal Server Error',
 
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
5
+ import type { StreamingOptions } from '~/lib/.server/llm/stream-text';
6
 
7
  const encoder = new TextEncoder();
8
  const decoder = new TextDecoder();
 
12
  }
13
 
14
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
15
+ const { message, model, provider, apiKeys } = await request.json<{
16
+ message: string;
17
+ model: string;
18
+ provider: string;
19
+ apiKeys?: Record<string, string>;
20
+ }>();
21
+
22
+ // Validate 'model' and 'provider' fields
23
+ if (!model || typeof model !== 'string') {
24
+ throw new Response('Invalid or missing model', {
25
+ status: 400,
26
+ statusText: 'Bad Request'
27
+ });
28
+ }
29
+
30
+ if (!provider || typeof provider !== 'string') {
31
+ throw new Response('Invalid or missing provider', {
32
+ status: 400,
33
+ statusText: 'Bad Request'
34
+ });
35
+ }
36
 
37
  try {
38
  const result = await streamText(
39
  [
40
  {
41
  role: 'user',
42
+ content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n` + stripIndents`
43
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
44
 
45
  IMPORTANT: Only respond with the improved prompt and nothing else!
 
51
  },
52
  ],
53
  context.cloudflare.env,
54
+ undefined,
55
+ apiKeys
56
  );
57
 
58
  const transformStream = new TransformStream({
59
  transform(chunk, controller) {
60
+ const text = decoder.decode(chunk);
61
+ const lines = text.split('\n').filter(line => line.trim() !== '');
62
+
63
+ for (const line of lines) {
64
+ try {
65
+ const parsed = parseStreamPart(line);
66
+ if (parsed.type === 'text') {
67
+ controller.enqueue(encoder.encode(parsed.value));
68
+ }
69
+ } catch (e) {
70
+ // Skip invalid JSON lines
71
+ console.warn('Failed to parse stream part:', line);
72
+ }
73
+ }
74
  },
75
  });
76
 
77
  const transformedStream = result.toAIStream().pipeThrough(transformStream);
78
 
79
  return new StreamingTextResponse(transformedStream);
80
+ } catch (error: unknown) {
81
  console.log(error);
82
 
83
+ if (error instanceof Error && error.message?.includes('API key')) {
84
+ throw new Response('Invalid or missing API key', {
85
+ status: 401,
86
+ statusText: 'Unauthorized'
87
+ });
88
+ }
89
+
90
  throw new Response(null, {
91
  status: 500,
92
  statusText: 'Internal Server Error',
app/types/actions.ts CHANGED
@@ -13,6 +13,10 @@ export interface ShellAction extends BaseAction {
13
  type: 'shell';
14
  }
15
 
16
- export type BoltAction = FileAction | ShellAction;
 
 
 
 
17
 
18
  export type BoltActionData = BoltAction | BaseAction;
 
13
  type: 'shell';
14
  }
15
 
16
+ export interface StartAction extends BaseAction {
17
+ type: 'start';
18
+ }
19
+
20
+ export type BoltAction = FileAction | ShellAction | StartAction;
21
 
22
  export type BoltActionData = BoltAction | BaseAction;
app/types/terminal.ts CHANGED
@@ -5,4 +5,5 @@ export interface ITerminal {
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
 
8
  }
 
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
8
+ input: (data: string) => void;
9
  }
app/utils/constants.ts CHANGED
@@ -4,11 +4,11 @@ export const WORK_DIR_NAME = 'project';
4
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
5
  export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
6
  export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
7
- export const DEFAULT_MODEL = 'claude-3-5-sonnet-20240620';
 
8
  export const DEFAULT_PROVIDER = 'Anthropic';
9
 
10
  const staticModels: ModelInfo[] = [
11
- { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
12
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
13
  { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' },
14
  { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
@@ -20,13 +20,16 @@ const staticModels: ModelInfo[] = [
20
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
21
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
22
  { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google' },
23
- { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google'},
24
  { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq' },
25
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq' },
26
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq' },
27
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq' },
28
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq' },
29
- { name: 'claude-3-opus-20240229', label: 'Claude 3 Opus', provider: 'Anthropic' },
 
 
 
30
  { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic' },
31
  { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic' },
32
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
@@ -56,11 +59,11 @@ const getOllamaBaseUrl = () => {
56
  // Frontend always uses localhost
57
  return defaultBaseUrl;
58
  }
59
-
60
  // Backend: Check if we're running in Docker
61
  const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
62
-
63
- return isDocker
64
  ? defaultBaseUrl.replace("localhost", "host.docker.internal")
65
  : defaultBaseUrl;
66
  };
@@ -82,32 +85,50 @@ async function getOllamaModels(): Promise<ModelInfo[]> {
82
  }
83
 
84
  async function getOpenAILikeModels(): Promise<ModelInfo[]> {
85
- try {
86
- const base_url =import.meta.env.OPENAI_LIKE_API_BASE_URL || "";
87
- if (!base_url) {
88
  return [];
89
- }
90
- const api_key = import.meta.env.OPENAI_LIKE_API_KEY ?? "";
91
- const response = await fetch(`${base_url}/models`, {
92
- headers: {
93
- Authorization: `Bearer ${api_key}`,
94
- }
95
- });
96
  const res = await response.json() as any;
97
  return res.data.map((model: any) => ({
98
  name: model.id,
99
  label: model.id,
100
  provider: 'OpenAILike',
101
  }));
102
- }catch (e) {
103
- return []
104
- }
 
 
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
 
 
107
  async function initializeModelList(): Promise<void> {
108
  const ollamaModels = await getOllamaModels();
109
  const openAiLikeModels = await getOpenAILikeModels();
110
- MODEL_LIST = [...ollamaModels,...openAiLikeModels, ...staticModels];
 
111
  }
112
  initializeModelList().then();
113
- export { getOllamaModels, getOpenAILikeModels, initializeModelList };
 
4
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
5
  export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
6
  export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
7
+ export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
8
+ export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
9
  export const DEFAULT_PROVIDER = 'Anthropic';
10
 
11
  const staticModels: ModelInfo[] = [
 
12
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
13
  { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' },
14
  { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
 
20
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
21
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
22
  { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google' },
23
+ { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google' },
24
  { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq' },
25
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq' },
26
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq' },
27
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq' },
28
  { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq' },
29
+ { name: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (new)', provider: 'Anthropic' },
30
+ { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet (old)', provider: 'Anthropic' },
31
+ { name: 'claude-3-5-haiku-latest', label: 'Claude 3.5 Haiku (new)', provider: 'Anthropic' },
32
+ { name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic' },
33
  { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic' },
34
  { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic' },
35
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
 
59
  // Frontend always uses localhost
60
  return defaultBaseUrl;
61
  }
62
+
63
  // Backend: Check if we're running in Docker
64
  const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
65
+
66
+ return isDocker
67
  ? defaultBaseUrl.replace("localhost", "host.docker.internal")
68
  : defaultBaseUrl;
69
  };
 
85
  }
86
 
87
  async function getOpenAILikeModels(): Promise<ModelInfo[]> {
88
+ try {
89
+ const base_url = import.meta.env.OPENAI_LIKE_API_BASE_URL || "";
90
+ if (!base_url) {
91
  return [];
92
+ }
93
+ const api_key = import.meta.env.OPENAI_LIKE_API_KEY ?? "";
94
+ const response = await fetch(`${base_url}/models`, {
95
+ headers: {
96
+ Authorization: `Bearer ${api_key}`,
97
+ }
98
+ });
99
  const res = await response.json() as any;
100
  return res.data.map((model: any) => ({
101
  name: model.id,
102
  label: model.id,
103
  provider: 'OpenAILike',
104
  }));
105
+ } catch (e) {
106
+ return []
107
+ }
108
+
109
+ }
110
 
111
+ async function getLMStudioModels(): Promise<ModelInfo[]> {
112
+ try {
113
+ const base_url = import.meta.env.LMSTUDIO_API_BASE_URL || "http://localhost:1234";
114
+ const response = await fetch(`${base_url}/v1/models`);
115
+ const data = await response.json() as any;
116
+ return data.data.map((model: any) => ({
117
+ name: model.id,
118
+ label: model.id,
119
+ provider: 'LMStudio',
120
+ }));
121
+ } catch (e) {
122
+ return [];
123
+ }
124
  }
125
+
126
+
127
  async function initializeModelList(): Promise<void> {
128
  const ollamaModels = await getOllamaModels();
129
  const openAiLikeModels = await getOpenAILikeModels();
130
+ const lmstudioModels = await getLMStudioModels();
131
+ MODEL_LIST = [...ollamaModels,...openAiLikeModels, ...staticModels,...lmstudioModels,];
132
  }
133
  initializeModelList().then();
134
+ export { getOllamaModels,getOpenAILikeModels,getLMStudioModels,initializeModelList };
app/utils/shell.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { WebContainer } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
 
4
 
5
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
6
  const args: string[] = [];
@@ -19,7 +20,6 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
19
  const jshReady = withResolvers<void>();
20
 
21
  let isInteractive = false;
22
-
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
@@ -40,6 +40,8 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
40
  );
41
 
42
  terminal.onData((data) => {
 
 
43
  if (isInteractive) {
44
  input.write(data);
45
  }
@@ -49,3 +51,145 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
49
 
50
  return process;
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
4
+ import { atom } from 'nanostores';
5
 
6
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
7
  const args: string[] = [];
 
20
  const jshReady = withResolvers<void>();
21
 
22
  let isInteractive = false;
 
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
 
40
  );
41
 
42
  terminal.onData((data) => {
43
+ // console.log('terminal onData', { data, isInteractive });
44
+
45
  if (isInteractive) {
46
  input.write(data);
47
  }
 
51
 
52
  return process;
53
  }
54
+
55
+
56
+
57
+ export class BoltShell {
58
+ #initialized: (() => void) | undefined
59
+ #readyPromise: Promise<void>
60
+ #webcontainer: WebContainer | undefined
61
+ #terminal: ITerminal | undefined
62
+ #process: WebContainerProcess | undefined
63
+ executionState = atom<{ sessionId: string, active: boolean, executionPrms?: Promise<any> } | undefined>()
64
+ #outputStream: ReadableStreamDefaultReader<string> | undefined
65
+ #shellInputStream: WritableStreamDefaultWriter<string> | undefined
66
+ constructor() {
67
+ this.#readyPromise = new Promise((resolve) => {
68
+ this.#initialized = resolve
69
+ })
70
+ }
71
+ ready() {
72
+ return this.#readyPromise;
73
+ }
74
+ async init(webcontainer: WebContainer, terminal: ITerminal) {
75
+ this.#webcontainer = webcontainer
76
+ this.#terminal = terminal
77
+ let callback = (data: string) => {
78
+ console.log(data)
79
+ }
80
+ let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
81
+ this.#process = process
82
+ this.#outputStream = output.getReader()
83
+ await this.waitTillOscCode('interactive')
84
+ this.#initialized?.()
85
+ }
86
+ get terminal() {
87
+ return this.#terminal
88
+ }
89
+ get process() {
90
+ return this.#process
91
+ }
92
+ async executeCommand(sessionId: string, command: string) {
93
+ if (!this.process || !this.terminal) {
94
+ return
95
+ }
96
+ let state = this.executionState.get()
97
+
98
+ //interrupt the current execution
99
+ // this.#shellInputStream?.write('\x03');
100
+ this.terminal.input('\x03');
101
+ if (state && state.executionPrms) {
102
+ await state.executionPrms
103
+ }
104
+ //start a new execution
105
+ this.terminal.input(command.trim() + '\n');
106
+
107
+ //wait for the execution to finish
108
+ let executionPrms = this.getCurrentExecutionResult()
109
+ this.executionState.set({ sessionId, active: true, executionPrms })
110
+
111
+ let resp = await executionPrms
112
+ this.executionState.set({ sessionId, active: false })
113
+ return resp
114
+
115
+ }
116
+ async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
117
+ const args: string[] = [];
118
+
119
+ // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
120
+ const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
121
+ terminal: {
122
+ cols: terminal.cols ?? 80,
123
+ rows: terminal.rows ?? 15,
124
+ },
125
+ });
126
+
127
+ const input = process.input.getWriter();
128
+ this.#shellInputStream = input;
129
+ const [internalOutput, terminalOutput] = process.output.tee();
130
+
131
+ const jshReady = withResolvers<void>();
132
+
133
+ let isInteractive = false;
134
+ terminalOutput.pipeTo(
135
+ new WritableStream({
136
+ write(data) {
137
+ if (!isInteractive) {
138
+ const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
139
+
140
+ if (osc === 'interactive') {
141
+ // wait until we see the interactive OSC
142
+ isInteractive = true;
143
+
144
+ jshReady.resolve();
145
+ }
146
+ }
147
+
148
+ terminal.write(data);
149
+ },
150
+ }),
151
+ );
152
+
153
+ terminal.onData((data) => {
154
+ // console.log('terminal onData', { data, isInteractive });
155
+
156
+ if (isInteractive) {
157
+ input.write(data);
158
+ }
159
+ });
160
+
161
+ await jshReady.promise;
162
+
163
+ return { process, output: internalOutput };
164
+ }
165
+ async getCurrentExecutionResult() {
166
+ let { output, exitCode } = await this.waitTillOscCode('exit')
167
+ return { output, exitCode };
168
+ }
169
+ async waitTillOscCode(waitCode: string) {
170
+ let fullOutput = '';
171
+ let exitCode: number = 0;
172
+ if (!this.#outputStream) return { output: fullOutput, exitCode };
173
+ let tappedStream = this.#outputStream
174
+
175
+ while (true) {
176
+ const { value, done } = await tappedStream.read();
177
+ if (done) break;
178
+ const text = value || '';
179
+ fullOutput += text;
180
+
181
+ // Check if command completion signal with exit code
182
+ const [, osc, , pid, code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
183
+ if (osc === 'exit') {
184
+ exitCode = parseInt(code, 10);
185
+ }
186
+ if (osc === waitCode) {
187
+ break;
188
+ }
189
+ }
190
+ return { output: fullOutput, exitCode };
191
+ }
192
+ }
193
+ export function newBoltShellProcess() {
194
+ return new BoltShell();
195
+ }
package.json CHANGED
@@ -16,7 +16,7 @@
16
  "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings",
17
  "dockerstart": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session",
18
  "dockerrun": "docker run -it -d --name bolt-ai-live -p 5173:5173 --env-file .env.local bolt-ai",
19
- "dockerbuild:prod": "docker build -t bolt-ai:production bolt-ai:latest --target bolt-ai-production .",
20
  "dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .",
21
  "typecheck": "tsc",
22
  "typegen": "wrangler types",
@@ -28,8 +28,8 @@
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
31
- "@ai-sdk/openai": "^0.0.66",
32
  "@ai-sdk/mistral": "^0.0.43",
 
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
@@ -71,6 +71,7 @@
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
 
74
  "jszip": "^3.10.1",
75
  "nanostores": "^0.10.3",
76
  "ollama-ai-provider": "^0.15.2",
@@ -94,6 +95,7 @@
94
  "@remix-run/dev": "^2.10.0",
95
  "@types/diff": "^5.2.1",
96
  "@types/file-saver": "^2.0.7",
 
97
  "@types/react": "^18.2.20",
98
  "@types/react-dom": "^18.2.7",
99
  "fast-glob": "^3.3.2",
 
16
  "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings",
17
  "dockerstart": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session",
18
  "dockerrun": "docker run -it -d --name bolt-ai-live -p 5173:5173 --env-file .env.local bolt-ai",
19
+ "dockerbuild:prod": "docker build -t bolt-ai:production -t bolt-ai:latest --target bolt-ai-production .",
20
  "dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .",
21
  "typecheck": "tsc",
22
  "typegen": "wrangler types",
 
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
 
31
  "@ai-sdk/mistral": "^0.0.43",
32
+ "@ai-sdk/openai": "^0.0.66",
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
 
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
74
+ "js-cookie": "^3.0.5",
75
  "jszip": "^3.10.1",
76
  "nanostores": "^0.10.3",
77
  "ollama-ai-provider": "^0.15.2",
 
95
  "@remix-run/dev": "^2.10.0",
96
  "@types/diff": "^5.2.1",
97
  "@types/file-saver": "^2.0.7",
98
+ "@types/js-cookie": "^3.0.6",
99
  "@types/react": "^18.2.20",
100
  "@types/react-dom": "^18.2.7",
101
  "fast-glob": "^3.3.2",
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
vite.config.ts CHANGED
@@ -27,7 +27,7 @@ export default defineConfig((config) => {
27
  chrome129IssuePlugin(),
28
  config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
29
  ],
30
- envPrefix:["VITE_","OPENAI_LIKE_API_","OLLAMA_API_BASE_URL"],
31
  css: {
32
  preprocessorOptions: {
33
  scss: {
 
27
  chrome129IssuePlugin(),
28
  config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
29
  ],
30
+ envPrefix:["VITE_","OPENAI_LIKE_API_","OLLAMA_API_BASE_URL","LMSTUDIO_API_BASE_URL"],
31
  css: {
32
  preprocessorOptions: {
33
  scss: {
worker-configuration.d.ts CHANGED
@@ -7,4 +7,5 @@ interface Env {
7
  OPENAI_LIKE_API_KEY: string;
8
  OPENAI_LIKE_API_BASE_URL: string;
9
  DEEPSEEK_API_KEY: string;
 
10
  }
 
7
  OPENAI_LIKE_API_KEY: string;
8
  OPENAI_LIKE_API_BASE_URL: string;
9
  DEEPSEEK_API_KEY: string;
10
+ LMSTUDIO_API_BASE_URL: string;
11
  }