CoolFace
Apppublic

legends810/testingnew

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
BaseChat.tsx631 linesDownload Raw Back to chat
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