Chris Mahoney commited on
Commit
cb8a096
·
unverified ·
2 Parent(s): 5359423 9792e93

Merge branch 'main' into main

Browse files
.env.example CHANGED
@@ -5,6 +5,12 @@
5
  # You only need this environment variable set if you want to use Groq models
6
  GROQ_API_KEY=
7
 
 
 
 
 
 
 
8
  # Get your Open AI API Key by following these instructions -
9
  # https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
10
  # You only need this environment variable set if you want to use GPT models
@@ -43,6 +49,10 @@ OPENAI_LIKE_API_KEY=
43
  # You only need this environment variable set if you want to use Mistral models
44
  MISTRAL_API_KEY=
45
 
 
 
 
 
46
 
47
  # Get LMStudio Base URL from LM Studio Developer Console
48
  # Make sure to enable CORS
@@ -63,4 +73,4 @@ VITE_LOG_LEVEL=debug
63
  # DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM
64
  # DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM
65
  # DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM
66
- DEFAULT_NUM_CTX=
 
5
  # You only need this environment variable set if you want to use Groq models
6
  GROQ_API_KEY=
7
 
8
+ # Get your HuggingFace API Key here -
9
+ # https://huggingface.co/settings/tokens
10
+ # You only need this environment variable set if you want to use HuggingFace models
11
+ HuggingFace_API_KEY=
12
+
13
+
14
  # Get your Open AI API Key by following these instructions -
15
  # https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
16
  # You only need this environment variable set if you want to use GPT models
 
49
  # You only need this environment variable set if you want to use Mistral models
50
  MISTRAL_API_KEY=
51
 
52
+ # Get the Cohere Api key by following these instructions -
53
+ # https://dashboard.cohere.com/api-keys
54
+ # You only need this environment variable set if you want to use Cohere models
55
+ COHERE_API_KEY=
56
 
57
  # Get LMStudio Base URL from LM Studio Developer Console
58
  # Make sure to enable CORS
 
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=
.gitignore CHANGED
@@ -22,6 +22,7 @@ dist-ssr
22
  *.sln
23
  *.sw?
24
 
 
25
  /.cache
26
  /build
27
  .env.local
 
22
  *.sln
23
  *.sw?
24
 
25
+ /.history
26
  /.cache
27
  /build
28
  .env.local
.husky/commit-msg DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env sh
2
-
3
- . "$(dirname "$0")/_/husky.sh"
4
-
5
- npx commitlint --edit $1
6
-
7
- exit 0
 
 
 
 
 
 
 
 
CONTRIBUTING.md CHANGED
@@ -75,6 +75,7 @@ pnpm install
75
  - Add your LLM API keys (only set the ones you plan to use):
76
  ```bash
77
  GROQ_API_KEY=XXX
 
78
  OPENAI_API_KEY=XXX
79
  ANTHROPIC_API_KEY=XXX
80
  ...
 
75
  - Add your LLM API keys (only set the ones you plan to use):
76
  ```bash
77
  GROQ_API_KEY=XXX
78
+ HuggingFace_API_KEY=XXX
79
  OPENAI_API_KEY=XXX
80
  ANTHROPIC_API_KEY=XXX
81
  ...
Dockerfile CHANGED
@@ -19,6 +19,7 @@ FROM base AS bolt-ai-production
19
 
20
  # Define environment variables with default values or let them be overridden
21
  ARG GROQ_API_KEY
 
22
  ARG OPENAI_API_KEY
23
  ARG ANTHROPIC_API_KEY
24
  ARG OPEN_ROUTER_API_KEY
@@ -29,6 +30,7 @@ ARG DEFAULT_NUM_CTX
29
 
30
  ENV WRANGLER_SEND_METRICS=false \
31
  GROQ_API_KEY=${GROQ_API_KEY} \
 
32
  OPENAI_API_KEY=${OPENAI_API_KEY} \
33
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
34
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
@@ -50,6 +52,7 @@ FROM base AS bolt-ai-development
50
 
51
  # Define the same environment variables for development
52
  ARG GROQ_API_KEY
 
53
  ARG OPENAI_API_KEY
54
  ARG ANTHROPIC_API_KEY
55
  ARG OPEN_ROUTER_API_KEY
@@ -59,6 +62,7 @@ ARG VITE_LOG_LEVEL=debug
59
  ARG DEFAULT_NUM_CTX
60
 
61
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
 
62
  OPENAI_API_KEY=${OPENAI_API_KEY} \
63
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
64
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
 
19
 
20
  # Define environment variables with default values or let them be overridden
21
  ARG GROQ_API_KEY
22
+ ARG HuggingFace_API_KEY
23
  ARG OPENAI_API_KEY
24
  ARG ANTHROPIC_API_KEY
25
  ARG OPEN_ROUTER_API_KEY
 
30
 
31
  ENV WRANGLER_SEND_METRICS=false \
32
  GROQ_API_KEY=${GROQ_API_KEY} \
33
+ HuggingFace_KEY=${HuggingFace_API_KEY} \
34
  OPENAI_API_KEY=${OPENAI_API_KEY} \
35
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
36
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
 
52
 
53
  # Define the same environment variables for development
54
  ARG GROQ_API_KEY
55
+ ARG HuggingFace
56
  ARG OPENAI_API_KEY
57
  ARG ANTHROPIC_API_KEY
58
  ARG OPEN_ROUTER_API_KEY
 
62
  ARG DEFAULT_NUM_CTX
63
 
64
  ENV GROQ_API_KEY=${GROQ_API_KEY} \
65
+ HuggingFace_API_KEY=${HuggingFace_API_KEY} \
66
  OPENAI_API_KEY=${OPENAI_API_KEY} \
67
  ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
68
  OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
