Chris Mahoney commited on
Commit
d18517a
·
unverified ·
2 Parent(s): 1ba0606 52cd1ae

Merge pull request #101 from ali00209/new_bolt1

Browse files
app/components/chat/APIKeyManager.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+
4
+ interface APIKeyManagerProps {
5
+ provider: string;
6
+ apiKey: string;
7
+ setApiKey: (key: string) => void;
8
+ }
9
+
10
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
11
+ const [isEditing, setIsEditing] = useState(false);
12
+ const [tempKey, setTempKey] = useState(apiKey);
13
+
14
+ const handleSave = () => {
15
+ setApiKey(tempKey);
16
+ setIsEditing(false);
17
+ };
18
+
19
+ return (
20
+ <div className="flex items-center gap-2 mt-2 mb-2">
21
+ <span className="text-sm text-bolt-elements-textSecondary">{provider} API Key:</span>
22
+ {isEditing ? (
23
+ <>
24
+ <input
25
+ type="password"
26
+ value={tempKey}
27
+ onChange={(e) => setTempKey(e.target.value)}
28
+ className="flex-1 p-1 text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
29
+ />
30
+ <IconButton onClick={handleSave} title="Save API Key">
31
+ <div className="i-ph:check" />
32
+ </IconButton>
33
+ <IconButton onClick={() => setIsEditing(false)} title="Cancel">
34
+ <div className="i-ph:x" />
35
+ </IconButton>
36
+ </>
37
+ ) : (
38
+ <>
39
+ <span className="flex-1 text-sm text-bolt-elements-textPrimary">
40
+ {apiKey ? '••••••••' : 'Not set'}
41
+ </span>
42
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
43
+ <div className="i-ph:pencil-simple" />
44
+ </IconButton>
45
+ </>
46
+ )}
47
+ </div>
48
+ );
49
+ };
app/components/chat/BaseChat.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
- import React, { type RefCallback } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
@@ -11,6 +11,8 @@ import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
 
 
14
 
15
  import styles from './BaseChat.module.scss';
16
 
