Eduards commited on
Commit
f6a7c4f
·
2 Parent(s): fb34a4c 7fc8e40

Merge remote-tracking branch 'coleam00/main' into import-export-individual-chats

Browse files

# Conflicts:
# app/components/chat/BaseChat.tsx
# app/components/chat/Messages.client.tsx
# app/lib/persistence/db.ts
# app/lib/persistence/useChatHistory.ts

.env.example CHANGED
@@ -65,4 +65,12 @@ LMSTUDIO_API_BASE_URL=
65
  XAI_API_KEY=
66
 
67
  # Include this environment variable if you want more logging for debugging locally
68
- VITE_LOG_LEVEL=debug
 
 
 
 
 
 
 
 
 
65
  XAI_API_KEY=
66
 
67
  # Include this environment variable if you want more logging for debugging locally
68
+ VITE_LOG_LEVEL=debug
69
+
70
+ # Example Context Values for qwen2.5-coder:32b
71
+ #
72
+ # DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM
73
+ # DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM
74
+ # DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM
75
+ # DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM
76
+ DEFAULT_NUM_CTX=
CONTRIBUTING.md CHANGED
@@ -1,4 +1,7 @@
1
  # Contributing to Bolt.new Fork
 
 
 
2
 
3
  First off, thank you for considering contributing to Bolt.new! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make Bolt.new a better tool for developers worldwide.
4
 
@@ -81,6 +84,19 @@ ANTHROPIC_API_KEY=XXX
81
  ```bash
82
  VITE_LOG_LEVEL=debug
83
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
85
 
86
  ### 🚀 Running the Development Server
 
1
  # Contributing to Bolt.new Fork
2
+ ## DEFAULT_NUM_CTX
3
+
4
+ The `DEFAULT_NUM_CTX` environment variable can be used to limit the maximum number of context values used by the qwen2.5-coder model. For example, to limit the context to 24576 values (which uses 32GB of VRAM), set `DEFAULT_NUM_CTX=24576` in your `.env.local` file.
5
 
6
  First off, thank you for considering contributing to Bolt.new! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make Bolt.new a better tool for developers worldwide.
7
 
 
84
  ```bash
85
  VITE_LOG_LEVEL=debug