README.md CHANGED
@@ -1,8 +1,12 @@
1
  [![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)
2
 
3
- # Bolt.new Fork by Cole Medin
4
 
5
- This fork of Bolt.new allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
 
 
 
 
6
 
7
  # Requested Additions to this Fork - Feel Free to Contribute!!
8
 
@@ -20,21 +24,24 @@ This fork of Bolt.new allows you to choose the LLM that you use for each prompt!
20
  - ✅ Publish projects directly to GitHub (@goncaloalves)
21
  - ✅ Ability to enter API keys in the UI (@ali00209)
22
  - ✅ xAI Grok Beta Integration (@milutinke)
 
 
 
 
 
23
  - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
24
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
25
- - ⬜ **HIGH PRIORITY** Load local projects into the app
26
  - ⬜ **HIGH PRIORITY** - Attach images to prompts
27
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
28
  - ⬜ Mobile friendly
29
- - ⬜ LM Studio Integration
30
  - ⬜ Together Integration
31
  - ⬜ Azure Open AI API Integration
32
- - ⬜ HuggingFace Integration
33
  - ⬜ Perplexity Integration
34
  - ⬜ Vertex AI Integration
35
- - Cohere Integration
 
36
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
37
- - ⬜ Ability to revert code to earlier version
38
  - ⬜ Prompt caching
39
  - ⬜ Better prompt enhancing
40
  - ⬜ Have LLM plan the project in a MD file for better results/transparency
 
1
  [![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)
2
 
3
+ # Bolt.new Fork by Cole Medin - oTToDev
4
 
5
+ This fork of Bolt.new (oTToDev) allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
6
+
7
+ Join the community for oTToDev!
8
+
9
+ https://thinktank.ottomator.ai
10
 
11
  # Requested Additions to this Fork - Feel Free to Contribute!!
12
 
 
24
  - ✅ Publish projects directly to GitHub (@goncaloalves)
25
  - ✅ Ability to enter API keys in the UI (@ali00209)
26
  - ✅ xAI Grok Beta Integration (@milutinke)
27
+ - ✅ LM Studio Integration (@karrot0)
28
+ - ✅ HuggingFace Integration (@ahsan3219)
29
+ - ✅ Bolt terminal to see the output of LLM run commands (@thecodacus)
30
+ - ✅ Streaming of code output (@thecodacus)
31
+ - ✅ Ability to revert code to earlier version (@wonderwhy-er)
32
  - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
33
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
34
+ - ⬜ **HIGH PRIORITY** - Load local projects into the app
35
  - ⬜ **HIGH PRIORITY** - Attach images to prompts
36
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
37
  - ⬜ Mobile friendly
 
38
  - ⬜ Together Integration
39
  - ⬜ Azure Open AI API Integration
 
40
  - ⬜ Perplexity Integration
41
  - ⬜ Vertex AI Integration
42
+ - Cohere Integration (@hasanraiyan)
43
+ - ✅ Dynamic model max token length (@hasanraiyan)
44
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
 
45
  - ⬜ Prompt caching
46
  - ⬜ Better prompt enhancing
47
  - ⬜ Have LLM plan the project in a MD file for better results/transparency
app/components/chat/APIKeyManager.tsx CHANGED
@@ -10,11 +10,7 @@ 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 +20,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 +50,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
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
 
 
 
 
14
  const [isEditing, setIsEditing] = useState(false);
15
  const [tempKey, setTempKey] = useState(apiKey);
16
 
 
20
  };
21
 
22
  return (
23
+ <div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
24
+ <div>
25
+ <span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
26
+ {!isEditing && (
27
+ <div className="flex items-center mb-4">
28
+ <span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
29
+ {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
30
+ </span>
31
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
32
+ <div className="i-ph:pencil-simple" />
33
+ </IconButton>
34
+ </div>
35
+ )}
36
+ </div>
37
+
38
  {isEditing ? (
39
+ <div className="flex items-center gap-3 mt-2">
40
  <input
41
  type="password"
42
  value={tempKey}
43
+ placeholder="Your API Key"
44
  onChange={(e) => setTempKey(e.target.value)}
45
+ 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"
46
  />
47
  <IconButton onClick={handleSave} title="Save API Key">
48
  <div className="i-ph:check" />
 
50
  <IconButton onClick={() => setIsEditing(false)} title="Cancel">
51
  <div className="i-ph:x" />
52
  </IconButton>
53
+ </div>
54
  ) : (
55
  <>
56
+ {provider?.getApiKeyLink && (
57
+ <IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
58
+ <span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
59
+ <div className={provider?.icon || 'i-ph:key'} />
60
+ </IconButton>
61
+ )}
 
 
 
 
 
62
  </>
63
  )}
64
  </div>
app/components/chat/Artifact.tsx CHANGED
@@ -7,6 +7,7 @@ import type { ActionState } from '~/lib/runtime/action-runner';
7
  import { workbenchStore } from '~/lib/stores/workbench';
8
  import { classNames } from '~/utils/classNames';
9
  import { cubicEasingFn } from '~/utils/easings';
 
10
 
11
  const highlighterOptions = {
12
  langs: ['shell'],
@@ -129,6 +130,14 @@ const actionVariants = {
129
  visible: { opacity: 1, y: 0 },
130
  };
131
 
 
 
 
 
 
 
 
 
132
  const ActionList = memo(({ actions }: ActionListProps) => {
133
  return (
134
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
@@ -169,7 +178,10 @@ const ActionList = memo(({ actions }: ActionListProps) => {
169
  {type === 'file' ? (
170
  <div>
171
  Create{' '}
172
- <code className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md">
 
 
 
173
  {action.filePath}
174
  </code>
175
  </div>
 
7
  import { workbenchStore } from '~/lib/stores/workbench';
8
  import { classNames } from '~/utils/classNames';
9
  import { cubicEasingFn } from '~/utils/easings';
10
+ import { WORK_DIR } from '~/utils/constants';
11
 
12
  const highlighterOptions = {
13
  langs: ['shell'],
 
130
  visible: { opacity: 1, y: 0 },
131
  };
132
 
133
+ function openArtifactInWorkbench(filePath: any) {
134
+ if (workbenchStore.currentView.get() !== 'code') {
135
+ workbenchStore.currentView.set('code');
136
+ }
137
+
138
+ workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
139
+ }
140
+
141
  const ActionList = memo(({ actions }: ActionListProps) => {
142
  return (
143
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
 
178
  {type === 'file' ? (
179
  <div>
180
  Create{' '}
181
+ <code
182
+ className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
183
+ onClick={() => openArtifactInWorkbench(action.filePath)}
184
+ >
185
  {action.filePath}
186
  </code>
187
  </div>
app/components/chat/BaseChat.tsx CHANGED
@@ -27,9 +27,9 @@ const EXAMPLE_PROMPTS = [
27
 
28
  const providerList = PROVIDER_LIST;
29
 
30
- const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
31
  return (
32
- <div className="mb-2 flex gap-2">
33
  <select
34
  value={provider?.name}
35
  onChange={(e) => {
@@ -49,8 +49,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
49
  key={provider?.name}
50
  value={model}
51
  onChange={(e) => setModel(e.target.value)}
52
- style={{ maxWidth: '70%' }}
53
- 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"
54
  >
55
  {[...modelList]
56
  .filter((e) => e.provider == provider?.name && e.name)
@@ -157,25 +156,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
157
  ref={ref}
158
  className={classNames(
159
  styles.BaseChat,
160
- 'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
161
  )}
162
  data-chat-visible={showChat}
163
  >
164
  <ClientOnly>{() => <Menu />}</ClientOnly>
165
- <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
166
- <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
167
  {!chatStarted && (
168
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
169
- <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
170
  Where ideas begin
171
  </h1>
172
- <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
173
  Bring ideas to life in seconds or get help on existing projects.
174
  </p>
175
  </div>
176
  )}
177
  <div
178
- className={classNames('pt-6 px-6', {
179
  'h-full flex flex-col': chatStarted,
180
  })}
181
  >
@@ -184,7 +183,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
184
  return chatStarted ? (
185
  <Messages
186
  ref={messageRef}
187
- className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
188
  messages={messages}
189
  isStreaming={isStreaming}
190
  />
@@ -192,9 +191,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
192
  }}
193
  </ClientOnly>
194
  <div
195
- className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
196
- 'sticky bottom-0': chatStarted,
197
- })}
 
 
 
198
  >
199
  <ModelSelector
200
  key={provider?.name + ':' + modelList.length}
@@ -204,7 +206,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
204
  provider={provider}
205
  setProvider={setProvider}
206
  providerList={PROVIDER_LIST}
 
207
  />
 
208
  {provider && (
209
  <APIKeyManager
210
  provider={provider}
@@ -212,6 +216,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
212
  setApiKey={(key) => updateApiKey(provider.name, key)}
213
  />
214
  )}
 
215
  <div
216
  className={classNames(
217
  'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
@@ -219,7 +224,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
219
  >
220
  <textarea
221
  ref={textareaRef}
222
- 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`}
223
  onKeyDown={(event) => {
224
  if (event.key === 'Enter') {
225
  if (event.shiftKey) {
@@ -292,7 +297,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
292
  ) : null}
293
  </div>
294
  </div>
295
- <div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
296
  </div>
297
  </div>
298
  {!chatStarted && (
 
27
 
28
  const providerList = PROVIDER_LIST;
29
 
30
+ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
31
  return (
32
+ <div className="mb-2 flex gap-2 flex-col sm:flex-row">
33
  <select
34
  value={provider?.name}
35
  onChange={(e) => {
 
49
  key={provider?.name}
50
  value={model}
51
  onChange={(e) => setModel(e.target.value)}
52
+ 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%] "
 
53
  >
54
  {[...modelList]
55
  .filter((e) => e.provider == provider?.name && e.name)
 
156
  ref={ref}
157
  className={classNames(
158
  styles.BaseChat,
159
+ 'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
160
  )}
161
  data-chat-visible={showChat}
162
  >
163
  <ClientOnly>{() => <Menu />}</ClientOnly>
164
+ <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
165
+ <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
166
  {!chatStarted && (
167
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
168
+ <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
169
  Where ideas begin
170
  </h1>
171
+ <p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
172
  Bring ideas to life in seconds or get help on existing projects.
173
  </p>
174
  </div>
175
  )}
176
  <div
177
+ className={classNames('pt-6 px-2 sm:px-6', {
178
  'h-full flex flex-col': chatStarted,
179
  })}
180
  >
 
183
  return chatStarted ? (
184
  <Messages
185
  ref={messageRef}
186
+ className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
187
  messages={messages}
188
  isStreaming={isStreaming}
189
  />
 
191
  }}
192
  </ClientOnly>
193
  <div
194
+ className={classNames(
195
+ ' 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',
196
+ {
197
+ 'sticky bottom-2': chatStarted,
198
+ },
199
+ )}
200
  >
201
  <ModelSelector
202
  key={provider?.name + ':' + modelList.length}
 
206
  provider={provider}
207
  setProvider={setProvider}
208
  providerList={PROVIDER_LIST}
209
+ apiKeys={apiKeys}
210
  />
211
+
212
  {provider && (
213
  <APIKeyManager
214
  provider={provider}
 
216
  setApiKey={(key) => updateApiKey(provider.name, key)}
217
  />
218
  )}
219
+
220
  <div
221
  className={classNames(
222
  'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
 
224
  >
225
  <textarea
226
  ref={textareaRef}
227
+ 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`}
228
  onKeyDown={(event) => {
229
  if (event.key === 'Enter') {
230
  if (event.shiftKey) {
 
297
  ) : null}
298
  </div>
299
  </div>
 
300
  </div>
301
  </div>
302
  {!chatStarted && (
app/components/chat/Messages.client.tsx CHANGED
@@ -3,6 +3,11 @@ import React from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
 
 
 
 
 
6
 
7
  interface MessagesProps {
8
  id?: string;
@@ -13,41 +18,115 @@ interface MessagesProps {
13
 
14
  export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
15
  const { id, isStreaming = false, messages = [] } = props;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  return (
18
- <div id={id} ref={ref} className={props.className}>
19
- {messages.length > 0
20
- ? messages.map((message, index) => {
21
- const { role, content } = message;
22
- const isUserMessage = role === 'user';
23
- const isFirst = index === 0;
24
- const isLast = index === messages.length - 1;
25
-
26
- return (
27
- <div
28
- key={index}
29
- className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
30
- 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
31
- 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
32
- isStreaming && isLast,
33
- 'mt-4': !isFirst,
34
- })}
35
- >
36
- {isUserMessage && (
37
- <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
38
- <div className="i-ph:user-fill text-xl"></div>
 
 
 
 
 
39
  </div>
40
- )}
41
- <div className="grid grid-col-1 w-full">
42
- {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
- </div>
45
- );
46
- })
47
- : null}
48
- {isStreaming && (
49
- <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
50
- )}
51
- </div>
52
  );
53
  });
 
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
+ import * as Tooltip from '@radix-ui/react-tooltip';
7
+ import { useLocation } from '@remix-run/react';
8
+ import { db, chatId } from '~/lib/persistence/useChatHistory';
9
+ import { forkChat } from '~/lib/persistence/db';
10
+ import { toast } from 'react-toastify';
11
 
12
  interface MessagesProps {
13
  id?: string;
 
18
 
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);
25
+ searchParams.set('rewindTo', messageId);
26
+ window.location.search = searchParams.toString();
27
+ };
28
+
29
+ const handleFork = async (messageId: string) => {
30
+ try {
31
+ if (!db || !chatId.get()) {
32
+ toast.error('Chat persistence is not available');
33
+ return;
34
+ }
35
+
36
+ const urlId = await forkChat(db, chatId.get()!, messageId);
37
+ window.location.href = `/chat/${urlId}`;
38
+ } catch (error) {
39
+ toast.error('Failed to fork chat: ' + (error as Error).message);
40
+ }
41
+ };
42
 
43
  return (
44
+ <Tooltip.Provider delayDuration={200}>
45
+ <div id={id} ref={ref} className={props.className}>
46
+ {messages.length > 0
47
+ ? messages.map((message, index) => {
48
+ const { role, content, id: messageId } = message;
49
+ const isUserMessage = role === 'user';
50
+ const isFirst = index === 0;
51
+ const isLast = index === messages.length - 1;
52
+
53
+ return (
54
+ <div
55
+ key={index}
56
+ className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
57
+ 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
58
+ 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
59
+ isStreaming && isLast,
60
+ 'mt-4': !isFirst,
61
+ })}
62
+ >
63
+ {isUserMessage && (
64
+ <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
65
+ <div className="i-ph:user-fill text-xl"></div>
66
+ </div>
67
+ )}
68
+ <div className="grid grid-col-1 w-full">
69
+ {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
70
  </div>
71
+ {!isUserMessage && (
72
+ <div className="flex gap-2 flex-col lg:flex-row">
73
+ <Tooltip.Root>
74
+ <Tooltip.Trigger asChild>
75
+ {messageId && (
76
+ <button
77
+ onClick={() => handleRewind(messageId)}
78
+ key="i-ph:arrow-u-up-left"
79
+ className={classNames(
80
+ 'i-ph:arrow-u-up-left',
81
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
82
+ )}
83
+ />
84
+ )}
85
+ </Tooltip.Trigger>
86
+ <Tooltip.Portal>
87
+ <Tooltip.Content
88
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
89
+ sideOffset={5}
90
+ style={{ zIndex: 1000 }}
91
+ >
92
+ Revert to this message
93
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
94
+ </Tooltip.Content>
95
+ </Tooltip.Portal>
96
+ </Tooltip.Root>
97
+
98
+ <Tooltip.Root>
99
+ <Tooltip.Trigger asChild>
100
+ <button
101
+ onClick={() => handleFork(messageId)}
102
+ key="i-ph:git-fork"
103
+ className={classNames(
104
+ 'i-ph:git-fork',
105
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
106
+ )}
107
+ />
108
+ </Tooltip.Trigger>
109
+ <Tooltip.Portal>
110
+ <Tooltip.Content
111
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
112
+ sideOffset={5}
113
+ style={{ zIndex: 1000 }}
114
+ >
115
+ Fork chat from this message
116
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
117
+ </Tooltip.Content>
118
+ </Tooltip.Portal>
119
+ </Tooltip.Root>
120
+ </div>
121
+ )}
122
  </div>
123
+ );
124
+ })
125
+ : null}
126
+ {isStreaming && (
127
+ <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
128
+ )}
129
+ </div>
130
+ </Tooltip.Provider>
131
  );
132
  });
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/HistoryItem.tsx CHANGED
@@ -5,9 +5,10 @@ import { type ChatHistoryItem } from '~/lib/persistence';
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
 
8
  }
9
 
10
- export function HistoryItem({ item, onDelete }: HistoryItemProps) {
11
  const [hovering, setHovering] = useState(false);
12
  const hoverRef = useRef<HTMLDivElement>(null);
13
 
@@ -44,7 +45,14 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) {
44
  {item.description}
45
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
46
  {hovering && (
47
- <div className="flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger">
 
 
 
 
 
 
 
48
  <Dialog.Trigger asChild>
49
  <button
50
  className="i-ph:trash scale-110"
 
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
8
+ onDuplicate?: (id: string) => void;
9
  }
10
 
11
+ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
12
  const [hovering, setHovering] = useState(false);
13
  const hoverRef = useRef<HTMLDivElement>(null);
14
 
 
45
  {item.description}
46
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
47
  {hovering && (
48
+ <div className="flex items-center p-1 text-bolt-elements-textSecondary">
49
+ {onDuplicate && (
50
+ <button
51
+ className="i-ph:copy scale-110 mr-2"
52
+ onClick={() => onDuplicate?.(item.id)}
53
+ title="Duplicate chat"
54
+ />
55
+ )}
56
  <Dialog.Trigger asChild>
57
  <button
58
  className="i-ph:trash scale-110"
app/components/sidebar/Menu.client.tsx CHANGED
@@ -4,7 +4,7 @@ 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 } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
@@ -34,6 +34,7 @@ const menuVariants = {
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export function Menu() {
 
37
  const menuRef = useRef<HTMLDivElement>(null);
38
  const [list, setList] = useState<ChatHistoryItem[]>([]);
39
  const [open, setOpen] = useState(false);
@@ -99,6 +100,17 @@ export function Menu() {
99
  };
100
  }, []);
101
 
 
 
 
 
 
 
 
 
 
 
 
102
  return (
103
  <motion.div
104
  ref={menuRef}
@@ -128,7 +140,12 @@ export function Menu() {
128
  {category}
129
  </div>
130
  {items.map((item) => (
131
- <HistoryItem key={item.id} item={item} onDelete={() => setDialogContent({ type: 'delete', item })} />
 
 
 
 
 
132
  ))}
133
  </div>
134
  ))}
 
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';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export function Menu() {
37
+ const { duplicateCurrentChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
 
100
  };
101
  }, []);
102
 
103
+ const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
104
+ event.preventDefault();
105
+
106
+ setDialogContent({ type: 'delete', item });
107
+ };
108
+
109
+ const handleDuplicate = async (id: string) => {
110
+ await duplicateCurrentChat(id);
111
+ loadEntries(); // Reload the list after duplication
112
+ };
113
+
114
  return (
115
  <motion.div
116
  ref={menuRef}
 
140
  {category}
141
  </div>
142
  {items.map((item) => (
143
+ <HistoryItem
144
+ key={item.id}
145
+ item={item}
146
+ onDelete={(event) => handleDeleteClick(event, item)}
147
+ onDuplicate={() => handleDuplicate(item.id)}
148
+ />
149
  ))}
150
  </div>
151
  ))}
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,32 @@ 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
  if (!repoName) {
178
+ alert('Repository name is required. Push to GitHub cancelled.');
179
  return;
180
  }
181
+ const githubUsername = prompt('Please enter your GitHub username:');
182
  if (!githubUsername) {
183
+ alert('GitHub username is required. Push to GitHub cancelled.');
184
  return;
185
  }
186
+ const githubToken = prompt('Please enter your GitHub personal access token:');
187
  if (!githubToken) {
188
+ alert('GitHub token is required. Push to GitHub cancelled.');
189
  return;
190
  }
191
+
192
+ workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
193
  }}
194
  >
195
  <div className="i-ph:github-logo" />
196
  Push to GitHub
197
  </PanelHeaderButton>
198
+ </div>
199
  )}
200
  <IconButton
201
  icon="i-ph:x-circle"
app/lib/.server/llm/api-key.ts CHANGED
@@ -23,6 +23,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
23
  return env.GOOGLE_GENERATIVE_AI_API_KEY || cloudflareEnv.GOOGLE_GENERATIVE_AI_API_KEY;
24
  case 'Groq':
25
  return env.GROQ_API_KEY || cloudflareEnv.GROQ_API_KEY;
 
 
26
  case 'OpenRouter':
27
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
28
  case 'Deepseek':
@@ -33,6 +35,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
33
  return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
34
  case "xAI":
35
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
 
 
36
  default:
37
  return "";
38
  }
 
23
  return env.GOOGLE_GENERATIVE_AI_API_KEY || cloudflareEnv.GOOGLE_GENERATIVE_AI_API_KEY;
24
  case 'Groq':
25
  return env.GROQ_API_KEY || cloudflareEnv.GROQ_API_KEY;
26
+ case 'HuggingFace':
27
+ return env.HuggingFace_API_KEY || cloudflareEnv.HuggingFace_API_KEY;
28
  case 'OpenRouter':
29
  return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY;
30
  case 'Deepseek':
 
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
  }
app/lib/.server/llm/model.ts CHANGED
@@ -7,6 +7,7 @@ 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
 
11
  export const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ?
12
  parseInt(process.env.DEFAULT_NUM_CTX, 10) :
@@ -27,6 +28,15 @@ export function getOpenAILikeModel(baseURL:string,apiKey: string, model: string)
27
 
28
  return openai(model);
29
  }
 
 
 
 
 
 
 
 
 
30
  export function getOpenAIModel(apiKey: string, model: string) {
31
  const openai = createOpenAI({
32
  apiKey,
@@ -60,6 +70,15 @@ export function getGroqModel(apiKey: string, model: string) {
60
  return openai(model);
61
  }
62
 
 
 
 
 
 
 
 
 
 
63
  export function getOllamaModel(baseURL: string, model: string) {
64
  let Ollama = ollama(model, {
65
  numCtx: DEFAULT_NUM_CTX,
@@ -103,6 +122,8 @@ export function getXAIModel(apiKey: string, model: string) {
103
 
104
  return openai(model);
105
  }
 
 
106
  export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
107
  const apiKey = getAPIKey(env, provider, apiKeys);
108
  const baseURL = getBaseURL(env, provider);
@@ -114,6 +135,8 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
114
  return getOpenAIModel(apiKey, model);
115
  case 'Groq':
116
  return getGroqModel(apiKey, model);
 
 
117
  case 'OpenRouter':
118
  return getOpenRouterModel(apiKey, model);
119
  case 'Google':
@@ -128,6 +151,8 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
128
  return getLMStudioModel(baseURL, model);
129
  case 'xAI':
130
  return getXAIModel(apiKey, model);
 
 
131
  default:
132
  return getOllamaModel(baseURL, model);
133
  }
 
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 const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ?
13
  parseInt(process.env.DEFAULT_NUM_CTX, 10) :
 
28
 
29
  return openai(model);
30
  }
31
+
32
+ export function getCohereAIModel(apiKey:string, model: string){
33
+ const cohere = createCohere({
34
+ apiKey,
35
+ });
36
+
37
+ return cohere(model);
38
+ }
39
+
40
  export function getOpenAIModel(apiKey: string, model: string) {
41
  const openai = createOpenAI({
42
  apiKey,
 
70
  return openai(model);
71
  }
72
 
73
+ export function getHuggingFaceModel(apiKey: string, model: string) {
74
+ const openai = createOpenAI({
75
+ baseURL: 'https://api-inference.huggingface.co/v1/',
76
+ apiKey,
77
+ });
78
+
79
+ return openai(model);
80
+ }
81
+
82
  export function getOllamaModel(baseURL: string, model: string) {
83
  let Ollama = ollama(model, {
84
  numCtx: DEFAULT_NUM_CTX,
 
122
 
123
  return openai(model);
124
  }
125
+
126
+
127
  export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
128
  const apiKey = getAPIKey(env, provider, apiKeys);
129
  const baseURL = getBaseURL(env, provider);
 
135
  return getOpenAIModel(apiKey, model);
136
  case 'Groq':
137
  return getGroqModel(apiKey, model);
138
+ case 'HuggingFace':
139
+ return getHuggingFaceModel(apiKey, model);
140
  case 'OpenRouter':
141
  return getOpenRouterModel(apiKey, model);
142
  case 'Google':
 
151
  return getLMStudioModel(baseURL, model);
152
  case 'xAI':
153
  return getXAIModel(apiKey, model);
154
+ case 'Cohere':
155
+ return getCohereAIModel(apiKey, model);
156
  default:
157
  return getOllamaModel(baseURL, model);
158
  }
app/lib/.server/llm/prompts.ts CHANGED
@@ -88,7 +88,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
88
  Example:
89
 
90
  <${MODIFICATIONS_TAG_NAME}>
91
- <diff path="/home/project/src/main.js">
92
  @@ -2,7 +2,10 @@
93
  return a + b;
94
  }
@@ -103,7 +103,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
103
  +
104
  +console.log('The End');
105
  </diff>
106
- <file path="/home/project/package.json">
107
  // full file content here
108
  </file>
109
  </${MODIFICATIONS_TAG_NAME}>
 
88
  Example:
89
 
90
  <${MODIFICATIONS_TAG_NAME}>
91
+ <diff path="${WORK_DIR}/src/main.js">
92
  @@ -2,7 +2,10 @@
93
  return a + b;
94
  }
 
103
  +
104
  +console.log('The End');
105
  </diff>
106
+ <file path="${WORK_DIR}/package.json">
107
  // full file content here
108
  </file>
109
  </${MODIFICATIONS_TAG_NAME}>
app/lib/.server/llm/stream-text.ts CHANGED
@@ -41,10 +41,9 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
41
 
42
  return { model, provider, content: cleanedContent };
43
  }
44
-
45
  export function streamText(
46
- messages: Messages,
47
- env: Env,
48
  options?: StreamingOptions,
49
  apiKeys?: Record<string, string>
50
  ) {
@@ -64,13 +63,22 @@ export function streamText(
64
  return { ...message, content };
65
  }
66
 
67
- return message; // No changes for non-user messages
68
  });
69
 
 
 
 
 
 
 
 
 
 
70
  return _streamText({
71
  model: getModel(currentProvider, currentModel, env, apiKeys),
72
  system: getSystemPrompt(),
73
- maxTokens: MAX_TOKENS,
74
  messages: convertToCoreMessages(processedMessages),
75
  ...options,
76
  });
 
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
  ) {
 
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),
80
  system: getSystemPrompt(),
81
+ maxTokens: dynamicMaxTokens,
82
  messages: convertToCoreMessages(processedMessages),
83
  ...options,
84
  });
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/usePromptEnhancer.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { useState } from 'react';
 
2
  import { createScopedLogger } from '~/utils/logger';
3
 
4
  const logger = createScopedLogger('usePromptEnhancement');
@@ -13,54 +14,54 @@ export function usePromptEnhancer() {
13
  };
14
 
15
  const enhancePrompt = async (
16
- input: string,
17
  setInput: (value: string) => void,
18
  model: string,
19
- provider: string,
20
- apiKeys?: Record<string, string>
21
  ) => {
22
  setEnhancingPrompt(true);
23
  setPromptEnhanced(false);
24
-
25
  const requestBody: any = {
26
  message: input,
27
  model,
28
  provider,
29
  };
30
-
31
  if (apiKeys) {
32
  requestBody.apiKeys = apiKeys;
33
  }
34
-
35
  const response = await fetch('/api/enhancer', {
36
  method: 'POST',
37
  body: JSON.stringify(requestBody),
38
  });
39
-
40
  const reader = response.body?.getReader();
41
-
42
  const originalInput = input;
43
-
44
  if (reader) {
45
  const decoder = new TextDecoder();
46
-
47
  let _input = '';
48
  let _error;
49
-
50
  try {
51
  setInput('');
52
-
53
  while (true) {
54
  const { value, done } = await reader.read();
55
-
56
  if (done) {
57
  break;
58
  }
59
-
60
  _input += decoder.decode(value);
61
-
62
  logger.trace('Set input', _input);
63
-
64
  setInput(_input);
65
  }
66
  } catch (error) {
@@ -70,10 +71,10 @@ export function usePromptEnhancer() {
70
  if (_error) {
71
  logger.error(_error);
72
  }
73
-
74
  setEnhancingPrompt(false);
75
  setPromptEnhanced(true);
76
-
77
  setTimeout(() => {
78
  setInput(_input);
79
  });
 
1
  import { useState } from 'react';
2
+ import type { ProviderInfo } from '~/types/model';
3
  import { createScopedLogger } from '~/utils/logger';
4
 
5
  const logger = createScopedLogger('usePromptEnhancement');
 
14
  };
15
 
16
  const enhancePrompt = async (
17
+ input: string,
18
  setInput: (value: string) => void,
19
  model: string,
20
+ provider: ProviderInfo,
21
+ apiKeys?: Record<string, string>,
22
  ) => {
23
  setEnhancingPrompt(true);
24
  setPromptEnhanced(false);
25
+
26
  const requestBody: any = {
27
  message: input,
28
  model,
29
  provider,
30
  };
31
+
32
  if (apiKeys) {
33
  requestBody.apiKeys = apiKeys;
34
  }
35
+
36
  const response = await fetch('/api/enhancer', {
37
  method: 'POST',
38
  body: JSON.stringify(requestBody),
39
  });
40
+
41
  const reader = response.body?.getReader();
42
+
43
  const originalInput = input;
44
+
45
  if (reader) {
46
  const decoder = new TextDecoder();
47
+
48
  let _input = '';
49
  let _error;
50
+
51
  try {
52
  setInput('');
53
+
54
  while (true) {
55
  const { value, done } = await reader.read();
56
+
57
  if (done) {
58
  break;
59
  }
60
+
61
  _input += decoder.decode(value);
62
+
63
  logger.trace('Set input', _input);
64
+
65
  setInput(_input);
66
  }
67
  } catch (error) {
 
71
  if (_error) {
72
  logger.error(_error);
73
  }
74
+
75
  setEnhancingPrompt(false);
76
  setPromptEnhanced(true);
77
+
78
  setTimeout(() => {
79
  setInput(_input);
80
  });
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
@@ -158,3 +158,50 @@ async function getUrlIds(db: IDBDatabase): Promise<string[]> {
158
  };
159
  });
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  };
159
  });
160
  }
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);
172
+
173
+ // Generate new IDs
174
+ const newId = await getNextId(db);
175
+ const urlId = await getUrlId(db, newId);
176
+
177
+ // Create the forked chat
178
+ await setMessages(
179
+ db,
180
+ newId,
181
+ messages,
182
+ urlId,
183
+ chat.description ? `${chat.description} (fork)` : 'Forked chat'
184
+ );
185
+
186
+ return urlId;
187
+ }
188
+
189
+ export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
190
+ const chat = await getMessages(db, id);
191
+ if (!chat) {
192
+ throw new Error('Chat not found');
193
+ }
194
+
195
+ const newId = await getNextId(db);
196
+ const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
197
+
198
+ await setMessages(
199
+ db,
200
+ newId,
201
+ chat.messages,
202
+ newUrlId, // Use the new urlId
203
+ `${chat.description || 'Chat'} (copy)`
204
+ );
205
+
206
+ return newUrlId; // Return the urlId instead of id for navigation
207
+ }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { useLoaderData, useNavigate } from '@remix-run/react';
2
  import { useState, useEffect } from 'react';
3
  import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
- import { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
@@ -24,6 +24,7 @@ export const description = atom<string | undefined>(undefined);
24
  export function useChatHistory() {
25
  const navigate = useNavigate();
26
  const { id: mixedId } = useLoaderData<{ id?: string }>();
 
27
 
28
  const [initialMessages, setInitialMessages] = useState<Message[]>([]);
29
  const [ready, setReady] = useState<boolean>(false);
@@ -44,7 +45,12 @@ export function useChatHistory() {
44
  getMessages(db, mixedId)
45
  .then((storedMessages) => {
46
  if (storedMessages && storedMessages.messages.length > 0) {
47
- setInitialMessages(storedMessages.messages);
 
 
 
 
 
48
  setUrlId(storedMessages.urlId);
49
  description.set(storedMessages.description);
50
  chatId.set(storedMessages.id);
@@ -93,6 +99,19 @@ export function useChatHistory() {
93
 
94
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
95
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  };
97
  }
98
 
 
1
+ import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
2
  import { useState, useEffect } from 'react';
3
  import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
+ import { getMessages, getNextId, getUrlId, openDatabase, setMessages, duplicateChat } from './db';
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
 
24
  export function useChatHistory() {
25
  const navigate = useNavigate();
26
  const { id: mixedId } = useLoaderData<{ id?: string }>();
27
+ const [searchParams] = useSearchParams();
28
 
29
  const [initialMessages, setInitialMessages] = useState<Message[]>([]);
30
  const [ready, setReady] = useState<boolean>(false);
 
45
  getMessages(db, mixedId)
46
  .then((storedMessages) => {
47
  if (storedMessages && storedMessages.messages.length > 0) {
48
+ const rewindId = searchParams.get('rewindTo');
49
+ const filteredMessages = rewindId
50
+ ? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1)
51
+ : storedMessages.messages;
52
+
53
+ setInitialMessages(filteredMessages);
54
  setUrlId(storedMessages.urlId);
55
  description.set(storedMessages.description);
56
  chatId.set(storedMessages.id);
 
99
 
100
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
101
  },
102
+ duplicateCurrentChat: async (listItemId:string) => {
103
+ if (!db || (!mixedId && !listItemId)) {
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const newId = await duplicateChat(db, mixedId || listItemId);
109
+ navigate(`/chat/${newId}`);
110
+ toast.success('Chat duplicated successfully');
111
+ } catch (error) {
112
+ toast.error('Failed to duplicate chat');
113
+ }
114
+ }
115
  };
116
  }
117
 
app/lib/runtime/action-runner.ts CHANGED
@@ -94,7 +94,7 @@ export class ActionRunner {
94
 
95
  this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
- this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
  return this.#executeAction(actionId, isStreaming);
100
  })
@@ -119,7 +119,14 @@ export class ActionRunner {
119
  break;
120
  }
121
  case 'start': {
122
- await this.#runStartAction(action)
 
 
 
 
 
 
 
123
  break;
124
  }
125
  }
 
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
  })
 
119
  break;
120
  }
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
  }
