Skip to content

Commit 79681e2

Browse files
Fix TypeScript errors: add missing props and rename ExampleAction to BoltAction
1 parent 07dc9c5 commit 79681e2

21 files changed

Lines changed: 1754 additions & 607 deletions

app/components/chat/BaseChat.tsx

Lines changed: 96 additions & 315 deletions
Large diffs are not rendered by default.

app/components/chat/Chat.client.tsx

Lines changed: 271 additions & 244 deletions
Large diffs are not rendered by default.

app/components/chat/Chatbox.tsx

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
import React from 'react';
2+
import { ClientOnly } from 'remix-utils/client-only';
3+
import { classNames } from '~/utils/classNames';
4+
import { PROVIDER_LIST } from '~/utils/constants';
5+
import { ModelSelector } from '~/components/chat/ModelSelector';
6+
import { APIKeyManager } from './APIKeyManager';
7+
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
8+
import FilePreview from './FilePreview';
9+
import { ScreenshotStateManager } from './ScreenshotStateManager';
10+
import { SendButton } from './SendButton.client';
11+
import { IconButton } from '~/components/ui/IconButton';
12+
import { toast } from 'react-toastify';
13+
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
14+
import { SupabaseConnection } from './SupabaseConnection';
15+
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
16+
import styles from './BaseChat.module.scss';
17+
import type { ProviderInfo } from '~/types/model';
18+
import { ColorSchemeDialog } from '~/components/chat/ColorSchemeDialog';
19+
import type { DesignScheme } from '~/types/design-scheme';
20+
import type { ElementInfo } from '~/components/workbench/Inspector';
21+
import { McpTools } from './MCPTools';
22+
23+
interface ChatBoxProps {
24+
isModelSettingsCollapsed: boolean;
25+
setIsModelSettingsCollapsed: (collapsed: boolean) => void;
26+
provider: any;
27+
providerList: any[];
28+
modelList: any[];
29+
apiKeys: Record<string, string>;
30+
isModelLoading: string | undefined;
31+
onApiKeysChange: (providerName: string, apiKey: string) => void;
32+
uploadedFiles: File[];
33+
imageDataList: string[];
34+
textareaRef: React.RefObject<HTMLTextAreaElement> | undefined;
35+
input: string;
36+
handlePaste: (e: React.ClipboardEvent) => void;
37+
TEXTAREA_MIN_HEIGHT: number;
38+
TEXTAREA_MAX_HEIGHT: number;
39+
isStreaming: boolean;
40+
handleSendMessage: (event: React.UIEvent, messageInput?: string) => void;
41+
isListening: boolean;
42+
startListening: () => void;
43+
stopListening: () => void;
44+
chatStarted: boolean;
45+
exportChat?: () => void;
46+
qrModalOpen: boolean;
47+
setQrModalOpen: (open: boolean) => void;
48+
handleFileUpload: () => void;
49+
setProvider?: ((provider: ProviderInfo) => void) | undefined;
50+
model?: string | undefined;
51+
setModel?: ((model: string) => void) | undefined;
52+
setUploadedFiles?: ((files: File[]) => void) | undefined;
53+
setImageDataList?: ((dataList: string[]) => void) | undefined;
54+
handleInputChange?: ((event: React.ChangeEvent<HTMLTextAreaElement>) => void) | undefined;
55+
handleStop?: (() => void) | undefined;
56+
enhancingPrompt?: boolean | undefined;
57+
enhancePrompt?: (() => void) | undefined;
58+
chatMode?: 'discuss' | 'build';
59+
setChatMode?: (mode: 'discuss' | 'build') => void;
60+
designScheme?: DesignScheme;
61+
setDesignScheme?: (scheme: DesignScheme) => void;
62+
selectedElement?: ElementInfo | null;
63+
setSelectedElement?: ((element: ElementInfo | null) => void) | undefined;
64+
}
65+
66+
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
67+
return (
68+
<div
69+
className={classNames(
70+
'relative bg-bolt-elements-background-depth-2 backdrop-blur p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
71+
72+
/*
73+
* {
74+
* 'sticky bottom-2': chatStarted,
75+
* },
76+
*/
77+
)}
78+
>
79+
<svg className={classNames(styles.PromptEffectContainer)}>
80+
<defs>
81+
<linearGradient
82+
id="line-gradient"
83+
x1="20%"
84+
y1="0%"
85+
x2="-14%"
86+
y2="10%"
87+
gradientUnits="userSpaceOnUse"
88+
gradientTransform="rotate(-45)"
89+
>
90+
<stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
91+
<stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
92+
<stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
93+
<stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
94+
</linearGradient>
95+
<linearGradient id="shine-gradient">
96+
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
97+
<stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
98+
<stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
99+
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
100+
</linearGradient>
101+
</defs>
102+
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
103+
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
104+
</svg>
105+
<div>
106+
<ClientOnly>
107+
{() => (
108+
<div className={props.isModelSettingsCollapsed ? 'hidden' : ''}>
109+
<ModelSelector
110+
key={props.provider?.name + ':' + props.modelList.length}
111+
model={props.model}
112+
setModel={props.setModel}
113+
modelList={props.modelList}
114+
provider={props.provider}
115+
setProvider={props.setProvider}
116+
providerList={props.providerList || (PROVIDER_LIST as ProviderInfo[])}
117+
apiKeys={props.apiKeys}
118+
modelLoading={props.isModelLoading}
119+
/>
120+
{(props.providerList || []).length > 0 &&
121+
props.provider &&
122+
!LOCAL_PROVIDERS.includes(props.provider.name) && (
123+
<APIKeyManager
124+
provider={props.provider}
125+
apiKey={props.apiKeys[props.provider.name] || ''}
126+
setApiKey={(key) => {
127+
props.onApiKeysChange(props.provider.name, key);
128+
}}
129+
/>
130+
)}
131+
</div>
132+
)}
133+
</ClientOnly>
134+
</div>
135+
<FilePreview
136+
files={props.uploadedFiles}
137+
imageDataList={props.imageDataList}
138+
onRemove={(index) => {
139+
props.setUploadedFiles?.(props.uploadedFiles.filter((_, i) => i !== index));
140+
props.setImageDataList?.(props.imageDataList.filter((_, i) => i !== index));
141+
}}
142+
/>
143+
<ClientOnly>
144+
{() => (
145+
<ScreenshotStateManager
146+
setUploadedFiles={props.setUploadedFiles}
147+
setImageDataList={props.setImageDataList}
148+
uploadedFiles={props.uploadedFiles}
149+
imageDataList={props.imageDataList}
150+
/>
151+
)}
152+
</ClientOnly>
153+
{props.selectedElement && (
154+
<div className="flex mx-1.5 gap-2 items-center justify-between rounded-lg rounded-b-none border border-b-none border-bolt-elements-borderColor text-bolt-elements-textPrimary flex py-1 px-2.5 font-medium text-xs">
155+
<div className="flex gap-2 items-center lowercase">
156+
<code className="bg-accent-500 rounded-4px px-1.5 py-1 mr-0.5 text-white">
157+
{props?.selectedElement?.tagName}
158+
</code>
159+
selected for inspection
160+
</div>
161+
<button
162+
className="bg-transparent text-accent-500 pointer-auto"
163+
onClick={() => props.setSelectedElement?.(null)}
164+
>
165+
Clear
166+
</button>
167+
</div>
168+
)}
169+
<div
170+
className={classNames('relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg')}
171+
>
172+
<textarea
173+
ref={props.textareaRef}
174+
className={classNames(
175+
'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
176+
'transition-all duration-200',
177+
'hover:border-bolt-elements-focus',
178+
)}
179+
onDragEnter={(e) => {
180+
e.preventDefault();
181+
e.currentTarget.style.border = '2px solid #1488fc';
182+
}}
183+
onDragOver={(e) => {
184+
e.preventDefault();
185+
e.currentTarget.style.border = '2px solid #1488fc';
186+
}}
187+
onDragLeave={(e) => {
188+
e.preventDefault();
189+
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
190+
}}
191+
onDrop={(e) => {
192+
e.preventDefault();
193+
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
194+
195+
const files = Array.from(e.dataTransfer.files);
196+
files.forEach((file) => {
197+
if (file.type.startsWith('image/')) {
198+
const reader = new FileReader();
199+
200+
reader.onload = (e) => {
201+
const base64Image = e.target?.result as string;
202+
props.setUploadedFiles?.([...props.uploadedFiles, file]);
203+
props.setImageDataList?.([...props.imageDataList, base64Image]);
204+
};
205+
reader.readAsDataURL(file);
206+
}
207+
});
208+
}}
209+
onKeyDown={(event) => {
210+
if (event.key === 'Enter') {
211+
if (event.shiftKey) {
212+
return;
213+
}
214+
215+
event.preventDefault();
216+
217+
if (props.isStreaming) {
218+
props.handleStop?.();
219+
return;
220+
}
221+
222+
// ignore if using input method engine
223+
if (event.nativeEvent.isComposing) {
224+
return;
225+
}
226+
227+
props.handleSendMessage?.(event);
228+
}
229+
}}
230+
value={props.input}
231+
onChange={(event) => {
232+
props.handleInputChange?.(event);
233+
}}
234+
onPaste={props.handlePaste}
235+
style={{
236+
minHeight: props.TEXTAREA_MIN_HEIGHT,
237+
maxHeight: props.TEXTAREA_MAX_HEIGHT,
238+
}}
239+
placeholder={props.chatMode === 'build' ? 'How can Bolt help you today?' : 'What would you like to discuss?'}
240+
translate="no"
241+
/>
242+
<ClientOnly>
243+
{() => (
244+
<SendButton
245+
show={props.input.length > 0 || props.isStreaming || props.uploadedFiles.length > 0}
246+
isStreaming={props.isStreaming}
247+
disabled={!props.providerList || props.providerList.length === 0}
248+
onClick={(event) => {
249+
if (props.isStreaming) {
250+
props.handleStop?.();
251+
return;
252+
}
253+
254+
if (props.input.length > 0 || props.uploadedFiles.length > 0) {
255+
props.handleSendMessage?.(event);
256+
}
257+
}}
258+
/>
259+
)}
260+
</ClientOnly>
261+
<div className="flex justify-between items-center text-sm p-4 pt-2">
262+
<div className="flex gap-1 items-center">
263+
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
264+
<McpTools
265+
onOpenPanel={function (): void {
266+
throw new Error('Function not implemented.');
267+
}}
268+
/>
269+
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
270+
<div className="i-ph:paperclip text-xl"></div>
271+
</IconButton>
272+
<IconButton
273+
title="Enhance prompt"
274+
disabled={props.input.length === 0 || props.enhancingPrompt}
275+
className={classNames('transition-all', props.enhancingPrompt ? 'opacity-100' : '')}
276+
onClick={() => {
277+
props.enhancePrompt?.();
278+
toast.success('Prompt enhanced!');
279+
}}
280+
>
281+
{props.enhancingPrompt ? (
282+
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
283+
) : (
284+
<div className="i-bolt:stars text-xl"></div>
285+
)}
286+
</IconButton>
287+
288+
<SpeechRecognitionButton
289+
isListening={props.isListening}
290+
onStart={props.startListening}
291+
onStop={props.stopListening}
292+
disabled={props.isStreaming}
293+
/>
294+
{props.chatStarted && (
295+
<IconButton
296+
title="Discuss"
297+
className={classNames(
298+
'transition-all flex items-center gap-1 px-1.5',
299+
props.chatMode === 'discuss'
300+
? '!bg-bolt-elements-item-backgroundAccent !text-bolt-elements-item-contentAccent'
301+
: 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault',
302+
)}
303+
onClick={() => {
304+
props.setChatMode?.(props.chatMode === 'discuss' ? 'build' : 'discuss');
305+
}}
306+
>
307+
<div className={`i-ph:chats text-xl`} />
308+
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
309+
</IconButton>
310+
)}
311+
<IconButton
312+
title="Model Settings"
313+
className={classNames('transition-all flex items-center gap-1', {
314+
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
315+
props.isModelSettingsCollapsed,
316+
'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
317+
!props.isModelSettingsCollapsed,
318+
})}
319+
onClick={() => props.setIsModelSettingsCollapsed(!props.isModelSettingsCollapsed)}
320+
disabled={!props.providerList || props.providerList.length === 0}
321+
>
322+
<div className={`i-ph:caret-${props.isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
323+
{props.isModelSettingsCollapsed ? <span className="text-xs">{props.model}</span> : <span />}
324+
</IconButton>
325+
</div>
326+
{props.input.length > 3 ? (
327+
<div className="text-xs text-bolt-elements-textTertiary">
328+
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
329+
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a new line
330+
</div>
331+
) : null}
332+
<SupabaseConnection />
333+
<ExpoQrModal open={props.qrModalOpen} onClose={() => props.setQrModalOpen(false)} />
334+
</div>
335+
</div>
336+
</div>
337+
);
338+
};

0 commit comments

Comments
 (0)