AliHassan00 commited on
Commit
a544611
·
1 Parent(s): 3c7bf8c

fix: working

Browse files
app/components/chat/BaseChat.tsx CHANGED
@@ -12,6 +12,7 @@ import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { useState } from 'react';
14
  import { APIKeyManager } from './APIKeyManager';
 
15
 
16
  import styles from './BaseChat.module.scss';
17
 
@@ -112,18 +113,36 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
112
  const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
113
 
114
  useEffect(() => {
115
- // Load API keys from localStorage on component mount
116
- const storedApiKeys = localStorage.getItem('apiKeys');
117
- if (storedApiKeys) {
118
- setApiKeys(JSON.parse(storedApiKeys));
 
 
 
 
 
 
 
 
 
119
  }
120
  }, []);
121
 
122
  const updateApiKey = (provider: string, key: string) => {
123
- const updatedApiKeys = { ...apiKeys, [provider]: key };
124
- setApiKeys(updatedApiKeys);
125
- // Save updated API keys to localStorage
126
- localStorage.setItem('apiKeys', JSON.stringify(updatedApiKeys));
 
 
 
 
 
 
 
 
 
127
  };
128
 
129
  return (
 
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
 
 
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 (
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
@@ -80,8 +80,8 @@ export function getOpenRouterModel(apiKey: string, model: string) {
80
  return openRouter.chat(model);
81
  }
82
 
83
- export function getModel(provider: string, model: string, env: Env) {
84
- const apiKey = getAPIKey(env, provider);
85
  const baseURL = getBaseURL(env, provider);
86
 
87
  switch (provider) {
 
80
  return openRouter.chat(model);
81
  }
82
 
83
+ export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
84
+ const apiKey = getAPIKey(env, provider, apiKeys);
85
  const baseURL = getBaseURL(env, provider);
86
 
87
  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
@@ -4,8 +4,8 @@ export const WORK_DIR_NAME = 'project';
4
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
5
  export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
6
  export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
7
- export const DEFAULT_MODEL = 'claude-3-5-sonnet-20240620';
8
- export const DEFAULT_PROVIDER = 'Anthropic';
9
 
10
  const staticModels: ModelInfo[] = [
11
  { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
@@ -13,8 +13,8 @@ const staticModels: ModelInfo[] = [
13
  { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' },
14
  { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
15
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
16
- { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
17
- { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
18
  { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter' },
19
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
20
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
 
4
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
5
  export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
6
  export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
7
+ export const DEFAULT_MODEL = 'google/gemini-flash-1.5-exp';
8
+ export const DEFAULT_PROVIDER = 'OpenRouter';
9
 
10
  const staticModels: ModelInfo[] = [
11
  { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
 
13
  { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' },
14
  { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
15
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
16
+ { name: 'google/gemini-flash-1.5-exp', label: 'Google Gemini Flash 1.5 Exp (OpenRouter)', provider: 'OpenRouter' },
17
+ { name: 'google/gemini-pro-1.5-exp', label: 'Google Gemini Pro 1.5 Exp (OpenRouter)', provider: 'OpenRouter' },
18
  { name: 'mistralai/mistral-nemo', label: 'OpenRouter Mistral Nemo (OpenRouter)', provider: 'OpenRouter' },
19
  { name: 'qwen/qwen-110b-chat', label: 'OpenRouter Qwen 110b Chat (OpenRouter)', provider: 'OpenRouter' },
20
  { name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter' },
package.json CHANGED
@@ -24,8 +24,8 @@
24
  "dependencies": {
25
  "@ai-sdk/anthropic": "^0.0.39",
26
  "@ai-sdk/google": "^0.0.52",
27
- "@ai-sdk/openai": "^0.0.66",
28
  "@ai-sdk/mistral": "^0.0.43",
 
29
  "@codemirror/autocomplete": "^6.17.0",
30
  "@codemirror/commands": "^6.6.0",
31
  "@codemirror/lang-cpp": "^6.0.2",
@@ -67,6 +67,7 @@
67
  "isbot": "^4.1.0",
68
  "istextorbinary": "^9.5.0",
69
  "jose": "^5.6.3",
 
70
  "jszip": "^3.10.1",
71
  "nanostores": "^0.10.3",
72
  "ollama-ai-provider": "^0.15.2",
@@ -90,6 +91,7 @@
90
  "@remix-run/dev": "^2.10.0",
91
  "@types/diff": "^5.2.1",
92
  "@types/file-saver": "^2.0.7",
 
93
  "@types/react": "^18.2.20",
94
  "@types/react-dom": "^18.2.7",
95
  "fast-glob": "^3.3.2",
 
24
  "dependencies": {
25
  "@ai-sdk/anthropic": "^0.0.39",
26
  "@ai-sdk/google": "^0.0.52",
 
27
  "@ai-sdk/mistral": "^0.0.43",
28
+ "@ai-sdk/openai": "^0.0.66",
29
  "@codemirror/autocomplete": "^6.17.0",
30
  "@codemirror/commands": "^6.6.0",
31
  "@codemirror/lang-cpp": "^6.0.2",
 
67
  "isbot": "^4.1.0",
68
  "istextorbinary": "^9.5.0",
69
  "jose": "^5.6.3",
70
+ "js-cookie": "^3.0.5",
71
  "jszip": "^3.10.1",
72
  "nanostores": "^0.10.3",
73
  "ollama-ai-provider": "^0.15.2",
 
91
  "@remix-run/dev": "^2.10.0",
92
  "@types/diff": "^5.2.1",
93
  "@types/file-saver": "^2.0.7",
94
+ "@types/js-cookie": "^3.0.6",
95
  "@types/react": "^18.2.20",
96
  "@types/react-dom": "^18.2.7",
97
  "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