@@ -24,18 +26,17 @@ const EXAMPLE_PROMPTS = [
24
 
25
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
26
 
27
- const ModelSelector = ({ model, setModel, modelList, providerList }) => {
28
- const [provider, setProvider] = useState(DEFAULT_PROVIDER);
29
  return (
30
- <div className="mb-2">
31
- <select
32
  value={provider}
33
  onChange={(e) => {
34
  setProvider(e.target.value);
35
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
36
  setModel(firstModel ? firstModel.name : '');
37
  }}
38
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
39
  >
40
  {providerList.map((provider) => (
41
  <option key={provider} value={provider}>
@@ -52,7 +53,7 @@ const ModelSelector = ({ model, setModel, modelList, providerList }) => {
52
  <select
53
  value={model}
54
  onChange={(e) => setModel(e.target.value)}
55
- className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none"
56
  >
57
  {[...modelList].filter(e => e.provider == provider && e.name).map((modelOption) => (
58
  <option key={modelOption.name} value={modelOption.name}>
@@ -108,6 +109,41 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
108
  ref,
109
  ) => {
110
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  return (
113
  <div
@@ -122,11 +158,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
122
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
123
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
124
  {!chatStarted && (
125
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
126
- <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
127
  Where ideas begin
128
  </h1>
129
- <p className="mb-4 text-center text-bolt-elements-textSecondary">
130
  Bring ideas to life in seconds or get help on existing projects.
131
  </p>
132
  </div>
@@ -158,15 +194,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
158
  setModel={setModel}
159
  modelList={MODEL_LIST}
160
  providerList={providerList}
 
 
 
 
 
 
 
161
  />
162
  <div
163
  className={classNames(
164
- 'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
165
  )}
166
  >
167
  <textarea
168
  ref={textareaRef}
169
- className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
170
  onKeyDown={(event) => {
171
  if (event.key === 'Enter') {
172
  if (event.shiftKey) {
@@ -205,12 +248,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
205
  />
206
  )}
207
  </ClientOnly>
208
- <div className="flex justify-between text-sm p-4 pt-2">
209
  <div className="flex gap-1 items-center">
210
  <IconButton
211
  title="Enhance prompt"
212
  disabled={input.length === 0 || enhancingPrompt}
213
- className={classNames({
214
  'opacity-100!': enhancingPrompt,
215
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
216
  promptEnhanced,
@@ -219,7 +262,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
219
  >
220
  {enhancingPrompt ? (
221
  <>
222
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
223
  <div className="ml-1.5">Enhancing prompt...</div>
224
  </>
225
  ) : (
@@ -232,7 +275,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
232
  </div>
233
  {input.length > 3 ? (
234
  <div className="text-xs text-bolt-elements-textTertiary">
235
- Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
236
  </div>
237
  ) : null}
238
  </div>
@@ -266,4 +309,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
266
  </div>
267
  );
268
  },
269
- );
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
+ import React, { type RefCallback, useEffect } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
 
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
14
+ import { APIKeyManager } from './APIKeyManager';
15
+ import Cookies from 'js-cookie';
16
 
17
  import styles from './BaseChat.module.scss';
18
 
 
26
 
27
  const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))]
28
 
29
+ const ModelSelector = ({ model, setModel, modelList, providerList, provider, setProvider }) => {
 
30
  return (
31
+ <div className="mb-2 flex gap-2">
32
+ <select
33
  value={provider}
34
  onChange={(e) => {
35
  setProvider(e.target.value);
36
  const firstModel = [...modelList].find(m => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
+ className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
40
  >
41
  {providerList.map((provider) => (
42
  <option key={provider} value={provider}>
 
53
  <select
54
  value={model}
55
  onChange={(e) => setModel(e.target.value)}
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].filter(e => e.provider == provider && e.name).map((modelOption) => (
59
  <option key={modelOption.name} value={modelOption.name}>
 
109
  ref,
110
  ) => {
111
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
112
+ const [provider, setProvider] = useState(DEFAULT_PROVIDER);
113
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
114
+
115
+ useEffect(() => {
116
+ // Load API keys from cookies on component mount
117
+ try {
118
+ const storedApiKeys = Cookies.get('apiKeys');
119
+ if (storedApiKeys) {
120
+ const parsedKeys = JSON.parse(storedApiKeys);
121
+ if (typeof parsedKeys === 'object' && parsedKeys !== null) {
122
+ setApiKeys(parsedKeys);
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error('Error loading API keys from cookies:', error);
127
+ // Clear invalid cookie data
128
+ Cookies.remove('apiKeys');
129
+ }
130
+ }, []);
131
+
132
+ const updateApiKey = (provider: string, key: string) => {
133
+ try {
134
+ const updatedApiKeys = { ...apiKeys, [provider]: key };
135
+ setApiKeys(updatedApiKeys);
136
+ // Save updated API keys to cookies with 30 day expiry and secure settings
137
+ Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
138
+ expires: 30, // 30 days
139
+ secure: true, // Only send over HTTPS
140
+ sameSite: 'strict', // Protect against CSRF
141
+ path: '/' // Accessible across the site
142
+ });
143
+ } catch (error) {
144
+ console.error('Error saving API keys to cookies:', error);
145
+ }
146
+ };
147
 
148
  return (
149
  <div
 
158
  <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
159
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
160
  {!chatStarted && (
161
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
162
+ <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
163
  Where ideas begin
164
  </h1>
165
+ <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
166
  Bring ideas to life in seconds or get help on existing projects.
167
  </p>
168
  </div>
 
194
  setModel={setModel}
195
  modelList={MODEL_LIST}
196
  providerList={providerList}
197
+ provider={provider}
198
+ setProvider={setProvider}
199
+ />
200
+ <APIKeyManager
201
+ provider={provider}
202
+ apiKey={apiKeys[provider] || ''}
203
+ setApiKey={(key) => updateApiKey(provider, key)}
204
  />
205
  <div
206
  className={classNames(
207
+ 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
208
  )}
209
  >
210
  <textarea
211
  ref={textareaRef}
212
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
213
  onKeyDown={(event) => {
214
  if (event.key === 'Enter') {
215
  if (event.shiftKey) {
 
248
  />
249
  )}
250
  </ClientOnly>
251
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
252
  <div className="flex gap-1 items-center">
253
  <IconButton
254
  title="Enhance prompt"
255
  disabled={input.length === 0 || enhancingPrompt}
256
+ className={classNames('transition-all', {
257
  'opacity-100!': enhancingPrompt,
258
  'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
259
  promptEnhanced,
 
262
  >
263
  {enhancingPrompt ? (
264
  <>
265
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
266
  <div className="ml-1.5">Enhancing prompt...</div>
267
  </>
268
  ) : (
 
275
  </div>
276
  {input.length > 3 ? (
277
  <div className="text-xs text-bolt-elements-textTertiary">
278
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for a new line
279
  </div>
280
  ) : null}
281
  </div>
 
309
  </div>
310
  );
311
  },
312
+ );
app/components/chat/Chat.client.tsx CHANGED
@@ -15,6 +15,7 @@ import { DEFAULT_MODEL } from '~/utils/constants';
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
 
18
 
19
  const toastAnimation = cssTransition({
20
  enter: 'animated fadeInRight',
@@ -79,8 +80,13 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
79
 
80
  const [animationScope, animate] = useAnimate();
81
 
 
 
82
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
83
  api: '/api/chat',
 
 
 
84
  onError: (error) => {
85
  logger.error('Request failed\n\n', error);
86
  toast.error('There was an error processing your request');
@@ -202,6 +208,13 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
202
 
203
  const [messageRef, scrollRef] = useSnapScroll();
204
 
 
 
 
 
 
 
 
205
  return (
206
  <BaseChat
207
  ref={animationScope}
 
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { createScopedLogger, renderLogger } from '~/utils/logger';
17
  import { BaseChat } from './BaseChat';
18
+ import Cookies from 'js-cookie';
19
 
20
  const toastAnimation = cssTransition({
21
  enter: 'animated fadeInRight',
 
80
 
81
  const [animationScope, animate] = useAnimate();
82
 
83
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
84
+
85
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
86
  api: '/api/chat',
87
+ body: {
88
+ apiKeys
89
+ },
90
  onError: (error) => {
91
  logger.error('Request failed\n\n', error);
92
  toast.error('There was an error processing your request');
 
208
 
209
  const [messageRef, scrollRef] = useSnapScroll();
210
 
211
+ useEffect(() => {
212
+ const storedApiKeys = Cookies.get('apiKeys');
213
+ if (storedApiKeys) {
214
+ setApiKeys(JSON.parse(storedApiKeys));
215
+ }
216
+ }, []);
217
+
218
  return (
219
  <BaseChat
220
  ref={animationScope}
app/lib/.server/llm/api-key.ts CHANGED
@@ -2,12 +2,18 @@
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
- export function getAPIKey(cloudflareEnv: Env, provider: string) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
 
 
 
 
 
 
11
  switch (provider) {
12
  case 'Anthropic':
13
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
 
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import { env } from 'node:process';
4
 
5
+ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record<string, string>) {
6
  /**
7
  * The `cloudflareEnv` is only used when deployed or when previewing locally.
8
  * In development the environment variables are available through `env`.
9
  */
10
 
11
+ // First check user-provided API keys
12
+ if (userApiKeys?.[provider]) {
13
+ return userApiKeys[provider];
14
+ }
15
+
16
+ // Fall back to environment variables
17
  switch (provider) {
18
  case 'Anthropic':
19
  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
app/lib/.server/llm/model.ts CHANGED
@@ -91,9 +91,8 @@ export function getXAIModel(apiKey: string, model: string) {
91
 
92
  return openai(model);
93
  }
94
-
95
- export function getModel(provider: string, model: string, env: Env) {
96
- const apiKey = getAPIKey(env, provider);
97
  const baseURL = getBaseURL(env, provider);
98
 
99
  switch (provider) {
 
91
 
92
  return openai(model);
93
  }
94
+ export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
95
+ const apiKey = getAPIKey(env, provider, apiKeys);
 
96
  const baseURL = getBaseURL(env, provider);
97
 
98
  switch (provider) {
app/lib/.server/llm/stream-text.ts CHANGED
@@ -38,7 +38,12 @@ function extractModelFromMessage(message: Message): { model: string; content: st
38
  return { model: DEFAULT_MODEL, content: message.content };
39
  }
40
 
41
- export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
 
 
 
 
 
42
  let currentModel = DEFAULT_MODEL;
43
  const processedMessages = messages.map((message) => {
44
  if (message.role === 'user') {
@@ -54,7 +59,7 @@ export function streamText(messages: Messages, env: Env, options?: StreamingOpti
54
  const provider = MODEL_LIST.find((model) => model.name === currentModel)?.provider || DEFAULT_PROVIDER;
55
 
56
  return _streamText({
57
- model: getModel(provider, currentModel, env),
58
  system: getSystemPrompt(),
59
  maxTokens: MAX_TOKENS,
60
  // headers: {
 
38
  return { model: DEFAULT_MODEL, content: message.content };
39
  }
40
 
41
+ export function streamText(
42
+ messages: Messages,
43
+ env: Env,
44
+ options?: StreamingOptions,
45
+ apiKeys?: Record<string, string>
46
+ ) {
47
  let currentModel = DEFAULT_MODEL;
48
  const processedMessages = messages.map((message) => {
49
  if (message.role === 'user') {
 
59
  const provider = MODEL_LIST.find((model) => model.name === currentModel)?.provider || DEFAULT_PROVIDER;
60
 
61
  return _streamText({
62
+ model: getModel(provider, currentModel, env, apiKeys),
63
  system: getSystemPrompt(),
64
  maxTokens: MAX_TOKENS,
65
  // headers: {
app/routes/api.chat.ts CHANGED
@@ -11,13 +11,17 @@ export async function action(args: ActionFunctionArgs) {
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
- const { messages } = await request.json<{ messages: Messages }>();
 
 
 
15
 
16
  const stream = new SwitchableStream();
17
 
18
  try {
19
  const options: StreamingOptions = {
20
  toolChoice: 'none',
 
21
  onFinish: async ({ text: content, finishReason }) => {
22
  if (finishReason !== 'length') {
23
  return stream.close();
@@ -40,7 +44,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
40
  },
41
  };
42
 
43
- const result = await streamText(messages, context.cloudflare.env, options);
44
 
45
  stream.switchSource(result.toAIStream());
46
 
@@ -52,6 +56,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
52
  });
53
  } catch (error) {
54
  console.log(error);
 
 
 
 
 
 
 
55
 
56
  throw new Response(null, {
57
  status: 500,
 
11
  }
12
 
13
  async function chatAction({ context, request }: ActionFunctionArgs) {
14
+ const { messages, apiKeys } = await request.json<{
15
+ messages: Messages,
16
+ apiKeys: Record<string, string>
17
+ }>();
18
 
19
  const stream = new SwitchableStream();
20
 
21
  try {
22
  const options: StreamingOptions = {
23
  toolChoice: 'none',
24
+ apiKeys,
25
  onFinish: async ({ text: content, finishReason }) => {
26
  if (finishReason !== 'length') {
27
  return stream.close();
 
44
  },
45
  };
46
 
47
+ const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
48
 
49
  stream.switchSource(result.toAIStream());
50
 
 
56
  });
57
  } catch (error) {
58
  console.log(error);
59
+
60
+ if (error.message?.includes('API key')) {
61
+ throw new Response('Invalid or missing API key', {
62
+ status: 401,
63
+ statusText: 'Unauthorized'
64
+ });
65
+ }
66
 
67
  throw new Response(null, {
68
  status: 500,
app/utils/constants.ts CHANGED
@@ -110,4 +110,4 @@ async function initializeModelList(): Promise<void> {
110
  MODEL_LIST = [...ollamaModels,...openAiLikeModels, ...staticModels];
111
  }
112
  initializeModelList().then();
113
- export { getOllamaModels, getOpenAILikeModels, initializeModelList };
 
110
  MODEL_LIST = [...ollamaModels,...openAiLikeModels, ...staticModels];
111
  }
112
  initializeModelList().then();
113
+ export { getOllamaModels, getOpenAILikeModels, initializeModelList };
package.json CHANGED
@@ -28,8 +28,8 @@
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
31
- "@ai-sdk/openai": "^0.0.66",
32
  "@ai-sdk/mistral": "^0.0.43",
 
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
@@ -71,6 +71,7 @@
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
 
74
  "jszip": "^3.10.1",
75
  "nanostores": "^0.10.3",
76
  "ollama-ai-provider": "^0.15.2",
@@ -94,6 +95,7 @@
94
  "@remix-run/dev": "^2.10.0",
95
  "@types/diff": "^5.2.1",
96
  "@types/file-saver": "^2.0.7",
 
97
  "@types/react": "^18.2.20",
98
  "@types/react-dom": "^18.2.7",
99
  "fast-glob": "^3.3.2",
 
28
  "dependencies": {
29
  "@ai-sdk/anthropic": "^0.0.39",
30
  "@ai-sdk/google": "^0.0.52",
 
31
  "@ai-sdk/mistral": "^0.0.43",
32
+ "@ai-sdk/openai": "^0.0.66",
33
  "@codemirror/autocomplete": "^6.17.0",
34
  "@codemirror/commands": "^6.6.0",
35
  "@codemirror/lang-cpp": "^6.0.2",
 
71
  "isbot": "^4.1.0",
72
  "istextorbinary": "^9.5.0",
73
  "jose": "^5.6.3",
74
+ "js-cookie": "^3.0.5",
75
  "jszip": "^3.10.1",
76
  "nanostores": "^0.10.3",
77
  "ollama-ai-provider": "^0.15.2",
 
95
  "@remix-run/dev": "^2.10.0",
96
  "@types/diff": "^5.2.1",
97
  "@types/file-saver": "^2.0.7",
98
+ "@types/js-cookie": "^3.0.6",
99
  "@types/react": "^18.2.20",
100
  "@types/react-dom": "^18.2.7",
101
  "fast-glob": "^3.3.2",
pnpm-lock.yaml CHANGED
@@ -146,6 +146,9 @@ importers:
146
  jose:
147
  specifier: ^5.6.3
148
  version: 5.6.3
 
 
 
149
  jszip:
150
  specifier: ^3.10.1
151
  version: 3.10.1
@@ -210,6 +213,9 @@ importers:
210
  '@types/file-saver':
211
  specifier: ^2.0.7
212
  version: 2.0.7
 
 
 
213
  '@types/react':
214
  specifier: ^18.2.20
215
  version: 18.3.3
@@ -1872,6 +1878,9 @@ packages:
1872
  '@types/[email protected]':
1873
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
1874
 
 
 
 
1875
  '@types/[email protected]':
1876
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1877
 
@@ -3455,6 +3464,10 @@ packages:
3455
3456
  resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
3457
 
 
 
 
 
3458
3459
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
3460
 
@@ -7248,6 +7261,8 @@ snapshots:
7248
  dependencies:
7249
  '@types/unist': 3.0.2
7250
 
 
 
7251
  '@types/[email protected]': {}
7252
 
7253
  '@types/[email protected]':
@@ -9211,6 +9226,8 @@ snapshots:
9211
 
9212
9213
 
 
 
9214
9215
 
9216
 
146
  jose:
147
  specifier: ^5.6.3
148
  version: 5.6.3
149
+ js-cookie:
150
+ specifier: ^3.0.5
151
+ version: 3.0.5
152
  jszip:
153
  specifier: ^3.10.1
154
  version: 3.10.1
 
213
  '@types/file-saver':
214
  specifier: ^2.0.7
215
  version: 2.0.7
216
+ '@types/js-cookie':
217
+ specifier: ^3.0.6
218
+ version: 3.0.6
219
  '@types/react':
220
  specifier: ^18.2.20
221
  version: 18.3.3
 
1878
  '@types/[email protected]':
1879
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
1880
 
1881
+ '@types/[email protected]':
1882
+ resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
1883
+
1884
  '@types/[email protected]':
1885
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1886
 
 
3464
3465
  resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
3466
 
3467
3468
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
3469
+ engines: {node: '>=14'}
3470
+
3471
3472
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
3473
 
 
7261
  dependencies:
7262
  '@types/unist': 3.0.2
7263
 
7264
+ '@types/[email protected]': {}
7265
+
7266
  '@types/[email protected]': {}
7267
 
7268
  '@types/[email protected]':
 
9226
 
9227
9228
 
9229
9230
+
9231
9232
 
9233