app/lib/stores/workbench.ts CHANGED
@@ -14,6 +14,7 @@ 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
 
18
  export interface ArtifactState {
19
  id: string;
@@ -42,7 +43,7 @@ export class WorkbenchStore {
42
  modifiedFiles = new Set<string>();
43
  artifactIdList: string[] = [];
44
  #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
45
-
46
  constructor() {
47
  if (import.meta.hot) {
48
  import.meta.hot.data.artifacts = this.artifacts;
@@ -52,6 +53,10 @@ export class WorkbenchStore {
52
  }
53
  }
54
 
 
 
 
 
55
  get previews() {
56
  return this.#previewsStore.previews;
57
  }
@@ -255,8 +260,11 @@ export class WorkbenchStore {
255
 
256
  this.artifacts.setKey(messageId, { ...artifact, ...state });
257
  }
258
-
259
- async addAction(data: ActionCallbackData) {
 
 
 
260
  const { messageId } = data;
261
 
262
  const artifact = this.#getArtifact(messageId);
@@ -265,10 +273,18 @@ export class WorkbenchStore {
265
  unreachable('Artifact not found');
266
  }
267
 
268
- artifact.runner.addAction(data);
269
  }
270
 
271
- async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
 
 
 
 
 
 
 
 
272
  const { messageId } = data;
273
 
274
  const artifact = this.#getArtifact(messageId);
@@ -293,11 +309,11 @@ export class WorkbenchStore {
293
  this.#editorStore.updateFile(fullPath, data.action.content);
294
 
295
  if (!isStreaming) {
296
- this.resetCurrentDocument();
297
  await artifact.runner.runAction(data);
 
298
  }
299
  } else {
300
- artifact.runner.runAction(data);
301
  }
302
  }
303
 
@@ -312,8 +328,7 @@ export class WorkbenchStore {
312
 
313
  for (const [filePath, dirent] of Object.entries(files)) {
314
  if (dirent?.type === 'file' && !dirent.isBinary) {
315
- // remove '/home/project/' from the beginning of the path
316
- const relativePath = filePath.replace(/^\/home\/project\//, '');
317
 
318
  // split the path into segments
319
  const pathSegments = relativePath.split('/');
@@ -343,7 +358,7 @@ export class WorkbenchStore {
343
 
344
  for (const [filePath, dirent] of Object.entries(files)) {
345
  if (dirent?.type === 'file' && !dirent.isBinary) {
346
- const relativePath = filePath.replace(/^\/home\/project\//, '');
347
  const pathSegments = relativePath.split('/');
348
  let currentHandle = targetHandle;
349
 
@@ -417,7 +432,7 @@ export class WorkbenchStore {
417
  content: Buffer.from(dirent.content).toString('base64'),
418
  encoding: 'base64',
419
  });
420
- return { path: filePath.replace(/^\/home\/project\//, ''), sha: blob.sha };
421
  }
422
  })
423
  );
 
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 {
20
  id: 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;
 
53
  }
54
  }
55
 
56
+ addToExecutionQueue(callback: () => Promise<void>) {
57
+ this.#globalExecutionQueue=this.#globalExecutionQueue.then(()=>callback())
58
+ }
59
+
60
  get previews() {
61
  return this.#previewsStore.previews;
62
  }
 
260
 
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) {
268
  const { messageId } = data;
269
 
270
  const artifact = this.#getArtifact(messageId);
 
273
  unreachable('Artifact not found');
274
  }
275
 
276
+ return artifact.runner.addAction(data);
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) {
288
  const { messageId } = data;
289
 
290
  const artifact = this.#getArtifact(messageId);
 
309
  this.#editorStore.updateFile(fullPath, data.action.content);
310
 
311
  if (!isStreaming) {
 
312
  await artifact.runner.runAction(data);
313
+ this.resetAllFileModifications();
314
  }
315
  } else {
316
+ await artifact.runner.runAction(data);
317
  }
318
  }
319
 
 
328
 
329
  for (const [filePath, dirent] of Object.entries(files)) {
330
  if (dirent?.type === 'file' && !dirent.isBinary) {
331
+ const relativePath = extractRelativePath(filePath);
 
332
 
333
  // split the path into segments
334
  const pathSegments = relativePath.split('/');
 
358
 
359
  for (const [filePath, dirent] of Object.entries(files)) {
360
  if (dirent?.type === 'file' && !dirent.isBinary) {
361
+ const relativePath = extractRelativePath(filePath);
362
  const pathSegments = relativePath.split('/');
363
  let currentHandle = targetHandle;
364
 
 
432
  content: Buffer.from(dirent.content).toString('base64'),
433
  encoding: 'base64',
434
  });
435
+ return { path: extractRelativePath(filePath), sha: blob.sha };
436
  }
437
  })
438
  );
app/routes/api.enhancer.ts CHANGED
@@ -2,7 +2,7 @@ import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
5
- import type { StreamingOptions } from '~/lib/.server/llm/stream-text';
6
 
7
  const encoder = new TextEncoder();
8
  const decoder = new TextDecoder();
@@ -12,25 +12,27 @@ export async function action(args: ActionFunctionArgs) {
12
  }
13
 
14
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
15
- const { message, model, provider, apiKeys } = await request.json<{
16
  message: string;
17
  model: string;
18
- provider: string;
19
  apiKeys?: Record<string, string>;
20
  }>();
21
 
22
- // Validate 'model' and 'provider' fields
 
 
23
  if (!model || typeof model !== 'string') {
24
  throw new Response('Invalid or missing model', {
25
  status: 400,
26
- statusText: 'Bad Request'
27
  });
28
  }
29
 
30
- if (!provider || typeof provider !== 'string') {
31
  throw new Response('Invalid or missing provider', {
32
  status: 400,
33
- statusText: 'Bad Request'
34
  });
35
  }
36
 
@@ -39,7 +41,9 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
39
  [
40
  {
41
  role: 'user',
42
- content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n` + stripIndents`
 
 
43
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
44
 
45
  IMPORTANT: Only respond with the improved prompt and nothing else!
@@ -52,23 +56,24 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
52
  ],
53
  context.cloudflare.env,
54
  undefined,
55
- apiKeys
56
  );
57
 
58
  const transformStream = new TransformStream({
59
  transform(chunk, controller) {
60
  const text = decoder.decode(chunk);
61
- const lines = text.split('\n').filter(line => line.trim() !== '');
62
-
63
  for (const line of lines) {
64
  try {
65
  const parsed = parseStreamPart(line);
 
66
  if (parsed.type === 'text') {
67
  controller.enqueue(encoder.encode(parsed.value));
68
  }
69
  } catch (e) {
70
- // Skip invalid JSON lines
71
- console.warn('Failed to parse stream part:', line);
72
  }
73
  }
74
  },
@@ -83,7 +88,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
83
  if (error instanceof Error && error.message?.includes('API key')) {
84
  throw new Response('Invalid or missing API key', {
85
  status: 401,
86
- statusText: 'Unauthorized'
87
  });
88
  }
89
 
 
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
5
+ import type { ProviderInfo } from '~/types/model';
6
 
7
  const encoder = new TextEncoder();
8
  const decoder = new TextDecoder();
 
12
  }
13
 
14
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
15
+ const { message, model, provider, apiKeys } = await request.json<{
16
  message: string;
17
  model: string;
18
+ provider: ProviderInfo;
19
  apiKeys?: Record<string, string>;
20
  }>();
21
 
22
+ const { name: providerName } = provider;
23
+
24
+ // validate 'model' and 'provider' fields
25
  if (!model || typeof model !== 'string') {
26
  throw new Response('Invalid or missing model', {
27
  status: 400,
28
+ statusText: 'Bad Request',
29
  });
30
  }
31
 
32
+ if (!providerName || typeof providerName !== 'string') {
33
  throw new Response('Invalid or missing provider', {
34
  status: 400,
35
+ statusText: 'Bad Request',
36
  });
37
  }
38
 
 
41
  [
42
  {
43
  role: 'user',
44
+ content:
45
+ `[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
46
+ stripIndents`
47
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
48
 
49
  IMPORTANT: Only respond with the improved prompt and nothing else!
 
56
  ],
57
  context.cloudflare.env,
58
  undefined,
59
+ apiKeys,
60
  );
61
 
62
  const transformStream = new TransformStream({
63
  transform(chunk, controller) {
64
  const text = decoder.decode(chunk);
65
+ const lines = text.split('\n').filter((line) => line.trim() !== '');
66
+
67
  for (const line of lines) {
68
  try {
69
  const parsed = parseStreamPart(line);
70
+
71
  if (parsed.type === 'text') {
72
  controller.enqueue(encoder.encode(parsed.value));
73
  }
74
  } catch (e) {
75
+ // skip invalid JSON lines
76
+ console.warn('Failed to parse stream part:', line, e);
77
  }
78
  }
79
  },
 
88
  if (error instanceof Error && error.message?.includes('API key')) {
89
  throw new Response('Invalid or missing API key', {
90
  status: 401,
91
+ statusText: 'Unauthorized',
92
  });
93
  }
94
 
app/utils/constants.ts CHANGED
@@ -12,12 +12,12 @@ 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' },
16
- { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet (old)', provider: 'Anthropic' },
17
- { name: 'claude-3-5-haiku-latest', label: 'Claude 3.5 Haiku (new)', provider: 'Anthropic' },
18
- { name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic' },
19
- { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic' },
20
- { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic' }
21
  ],
22
  getApiKeyLink: "https://console.anthropic.com/settings/keys",
23
  },
@@ -33,23 +33,40 @@ const PROVIDER_LIST: ProviderInfo[] = [
33
  staticModels: [],
34
  getDynamicModels: getOpenAILikeModels
35
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  {
37
  name: 'OpenRouter',
38
  staticModels: [
39
- { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
40
  {
41
  name: 'anthropic/claude-3.5-sonnet',
42
  label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
43
  provider: 'OpenRouter'
 
44
  },
45
- { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
46
- { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
47
- { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
48
- { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
49
- { name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter' },
50
- { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter' },
51
- { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
52
- { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' }
53
  ],
54
  getDynamicModels: getOpenRouterModels,
55
  getApiKeyLink: 'https://openrouter.ai/settings/keys',
@@ -57,54 +74,70 @@ const PROVIDER_LIST: ProviderInfo[] = [
57
  }, {
58
  name: 'Google',
59
  staticModels: [
60
- { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google' },
61
- { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google' }
 
 
 
 
62
  ],
63
  getApiKeyLink: 'https://aistudio.google.com/app/apikey'
64
  }, {
65
  name: 'Groq',
66
  staticModels: [
67
- { name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq' },
68
- { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq' },
69
- { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq' },
70
- { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq' },
71
- { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq' }
72
  ],
73
  getApiKeyLink: 'https://console.groq.com/keys'
74
- }, {
 
 
 
 
 
 
 
 
 
 
 
 
75
  name: 'OpenAI',
76
  staticModels: [
77
- { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
78
- { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
79
- { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI' },
80
- { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI' }
81
  ],
82
  getApiKeyLink: "https://platform.openai.com/api-keys",
83
  }, {
84
  name: 'xAI',
85
  staticModels: [
86
- { name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI' }
87
  ],
88
  getApiKeyLink: 'https://docs.x.ai/docs/quickstart#creating-an-api-key'
89
  }, {
90
  name: 'Deepseek',
91
  staticModels: [
92
- { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek' },
93
- { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek' }
94
  ],
95
  getApiKeyLink: 'https://platform.deepseek.com/api_keys'
96
  }, {
97
  name: 'Mistral',
98
  staticModels: [
99
- { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral' },
100
- { name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral' },
101
- { name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral' },
102
- { name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral' },
103
- { name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral' },
104
- { name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral' },
105
- { name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral' },
106
- { name: 'codestral-latest', label: 'Codestral', provider: 'Mistral' },
107
- { name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral' }
108
  ],
109
  getApiKeyLink: 'https://console.mistral.ai/api-keys/'
110
  }, {
@@ -148,7 +181,8 @@ async function getOllamaModels(): Promise<ModelInfo[]> {
148
  return data.models.map((model: OllamaModel) => ({
149
  name: model.name,
150
  label: `${model.name} (${model.details.parameter_size})`,
151
- provider: 'Ollama'
 
152
  }));
153
  } catch (e) {
154
  return [];
@@ -201,8 +235,9 @@ async function getOpenRouterModels(): Promise<ModelInfo[]> {
201
  name: m.id,
202
  label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(
203
  2)} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(
204
- m.context_length / 1000)}k`,
205
- provider: 'OpenRouter'
 
206
  }));
207
  }
208
 
 
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
  },
 
33
  staticModels: [],
34
  getDynamicModels: getOpenAILikeModels
35
  },
36
+ {
37
+ name: 'Cohere',
38
+ staticModels: [
39
+ { name: 'command-r-plus-08-2024', label: 'Command R plus Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
40
+ { name: 'command-r-08-2024', label: 'Command R Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
41
+ { name: 'command-r-plus', label: 'Command R plus', provider: 'Cohere', maxTokenAllowed: 4096 },
42
+ { name: 'command-r', label: 'Command R', provider: 'Cohere', maxTokenAllowed: 4096 },
43
+ { name: 'command', label: 'Command', provider: 'Cohere', maxTokenAllowed: 4096 },
44
+ { name: 'command-nightly', label: 'Command Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
45
+ { name: 'command-light', label: 'Command Light', provider: 'Cohere', maxTokenAllowed: 4096 },
46
+ { name: 'command-light-nightly', label: 'Command Light Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
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',
54
  staticModels: [
55
+ { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI', maxTokenAllowed: 8000 },
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',
 
74
  }, {
75
  name: 'Google',
76
  staticModels: [
77
+ { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
78
+ { name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 },
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
+ {
108
  name: 'OpenAI',
109
  staticModels: [
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 },
133
+ { name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral', maxTokenAllowed: 8000 },
134
+ { name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral', maxTokenAllowed: 8000 },
135
+ { name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral', maxTokenAllowed: 8000 },
136
+ { name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral', maxTokenAllowed: 8000 },
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
  }, {
 
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 [];
 
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
 
app/utils/diff.spec.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { extractRelativePath } from './diff';
3
+ import { WORK_DIR } from './constants';
4
+
5
+ describe('Diff', () => {
6
+ it('should strip out Work_dir', () => {
7
+ const filePath = `${WORK_DIR}/index.js`;
8
+ const result = extractRelativePath(filePath);
9
+ expect(result).toBe('index.js');
10
+ });
11
+ });
app/utils/diff.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { createTwoFilesPatch } from 'diff';
2
  import type { FileMap } from '~/lib/stores/files';
3
- import { MODIFICATIONS_TAG_NAME } from './constants';
4
 
5
  export const modificationsRegex = new RegExp(
6
  `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
@@ -75,6 +75,15 @@ export function diffFiles(fileName: string, oldFileContent: string, newFileConte
75
  return unifiedDiff;
76
  }
77
 
 
 
 
 
 
 
 
 
 
78
  /**
79
  * Converts the unified diff to HTML.
80
  *
 
1
  import { createTwoFilesPatch } from 'diff';
2
  import type { FileMap } from '~/lib/stores/files';
3
+ import { MODIFICATIONS_TAG_NAME, WORK_DIR } from './constants';
4
 
5
  export const modificationsRegex = new RegExp(
6
  `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
 
75
  return unifiedDiff;
76
  }
77
 
78
+ const regex = new RegExp(`^${WORK_DIR}\/`);
79
+
80
+ /**
81
+ * Strips out the work directory from the file path.
82
+ */
83
+ export function extractRelativePath(filePath: string) {
84
+ return filePath.replace(regex, '');
85
+ }
86
+
87
  /**
88
  * Converts the unified diff to HTML.
89
  *
app/utils/types.ts CHANGED
@@ -25,6 +25,7 @@ export interface ModelInfo {
25
  name: string;
26
  label: string;
27
  provider: string;
 
28
  }
29
 
30
  export interface ProviderInfo {
 
25
  name: string;
26
  label: string;
27
  provider: string;
28
+ maxTokenAllowed: number;
29
  }
30
 
31
  export interface ProviderInfo {
docker-compose.yaml CHANGED
@@ -14,6 +14,7 @@ services:
14
  # No strictly neded but serving as hints for Coolify
15
  - PORT=5173
16
  - GROQ_API_KEY=${GROQ_API_KEY}
 
17
  - OPENAI_API_KEY=${OPENAI_API_KEY}
18
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
19
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
@@ -41,6 +42,7 @@ services:
41
  - WATCHPACK_POLLING=true
42
  - PORT=5173
43
  - GROQ_API_KEY=${GROQ_API_KEY}
 
44
  - OPENAI_API_KEY=${OPENAI_API_KEY}
45
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
46
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
 
14
  # No strictly neded but serving as hints for Coolify
15
  - PORT=5173
16
  - GROQ_API_KEY=${GROQ_API_KEY}
17
+ - HuggingFace_API_KEY=${HuggingFace_API_KEY}
18
  - OPENAI_API_KEY=${OPENAI_API_KEY}
19
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
20
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
 
42
  - WATCHPACK_POLLING=true
43
  - PORT=5173
44
  - GROQ_API_KEY=${GROQ_API_KEY}
45
+ - HuggingFace_API_KEY=${HuggingFace_API_KEY}
46
  - OPENAI_API_KEY=${OPENAI_API_KEY}
47
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
48
  - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
eslint.config.mjs CHANGED
@@ -4,7 +4,7 @@ import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/
4
 
5
  export default [
6
  {
7
- ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
 
4
 
5
  export default [
6
  {
7
+ ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
package.json CHANGED
@@ -27,6 +27,7 @@
27
  },
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
 
30
  "@ai-sdk/google": "^0.0.52",
31
  "@ai-sdk/mistral": "^0.0.43",
32
  "@ai-sdk/openai": "^0.0.66",
@@ -54,6 +55,7 @@
54
  "@openrouter/ai-sdk-provider": "^0.0.5",
55
  "@radix-ui/react-dialog": "^1.1.1",
56
  "@radix-ui/react-dropdown-menu": "^2.1.1",
 
57
  "@remix-run/cloudflare": "^2.10.2",
58
  "@remix-run/cloudflare-pages": "^2.10.2",
59
  "@remix-run/react": "^2.10.2",
 
27
  },
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
+ "@ai-sdk/cohere": "^1.0.1",
31
  "@ai-sdk/google": "^0.0.52",
32
  "@ai-sdk/mistral": "^0.0.43",
33
  "@ai-sdk/openai": "^0.0.66",
 
55
  "@openrouter/ai-sdk-provider": "^0.0.5",
56
  "@radix-ui/react-dialog": "^1.1.1",
57
  "@radix-ui/react-dropdown-menu": "^2.1.1",
58
+ "@radix-ui/react-tooltip": "^1.1.4",
59
  "@remix-run/cloudflare": "^2.10.2",
60
  "@remix-run/cloudflare-pages": "^2.10.2",
61
  "@remix-run/react": "^2.10.2",
pnpm-lock.yaml CHANGED
@@ -14,6 +14,9 @@ importers:
14
  '@ai-sdk/anthropic':
15
  specifier: ^0.0.39
16
  version: 0.0.39([email protected])
 
 
 
17
  '@ai-sdk/google':
18
  specifier: ^0.0.52
19
  version: 0.0.52([email protected])
@@ -95,6 +98,9 @@ importers:
95
  '@radix-ui/react-dropdown-menu':
96
  specifier: ^2.1.1
97
 
 
 
98
  '@remix-run/cloudflare':
99
  specifier: ^2.10.2
100
  version: 2.10.2(@cloudflare/[email protected])([email protected])
@@ -276,6 +282,12 @@ packages:
276
  peerDependencies:
277
  zod: ^3.0.0
278
 
 
 
 
 
 
 
279
  '@ai-sdk/[email protected]':
280
  resolution: {integrity: sha512-bfsA/1Ae0SQ6NfLwWKs5SU4MBwlzJjVhK6bTVBicYFjUxg9liK/W76P1Tq/qK9OlrODACz3i1STOIWsFPpIOuQ==}
281
  engines: {node: '>=18'}
@@ -321,6 +333,15 @@ packages:
321
  zod:
322
  optional: true
323
 
 
 
 
 
 
 
 
 
 
324
  '@ai-sdk/[email protected]':
325
  resolution: {integrity: sha512-oOwPQD8i2Ynpn22cur4sk26FW3mSy6t6/X/K1Ay2yGBKYiSpRyLfObhOrZEGsXDx+3euKy4nEZ193R36NM+tpQ==}
326
  engines: {node: '>=18'}
@@ -333,6 +354,10 @@ packages:
333
  resolution: {integrity: sha512-XMsNGJdGO+L0cxhhegtqZ8+T6nn4EoShS819OvCgI2kLbYTIvk0GWFGD0AXJmxkxs3DrpsJxKAFukFR7bvTkgQ==}
334
  engines: {node: '>=18'}
335
 
 
 
 
 
336
  '@ai-sdk/[email protected]':
337
  resolution: {integrity: sha512-1asDpxgmeHWL0/EZPCLENxfOHT+0jce0z/zasRhascodm2S6f6/KZn5doLG9jdmarcb+GjMjFmmwyOVXz3W1xg==}
338
  engines: {node: '>=18'}
@@ -1377,6 +1402,15 @@ packages:
1377
  '@types/react':
1378
  optional: true
1379
 
 
 
 
 
 
 
 
 
 
1380
  '@radix-ui/[email protected]':
1381
  resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
1382
  peerDependencies:
@@ -1412,6 +1446,19 @@ packages:
1412
  '@types/react-dom':
1413
  optional: true
1414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1415
  '@radix-ui/[email protected]':
1416
  resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==}
1417
  peerDependencies:
@@ -1495,6 +1542,19 @@ packages:
1495
  '@types/react-dom':
1496
  optional: true
1497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1498
  '@radix-ui/[email protected]':
1499
  resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
1500
  peerDependencies:
@@ -1508,6 +1568,19 @@ packages:
1508
  '@types/react-dom':
1509
  optional: true
1510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1511
  '@radix-ui/[email protected]':
1512
  resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
1513
  peerDependencies:
@@ -1543,6 +1616,19 @@ packages:
1543
  '@types/react':
1544
  optional: true
1545
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1546
  '@radix-ui/[email protected]':
1547
  resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
1548
  peerDependencies:
@@ -1597,6 +1683,19 @@ packages:
1597
  '@types/react':
1598
  optional: true
1599
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1600
  '@radix-ui/[email protected]':
1601
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
1602
 
@@ -2956,6 +3055,10 @@ packages:
2956
  resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==}
2957
  engines: {node: '>=14.18'}
2958
 
 
 
 
 
2959
2960
  resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
2961
 
@@ -5610,6 +5713,12 @@ snapshots:
5610
  '@ai-sdk/provider-utils': 1.0.9([email protected])
5611
  zod: 3.23.8
5612
 
 
 
 
 
 
 
5613
5614
  dependencies:
5615
  '@ai-sdk/provider': 0.0.24
@@ -5656,6 +5765,15 @@ snapshots:
5656
  optionalDependencies:
5657
  zod: 3.23.8
5658
 
 
 
 
 
 
 
 
 
 
5659
  '@ai-sdk/[email protected]':
5660
  dependencies:
5661
  json-schema: 0.4.0
@@ -5668,6 +5786,10 @@ snapshots:
5668
  dependencies:
5669
  json-schema: 0.4.0
5670
 
 
 
 
 
5671
5672
  dependencies:
5673
  '@ai-sdk/provider-utils': 1.0.20([email protected])
@@ -6712,6 +6834,12 @@ snapshots:
6712
  optionalDependencies:
6713
  '@types/react': 18.3.3
6714
 
 
 
 
 
 
 
6715
6716
  dependencies:
6717
  '@radix-ui/primitive': 1.1.0
@@ -6753,6 +6881,19 @@ snapshots:
6753
  '@types/react': 18.3.3
6754
  '@types/react-dom': 18.3.0
6755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6756
6757
  dependencies:
6758
  '@radix-ui/primitive': 1.1.0
@@ -6846,6 +6987,16 @@ snapshots:
6846
  '@types/react': 18.3.3
6847
  '@types/react-dom': 18.3.0
6848
 
 
 
 
 
 
 
 
 
 
 
6849
6850
  dependencies:
6851
  '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
@@ -6856,6 +7007,16 @@ snapshots:
6856
  '@types/react': 18.3.3
6857
  '@types/react-dom': 18.3.0
6858
 
 
 
 
 
 
 
 
 
 
 
6859
6860
  dependencies:
6861
  '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
@@ -6889,6 +7050,26 @@ snapshots:
6889
  optionalDependencies:
6890
  '@types/react': 18.3.3
6891
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6892
6893
  dependencies:
6894
  react: 18.3.1
@@ -6929,6 +7110,15 @@ snapshots:
6929
  optionalDependencies:
6930
  '@types/react': 18.3.3
6931
 
 
 
 
 
 
 
 
 
 
6932
  '@radix-ui/[email protected]': {}
6933
 
6934
@@ -8606,6 +8796,8 @@ snapshots:
8606
 
8607
8608
 
 
 
8609
8610
  dependencies:
8611
  md5.js: 1.3.5
 
14
  '@ai-sdk/anthropic':
15
  specifier: ^0.0.39
16
  version: 0.0.39([email protected])
17
+ '@ai-sdk/cohere':
18
+ specifier: ^1.0.1
19
+ version: 1.0.1([email protected])
20
  '@ai-sdk/google':
21
  specifier: ^0.0.52
22
  version: 0.0.52([email protected])
 
98
  '@radix-ui/react-dropdown-menu':
99
  specifier: ^2.1.1
100
101
+ '@radix-ui/react-tooltip':
102
+ specifier: ^1.1.4
103
104
  '@remix-run/cloudflare':
105
  specifier: ^2.10.2
106
  version: 2.10.2(@cloudflare/[email protected])([email protected])
 
282
  peerDependencies:
283
  zod: ^3.0.0
284
 
285
+ '@ai-sdk/[email protected]':
286
+ resolution: {integrity: sha512-xLaSYl/hs9EqfpvT9PvqZrDWjJPQPZBd0iT32T6812vN6kwuEQ6sSgQvqHWczIqxeej2GNRgMQwDL6Lh0L5pZw==}
287
+ engines: {node: '>=18'}
288
+ peerDependencies:
289
+ zod: ^3.0.0
290
+
291
  '@ai-sdk/[email protected]':
292
  resolution: {integrity: sha512-bfsA/1Ae0SQ6NfLwWKs5SU4MBwlzJjVhK6bTVBicYFjUxg9liK/W76P1Tq/qK9OlrODACz3i1STOIWsFPpIOuQ==}
293
  engines: {node: '>=18'}
 
333
  zod:
334
  optional: true
335
 
336
+ '@ai-sdk/[email protected]':
337
+ resolution: {integrity: sha512-TNg7rPhRtETB2Z9F0JpOvpGii9Fs8EWM8nYy1jEkvSXkrPJ6b/9zVnDdaJsmLFDyrMbOsPJlkblYtmYEQou36w==}
338
+ engines: {node: '>=18'}
339
+ peerDependencies:
340
+ zod: ^3.0.0
341
+ peerDependenciesMeta:
342
+ zod:
343
+ optional: true
344
+
345
  '@ai-sdk/[email protected]':
346
  resolution: {integrity: sha512-oOwPQD8i2Ynpn22cur4sk26FW3mSy6t6/X/K1Ay2yGBKYiSpRyLfObhOrZEGsXDx+3euKy4nEZ193R36NM+tpQ==}
347
  engines: {node: '>=18'}
 
354
  resolution: {integrity: sha512-XMsNGJdGO+L0cxhhegtqZ8+T6nn4EoShS819OvCgI2kLbYTIvk0GWFGD0AXJmxkxs3DrpsJxKAFukFR7bvTkgQ==}
355
  engines: {node: '>=18'}
356
 
357
+ '@ai-sdk/[email protected]':
358
+ resolution: {integrity: sha512-Sj29AzooJ7SYvhPd+AAWt/E7j63E9+AzRnoMHUaJPRYzOd/WDrVNxxv85prF9gDcQ7XPVlSk9j6oAZV9/DXYpA==}
359
+ engines: {node: '>=18'}
360
+
361
  '@ai-sdk/[email protected]':
362
  resolution: {integrity: sha512-1asDpxgmeHWL0/EZPCLENxfOHT+0jce0z/zasRhascodm2S6f6/KZn5doLG9jdmarcb+GjMjFmmwyOVXz3W1xg==}
363
  engines: {node: '>=18'}
 
1402
  '@types/react':
1403
  optional: true
1404
 
1405
+ '@radix-ui/[email protected]':
1406
+ resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
1407
+ peerDependencies:
1408
+ '@types/react': '*'
1409
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1410
+ peerDependenciesMeta:
1411
+ '@types/react':
1412
+ optional: true
1413
+
1414
  '@radix-ui/[email protected]':
1415
  resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
1416
  peerDependencies:
 
1446
  '@types/react-dom':
1447
  optional: true
1448
 
1449
+ '@radix-ui/[email protected]':
1450
+ resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
1451
+ peerDependencies:
1452
+ '@types/react': '*'
1453
+ '@types/react-dom': '*'
1454
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1455
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1456
+ peerDependenciesMeta:
1457
+ '@types/react':
1458
+ optional: true
1459
+ '@types/react-dom':
1460
+ optional: true
1461
+
1462
  '@radix-ui/[email protected]':
1463
  resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==}
1464
  peerDependencies:
 
1542
  '@types/react-dom':
1543
  optional: true
1544
 
1545
+ '@radix-ui/[email protected]':
1546
+ resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
1547
+ peerDependencies:
1548
+ '@types/react': '*'
1549
+ '@types/react-dom': '*'
1550
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1551
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1552
+ peerDependenciesMeta:
1553
+ '@types/react':
1554
+ optional: true
1555
+ '@types/react-dom':
1556
+ optional: true
1557
+
1558
  '@radix-ui/[email protected]':
1559
  resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
1560
  peerDependencies:
 
1568
  '@types/react-dom':
1569
  optional: true
1570
 
1571
+ '@radix-ui/[email protected]':
1572
+ resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
1573
+ peerDependencies:
1574
+ '@types/react': '*'
1575
+ '@types/react-dom': '*'
1576
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1577
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1578
+ peerDependenciesMeta:
1579
+ '@types/react':
1580
+ optional: true
1581
+ '@types/react-dom':
1582
+ optional: true
1583
+
1584
  '@radix-ui/[email protected]':
1585
  resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
1586
  peerDependencies:
 
1616
  '@types/react':
1617
  optional: true
1618
 
1619
+ '@radix-ui/[email protected]':
1620
+ resolution: {integrity: sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==}
1621
+ peerDependencies:
1622
+ '@types/react': '*'
1623
+ '@types/react-dom': '*'
1624
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1625
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1626
+ peerDependenciesMeta:
1627
+ '@types/react':
1628
+ optional: true
1629
+ '@types/react-dom':
1630
+ optional: true
1631
+
1632
  '@radix-ui/[email protected]':
1633
  resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
1634
  peerDependencies:
 
1683
  '@types/react':
1684
  optional: true
1685
 
1686
+ '@radix-ui/[email protected]':
1687
+ resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
1688
+ peerDependencies:
1689
+ '@types/react': '*'
1690
+ '@types/react-dom': '*'
1691
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1692
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
1693
+ peerDependenciesMeta:
1694
+ '@types/react':
1695
+ optional: true
1696
+ '@types/react-dom':
1697
+ optional: true
1698
+
1699
  '@radix-ui/[email protected]':
1700
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
1701
 
 
3055
  resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==}
3056
  engines: {node: '>=14.18'}
3057
 
3058
3059
+ resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
3060
+ engines: {node: '>=18.0.0'}
3061
+
3062
3063
  resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
3064
 
 
5713
  '@ai-sdk/provider-utils': 1.0.9([email protected])
5714
  zod: 3.23.8
5715
 
5716
5717
+ dependencies:
5718
+ '@ai-sdk/provider': 1.0.0
5719
+ '@ai-sdk/provider-utils': 2.0.1([email protected])
5720
+ zod: 3.23.8
5721
+
5722
5723
  dependencies:
5724
  '@ai-sdk/provider': 0.0.24
 
5765
  optionalDependencies:
5766
  zod: 3.23.8
5767
 
5768
5769
+ dependencies:
5770
+ '@ai-sdk/provider': 1.0.0
5771
+ eventsource-parser: 3.0.0
5772
+ nanoid: 3.3.7
5773
+ secure-json-parse: 2.7.0
5774
+ optionalDependencies:
5775
+ zod: 3.23.8
5776
+
5777
  '@ai-sdk/[email protected]':
5778
  dependencies:
5779
  json-schema: 0.4.0
 
5786
  dependencies:
5787
  json-schema: 0.4.0
5788
 
5789
+ '@ai-sdk/[email protected]':
5790
+ dependencies:
5791
+ json-schema: 0.4.0
5792
+
5793
5794
  dependencies:
5795
  '@ai-sdk/provider-utils': 1.0.20([email protected])
 
6834
  optionalDependencies:
6835
  '@types/react': 18.3.3
6836
 
6837
6838
+ dependencies:
6839
+ react: 18.3.1
6840
+ optionalDependencies:
6841
+ '@types/react': 18.3.3
6842
+
6843
6844
  dependencies:
6845
  '@radix-ui/primitive': 1.1.0
 
6881
  '@types/react': 18.3.3
6882
  '@types/react-dom': 18.3.0
6883
 
6884
6885
+ dependencies:
6886
+ '@radix-ui/primitive': 1.1.0
6887
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
6888
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
6889
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
6890
+ '@radix-ui/react-use-escape-keydown': 1.1.0(@types/[email protected])([email protected])
6891
+ react: 18.3.1
6892
+ react-dom: 18.3.1([email protected])
6893
+ optionalDependencies:
6894
+ '@types/react': 18.3.3
6895
+ '@types/react-dom': 18.3.0
6896
+
6897
6898
  dependencies:
6899
  '@radix-ui/primitive': 1.1.0
 
6987
  '@types/react': 18.3.3
6988
  '@types/react-dom': 18.3.0
6989
 
6990
6991
+ dependencies:
6992
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
6993
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
6994
+ react: 18.3.1
6995
+ react-dom: 18.3.1([email protected])
6996
+ optionalDependencies:
6997
+ '@types/react': 18.3.3
6998
+ '@types/react-dom': 18.3.0
6999
+
7000
7001
  dependencies:
7002
  '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
 
7007
  '@types/react': 18.3.3
7008
  '@types/react-dom': 18.3.0
7009
 
7010
7011
+ dependencies:
7012
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
7013
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
7014
+ react: 18.3.1
7015
+ react-dom: 18.3.1([email protected])
7016
+ optionalDependencies:
7017
+ '@types/react': 18.3.3
7018
+ '@types/react-dom': 18.3.0
7019
+
7020
7021
  dependencies:
7022
  '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
 
7050
  optionalDependencies:
7051
  '@types/react': 18.3.3
7052
 
7053
7054
+ dependencies:
7055
+ '@radix-ui/primitive': 1.1.0
7056
+ '@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
7057
+ '@radix-ui/react-context': 1.1.1(@types/[email protected])([email protected])
7058
+ '@radix-ui/react-dismissable-layer': 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7059
+ '@radix-ui/react-id': 1.1.0(@types/[email protected])([email protected])
7060
7061
7062
+ '@radix-ui/react-presence': 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7063
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7064
+ '@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
7065
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
7066
+ '@radix-ui/react-visually-hidden': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7067
+ react: 18.3.1
7068
+ react-dom: 18.3.1([email protected])
7069
+ optionalDependencies:
7070
+ '@types/react': 18.3.3
7071
+ '@types/react-dom': 18.3.0
7072
+
7073
7074
  dependencies:
7075
  react: 18.3.1
 
7110
  optionalDependencies:
7111
  '@types/react': 18.3.3
7112
 
7113
7114
+ dependencies:
7115
+ '@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
7116
+ react: 18.3.1
7117
+ react-dom: 18.3.1([email protected])
7118
+ optionalDependencies:
7119
+ '@types/react': 18.3.3
7120
+ '@types/react-dom': 18.3.0
7121
+
7122
  '@radix-ui/[email protected]': {}
7123
 
7124
 
8796
 
8797
8798
 
8799
8800
+
8801
8802
  dependencies:
8803
  md5.js: 1.3.5
worker-configuration.d.ts CHANGED
@@ -2,6 +2,7 @@ interface Env {
2
  ANTHROPIC_API_KEY: string;
3
  OPENAI_API_KEY: string;
4
  GROQ_API_KEY: string;
 
5
  OPEN_ROUTER_API_KEY: string;
6
  OLLAMA_API_BASE_URL: string;
7
  OPENAI_LIKE_API_KEY: string;
 
2
  ANTHROPIC_API_KEY: string;
3
  OPENAI_API_KEY: string;
4
  GROQ_API_KEY: string;
5
+ HuggingFace_API_KEY: string;
6
  OPEN_ROUTER_API_KEY: string;
7
  OLLAMA_API_BASE_URL: string;
8
  OPENAI_LIKE_API_KEY: string;