legends810/testingnew
0
1/*2 * @ts-nocheck3 * Preventing TS checks with files presented in the video for a better presentation.4 */5import type { JSONValue, Message } from 'ai';6import React, { type RefCallback, useEffect, useState } from 'react';7import { ClientOnly } from 'remix-utils/client-only';8import { Menu } from '~/components/sidebar/Menu.client';9import { IconButton } from '~/components/ui/IconButton';10import { Workbench } from '~/components/workbench/Workbench.client';11import { classNames } from '~/utils/classNames';12import { PROVIDER_LIST } from '~/utils/constants';13import { Messages } from './Messages.client';14import { SendButton } from './SendButton.client';15import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';16import Cookies from 'js-cookie';17import * as Tooltip from '@radix-ui/react-tooltip';18 19import styles from './BaseChat.module.scss';20import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';21import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';22import { ExamplePrompts } from '~/components/chat/ExamplePrompts';23import GitCloneButton from './GitCloneButton';24 25import FilePreview from './FilePreview';26import { ModelSelector } from '~/components/chat/ModelSelector';27import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';28import type { ProviderInfo } from '~/types/model';29import { ScreenshotStateManager } from './ScreenshotStateManager';30import { toast } from 'react-toastify';31import StarterTemplates from './StarterTemplates';32import type { ActionAlert } from '~/types/actions';33import ChatAlert from './ChatAlert';34import type { ModelInfo } from '~/lib/modules/llm/types';35import ProgressCompilation from './ProgressCompilation';36import type { ProgressAnnotation } from '~/types/context';37import type { ActionRunner } from '~/lib/runtime/action-runner';38import { LOCAL_PROVIDERS } from '~/lib/stores/settings';39 40const TEXTAREA_MIN_HEIGHT = 76;41 42interface BaseChatProps {43 textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;44 messageRef?: RefCallback<HTMLDivElement> | undefined;45 scrollRef?: RefCallback<HTMLDivElement> | undefined;46 showChat?: boolean;47 chatStarted?: boolean;48 isStreaming?: boolean;49 onStreamingChange?: (streaming: boolean) => void;50 messages?: Message[];51 description?: string;52 enhancingPrompt?: boolean;53 promptEnhanced?: boolean;54 input?: string;55 model?: string;56 setModel?: (model: string) => void;57 provider?: ProviderInfo;58 setProvider?: (provider: ProviderInfo) => void;59 providerList?: ProviderInfo[];60 handleStop?: () => void;61 sendMessage?: (event: React.UIEvent, messageInput?: string) => void;62 handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;63 enhancePrompt?: () => void;64 importChat?: (description: string, messages: Message[]) => Promise<void>;65 exportChat?: () => void;66 uploadedFiles?: File[];67 setUploadedFiles?: (files: File[]) => void;68 imageDataList?: string[];69 setImageDataList?: (dataList: string[]) => void;70 actionAlert?: ActionAlert;71 clearAlert?: () => void;72 data?: JSONValue[] | undefined;73 actionRunner?: ActionRunner;74}75 76export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(77 (78 {79 textareaRef,80 messageRef,81 scrollRef,82 showChat = true,83 chatStarted = false,84 isStreaming = false,85 onStreamingChange,86 model,87 setModel,88 provider,89 setProvider,90 providerList,91 input = '',92 enhancingPrompt,93 handleInputChange,94 95 // promptEnhanced,96 enhancePrompt,97 sendMessage,98 handleStop,99 importChat,100 exportChat,101 uploadedFiles = [],102 setUploadedFiles,103 imageDataList = [],104 setImageDataList,105 messages,106 actionAlert,107 clearAlert,108 data,109 actionRunner,110 },111 ref,112 ) => {113 const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;114 const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());115 const [modelList, setModelList] = useState<ModelInfo[]>([]);116 const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);117 const [isListening, setIsListening] = useState(false);118 const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);119 const [transcript, setTranscript] = useState('');120 const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');121 const [progressAnnotations, setProgressAnnotations] = useState<ProgressAnnotation[]>([]);122 useEffect(() => {123 if (data) {124 const progressList = data.filter(125 (x) => typeof x === 'object' && (x as any).type === 'progress',126 ) as ProgressAnnotation[];127 setProgressAnnotations(progressList);128 }129 }, [data]);130 useEffect(() => {131 console.log(transcript);132 }, [transcript]);133 134 useEffect(() => {135 onStreamingChange?.(isStreaming);136 }, [isStreaming, onStreamingChange]);137 138 useEffect(() => {139 if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {140 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;141 const recognition = new SpeechRecognition();142 recognition.continuous = true;143 recognition.interimResults = true;144 145 recognition.onresult = (event) => {146 const transcript = Array.from(event.results)147 .map((result) => result[0])148 .map((result) => result.transcript)149 .join('');150 151 setTranscript(transcript);152 153 if (handleInputChange) {154 const syntheticEvent = {155 target: { value: transcript },156 } as React.ChangeEvent<HTMLTextAreaElement>;157 handleInputChange(syntheticEvent);158 }159 };160 161 recognition.onerror = (event) => {162 console.error('Speech recognition error:', event.error);163 setIsListening(false);164 };165 166 setRecognition(recognition);167 }168 }, []);169 170 useEffect(() => {171 if (typeof window !== 'undefined') {172 let parsedApiKeys: Record<string, string> | undefined = {};173 174 try {175 parsedApiKeys = getApiKeysFromCookies();176 setApiKeys(parsedApiKeys);177 } catch (error) {178 console.error('Error loading API keys from cookies:', error);179 Cookies.remove('apiKeys');180 }181 182 setIsModelLoading('all');183 fetch('/api/models')184 .then((response) => response.json())185 .then((data) => {186 const typedData = data as { modelList: ModelInfo[] };187 setModelList(typedData.modelList);188 })189 .catch((error) => {190 console.error('Error fetching model list:', error);191 })192 .finally(() => {193 setIsModelLoading(undefined);194 });195 }196 }, [providerList, provider]);197 198 const onApiKeysChange = async (providerName: string, apiKey: string) => {199 const newApiKeys = { ...apiKeys, [providerName]: apiKey };200 setApiKeys(newApiKeys);201 Cookies.set('apiKeys', JSON.stringify(newApiKeys));202 203 setIsModelLoading(providerName);204 205 let providerModels: ModelInfo[] = [];206 207 try {208 const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);209 const data = await response.json();210 providerModels = (data as { modelList: ModelInfo[] }).modelList;211 } catch (error) {212 console.error('Error loading dynamic models for:', providerName, error);213 }214 215 // Only update models for the specific provider216 setModelList((prevModels) => {217 const otherModels = prevModels.filter((model) => model.provider !== providerName);218 return [...otherModels, ...providerModels];219 });220 setIsModelLoading(undefined);221 };222 223 const startListening = () => {224 if (recognition) {225 recognition.start();226 setIsListening(true);227 }228 };229 230 const stopListening = () => {231 if (recognition) {232 recognition.stop();233 setIsListening(false);234 }235 };236 237 const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {238 if (sendMessage) {239 sendMessage(event, messageInput);240 241 if (recognition) {242 recognition.abort(); // Stop current recognition243 setTranscript(''); // Clear transcript244 setIsListening(false);245 246 // Clear the input by triggering handleInputChange with empty value247 if (handleInputChange) {248 const syntheticEvent = {249 target: { value: '' },250 } as React.ChangeEvent<HTMLTextAreaElement>;251 handleInputChange(syntheticEvent);252 }253 }254 }255 };256 257 const handleFileUpload = () => {258 const input = document.createElement('input');259 input.type = 'file';260 input.accept = 'image/*';261 262 input.onchange = async (e) => {263 const file = (e.target as HTMLInputElement).files?.[0];264 265 if (file) {266 const reader = new FileReader();267 268 reader.onload = (e) => {269 const base64Image = e.target?.result as string;270 setUploadedFiles?.([...uploadedFiles, file]);271 setImageDataList?.([...imageDataList, base64Image]);272 };273 reader.readAsDataURL(file);274 }275 };276 277 input.click();278 };279 280 const handlePaste = async (e: React.ClipboardEvent) => {281 const items = e.clipboardData?.items;282 283 if (!items) {284 return;285 }286 287 for (const item of items) {288 if (item.type.startsWith('image/')) {289 e.preventDefault();290 291 const file = item.getAsFile();292 293 if (file) {294 const reader = new FileReader();295 296 reader.onload = (e) => {297 const base64Image = e.target?.result as string;298 setUploadedFiles?.([...uploadedFiles, file]);299 setImageDataList?.([...imageDataList, base64Image]);300 };301 reader.readAsDataURL(file);302 }303 304 break;305 }306 }307 };308 309 const baseChat = (310 <div311 ref={ref}312 className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}313 data-chat-visible={showChat}314 >315 <ClientOnly>{() => <Menu />}</ClientOnly>316 <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">317 <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>318 {!chatStarted && (319 <div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">320 <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">321 Where ideas begin322 </h1>323 <p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">324 Bring ideas to life in seconds or get help on existing projects.325 </p>326 </div>327 )}328 <div329 className={classNames('pt-6 px-2 sm:px-6', {330 'h-full flex flex-col': chatStarted,331 })}332 ref={scrollRef}333 >334 <ClientOnly>335 {() => {336 return chatStarted ? (337 <Messages338 ref={messageRef}339 className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"340 messages={messages}341 isStreaming={isStreaming}342 />343 ) : null;344 }}345 </ClientOnly>346 <div347 className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {348 'sticky bottom-2': chatStarted,349 })}350 >351 <div className="bg-bolt-elements-background-depth-2">352 {actionAlert && (353 <ChatAlert354 alert={actionAlert}355 clearAlert={() => clearAlert?.()}356 postMessage={(message) => {357 sendMessage?.({} as any, message);358 clearAlert?.();359 }}360 />361 )}362 </div>363 {progressAnnotations && <ProgressCompilation data={progressAnnotations} />}364 <div365 className={classNames(366 'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',367 368 /*369 * {370 * 'sticky bottom-2': chatStarted,371 * },372 */373 )}374 >375 <svg className={classNames(styles.PromptEffectContainer)}>376 <defs>377 <linearGradient378 id="line-gradient"379 x1="20%"380 y1="0%"381 x2="-14%"382 y2="10%"383 gradientUnits="userSpaceOnUse"384 gradientTransform="rotate(-45)"385 >386 <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>387 <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>388 <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>389 <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>390 </linearGradient>391 <linearGradient id="shine-gradient">392 <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>393 <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>394 <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>395 <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>396 </linearGradient>397 </defs>398 <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>399 <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>400 </svg>401 <div>402 <ClientOnly>403 {() => (404 <div className={isModelSettingsCollapsed ? 'hidden' : ''}>405 <ModelSelector406 key={provider?.name + ':' + modelList.length}407 model={model}408 setModel={setModel}409 modelList={modelList}410 provider={provider}411 setProvider={setProvider}412 providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}413 apiKeys={apiKeys}414 modelLoading={isModelLoading}415 />416 {(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && (417 <APIKeyManager418 provider={provider}419 apiKey={apiKeys[provider.name] || ''}420 setApiKey={(key) => {421 onApiKeysChange(provider.name, key);422 }}423 />424 )}425 </div>426 )}427 </ClientOnly>428 </div>429 <FilePreview430 files={uploadedFiles}431 imageDataList={imageDataList}432 onRemove={(index) => {433 setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));434 setImageDataList?.(imageDataList.filter((_, i) => i !== index));435 }}436 />437 <ClientOnly>438 {() => (439 <ScreenshotStateManager440 setUploadedFiles={setUploadedFiles}441 setImageDataList={setImageDataList}442 uploadedFiles={uploadedFiles}443 imageDataList={imageDataList}444 />445 )}446 </ClientOnly>447 <div448 className={classNames(449 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',450 )}451 >452 <textarea453 ref={textareaRef}454 className={classNames(455 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',456 'transition-all duration-200',457 'hover:border-bolt-elements-focus',458 )}459 onDragEnter={(e) => {460 e.preventDefault();461 e.currentTarget.style.border = '2px solid #1488fc';462 }}463 onDragOver={(e) => {464 e.preventDefault();465 e.currentTarget.style.border = '2px solid #1488fc';466 }}467 onDragLeave={(e) => {468 e.preventDefault();469 e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';470 }}471 onDrop={(e) => {472 e.preventDefault();473 e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';474 475 const files = Array.from(e.dataTransfer.files);476 files.forEach((file) => {477 if (file.type.startsWith('image/')) {478 const reader = new FileReader();479 480 reader.onload = (e) => {481 const base64Image = e.target?.result as string;482 setUploadedFiles?.([...uploadedFiles, file]);483 setImageDataList?.([...imageDataList, base64Image]);484 };485 reader.readAsDataURL(file);486 }487 });488 }}489 onKeyDown={(event) => {490 if (event.key === 'Enter') {491 if (event.shiftKey) {492 return;493 }494 495 event.preventDefault();496 497 if (isStreaming) {498 handleStop?.();499 return;500 }501 502 // ignore if using input method engine503 if (event.nativeEvent.isComposing) {504 return;505 }506 507 handleSendMessage?.(event);508 }509 }}510 value={input}511 onChange={(event) => {512 handleInputChange?.(event);513 }}514 onPaste={handlePaste}515 style={{516 minHeight: TEXTAREA_MIN_HEIGHT,517 maxHeight: TEXTAREA_MAX_HEIGHT,518 }}519 placeholder="How can Bolt help you today?"520 translate="no"521 />522 <ClientOnly>523 {() => (524 <SendButton525 show={input.length > 0 || isStreaming || uploadedFiles.length > 0}526 isStreaming={isStreaming}527 disabled={!providerList || providerList.length === 0}528 onClick={(event) => {529 if (isStreaming) {530 handleStop?.();531 return;532 }533 534 if (input.length > 0 || uploadedFiles.length > 0) {535 handleSendMessage?.(event);536 }537 }}538 />539 )}540 </ClientOnly>541 <div className="flex justify-between items-center text-sm p-4 pt-2">542 <div className="flex gap-1 items-center">543 <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>544 <div className="i-ph:paperclip text-xl"></div>545 </IconButton>546 <IconButton547 title="Enhance prompt"548 disabled={input.length === 0 || enhancingPrompt}549 className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}550 onClick={() => {551 enhancePrompt?.();552 toast.success('Prompt enhanced!');553 }}554 >555 {enhancingPrompt ? (556 <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>557 ) : (558 <div className="i-bolt:stars text-xl"></div>559 )}560 </IconButton>561 562 <SpeechRecognitionButton563 isListening={isListening}564 onStart={startListening}565 onStop={stopListening}566 disabled={isStreaming}567 />568 {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}569 <IconButton570 title="Model Settings"571 className={classNames('transition-all flex items-center gap-1', {572 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':573 isModelSettingsCollapsed,574 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':575 !isModelSettingsCollapsed,576 })}577 onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}578 disabled={!providerList || providerList.length === 0}579 >580 <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />581 {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}582 </IconButton>583 </div>584 {input.length > 3 ? (585 <div className="text-xs text-bolt-elements-textTertiary">586 Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}587 + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}588 a new line589 </div>590 ) : null}591 </div>592 </div>593 </div>594 </div>595 </div>596 <div className="flex flex-col justify-center gap-5">597 {!chatStarted && (598 <div className="flex justify-center gap-2">599 {ImportButtons(importChat)}600 <GitCloneButton importChat={importChat} />601 </div>602 )}603 {!chatStarted &&604 ExamplePrompts((event, messageInput) => {605 if (isStreaming) {606 handleStop?.();607 return;608 }609 610 handleSendMessage?.(event, messageInput);611 })}612 {!chatStarted && <StarterTemplates />}613 </div>614 </div>615 <ClientOnly>616 {() => (617 <Workbench618 actionRunner={actionRunner ?? ({} as ActionRunner)}619 chatStarted={chatStarted}620 isStreaming={isStreaming}621 />622 )}623 </ClientOnly>624 </div>625 </div>626 );627 628 return <Tooltip.Provider delayDuration={200}>{baseChat}</Tooltip.Provider>;629 },630);631 