Cole Medin commited on
Commit
8c64144
·
unverified ·
2 Parent(s): 7e18820 5adc0f6

Merge pull request #332 from atrokhym/main

Browse files
app/components/chat/BaseChat.tsx CHANGED
@@ -22,6 +22,8 @@ import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportCh
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
 
 
 
25
  // @ts-ignore TODO: Introduce proper types
26
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
@@ -85,8 +87,11 @@ interface BaseChatProps {
85
  enhancePrompt?: () => void;
86
  importChat?: (description: string, messages: Message[]) => Promise<void>;
87
  exportChat?: () => void;
 
 
 
 
88
  }
89
-
90
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
91
  (
92
  {
@@ -96,20 +101,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
96
  showChat = true,
97
  chatStarted = false,
98
  isStreaming = false,
99
- enhancingPrompt = false,
100
- promptEnhanced = false,
101
- messages,
102
- input = '',
103
  model,
104
  setModel,
105
  provider,
106
  setProvider,
107
- sendMessage,
 
108
  handleInputChange,
 
109
  enhancePrompt,
 
110
  handleStop,
111
  importChat,
112
  exportChat,
 
 
 
 
 
113
  },
114
  ref,
115
  ) => {
@@ -159,6 +168,58 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
159
  }
160
  };
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  const baseChat = (
163
  <div
164
  ref={ref}
@@ -276,7 +337,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
276
  )}
277
  </div>
278
  </div>
279
-
 
 
 
 
 
 
 
280
  <div
