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