push (#414)
This commit is contained in:
		
							parent
							
								
									e8150e7195
								
							
						
					
					
						commit
						e1f286efb8
					
				|  | @ -1,3 +1,8 @@ | ||||||
|  | # Chatbot UI | ||||||
| DEFAULT_MODEL=gpt-3.5-turbo | DEFAULT_MODEL=gpt-3.5-turbo | ||||||
| DEFAULT_SYSTEM_PROMPT=You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown. | DEFAULT_SYSTEM_PROMPT=You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown. | ||||||
| OPENAI_API_KEY=YOUR_KEY | OPENAI_API_KEY=YOUR_KEY | ||||||
|  | 
 | ||||||
|  | # Google | ||||||
|  | GOOGLE_API_KEY=YOUR_API_KEY | ||||||
|  | GOOGLE_CSE_ID=YOUR_ENGINE_ID | ||||||
|  |  | ||||||
|  | @ -2,14 +2,15 @@ import { Conversation, Message } from '@/types/chat'; | ||||||
| import { KeyValuePair } from '@/types/data'; | import { KeyValuePair } from '@/types/data'; | ||||||
| import { ErrorMessage } from '@/types/error'; | import { ErrorMessage } from '@/types/error'; | ||||||
| import { OpenAIModel, OpenAIModelID } from '@/types/openai'; | import { OpenAIModel, OpenAIModelID } from '@/types/openai'; | ||||||
|  | import { Plugin } from '@/types/plugin'; | ||||||
| import { Prompt } from '@/types/prompt'; | import { Prompt } from '@/types/prompt'; | ||||||
| import { throttle } from '@/utils'; | import { throttle } from '@/utils'; | ||||||
| import { IconArrowDown, IconClearAll, IconSettings } from '@tabler/icons-react'; | import { IconArrowDown, IconClearAll, IconSettings } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { | import { | ||||||
|   FC, |   FC, | ||||||
|   memo, |  | ||||||
|   MutableRefObject, |   MutableRefObject, | ||||||
|  |   memo, | ||||||
|   useCallback, |   useCallback, | ||||||
|   useEffect, |   useEffect, | ||||||
|   useRef, |   useRef, | ||||||
|  | @ -33,7 +34,11 @@ interface Props { | ||||||
|   modelError: ErrorMessage | null; |   modelError: ErrorMessage | null; | ||||||
|   loading: boolean; |   loading: boolean; | ||||||
|   prompts: Prompt[]; |   prompts: Prompt[]; | ||||||
|   onSend: (message: Message, deleteCount?: number) => void; |   onSend: ( | ||||||
|  |     message: Message, | ||||||
|  |     deleteCount: number, | ||||||
|  |     plugin: Plugin | null, | ||||||
|  |   ) => void; | ||||||
|   onUpdateConversation: ( |   onUpdateConversation: ( | ||||||
|     conversation: Conversation, |     conversation: Conversation, | ||||||
|     data: KeyValuePair, |     data: KeyValuePair, | ||||||
|  | @ -116,8 +121,6 @@ export const Chat: FC<Props> = memo( | ||||||
|     }; |     }; | ||||||
|     const throttledScrollDown = throttle(scrollDown, 250); |     const throttledScrollDown = throttle(scrollDown, 250); | ||||||
| 
 | 
 | ||||||
|     // appear scroll down button only when user scrolls up
 |  | ||||||
| 
 |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|       throttledScrollDown(); |       throttledScrollDown(); | ||||||
|       setCurrentMessage( |       setCurrentMessage( | ||||||
|  | @ -300,16 +303,15 @@ export const Chat: FC<Props> = memo( | ||||||
|               textareaRef={textareaRef} |               textareaRef={textareaRef} | ||||||
|               messageIsStreaming={messageIsStreaming} |               messageIsStreaming={messageIsStreaming} | ||||||
|               conversationIsEmpty={conversation.messages.length === 0} |               conversationIsEmpty={conversation.messages.length === 0} | ||||||
|               messages={conversation.messages} |  | ||||||
|               model={conversation.model} |               model={conversation.model} | ||||||
|               prompts={prompts} |               prompts={prompts} | ||||||
|               onSend={(message) => { |               onSend={(message, plugin) => { | ||||||
|                 setCurrentMessage(message); |                 setCurrentMessage(message); | ||||||
|                 onSend(message); |                 onSend(message, 0, plugin); | ||||||
|               }} |               }} | ||||||
|               onRegenerate={() => { |               onRegenerate={() => { | ||||||
|                 if (currentMessage) { |                 if (currentMessage) { | ||||||
|                   onSend(currentMessage, 2); |                   onSend(currentMessage, 2, null); | ||||||
|                 } |                 } | ||||||
|               }} |               }} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,14 @@ | ||||||
| import { Message } from '@/types/chat'; | import { Message } from '@/types/chat'; | ||||||
| import { OpenAIModel } from '@/types/openai'; | import { OpenAIModel } from '@/types/openai'; | ||||||
|  | import { Plugin } from '@/types/plugin'; | ||||||
| import { Prompt } from '@/types/prompt'; | import { Prompt } from '@/types/prompt'; | ||||||
| import { IconPlayerStop, IconRepeat, IconSend } from '@tabler/icons-react'; | import { | ||||||
|  |   IconBolt, | ||||||
|  |   IconBrandGoogle, | ||||||
|  |   IconPlayerStop, | ||||||
|  |   IconRepeat, | ||||||
|  |   IconSend, | ||||||
|  | } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { | import { | ||||||
|   FC, |   FC, | ||||||
|  | @ -12,6 +19,7 @@ import { | ||||||
|   useRef, |   useRef, | ||||||
|   useState, |   useState, | ||||||
| } from 'react'; | } from 'react'; | ||||||
|  | import { PluginSelect } from './PluginSelect'; | ||||||
| import { PromptList } from './PromptList'; | import { PromptList } from './PromptList'; | ||||||
| import { VariableModal } from './VariableModal'; | import { VariableModal } from './VariableModal'; | ||||||
| 
 | 
 | ||||||
|  | @ -19,9 +27,8 @@ interface Props { | ||||||
|   messageIsStreaming: boolean; |   messageIsStreaming: boolean; | ||||||
|   model: OpenAIModel; |   model: OpenAIModel; | ||||||
|   conversationIsEmpty: boolean; |   conversationIsEmpty: boolean; | ||||||
|   messages: Message[]; |  | ||||||
|   prompts: Prompt[]; |   prompts: Prompt[]; | ||||||
|   onSend: (message: Message) => void; |   onSend: (message: Message, plugin: Plugin | null) => void; | ||||||
|   onRegenerate: () => void; |   onRegenerate: () => void; | ||||||
|   stopConversationRef: MutableRefObject<boolean>; |   stopConversationRef: MutableRefObject<boolean>; | ||||||
|   textareaRef: MutableRefObject<HTMLTextAreaElement | null>; |   textareaRef: MutableRefObject<HTMLTextAreaElement | null>; | ||||||
|  | @ -31,7 +38,6 @@ export const ChatInput: FC<Props> = ({ | ||||||
|   messageIsStreaming, |   messageIsStreaming, | ||||||
|   model, |   model, | ||||||
|   conversationIsEmpty, |   conversationIsEmpty, | ||||||
|   messages, |  | ||||||
|   prompts, |   prompts, | ||||||
|   onSend, |   onSend, | ||||||
|   onRegenerate, |   onRegenerate, | ||||||
|  | @ -47,6 +53,8 @@ export const ChatInput: FC<Props> = ({ | ||||||
|   const [promptInputValue, setPromptInputValue] = useState(''); |   const [promptInputValue, setPromptInputValue] = useState(''); | ||||||
|   const [variables, setVariables] = useState<string[]>([]); |   const [variables, setVariables] = useState<string[]>([]); | ||||||
|   const [isModalVisible, setIsModalVisible] = useState(false); |   const [isModalVisible, setIsModalVisible] = useState(false); | ||||||
|  |   const [showPluginSelect, setShowPluginSelect] = useState(false); | ||||||
|  |   const [plugin, setPlugin] = useState<Plugin | null>(null); | ||||||
| 
 | 
 | ||||||
|   const promptListRef = useRef<HTMLUListElement | null>(null); |   const promptListRef = useRef<HTMLUListElement | null>(null); | ||||||
| 
 | 
 | ||||||
|  | @ -82,8 +90,9 @@ export const ChatInput: FC<Props> = ({ | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     onSend({ role: 'user', content }); |     onSend({ role: 'user', content }, plugin); | ||||||
|     setContent(''); |     setContent(''); | ||||||
|  |     setPlugin(null); | ||||||
| 
 | 
 | ||||||
|     if (window.innerWidth < 640 && textareaRef && textareaRef.current) { |     if (window.innerWidth < 640 && textareaRef && textareaRef.current) { | ||||||
|       textareaRef.current.blur(); |       textareaRef.current.blur(); | ||||||
|  | @ -149,6 +158,9 @@ export const ChatInput: FC<Props> = ({ | ||||||
|     } else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) { |     } else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       handleSend(); |       handleSend(); | ||||||
|  |     } else if (e.key === '/' && e.metaKey) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       setShowPluginSelect(!showPluginSelect); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -214,7 +226,8 @@ export const ChatInput: FC<Props> = ({ | ||||||
|     if (textareaRef && textareaRef.current) { |     if (textareaRef && textareaRef.current) { | ||||||
|       textareaRef.current.style.height = 'inherit'; |       textareaRef.current.style.height = 'inherit'; | ||||||
|       textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; |       textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; | ||||||
|       textareaRef.current.style.overflow = `${textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' |       textareaRef.current.style.overflow = `${ | ||||||
|  |         textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' | ||||||
|       }`;
 |       }`;
 | ||||||
|     } |     } | ||||||
|   }, [content]); |   }, [content]); | ||||||
|  | @ -241,7 +254,7 @@ export const ChatInput: FC<Props> = ({ | ||||||
|       <div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl"> |       <div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl"> | ||||||
|         {messageIsStreaming && ( |         {messageIsStreaming && ( | ||||||
|           <button |           <button | ||||||
|             className="absolute top-0 left-0 right-0 mb-3 md:mb-0 md:mt-2 mx-auto flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white" |             className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2" | ||||||
|             onClick={handleStopConversation} |             onClick={handleStopConversation} | ||||||
|           > |           > | ||||||
|             <IconPlayerStop size={16} /> {t('Stop Generating')} |             <IconPlayerStop size={16} /> {t('Stop Generating')} | ||||||
|  | @ -250,7 +263,7 @@ export const ChatInput: FC<Props> = ({ | ||||||
| 
 | 
 | ||||||
|         {!messageIsStreaming && !conversationIsEmpty && ( |         {!messageIsStreaming && !conversationIsEmpty && ( | ||||||
|           <button |           <button | ||||||
|             className="absolute top-0 left-0 right-0 mb-3 md:mb-0 md:mt-2 mx-auto flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white" |             className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2" | ||||||
|             onClick={onRegenerate} |             onClick={onRegenerate} | ||||||
|           > |           > | ||||||
|             <IconRepeat size={16} /> {t('Regenerate response')} |             <IconRepeat size={16} /> {t('Regenerate response')} | ||||||
|  | @ -258,15 +271,41 @@ export const ChatInput: FC<Props> = ({ | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         <div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4"> |         <div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4"> | ||||||
|  |           <button | ||||||
|  |             className="absolute left-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200" | ||||||
|  |             onClick={() => setShowPluginSelect(!showPluginSelect)} | ||||||
|  |             onKeyDown={(e) => {}} | ||||||
|  |           > | ||||||
|  |             {plugin ? <IconBrandGoogle size={20} /> : <IconBolt size={20} />} | ||||||
|  |           </button> | ||||||
|  | 
 | ||||||
|  |           {showPluginSelect && ( | ||||||
|  |             <div className="absolute left-0 bottom-14 bg-white dark:bg-[#343541]"> | ||||||
|  |               <PluginSelect | ||||||
|  |                 plugin={plugin} | ||||||
|  |                 onPluginChange={(plugin: Plugin) => { | ||||||
|  |                   setPlugin(plugin); | ||||||
|  |                   setShowPluginSelect(false); | ||||||
|  | 
 | ||||||
|  |                   if (textareaRef && textareaRef.current) { | ||||||
|  |                     textareaRef.current.focus(); | ||||||
|  |                   } | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  | 
 | ||||||
|           <textarea |           <textarea | ||||||
|             ref={textareaRef} |             ref={textareaRef} | ||||||
|             className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-2 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-4" |             className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-10 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-10" | ||||||
|             style={{ |             style={{ | ||||||
|               resize: 'none', |               resize: 'none', | ||||||
|               bottom: `${textareaRef?.current?.scrollHeight}px`, |               bottom: `${textareaRef?.current?.scrollHeight}px`, | ||||||
|               maxHeight: '400px', |               maxHeight: '400px', | ||||||
|               overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 |               overflow: `${ | ||||||
|                 ? 'auto' : 'hidden' |                 textareaRef.current && textareaRef.current.scrollHeight > 400 | ||||||
|  |                   ? 'auto' | ||||||
|  |                   : 'hidden' | ||||||
|               }`,
 |               }`,
 | ||||||
|             }} |             }} | ||||||
|             placeholder={ |             placeholder={ | ||||||
|  | @ -279,6 +318,7 @@ export const ChatInput: FC<Props> = ({ | ||||||
|             onChange={handleChange} |             onChange={handleChange} | ||||||
|             onKeyDown={handleKeyDown} |             onKeyDown={handleKeyDown} | ||||||
|           /> |           /> | ||||||
|  | 
 | ||||||
|           <button |           <button | ||||||
|             className="absolute right-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200" |             className="absolute right-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200" | ||||||
|             onClick={handleSend} |             onClick={handleSend} | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ export const ChatLoader: FC<Props> = () => { | ||||||
|       className="group border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100" |       className="group border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100" | ||||||
|       style={{ overflowWrap: 'anywhere' }} |       style={{ overflowWrap: 'anywhere' }} | ||||||
|     > |     > | ||||||
|       <div className="flex gap-4 p-4 m-auto text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> |       <div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> | ||||||
|         <div className="min-w-[40px] text-right font-bold">AI:</div> |         <div className="min-w-[40px] text-right font-bold">AI:</div> | ||||||
|         <IconDots className="animate-pulse" /> |         <IconDots className="animate-pulse" /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | import { Plugin, PluginList } from '@/types/plugin'; | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | import { FC, useEffect, useRef } from 'react'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   plugin: Plugin | null; | ||||||
|  |   onPluginChange: (plugin: Plugin) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const PluginSelect: FC<Props> = ({ plugin, onPluginChange }) => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|  | 
 | ||||||
|  |   const selectRef = useRef<HTMLSelectElement>(null); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (selectRef.current) { | ||||||
|  |       selectRef.current.focus(); | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-col"> | ||||||
|  |       <div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white"> | ||||||
|  |         <select | ||||||
|  |           ref={selectRef} | ||||||
|  |           className="w-full cursor-pointer bg-transparent p-2" | ||||||
|  |           placeholder={t('Select a plugin') || ''} | ||||||
|  |           value={plugin?.id || ''} | ||||||
|  |           onChange={(e) => { | ||||||
|  |             onPluginChange( | ||||||
|  |               PluginList.find( | ||||||
|  |                 (plugin) => plugin.id === e.target.value, | ||||||
|  |               ) as Plugin, | ||||||
|  |             ); | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <option | ||||||
|  |             key="none" | ||||||
|  |             value="" | ||||||
|  |             className="dark:bg-[#343541] dark:text-white" | ||||||
|  |           > | ||||||
|  |             Select Plugin | ||||||
|  |           </option> | ||||||
|  | 
 | ||||||
|  |           {PluginList.map((plugin) => ( | ||||||
|  |             <option | ||||||
|  |               key={plugin.id} | ||||||
|  |               value={plugin.id} | ||||||
|  |               className="dark:bg-[#343541] dark:text-white" | ||||||
|  |             > | ||||||
|  |               {plugin.name} | ||||||
|  |             </option> | ||||||
|  |           ))} | ||||||
|  |         </select> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -19,7 +19,7 @@ export const PromptList: FC<Props> = ({ | ||||||
|   return ( |   return ( | ||||||
|     <ul |     <ul | ||||||
|       ref={promptListRef} |       ref={promptListRef} | ||||||
|       className="z-10 w-full rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] max-h-52 overflow-scroll" |       className="z-10 max-h-52 w-full overflow-scroll rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]" | ||||||
|     > |     > | ||||||
|       {prompts.map((prompt, index) => ( |       {prompts.map((prompt, index) => ( | ||||||
|         <li |         <li | ||||||
|  |  | ||||||
|  | @ -2,11 +2,8 @@ import { Conversation } from '@/types/chat'; | ||||||
| import { KeyValuePair } from '@/types/data'; | import { KeyValuePair } from '@/types/data'; | ||||||
| import { SupportedExportFormats } from '@/types/export'; | import { SupportedExportFormats } from '@/types/export'; | ||||||
| import { Folder } from '@/types/folder'; | import { Folder } from '@/types/folder'; | ||||||
| import { | import { PluginKey } from '@/types/plugin'; | ||||||
|   IconFolderPlus, | import { IconFolderPlus, IconMessagesOff, IconPlus } from '@tabler/icons-react'; | ||||||
|   IconMessagesOff, |  | ||||||
|   IconPlus, |  | ||||||
| } from '@tabler/icons-react'; |  | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { FC, useEffect, useState } from 'react'; | import { FC, useEffect, useState } from 'react'; | ||||||
| import { ChatFolders } from '../Folders/Chat/ChatFolders'; | import { ChatFolders } from '../Folders/Chat/ChatFolders'; | ||||||
|  | @ -20,6 +17,7 @@ interface Props { | ||||||
|   lightMode: 'light' | 'dark'; |   lightMode: 'light' | 'dark'; | ||||||
|   selectedConversation: Conversation; |   selectedConversation: Conversation; | ||||||
|   apiKey: string; |   apiKey: string; | ||||||
|  |   pluginKeys: PluginKey[]; | ||||||
|   folders: Folder[]; |   folders: Folder[]; | ||||||
|   onCreateFolder: (name: string) => void; |   onCreateFolder: (name: string) => void; | ||||||
|   onDeleteFolder: (folderId: string) => void; |   onDeleteFolder: (folderId: string) => void; | ||||||
|  | @ -36,6 +34,8 @@ interface Props { | ||||||
|   onClearConversations: () => void; |   onClearConversations: () => void; | ||||||
|   onExportConversations: () => void; |   onExportConversations: () => void; | ||||||
|   onImportConversations: (data: SupportedExportFormats) => void; |   onImportConversations: (data: SupportedExportFormats) => void; | ||||||
|  |   onPluginKeyChange: (pluginKey: PluginKey) => void; | ||||||
|  |   onClearPluginKey: (pluginKey: PluginKey) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Chatbar: FC<Props> = ({ | export const Chatbar: FC<Props> = ({ | ||||||
|  | @ -44,6 +44,7 @@ export const Chatbar: FC<Props> = ({ | ||||||
|   lightMode, |   lightMode, | ||||||
|   selectedConversation, |   selectedConversation, | ||||||
|   apiKey, |   apiKey, | ||||||
|  |   pluginKeys, | ||||||
|   folders, |   folders, | ||||||
|   onCreateFolder, |   onCreateFolder, | ||||||
|   onDeleteFolder, |   onDeleteFolder, | ||||||
|  | @ -57,6 +58,8 @@ export const Chatbar: FC<Props> = ({ | ||||||
|   onClearConversations, |   onClearConversations, | ||||||
|   onExportConversations, |   onExportConversations, | ||||||
|   onImportConversations, |   onImportConversations, | ||||||
|  |   onPluginKeyChange, | ||||||
|  |   onClearPluginKey, | ||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation('sidebar'); |   const { t } = useTranslation('sidebar'); | ||||||
|   const [searchTerm, setSearchTerm] = useState<string>(''); |   const [searchTerm, setSearchTerm] = useState<string>(''); | ||||||
|  | @ -185,7 +188,7 @@ export const Chatbar: FC<Props> = ({ | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <div className="flex flex-col gap-3 items-center text-sm leading-normal mt-8 text-white opacity-50"> |           <div className="mt-8 flex flex-col items-center gap-3 text-sm leading-normal text-white opacity-50"> | ||||||
|             <IconMessagesOff /> |             <IconMessagesOff /> | ||||||
|             {t('No conversations.')} |             {t('No conversations.')} | ||||||
|           </div> |           </div> | ||||||
|  | @ -195,12 +198,15 @@ export const Chatbar: FC<Props> = ({ | ||||||
|       <ChatbarSettings |       <ChatbarSettings | ||||||
|         lightMode={lightMode} |         lightMode={lightMode} | ||||||
|         apiKey={apiKey} |         apiKey={apiKey} | ||||||
|  |         pluginKeys={pluginKeys} | ||||||
|         conversationsCount={conversations.length} |         conversationsCount={conversations.length} | ||||||
|         onToggleLightMode={onToggleLightMode} |         onToggleLightMode={onToggleLightMode} | ||||||
|         onApiKeyChange={onApiKeyChange} |         onApiKeyChange={onApiKeyChange} | ||||||
|         onClearConversations={onClearConversations} |         onClearConversations={onClearConversations} | ||||||
|         onExportConversations={onExportConversations} |         onExportConversations={onExportConversations} | ||||||
|         onImportConversations={onImportConversations} |         onImportConversations={onImportConversations} | ||||||
|  |         onPluginKeyChange={onPluginKeyChange} | ||||||
|  |         onClearPluginKey={onClearPluginKey} | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { SupportedExportFormats } from '@/types/export'; | import { SupportedExportFormats } from '@/types/export'; | ||||||
|  | import { PluginKey } from '@/types/plugin'; | ||||||
| import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react'; | import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
|  | @ -6,29 +7,37 @@ import { Import } from '../Settings/Import'; | ||||||
| import { Key } from '../Settings/Key'; | import { Key } from '../Settings/Key'; | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; | import { SidebarButton } from '../Sidebar/SidebarButton'; | ||||||
| import { ClearConversations } from './ClearConversations'; | import { ClearConversations } from './ClearConversations'; | ||||||
|  | import { PluginKeys } from './PluginKeys'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   lightMode: 'light' | 'dark'; |   lightMode: 'light' | 'dark'; | ||||||
|   apiKey: string; |   apiKey: string; | ||||||
|  |   pluginKeys: PluginKey[]; | ||||||
|   conversationsCount: number; |   conversationsCount: number; | ||||||
|   onToggleLightMode: (mode: 'light' | 'dark') => void; |   onToggleLightMode: (mode: 'light' | 'dark') => void; | ||||||
|   onApiKeyChange: (apiKey: string) => void; |   onApiKeyChange: (apiKey: string) => void; | ||||||
|   onClearConversations: () => void; |   onClearConversations: () => void; | ||||||
|   onExportConversations: () => void; |   onExportConversations: () => void; | ||||||
|   onImportConversations: (data: SupportedExportFormats) => void; |   onImportConversations: (data: SupportedExportFormats) => void; | ||||||
|  |   onPluginKeyChange: (pluginKey: PluginKey) => void; | ||||||
|  |   onClearPluginKey: (pluginKey: PluginKey) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatbarSettings: FC<Props> = ({ | export const ChatbarSettings: FC<Props> = ({ | ||||||
|   lightMode, |   lightMode, | ||||||
|   apiKey, |   apiKey, | ||||||
|  |   pluginKeys, | ||||||
|   conversationsCount, |   conversationsCount, | ||||||
|   onToggleLightMode, |   onToggleLightMode, | ||||||
|   onApiKeyChange, |   onApiKeyChange, | ||||||
|   onClearConversations, |   onClearConversations, | ||||||
|   onExportConversations, |   onExportConversations, | ||||||
|   onImportConversations, |   onImportConversations, | ||||||
|  |   onPluginKeyChange, | ||||||
|  |   onClearPluginKey, | ||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation('sidebar'); |   const { t } = useTranslation('sidebar'); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col items-center space-y-1 border-t border-white/20 pt-1 text-sm"> |     <div className="flex flex-col items-center space-y-1 border-t border-white/20 pt-1 text-sm"> | ||||||
|       {conversationsCount > 0 ? ( |       {conversationsCount > 0 ? ( | ||||||
|  | @ -54,6 +63,12 @@ export const ChatbarSettings: FC<Props> = ({ | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <Key apiKey={apiKey} onApiKeyChange={onApiKeyChange} /> |       <Key apiKey={apiKey} onApiKeyChange={onApiKeyChange} /> | ||||||
|  | 
 | ||||||
|  |       <PluginKeys | ||||||
|  |         pluginKeys={pluginKeys} | ||||||
|  |         onPluginKeyChange={onPluginKeyChange} | ||||||
|  |         onClearPluginKey={onClearPluginKey} | ||||||
|  |       /> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,232 @@ | ||||||
|  | import { PluginID, PluginKey } from '@/types/plugin'; | ||||||
|  | import { IconKey } from '@tabler/icons-react'; | ||||||
|  | import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { SidebarButton } from '../Sidebar/SidebarButton'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   pluginKeys: PluginKey[]; | ||||||
|  |   onPluginKeyChange: (pluginKey: PluginKey) => void; | ||||||
|  |   onClearPluginKey: (pluginKey: PluginKey) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const PluginKeys: FC<Props> = ({ | ||||||
|  |   pluginKeys, | ||||||
|  |   onPluginKeyChange, | ||||||
|  |   onClearPluginKey, | ||||||
|  | }) => { | ||||||
|  |   const { t } = useTranslation('sidebar'); | ||||||
|  | 
 | ||||||
|  |   const [isChanging, setIsChanging] = useState(false); | ||||||
|  | 
 | ||||||
|  |   const modalRef = useRef<HTMLDivElement>(null); | ||||||
|  | 
 | ||||||
|  |   const handleEnter = (e: KeyboardEvent<HTMLDivElement>) => { | ||||||
|  |     if (e.key === 'Enter' && !e.shiftKey) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       setIsChanging(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const handleMouseDown = (e: MouseEvent) => { | ||||||
|  |       if (modalRef.current && !modalRef.current.contains(e.target as Node)) { | ||||||
|  |         window.addEventListener('mouseup', handleMouseUp); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleMouseUp = (e: MouseEvent) => { | ||||||
|  |       window.removeEventListener('mouseup', handleMouseUp); | ||||||
|  |       setIsChanging(false); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     window.addEventListener('mousedown', handleMouseDown); | ||||||
|  | 
 | ||||||
|  |     return () => { | ||||||
|  |       window.removeEventListener('mousedown', handleMouseDown); | ||||||
|  |     }; | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <SidebarButton | ||||||
|  |         text={t('Plugin Keys')} | ||||||
|  |         icon={<IconKey size={18} />} | ||||||
|  |         onClick={() => setIsChanging(true)} | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       {isChanging && ( | ||||||
|  |         <div | ||||||
|  |           className="z-100 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50" | ||||||
|  |           onKeyDown={handleEnter} | ||||||
|  |         > | ||||||
|  |           <div className="fixed inset-0 z-10 overflow-y-auto"> | ||||||
|  |             <div className="flex min-h-screen items-center justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0"> | ||||||
|  |               <div | ||||||
|  |                 className="hidden sm:inline-block sm:h-screen sm:align-middle" | ||||||
|  |                 aria-hidden="true" | ||||||
|  |               /> | ||||||
|  | 
 | ||||||
|  |               <div | ||||||
|  |                 ref={modalRef} | ||||||
|  |                 className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle" | ||||||
|  |                 role="dialog" | ||||||
|  |               > | ||||||
|  |                 <div className="mb-10 text-4xl">Plugin Keys</div> | ||||||
|  | 
 | ||||||
|  |                 <div className="mt-6 rounded border p-4"> | ||||||
|  |                   <div className="text-xl font-bold">Google Search Plugin</div> | ||||||
|  |                   <div className="mt-4 italic"> | ||||||
|  |                     Please enter your Google API Key and Google CSE ID to enable | ||||||
|  |                     the Google Search Plugin. | ||||||
|  |                   </div> | ||||||
|  | 
 | ||||||
|  |                   <div className="mt-6 text-sm font-bold text-black dark:text-neutral-200"> | ||||||
|  |                     Google API Key | ||||||
|  |                   </div> | ||||||
|  |                   <input | ||||||
|  |                     className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100" | ||||||
|  |                     type="password" | ||||||
|  |                     value={ | ||||||
|  |                       pluginKeys | ||||||
|  |                         .find((p) => p.pluginId === PluginID.GOOGLE_SEARCH) | ||||||
|  |                         ?.requiredKeys.find((k) => k.key === 'GOOGLE_API_KEY') | ||||||
|  |                         ?.value | ||||||
|  |                     } | ||||||
|  |                     onChange={(e) => { | ||||||
|  |                       const pluginKey = pluginKeys.find( | ||||||
|  |                         (p) => p.pluginId === PluginID.GOOGLE_SEARCH, | ||||||
|  |                       ); | ||||||
|  | 
 | ||||||
|  |                       if (pluginKey) { | ||||||
|  |                         const requiredKey = pluginKey.requiredKeys.find( | ||||||
|  |                           (k) => k.key === 'GOOGLE_API_KEY', | ||||||
|  |                         ); | ||||||
|  | 
 | ||||||
|  |                         if (requiredKey) { | ||||||
|  |                           const updatedPluginKey = { | ||||||
|  |                             ...pluginKey, | ||||||
|  |                             requiredKeys: pluginKey.requiredKeys.map((k) => { | ||||||
|  |                               if (k.key === 'GOOGLE_API_KEY') { | ||||||
|  |                                 return { | ||||||
|  |                                   ...k, | ||||||
|  |                                   value: e.target.value, | ||||||
|  |                                 }; | ||||||
|  |                               } | ||||||
|  | 
 | ||||||
|  |                               return k; | ||||||
|  |                             }), | ||||||
|  |                           }; | ||||||
|  | 
 | ||||||
|  |                           onPluginKeyChange(updatedPluginKey); | ||||||
|  |                         } | ||||||
|  |                       } else { | ||||||
|  |                         const newPluginKey: PluginKey = { | ||||||
|  |                           pluginId: PluginID.GOOGLE_SEARCH, | ||||||
|  |                           requiredKeys: [ | ||||||
|  |                             { | ||||||
|  |                               key: 'GOOGLE_API_KEY', | ||||||
|  |                               value: e.target.value, | ||||||
|  |                             }, | ||||||
|  |                             { | ||||||
|  |                               key: 'GOOGLE_CSE_ID', | ||||||
|  |                               value: '', | ||||||
|  |                             }, | ||||||
|  |                           ], | ||||||
|  |                         }; | ||||||
|  | 
 | ||||||
|  |                         onPluginKeyChange(newPluginKey); | ||||||
|  |                       } | ||||||
|  |                     }} | ||||||
|  |                   /> | ||||||
|  | 
 | ||||||
|  |                   <div className="mt-6 text-sm font-bold text-black dark:text-neutral-200"> | ||||||
|  |                     Google CSE ID | ||||||
|  |                   </div> | ||||||
|  |                   <input | ||||||
|  |                     className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100" | ||||||
|  |                     type="password" | ||||||
|  |                     value={ | ||||||
|  |                       pluginKeys | ||||||
|  |                         .find((p) => p.pluginId === PluginID.GOOGLE_SEARCH) | ||||||
|  |                         ?.requiredKeys.find((k) => k.key === 'GOOGLE_CSE_ID') | ||||||
|  |                         ?.value | ||||||
|  |                     } | ||||||
|  |                     onChange={(e) => { | ||||||
|  |                       const pluginKey = pluginKeys.find( | ||||||
|  |                         (p) => p.pluginId === PluginID.GOOGLE_SEARCH, | ||||||
|  |                       ); | ||||||
|  | 
 | ||||||
|  |                       if (pluginKey) { | ||||||
|  |                         const requiredKey = pluginKey.requiredKeys.find( | ||||||
|  |                           (k) => k.key === 'GOOGLE_CSE_ID', | ||||||
|  |                         ); | ||||||
|  | 
 | ||||||
|  |                         if (requiredKey) { | ||||||
|  |                           const updatedPluginKey = { | ||||||
|  |                             ...pluginKey, | ||||||
|  |                             requiredKeys: pluginKey.requiredKeys.map((k) => { | ||||||
|  |                               if (k.key === 'GOOGLE_CSE_ID') { | ||||||
|  |                                 return { | ||||||
|  |                                   ...k, | ||||||
|  |                                   value: e.target.value, | ||||||
|  |                                 }; | ||||||
|  |                               } | ||||||
|  | 
 | ||||||
|  |                               return k; | ||||||
|  |                             }), | ||||||
|  |                           }; | ||||||
|  | 
 | ||||||
|  |                           onPluginKeyChange(updatedPluginKey); | ||||||
|  |                         } | ||||||
|  |                       } else { | ||||||
|  |                         const newPluginKey: PluginKey = { | ||||||
|  |                           pluginId: PluginID.GOOGLE_SEARCH, | ||||||
|  |                           requiredKeys: [ | ||||||
|  |                             { | ||||||
|  |                               key: 'GOOGLE_API_KEY', | ||||||
|  |                               value: '', | ||||||
|  |                             }, | ||||||
|  |                             { | ||||||
|  |                               key: 'GOOGLE_CSE_ID', | ||||||
|  |                               value: e.target.value, | ||||||
|  |                             }, | ||||||
|  |                           ], | ||||||
|  |                         }; | ||||||
|  | 
 | ||||||
|  |                         onPluginKeyChange(newPluginKey); | ||||||
|  |                       } | ||||||
|  |                     }} | ||||||
|  |                   /> | ||||||
|  | 
 | ||||||
|  |                   <button | ||||||
|  |                     className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300" | ||||||
|  |                     onClick={() => { | ||||||
|  |                       const pluginKey = pluginKeys.find( | ||||||
|  |                         (p) => p.pluginId === PluginID.GOOGLE_SEARCH, | ||||||
|  |                       ); | ||||||
|  | 
 | ||||||
|  |                       if (pluginKey) { | ||||||
|  |                         onClearPluginKey(pluginKey); | ||||||
|  |                       } | ||||||
|  |                     }} | ||||||
|  |                   > | ||||||
|  |                     Clear Google Search Plugin Keys | ||||||
|  |                   </button> | ||||||
|  |                 </div> | ||||||
|  | 
 | ||||||
|  |                 <button | ||||||
|  |                   type="button" | ||||||
|  |                   className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300" | ||||||
|  |                   onClick={() => setIsChanging(false)} | ||||||
|  |                 > | ||||||
|  |                   {t('Save')} | ||||||
|  |                 </button> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | # Google Search Tool | ||||||
|  | 
 | ||||||
|  | Use the Google Search API to search the web in Chatbot UI. | ||||||
|  | 
 | ||||||
|  | ## How To Enable | ||||||
|  | 
 | ||||||
|  | 1. Create a new project at https://console.developers.google.com/apis/dashboard | ||||||
|  | 
 | ||||||
|  | 2. Create a new API key at https://console.developers.google.com/apis/credentials | ||||||
|  | 
 | ||||||
|  | 3. Enable the Custom Search API at https://console.developers.google.com/apis/library/customsearch.googleapis.com | ||||||
|  | 
 | ||||||
|  | 4. Create a new Custom Search Engine at https://cse.google.com/cse/all | ||||||
|  | 
 | ||||||
|  | 5. Add your API Key and your Custom Search Engine ID to your .env.local file | ||||||
|  | 
 | ||||||
|  | 6. You can now select the Google Search Tool in the search tools dropdown | ||||||
|  | 
 | ||||||
|  | ## Usage Limits | ||||||
|  | 
 | ||||||
|  | Google gives you 100 free searches per day. You can increase this limit by creating a billing account. | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -31,7 +31,9 @@ | ||||||
|     "uuid": "^9.0.0" |     "uuid": "^9.0.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@mozilla/readability": "^0.4.4", | ||||||
|     "@tailwindcss/typography": "^0.5.9", |     "@tailwindcss/typography": "^0.5.9", | ||||||
|  |     "@types/jsdom": "^21.1.1", | ||||||
|     "@types/node": "18.15.0", |     "@types/node": "18.15.0", | ||||||
|     "@types/react": "18.0.28", |     "@types/react": "18.0.28", | ||||||
|     "@types/react-dom": "18.0.11", |     "@types/react-dom": "18.0.11", | ||||||
|  | @ -39,8 +41,11 @@ | ||||||
|     "@types/uuid": "^9.0.1", |     "@types/uuid": "^9.0.1", | ||||||
|     "@vitest/coverage-c8": "^0.29.7", |     "@vitest/coverage-c8": "^0.29.7", | ||||||
|     "autoprefixer": "^10.4.14", |     "autoprefixer": "^10.4.14", | ||||||
|  |     "endent": "^2.1.0", | ||||||
|     "eslint": "8.36.0", |     "eslint": "8.36.0", | ||||||
|     "eslint-config-next": "13.2.4", |     "eslint-config-next": "13.2.4", | ||||||
|  |     "gpt-3-encoder": "^1.1.4", | ||||||
|  |     "jsdom": "^21.1.1", | ||||||
|     "postcss": "^8.4.21", |     "postcss": "^8.4.21", | ||||||
|     "prettier": "^2.8.7", |     "prettier": "^2.8.7", | ||||||
|     "prettier-plugin-tailwindcss": "^0.2.5", |     "prettier-plugin-tailwindcss": "^0.2.5", | ||||||
|  |  | ||||||
|  | @ -0,0 +1,130 @@ | ||||||
|  | import { ChatBody, Message } from '@/types/chat'; | ||||||
|  | import { GoogleSource } from '@/types/google'; | ||||||
|  | import { OPENAI_API_HOST } from '@/utils/app/const'; | ||||||
|  | import { cleanSourceText } from '@/utils/server/google'; | ||||||
|  | import { Readability } from '@mozilla/readability'; | ||||||
|  | import endent from 'endent'; | ||||||
|  | import jsdom, { JSDOM } from 'jsdom'; | ||||||
|  | import { NextApiRequest, NextApiResponse } from 'next'; | ||||||
|  | 
 | ||||||
|  | const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => { | ||||||
|  |   try { | ||||||
|  |     const { messages, key, model } = req.body as ChatBody; | ||||||
|  | 
 | ||||||
|  |     const userMessage = messages[messages.length - 1]; | ||||||
|  | 
 | ||||||
|  |     const googleRes = await fetch( | ||||||
|  |       `https://customsearch.googleapis.com/customsearch/v1?key=${ | ||||||
|  |         process.env.GOOGLE_API_KEY | ||||||
|  |       }&cx=${process.env.GOOGLE_CSE_ID}&q=${userMessage.content.trim()}&num=5`,
 | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const googleData = await googleRes.json(); | ||||||
|  | 
 | ||||||
|  |     const sources: GoogleSource[] = googleData.items.map((item: any) => ({ | ||||||
|  |       title: item.title, | ||||||
|  |       link: item.link, | ||||||
|  |       displayLink: item.displayLink, | ||||||
|  |       snippet: item.snippet, | ||||||
|  |       image: item.pagemap?.cse_image?.[0]?.src, | ||||||
|  |       text: '', | ||||||
|  |     })); | ||||||
|  | 
 | ||||||
|  |     const sourcesWithText: any = await Promise.all( | ||||||
|  |       sources.map(async (source) => { | ||||||
|  |         try { | ||||||
|  |           const res = await fetch(source.link); | ||||||
|  |           const html = await res.text(); | ||||||
|  | 
 | ||||||
|  |           const virtualConsole = new jsdom.VirtualConsole(); | ||||||
|  |           virtualConsole.on('error', (error) => { | ||||||
|  |             if (!error.message.includes('Could not parse CSS stylesheet')) { | ||||||
|  |               console.error(error); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  | 
 | ||||||
|  |           const dom = new JSDOM(html, { virtualConsole }); | ||||||
|  |           const doc = dom.window.document; | ||||||
|  |           const parsed = new Readability(doc).parse(); | ||||||
|  | 
 | ||||||
|  |           if (parsed) { | ||||||
|  |             let sourceText = cleanSourceText(parsed.textContent); | ||||||
|  | 
 | ||||||
|  |             return { | ||||||
|  |               ...source, | ||||||
|  |               // TODO: switch to tokens
 | ||||||
|  |               text: sourceText.slice(0, 2000), | ||||||
|  |             } as GoogleSource; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         } catch (error) { | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |       }), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const filteredSources: GoogleSource[] = sourcesWithText.filter(Boolean); | ||||||
|  | 
 | ||||||
|  |     const answerPrompt = endent` | ||||||
|  |     Provide me with the information I requested. Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as a markdown link as you use them at the end of each sentence by number of the source (ex: [[1]](link.com)). Provide an accurate response and then stop. Today's date is ${new Date().toLocaleDateString()}. | ||||||
|  | 
 | ||||||
|  |     Example Input: | ||||||
|  |     What's the weather in San Francisco today? | ||||||
|  | 
 | ||||||
|  |     Example Sources: | ||||||
|  |     [Weather in San Francisco](https://www.google.com/search?q=weather+san+francisco)
 | ||||||
|  | 
 | ||||||
|  |     Example Response: | ||||||
|  |     It's 70 degrees and sunny in San Francisco today. [[1]](https://www.google.com/search?q=weather+san+francisco)
 | ||||||
|  | 
 | ||||||
|  |     Input: | ||||||
|  |     ${userMessage.content.trim()} | ||||||
|  | 
 | ||||||
|  |     Sources: | ||||||
|  |     ${filteredSources.map((source) => { | ||||||
|  |       return endent` | ||||||
|  |       ${source.title} (${source.link}): | ||||||
|  |       ${source.text} | ||||||
|  |       `;
 | ||||||
|  |     })} | ||||||
|  | 
 | ||||||
|  |     Response: | ||||||
|  |     `;
 | ||||||
|  | 
 | ||||||
|  |     const answerMessage: Message = { role: 'user', content: answerPrompt }; | ||||||
|  | 
 | ||||||
|  |     const answerRes = await fetch(`${OPENAI_API_HOST}/v1/chat/completions`, { | ||||||
|  |       headers: { | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |         Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`, | ||||||
|  |         ...(process.env.OPENAI_ORGANIZATION && { | ||||||
|  |           'OpenAI-Organization': process.env.OPENAI_ORGANIZATION, | ||||||
|  |         }), | ||||||
|  |       }, | ||||||
|  |       method: 'POST', | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         model: model.id, | ||||||
|  |         messages: [ | ||||||
|  |           { | ||||||
|  |             role: 'system', | ||||||
|  |             content: `Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as [1](link), etc, as you use them.`, | ||||||
|  |           }, | ||||||
|  |           answerMessage, | ||||||
|  |         ], | ||||||
|  |         max_tokens: 1000, | ||||||
|  |         temperature: 1, | ||||||
|  |         stream: false, | ||||||
|  |       }), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const { choices: choices2 } = await answerRes.json(); | ||||||
|  |     const answer = choices2[0].message.content; | ||||||
|  | 
 | ||||||
|  |     res.status(200).json({ answer }); | ||||||
|  |   } catch (error) { | ||||||
|  |     return new Response('Error', { status: 500 }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default handler; | ||||||
							
								
								
									
										127
									
								
								pages/index.tsx
								
								
								
								
							
							
						
						
									
										127
									
								
								pages/index.tsx
								
								
								
								
							|  | @ -8,12 +8,14 @@ import { ErrorMessage } from '@/types/error'; | ||||||
| import { LatestExportFormat, SupportedExportFormats } from '@/types/export'; | import { LatestExportFormat, SupportedExportFormats } from '@/types/export'; | ||||||
| import { Folder, FolderType } from '@/types/folder'; | import { Folder, FolderType } from '@/types/folder'; | ||||||
| import { | import { | ||||||
|   fallbackModelID, |  | ||||||
|   OpenAIModel, |   OpenAIModel, | ||||||
|   OpenAIModelID, |   OpenAIModelID, | ||||||
|   OpenAIModels, |   OpenAIModels, | ||||||
|  |   fallbackModelID, | ||||||
| } from '@/types/openai'; | } from '@/types/openai'; | ||||||
|  | import { Plugin, PluginKey } from '@/types/plugin'; | ||||||
| import { Prompt } from '@/types/prompt'; | import { Prompt } from '@/types/prompt'; | ||||||
|  | import { getEndpoint } from '@/utils/app/api'; | ||||||
| import { | import { | ||||||
|   cleanConversationHistory, |   cleanConversationHistory, | ||||||
|   cleanSelectedConversation, |   cleanSelectedConversation, | ||||||
|  | @ -33,16 +35,18 @@ import { useTranslation } from 'next-i18next'; | ||||||
| import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; | import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; | ||||||
| import Head from 'next/head'; | import Head from 'next/head'; | ||||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||||
| import { v4 as uuidv4 } from 'uuid'; |  | ||||||
| import toast from 'react-hot-toast'; | import toast from 'react-hot-toast'; | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
| 
 | 
 | ||||||
| interface HomeProps { | interface HomeProps { | ||||||
|   serverSideApiKeyIsSet: boolean; |   serverSideApiKeyIsSet: boolean; | ||||||
|  |   serverSidePluginKeysSet: boolean; | ||||||
|   defaultModelId: OpenAIModelID; |   defaultModelId: OpenAIModelID; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Home: React.FC<HomeProps> = ({ | const Home: React.FC<HomeProps> = ({ | ||||||
|   serverSideApiKeyIsSet, |   serverSideApiKeyIsSet, | ||||||
|  |   serverSidePluginKeysSet, | ||||||
|   defaultModelId, |   defaultModelId, | ||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation('chat'); |   const { t } = useTranslation('chat'); | ||||||
|  | @ -50,6 +54,7 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|   // STATE ----------------------------------------------
 |   // STATE ----------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|   const [apiKey, setApiKey] = useState<string>(''); |   const [apiKey, setApiKey] = useState<string>(''); | ||||||
|  |   const [pluginKeys, setPluginKeys] = useState<PluginKey[]>([]); | ||||||
|   const [loading, setLoading] = useState<boolean>(false); |   const [loading, setLoading] = useState<boolean>(false); | ||||||
|   const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark'); |   const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark'); | ||||||
|   const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false); |   const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false); | ||||||
|  | @ -76,7 +81,11 @@ const Home: React.FC<HomeProps> = ({ | ||||||
| 
 | 
 | ||||||
|   // FETCH RESPONSE ----------------------------------------------
 |   // FETCH RESPONSE ----------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|   const handleSend = async (message: Message, deleteCount = 0) => { |   const handleSend = async ( | ||||||
|  |     message: Message, | ||||||
|  |     deleteCount = 0, | ||||||
|  |     plugin: Plugin | null = null, | ||||||
|  |   ) => { | ||||||
|     if (selectedConversation) { |     if (selectedConversation) { | ||||||
|       let updatedConversation: Conversation; |       let updatedConversation: Conversation; | ||||||
| 
 | 
 | ||||||
|  | @ -108,8 +117,10 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|         prompt: updatedConversation.prompt, |         prompt: updatedConversation.prompt, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|  |       const endpoint = getEndpoint(plugin); | ||||||
|  | 
 | ||||||
|       const controller = new AbortController(); |       const controller = new AbortController(); | ||||||
|       const response = await fetch('/api/chat', { |       const response = await fetch(endpoint, { | ||||||
|         method: 'POST', |         method: 'POST', | ||||||
|         headers: { |         headers: { | ||||||
|           'Content-Type': 'application/json', |           'Content-Type': 'application/json', | ||||||
|  | @ -133,6 +144,7 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (!plugin) { | ||||||
|         if (updatedConversation.messages.length === 1) { |         if (updatedConversation.messages.length === 1) { | ||||||
|           const { content } = message; |           const { content } = message; | ||||||
|           const customName = |           const customName = | ||||||
|  | @ -217,10 +229,45 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         setConversations(updatedConversations); |         setConversations(updatedConversations); | ||||||
| 
 |  | ||||||
|         saveConversations(updatedConversations); |         saveConversations(updatedConversations); | ||||||
| 
 | 
 | ||||||
|         setMessageIsStreaming(false); |         setMessageIsStreaming(false); | ||||||
|  |       } else { | ||||||
|  |         const { answer } = await response.json(); | ||||||
|  | 
 | ||||||
|  |         const updatedMessages: Message[] = [ | ||||||
|  |           ...updatedConversation.messages, | ||||||
|  |           { role: 'assistant', content: answer }, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         updatedConversation = { | ||||||
|  |           ...updatedConversation, | ||||||
|  |           messages: updatedMessages, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         setSelectedConversation(updatedConversation); | ||||||
|  |         saveConversation(updatedConversation); | ||||||
|  | 
 | ||||||
|  |         const updatedConversations: Conversation[] = conversations.map( | ||||||
|  |           (conversation) => { | ||||||
|  |             if (conversation.id === selectedConversation.id) { | ||||||
|  |               return updatedConversation; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return conversation; | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if (updatedConversations.length === 0) { | ||||||
|  |           updatedConversations.push(updatedConversation); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setConversations(updatedConversations); | ||||||
|  |         saveConversations(updatedConversations); | ||||||
|  | 
 | ||||||
|  |         setLoading(false); | ||||||
|  |         setMessageIsStreaming(false); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -283,6 +330,45 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|     localStorage.setItem('apiKey', apiKey); |     localStorage.setItem('apiKey', apiKey); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const handlePluginKeyChange = (pluginKey: PluginKey) => { | ||||||
|  |     if (pluginKeys.some((key) => key.pluginId === pluginKey.pluginId)) { | ||||||
|  |       const updatedPluginKeys = pluginKeys.map((key) => { | ||||||
|  |         if (key.pluginId === pluginKey.pluginId) { | ||||||
|  |           return pluginKey; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return key; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       setPluginKeys(updatedPluginKeys); | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys)); | ||||||
|  |     } else { | ||||||
|  |       setPluginKeys([...pluginKeys, pluginKey]); | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem( | ||||||
|  |         'pluginKeys', | ||||||
|  |         JSON.stringify([...pluginKeys, pluginKey]), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleClearPluginKey = (pluginKey: PluginKey) => { | ||||||
|  |     const updatedPluginKeys = pluginKeys.filter( | ||||||
|  |       (key) => key.pluginId !== pluginKey.pluginId, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (updatedPluginKeys.length === 0) { | ||||||
|  |       setPluginKeys([]); | ||||||
|  |       localStorage.removeItem('pluginKeys'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     setPluginKeys(updatedPluginKeys); | ||||||
|  | 
 | ||||||
|  |     localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const handleToggleChatbar = () => { |   const handleToggleChatbar = () => { | ||||||
|     setShowSidebar(!showSidebar); |     setShowSidebar(!showSidebar); | ||||||
|     localStorage.setItem('showChatbar', JSON.stringify(!showSidebar)); |     localStorage.setItem('showChatbar', JSON.stringify(!showSidebar)); | ||||||
|  | @ -496,8 +582,6 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|   // PROMPT OPERATIONS --------------------------------------------
 |   // PROMPT OPERATIONS --------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|   const handleCreatePrompt = () => { |   const handleCreatePrompt = () => { | ||||||
|     const lastPrompt = prompts[prompts.length - 1]; |  | ||||||
| 
 |  | ||||||
|     const newPrompt: Prompt = { |     const newPrompt: Prompt = { | ||||||
|       id: uuidv4(), |       id: uuidv4(), | ||||||
|       name: `Prompt ${prompts.length + 1}`, |       name: `Prompt ${prompts.length + 1}`, | ||||||
|  | @ -562,11 +646,21 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const apiKey = localStorage.getItem('apiKey'); |     const apiKey = localStorage.getItem('apiKey'); | ||||||
|     if (apiKey) { |     if (serverSideApiKeyIsSet) { | ||||||
|  |       fetchModels(''); | ||||||
|  |       setApiKey(''); | ||||||
|  |       localStorage.removeItem('apiKey'); | ||||||
|  |     } else if (apiKey) { | ||||||
|       setApiKey(apiKey); |       setApiKey(apiKey); | ||||||
|       fetchModels(apiKey); |       fetchModels(apiKey); | ||||||
|     } else if (serverSideApiKeyIsSet) { |     } | ||||||
|       fetchModels(''); | 
 | ||||||
|  |     const pluginKeys = localStorage.getItem('pluginKeys'); | ||||||
|  |     if (serverSidePluginKeysSet) { | ||||||
|  |       setPluginKeys([]); | ||||||
|  |       localStorage.removeItem('pluginKeys'); | ||||||
|  |     } else if (pluginKeys) { | ||||||
|  |       setPluginKeys(JSON.parse(pluginKeys)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (window.innerWidth < 640) { |     if (window.innerWidth < 640) { | ||||||
|  | @ -654,6 +748,7 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|                   lightMode={lightMode} |                   lightMode={lightMode} | ||||||
|                   selectedConversation={selectedConversation} |                   selectedConversation={selectedConversation} | ||||||
|                   apiKey={apiKey} |                   apiKey={apiKey} | ||||||
|  |                   pluginKeys={pluginKeys} | ||||||
|                   folders={folders.filter((folder) => folder.type === 'chat')} |                   folders={folders.filter((folder) => folder.type === 'chat')} | ||||||
|                   onToggleLightMode={handleLightMode} |                   onToggleLightMode={handleLightMode} | ||||||
|                   onCreateFolder={(name) => handleCreateFolder(name, 'chat')} |                   onCreateFolder={(name) => handleCreateFolder(name, 'chat')} | ||||||
|  | @ -667,6 +762,8 @@ const Home: React.FC<HomeProps> = ({ | ||||||
|                   onClearConversations={handleClearConversations} |                   onClearConversations={handleClearConversations} | ||||||
|                   onExportConversations={handleExportData} |                   onExportConversations={handleExportData} | ||||||
|                   onImportConversations={handleImportConversations} |                   onImportConversations={handleImportConversations} | ||||||
|  |                   onPluginKeyChange={handlePluginKeyChange} | ||||||
|  |                   onClearPluginKey={handleClearPluginKey} | ||||||
|                 /> |                 /> | ||||||
| 
 | 
 | ||||||
|                 <button |                 <button | ||||||
|  | @ -755,10 +852,20 @@ export const getServerSideProps: GetServerSideProps = async ({ locale }) => { | ||||||
|       process.env.DEFAULT_MODEL) || |       process.env.DEFAULT_MODEL) || | ||||||
|     fallbackModelID; |     fallbackModelID; | ||||||
| 
 | 
 | ||||||
|  |   let serverSidePluginKeysSet = false; | ||||||
|  | 
 | ||||||
|  |   const googleApiKey = process.env.GOOGLE_API_KEY; | ||||||
|  |   const googleCSEId = process.env.GOOGLE_CSE_ID; | ||||||
|  | 
 | ||||||
|  |   if (googleApiKey && googleCSEId) { | ||||||
|  |     serverSidePluginKeysSet = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   return { |   return { | ||||||
|     props: { |     props: { | ||||||
|       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, |       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, | ||||||
|       defaultModelId, |       defaultModelId, | ||||||
|  |       serverSidePluginKeysSet, | ||||||
|       ...(await serverSideTranslations(locale ?? 'en', [ |       ...(await serverSideTranslations(locale ?? 'en', [ | ||||||
|         'common', |         'common', | ||||||
|         'chat', |         'chat', | ||||||
|  |  | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | import { Message } from './chat'; | ||||||
|  | 
 | ||||||
|  | export interface GoogleResponse { | ||||||
|  |   message: Message; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface GoogleSource { | ||||||
|  |   title: string; | ||||||
|  |   link: string; | ||||||
|  |   displayLink: string; | ||||||
|  |   snippet: string; | ||||||
|  |   image: string; | ||||||
|  |   text: string; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | import { KeyValuePair } from './data'; | ||||||
|  | 
 | ||||||
|  | export interface Plugin { | ||||||
|  |   id: PluginID; | ||||||
|  |   name: PluginName; | ||||||
|  |   requiredKeys: KeyValuePair[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface PluginKey { | ||||||
|  |   pluginId: PluginID; | ||||||
|  |   requiredKeys: KeyValuePair[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export enum PluginID { | ||||||
|  |   GOOGLE_SEARCH = 'google-search', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export enum PluginName { | ||||||
|  |   GOOGLE_SEARCH = 'Google Search', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Plugins: Record<PluginID, Plugin> = { | ||||||
|  |   [PluginID.GOOGLE_SEARCH]: { | ||||||
|  |     id: PluginID.GOOGLE_SEARCH, | ||||||
|  |     name: PluginName.GOOGLE_SEARCH, | ||||||
|  |     requiredKeys: [ | ||||||
|  |       { | ||||||
|  |         key: 'GOOGLE_API_KEY', | ||||||
|  |         value: '', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'GOOGLE_CSE_ID', | ||||||
|  |         value: '', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const PluginList = Object.values(Plugins); | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Conversation } from './chat'; | import { Conversation } from './chat'; | ||||||
| import { Folder } from './folder'; | import { Folder } from './folder'; | ||||||
|  | import { PluginKey } from './plugin'; | ||||||
| import { Prompt } from './prompt'; | import { Prompt } from './prompt'; | ||||||
| 
 | 
 | ||||||
| // keep track of local storage schema
 | // keep track of local storage schema
 | ||||||
|  | @ -15,4 +16,6 @@ export interface LocalStorage { | ||||||
|   // added showChatbar and showPromptbar (3/26/23)
 |   // added showChatbar and showPromptbar (3/26/23)
 | ||||||
|   showChatbar: boolean; |   showChatbar: boolean; | ||||||
|   showPromptbar: boolean; |   showPromptbar: boolean; | ||||||
|  |   // added plugin keys (4/3/23)
 | ||||||
|  |   pluginKeys: PluginKey[]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | import { Plugin, PluginID } from '@/types/plugin'; | ||||||
|  | 
 | ||||||
|  | export const getEndpoint = (plugin: Plugin | null) => { | ||||||
|  |   if (!plugin) { | ||||||
|  |     return 'api/chat'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (plugin.id === PluginID.GOOGLE_SEARCH) { | ||||||
|  |     return 'api/google'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return 'api/chat'; | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | export const cleanSourceText = (text: string) => { | ||||||
|  |   return text | ||||||
|  |     .trim() | ||||||
|  |     .replace(/(\n){4,}/g, '\n\n\n') | ||||||
|  |     .replace(/\n\n/g, ' ') | ||||||
|  |     .replace(/ {3,}/g, '  ') | ||||||
|  |     .replace(/\t/g, '') | ||||||
|  |     .replace(/\n+(\s*\n)*/g, '\n'); | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue