aproli90 commited on
Commit
31ab2d8
·
verified ·
1 Parent(s): 18aed00

Upload 25 files

Browse files
app/components/chat/APIKeyManager.tsx CHANGED
@@ -1,138 +1,112 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { IconButton } from '~/components/ui/IconButton';
3
- import { Switch } from '~/components/ui/Switch';
4
- import type { ProviderInfo } from '~/types/model';
5
- import Cookies from 'js-cookie';
6
-
7
- interface APIKeyManagerProps {
8
- provider: ProviderInfo;
9
- apiKey: string;
10
- setApiKey: (key: string) => void;
11
- getApiKeyLink?: string;
12
- labelForGetApiKey?: string;
13
- }
14
-
15
- const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
16
-
17
- export function getApiKeysFromCookies() {
18
- const storedApiKeys = Cookies.get('apiKeys');
19
- let parsedKeys = {};
20
-
21
- if (storedApiKeys) {
22
- parsedKeys = apiKeyMemoizeCache[storedApiKeys];
23
-
24
- if (!parsedKeys) {
25
- parsedKeys = apiKeyMemoizeCache[storedApiKeys] = JSON.parse(storedApiKeys);
26
- }
27
- }
28
-
29
- return parsedKeys;
30
- }
31
-
32
- export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
33
- provider,
34
- apiKey,
35
- setApiKey
36
- }) => {
37
- // Use localStorage to store and retrieve API key for the specific provider
38
- const [isEditing, setIsEditing] = useState(false);
39
- const [tempKey, setTempKey] = useState('');
40
- const [isPromptCachingEnabled, setIsPromptCachingEnabled] = useState(() => {
41
- const savedState = localStorage.getItem('PROMPT_CACHING_ENABLED');
42
- return savedState !== null ? JSON.parse(savedState) : true;
43
- });
44
-
45
- // Load stored API key for this provider on component mount
46
- useEffect(() => {
47
- const storedKey = localStorage.getItem(`API_KEY_${provider.name}`);
48
- if (storedKey) {
49
- setApiKey(storedKey);
50
- }
51
- }, [provider.name]);
52
-
53
- useEffect(() => {
54
- // Update localStorage whenever the prompt caching state changes
55
- localStorage.setItem('PROMPT_CACHING_ENABLED', JSON.stringify(isPromptCachingEnabled));
56
- }, [isPromptCachingEnabled]);
57
-
58
- const handleSave = () => {
59
- if (!tempKey.trim()) {
60
- // Prevent saving empty API key
61
- alert(`Please enter a valid API key for ${provider.name}`);
62
- return;
63
- }
64
-
65
- // Save to localStorage
66
- localStorage.setItem(`API_KEY_${provider.name}`, tempKey);
67
-
68
- // Update the API key in parent component
69
- setApiKey(tempKey);
70
-
71
- // Exit editing mode
72
- setIsEditing(false);
73
- };
74
-
75
- return (
76
- <div className="space-y-4">
77
- <div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
78
- <div>
79
- <span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
80
- {!isEditing && (
81
- <div className="flex items-center">
82
- <span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
83
- {apiKey ? '••••••••' : 'API Key Required'}
84
- </span>
85
- <IconButton onClick={() => {
86
- setTempKey(apiKey || '');
87
- setIsEditing(true);
88
- }} title="Edit API Key">
89
- <div className="i-ph:pencil-simple" />
90
- </IconButton>
91
- </div>
92
- )}
93
- </div>
94
-
95
- {isEditing ? (
96
- <div className="flex items-center gap-3 mt-2">
97
- <input
98
- type="password"
99
- value={tempKey}
100
- placeholder="Your API Key"
101
- onChange={(e) => setTempKey(e.target.value)}
102
- 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"
103
- />
104
- <IconButton onClick={handleSave} title="Save API Key">
105
- <div className="i-ph:check" />
106
- </IconButton>
107
- <IconButton onClick={() => setIsEditing(false)} title="Cancel">
108
- <div className="i-ph:x" />
109
- </IconButton>
110
- </div>
111
- ) : (
112
- <>
113
- {provider?.getApiKeyLink && (
114
- <IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
115
- <span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
116
- <div className={provider?.icon || 'i-ph:key'} />
117
- </IconButton>
118
- )}
119
- </>
120
- )}
121
- </div>
122
-
123
- {provider?.name === 'Anthropic' && (
124
- <div className="border-t pt-4 pb-4 -mt-4">
125
- <div className="flex items-center space-x-2">
126
- <Switch checked={isPromptCachingEnabled} onCheckedChange={setIsPromptCachingEnabled} />
127
- <label htmlFor="prompt-caching" className="text-sm text-bolt-elements-textSecondary">
128
- Enable Prompt Caching
129
- </label>
130
- </div>
131
- <p className="text-xs text-bolt-elements-textTertiary mt-2">
132
- When enabled, allows caching of prompts for 10x cheaper responses. Recommended for Claude models.
133
- </p>
134
- </div>
135
- )}
136
- </div>
137
- );
138
- };
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+ import { Switch } from '~/components/ui/Switch';
4
+ import type { ProviderInfo } from '~/types/model';
5
+ import Cookies from 'js-cookie';
6
+
7
+ interface APIKeyManagerProps {
8
+ provider: ProviderInfo;
9
+ apiKey: string;
10
+ setApiKey: (key: string) => void;
11
+ getApiKeyLink?: string;
12
+ labelForGetApiKey?: string;
13
+ }
14
+
15
+ const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
16
+
17
+ export function getApiKeysFromCookies() {
18
+ const storedApiKeys = Cookies.get('apiKeys');
19
+ let parsedKeys = {};
20
+
21
+ if (storedApiKeys) {
22
+ parsedKeys = apiKeyMemoizeCache[storedApiKeys];
23
+
24
+ if (!parsedKeys) {
25
+ parsedKeys = apiKeyMemoizeCache[storedApiKeys] = JSON.parse(storedApiKeys);
26
+ }
27
+ }
28
+
29
+ return parsedKeys;
30
+ }
31
+
32
+ // eslint-disable-next-line @typescript-eslint/naming-convention
33
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
34
+ const [isEditing, setIsEditing] = useState(false);
35
+ const [tempKey, setTempKey] = useState(apiKey);
36
+ const [isPromptCachingEnabled, setIsPromptCachingEnabled] = useState(() => {
37
+ // Read initial state from localStorage, defaulting to true
38
+ const savedState = localStorage.getItem('PROMPT_CACHING_ENABLED');
39
+ return savedState !== null ? JSON.parse(savedState) : true;
40
+ });
41
+
42
+ useEffect(() => {
43
+ // Update localStorage whenever the prompt caching state changes
44
+ localStorage.setItem('PROMPT_CACHING_ENABLED', JSON.stringify(isPromptCachingEnabled));
45
+ }, [isPromptCachingEnabled]);
46
+
47
+ const handleSave = () => {
48
+ setApiKey(tempKey);
49
+ setIsEditing(false);
50
+ };
51
+
52
+ return (
53
+ <div className="space-y-4">
54
+ <div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
55
+ <div>
56
+ <span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
57
+ {!isEditing && (
58
+ <div className="flex items-center">
59
+ <span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
60
+ {apiKey ? '••••••••' : 'API Key Required'}
61
+ </span>
62
+ <IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
63
+ <div className="i-ph:pencil-simple" />
64
+ </IconButton>
65
+ </div>
66
+ )}
67
+ </div>
68
+
69
+ {isEditing ? (
70
+ <div className="flex items-center gap-3 mt-2">
71
+ <input
72
+ type="password"
73
+ value={tempKey}
74
+ placeholder="Your API Key"
75
+ onChange={(e) => setTempKey(e.target.value)}
76
+ 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"
77
+ />
78
+ <IconButton onClick={handleSave} title="Save API Key">
79
+ <div className="i-ph:check" />
80
+ </IconButton>
81
+ <IconButton onClick={() => setIsEditing(false)} title="Cancel">
82
+ <div className="i-ph:x" />
83
+ </IconButton>
84
+ </div>
85
+ ) : (
86
+ <>
87
+ {provider?.getApiKeyLink && (
88
+ <IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
89
+ <span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
90
+ <div className={provider?.icon || 'i-ph:key'} />
91
+ </IconButton>
92
+ )}
93
+ </>
94
+ )}
95
+ </div>
96
+
97
+ {provider?.name === 'Anthropic' && (
98
+ <div className="border-t pt-4 pb-4 -mt-4">
99
+ <div className="flex items-center space-x-2">
100
+ <Switch checked={isPromptCachingEnabled} onCheckedChange={setIsPromptCachingEnabled} />
101
+ <label htmlFor="prompt-caching" className="text-sm text-bolt-elements-textSecondary">
102
+ Enable Prompt Caching
103
+ </label>
104
+ </div>
105
+ <p className="text-xs text-bolt-elements-textTertiary mt-2">
106
+ When enabled, allows caching of prompts for 10x cheaper responses. Recommended for Claude models.
107
+ </p>
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/chat/BaseChat.tsx CHANGED
@@ -13,7 +13,6 @@ import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constant
13
  import { Messages } from './Messages.client';
14
  import { SendButton } from './SendButton.client';
15
  import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
16
- import { ApiKeyWarning } from './ApiKeyWarning'
17
  import Cookies from 'js-cookie';
18
  import * as Tooltip from '@radix-ui/react-tooltip';
19
 
@@ -78,8 +77,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
78
  isStreaming = false,
79
  model,
80
  setModel,
81
- apiKeys,
82
- setApiKeys,
83
  provider,
84
  setProvider,
85
  providerList,
@@ -104,6 +101,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
104
  ref,
105
  ) => {
106
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 
107
  const [modelList, setModelList] = useState(MODEL_LIST);
108
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
109
  const [isListening, setIsListening] = useState(false);
@@ -314,18 +312,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
314
  }
315
  };
316
 
317
- const isApiKeyAvailable = apiKeys && apiKeys[provider?.name];
318
-
319
  const baseChat = (
320
  <div
321
  ref={ref}
322
  className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
323
  data-chat-visible={showChat}
324
  >
325
- <ApiKeyWarning
326
- provider={provider}
327
- apiKeys={apiKeys}
328
- />
329
  <ClientOnly>{() => <Menu />}</ClientOnly>
330
  <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
331
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
@@ -536,7 +528,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
536
  <SendButton
537
  show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
538
  isStreaming={isStreaming}
539
- disabled={!isApiKeyAvailable || !providerList || providerList.length === 0}
540
  onClick={(event) => {
541
  if (isStreaming) {
542
  handleStop?.();
 
13
  import { Messages } from './Messages.client';
14
  import { SendButton } from './SendButton.client';
15
  import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
 
16
  import Cookies from 'js-cookie';
17
  import * as Tooltip from '@radix-ui/react-tooltip';
18
 
 
77
  isStreaming = false,
78
  model,
79
  setModel,
 
 
80
  provider,
81
  setProvider,
82
  providerList,
 
101
  ref,
102
  ) => {
103
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
104
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
105
  const [modelList, setModelList] = useState(MODEL_LIST);
106
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
107
  const [isListening, setIsListening] = useState(false);
 
312
  }
313
  };
314
 
 
 
315
  const baseChat = (
316
  <div
317
  ref={ref}
318
  className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
319
  data-chat-visible={showChat}
320
  >
 
 
 
 
321
  <ClientOnly>{() => <Menu />}</ClientOnly>
322
  <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
323
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
 
528
  <SendButton
529
  show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
530
  isStreaming={isStreaming}
531
+ disabled={!providerList || providerList.length === 0}
532
  onClick={(event) => {
533
  if (isStreaming) {
534
  handleStop?.();
app/components/chat/Chat.client.tsx CHANGED
@@ -1,558 +1,556 @@
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';
8
- import { useAnimate } from 'framer-motion';
9
- import { memo, useCallback, useEffect, useRef, useState } from 'react';
10
- import { cssTransition, toast, ToastContainer } from 'react-toastify';
11
- import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
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';
18
- import { BaseChat } from './BaseChat';
19
- import Cookies from 'js-cookie';
20
- import { debounce } from '~/utils/debounce';
21
- import { useSettings } from '~/lib/hooks/useSettings';
22
- import type { ProviderInfo } from '~/types/model';
23
- import { useSearchParams } from '@remix-run/react';
24
- import { createSampler } from '~/utils/sampler';
25
- import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
26
-
27
- const toastAnimation = cssTransition({
28
- enter: 'animated fadeInRight',
29
- exit: 'animated fadeOutRight',
30
- });
31
-
32
- const logger = createScopedLogger('Chat');
33
-
34
- export function Chat() {
35
- renderLogger.trace('Chat');
36
-
37
- const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
38
- const title = useStore(description);
39
- useEffect(() => {
40
- workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
41
- }, [initialMessages]);
42
-
43
- return (
44
- <>
45
- {ready && (
46
- <ChatImpl
47
- description={title}
48
- initialMessages={initialMessages}
49
- exportChat={exportChat}
50
- storeMessageHistory={storeMessageHistory}
51
- importChat={importChat}
52
- />
53
- )}
54
- <ToastContainer
55
- closeButton={({ closeToast }) => {
56
- return (
57
- <button className="Toastify__close-button" onClick={closeToast}>
58
- <div className="i-ph:x text-lg" />
59
- </button>
60
- );
61
- }}
62
- icon={({ type }) => {
63
- /**
64
- * @todo Handle more types if we need them. This may require extra color palettes.
65
- */
66
- switch (type) {
67
- case 'success': {
68
- return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
69
- }
70
- case 'error': {
71
- return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
72
- }
73
- }
74
-
75
- return undefined;
76
- }}
77
- position="bottom-right"
78
- pauseOnFocusLoss
79
- transition={toastAnimation}
80
- />
81
- </>
82
- );
83
- }
84
-
85
- const processSampledMessages = createSampler(
86
- (options: {
87
- messages: Message[];
88
- initialMessages: Message[];
89
- isLoading: boolean;
90
- parseMessages: (messages: Message[], isLoading: boolean) => void;
91
- storeMessageHistory: (messages: Message[]) => Promise<void>;
92
- }) => {
93
- const { messages, initialMessages, isLoading, parseMessages, storeMessageHistory } = options;
94
- parseMessages(messages, isLoading);
95
-
96
- if (messages.length > initialMessages.length) {
97
- storeMessageHistory(messages).catch((error) => toast.error(error.message));
98
- }
99
- },
100
- 50,
101
- );
102
-
103
- interface ChatProps {
104
- initialMessages: Message[];
105
- storeMessageHistory: (messages: Message[]) => Promise<void>;
106
- importChat: (description: string, messages: Message[]) => Promise<void>;
107
- exportChat: () => void;
108
- description?: string;
109
- }
110
-
111
- export const ChatImpl = memo(
112
- ({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
113
-
114
- useShortcuts();
115
- const textareaRef = useRef<HTMLTextAreaElement>(null);
116
- const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
117
- const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
118
- const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
119
- const [searchParams, setSearchParams] = useSearchParams();
120
- const [fakeLoading, setFakeLoading] = useState(false);
121
- const files = useStore(workbenchStore.files);
122
- const actionAlert = useStore(workbenchStore.alert);
123
- const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
124
-
125
- function isPromptCachingEnabled(): boolean {
126
- // Server-side default
127
- if (typeof window === 'undefined') {
128
- console.log('Server-side: isPromptCachingEnabled: window undefined');
129
- return false;
130
- }
131
-
132
- try {
133
- // Read from localStorage in browser
134
- const savedState = localStorage.getItem('PROMPT_CACHING_ENABLED');
135
- console.log('Saved prompt caching state:', savedState);
136
-
137
- return savedState !== null ? JSON.parse(savedState) : false;
138
- } catch (error) {
139
- console.error('Error reading prompt caching setting:', error);
140
- return false; // Default to true if reading fails
141
- }
142
- }
143
-
144
- const [model, setModel] = useState(() => {
145
- const savedModel = Cookies.get('selectedModel');
146
- return savedModel || DEFAULT_MODEL;
147
- });
148
- const [provider, setProvider] = useState(() => {
149
- const savedProvider = Cookies.get('selectedProvider');
150
- return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo;
151
- });
152
-
153
- const { showChat } = useStore(chatStore);
154
-
155
- const [animationScope, animate] = useAnimate();
156
-
157
- const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
158
-
159
- const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
160
- api: '/api/chat',
161
- body: {
162
- apiKeys,
163
- files,
164
- promptId,
165
- contextOptimization: contextOptimizationEnabled,
166
- isPromptCachingEnabled: provider.name === 'Anthropic' && isPromptCachingEnabled(),
167
- },
168
- sendExtraMessageFields: true,
169
- onError: (error) => {
170
- logger.error('Request failed\n\n', error);
171
- toast.error(
172
- 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
173
- );
174
- },
175
- onFinish: (message, response) => {
176
- const usage = response.usage;
177
-
178
- if (usage) {
179
- console.log('Token usage:', usage);
180
-
181
- // You can now use the usage data as needed
182
- }
183
-
184
- logger.debug('Finished streaming');
185
- },
186
- initialMessages,
187
- initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
188
- });
189
- useEffect(() => {
190
- const prompt = searchParams.get('prompt');
191
-
192
- // console.log(prompt, searchParams, model, provider);
193
-
194
- if (prompt) {
195
- setSearchParams({});
196
- runAnimation();
197
- append({
198
- role: 'user',
199
- content: [
200
- {
201
- type: 'text',
202
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`,
203
- },
204
- ] as any, // Type assertion to bypass compiler check
205
- });
206
- }
207
- }, [model, provider, searchParams]);
208
-
209
- const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
210
- const { parsedMessages, parseMessages } = useMessageParser();
211
-
212
- const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
213
-
214
- useEffect(() => {
215
- chatStore.setKey('started', initialMessages.length > 0);
216
- }, []);
217
-
218
- useEffect(() => {
219
- processSampledMessages({
220
- messages,
221
- initialMessages,
222
- isLoading,
223
- parseMessages,
224
- storeMessageHistory,
225
- });
226
- }, [messages, isLoading, parseMessages]);
227
-
228
- const scrollTextArea = () => {
229
- const textarea = textareaRef.current;
230
-
231
- if (textarea) {
232
- textarea.scrollTop = textarea.scrollHeight;
233
- }
234
- };
235
-
236
- const abort = () => {
237
- stop();
238
- chatStore.setKey('aborted', true);
239
- workbenchStore.abortAllActions();
240
- };
241
-
242
- useEffect(() => {
243
- const textarea = textareaRef.current;
244
-
245
- if (textarea) {
246
- textarea.style.height = 'auto';
247
-
248
- const scrollHeight = textarea.scrollHeight;
249
-
250
- textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
251
- textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
252
- }
253
- }, [input, textareaRef]);
254
-
255
- const runAnimation = async () => {
256
- if (chatStarted) {
257
- return;
258
- }
259
-
260
- await Promise.all([
261
- animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
262
- animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
263
- ]);
264
-
265
- chatStore.setKey('started', true);
266
-
267
- setChatStarted(true);
268
- };
269
-
270
- const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
271
- const _input = messageInput || input;
272
-
273
- if (_input.length === 0 || isLoading) {
274
- return;
275
- }
276
-
277
- /**
278
- * @note (delm) Usually saving files shouldn't take long but it may take longer if there
279
- * many unsaved files. In that case we need to block user input and show an indicator
280
- * of some kind so the user is aware that something is happening. But I consider the
281
- * happy case to be no unsaved files and I would expect users to save their changes
282
- * before they send another message.
283
- */
284
- await workbenchStore.saveAllFiles();
285
-
286
- const fileModifications = workbenchStore.getFileModifcations();
287
-
288
- chatStore.setKey('aborted', false);
289
-
290
- runAnimation();
291
-
292
- if (!chatStarted && messageInput && autoSelectTemplate) {
293
- setFakeLoading(true);
294
- setMessages([
295
- {
296
- id: `${new Date().getTime()}`,
297
- role: 'user',
298
- content: [
299
- {
300
- type: 'text',
301
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
302
- },
303
- ...imageDataList.map((imageData) => ({
304
- type: 'image',
305
- image: imageData,
306
- })),
307
- ] as any, // Type assertion to bypass compiler check
308
- },
309
- ]);
310
-
311
- // reload();
312
-
313
- const { template, title } = await selectStarterTemplate({
314
- message: messageInput,
315
- model,
316
- provider,
317
- });
318
-
319
- if (template !== 'blank') {
320
- const temResp = await getTemplates(template, title).catch((e) => {
321
- if (e.message.includes('rate limit')) {
322
- toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
323
- } else {
324
- toast.warning('Failed to import starter template\n Continuing with blank template');
325
- }
326
-
327
- return null;
328
- });
329
-
330
- if (temResp) {
331
- const { assistantMessage, userMessage } = temResp;
332
-
333
- setMessages([
334
- {
335
- id: `${new Date().getTime()}`,
336
- role: 'user',
337
- content: messageInput,
338
-
339
- // annotations: ['hidden'],
340
- },
341
- {
342
- id: `${new Date().getTime()}`,
343
- role: 'assistant',
344
- content: assistantMessage,
345
- },
346
- {
347
- id: `${new Date().getTime()}`,
348
- role: 'user',
349
- content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
350
- annotations: ['hidden'],
351
- },
352
- ]);
353
-
354
- reload();
355
- setFakeLoading(false);
356
-
357
- return;
358
- } else {
359
- setMessages([
360
- {
361
- id: `${new Date().getTime()}`,
362
- role: 'user',
363
- content: [
364
- {
365
- type: 'text',
366
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
367
- },
368
- ...imageDataList.map((imageData) => ({
369
- type: 'image',
370
- image: imageData,
371
- })),
372
- ] as any, // Type assertion to bypass compiler check
373
- },
374
- ]);
375
- reload();
376
- setFakeLoading(false);
377
-
378
- return;
379
- }
380
- } else {
381
- setMessages([
382
- {
383
- id: `${new Date().getTime()}`,
384
- role: 'user',
385
- content: [
386
- {
387
- type: 'text',
388
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
389
- },
390
- ...imageDataList.map((imageData) => ({
391
- type: 'image',
392
- image: imageData,
393
- })),
394
- ] as any, // Type assertion to bypass compiler check
395
- },
396
- ]);
397
- reload();
398
- setFakeLoading(false);
399
-
400
- return;
401
- }
402
- }
403
-
404
- if (fileModifications !== undefined) {
405
- /**
406
- * If we have file modifications we append a new user message manually since we have to prefix
407
- * the user input with the file modifications and we don't want the new user input to appear
408
- * in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
409
- * manually reset the input and we'd have to manually pass in file attachments. However, those
410
- * aren't relevant here.
411
- */
412
- append({
413
- role: 'user',
414
- content: [
415
- {
416
- type: 'text',
417
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
418
- },
419
- ...imageDataList.map((imageData) => ({
420
- type: 'image',
421
- image: imageData,
422
- })),
423
- ] as any, // Type assertion to bypass compiler check
424
- });
425
-
426
- /**
427
- * After sending a new message we reset all modifications since the model
428
- * should now be aware of all the changes.
429
- */
430
- workbenchStore.resetAllFileModifications();
431
- } else {
432
- append({
433
- role: 'user',
434
- content: [
435
- {
436
- type: 'text',
437
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
438
- },
439
- ...imageDataList.map((imageData) => ({
440
- type: 'image',
441
- image: imageData,
442
- })),
443
- ] as any, // Type assertion to bypass compiler check
444
- });
445
- }
446
-
447
- setInput('');
448
- Cookies.remove(PROMPT_COOKIE_KEY);
449
-
450
- // Add file cleanup here
451
- setUploadedFiles([]);
452
- setImageDataList([]);
453
-
454
- resetEnhancer();
455
-
456
- textareaRef.current?.blur();
457
- };
458
-
459
- /**
460
- * Handles the change event for the textarea and updates the input state.
461
- * @param event - The change event from the textarea.
462
- */
463
- const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
464
- handleInputChange(event);
465
- };
466
-
467
- /**
468
- * Debounced function to cache the prompt in cookies.
469
- * Caches the trimmed value of the textarea input after a delay to optimize performance.
470
- */
471
- const debouncedCachePrompt = useCallback(
472
- debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
473
- const trimmedValue = event.target.value.trim();
474
- Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
475
- }, 1000),
476
- [],
477
- );
478
-
479
- const [messageRef, scrollRef] = useSnapScroll();
480
-
481
- useEffect(() => {
482
- const storedApiKeys = Cookies.get('apiKeys');
483
-
484
- if (storedApiKeys) {
485
- setApiKeys(JSON.parse(storedApiKeys));
486
- }
487
- }, []);
488
-
489
- const handleModelChange = (newModel: string) => {
490
- setModel(newModel);
491
- Cookies.set('selectedModel', newModel, { expires: 30 });
492
- };
493
-
494
- const handleProviderChange = (newProvider: ProviderInfo) => {
495
- setProvider(newProvider);
496
- Cookies.set('selectedProvider', newProvider.name, { expires: 30 });
497
- };
498
-
499
- return (
500
- <BaseChat
501
- ref={animationScope}
502
- textareaRef={textareaRef}
503
- input={input}
504
- showChat={showChat}
505
- chatStarted={chatStarted}
506
- isStreaming={isLoading || fakeLoading}
507
- enhancingPrompt={enhancingPrompt}
508
- promptEnhanced={promptEnhanced}
509
- sendMessage={sendMessage}
510
- model={model}
511
- setModel={handleModelChange}
512
- provider={provider}
513
- setProvider={handleProviderChange}
514
- apiKeys={apiKeys}
515
- setApiKeys={setApiKeys}
516
- providerList={activeProviders}
517
- messageRef={messageRef}
518
- scrollRef={scrollRef}
519
- handleInputChange={(e) => {
520
- onTextareaChange(e);
521
- debouncedCachePrompt(e);
522
- }}
523
- handleStop={abort}
524
- description={description}
525
- importChat={importChat}
526
- exportChat={exportChat}
527
- messages={messages.map((message, i) => {
528
- if (message.role === 'user') {
529
- return message;
530
- }
531
-
532
- return {
533
- ...message,
534
- content: parsedMessages[i] || '',
535
- };
536
- })}
537
- enhancePrompt={() => {
538
- enhancePrompt(
539
- input,
540
- (input) => {
541
- setInput(input);
542
- scrollTextArea();
543
- },
544
- model,
545
- provider,
546
- apiKeys,
547
- );
548
- }}
549
- uploadedFiles={uploadedFiles}
550
- setUploadedFiles={setUploadedFiles}
551
- imageDataList={imageDataList}
552
- setImageDataList={setImageDataList}
553
- actionAlert={actionAlert}
554
- clearAlert={() => workbenchStore.clearAlert()}
555
- />
556
- );
557
- },
558
- );
 
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';
8
+ import { useAnimate } from 'framer-motion';
9
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
10
+ import { cssTransition, toast, ToastContainer } from 'react-toastify';
11
+ import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
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';
18
+ import { BaseChat } from './BaseChat';
19
+ import Cookies from 'js-cookie';
20
+ import { debounce } from '~/utils/debounce';
21
+ import { useSettings } from '~/lib/hooks/useSettings';
22
+ import type { ProviderInfo } from '~/types/model';
23
+ import { useSearchParams } from '@remix-run/react';
24
+ import { createSampler } from '~/utils/sampler';
25
+ import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
26
+
27
+ const toastAnimation = cssTransition({
28
+ enter: 'animated fadeInRight',
29
+ exit: 'animated fadeOutRight',
30
+ });
31
+
32
+ const logger = createScopedLogger('Chat');
33
+
34
+ export function Chat() {
35
+ renderLogger.trace('Chat');
36
+
37
+ const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
38
+ const title = useStore(description);
39
+ useEffect(() => {
40
+ workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
41
+ }, [initialMessages]);
42
+
43
+ return (
44
+ <>
45
+ {ready && (
46
+ <ChatImpl
47
+ description={title}
48
+ initialMessages={initialMessages}
49
+ exportChat={exportChat}
50
+ storeMessageHistory={storeMessageHistory}
51
+ importChat={importChat}
52
+ />
53
+ )}
54
+ <ToastContainer
55
+ closeButton={({ closeToast }) => {
56
+ return (
57
+ <button className="Toastify__close-button" onClick={closeToast}>
58
+ <div className="i-ph:x text-lg" />
59
+ </button>
60
+ );
61
+ }}
62
+ icon={({ type }) => {
63
+ /**
64
+ * @todo Handle more types if we need them. This may require extra color palettes.
65
+ */
66
+ switch (type) {
67
+ case 'success': {
68
+ return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
69
+ }
70
+ case 'error': {
71
+ return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
72
+ }
73
+ }
74
+
75
+ return undefined;
76
+ }}
77
+ position="bottom-right"
78
+ pauseOnFocusLoss
79
+ transition={toastAnimation}
80
+ />
81
+ </>
82
+ );
83
+ }
84
+
85
+ const processSampledMessages = createSampler(
86
+ (options: {
87
+ messages: Message[];
88
+ initialMessages: Message[];
89
+ isLoading: boolean;
90
+ parseMessages: (messages: Message[], isLoading: boolean) => void;
91
+ storeMessageHistory: (messages: Message[]) => Promise<void>;
92
+ }) => {
93
+ const { messages, initialMessages, isLoading, parseMessages, storeMessageHistory } = options;
94
+ parseMessages(messages, isLoading);
95
+
96
+ if (messages.length > initialMessages.length) {
97
+ storeMessageHistory(messages).catch((error) => toast.error(error.message));
98
+ }
99
+ },
100
+ 50,
101
+ );
102
+
103
+ interface ChatProps {
104
+ initialMessages: Message[];
105
+ storeMessageHistory: (messages: Message[]) => Promise<void>;
106
+ importChat: (description: string, messages: Message[]) => Promise<void>;
107
+ exportChat: () => void;
108
+ description?: string;
109
+ }
110
+
111
+ export const ChatImpl = memo(
112
+ ({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
113
+ useShortcuts();
114
+
115
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
116
+ const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
117
+ const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
118
+ const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
119
+ const [searchParams, setSearchParams] = useSearchParams();
120
+ const [fakeLoading, setFakeLoading] = useState(false);
121
+ const files = useStore(workbenchStore.files);
122
+ const actionAlert = useStore(workbenchStore.alert);
123
+ const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
124
+
125
+ function isPromptCachingEnabled(): boolean {
126
+ // Server-side default
127
+ if (typeof window === 'undefined') {
128
+ console.log('Server-side: isPromptCachingEnabled: window undefined');
129
+ return false;
130
+ }
131
+
132
+ try {
133
+ // Read from localStorage in browser
134
+ const savedState = localStorage.getItem('PROMPT_CACHING_ENABLED');
135
+ console.log('Saved prompt caching state:', savedState);
136
+
137
+ return savedState !== null ? JSON.parse(savedState) : false;
138
+ } catch (error) {
139
+ console.error('Error reading prompt caching setting:', error);
140
+ return false; // Default to true if reading fails
141
+ }
142
+ }
143
+
144
+ const [model, setModel] = useState(() => {
145
+ const savedModel = Cookies.get('selectedModel');
146
+ return savedModel || DEFAULT_MODEL;
147
+ });
148
+ const [provider, setProvider] = useState(() => {
149
+ const savedProvider = Cookies.get('selectedProvider');
150
+ return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo;
151
+ });
152
+
153
+ const { showChat } = useStore(chatStore);
154
+
155
+ const [animationScope, animate] = useAnimate();
156
+
157
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
158
+
159
+ const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
160
+ api: '/api/chat',
161
+ body: {
162
+ apiKeys,
163
+ files,
164
+ promptId,
165
+ contextOptimization: contextOptimizationEnabled,
166
+ isPromptCachingEnabled: provider.name === 'Anthropic' && isPromptCachingEnabled(),
167
+ },
168
+ sendExtraMessageFields: true,
169
+ onError: (error) => {
170
+ logger.error('Request failed\n\n', error);
171
+ toast.error(
172
+ 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
173
+ );
174
+ },
175
+ onFinish: (message, response) => {
176
+ const usage = response.usage;
177
+
178
+ if (usage) {
179
+ console.log('Token usage:', usage);
180
+
181
+ // You can now use the usage data as needed
182
+ }
183
+
184
+ logger.debug('Finished streaming');
185
+ },
186
+ initialMessages,
187
+ initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
188
+ });
189
+ useEffect(() => {
190
+ const prompt = searchParams.get('prompt');
191
+
192
+ // console.log(prompt, searchParams, model, provider);
193
+
194
+ if (prompt) {
195
+ setSearchParams({});
196
+ runAnimation();
197
+ append({
198
+ role: 'user',
199
+ content: [
200
+ {
201
+ type: 'text',
202
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`,
203
+ },
204
+ ] as any, // Type assertion to bypass compiler check
205
+ });
206
+ }
207
+ }, [model, provider, searchParams]);
208
+
209
+ const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
210
+ const { parsedMessages, parseMessages } = useMessageParser();
211
+
212
+ const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
213
+
214
+ useEffect(() => {
215
+ chatStore.setKey('started', initialMessages.length > 0);
216
+ }, []);
217
+
218
+ useEffect(() => {
219
+ processSampledMessages({
220
+ messages,
221
+ initialMessages,
222
+ isLoading,
223
+ parseMessages,
224
+ storeMessageHistory,
225
+ });
226
+ }, [messages, isLoading, parseMessages]);
227
+
228
+ const scrollTextArea = () => {
229
+ const textarea = textareaRef.current;
230
+
231
+ if (textarea) {
232
+ textarea.scrollTop = textarea.scrollHeight;
233
+ }
234
+ };
235
+
236
+ const abort = () => {
237
+ stop();
238
+ chatStore.setKey('aborted', true);
239
+ workbenchStore.abortAllActions();
240
+ };
241
+
242
+ useEffect(() => {
243
+ const textarea = textareaRef.current;
244
+
245
+ if (textarea) {
246
+ textarea.style.height = 'auto';
247
+
248
+ const scrollHeight = textarea.scrollHeight;
249
+
250
+ textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
251
+ textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
252
+ }
253
+ }, [input, textareaRef]);
254
+
255
+ const runAnimation = async () => {
256
+ if (chatStarted) {
257
+ return;
258
+ }
259
+
260
+ await Promise.all([
261
+ animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
262
+ animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
263
+ ]);
264
+
265
+ chatStore.setKey('started', true);
266
+
267
+ setChatStarted(true);
268
+ };
269
+
270
+ const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
271
+ const _input = messageInput || input;
272
+
273
+ if (_input.length === 0 || isLoading) {
274
+ return;
275
+ }
276
+
277
+ /**
278
+ * @note (delm) Usually saving files shouldn't take long but it may take longer if there
279
+ * many unsaved files. In that case we need to block user input and show an indicator
280
+ * of some kind so the user is aware that something is happening. But I consider the
281
+ * happy case to be no unsaved files and I would expect users to save their changes
282
+ * before they send another message.
283
+ */
284
+ await workbenchStore.saveAllFiles();
285
+
286
+ const fileModifications = workbenchStore.getFileModifcations();
287
+
288
+ chatStore.setKey('aborted', false);
289
+
290
+ runAnimation();
291
+
292
+ if (!chatStarted && messageInput && autoSelectTemplate) {
293
+ setFakeLoading(true);
294
+ setMessages([
295
+ {
296
+ id: `${new Date().getTime()}`,
297
+ role: 'user',
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
302
+ },
303
+ ...imageDataList.map((imageData) => ({
304
+ type: 'image',
305
+ image: imageData,
306
+ })),
307
+ ] as any, // Type assertion to bypass compiler check
308
+ },
309
+ ]);
310
+
311
+ // reload();
312
+
313
+ const { template, title } = await selectStarterTemplate({
314
+ message: messageInput,
315
+ model,
316
+ provider,
317
+ });
318
+
319
+ if (template !== 'blank') {
320
+ const temResp = await getTemplates(template, title).catch((e) => {
321
+ if (e.message.includes('rate limit')) {
322
+ toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
323
+ } else {
324
+ toast.warning('Failed to import starter template\n Continuing with blank template');
325
+ }
326
+
327
+ return null;
328
+ });
329
+
330
+ if (temResp) {
331
+ const { assistantMessage, userMessage } = temResp;
332
+
333
+ setMessages([
334
+ {
335
+ id: `${new Date().getTime()}`,
336
+ role: 'user',
337
+ content: messageInput,
338
+
339
+ // annotations: ['hidden'],
340
+ },
341
+ {
342
+ id: `${new Date().getTime()}`,
343
+ role: 'assistant',
344
+ content: assistantMessage,
345
+ },
346
+ {
347
+ id: `${new Date().getTime()}`,
348
+ role: 'user',
349
+ content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
350
+ annotations: ['hidden'],
351
+ },
352
+ ]);
353
+
354
+ reload();
355
+ setFakeLoading(false);
356
+
357
+ return;
358
+ } else {
359
+ setMessages([
360
+ {
361
+ id: `${new Date().getTime()}`,
362
+ role: 'user',
363
+ content: [
364
+ {
365
+ type: 'text',
366
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
367
+ },
368
+ ...imageDataList.map((imageData) => ({
369
+ type: 'image',
370
+ image: imageData,
371
+ })),
372
+ ] as any, // Type assertion to bypass compiler check
373
+ },
374
+ ]);
375
+ reload();
376
+ setFakeLoading(false);
377
+
378
+ return;
379
+ }
380
+ } else {
381
+ setMessages([
382
+ {
383
+ id: `${new Date().getTime()}`,
384
+ role: 'user',
385
+ content: [
386
+ {
387
+ type: 'text',
388
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
389
+ },
390
+ ...imageDataList.map((imageData) => ({
391
+ type: 'image',
392
+ image: imageData,
393
+ })),
394
+ ] as any, // Type assertion to bypass compiler check
395
+ },
396
+ ]);
397
+ reload();
398
+ setFakeLoading(false);
399
+
400
+ return;
401
+ }
402
+ }
403
+
404
+ if (fileModifications !== undefined) {
405
+ /**
406
+ * If we have file modifications we append a new user message manually since we have to prefix
407
+ * the user input with the file modifications and we don't want the new user input to appear
408
+ * in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
409
+ * manually reset the input and we'd have to manually pass in file attachments. However, those
410
+ * aren't relevant here.
411
+ */
412
+ append({
413
+ role: 'user',
414
+ content: [
415
+ {
416
+ type: 'text',
417
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
418
+ },
419
+ ...imageDataList.map((imageData) => ({
420
+ type: 'image',
421
+ image: imageData,
422
+ })),
423
+ ] as any, // Type assertion to bypass compiler check
424
+ });
425
+
426
+ /**
427
+ * After sending a new message we reset all modifications since the model
428
+ * should now be aware of all the changes.
429
+ */
430
+ workbenchStore.resetAllFileModifications();
431
+ } else {
432
+ append({
433
+ role: 'user',
434
+ content: [
435
+ {
436
+ type: 'text',
437
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
438
+ },
439
+ ...imageDataList.map((imageData) => ({
440
+ type: 'image',
441
+ image: imageData,
442
+ })),
443
+ ] as any, // Type assertion to bypass compiler check
444
+ });
445
+ }
446
+
447
+ setInput('');
448
+ Cookies.remove(PROMPT_COOKIE_KEY);
449
+
450
+ // Add file cleanup here
451
+ setUploadedFiles([]);
452
+ setImageDataList([]);
453
+
454
+ resetEnhancer();
455
+
456
+ textareaRef.current?.blur();
457
+ };
458
+
459
+ /**
460
+ * Handles the change event for the textarea and updates the input state.
461
+ * @param event - The change event from the textarea.
462
+ */
463
+ const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
464
+ handleInputChange(event);
465
+ };
466
+
467
+ /**
468
+ * Debounced function to cache the prompt in cookies.
469
+ * Caches the trimmed value of the textarea input after a delay to optimize performance.
470
+ */
471
+ const debouncedCachePrompt = useCallback(
472
+ debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
473
+ const trimmedValue = event.target.value.trim();
474
+ Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
475
+ }, 1000),
476
+ [],
477
+ );
478
+
479
+ const [messageRef, scrollRef] = useSnapScroll();
480
+
481
+ useEffect(() => {
482
+ const storedApiKeys = Cookies.get('apiKeys');
483
+
484
+ if (storedApiKeys) {
485
+ setApiKeys(JSON.parse(storedApiKeys));
486
+ }
487
+ }, []);
488
+
489
+ const handleModelChange = (newModel: string) => {
490
+ setModel(newModel);
491
+ Cookies.set('selectedModel', newModel, { expires: 30 });
492
+ };
493
+
494
+ const handleProviderChange = (newProvider: ProviderInfo) => {
495
+ setProvider(newProvider);
496
+ Cookies.set('selectedProvider', newProvider.name, { expires: 30 });
497
+ };
498
+
499
+ return (
500
+ <BaseChat
501
+ ref={animationScope}
502
+ textareaRef={textareaRef}
503
+ input={input}
504
+ showChat={showChat}
505
+ chatStarted={chatStarted}
506
+ isStreaming={isLoading || fakeLoading}
507
+ enhancingPrompt={enhancingPrompt}
508
+ promptEnhanced={promptEnhanced}
509
+ sendMessage={sendMessage}
510
+ model={model}
511
+ setModel={handleModelChange}
512
+ provider={provider}
513
+ setProvider={handleProviderChange}
514
+ providerList={activeProviders}
515
+ messageRef={messageRef}
516
+ scrollRef={scrollRef}
517
+ handleInputChange={(e) => {
518
+ onTextareaChange(e);
519
+ debouncedCachePrompt(e);
520
+ }}
521
+ handleStop={abort}
522
+ description={description}
523
+ importChat={importChat}
524
+ exportChat={exportChat}
525
+ messages={messages.map((message, i) => {
526
+ if (message.role === 'user') {
527
+ return message;
528
+ }
529
+
530
+ return {
531
+ ...message,
532
+ content: parsedMessages[i] || '',
533
+ };
534
+ })}
535
+ enhancePrompt={() => {
536
+ enhancePrompt(
537
+ input,
538
+ (input) => {
539
+ setInput(input);
540
+ scrollTextArea();
541
+ },
542
+ model,
543
+ provider,
544
+ apiKeys,
545
+ );
546
+ }}
547
+ uploadedFiles={uploadedFiles}
548
+ setUploadedFiles={setUploadedFiles}
549
+ imageDataList={imageDataList}
550
+ setImageDataList={setImageDataList}
551
+ actionAlert={actionAlert}
552
+ clearAlert={() => workbenchStore.clearAlert()}
553
+ />
554
+ );
555
+ },
556
+ );
 
 
app/components/chat/GitCloneButton.tsx CHANGED
@@ -1,125 +1,125 @@
1
- import ignore from 'ignore';
2
- import { useGit } from '~/lib/hooks/useGit';
3
- import type { Message } from 'ai';
4
- import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
5
- import { generateId } from '~/utils/fileUtils';
6
- import { useState } from 'react';
7
- import { toast } from 'react-toastify';
8
- import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
-
10
- const IGNORE_PATTERNS = [
11
- 'node_modules/**',
12
- '.git/**',
13
- '.github/**',
14
- '.vscode/**',
15
- '**/*.jpg',
16
- '**/*.jpeg',
17
- '**/*.png',
18
- 'dist/**',
19
- 'build/**',
20
- '.next/**',
21
- 'coverage/**',
22
- '.cache/**',
23
- '.vscode/**',
24
- '.idea/**',
25
- '**/*.log',
26
- '**/.DS_Store',
27
- '**/npm-debug.log*',
28
- '**/yarn-debug.log*',
29
- '**/yarn-error.log*',
30
- '**/*lock.json',
31
- '**/*lock.yaml',
32
- ];
33
-
34
- const ig = ignore().add(IGNORE_PATTERNS);
35
-
36
- interface GitCloneButtonProps {
37
- className?: string;
38
- importChat?: (description: string, messages: Message[]) => Promise<void>;
39
- }
40
-
41
- export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
42
- const { ready, gitClone } = useGit();
43
- const [loading, setLoading] = useState(false);
44
-
45
- const onClick = async (_e: any) => {
46
- if (!ready) {
47
- return;
48
- }
49
-
50
- const repoUrl = prompt('Enter the Git url');
51
-
52
- if (repoUrl) {
53
- setLoading(true);
54
-
55
- try {
56
- const { workdir, data } = await gitClone(repoUrl);
57
-
58
- if (importChat) {
59
- const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
60
- console.log(filePaths);
61
-
62
- const textDecoder = new TextDecoder('utf-8');
63
-
64
- const fileContents = filePaths
65
- .map((filePath) => {
66
- const { data: content, encoding } = data[filePath];
67
- return {
68
- path: filePath,
69
- content:
70
- encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
71
- };
72
- })
73
- .filter((f) => f.content);
74
-
75
- const commands = await detectProjectCommands(fileContents);
76
- const commandsMessage = createCommandsMessage(commands);
77
-
78
- const filesMessage: Message = {
79
- role: 'assistant',
80
- content: `Cloning the repo ${repoUrl} into ${workdir}
81
- <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
82
- ${fileContents
83
- .map(
84
- (file) =>
85
- `<boltAction type="file" filePath="${file.path}">
86
- ${file.content}
87
- </boltAction>`,
88
- )
89
- .join('\n')}
90
- </boltArtifact>`,
91
- id: generateId(),
92
- createdAt: new Date(),
93
- };
94
-
95
- const messages = [filesMessage];
96
-
97
- if (commandsMessage) {
98
- messages.push(commandsMessage);
99
- }
100
-
101
- await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
102
- }
103
- } catch (error) {
104
- console.error('Error during import:', error);
105
- toast.error('Failed to import repository');
106
- } finally {
107
- setLoading(false);
108
- }
109
- }
110
- };
111
-
112
- return (
113
- <>
114
- <button
115
- onClick={onClick}
116
- title="Clone a Git Repo"
117
- className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
118
- >
119
- <span className="i-ph:git-branch" />
120
- Clone a Git Repo
121
- </button>
122
- {loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
123
- </>
124
- );
125
- }
 
1
+ import ignore from 'ignore';
2
+ import { useGit } from '~/lib/hooks/useGit';
3
+ import type { Message } from 'ai';
4
+ import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
5
+ import { generateId } from '~/utils/fileUtils';
6
+ import { useState } from 'react';
7
+ import { toast } from 'react-toastify';
8
+ import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
+
10
+ const IGNORE_PATTERNS = [
11
+ 'node_modules/**',
12
+ '.git/**',
13
+ '.github/**',
14
+ '.vscode/**',
15
+ '**/*.jpg',
16
+ '**/*.jpeg',
17
+ '**/*.png',
18
+ 'dist/**',
19
+ 'build/**',
20
+ '.next/**',
21
+ 'coverage/**',
22
+ '.cache/**',
23
+ '.vscode/**',
24
+ '.idea/**',
25
+ '**/*.log',
26
+ '**/.DS_Store',
27
+ '**/npm-debug.log*',
28
+ '**/yarn-debug.log*',
29
+ '**/yarn-error.log*',
30
+ '**/*lock.json',
31
+ '**/*lock.yaml',
32
+ ];
33
+
34
+ const ig = ignore().add(IGNORE_PATTERNS);
35
+
36
+ interface GitCloneButtonProps {
37
+ className?: string;
38
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
39
+ }
40
+
41
+ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
42
+ const { ready, gitClone } = useGit();
43
+ const [loading, setLoading] = useState(false);
44
+
45
+ const onClick = async (_e: any) => {
46
+ if (!ready) {
47
+ return;
48
+ }
49
+
50
+ const repoUrl = prompt('Enter the Git url');
51
+
52
+ if (repoUrl) {
53
+ setLoading(true);
54
+
55
+ try {
56
+ const { workdir, data } = await gitClone(repoUrl);
57
+
58
+ if (importChat) {
59
+ const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
60
+ console.log(filePaths);
61
+
62
+ const textDecoder = new TextDecoder('utf-8');
63
+
64
+ const fileContents = filePaths
65
+ .map((filePath) => {
66
+ const { data: content, encoding } = data[filePath];
67
+ return {
68
+ path: filePath,
69
+ content:
70
+ encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
71
+ };
72
+ })
73
+ .filter((f) => f.content);
74
+
75
+ const commands = await detectProjectCommands(fileContents);
76
+ const commandsMessage = createCommandsMessage(commands);
77
+
78
+ const filesMessage: Message = {
79
+ role: 'assistant',
80
+ content: `Cloning the repo ${repoUrl} into ${workdir}
81
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
82
+ ${fileContents
83
+ .map(
84
+ (file) =>
85
+ `<boltAction type="file" filePath="${file.path}">
86
+ ${file.content}
87
+ </boltAction>`,
88
+ )
89
+ .join('\n')}
90
+ </boltArtifact>`,
91
+ id: generateId(),
92
+ createdAt: new Date(),
93
+ };
94
+
95
+ const messages = [filesMessage];
96
+
97
+ if (commandsMessage) {
98
+ messages.push(commandsMessage);
99
+ }
100
+
101
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
102
+ }
103
+ } catch (error) {
104
+ console.error('Error during import:', error);
105
+ toast.error('Failed to import repository');
106
+ } finally {
107
+ setLoading(false);
108
+ }
109
+ }
110
+ };
111
+
112
+ return (
113
+ <>
114
+ <button
115
+ onClick={onClick}
116
+ title="Clone a Git Repo"
117
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
118
+ >
119
+ <span className="i-ph:git-branch" />
120
+ Clone a Git Repo
121
+ </button>
122
+ {loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
123
+ </>
124
+ );
125
+ }