86
  ```
87
+
88
+ - Optionally set context size:
89
+ ```bash
90
+ DEFAULT_NUM_CTX=32768
91
+ ```
92
+
93
+ Some Example Context Values for the qwen2.5-coder:32b models are.
94
+
95
+ * DEFAULT_NUM_CTX=32768 - Consumes 36GB of VRAM
96
+ * DEFAULT_NUM_CTX=24576 - Consumes 32GB of VRAM
97
+ * DEFAULT_NUM_CTX=12288 - Consumes 26GB of VRAM
98
+ * DEFAULT_NUM_CTX=6144 - Consumes 24GB of VRAM
99
+
100
  **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
101
 
102
  ### 🚀 Running the Development Server
Dockerfile CHANGED
@@ -26,6 +26,7 @@ ARG OPEN_ROUTER_API_KEY
26
  ARG GOOGLE_GENERATIVE_AI_API_KEY
27
  ARG OLLAMA_API_BASE_URL
28
  ARG VITE_LOG_LEVEL=debug
 
29
 
30
  ENV WRANGLER_SEND_METRICS=false \
31
  GROQ_API_KEY=${GROQ_API_KEY} \
@@ -35,7 +36,8 @@ ENV WRANGLER_SEND_METRICS=false \
35
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
36
  GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
37
  OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
38
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL}
 
39
 
40
  # Pre-configure wrangler to disable metrics
41
  RUN mkdir -p /root/.config/.wrangler && \
@@ -57,6 +59,7 @@ ARG OPEN_ROUTER_API_KEY
57
  ARG GOOGLE_GENERATIVE_AI_API_KEY
58
  ARG OLLAMA_API_BASE_URL
59
  ARG VITE_LOG_LEVEL=debug
 
60
 
61
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
62
  HuggingFace_API_KEY=${HuggingFace_API_KEY} \
@@ -65,7 +68,8 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \
65
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
66
  GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
67
  OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
68
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL}
 
69
 
70
  RUN mkdir -p ${WORKDIR}/run
71
  CMD pnpm run dev --host
 
26
  ARG GOOGLE_GENERATIVE_AI_API_KEY
27
  ARG OLLAMA_API_BASE_URL
28
  ARG VITE_LOG_LEVEL=debug
29
+ ARG DEFAULT_NUM_CTX
30
 
31
  ENV WRANGLER_SEND_METRICS=false \
32
  GROQ_API_KEY=${GROQ_API_KEY} \
 
36
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
37
  GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
38
  OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
39
+ VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
40
+ DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
41
 
42
  # Pre-configure wrangler to disable metrics
43
  RUN mkdir -p /root/.config/.wrangler && \
 
59
  ARG GOOGLE_GENERATIVE_AI_API_KEY
60
  ARG OLLAMA_API_BASE_URL
61
  ARG VITE_LOG_LEVEL=debug
62
+ ARG DEFAULT_NUM_CTX
63
 
64
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
65
  HuggingFace_API_KEY=${HuggingFace_API_KEY} \
 
68
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
69
  GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
70
  OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
71
+ VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
72
+ DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
73
 
74
  RUN mkdir -p ${WORKDIR}/run
75
  CMD pnpm run dev --host
app/components/chat/APIKeyManager.tsx CHANGED
@@ -10,11 +10,8 @@ interface APIKeyManagerProps {
10
  labelForGetApiKey?: string;
11
  }
12
 
13
- export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
14
- provider,
15
- apiKey,
16
- setApiKey,
17
- }) => {
18
  const [isEditing, setIsEditing] = useState(false);
19
  const [tempKey, setTempKey] = useState(apiKey);
20
 
@@ -24,15 +21,29 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
24
  };
25
 
26
  return (
27
- <div className="flex items-center gap-2 mt-2 mb-2">
28
- <span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  {isEditing ? (
30
- <>
31
  <input
32
  type="password"
33
  value={tempKey}
 
34
  onChange={(e) => setTempKey(e.target.value)}
35
- 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"
36
  />
37
  <IconButton onClick={handleSave} title="Save API Key">
38
  <div className="i-ph:check" />
@@ -40,20 +51,15 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
40
  <IconButton onClick={() => setIsEditing(false)} title="Cancel">
41
  <div className="i-ph:x" />
42
  </IconButton>
43
- </>
44
  ) : (
45
  <>
46
- <span className="flex-1 text-sm text-bolt-elements-textPrimary">
47
- {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
48
- </span>
49
- <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
50
- <div className="i-ph:pencil-simple" />
51
- </IconButton>
52
-
53
- {provider?.getApiKeyLink && <IconButton onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
54
- <span className="mr-2">{provider?.labelForGetApiKey || 'Get API Key'}</span>
55
- <div className={provider?.icon || "i-ph:key"} />
56
- </IconButton>}
57
  </>
58
  )}
59
  </div>
 
10
  labelForGetApiKey?: string;
11
  }
12
 
13
+ // eslint-disable-next-line @typescript-eslint/naming-convention
14
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
 
 
 
15
  const [isEditing, setIsEditing] = useState(false);
16
  const [tempKey, setTempKey] = useState(apiKey);
17
 
 
21
  };
22
 
23
  return (
24
+ <div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
25
+ <div>
26
+ <span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
27
+ {!isEditing && (
28
+ <div className="flex items-center mb-4">
29
+ <span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
30
+ {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
31
+ </span>
32
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
33
+ <div className="i-ph:pencil-simple" />
34
+ </IconButton>
35
+ </div>
36
+ )}
37
+ </div>
38
+
39
  {isEditing ? (
40
+ <div className="flex items-center gap-3 mt-2">
41
  <input
42
  type="password"
43
  value={tempKey}
44
+ placeholder="Your API Key"
45
  onChange={(e) => setTempKey(e.target.value)}
46
+ className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
47
  />
48
  <IconButton onClick={handleSave} title="Save API Key">
49
  <div className="i-ph:check" />
 
51
  <IconButton onClick={() => setIsEditing(false)} title="Cancel">
52
  <div className="i-ph:x" />
53
  </IconButton>
54
+ </div>
55
  ) : (
56
  <>
57
+ {provider?.getApiKeyLink && (
58
+ <IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
59
+ <span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
60
+ <div className={provider?.icon || 'i-ph:key'} />
61
+ </IconButton>
62
+ )}
 
 
 
 
 
63
  </>
64
  )}
65
  </div>
app/components/chat/BaseChat.tsx CHANGED
@@ -1,5 +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, useEffect, useState } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
@@ -7,7 +9,7 @@ import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
8
  import { Workbench } from '~/components/workbench/Workbench.client';
9
  import { classNames } from '~/utils/classNames';
10
- import { MODEL_LIST, DEFAULT_PROVIDER, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { APIKeyManager } from './APIKeyManager';
@@ -28,21 +30,25 @@ const EXAMPLE_PROMPTS = [
28
  { text: 'How do I center a div?' }
29
  ];
30
 
 
31
  const providerList = PROVIDER_LIST;
32
 
33
- const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
 
 
34
  return (
35
- <div className="mb-2 flex gap-2">
36
  <select
37
  value={provider?.name}
38
  onChange={(e) => {
39
- setProvider(providerList.find((p) => p.name === e.target.value));
 
40
  const firstModel = [...modelList].find((m) => m.provider == e.target.value);
41
  setModel(firstModel ? firstModel.name : '');
42
  }}
43
  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"
44
  >
45
- {providerList.map((provider) => (
46
  <option key={provider.name} value={provider.name}>
47
  {provider.name}
48
  </option>
@@ -52,8 +58,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
52
  key={provider?.name}
53
  value={model}
54
  onChange={(e) => setModel(e.target.value)}
55
- style={{ maxWidth: '70%' }}
56
- 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"
57
  >
58
  {[...modelList]
59
  .filter((e) => e.provider == provider?.name && e.name)
@@ -128,14 +133,17 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
128
  // Load API keys from cookies on component mount
129
  try {
130
  const storedApiKeys = Cookies.get('apiKeys');
 
131
  if (storedApiKeys) {
132
  const parsedKeys = JSON.parse(storedApiKeys);
 
133
  if (typeof parsedKeys === 'object' && parsedKeys !== null) {
134
  setApiKeys(parsedKeys);
135
  }
136
  }
137
  } catch (error) {
138
  console.error('Error loading API keys from cookies:', error);
 
139
  // Clear invalid cookie data
140
  Cookies.remove('apiKeys');
141
  }
@@ -149,6 +157,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
149
  try {
150
  const updatedApiKeys = { ...apiKeys, [provider]: key };
151
  setApiKeys(updatedApiKeys);
 
152
  // Save updated API keys to cookies with 30 day expiry and secure settings
153
  Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
154
  expires: 30, // 30 days
@@ -167,25 +176,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
167
  ref={ref}
168
  className={classNames(
169
  styles.BaseChat,
170
- 'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1'
171
  )}
172
  data-chat-visible={showChat}
173
  >
174
  <ClientOnly>{() => <Menu />}</ClientOnly>
175
- <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
176
- <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
177
  {!chatStarted && (
178
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
179
- <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
180
  Where ideas begin
181
  </h1>
182
- <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
183
  Bring ideas to life in seconds or get help on existing projects.
184
  </p>
185
  </div>
186
  )}
187
  <div
188
- className={classNames('pt-6 px-6', {
189
  'h-full flex flex-col': chatStarted
190
  })}
191
  >
@@ -194,7 +203,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
194
  return chatStarted ? (
195
  <Messages
196
  ref={messageRef}
197
- className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
198
  messages={messages}
199
  isStreaming={isStreaming}
200
  />
@@ -203,10 +212,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
203
  </ClientOnly>
204
  <div
205
  className={classNames(
206
- 'bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
207
  {
208
- 'sticky bottom-0': chatStarted
209
- })}
 
210
  >
211
  <ModelSelector
212
  key={provider?.name + ':' + modelList.length}
@@ -216,14 +226,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
216
  provider={provider}
217
  setProvider={setProvider}
218
  providerList={PROVIDER_LIST}
219
- />
 
220
  {provider && (
221
  <APIKeyManager
222
  provider={provider}
223
  apiKey={apiKeys[provider.name] || ''}
224
- setApiKey={(key) => updateApiKey(provider.name, key)}
225
- />
226
- )}
227
  <div
228
  className={classNames(
229
  'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all'
@@ -231,7 +242,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
231
  >
232
  <textarea
233
  ref={textareaRef}
234
- 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`}
235
  onKeyDown={(event) => {
236
  if (event.key === 'Enter') {
237
  if (event.shiftKey) {
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
  import type { Message } from 'ai';
6
  import React, { type RefCallback, useEffect, useState } from 'react';
7
  import { ClientOnly } from 'remix-utils/client-only';
 
9
  import { IconButton } from '~/components/ui/IconButton';
10
  import { Workbench } from '~/components/workbench/Workbench.client';
11
  import { classNames } from '~/utils/classNames';
12
+ import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
13
  import { Messages } from './Messages.client';
14
  import { SendButton } from './SendButton.client';
15
  import { APIKeyManager } from './APIKeyManager';
 
30
  { text: 'How do I center a div?' }
31
  ];
32
 
33
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
34
  const providerList = PROVIDER_LIST;
35
 
36
+ // @ts-ignore TODO: Introduce proper types
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
+ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
39
  return (
40
+ <div className="mb-2 flex gap-2 flex-col sm:flex-row">
41
  <select
42
  value={provider?.name}
43
  onChange={(e) => {
44
+ setProvider(providerList.find((p: ProviderInfo) => p.name === e.target.value));
45
+
46
  const firstModel = [...modelList].find((m) => m.provider == e.target.value);
47
  setModel(firstModel ? firstModel.name : '');
48
  }}
49
  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"
50
  >
51
+ {providerList.map((provider: ProviderInfo) => (
52
  <option key={provider.name} value={provider.name}>
53
  {provider.name}
54
  </option>
 
58
  key={provider?.name}
59
  value={model}
60
  onChange={(e) => setModel(e.target.value)}
61
+ className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%] "
 
62
  >
63
  {[...modelList]
64
  .filter((e) => e.provider == provider?.name && e.name)
 
133
  // Load API keys from cookies on component mount
134
  try {
135
  const storedApiKeys = Cookies.get('apiKeys');
136
+
137
  if (storedApiKeys) {
138
  const parsedKeys = JSON.parse(storedApiKeys);
139
+
140
  if (typeof parsedKeys === 'object' && parsedKeys !== null) {
141
  setApiKeys(parsedKeys);
142
  }
143
  }
144
  } catch (error) {
145
  console.error('Error loading API keys from cookies:', error);
146
+
147
  // Clear invalid cookie data
148
  Cookies.remove('apiKeys');
149
  }
 
157
  try {
158
  const updatedApiKeys = { ...apiKeys, [provider]: key };
159
  setApiKeys(updatedApiKeys);
160
+
161
  // Save updated API keys to cookies with 30 day expiry and secure settings
162
  Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
163
  expires: 30, // 30 days
 
176
  ref={ref}
177
  className={classNames(
178
  styles.BaseChat,
179
+ 'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1'
180
  )}
181
  data-chat-visible={showChat}
182
  >
183
  <ClientOnly>{() => <Menu />}</ClientOnly>
184
+ <div ref={scrollRef} className="flex flex-col lg:flex-rowoverflow-y-auto w-full h-full">
185
+ <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
186
  {!chatStarted && (
187
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-centerpx-4 lg:px-0">
188
+ <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
189
  Where ideas begin
190
  </h1>
191
+ <p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
192
  Bring ideas to life in seconds or get help on existing projects.
193
  </p>
194
  </div>
195
  )}
196
  <div
197
+ className={classNames('pt-6 px-2 sm:px-6', {
198
  'h-full flex flex-col': chatStarted
199
  })}
200
  >
 
203
  return chatStarted ? (
204
  <Messages
205
  ref={messageRef}
206
+ className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
207
  messages={messages}
208
  isStreaming={isStreaming}
209
  />
 
212
  </ClientOnly>
213
  <div
214
  className={classNames(
215
+ 'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
216
  {
217
+ 'sticky bottom-2': chatStarted,
218
+ },
219
+ )}
220
  >
221
  <ModelSelector
222
  key={provider?.name + ':' + modelList.length}
 
226
  provider={provider}
227
  setProvider={setProvider}
228
  providerList={PROVIDER_LIST}
229
+ apiKeys={apiKeys}
230
+ />
231
  {provider && (
232
  <APIKeyManager
233
  provider={provider}
234
  apiKey={apiKeys[provider.name] || ''}
235
+ setApiKey={(key) => updateApiKey(provider.name, key)}/>
236
+ )}
237
+
238
  <div
239
  className={classNames(
240
  'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all'
 
242
  >
243
  <textarea
244
  ref={textareaRef}
245
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-0 focus:border-none focus:shadow-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
246
  onKeyDown={(event) => {
247
  if (event.key === 'Enter') {
248
  if (event.shiftKey) {
app/components/chat/Chat.client.tsx CHANGED
@@ -1,5 +1,7 @@
1
- // @ts-nocheck
2
- // Preventing TS checks with files presented in the video for a better presentation.
 
 
3
  import { useStore } from '@nanostores/react';
4
  import type { Message } from 'ai';
5
  import { useChat } from 'ai/react';
@@ -84,7 +86,7 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor
84
  });
85
  const [provider, setProvider] = useState(() => {
86
  const savedProvider = Cookies.get('selectedProvider');
87
- return PROVIDER_LIST.find(p => p.name === savedProvider) || DEFAULT_PROVIDER;
88
  });
89
 
90
  const { showChat } = useStore(chatStore);
@@ -96,11 +98,13 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor
96
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
97
  api: '/api/chat',
98
  body: {
99
- apiKeys
100
  },
101
  onError: (error) => {
102
  logger.error('Request failed\n\n', error);
103
- toast.error('There was an error processing your request: ' + (error.message ? error.message : "No details were returned"));
 
 
104
  },
105
  onFinish: () => {
106
  logger.debug('Finished streaming');
@@ -221,6 +225,7 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor
221
 
222
  useEffect(() => {
223
  const storedApiKeys = Cookies.get('apiKeys');
 
224
  if (storedApiKeys) {
225
  setApiKeys(JSON.parse(storedApiKeys));
226
  }
@@ -277,7 +282,7 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor
277
  },
278
  model,
279
  provider,
280
- apiKeys
281
  );
282
  }}
283
  />
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
  import { useStore } from '@nanostores/react';
6
  import type { Message } from 'ai';
7
  import { useChat } from 'ai/react';
 
86
  });
87
  const [provider, setProvider] = useState(() => {
88
  const savedProvider = Cookies.get('selectedProvider');
89
+ return PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER;
90
  });
91
 
92
  const { showChat } = useStore(chatStore);
 
98
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
99
  api: '/api/chat',
100
  body: {
101
+ apiKeys,
102
  },
103
  onError: (error) => {
104
  logger.error('Request failed\n\n', error);
105
+ toast.error(
106
+ 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
107
+ );
108
  },
109
  onFinish: () => {
110
  logger.debug('Finished streaming');
 
225
 
226
  useEffect(() => {
227
  const storedApiKeys = Cookies.get('apiKeys');
228
+
229
  if (storedApiKeys) {
230
  setApiKeys(JSON.parse(storedApiKeys));
231
  }
 
282
  },
283
  model,
284
  provider,
285
+ apiKeys,
286
  );
287
  }}
288
  />
app/components/chat/Messages.client.tsx CHANGED
@@ -3,7 +3,7 @@ import React from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
- import { useLocation, useNavigate } from '@remix-run/react';
7
  import { db, chatId } from '~/lib/persistence/useChatHistory';
8
  import { forkChat } from '~/lib/persistence/db';
9
  import { toast } from 'react-toastify';
@@ -19,7 +19,6 @@ interface MessagesProps {
19
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
20
  const { id, isStreaming = false, messages = [] } = props;
21
  const location = useLocation();
22
- const navigate = useNavigate();
23
 
24
  const handleRewind = (messageId: string) => {
25
  const searchParams = new URLSearchParams(location.search);
@@ -67,29 +66,32 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
67
  <div className="grid grid-col-1 w-full">
68
  {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
69
  </div>
70
- {!isUserMessage && (<div className="flex gap-2">
71
- <WithTooltip tooltip="Revert to this message">
 
72
  {messageId && (<button
73
  onClick={() => handleRewind(messageId)}
74
- key='i-ph:arrow-u-up-left'
75
  className={classNames(
76
  'i-ph:arrow-u-up-left',
77
- 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
78
- )}
79
- />)}
 
80
  </WithTooltip>
81
 
82
- <WithTooltip tooltip="Fork chat from this message">
83
  <button
84
  onClick={() => handleFork(messageId)}
85
  key="i-ph:git-fork"
86
  className={classNames(
87
  'i-ph:git-fork',
88
- 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
89
  )}
90
  />
91
  </WithTooltip>
92
- </div>)}
 
93
  </div>
94
  );
95
  })
 
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
+ import { useLocation } from '@remix-run/react';
7
  import { db, chatId } from '~/lib/persistence/useChatHistory';
8
  import { forkChat } from '~/lib/persistence/db';
9
  import { toast } from 'react-toastify';
 
19
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
20
  const { id, isStreaming = false, messages = [] } = props;
21
  const location = useLocation();
 
22
 
23
  const handleRewind = (messageId: string) => {
24
  const searchParams = new URLSearchParams(location.search);
 
66
  <div className="grid grid-col-1 w-full">
67
  {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
68
  </div>
69
+ {!isUserMessage && (
70
+ <div className="flex gap-2 flex-col lg:flex-row">
71
+ <WithTooltip tooltip="Revert to this message">
72
  {messageId && (<button
73
  onClick={() => handleRewind(messageId)}
74
+ key="i-ph:arrow-u-up-left"
75
  className={classNames(
76
  'i-ph:arrow-u-up-left',
77
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
78
+ )}
79
+ />
80
+ )}
81
  </WithTooltip>
82
 
83
+ <WithTooltip tooltip="Fork chat from this message">
84
  <button
85
  onClick={() => handleFork(messageId)}
86
  key="i-ph:git-fork"
87
  className={classNames(
88
  'i-ph:git-fork',
89
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
90
  )}
91
  />
92
  </WithTooltip>
93
+ </div>
94
+ )}
95
  </div>
96
  );
97
  })
app/components/chat/UserMessage.tsx CHANGED
@@ -1,5 +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, PROVIDER_REGEX } from '~/utils/constants';
5
  import { Markdown } from './Markdown';
@@ -17,5 +19,9 @@ export function UserMessage({ content }: 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
  }
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
  import { modificationsRegex } from '~/utils/diff';
6
  import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
7
  import { Markdown } from './Markdown';
 
19
  }
20
 
21
  function sanitizeUserMessage(content: string) {
22
+ return content
23
+ .replace(modificationsRegex, '')
24
+ .replace(MODEL_REGEX, 'Using: $1')
25
+ .replace(PROVIDER_REGEX, ' ($1)\n\n')
26
+ .trim();
27
  }
app/components/header/HeaderActionButtons.client.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { useStore } from '@nanostores/react';
 
2
  import { chatStore } from '~/lib/stores/chat';
3
  import { workbenchStore } from '~/lib/stores/workbench';
4
  import { classNames } from '~/utils/classNames';
@@ -9,6 +10,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
9
  const showWorkbench = useStore(workbenchStore.showWorkbench);
10
  const { showChat } = useStore(chatStore);
11
 
 
 
12
  const canHideChat = showWorkbench || !showChat;
13
 
14
  return (
@@ -16,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
16
  <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
17
  <Button
18
  active={showChat}
19
- disabled={!canHideChat}
20
  onClick={() => {
21
  if (canHideChat) {
22
  chatStore.setKey('showChat', !showChat);
 
1
  import { useStore } from '@nanostores/react';
2
+ import useViewport from '~/lib/hooks';
3
  import { chatStore } from '~/lib/stores/chat';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { classNames } from '~/utils/classNames';
 
10
  const showWorkbench = useStore(workbenchStore.showWorkbench);
11
  const { showChat } = useStore(chatStore);
12
 
13
+ const isSmallViewport = useViewport(1024);
14
+
15
  const canHideChat = showWorkbench || !showChat;
16
 
17
  return (
 
19
  <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
20
  <Button
21
  active={showChat}
22
+ disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's needed
23
  onClick={() => {
24
  if (canHideChat) {
25
  chatStore.setKey('showChat', !showChat);
app/components/sidebar/Menu.client.tsx CHANGED
@@ -2,7 +2,6 @@ import { motion, type Variants } from 'framer-motion';
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
- import { IconButton } from '~/components/ui/IconButton';
6
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
7
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
 
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
 
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
7
  import { cubicEasingFn } from '~/utils/easings';
app/components/workbench/EditorPanel.tsx CHANGED
@@ -255,6 +255,7 @@ export const EditorPanel = memo(
255
  </div>
256
  {Array.from({ length: terminalCount + 1 }, (_, index) => {
257
  const isActive = activeTerminal === index;
 
258
  if (index == 0) {
259
  logger.info('Starting bolt terminal');
260
 
@@ -273,6 +274,7 @@ export const EditorPanel = memo(
273
  />
274
  );
275
  }
 
276
  return (
277
  <Terminal
278
  key={index}
 
255
  </div>
256
  {Array.from({ length: terminalCount + 1 }, (_, index) => {
257
  const isActive = activeTerminal === index;
258
+
259
  if (index == 0) {
260
  logger.info('Starting bolt terminal');
261
 
 
274
  />
275
  );
276
  }
277
+
278
  return (
279
  <Terminal
280
  key={index}
app/components/workbench/FileTree.tsx CHANGED
@@ -111,7 +111,7 @@ export const FileTree = memo(
111
  };
112
 
113
  return (
114
- <div className={classNames('text-sm', className ,'overflow-y-auto')}>
115
  {filteredFileList.map((fileOrFolder) => {
116
  switch (fileOrFolder.kind) {
117
  case 'file': {
 
111
  };
112
 
113
  return (
114
+ <div className={classNames('text-sm', className, 'overflow-y-auto')}>
115
  {filteredFileList.map((fileOrFolder) => {
116
  switch (fileOrFolder.kind) {
117
  case 'file': {
app/components/workbench/Workbench.client.tsx CHANGED
@@ -16,6 +16,7 @@ import { cubicEasingFn } from '~/utils/easings';
16
  import { renderLogger } from '~/utils/logger';
17
  import { EditorPanel } from './EditorPanel';
18
  import { Preview } from './Preview';
 
19
 
20
  interface WorkspaceProps {
21
  chatStarted?: boolean;
@@ -65,6 +66,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
65
  const files = useStore(workbenchStore.files);
66
  const selectedView = useStore(workbenchStore.currentView);
67
 
 
 
68
  const setSelectedView = (view: WorkbenchViewType) => {
69
  workbenchStore.currentView.set(view);
70
  };
@@ -128,18 +131,20 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
128
  className={classNames(
129
  'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
130
  {
 
 
131
  'left-[var(--workbench-left)]': showWorkbench,
132
  'left-[100%]': !showWorkbench,
133
  },
134
  )}
135
  >
136
- <div className="absolute inset-0 px-6">
137
  <div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
138
  <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
139
  <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
140
  <div className="ml-auto" />
141
  {selectedView === 'code' && (
142
- <>
143
  <PanelHeaderButton
144
  className="mr-1 text-sm"
145
  onClick={() => {
@@ -165,29 +170,37 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
165
  <PanelHeaderButton
166
  className="mr-1 text-sm"
167
  onClick={() => {
168
- const repoName = prompt("Please enter a name for your new GitHub repository:", "bolt-generated-project");
 
 
 
 
169
  if (!repoName) {
170
- alert("Repository name is required. Push to GitHub cancelled.");
171
  return;
172
  }
173
- const githubUsername = prompt("Please enter your GitHub username:");
 
 
174
  if (!githubUsername) {
175
- alert("GitHub username is required. Push to GitHub cancelled.");
176
  return;
177
  }
178
- const githubToken = prompt("Please enter your GitHub personal access token:");
 
 
179
  if (!githubToken) {
180
- alert("GitHub token is required. Push to GitHub cancelled.");
181
  return;
182
  }
183
-
184
- workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
185
  }}
186
  >
187
  <div className="i-ph:github-logo" />
188
  Push to GitHub
189
  </PanelHeaderButton>
190
- </>
191
  )}
192
  <IconButton
193
  icon="i-ph:x-circle"
 
16
  import { renderLogger } from '~/utils/logger';
17
  import { EditorPanel } from './EditorPanel';
18
  import { Preview } from './Preview';
19
+ import useViewport from '~/lib/hooks';
20
 
21
  interface WorkspaceProps {
22
  chatStarted?: boolean;
 
66
  const files = useStore(workbenchStore.files);
67
  const selectedView = useStore(workbenchStore.currentView);
68
 
69
+ const isSmallViewport = useViewport(1024);
70
+
71
  const setSelectedView = (view: WorkbenchViewType) => {
72
  workbenchStore.currentView.set(view);
73
  };
 
131
  className={classNames(
132
  'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
133
  {
134
+ 'w-full': isSmallViewport,
135
+ 'left-0': showWorkbench && isSmallViewport,
136
  'left-[var(--workbench-left)]': showWorkbench,
137
  'left-[100%]': !showWorkbench,
138
  },
139
  )}
140
  >
141
+ <div className="absolute inset-0 px-2 lg:px-6">
142
  <div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
143
  <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
144
  <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
145
  <div className="ml-auto" />
146
  {selectedView === 'code' && (
147
+ <div className="flex overflow-y-auto">
148
  <PanelHeaderButton
149
  className="mr-1 text-sm"
150
  onClick={() => {
 
170
  <PanelHeaderButton
171
  className="mr-1 text-sm"
172
  onClick={() => {
173
+ const repoName = prompt(
174
+ 'Please enter a name for your new GitHub repository:',
175
+ 'bolt-generated-project',
176
+ );
177
+
178
  if (!repoName) {
179
+ alert('Repository name is required. Push to GitHub cancelled.');
180
  return;
181
  }
182
+
183
+ const githubUsername = prompt('Please enter your GitHub username:');
184
+
185
  if (!githubUsername) {
186
+ alert('GitHub username is required. Push to GitHub cancelled.');
187
  return;
188
  }
189
+
190
+ const githubToken = prompt('Please enter your GitHub personal access token:');
191
+
192
  if (!githubToken) {
193
+ alert('GitHub token is required. Push to GitHub cancelled.');
194
  return;
195
  }
196
+
197
+ workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
198
  }}
199
  >
200
  <div className="i-ph:github-logo" />
201
  Push to GitHub
202
  </PanelHeaderButton>
203
+ </div>
204
  )}
205
  <IconButton
206
  icon="i-ph:x-circle"
app/lib/.server/llm/api-key.ts CHANGED
@@ -1,5 +1,7 @@
1
- // @ts-nocheck
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>) {
@@ -28,17 +30,19 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
28
  case 'OpenRouter':
29
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
30
  case 'Deepseek':
31
- return env.DEEPSEEK_API_KEY || cloudflareEnv.DEEPSEEK_API_KEY
32
  case 'Mistral':
33
- return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
34
- case "OpenAILike":
35
  return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
36
- case "xAI":
37
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
38
- case "Cohere":
39
  return env.COHERE_API_KEY;
 
 
40
  default:
41
- return "";
42
  }
43
  }
44
 
@@ -47,14 +51,17 @@ export function getBaseURL(cloudflareEnv: Env, provider: string) {
47
  case 'OpenAILike':
48
  return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
49
  case 'LMStudio':
50
- return env.LMSTUDIO_API_BASE_URL || cloudflareEnv.LMSTUDIO_API_BASE_URL || "http://localhost:1234";
51
- case 'Ollama':
52
- let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || "http://localhost:11434";
53
- if (env.RUNNING_IN_DOCKER === 'true') {
54
- baseUrl = baseUrl.replace("localhost", "host.docker.internal");
55
- }
56
- return baseUrl;
 
 
 
57
  default:
58
- return "";
59
  }
60
  }
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
  import { env } from 'node:process';
6
 
7
  export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
 
30
  case 'OpenRouter':
31
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
32
  case 'Deepseek':
33
+ return env.DEEPSEEK_API_KEY || cloudflareEnv.DEEPSEEK_API_KEY;
34
  case 'Mistral':
35
+ return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
36
+ case 'OpenAILike':
37
  return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
38
+ case 'xAI':
39
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
40
+ case 'Cohere':
41
  return env.COHERE_API_KEY;
42
+ case 'AzureOpenAI':
43
+ return env.AZURE_OPENAI_API_KEY;
44
  default:
45
+ return '';
46
  }
47
  }
48
 
 
51
  case 'OpenAILike':
52
  return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
53
  case 'LMStudio':
54
+ return env.LMSTUDIO_API_BASE_URL || cloudflareEnv.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
55
+ case 'Ollama': {
56
+ let baseUrl = env.OLLAMA_API_BASE_URL || cloudflareEnv.OLLAMA_API_BASE_URL || 'http://localhost:11434';
57
+
58
+ if (env.RUNNING_IN_DOCKER === 'true') {
59
+ baseUrl = baseUrl.replace('localhost', 'host.docker.internal');
60
+ }
61
+
62
+ return baseUrl;
63
+ }
64
  default:
65
+ return '';
66
  }
67
  }
app/lib/.server/llm/model.ts CHANGED
@@ -1,22 +1,29 @@
1
- // @ts-nocheck
2
- // Preventing TS checks with files presented in the video for a better presentation.
 
 
3
  import { getAPIKey, getBaseURL } from '~/lib/.server/llm/api-key';
4
  import { createAnthropic } from '@ai-sdk/anthropic';
5
  import { createOpenAI } from '@ai-sdk/openai';
6
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
7
  import { ollama } from 'ollama-ai-provider';
8
- import { createOpenRouter } from "@openrouter/ai-sdk-provider";
9
  import { createMistral } from '@ai-sdk/mistral';
10
- import { createCohere } from '@ai-sdk/cohere'
 
11
 
12
- export function getAnthropicModel(apiKey: string, model: string) {
 
 
 
 
13
  const anthropic = createAnthropic({
14
  apiKey,
15
  });
16
 
17
  return anthropic(model);
18
  }
19
- export function getOpenAILikeModel(baseURL:string,apiKey: string, model: string) {
20
  const openai = createOpenAI({
21
  baseURL,
22
  apiKey,
@@ -25,7 +32,7 @@ export function getOpenAILikeModel(baseURL:string,apiKey: string, model: string)
25
  return openai(model);
26
  }
27
 
28
- export function getCohereAIModel(apiKey:string, model: string){
29
  const cohere = createCohere({
30
  apiKey,
31
  });
@@ -33,7 +40,7 @@ export function getCohereAIModel(apiKey:string, model: string){
33
  return cohere(model);
34
  }
35
 
36
- export function getOpenAIModel(apiKey: string, model: string) {
37
  const openai = createOpenAI({
38
  apiKey,
39
  });
@@ -41,15 +48,15 @@ export function getOpenAIModel(apiKey: string, model: string) {
41
  return openai(model);
42
  }
43
 
44
- export function getMistralModel(apiKey: string, model: string) {
45
  const mistral = createMistral({
46
- apiKey
47
  });
48
 
49
  return mistral(model);
50
  }
51
 
52
- export function getGoogleModel(apiKey: string, model: string) {
53
  const google = createGoogleGenerativeAI({
54
  apiKey,
55
  });
@@ -57,7 +64,7 @@ export function getGoogleModel(apiKey: string, model: string) {
57
  return google(model);
58
  }
59
 
60
- export function getGroqModel(apiKey: string, model: string) {
61
  const openai = createOpenAI({
62
  baseURL: 'https://api.groq.com/openai/v1',
63
  apiKey,
@@ -66,7 +73,7 @@ export function getGroqModel(apiKey: string, model: string) {
66
  return openai(model);
67
  }
68
 
69
- export function getHuggingFaceModel(apiKey: string, model: string) {
70
  const openai = createOpenAI({
71
  baseURL: 'https://api-inference.huggingface.co/v1/',
72
  apiKey,
@@ -76,15 +83,16 @@ export function getHuggingFaceModel(apiKey: string, model: string) {
76
  }
77
 
78
  export function getOllamaModel(baseURL: string, model: string) {
79
- let Ollama = ollama(model, {
80
- numCtx: 32768,
81
- });
82
 
83
- Ollama.config.baseURL = `${baseURL}/api`;
84
- return Ollama;
 
85
  }
86
 
87
- export function getDeepseekModel(apiKey: string, model: string){
88
  const openai = createOpenAI({
89
  baseURL: 'https://api.deepseek.com/beta',
90
  apiKey,
@@ -93,9 +101,9 @@ export function getDeepseekModel(apiKey: string, model: string){
93
  return openai(model);
94
  }
95
 
96
- export function getOpenRouterModel(apiKey: string, model: string) {
97
  const openRouter = createOpenRouter({
98
- apiKey
99
  });
100
 
101
  return openRouter.chat(model);
@@ -104,13 +112,13 @@ export function getOpenRouterModel(apiKey: string, model: string) {
104
  export function getLMStudioModel(baseURL: string, model: string) {
105
  const lmstudio = createOpenAI({
106
  baseUrl: `${baseURL}/v1`,
107
- apiKey: "",
108
  });
109
 
110
  return lmstudio(model);
111
  }
112
 
113
- export function getXAIModel(apiKey: string, model: string) {
114
  const openai = createOpenAI({
115
  baseURL: 'https://api.x.ai/v1',
116
  apiKey,
@@ -119,7 +127,6 @@ export function getXAIModel(apiKey: string, model: string) {
119
  return openai(model);
120
  }
121
 
122
-
123
  export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
124
  const apiKey = getAPIKey(env, provider, apiKeys);
125
  const baseURL = getBaseURL(env, provider);
@@ -138,11 +145,11 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
138
  case 'Google':
139
  return getGoogleModel(apiKey, model);
140
  case 'OpenAILike':
141
- return getOpenAILikeModel(baseURL,apiKey, model);
142
  case 'Deepseek':
143
  return getDeepseekModel(apiKey, model);
144
  case 'Mistral':
145
- return getMistralModel(apiKey, model);
146
  case 'LMStudio':
147
  return getLMStudioModel(baseURL, model);
148
  case 'xAI':
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
  import { getAPIKey, getBaseURL } from '~/lib/.server/llm/api-key';
6
  import { createAnthropic } from '@ai-sdk/anthropic';
7
  import { createOpenAI } from '@ai-sdk/openai';
8
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
9
  import { ollama } from 'ollama-ai-provider';
10
+ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
  import { createMistral } from '@ai-sdk/mistral';
12
+ import { createCohere } from '@ai-sdk/cohere';
13
+ import type { LanguageModelV1 } from 'ai';
14
 
15
+ export const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
16
+
17
+ type OptionalApiKey = string | undefined;
18
+
19
+ export function getAnthropicModel(apiKey: OptionalApiKey, model: string) {
20
  const anthropic = createAnthropic({
21
  apiKey,
22
  });
23
 
24
  return anthropic(model);
25
  }
26
+ export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) {
27
  const openai = createOpenAI({
28
  baseURL,
29
  apiKey,
 
32
  return openai(model);
33
  }
34
 
35
+ export function getCohereAIModel(apiKey: OptionalApiKey, model: string) {
36
  const cohere = createCohere({
37
  apiKey,
38
  });
 
40
  return cohere(model);
41
  }
42
 
43
+ export function getOpenAIModel(apiKey: OptionalApiKey, model: string) {
44
  const openai = createOpenAI({
45
  apiKey,
46
  });
 
48
  return openai(model);
49
  }
50
 
51
+ export function getMistralModel(apiKey: OptionalApiKey, model: string) {
52
  const mistral = createMistral({
53
+ apiKey,
54
  });
55
 
56
  return mistral(model);
57
  }
58
 
59
+ export function getGoogleModel(apiKey: OptionalApiKey, model: string) {
60
  const google = createGoogleGenerativeAI({
61
  apiKey,
62
  });
 
64
  return google(model);
65
  }
66
 
67
+ export function getGroqModel(apiKey: OptionalApiKey, model: string) {
68
  const openai = createOpenAI({
69
  baseURL: 'https://api.groq.com/openai/v1',
70
  apiKey,
 
73
  return openai(model);
74
  }
75
 
76
+ export function getHuggingFaceModel(apiKey: OptionalApiKey, model: string) {
77
  const openai = createOpenAI({
78
  baseURL: 'https://api-inference.huggingface.co/v1/',
79
  apiKey,
 
83
  }
84
 
85
  export function getOllamaModel(baseURL: string, model: string) {
86
+ const ollamaInstance = ollama(model, {
87
+ numCtx: DEFAULT_NUM_CTX,
88
+ }) as LanguageModelV1 & { config: any };
89
 
90
+ ollamaInstance.config.baseURL = `${baseURL}/api`;
91
+
92
+ return ollamaInstance;
93
  }
94
 
95
+ export function getDeepseekModel(apiKey: OptionalApiKey, model: string) {
96
  const openai = createOpenAI({
97
  baseURL: 'https://api.deepseek.com/beta',
98
  apiKey,
 
101
  return openai(model);
102
  }
103
 
104
+ export function getOpenRouterModel(apiKey: OptionalApiKey, model: string) {
105
  const openRouter = createOpenRouter({
106
+ apiKey,
107
  });
108
 
109
  return openRouter.chat(model);
 
112
  export function getLMStudioModel(baseURL: string, model: string) {
113
  const lmstudio = createOpenAI({
114
  baseUrl: `${baseURL}/v1`,
115
+ apiKey: '',
116
  });
117
 
118
  return lmstudio(model);
119
  }
120
 
121
+ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
122
  const openai = createOpenAI({
123
  baseURL: 'https://api.x.ai/v1',
124
  apiKey,
 
127
  return openai(model);
128
  }
129
 
 
130
  export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
131
  const apiKey = getAPIKey(env, provider, apiKeys);
132
  const baseURL = getBaseURL(env, provider);
 
145
  case 'Google':
146
  return getGoogleModel(apiKey, model);
147
  case 'OpenAILike':
148
+ return getOpenAILikeModel(baseURL, apiKey, model);
149
  case 'Deepseek':
150
  return getDeepseekModel(apiKey, model);
151
  case 'Mistral':
152
+ return getMistralModel(apiKey, model);
153
  case 'LMStudio':
154
  return getLMStudioModel(baseURL, model);
155
  case 'xAI':
app/lib/.server/llm/stream-text.ts CHANGED
@@ -1,5 +1,6 @@
1
- // @ts-nocheck
2
- // Preventing TS checks with files presented in the video for a better presentation.
 
3
  import { streamText as _streamText, convertToCoreMessages } from 'ai';
4
  import { getModel } from '~/lib/.server/llm/model';
5
  import { MAX_TOKENS } from './constants';
@@ -34,19 +35,12 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
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
- export function streamText(
45
- messages: Messages,
46
- env: Env,
47
- options?: StreamingOptions,
48
- apiKeys?: Record<string, string>
49
- ) {
50
  let currentModel = DEFAULT_MODEL;
51
  let currentProvider = DEFAULT_PROVIDER;
52
 
@@ -63,17 +57,12 @@ export function streamText(
63
  return { ...message, content };
64
  }
65
 
66
- return message;
67
  });
68
 
69
  const modelDetails = MODEL_LIST.find((m) => m.name === currentModel);
70
 
71
-
72
-
73
- const dynamicMaxTokens =
74
- modelDetails && modelDetails.maxTokenAllowed
75
- ? modelDetails.maxTokenAllowed
76
- : MAX_TOKENS;
77
 
78
  return _streamText({
79
  model: getModel(currentProvider, currentModel, env, apiKeys),
 
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-nocheck TODO: Provider proper types
3
+
4
  import { streamText as _streamText, convertToCoreMessages } from 'ai';
5
  import { getModel } from '~/lib/.server/llm/model';
6
  import { MAX_TOKENS } from './constants';
 
35
  const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER;
36
 
37
  // Remove model and provider lines from content
38
+ const cleanedContent = message.content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '').trim();
 
 
 
39
 
40
  return { model, provider, content: cleanedContent };
41
  }
42
+
43
+ export function streamText(messages: Messages, env: Env, options?: StreamingOptions, apiKeys?: Record<string, string>) {
 
 
 
 
44
  let currentModel = DEFAULT_MODEL;
45
  let currentProvider = DEFAULT_PROVIDER;
46
 
 
57
  return { ...message, content };
58
  }
59
 
60
+ return message;
61
  });
62
 
63
  const modelDetails = MODEL_LIST.find((m) => m.name === currentModel);
64
 
65
+ const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
 
 
 
 
 
66
 
67
  return _streamText({
68
  model: getModel(currentProvider, currentModel, env, apiKeys),
app/lib/hooks/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './useMessageParser';
2
  export * from './usePromptEnhancer';
3
  export * from './useShortcuts';
4
  export * from './useSnapScroll';
 
 
2
  export * from './usePromptEnhancer';
3
  export * from './useShortcuts';
4
  export * from './useSnapScroll';
5
+ export { default } from './useViewport';
app/lib/hooks/useViewport.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+
3
+ const useViewport = (threshold = 1024) => {
4
+ const [isSmallViewport, setIsSmallViewport] = useState(window.innerWidth < threshold);
5
+
6
+ useEffect(() => {
7
+ const handleResize = () => setIsSmallViewport(window.innerWidth < threshold);
8
+ window.addEventListener('resize', handleResize);
9
+
10
+ return () => {
11
+ window.removeEventListener('resize', handleResize);
12
+ };
13
+ }, [threshold]);
14
+
15
+ return isSmallViewport;
16
+ };
17
+
18
+ export default useViewport;
app/lib/persistence/db.ts CHANGED
@@ -161,11 +161,17 @@ async function getUrlIds(db: IDBDatabase): Promise<string[]> {
161
 
162
  export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
163
  const chat = await getMessages(db, chatId);
164
- if (!chat) throw new Error('Chat not found');
 
 
 
165
 
166
  // Find the index of the message to fork at
167
- const messageIndex = chat.messages.findIndex(msg => msg.id === messageId);
168
- if (messageIndex === -1) throw new Error('Message not found');
 
 
 
169
 
170
  // Get messages up to and including the selected message
171
  const messages = chat.messages.slice(0, messageIndex + 1);
@@ -177,6 +183,7 @@ export async function forkChat(db: IDBDatabase, chatId: string, messageId: strin
177
 
178
  export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
179
  const chat = await getMessages(db, id);
 
180
  if (!chat) {
181
  throw new Error('Chat not found');
182
  }
 
161
 
162
  export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
163
  const chat = await getMessages(db, chatId);
164
+
165
+ if (!chat) {
166
+ throw new Error('Chat not found');
167
+ }
168
 
169
  // Find the index of the message to fork at
170
+ const messageIndex = chat.messages.findIndex((msg) => msg.id === messageId);
171
+
172
+ if (messageIndex === -1) {
173
+ throw new Error('Message not found');
174
+ }
175
 
176
  // Get messages up to and including the selected message
177
  const messages = chat.messages.slice(0, messageIndex + 1);
 
183
 
184
  export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
185
  const chat = await getMessages(db, id);
186
+
187
  if (!chat) {
188
  throw new Error('Chat not found');
189
  }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -107,7 +107,7 @@ export function useChatHistory() {
107
 
108
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
109
  },
110
- duplicateCurrentChat: async (listItemId:string) => {
111
  if (!db || (!mixedId && !listItemId)) {
112
  return;
113
  }
@@ -118,6 +118,7 @@ export function useChatHistory() {
118
  toast.success('Chat duplicated successfully');
119
  } catch (error) {
120
  toast.error('Failed to duplicate chat');
 
121
  }
122
  },
123
  importChat: async (description: string, messages:Message[]) => {
 
107
 
108
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
109
  },
110
+ duplicateCurrentChat: async (listItemId: string) => {
111
  if (!db || (!mixedId && !listItemId)) {
112
  return;
113
  }
 
118
  toast.success('Chat duplicated successfully');
119
  } catch (error) {
120
  toast.error('Failed to duplicate chat');
121
+ console.log(error);
122
  }
123
  },
124
  importChat: async (description: string, messages:Message[]) => {
app/lib/runtime/action-runner.ts CHANGED
@@ -1,11 +1,10 @@
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');
@@ -45,7 +44,6 @@ export class ActionRunner {
45
  constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
46
  this.#webcontainer = webcontainerPromise;
47
  this.#shellTerminal = getShellTerminal;
48
-
49
  }
50
 
51
  addAction(data: ActionCallbackData) {
@@ -88,19 +86,21 @@ export class ActionRunner {
88
  if (action.executed) {
89
  return;
90
  }
 
91
  if (isStreaming && action.type !== 'file') {
92
  return;
93
  }
94
 
95
  this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
- return this.#currentExecutionPromise = this.#currentExecutionPromise
 
98
  .then(() => {
99
- return this.#executeAction(actionId, isStreaming);
100
  })
101
  .catch((error) => {
102
  console.error('Action failed:', error);
103
- });
104
  }
105
 
106
  async #executeAction(actionId: string, isStreaming: boolean = false) {
@@ -121,17 +121,23 @@ export class ActionRunner {
121
  case 'start': {
122
  // making the start app non blocking
123
 
124
- this.#runStartAction(action).then(()=>this.#updateAction(actionId, { status: 'complete' }))
125
- .catch(()=>this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }))
126
- // adding a delay to avoid any race condition between 2 start actions
127
- // i am up for a better approch
128
- await new Promise(resolve=>setTimeout(resolve,2000))
129
- return
130
- break;
 
 
 
 
131
  }
132
  }
133
 
134
- this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
 
 
135
  } catch (error) {
136
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
137
  logger.error(`[${action.type}]:Action failed\n\n`, error);
@@ -145,16 +151,19 @@ export class ActionRunner {
145
  if (action.type !== 'shell') {
146
  unreachable('Expected shell action');
147
  }
148
- const shell = this.#shellTerminal()
149
- await shell.ready()
 
 
150
  if (!shell || !shell.terminal || !shell.process) {
151
  unreachable('Shell terminal not found');
152
  }
153
- const resp = await shell.executeCommand(this.runnerId.get(), action.content)
154
- logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
155
- if (resp?.exitCode != 0) {
156
- throw new Error("Failed To Execute Shell Command");
157
 
 
 
 
 
 
158
  }
159
  }
160
 
@@ -162,21 +171,26 @@ export class ActionRunner {
162
  if (action.type !== 'start') {
163
  unreachable('Expected shell action');
164
  }
 
165
  if (!this.#shellTerminal) {
166
  unreachable('Shell terminal not found');
167
  }
168
- const shell = this.#shellTerminal()
169
- await shell.ready()
 
 
170
  if (!shell || !shell.terminal || !shell.process) {
171
  unreachable('Shell terminal not found');
172
  }
173
- const resp = await shell.executeCommand(this.runnerId.get(), action.content)
174
- logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
 
175
 
176
  if (resp?.exitCode != 0) {
177
- throw new Error("Failed To Start Application");
178
  }
179
- return resp
 
180
  }
181
 
182
  async #runFileAction(action: ActionState) {
 
1
+ import { WebContainer } 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 { BoltShell } from '~/utils/shell';
9
 
10
  const logger = createScopedLogger('ActionRunner');
 
44
  constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
45
  this.#webcontainer = webcontainerPromise;
46
  this.#shellTerminal = getShellTerminal;
 
47
  }
48
 
49
  addAction(data: ActionCallbackData) {
 
86
  if (action.executed) {
87
  return;
88
  }
89
+
90
  if (isStreaming && action.type !== 'file') {
91
  return;
92
  }
93
 
94
  this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
95
 
96
+ // eslint-disable-next-line consistent-return
97
+ return (this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
+ this.#executeAction(actionId, isStreaming);
100
  })
101
  .catch((error) => {
102
  console.error('Action failed:', error);
103
+ }));
104
  }
105
 
106
  async #executeAction(actionId: string, isStreaming: boolean = false) {
 
121
  case 'start': {
122
  // making the start app non blocking
123
 
124
+ this.#runStartAction(action)
125
+ .then(() => this.#updateAction(actionId, { status: 'complete' }))
126
+ .catch(() => this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }));
127
+
128
+ /*
129
+ * adding a delay to avoid any race condition between 2 start actions
130
+ * i am up for a better approach
131
+ */
132
+ await new Promise((resolve) => setTimeout(resolve, 2000));
133
+
134
+ return;
135
  }
136
  }
137
 
138
+ this.#updateAction(actionId, {
139
+ status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
140
+ });
141
  } catch (error) {
142
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
143
  logger.error(`[${action.type}]:Action failed\n\n`, error);
 
151
  if (action.type !== 'shell') {
152
  unreachable('Expected shell action');
153
  }
154
+
155
+ const shell = this.#shellTerminal();
156
+ await shell.ready();
157
+
158
  if (!shell || !shell.terminal || !shell.process) {
159
  unreachable('Shell terminal not found');
160
  }
 
 
 
 
161
 
162
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content);
163
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
164
+
165
+ if (resp?.exitCode != 0) {
166
+ throw new Error('Failed To Execute Shell Command');
167
  }
168
  }
169
 
 
171
  if (action.type !== 'start') {
172
  unreachable('Expected shell action');
173
  }
174
+
175
  if (!this.#shellTerminal) {
176
  unreachable('Shell terminal not found');
177
  }
178
+
179
+ const shell = this.#shellTerminal();
180
+ await shell.ready();
181
+
182
  if (!shell || !shell.terminal || !shell.process) {
183
  unreachable('Shell terminal not found');
184
  }
185
+
186
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content);
187
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
188
 
189
  if (resp?.exitCode != 0) {
190
+ throw new Error('Failed To Start Application');
191
  }
192
+
193
+ return resp;
194
  }
195
 
196
  async #runFileAction(action: ActionState) {
app/lib/runtime/message-parser.ts CHANGED
@@ -55,7 +55,7 @@ interface MessageState {
55
  export class StreamingMessageParser {
56
  #messages = new Map<string, MessageState>();
57
 
58
- constructor(private _options: StreamingMessageParserOptions = {}) { }
59
 
60
  parse(messageId: string, input: string) {
61
  let state = this.#messages.get(messageId);
@@ -120,20 +120,20 @@ export class StreamingMessageParser {
120
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
121
  } else {
122
  if ('type' in currentAction && currentAction.type === 'file') {
123
- let content = input.slice(i);
124
 
125
  this._options.callbacks?.onActionStream?.({
126
  artifactId: currentArtifact.id,
127
  messageId,
128
  actionId: String(state.actionId - 1),
129
  action: {
130
- ...currentAction as FileAction,
131
  content,
132
  filePath: currentAction.filePath,
133
  },
134
-
135
  });
136
  }
 
137
  break;
138
  }
139
  } else {
@@ -272,7 +272,7 @@ export class StreamingMessageParser {
272
  }
273
 
274
  (actionAttributes as FileAction).filePath = filePath;
275
- } else if (!(['shell', 'start'].includes(actionType))) {
276
  logger.warn(`Unknown action type '${actionType}'`);
277
  }
278
 
 
55
  export class StreamingMessageParser {
56
  #messages = new Map<string, MessageState>();
57
 
58
+ constructor(private _options: StreamingMessageParserOptions = {}) {}
59
 
60
  parse(messageId: string, input: string) {
61
  let state = this.#messages.get(messageId);
 
120
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
121
  } else {
122
  if ('type' in currentAction && currentAction.type === 'file') {
123
+ const content = input.slice(i);
124
 
125
  this._options.callbacks?.onActionStream?.({
126
  artifactId: currentArtifact.id,
127
  messageId,
128
  actionId: String(state.actionId - 1),
129
  action: {
130
+ ...(currentAction as FileAction),
131
  content,
132
  filePath: currentAction.filePath,
133
  },
 
134
  });
135
  }
136
+
137
  break;
138
  }
139
  } else {
 
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
@@ -7,7 +7,7 @@ import { coloredText } from '~/utils/terminal';
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
 
@@ -27,8 +27,8 @@ export class TerminalStore {
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;
 
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
 
 
27
  }
28
  async attachBoltTerminal(terminal: ITerminal) {
29
  try {
30
+ const 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;
app/lib/stores/workbench.ts CHANGED
@@ -11,9 +11,8 @@ import { PreviewsStore } from './previews';
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
- import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
15
  import * as nodePath from 'node:path';
16
- import type { WebContainerProcess } from '@webcontainer/api';
17
  import { extractRelativePath } from '~/utils/diff';
18
 
19
  export interface ArtifactState {
@@ -42,8 +41,7 @@ export class WorkbenchStore {
42
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
43
  modifiedFiles = new Set<string>();
44
  artifactIdList: string[] = [];
45
- #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
46
- #globalExecutionQueue=Promise.resolve();
47
  constructor() {
48
  if (import.meta.hot) {
49
  import.meta.hot.data.artifacts = this.artifacts;
@@ -54,7 +52,7 @@ export class WorkbenchStore {
54
  }
55
 
56
  addToExecutionQueue(callback: () => Promise<void>) {
57
- this.#globalExecutionQueue=this.#globalExecutionQueue.then(()=>callback())
58
  }
59
 
60
  get previews() {
@@ -96,7 +94,6 @@ export class WorkbenchStore {
96
  this.#terminalStore.attachTerminal(terminal);
97
  }
98
  attachBoltTerminal(terminal: ITerminal) {
99
-
100
  this.#terminalStore.attachBoltTerminal(terminal);
101
  }
102
 
@@ -261,7 +258,8 @@ export class WorkbenchStore {
261
  this.artifacts.setKey(messageId, { ...artifact, ...state });
262
  }
263
  addAction(data: ActionCallbackData) {
264
- this._addAction(data)
 
265
  // this.addToExecutionQueue(()=>this._addAction(data))
266
  }
267
  async _addAction(data: ActionCallbackData) {
@@ -277,11 +275,10 @@ export class WorkbenchStore {
277
  }
278
 
279
  runAction(data: ActionCallbackData, isStreaming: boolean = false) {
280
- if(isStreaming) {
281
- this._runAction(data, isStreaming)
282
- }
283
- else{
284
- this.addToExecutionQueue(()=>this._runAction(data, isStreaming))
285
  }
286
  }
287
  async _runAction(data: ActionCallbackData, isStreaming: boolean = false) {
@@ -292,16 +289,21 @@ export class WorkbenchStore {
292
  if (!artifact) {
293
  unreachable('Artifact not found');
294
  }
 
295
  if (data.action.type === 'file') {
296
- let wc = await webcontainer
297
  const fullPath = nodePath.join(wc.workdir, data.action.filePath);
 
298
  if (this.selectedFile.value !== fullPath) {
299
  this.setSelectedFile(fullPath);
300
  }
 
301
  if (this.currentView.value !== 'code') {
302
  this.currentView.set('code');
303
  }
 
304
  const doc = this.#editorStore.documents.get()[fullPath];
 
305
  if (!doc) {
306
  await artifact.runner.runAction(data, isStreaming);
307
  }
@@ -382,7 +384,6 @@ export class WorkbenchStore {
382
  }
383
 
384
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
385
-
386
  try {
387
  // Get the GitHub auth token from environment variables
388
  const githubToken = ghToken;
@@ -397,10 +398,11 @@ export class WorkbenchStore {
397
  const octokit = new Octokit({ auth: githubToken });
398
 
399
  // Check if the repository already exists before creating it
400
- let repo: RestEndpointMethodTypes["repos"]["get"]["response"]['data']
 
401
  try {
402
- let resp = await octokit.repos.get({ owner: owner, repo: repoName });
403
- repo = resp.data
404
  } catch (error) {
405
  if (error instanceof Error && 'status' in error && error.status === 404) {
406
  // Repository doesn't exist, so create a new one
@@ -418,6 +420,7 @@ export class WorkbenchStore {
418
 
419
  // Get all files
420
  const files = this.files.get();
 
421
  if (!files || Object.keys(files).length === 0) {
422
  throw new Error('No files found to push');
423
  }
@@ -434,7 +437,9 @@ export class WorkbenchStore {
434
  });
435
  return { path: extractRelativePath(filePath), sha: blob.sha };
436
  }
437
- })
 
 
438
  );
439
 
440
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
 
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
+ import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
15
  import * as nodePath from 'node:path';
 
16
  import { extractRelativePath } from '~/utils/diff';
17
 
18
  export interface ArtifactState {
 
41
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
42
  modifiedFiles = new Set<string>();
43
  artifactIdList: string[] = [];
44
+ #globalExecutionQueue = Promise.resolve();
 
45
  constructor() {
46
  if (import.meta.hot) {
47
  import.meta.hot.data.artifacts = this.artifacts;
 
52
  }
53
 
54
  addToExecutionQueue(callback: () => Promise<void>) {
55
+ this.#globalExecutionQueue = this.#globalExecutionQueue.then(() => callback());
56
  }
57
 
58
  get previews() {
 
94
  this.#terminalStore.attachTerminal(terminal);
95
  }
96
  attachBoltTerminal(terminal: ITerminal) {
 
97
  this.#terminalStore.attachBoltTerminal(terminal);
98
  }
99
 
 
258
  this.artifacts.setKey(messageId, { ...artifact, ...state });
259
  }
260
  addAction(data: ActionCallbackData) {
261
+ this._addAction(data);
262
+
263
  // this.addToExecutionQueue(()=>this._addAction(data))
264
  }
265
  async _addAction(data: ActionCallbackData) {
 
275
  }
276
 
277
  runAction(data: ActionCallbackData, isStreaming: boolean = false) {
278
+ if (isStreaming) {
279
+ this._runAction(data, isStreaming);
280
+ } else {
281
+ this.addToExecutionQueue(() => this._runAction(data, isStreaming));
 
282
  }
283
  }
284
  async _runAction(data: ActionCallbackData, isStreaming: boolean = false) {
 
289
  if (!artifact) {
290
  unreachable('Artifact not found');
291
  }
292
+
293
  if (data.action.type === 'file') {
294
+ const wc = await webcontainer;
295
  const fullPath = nodePath.join(wc.workdir, data.action.filePath);
296
+
297
  if (this.selectedFile.value !== fullPath) {
298
  this.setSelectedFile(fullPath);
299
  }
300
+
301
  if (this.currentView.value !== 'code') {
302
  this.currentView.set('code');
303
  }
304
+
305
  const doc = this.#editorStore.documents.get()[fullPath];
306
+
307
  if (!doc) {
308
  await artifact.runner.runAction(data, isStreaming);
309
  }
 
384
  }
385
 
386
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
 
387
  try {
388
  // Get the GitHub auth token from environment variables
389
  const githubToken = ghToken;
 
398
  const octokit = new Octokit({ auth: githubToken });
399
 
400
  // Check if the repository already exists before creating it
401
+ let repo: RestEndpointMethodTypes['repos']['get']['response']['data'];
402
+
403
  try {
404
+ const resp = await octokit.repos.get({ owner, repo: repoName });
405
+ repo = resp.data;
406
  } catch (error) {
407
  if (error instanceof Error && 'status' in error && error.status === 404) {
408
  // Repository doesn't exist, so create a new one
 
420
 
421
  // Get all files
422
  const files = this.files.get();
423
+
424
  if (!files || Object.keys(files).length === 0) {
425
  throw new Error('No files found to push');
426
  }
 
437
  });
438
  return { path: extractRelativePath(filePath), sha: blob.sha };
439
  }
440
+
441
+ return null;
442
+ }),
443
  );
444
 
445
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
app/routes/api.chat.ts CHANGED
@@ -1,5 +1,6 @@
1
- // @ts-nocheck
2
- // Preventing TS checks with files presented in the video for a better presentation.
 
3
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
4
  import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
5
  import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
@@ -14,14 +15,15 @@ function parseCookies(cookieHeader) {
14
  const cookies = {};
15
 
16
  // Split the cookie string by semicolons and spaces
17
- const items = cookieHeader.split(";").map(cookie => cookie.trim());
 
 
 
18
 
19
- items.forEach(item => {
20
- const [name, ...rest] = item.split("=");
21
  if (name && rest) {
22
  // Decode the name and value, and join value parts in case it contains '='
23
  const decodedName = decodeURIComponent(name.trim());
24
- const decodedValue = decodeURIComponent(rest.join("=").trim());
25
  cookies[decodedName] = decodedValue;
26
  }
27
  });
@@ -31,13 +33,13 @@ function parseCookies(cookieHeader) {
31
 
32
  async function chatAction({ context, request }: ActionFunctionArgs) {
33
  const { messages } = await request.json<{
34
- messages: Messages
35
  }>();
36
 
37
- const cookieHeader = request.headers.get("Cookie");
38
 
39
  // Parse the cookie's value (returns an object or null if no cookie exists)
40
- const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || "{}");
41
 
42
  const stream = new SwitchableStream();
43
 
@@ -83,7 +85,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
83
  if (error.message?.includes('API key')) {
84
  throw new Response('Invalid or missing API key', {
85
  status: 401,
86
- statusText: 'Unauthorized'
87
  });
88
  }
89
 
 
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-nocheck TODO: Provider proper types
3
+
4
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
5
  import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
6
  import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
 
15
  const cookies = {};
16
 
17
  // Split the cookie string by semicolons and spaces
18
+ const items = cookieHeader.split(';').map((cookie) => cookie.trim());
19
+
20
+ items.forEach((item) => {
21
+ const [name, ...rest] = item.split('=');
22
 
 
 
23
  if (name && rest) {
24
  // Decode the name and value, and join value parts in case it contains '='
25
  const decodedName = decodeURIComponent(name.trim());
26
+ const decodedValue = decodeURIComponent(rest.join('=').trim());
27
  cookies[decodedName] = decodedValue;
28
  }
29
  });
 
33
 
34
  async function chatAction({ context, request }: ActionFunctionArgs) {
35
  const { messages } = await request.json<{
36
+ messages: Messages;
37
  }>();
38
 
39
+ const cookieHeader = request.headers.get('Cookie');
40
 
41
  // Parse the cookie's value (returns an object or null if no cookie exists)
42
+ const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || '{}');
43
 
44
  const stream = new SwitchableStream();
45
 
 
85
  if (error.message?.includes('API key')) {
86
  throw new Response('Invalid or missing API key', {
87
  status: 401,
88
+ statusText: 'Unauthorized',
89
  });
90
  }
91
 
app/types/model.ts CHANGED
@@ -1,10 +1,10 @@
1
  import type { ModelInfo } from '~/utils/types';
2
 
3
  export type ProviderInfo = {
4
- staticModels: ModelInfo[],
5
- name: string,
6
- getDynamicModels?: () => Promise<ModelInfo[]>,
7
- getApiKeyLink?: string,
8
- labelForGetApiKey?: string,
9
- icon?:string,
10
  };
 
1
  import type { ModelInfo } from '~/utils/types';
2
 
3
  export type ProviderInfo = {
4
+ staticModels: ModelInfo[];
5
+ name: string;
6
+ getDynamicModels?: () => Promise<ModelInfo[]>;
7
+ getApiKeyLink?: string;
8
+ labelForGetApiKey?: string;
9
+ icon?: string;
10
  };
app/utils/constants.ts CHANGED
@@ -12,26 +12,42 @@ const PROVIDER_LIST: ProviderInfo[] = [
12
  {
13
  name: 'Anthropic',
14
  staticModels: [
15
- { name: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (new)', provider: 'Anthropic', maxTokenAllowed: 8000 },
16
- { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet (old)', provider: 'Anthropic', maxTokenAllowed: 8000 },
17
- { name: 'claude-3-5-haiku-latest', label: 'Claude 3.5 Haiku (new)', provider: 'Anthropic', maxTokenAllowed: 8000 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  { name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 },
19
  { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 },
20
- { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 }
21
  ],
22
- getApiKeyLink: "https://console.anthropic.com/settings/keys",
23
  },
24
  {
25
  name: 'Ollama',
26
  staticModels: [],
27
  getDynamicModels: getOllamaModels,
28
- getApiKeyLink: "https://ollama.com/download",
29
- labelForGetApiKey: "Download Ollama",
30
- icon: "i-ph:cloud-arrow-down",
31
- }, {
 
32
  name: 'OpenAILike',
33
  staticModels: [],
34
- getDynamicModels: getOpenAILikeModels
35
  },
36
  {
37
  name: 'Cohere',
@@ -47,7 +63,7 @@ const PROVIDER_LIST: ProviderInfo[] = [
47
  { name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 },
48
  { name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 },
49
  ],
50
- getApiKeyLink: 'https://dashboard.cohere.com/api-keys'
51
  },
52
  {
53
  name: 'OpenRouter',
@@ -56,22 +72,52 @@ const PROVIDER_LIST: ProviderInfo[] = [
56
  {
57
  name: 'anthropic/claude-3.5-sonnet',
58
  label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
59
- provider: 'OpenRouter'
60
- , maxTokenAllowed: 8000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  },
62
- { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
63
- { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
64
- { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
65
- { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
66
  { name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
67
- { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
68
- { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
69
- { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 4096 }
 
 
 
 
 
 
 
 
 
 
70
  ],
71
  getDynamicModels: getOpenRouterModels,
72
  getApiKeyLink: 'https://openrouter.ai/settings/keys',
73
-
74
- }, {
75
  name: 'Google',
76
  staticModels: [
77
  { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
@@ -79,29 +125,92 @@ const PROVIDER_LIST: ProviderInfo[] = [
79
  { name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
80
  { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
81
  { name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
82
- { name: 'gemini-exp-1114', label: 'Gemini exp-1114', provider: 'Google', maxTokenAllowed: 8192 }
83
  ],
84
- getApiKeyLink: 'https://aistudio.google.com/app/apikey'
85
- }, {
 
86
  name: 'Groq',
87
  staticModels: [
88
  { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
89
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
90
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
91
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
92
- { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }
93
  ],
94
- getApiKeyLink: 'https://console.groq.com/keys'
95
  },
96
  {
97
  name: 'HuggingFace',
98
  staticModels: [
99
- { name: 'Qwen/Qwen2.5-Coder-32B-Instruct', label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)', provider: 'HuggingFace', maxTokenAllowed: 8000 },
100
- { name: '01-ai/Yi-1.5-34B-Chat', label: 'Yi-1.5-34B-Chat (HuggingFace)', provider: 'HuggingFace', maxTokenAllowed: 8000 },
101
- { name: 'codellama/CodeLlama-34b-Instruct-hf', label: 'CodeLlama-34b-Instruct (HuggingFace)', provider: 'HuggingFace', maxTokenAllowed: 8000 },
102
- { name: 'NousResearch/Hermes-3-Llama-3.1-8B', label: 'Hermes-3-Llama-3.1-8B (HuggingFace)', provider: 'HuggingFace', maxTokenAllowed: 8000 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  ],
104
- getApiKeyLink: 'https://huggingface.co/settings/tokens'
105
  },
106
 
107
  {
@@ -110,23 +219,24 @@ const PROVIDER_LIST: ProviderInfo[] = [
110
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 },
111
  { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
112
  { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 },
113
- { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 }
114
  ],
115
- getApiKeyLink: "https://platform.openai.com/api-keys",
116
- }, {
 
117
  name: 'xAI',
118
- staticModels: [
119
- { name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 }
120
- ],
121
- getApiKeyLink: 'https://docs.x.ai/docs/quickstart#creating-an-api-key'
122
- }, {
123
  name: 'Deepseek',
124
  staticModels: [
125
  { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
126
- { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 }
127
  ],
128
- getApiKeyLink: 'https://platform.deepseek.com/api_keys'
129
- }, {
 
130
  name: 'Mistral',
131
  staticModels: [
132
  { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 },
@@ -137,27 +247,29 @@ const PROVIDER_LIST: ProviderInfo[] = [
137
  { name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 },
138
  { name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 },
139
  { name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 },
140
- { name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', maxTokenAllowed: 8000 }
141
  ],
142
- getApiKeyLink: 'https://console.mistral.ai/api-keys/'
143
- }, {
 
144
  name: 'LMStudio',
145
  staticModels: [],
146
  getDynamicModels: getLMStudioModels,
147
  getApiKeyLink: 'https://lmstudio.ai/',
148
  labelForGetApiKey: 'Get LMStudio',
149
- icon: "i-ph:cloud-arrow-down",
150
- }
151
  ];
152
 
153
  export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
154
 
155
- const staticModels: ModelInfo[] = PROVIDER_LIST.map(p => p.staticModels).flat();
156
 
157
  export let MODEL_LIST: ModelInfo[] = [...staticModels];
158
 
159
  const getOllamaBaseUrl = () => {
160
  const defaultBaseUrl = import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
 
161
  // Check if we're in the browser
162
  if (typeof window !== 'undefined') {
163
  // Frontend always uses localhost
@@ -167,23 +279,22 @@ const getOllamaBaseUrl = () => {
167
  // Backend: Check if we're running in Docker
168
  const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
169
 
170
- return isDocker
171
- ? defaultBaseUrl.replace('localhost', 'host.docker.internal')
172
- : defaultBaseUrl;
173
  };
174
 
175
  async function getOllamaModels(): Promise<ModelInfo[]> {
176
  try {
177
- const base_url = getOllamaBaseUrl();
178
- const response = await fetch(`${base_url}/api/tags`);
179
- const data = await response.json() as OllamaApiResponse;
180
 
181
  return data.models.map((model: OllamaModel) => ({
182
  name: model.name,
183
  label: `${model.name} (${model.details.parameter_size})`,
184
  provider: 'Ollama',
185
- maxTokenAllowed:8000,
186
  }));
 
187
  } catch (e) {
188
  return [];
189
  }
@@ -191,22 +302,26 @@ async function getOllamaModels(): Promise<ModelInfo[]> {
191
 
192
  async function getOpenAILikeModels(): Promise<ModelInfo[]> {
193
  try {
194
- const base_url = import.meta.env.OPENAI_LIKE_API_BASE_URL || '';
195
- if (!base_url) {
 
196
  return [];
197
  }
198
- const api_key = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
199
- const response = await fetch(`${base_url}/models`, {
 
200
  headers: {
201
- Authorization: `Bearer ${api_key}`
202
- }
203
  });
204
- const res = await response.json() as any;
 
205
  return res.data.map((model: any) => ({
206
  name: model.id,
207
  label: model.id,
208
- provider: 'OpenAILike'
209
  }));
 
210
  } catch (e) {
211
  return [];
212
  }
@@ -220,51 +335,67 @@ type OpenRouterModelsResponse = {
220
  pricing: {
221
  prompt: number;
222
  completion: number;
223
- }
224
- }[]
225
  };
226
 
227
  async function getOpenRouterModels(): Promise<ModelInfo[]> {
228
- const data: OpenRouterModelsResponse = await (await fetch('https://openrouter.ai/api/v1/models', {
229
- headers: {
230
- 'Content-Type': 'application/json'
231
- }
232
- })).json();
 
 
233
 
234
- return data.data.sort((a, b) => a.name.localeCompare(b.name)).map(m => ({
235
- name: m.id,
236
- label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(
237
- 2)} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(
238
- m.context_length / 1000)}k`,
239
- provider: 'OpenRouter',
240
- maxTokenAllowed:8000,
241
- }));
 
 
242
  }
243
 
244
  async function getLMStudioModels(): Promise<ModelInfo[]> {
245
  try {
246
- const base_url = import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
247
- const response = await fetch(`${base_url}/v1/models`);
248
- const data = await response.json() as any;
 
249
  return data.data.map((model: any) => ({
250
  name: model.id,
251
  label: model.id,
252
- provider: 'LMStudio'
253
  }));
 
254
  } catch (e) {
255
  return [];
256
  }
257
  }
258
 
259
-
260
-
261
  async function initializeModelList(): Promise<ModelInfo[]> {
262
- MODEL_LIST = [...(await Promise.all(
263
- PROVIDER_LIST
264
- .filter((p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels)
265
- .map(p => p.getDynamicModels())))
266
- .flat(), ...staticModels];
 
 
 
 
 
267
  return MODEL_LIST;
268
  }
269
 
270
- export { getOllamaModels, getOpenAILikeModels, getLMStudioModels, initializeModelList, getOpenRouterModels, PROVIDER_LIST };
 
 
 
 
 
 
 
 
12
  {
13
  name: 'Anthropic',
14
  staticModels: [
15
+ {
16
+ name: 'claude-3-5-sonnet-latest',
17
+ label: 'Claude 3.5 Sonnet (new)',
18
+ provider: 'Anthropic',
19
+ maxTokenAllowed: 8000,
20
+ },
21
+ {
22
+ name: 'claude-3-5-sonnet-20240620',
23
+ label: 'Claude 3.5 Sonnet (old)',
24
+ provider: 'Anthropic',
25
+ maxTokenAllowed: 8000,
26
+ },
27
+ {
28
+ name: 'claude-3-5-haiku-latest',
29
+ label: 'Claude 3.5 Haiku (new)',
30
+ provider: 'Anthropic',
31
+ maxTokenAllowed: 8000,
32
+ },
33
  { name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 },
34
  { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 },
35
+ { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 },
36
  ],
37
+ getApiKeyLink: 'https://console.anthropic.com/settings/keys',
38
  },
39
  {
40
  name: 'Ollama',
41
  staticModels: [],
42
  getDynamicModels: getOllamaModels,
43
+ getApiKeyLink: 'https://ollama.com/download',
44
+ labelForGetApiKey: 'Download Ollama',
45
+ icon: 'i-ph:cloud-arrow-down',
46
+ },
47
+ {
48
  name: 'OpenAILike',
49
  staticModels: [],
50
+ getDynamicModels: getOpenAILikeModels,
51
  },
52
  {
53
  name: 'Cohere',
 
63
  { name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 },
64
  { name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 },
65
  ],
66
+ getApiKeyLink: 'https://dashboard.cohere.com/api-keys',
67
  },
68
  {
69
  name: 'OpenRouter',
 
72
  {
73
  name: 'anthropic/claude-3.5-sonnet',
74
  label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
75
+ provider: 'OpenRouter',
76
+ maxTokenAllowed: 8000,
77
+ },
78
+ {
79
+ name: 'anthropic/claude-3-haiku',
80
+ label: 'Anthropic: Claude 3 Haiku (OpenRouter)',
81
+ provider: 'OpenRouter',
82
+ maxTokenAllowed: 8000,
83
+ },
84
+ {
85
+ name: 'deepseek/deepseek-coder',
86
+ label: 'Deepseek-Coder V2 236B (OpenRouter)',
87
+ provider: 'OpenRouter',
88
+ maxTokenAllowed: 8000,
89
+ },
90
+ {
91
+ name: 'google/gemini-flash-1.5',
92
+ label: 'Google Gemini Flash 1.5 (OpenRouter)',
93
+ provider: 'OpenRouter',
94
+ maxTokenAllowed: 8000,
95
+ },
96
+ {
97
+ name: 'google/gemini-pro-1.5',
98
+ label: 'Google Gemini Pro 1.5 (OpenRouter)',
99
+ provider: 'OpenRouter',
100
+ maxTokenAllowed: 8000,
101
  },
 
 
 
 
102
  { name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
103
+ {
104
+ name: 'mistralai/mistral-nemo',
105
+ label: 'OpenRouter Mistral Nemo (OpenRouter)',
106
+ provider: 'OpenRouter',
107
+ maxTokenAllowed: 8000,
108
+ },
109
+ {
110
+ name: 'qwen/qwen-110b-chat',
111
+ label: 'OpenRouter Qwen 110b Chat (OpenRouter)',
112
+ provider: 'OpenRouter',
113
+ maxTokenAllowed: 8000,
114
+ },
115
+ { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 4096 },
116
  ],
117
  getDynamicModels: getOpenRouterModels,
118
  getApiKeyLink: 'https://openrouter.ai/settings/keys',
119
+ },
120
+ {
121
  name: 'Google',
122
  staticModels: [
123
  { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
 
125
  { name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
126
  { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
127
  { name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
128
+ { name: 'gemini-exp-1121', label: 'Gemini exp-1121', provider: 'Google', maxTokenAllowed: 8192 },
129
  ],
130
+ getApiKeyLink: 'https://aistudio.google.com/app/apikey',
131
+ },
132
+ {
133
  name: 'Groq',
134
  staticModels: [
135
  { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
136
  { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
137
  { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
138
  { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
139
+ { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
140
  ],
141
+ getApiKeyLink: 'https://console.groq.com/keys',
142
  },
143
  {
144
  name: 'HuggingFace',
145
  staticModels: [
146
+ {
147
+ name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
148
+ label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
149
+ provider: 'HuggingFace',
150
+ maxTokenAllowed: 8000,
151
+ },
152
+ {
153
+ name: '01-ai/Yi-1.5-34B-Chat',
154
+ label: 'Yi-1.5-34B-Chat (HuggingFace)',
155
+ provider: 'HuggingFace',
156
+ maxTokenAllowed: 8000,
157
+ },
158
+ {
159
+ name: 'codellama/CodeLlama-34b-Instruct-hf',
160
+ label: 'CodeLlama-34b-Instruct (HuggingFace)',
161
+ provider: 'HuggingFace',
162
+ maxTokenAllowed: 8000,
163
+ },
164
+ {
165
+ name: 'NousResearch/Hermes-3-Llama-3.1-8B',
166
+ label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
167
+ provider: 'HuggingFace',
168
+ maxTokenAllowed: 8000,
169
+ },
170
+ {
171
+ name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
172
+ label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
173
+ provider: 'HuggingFace',
174
+ maxTokenAllowed: 8000,
175
+ },
176
+ {
177
+ name: 'Qwen/Qwen2.5-72B-Instruct',
178
+ label: 'Qwen2.5-72B-Instruct (HuggingFace)',
179
+ provider: 'HuggingFace',
180
+ maxTokenAllowed: 8000,
181
+ },
182
+ {
183
+ name: 'meta-llama/Llama-3.1-70B-Instruct',
184
+ label: 'Llama-3.1-70B-Instruct (HuggingFace)',
185
+ provider: 'HuggingFace',
186
+ maxTokenAllowed: 8000,
187
+ },
188
+ {
189
+ name: 'meta-llama/Llama-3.1-405B',
190
+ label: 'Llama-3.1-405B (HuggingFace)',
191
+ provider: 'HuggingFace',
192
+ maxTokenAllowed: 8000,
193
+ },
194
+ {
195
+ name: '01-ai/Yi-1.5-34B-Chat',
196
+ label: 'Yi-1.5-34B-Chat (HuggingFace)',
197
+ provider: 'HuggingFace',
198
+ maxTokenAllowed: 8000,
199
+ },
200
+ {
201
+ name: 'codellama/CodeLlama-34b-Instruct-hf',
202
+ label: 'CodeLlama-34b-Instruct (HuggingFace)',
203
+ provider: 'HuggingFace',
204
+ maxTokenAllowed: 8000,
205
+ },
206
+ {
207
+ name: 'NousResearch/Hermes-3-Llama-3.1-8B',
208
+ label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
209
+ provider: 'HuggingFace',
210
+ maxTokenAllowed: 8000,
211
+ },
212
  ],
213
+ getApiKeyLink: 'https://huggingface.co/settings/tokens',
214
  },
215
 
216
  {
 
219
  { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 },
220
  { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
221
  { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 },
222
+ { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
223
  ],
224
+ getApiKeyLink: 'https://platform.openai.com/api-keys',
225
+ },
226
+ {
227
  name: 'xAI',
228
+ staticModels: [{ name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 }],
229
+ getApiKeyLink: 'https://docs.x.ai/docs/quickstart#creating-an-api-key',
230
+ },
231
+ {
 
232
  name: 'Deepseek',
233
  staticModels: [
234
  { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
235
+ { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 },
236
  ],
237
+ getApiKeyLink: 'https://platform.deepseek.com/apiKeys',
238
+ },
239
+ {
240
  name: 'Mistral',
241
  staticModels: [
242
  { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 },
 
247
  { name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 },
248
  { name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 },
249
  { name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 },
250
+ { name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', maxTokenAllowed: 8000 },
251
  ],
252
+ getApiKeyLink: 'https://console.mistral.ai/api-keys/',
253
+ },
254
+ {
255
  name: 'LMStudio',
256
  staticModels: [],
257
  getDynamicModels: getLMStudioModels,
258
  getApiKeyLink: 'https://lmstudio.ai/',
259
  labelForGetApiKey: 'Get LMStudio',
260
+ icon: 'i-ph:cloud-arrow-down',
261
+ },
262
  ];
263
 
264
  export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
265
 
266
+ const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat();
267
 
268
  export let MODEL_LIST: ModelInfo[] = [...staticModels];
269
 
270
  const getOllamaBaseUrl = () => {
271
  const defaultBaseUrl = import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434';
272
+
273
  // Check if we're in the browser
274
  if (typeof window !== 'undefined') {
275
  // Frontend always uses localhost
 
279
  // Backend: Check if we're running in Docker
280
  const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
281
 
282
+ return isDocker ? defaultBaseUrl.replace('localhost', 'host.docker.internal') : defaultBaseUrl;
 
 
283
  };
284
 
285
  async function getOllamaModels(): Promise<ModelInfo[]> {
286
  try {
287
+ const baseUrl = getOllamaBaseUrl();
288
+ const response = await fetch(`${baseUrl}/api/tags`);
289
+ const data = (await response.json()) as OllamaApiResponse;
290
 
291
  return data.models.map((model: OllamaModel) => ({
292
  name: model.name,
293
  label: `${model.name} (${model.details.parameter_size})`,
294
  provider: 'Ollama',
295
+ maxTokenAllowed: 8000,
296
  }));
297
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
298
  } catch (e) {
299
  return [];
300
  }
 
302
 
303
  async function getOpenAILikeModels(): Promise<ModelInfo[]> {
304
  try {
305
+ const baseUrl = import.meta.env.OPENAI_LIKE_API_BASE_URL || '';
306
+
307
+ if (!baseUrl) {
308
  return [];
309
  }
310
+
311
+ const apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? '';
312
+ const response = await fetch(`${baseUrl}/models`, {
313
  headers: {
314
+ Authorization: `Bearer ${apiKey}`,
315
+ },
316
  });
317
+ const res = (await response.json()) as any;
318
+
319
  return res.data.map((model: any) => ({
320
  name: model.id,
321
  label: model.id,
322
+ provider: 'OpenAILike',
323
  }));
324
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
325
  } catch (e) {
326
  return [];
327
  }
 
335
  pricing: {
336
  prompt: number;
337
  completion: number;
338
+ };
339
+ }[];
340
  };
341
 
342
  async function getOpenRouterModels(): Promise<ModelInfo[]> {
343
+ const data: OpenRouterModelsResponse = await (
344
+ await fetch('https://openrouter.ai/api/v1/models', {
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ },
348
+ })
349
+ ).json();
350
 
351
+ return data.data
352
+ .sort((a, b) => a.name.localeCompare(b.name))
353
+ .map((m) => ({
354
+ name: m.id,
355
+ label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(
356
+ 2,
357
+ )} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
358
+ provider: 'OpenRouter',
359
+ maxTokenAllowed: 8000,
360
+ }));
361
  }
362
 
363
  async function getLMStudioModels(): Promise<ModelInfo[]> {
364
  try {
365
+ const baseUrl = import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
366
+ const response = await fetch(`${baseUrl}/v1/models`);
367
+ const data = (await response.json()) as any;
368
+
369
  return data.data.map((model: any) => ({
370
  name: model.id,
371
  label: model.id,
372
+ provider: 'LMStudio',
373
  }));
374
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
375
  } catch (e) {
376
  return [];
377
  }
378
  }
379
 
 
 
380
  async function initializeModelList(): Promise<ModelInfo[]> {
381
+ MODEL_LIST = [
382
+ ...(
383
+ await Promise.all(
384
+ PROVIDER_LIST.filter(
385
+ (p): p is ProviderInfo & { getDynamicModels: () => Promise<ModelInfo[]> } => !!p.getDynamicModels,
386
+ ).map((p) => p.getDynamicModels()),
387
+ )
388
+ ).flat(),
389
+ ...staticModels,
390
+ ];
391
  return MODEL_LIST;
392
  }
393
 
394
+ export {
395
+ getOllamaModels,
396
+ getOpenAILikeModels,
397
+ getLMStudioModels,
398
+ initializeModelList,
399
+ getOpenRouterModels,
400
+ PROVIDER_LIST,
401
+ };
app/utils/shell.ts CHANGED
@@ -52,67 +52,77 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
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
 
@@ -126,6 +136,7 @@ export class BoltShell {
126
 
127
  const input = process.input.getWriter();
128
  this.#shellInputStream = input;
 
129
  const [internalOutput, terminalOutput] = process.output.tee();
130
 
131
  const jshReady = withResolvers<void>();
@@ -162,34 +173,48 @@ export class BoltShell {
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
  }
 
52
  return process;
53
  }
54
 
55
+ export type ExecutionResult = { output: string; exitCode: number } | undefined;
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
+
67
  constructor() {
68
  this.#readyPromise = new Promise((resolve) => {
69
+ this.#initialized = resolve;
70
+ });
71
  }
72
+
73
  ready() {
74
  return this.#readyPromise;
75
  }
76
+
77
  async init(webcontainer: WebContainer, terminal: ITerminal) {
78
+ this.#webcontainer = webcontainer;
79
+ this.#terminal = terminal;
80
+
81
+ const { process, output } = await this.newBoltShellProcess(webcontainer, terminal);
82
+ this.#process = process;
83
+ this.#outputStream = output.getReader();
84
+ await this.waitTillOscCode('interactive');
85
+ this.#initialized?.();
 
 
86
  }
87
+
88
  get terminal() {
89
+ return this.#terminal;
90
  }
91
+
92
  get process() {
93
+ return this.#process;
94
  }
95
+
96
+ async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> {
97
  if (!this.process || !this.terminal) {
98
+ return undefined;
99
  }
 
100
 
101
+ const state = this.executionState.get();
102
+
103
+ /*
104
+ * interrupt the current execution
105
+ * this.#shellInputStream?.write('\x03');
106
+ */
107
  this.terminal.input('\x03');
108
+
109
  if (state && state.executionPrms) {
110
+ await state.executionPrms;
111
  }
112
+
113
  //start a new execution
114
  this.terminal.input(command.trim() + '\n');
115
 
116
  //wait for the execution to finish
117
+ const executionPromise = this.getCurrentExecutionResult();
118
+ this.executionState.set({ sessionId, active: true, executionPrms: executionPromise });
119
 
120
+ const resp = await executionPromise;
121
+ this.executionState.set({ sessionId, active: false });
 
122
 
123
+ return resp;
124
  }
125
+
126
  async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
127
  const args: string[] = [];
128
 
 
136
 
137
  const input = process.input.getWriter();
138
  this.#shellInputStream = input;
139
+
140
  const [internalOutput, terminalOutput] = process.output.tee();
141
 
142
  const jshReady = withResolvers<void>();
 
173
 
174
  return { process, output: internalOutput };
175
  }
176
+
177
+ async getCurrentExecutionResult(): Promise<ExecutionResult> {
178
+ const { output, exitCode } = await this.waitTillOscCode('exit');
179
  return { output, exitCode };
180
  }
181
+
182
  async waitTillOscCode(waitCode: string) {
183
  let fullOutput = '';
184
  let exitCode: number = 0;
185
+
186
+ if (!this.#outputStream) {
187
+ return { output: fullOutput, exitCode };
188
+ }
189
+
190
+ const tappedStream = this.#outputStream;
191
 
192
  while (true) {
193
  const { value, done } = await tappedStream.read();
194
+
195
+ if (done) {
196
+ break;
197
+ }
198
+
199
  const text = value || '';
200
  fullOutput += text;
201
 
202
  // Check if command completion signal with exit code
203
+ const [, osc, , , code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
204
+
205
  if (osc === 'exit') {
206
  exitCode = parseInt(code, 10);
207
  }
208
+
209
  if (osc === waitCode) {
210
  break;
211
  }
212
  }
213
+
214
  return { output: fullOutput, exitCode };
215
  }
216
  }
217
+
218
  export function newBoltShellProcess() {
219
  return new BoltShell();
220
  }
app/utils/types.ts CHANGED
@@ -1,4 +1,3 @@
1
-
2
  interface OllamaModelDetails {
3
  parent_model: string;
4
  format: string;
@@ -29,10 +28,10 @@ export interface ModelInfo {
29
  }
30
 
31
  export interface ProviderInfo {
32
- staticModels: ModelInfo[],
33
- name: string,
34
- getDynamicModels?: () => Promise<ModelInfo[]>,
35
- getApiKeyLink?: string,
36
- labelForGetApiKey?: string,
37
- icon?:string,
38
- };
 
 
1
  interface OllamaModelDetails {
2
  parent_model: string;
3
  format: string;
 
28
  }
29
 
30
  export interface ProviderInfo {
31
+ staticModels: ModelInfo[];
32
+ name: string;
33
+ getDynamicModels?: () => Promise<ModelInfo[]>;
34
+ getApiKeyLink?: string;
35
+ labelForGetApiKey?: string;
36
+ icon?: string;
37
+ }
docker-compose.yaml CHANGED
@@ -21,6 +21,7 @@ services:
21
  - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
22
  - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
23
  - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
 
24
  - RUNNING_IN_DOCKER=true
25
  extra_hosts:
26
  - "host.docker.internal:host-gateway"
@@ -48,6 +49,7 @@ services:
48
  - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
49
  - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
50
  - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
 
51
  - RUNNING_IN_DOCKER=true
52
  extra_hosts:
53
  - "host.docker.internal:host-gateway"
 
21
  - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
22
  - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
23
  - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
24
+ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
25
  - RUNNING_IN_DOCKER=true
26
  extra_hosts:
27
  - "host.docker.internal:host-gateway"
 
49
  - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
50
  - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
51
  - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
52
+ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
53
  - RUNNING_IN_DOCKER=true
54
  extra_hosts:
55
  - "host.docker.internal:host-gateway"
eslint.config.mjs CHANGED
@@ -12,6 +12,8 @@ export default [
12
  '@blitz/catch-error-name': 'off',
13
  '@typescript-eslint/no-this-alias': 'off',
14
  '@typescript-eslint/no-empty-object-type': 'off',
 
 
15
  },
16
  },
17
  {
 
12
  '@blitz/catch-error-name': 'off',
13
  '@typescript-eslint/no-this-alias': 'off',
14
  '@typescript-eslint/no-empty-object-type': 'off',
15
+ '@blitz/comment-syntax': 'off',
16
+ '@blitz/block-scope-case': 'off',
17
  },
18
  },
19
  {
package.json CHANGED
@@ -11,8 +11,8 @@
11
  "dev": "remix vite:dev",
12
  "test": "vitest --run",
13
  "test:watch": "vitest",
14
- "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
15
- "lint:fix": "npm run lint -- --fix",
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",
 
11
  "dev": "remix vite:dev",
12
  "test": "vitest --run",
13
  "test:watch": "vitest",
14
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app",
15
+ "lint:fix": "npm run lint -- --fix && prettier app --write",
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",
worker-configuration.d.ts CHANGED
@@ -9,4 +9,7 @@ interface Env {
9
  OPENAI_LIKE_API_BASE_URL: string;
10
  DEEPSEEK_API_KEY: string;
11
  LMSTUDIO_API_BASE_URL: string;
 
 
 
12
  }
 
9
  OPENAI_LIKE_API_BASE_URL: string;
10
  DEEPSEEK_API_KEY: string;
11
  LMSTUDIO_API_BASE_URL: string;
12
+ GOOGLE_GENERATIVE_AI_API_KEY: string;
13
+ MISTRAL_API_KEY: string;
14
+ XAI_API_KEY: string;
15
  }