|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | import { useStore } from '@nanostores/react'; | 
					
						
						|  | import type { Message } from 'ai'; | 
					
						
						|  | import { useChat } from 'ai/react'; | 
					
						
						|  | import { useAnimate } from 'framer-motion'; | 
					
						
						|  | import { memo, useCallback, useEffect, useRef, useState } from 'react'; | 
					
						
						|  | import { cssTransition, toast, ToastContainer } from 'react-toastify'; | 
					
						
						|  | import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks'; | 
					
						
						|  | import { description, useChatHistory } from '~/lib/persistence'; | 
					
						
						|  | import { chatStore } from '~/lib/stores/chat'; | 
					
						
						|  | import { workbenchStore } from '~/lib/stores/workbench'; | 
					
						
						|  | import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants'; | 
					
						
						|  | import { cubicEasingFn } from '~/utils/easings'; | 
					
						
						|  | import { createScopedLogger, renderLogger } from '~/utils/logger'; | 
					
						
						|  | import { BaseChat } from './BaseChat'; | 
					
						
						|  | import Cookies from 'js-cookie'; | 
					
						
						|  | import { debounce } from '~/utils/debounce'; | 
					
						
						|  | import { useSettings } from '~/lib/hooks/useSettings'; | 
					
						
						|  | import type { ProviderInfo } from '~/types/model'; | 
					
						
						|  | import { useSearchParams } from '@remix-run/react'; | 
					
						
						|  |  | 
					
						
						|  | const toastAnimation = cssTransition({ | 
					
						
						|  | enter: 'animated fadeInRight', | 
					
						
						|  | exit: 'animated fadeOutRight', | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  | const logger = createScopedLogger('Chat'); | 
					
						
						|  |  | 
					
						
						|  | export function Chat() { | 
					
						
						|  | renderLogger.trace('Chat'); | 
					
						
						|  |  | 
					
						
						|  | const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory(); | 
					
						
						|  | const title = useStore(description); | 
					
						
						|  |  | 
					
						
						|  | return ( | 
					
						
						|  | <> | 
					
						
						|  | {ready && ( | 
					
						
						|  | <ChatImpl | 
					
						
						|  | description={title} | 
					
						
						|  | initialMessages={initialMessages} | 
					
						
						|  | exportChat={exportChat} | 
					
						
						|  | storeMessageHistory={storeMessageHistory} | 
					
						
						|  | importChat={importChat} | 
					
						
						|  | /> | 
					
						
						|  | )} | 
					
						
						|  | <ToastContainer | 
					
						
						|  | closeButton={({ closeToast }) => { | 
					
						
						|  | return ( | 
					
						
						|  | <button className="Toastify__close-button" onClick={closeToast}> | 
					
						
						|  | <div className="i-ph:x text-lg" /> | 
					
						
						|  | </button> | 
					
						
						|  | ); | 
					
						
						|  | }} | 
					
						
						|  | icon={({ type }) => { | 
					
						
						|  | /** | 
					
						
						|  | * @todo Handle more types if we need them. This may require extra color palettes. | 
					
						
						|  | */ | 
					
						
						|  | switch (type) { | 
					
						
						|  | case 'success': { | 
					
						
						|  | return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />; | 
					
						
						|  | } | 
					
						
						|  | case 'error': { | 
					
						
						|  | return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />; | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | return undefined; | 
					
						
						|  | }} | 
					
						
						|  | position="bottom-right" | 
					
						
						|  | pauseOnFocusLoss | 
					
						
						|  | transition={toastAnimation} | 
					
						
						|  | /> | 
					
						
						|  | </> | 
					
						
						|  | ); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | interface ChatProps { | 
					
						
						|  | initialMessages: Message[]; | 
					
						
						|  | storeMessageHistory: (messages: Message[]) => Promise<void>; | 
					
						
						|  | importChat: (description: string, messages: Message[]) => Promise<void>; | 
					
						
						|  | exportChat: () => void; | 
					
						
						|  | description?: string; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | export const ChatImpl = memo( | 
					
						
						|  | ({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => { | 
					
						
						|  | useShortcuts(); | 
					
						
						|  |  | 
					
						
						|  | const textareaRef = useRef<HTMLTextAreaElement>(null); | 
					
						
						|  | const [chatStarted, setChatStarted] = useState(initialMessages.length > 0); | 
					
						
						|  | const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); | 
					
						
						|  | const [imageDataList, setImageDataList] = useState<string[]>([]); | 
					
						
						|  | const [searchParams, setSearchParams] = useSearchParams(); | 
					
						
						|  | const files = useStore(workbenchStore.files); | 
					
						
						|  | const { activeProviders, promptId } = useSettings(); | 
					
						
						|  |  | 
					
						
						|  | const [model, setModel] = useState(() => { | 
					
						
						|  | const savedModel = Cookies.get('selectedModel'); | 
					
						
						|  | return savedModel || DEFAULT_MODEL; | 
					
						
						|  | }); | 
					
						
						|  | const [provider, setProvider] = useState(() => { | 
					
						
						|  | const savedProvider = Cookies.get('selectedProvider'); | 
					
						
						|  | return PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER; | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  | const { showChat } = useStore(chatStore); | 
					
						
						|  |  | 
					
						
						|  | const [animationScope, animate] = useAnimate(); | 
					
						
						|  |  | 
					
						
						|  | const [apiKeys, setApiKeys] = useState<Record<string, string>>({}); | 
					
						
						|  |  | 
					
						
						|  | const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({ | 
					
						
						|  | api: '/api/chat', | 
					
						
						|  | body: { | 
					
						
						|  | apiKeys, | 
					
						
						|  | files, | 
					
						
						|  | promptId, | 
					
						
						|  | }, | 
					
						
						|  | sendExtraMessageFields: true, | 
					
						
						|  | onError: (error) => { | 
					
						
						|  | logger.error('Request failed\n\n', error); | 
					
						
						|  | toast.error( | 
					
						
						|  | 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'), | 
					
						
						|  | ); | 
					
						
						|  | }, | 
					
						
						|  | onFinish: (message, response) => { | 
					
						
						|  | const usage = response.usage; | 
					
						
						|  |  | 
					
						
						|  | if (usage) { | 
					
						
						|  | console.log('Token usage:', usage); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | logger.debug('Finished streaming'); | 
					
						
						|  | }, | 
					
						
						|  | initialMessages, | 
					
						
						|  | initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '', | 
					
						
						|  | }); | 
					
						
						|  | useEffect(() => { | 
					
						
						|  | const prompt = searchParams.get('prompt'); | 
					
						
						|  | console.log(prompt, searchParams, model, provider); | 
					
						
						|  |  | 
					
						
						|  | if (prompt) { | 
					
						
						|  | setSearchParams({}); | 
					
						
						|  | runAnimation(); | 
					
						
						|  | append({ | 
					
						
						|  | role: 'user', | 
					
						
						|  | content: [ | 
					
						
						|  | { | 
					
						
						|  | type: 'text', | 
					
						
						|  | text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`, | 
					
						
						|  | }, | 
					
						
						|  | ] as any, | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  | }, [model, provider, searchParams]); | 
					
						
						|  |  | 
					
						
						|  | const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer(); | 
					
						
						|  | const { parsedMessages, parseMessages } = useMessageParser(); | 
					
						
						|  |  | 
					
						
						|  | const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; | 
					
						
						|  |  | 
					
						
						|  | useEffect(() => { | 
					
						
						|  | chatStore.setKey('started', initialMessages.length > 0); | 
					
						
						|  | }, []); | 
					
						
						|  |  | 
					
						
						|  | useEffect(() => { | 
					
						
						|  | parseMessages(messages, isLoading); | 
					
						
						|  |  | 
					
						
						|  | if (messages.length > initialMessages.length) { | 
					
						
						|  | storeMessageHistory(messages).catch((error) => toast.error(error.message)); | 
					
						
						|  | } | 
					
						
						|  | }, [messages, isLoading, parseMessages]); | 
					
						
						|  |  | 
					
						
						|  | const scrollTextArea = () => { | 
					
						
						|  | const textarea = textareaRef.current; | 
					
						
						|  |  | 
					
						
						|  | if (textarea) { | 
					
						
						|  | textarea.scrollTop = textarea.scrollHeight; | 
					
						
						|  | } | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | const abort = () => { | 
					
						
						|  | stop(); | 
					
						
						|  | chatStore.setKey('aborted', true); | 
					
						
						|  | workbenchStore.abortAllActions(); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | useEffect(() => { | 
					
						
						|  | const textarea = textareaRef.current; | 
					
						
						|  |  | 
					
						
						|  | if (textarea) { | 
					
						
						|  | textarea.style.height = 'auto'; | 
					
						
						|  |  | 
					
						
						|  | const scrollHeight = textarea.scrollHeight; | 
					
						
						|  |  | 
					
						
						|  | textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`; | 
					
						
						|  | textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden'; | 
					
						
						|  | } | 
					
						
						|  | }, [input, textareaRef]); | 
					
						
						|  |  | 
					
						
						|  | const runAnimation = async () => { | 
					
						
						|  | if (chatStarted) { | 
					
						
						|  | return; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | await Promise.all([ | 
					
						
						|  | animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }), | 
					
						
						|  | animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }), | 
					
						
						|  | ]); | 
					
						
						|  |  | 
					
						
						|  | chatStore.setKey('started', true); | 
					
						
						|  |  | 
					
						
						|  | setChatStarted(true); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | const sendMessage = async (_event: React.UIEvent, messageInput?: string) => { | 
					
						
						|  | const _input = messageInput || input; | 
					
						
						|  |  | 
					
						
						|  | if (_input.length === 0 || isLoading) { | 
					
						
						|  | return; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | await workbenchStore.saveAllFiles(); | 
					
						
						|  |  | 
					
						
						|  | const fileModifications = workbenchStore.getFileModifcations(); | 
					
						
						|  |  | 
					
						
						|  | chatStore.setKey('aborted', false); | 
					
						
						|  |  | 
					
						
						|  | runAnimation(); | 
					
						
						|  |  | 
					
						
						|  | if (fileModifications !== undefined) { | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | append({ | 
					
						
						|  | role: 'user', | 
					
						
						|  | content: [ | 
					
						
						|  | { | 
					
						
						|  | type: 'text', | 
					
						
						|  | text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`, | 
					
						
						|  | }, | 
					
						
						|  | ...imageDataList.map((imageData) => ({ | 
					
						
						|  | type: 'image', | 
					
						
						|  | image: imageData, | 
					
						
						|  | })), | 
					
						
						|  | ] as any, | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | workbenchStore.resetAllFileModifications(); | 
					
						
						|  | } else { | 
					
						
						|  | append({ | 
					
						
						|  | role: 'user', | 
					
						
						|  | content: [ | 
					
						
						|  | { | 
					
						
						|  | type: 'text', | 
					
						
						|  | text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`, | 
					
						
						|  | }, | 
					
						
						|  | ...imageDataList.map((imageData) => ({ | 
					
						
						|  | type: 'image', | 
					
						
						|  | image: imageData, | 
					
						
						|  | })), | 
					
						
						|  | ] as any, | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | setInput(''); | 
					
						
						|  | Cookies.remove(PROMPT_COOKIE_KEY); | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | setUploadedFiles([]); | 
					
						
						|  | setImageDataList([]); | 
					
						
						|  |  | 
					
						
						|  | resetEnhancer(); | 
					
						
						|  |  | 
					
						
						|  | textareaRef.current?.blur(); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { | 
					
						
						|  | handleInputChange(event); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | const debouncedCachePrompt = useCallback( | 
					
						
						|  | debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => { | 
					
						
						|  | const trimmedValue = event.target.value.trim(); | 
					
						
						|  | Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 }); | 
					
						
						|  | }, 1000), | 
					
						
						|  | [], | 
					
						
						|  | ); | 
					
						
						|  |  | 
					
						
						|  | const [messageRef, scrollRef] = useSnapScroll(); | 
					
						
						|  |  | 
					
						
						|  | useEffect(() => { | 
					
						
						|  | const storedApiKeys = Cookies.get('apiKeys'); | 
					
						
						|  |  | 
					
						
						|  | if (storedApiKeys) { | 
					
						
						|  | setApiKeys(JSON.parse(storedApiKeys)); | 
					
						
						|  | } | 
					
						
						|  | }, []); | 
					
						
						|  |  | 
					
						
						|  | const handleModelChange = (newModel: string) => { | 
					
						
						|  | setModel(newModel); | 
					
						
						|  | Cookies.set('selectedModel', newModel, { expires: 30 }); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | const handleProviderChange = (newProvider: ProviderInfo) => { | 
					
						
						|  | setProvider(newProvider); | 
					
						
						|  | Cookies.set('selectedProvider', newProvider.name, { expires: 30 }); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | return ( | 
					
						
						|  | <BaseChat | 
					
						
						|  | ref={animationScope} | 
					
						
						|  | textareaRef={textareaRef} | 
					
						
						|  | input={input} | 
					
						
						|  | showChat={showChat} | 
					
						
						|  | chatStarted={chatStarted} | 
					
						
						|  | isStreaming={isLoading} | 
					
						
						|  | enhancingPrompt={enhancingPrompt} | 
					
						
						|  | promptEnhanced={promptEnhanced} | 
					
						
						|  | sendMessage={sendMessage} | 
					
						
						|  | model={model} | 
					
						
						|  | setModel={handleModelChange} | 
					
						
						|  | provider={provider} | 
					
						
						|  | setProvider={handleProviderChange} | 
					
						
						|  | providerList={activeProviders} | 
					
						
						|  | messageRef={messageRef} | 
					
						
						|  | scrollRef={scrollRef} | 
					
						
						|  | handleInputChange={(e) => { | 
					
						
						|  | onTextareaChange(e); | 
					
						
						|  | debouncedCachePrompt(e); | 
					
						
						|  | }} | 
					
						
						|  | handleStop={abort} | 
					
						
						|  | description={description} | 
					
						
						|  | importChat={importChat} | 
					
						
						|  | exportChat={exportChat} | 
					
						
						|  | messages={messages.map((message, i) => { | 
					
						
						|  | if (message.role === 'user') { | 
					
						
						|  | return message; | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | return { | 
					
						
						|  | ...message, | 
					
						
						|  | content: parsedMessages[i] || '', | 
					
						
						|  | }; | 
					
						
						|  | })} | 
					
						
						|  | enhancePrompt={() => { | 
					
						
						|  | enhancePrompt( | 
					
						
						|  | input, | 
					
						
						|  | (input) => { | 
					
						
						|  | setInput(input); | 
					
						
						|  | scrollTextArea(); | 
					
						
						|  | }, | 
					
						
						|  | model, | 
					
						
						|  | provider, | 
					
						
						|  | apiKeys, | 
					
						
						|  | ); | 
					
						
						|  | }} | 
					
						
						|  | uploadedFiles={uploadedFiles} | 
					
						
						|  | setUploadedFiles={setUploadedFiles} | 
					
						
						|  | imageDataList={imageDataList} | 
					
						
						|  | setImageDataList={setImageDataList} | 
					
						
						|  | /> | 
					
						
						|  | ); | 
					
						
						|  | }, | 
					
						
						|  | ); | 
					
						
						|  |  |