281
  className={classNames(
282
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
@@ -284,9 +352,41 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
284
  >
285
  <textarea
286
  ref={textareaRef}
287
- className={
288
- 'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm'
289
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  onKeyDown={(event) => {
291
  if (event.key === 'Enter') {
292
  if (event.shiftKey) {
@@ -302,6 +402,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
302
  onChange={(event) => {
303
  handleInputChange?.(event);
304
  }}
 
305
  style={{
306
  minHeight: TEXTAREA_MIN_HEIGHT,
307
  maxHeight: TEXTAREA_MAX_HEIGHT,
@@ -312,7 +413,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
312
  <ClientOnly>
313
  {() => (
314
  <SendButton
315
- show={input.length > 0 || isStreaming}
316
  isStreaming={isStreaming}
317
  onClick={(event) => {
318
  if (isStreaming) {
@@ -320,21 +421,28 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
320
  return;
321
  }
322
 
323
- sendMessage?.(event);
 
 
324
  }}
325
  />
326
  )}
327
  </ClientOnly>
328
  <div className="flex justify-between items-center text-sm p-4 pt-2">
329
  <div className="flex gap-1 items-center">
 
 
 
330
  <IconButton
331
  title="Enhance prompt"
332
  disabled={input.length === 0 || enhancingPrompt}
333
- className={classNames('transition-all', {
334
- 'opacity-100!': enhancingPrompt,
335
- 'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
336
- promptEnhanced,
337
- })}
 
 
338
  onClick={() => enhancePrompt?.()}
339
  >
340
  {enhancingPrompt ? (
 
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
 
25
+ import FilePreview from './FilePreview';
26
+
27
  // @ts-ignore TODO: Introduce proper types
28
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
  const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
 
87
  enhancePrompt?: () => void;
88
  importChat?: (description: string, messages: Message[]) => Promise<void>;
89
  exportChat?: () => void;
90
+ uploadedFiles?: File[];
91
+ setUploadedFiles?: (files: File[]) => void;
92
+ imageDataList?: string[];
93
+ setImageDataList?: (dataList: string[]) => void;
94
  }
 
95
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
96
  (
97
  {
 
101
  showChat = true,
102
  chatStarted = false,
103
  isStreaming = false,
 
 
 
 
104
  model,
105
  setModel,
106
  provider,
107
  setProvider,
108
+ input = '',
109
+ enhancingPrompt,
110
  handleInputChange,
111
+ promptEnhanced,
112
  enhancePrompt,
113
+ sendMessage,
114
  handleStop,
115
  importChat,
116
  exportChat,
117
+ uploadedFiles = [],
118
+ setUploadedFiles,
119
+ imageDataList = [],
120
+ setImageDataList,
121
+ messages,
122
  },
123
  ref,
124
  ) => {
 
168
  }
169
  };
170
 
171
+ const handleFileUpload = () => {
172
+ const input = document.createElement('input');
173
+ input.type = 'file';
174
+ input.accept = 'image/*';
175
+
176
+ input.onchange = async (e) => {
177
+ const file = (e.target as HTMLInputElement).files?.[0];
178
+
179
+ if (file) {
180
+ const reader = new FileReader();
181
+
182
+ reader.onload = (e) => {
183
+ const base64Image = e.target?.result as string;
184
+ setUploadedFiles?.([...uploadedFiles, file]);
185
+ setImageDataList?.([...imageDataList, base64Image]);
186
+ };
187
+ reader.readAsDataURL(file);
188
+ }
189
+ };
190
+
191
+ input.click();
192
+ };
193
+
194
+ const handlePaste = async (e: React.ClipboardEvent) => {
195
+ const items = e.clipboardData?.items;
196
+
197
+ if (!items) {
198
+ return;
199
+ }
200
+
201
+ for (const item of items) {
202
+ if (item.type.startsWith('image/')) {
203
+ e.preventDefault();
204
+
205
+ const file = item.getAsFile();
206
+
207
+ if (file) {
208
+ const reader = new FileReader();
209
+
210
+ reader.onload = (e) => {
211
+ const base64Image = e.target?.result as string;
212
+ setUploadedFiles?.([...uploadedFiles, file]);
213
+ setImageDataList?.([...imageDataList, base64Image]);
214
+ };
215
+ reader.readAsDataURL(file);
216
+ }
217
+
218
+ break;
219
+ }
220
+ }
221
+ };
222
+
223
  const baseChat = (
224
  <div
225
  ref={ref}
 
337
  )}
338
  </div>
339
  </div>
340
+ <FilePreview
341
+ files={uploadedFiles}
342
+ imageDataList={imageDataList}
343
+ onRemove={(index) => {
344
+ setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
345
+ setImageDataList?.(imageDataList.filter((_, i) => i !== index));
346
+ }}
347
+ />
348
  <div
349
  className={classNames(
350
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
 
352
  >
353
  <textarea
354
  ref={textareaRef}
355
+ className={classNames(
356
+ 'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
357
+ 'transition-all duration-200',
358
+ 'hover:border-bolt-elements-focus',
359
+ )}
360
+ onDragEnter={(e) => {
361
+ e.preventDefault();
362
+ e.currentTarget.style.border = '2px solid #1488fc';
363
+ }}
364
+ onDragOver={(e) => {
365
+ e.preventDefault();
366
+ e.currentTarget.style.border = '2px solid #1488fc';
367
+ }}
368
+ onDragLeave={(e) => {
369
+ e.preventDefault();
370
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
371
+ }}
372
+ onDrop={(e) => {
373
+ e.preventDefault();
374
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
375
+
376
+ const files = Array.from(e.dataTransfer.files);
377
+ files.forEach((file) => {
378
+ if (file.type.startsWith('image/')) {
379
+ const reader = new FileReader();
380
+
381
+ reader.onload = (e) => {
382
+ const base64Image = e.target?.result as string;
383
+ setUploadedFiles?.([...uploadedFiles, file]);
384
+ setImageDataList?.([...imageDataList, base64Image]);
385
+ };
386
+ reader.readAsDataURL(file);
387
+ }
388
+ });
389
+ }}
390
  onKeyDown={(event) => {
391
  if (event.key === 'Enter') {
392
  if (event.shiftKey) {
 
402
  onChange={(event) => {
403
  handleInputChange?.(event);
404
  }}
405
+ onPaste={handlePaste}
406
  style={{
407
  minHeight: TEXTAREA_MIN_HEIGHT,
408
  maxHeight: TEXTAREA_MAX_HEIGHT,
 
413
  <ClientOnly>
414
  {() => (
415
  <SendButton
416
+ show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
417
  isStreaming={isStreaming}
418
  onClick={(event) => {
419
  if (isStreaming) {
 
421
  return;
422
  }
423
 
424
+ if (input.length > 0 || uploadedFiles.length > 0) {
425
+ sendMessage?.(event);
426
+ }
427
  }}
428
  />
429
  )}
430
  </ClientOnly>
431
  <div className="flex justify-between items-center text-sm p-4 pt-2">
432
  <div className="flex gap-1 items-center">
433
+ <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
434
+ <div className="i-ph:paperclip text-xl"></div>
435
+ </IconButton>
436
  <IconButton
437
  title="Enhance prompt"
438
  disabled={input.length === 0 || enhancingPrompt}
439
+ className={classNames(
440
+ 'transition-all',
441
+ enhancingPrompt ? 'opacity-100' : '',
442
+ promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
443
+ promptEnhanced ? 'pr-1.5' : '',
444
+ promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
445
+ )}
446
  onClick={() => enhancePrompt?.()}
447
  >
448
  {enhancingPrompt ? (
app/components/chat/Chat.client.tsx CHANGED
@@ -12,7 +12,6 @@ import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from
12
  import { description, useChatHistory } from '~/lib/persistence';
13
  import { chatStore } from '~/lib/stores/chat';
14
  import { workbenchStore } from '~/lib/stores/workbench';
15
- import { fileModificationsToHTML } from '~/utils/diff';
16
  import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
17
  import { cubicEasingFn } from '~/utils/easings';
18
  import { createScopedLogger, renderLogger } from '~/utils/logger';
@@ -89,8 +88,10 @@ export const ChatImpl = memo(
89
  useShortcuts();
90
 
91
  const textareaRef = useRef<HTMLTextAreaElement>(null);
92
-
93
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
 
 
 
94
  const [model, setModel] = useState(() => {
95
  const savedModel = Cookies.get('selectedModel');
96
  return savedModel || DEFAULT_MODEL;
@@ -206,8 +207,6 @@ export const ChatImpl = memo(
206
  runAnimation();
207
 
208
  if (fileModifications !== undefined) {
209
- const diff = fileModificationsToHTML(fileModifications);
210
-
211
  /**
212
  * If we have file modifications we append a new user message manually since we have to prefix
213
  * the user input with the file modifications and we don't want the new user input to appear
@@ -215,7 +214,19 @@ export const ChatImpl = memo(
215
  * manually reset the input and we'd have to manually pass in file attachments. However, those
216
  * aren't relevant here.
217
  */
218
- append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${diff}\n\n${_input}` });
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  /**
221
  * After sending a new message we reset all modifications since the model
@@ -223,12 +234,28 @@ export const ChatImpl = memo(
223
  */
224
  workbenchStore.resetAllFileModifications();
225
  } else {
226
- append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}` });
 
 
 
 
 
 
 
 
 
 
 
 
227
  }
228
 
229
  setInput('');
230
  Cookies.remove(PROMPT_COOKIE_KEY);
231
 
 
 
 
 
232
  resetEnhancer();
233
 
234
  textareaRef.current?.blur();
@@ -321,6 +348,10 @@ export const ChatImpl = memo(
321
  apiKeys,
322
  );
323
  }}
 
 
 
 
324
  />
325
  );
326
  },
 
12
  import { description, useChatHistory } from '~/lib/persistence';
13
  import { chatStore } from '~/lib/stores/chat';
14
  import { workbenchStore } from '~/lib/stores/workbench';
 
15
  import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
16
  import { cubicEasingFn } from '~/utils/easings';
17
  import { createScopedLogger, renderLogger } from '~/utils/logger';
 
88
  useShortcuts();
89
 
90
  const textareaRef = useRef<HTMLTextAreaElement>(null);
 
91
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
92
+ const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
93
+ const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
94
+
95
  const [model, setModel] = useState(() => {
96
  const savedModel = Cookies.get('selectedModel');
97
  return savedModel || DEFAULT_MODEL;
 
207
  runAnimation();
208
 
209
  if (fileModifications !== undefined) {
 
 
210
  /**
211
  * If we have file modifications we append a new user message manually since we have to prefix
212
  * the user input with the file modifications and we don't want the new user input to appear
 
214
  * manually reset the input and we'd have to manually pass in file attachments. However, those
215
  * aren't relevant here.
216
  */
217
+ append({
218
+ role: 'user',
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
223
+ },
224
+ ...imageDataList.map((imageData) => ({
225
+ type: 'image',
226
+ image: imageData,
227
+ })),
228
+ ] as any, // Type assertion to bypass compiler check
229
+ });
230
 
231
  /**
232
  * After sending a new message we reset all modifications since the model
 
234
  */
235
  workbenchStore.resetAllFileModifications();
236
  } else {
237
+ append({
238
+ role: 'user',
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
243
+ },
244
+ ...imageDataList.map((imageData) => ({
245
+ type: 'image',
246
+ image: imageData,
247
+ })),
248
+ ] as any, // Type assertion to bypass compiler check
249
+ });
250
  }
251
 
252
  setInput('');
253
  Cookies.remove(PROMPT_COOKIE_KEY);
254
 
255
+ // Add file cleanup here
256
+ setUploadedFiles([]);
257
+ setImageDataList([]);
258
+
259
  resetEnhancer();
260
 
261
  textareaRef.current?.blur();
 
348
  apiKeys,
349
  );
350
  }}
351
+ uploadedFiles={uploadedFiles}
352
+ setUploadedFiles={setUploadedFiles}
353
+ imageDataList={imageDataList}
354
+ setImageDataList={setImageDataList}
355
  />
356
  );
357
  },
app/components/chat/FilePreview.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface FilePreviewProps {
4
+ files: File[];
5
+ imageDataList: string[];
6
+ onRemove: (index: number) => void;
7
+ }
8
+
9
+ const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
10
+ if (!files || files.length === 0) {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <div className="flex flex-row overflow-x-auto -mt-2">
16
+ {files.map((file, index) => (
17
+ <div key={file.name + file.size} className="mr-2 relative">
18
+ {imageDataList[index] && (
19
+ <div className="relative pt-4 pr-4">
20
+ <img src={imageDataList[index]} alt={file.name} className="max-h-20" />
21
+ <button
22
+ onClick={() => onRemove(index)}
23
+ className="absolute top-1 right-1 z-10 bg-black rounded-full w-5 h-5 shadow-md hover:bg-gray-900 transition-colors flex items-center justify-center"
24
+ >
25
+ <div className="i-ph:x w-3 h-3 text-gray-200" />
26
+ </button>
27
+ </div>
28
+ )}
29
+ </div>
30
+ ))}
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default FilePreview;
app/components/chat/SendButton.client.tsx CHANGED
@@ -4,11 +4,12 @@ interface SendButtonProps {
4
  show: boolean;
5
  isStreaming?: boolean;
6
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
 
7
  }
8
 
9
  const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
10
 
11
- export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
12
  return (
13
  <AnimatePresence>
14
  {show ? (
@@ -30,4 +31,4 @@ export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
30
  ) : null}
31
  </AnimatePresence>
32
  );
33
- }
 
4
  show: boolean;
5
  isStreaming?: boolean;
6
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
7
+ onImagesSelected?: (images: File[]) => void;
8
  }
9
 
10
  const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
11
 
12
+ export const SendButton = ({ show, isStreaming, onClick }: SendButtonProps) => {
13
  return (
14
  <AnimatePresence>
15
  {show ? (
 
31
  ) : null}
32
  </AnimatePresence>
33
  );
34
+ };
app/components/chat/UserMessage.tsx CHANGED
@@ -2,26 +2,52 @@
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';
8
 
9
  interface UserMessageProps {
10
- content: string;
11
  }
12
 
13
  export function UserMessage({ content }: UserMessageProps) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
  <div className="overflow-hidden pt-[4px]">
16
- <Markdown limitedMarkdown>{sanitizeUserMessage(content)}</Markdown>
17
  </div>
18
  );
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
  }
 
2
  * @ts-nocheck
3
  * Preventing TS checks with files presented in the video for a better presentation.
4
  */
 
5
  import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
6
  import { Markdown } from './Markdown';
7
 
8
  interface UserMessageProps {
9
+ content: string | Array<{ type: string; text?: string; image?: string }>;
10
  }
11
 
12
  export function UserMessage({ content }: UserMessageProps) {
13
+ if (Array.isArray(content)) {
14
+ const textItem = content.find((item) => item.type === 'text');
15
+ const textContent = sanitizeUserMessage(textItem?.text || '');
16
+ const images = content.filter((item) => item.type === 'image' && item.image);
17
+
18
+ return (
19
+ <div className="overflow-hidden pt-[4px]">
20
+ <div className="flex items-start gap-4">
21
+ <div className="flex-1">
22
+ <Markdown limitedMarkdown>{textContent}</Markdown>
23
+ </div>
24
+ {images.length > 0 && (
25
+ <div className="flex-shrink-0 w-[160px]">
26
+ {images.map((item, index) => (
27
+ <div key={index} className="relative">
28
+ <img
29
+ src={item.image}
30
+ alt={`Uploaded image ${index + 1}`}
31
+ className="w-full h-[160px] rounded-lg object-cover border border-bolt-elements-borderColor"
32
+ />
33
+ </div>
34
+ ))}
35
+ </div>
36
+ )}
37
+ </div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ const textContent = sanitizeUserMessage(content);
43
+
44
  return (
45
  <div className="overflow-hidden pt-[4px]">
46
+ <Markdown limitedMarkdown>{textContent}</Markdown>
47
  </div>
48
  );
49
  }
50
 
51
  function sanitizeUserMessage(content: string) {
52
+ return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
 
 
 
 
53
  }
app/components/sidebar/Menu.client.tsx CHANGED
@@ -33,7 +33,7 @@ const menuVariants = {
33
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
- export function Menu() {
37
  const { duplicateCurrentChat, exportChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
@@ -206,4 +206,4 @@ export function Menu() {
206
  </div>
207
  </motion.div>
208
  );
209
- }
 
33
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
+ export const Menu = () => {
37
  const { duplicateCurrentChat, exportChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
 
206
  </div>
207
  </motion.div>
208
  );
209
+ };
app/lib/.server/llm/model.ts CHANGED
@@ -128,7 +128,12 @@ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
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);
133
 
134
  switch (provider) {
 
128
  }
129
 
130
  export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
131
+ /*
132
+ * let apiKey; // Declare first
133
+ * let baseURL;
134
+ */
135
+
136
+ const apiKey = getAPIKey(env, provider, apiKeys); // Then assign
137
  const baseURL = getBaseURL(env, provider);
138
 
139
  switch (provider) {
app/lib/.server/llm/stream-text.ts CHANGED
@@ -26,16 +26,37 @@ export type Messages = Message[];
26
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
27
 
28
  function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
29
- // Extract model
30
- const modelMatch = message.content.match(MODEL_REGEX);
 
 
 
 
 
 
 
 
 
31
  const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
32
 
33
- // Extract provider
34
- const providerMatch = message.content.match(PROVIDER_REGEX);
 
 
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
  }
@@ -65,10 +86,10 @@ export function streamText(messages: Messages, env: Env, options?: StreamingOpti
65
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
66
 
67
  return _streamText({
 
68
  model: getModel(currentProvider, currentModel, env, apiKeys),
69
  system: getSystemPrompt(),
70
  maxTokens: dynamicMaxTokens,
71
  messages: convertToCoreMessages(processedMessages),
72
- ...options,
73
  });
74
  }
 
26
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
27
 
28
  function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
29
+ const textContent = Array.isArray(message.content)
30
+ ? message.content.find((item) => item.type === 'text')?.text || ''
31
+ : message.content;
32
+
33
+ const modelMatch = textContent.match(MODEL_REGEX);
34
+ const providerMatch = textContent.match(PROVIDER_REGEX);
35
+
36
+ /*
37
+ * Extract model
38
+ * const modelMatch = message.content.match(MODEL_REGEX);
39
+ */
40
  const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL;
41
 
42
+ /*
43
+ * Extract provider
44
+ * const providerMatch = message.content.match(PROVIDER_REGEX);
45
+ */
46
  const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER;
47
 
48
+ const cleanedContent = Array.isArray(message.content)
49
+ ? message.content.map((item) => {
50
+ if (item.type === 'text') {
51
+ return {
52
+ type: 'text',
53
+ text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''),
54
+ };
55
+ }
56
+
57
+ return item; // Preserve image_url and other types as is
58
+ })
59
+ : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
60
 
61
  return { model, provider, content: cleanedContent };
62
  }
 
86
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
87
 
88
  return _streamText({
89
+ ...options,
90
  model: getModel(currentProvider, currentModel, env, apiKeys),
91
  system: getSystemPrompt(),
92
  maxTokens: dynamicMaxTokens,
93
  messages: convertToCoreMessages(processedMessages),
 
94
  });
95
  }
app/lib/stores/files.ts CHANGED
@@ -212,9 +212,5 @@ function isBinaryFile(buffer: Uint8Array | undefined) {
212
  * array buffer.
213
  */
214
  function convertToBuffer(view: Uint8Array): Buffer {
215
- const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
216
-
217
- Object.setPrototypeOf(buffer, Buffer.prototype);
218
-
219
- return buffer as Buffer;
220
  }
 
212
  * array buffer.
213
  */
214
  function convertToBuffer(view: Uint8Array): Buffer {
215
+ return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
 
 
 
 
216
  }
app/routes/api.chat.ts CHANGED
@@ -32,8 +32,9 @@ function parseCookies(cookieHeader) {
32
  }
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');
@@ -47,6 +48,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
47
  const options: StreamingOptions = {
48
  toolChoice: 'none',
49
  apiKeys,
 
50
  onFinish: async ({ text: content, finishReason }) => {
51
  if (finishReason !== 'length') {
52
  return stream.close();
 
32
  }
33
 
34
  async function chatAction({ context, request }: ActionFunctionArgs) {
35
+ const { messages, model } = await request.json<{
36
  messages: Messages;
37
+ model: string;
38
  }>();
39
 
40
  const cookieHeader = request.headers.get('Cookie');
 
48
  const options: StreamingOptions = {
49
  toolChoice: 'none',
50
  apiKeys,
51
+ model,
52
  onFinish: async ({ text: content, finishReason }) => {
53
  if (finishReason !== 'length') {
54
  return stream.close();
app/utils/logger.ts CHANGED
@@ -11,7 +11,7 @@ interface Logger {
11
  setLevel: (level: DebugLevel) => void;
12
  }
13
 
14
- let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
15
 
16
  const isWorker = 'HTMLRewriter' in globalThis;
17
  const supportsColor = !isWorker;
 
11
  setLevel: (level: DebugLevel) => void;
12
  }
13
 
14
+ let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
15
 
16
  const isWorker = 'HTMLRewriter' in globalThis;
17
  const supportsColor = !isWorker;
vite.config.ts CHANGED
@@ -19,8 +19,7 @@ export default defineConfig((config) => {
19
  future: {
20
  v3_fetcherPersist: true,
21
  v3_relativeSplatPath: true,
22
- v3_throwAbortReason: true,
23
- v3_lazyRouteDiscovery: true,
24
  },
25
  }),
26
  UnoCSS(),
 
19
  future: {
20
  v3_fetcherPersist: true,
21
  v3_relativeSplatPath: true,
22
+ v3_throwAbortReason: true
 
23
  },
24
  }),
25
  UnoCSS(),