MAJOR REFACTOR (#494)
* move index to home folder, create state and context files and barrell folder * Sanity Check Commit: reducer added to home.tsx manual QA all working * WIP: promptBar * fix missing json parse on folders and prompts * split context and add promptbar context * add context to nested prompt componets and componetize Folder componet * remove log * Create buttons folder and componetize sidebar action button * tidy up prompt handlers * componetized sidebar * added back chatbar componet to left side sidebar * monster commit: Componetized the common code between chatbar and promptbar into new componet Sidebar and added context to both bars * add useFetch service * added prettier import sort to keep imports ordered and easier to indentify * added react query and useFetch to work with RQ * added apiService, errorService and reactQuery * add callback and tidy up error service * refactor chat and child componets to useContext * fix extra calls and bad calls to mel endpoint * minor import cleanup --------- Co-authored-by: jc.durbin <jc.durbin@ardanis.com>
This commit is contained in:
		
							parent
							
								
									68c9cd4bd8
								
							
						
					
					
						commit
						6500db9c1c
					
				|  | @ -7,7 +7,7 @@ name: Docker | ||||||
| 
 | 
 | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ "main" ] |     branches: ['main'] | ||||||
| 
 | 
 | ||||||
| env: | env: | ||||||
|   # Use docker.io for Docker Hub if empty |   # Use docker.io for Docker Hub if empty | ||||||
|  | @ -15,10 +15,8 @@ env: | ||||||
|   # github.repository as <account>/<repo> |   # github.repository as <account>/<repo> | ||||||
|   IMAGE_NAME: ${{ github.repository }} |   IMAGE_NAME: ${{ github.repository }} | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
| 
 |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|  |  | ||||||
|  | @ -14,11 +14,11 @@ jobs: | ||||||
|       image: node:16 |       image: node:16 | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|     - name: Checkout code |       - name: Checkout code | ||||||
|       uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
| 
 | 
 | ||||||
|     - name: Install dependencies |       - name: Install dependencies | ||||||
|       run: npm ci |         run: npm ci | ||||||
| 
 | 
 | ||||||
|     - name: Run Vitest Suite |       - name: Run Vitest Suite | ||||||
|       run: npm test |         run: npm test | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| # Contributing Guidelines | # Contributing Guidelines | ||||||
|  | 
 | ||||||
| **Welcome to Chatbot UI!** | **Welcome to Chatbot UI!** | ||||||
| 
 | 
 | ||||||
| We appreciate your interest in contributing to our project. | We appreciate your interest in contributing to our project. | ||||||
|  | @ -6,6 +7,7 @@ We appreciate your interest in contributing to our project. | ||||||
| Before you get started, please read our guidelines for contributing. | Before you get started, please read our guidelines for contributing. | ||||||
| 
 | 
 | ||||||
| ## Types of Contributions | ## Types of Contributions | ||||||
|  | 
 | ||||||
| We welcome the following types of contributions: | We welcome the following types of contributions: | ||||||
| 
 | 
 | ||||||
| - Bug fixes | - Bug fixes | ||||||
|  | @ -15,8 +17,8 @@ We welcome the following types of contributions: | ||||||
| - Translations | - Translations | ||||||
| - Tests | - Tests | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ## Getting Started | ## Getting Started | ||||||
|  | 
 | ||||||
| To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes. | To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes. | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
|  | @ -29,6 +31,7 @@ git checkout -b my-branch-name | ||||||
| Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines. | Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines. | ||||||
| 
 | 
 | ||||||
| ## Pull Request Process | ## Pull Request Process | ||||||
|  | 
 | ||||||
| 1. Fork the project on GitHub. | 1. Fork the project on GitHub. | ||||||
| 2. Clone your forked repository locally on your machine. | 2. Clone your forked repository locally on your machine. | ||||||
| 3. Create a new branch from the main branch. | 3. Create a new branch from the main branch. | ||||||
|  | @ -38,4 +41,5 @@ Before submitting your pull request, please make sure your changes pass our auto | ||||||
| 7. Submit a pull request to the main branch of the main repository. | 7. Submit a pull request to the main branch of the main repository. | ||||||
| 
 | 
 | ||||||
| ## Contact | ## Contact | ||||||
|  | 
 | ||||||
| If you have any questions or need help getting started, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley). | If you have any questions or need help getting started, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley). | ||||||
|  | @ -1,8 +1,4 @@ | ||||||
| import { ExportFormatV1, ExportFormatV2, ExportFormatV4 } from '@/types/export'; |  | ||||||
| import { OpenAIModels, OpenAIModelID } from '@/types/openai'; |  | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | ||||||
| import { it, describe, expect } from 'vitest'; |  | ||||||
| 
 |  | ||||||
| import { | import { | ||||||
|   cleanData, |   cleanData, | ||||||
|   isExportFormatV1, |   isExportFormatV1, | ||||||
|  | @ -12,6 +8,11 @@ import { | ||||||
|   isLatestExportFormat, |   isLatestExportFormat, | ||||||
| } from '@/utils/app/importExport'; | } from '@/utils/app/importExport'; | ||||||
| 
 | 
 | ||||||
|  | import { ExportFormatV1, ExportFormatV2, ExportFormatV4 } from '@/types/export'; | ||||||
|  | import { OpenAIModelID, OpenAIModels } from '@/types/openai'; | ||||||
|  | 
 | ||||||
|  | import { describe, expect, it } from 'vitest'; | ||||||
|  | 
 | ||||||
| describe('Export Format Functions', () => { | describe('Export Format Functions', () => { | ||||||
|   describe('isExportFormatV1', () => { |   describe('isExportFormatV1', () => { | ||||||
|     it('should return true for v1 format', () => { |     it('should return true for v1 format', () => { | ||||||
|  | @ -105,7 +106,7 @@ describe('cleanData Functions', () => { | ||||||
|           }, |           }, | ||||||
|         ], |         ], | ||||||
|         folders: [], |         folders: [], | ||||||
|         prompts:[] |         prompts: [], | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | @ -253,9 +254,7 @@ describe('cleanData Functions', () => { | ||||||
|             folderId: null, |             folderId: null, | ||||||
|           }, |           }, | ||||||
|         ], |         ], | ||||||
| 
 |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|    |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | import { MouseEventHandler, ReactElement } from 'react'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   handleClick: MouseEventHandler<HTMLButtonElement>; | ||||||
|  |   children: ReactElement; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const SidebarActionButton = ({ handleClick, children }: Props) => ( | ||||||
|  |   <button | ||||||
|  |     className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" | ||||||
|  |     onClick={handleClick} | ||||||
|  |   > | ||||||
|  |     {children} | ||||||
|  |   </button> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default SidebarActionButton; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './SidebarActionButton'; | ||||||
|  | @ -1,22 +1,31 @@ | ||||||
| import { Conversation, Message } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { ErrorMessage } from '@/types/error'; |  | ||||||
| import { OpenAIModel, OpenAIModelID } from '@/types/openai'; |  | ||||||
| import { Plugin } from '@/types/plugin'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| 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 { | import { | ||||||
|   FC, |  | ||||||
|   MutableRefObject, |   MutableRefObject, | ||||||
|   memo, |   memo, | ||||||
|   useCallback, |   useCallback, | ||||||
|  |   useContext, | ||||||
|   useEffect, |   useEffect, | ||||||
|   useRef, |   useRef, | ||||||
|   useState, |   useState, | ||||||
| } from 'react'; | } from 'react'; | ||||||
| import { Spinner } from '../Global/Spinner'; | import toast from 'react-hot-toast'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { getEndpoint } from '@/utils/app/api'; | ||||||
|  | import { | ||||||
|  |   saveConversation, | ||||||
|  |   saveConversations, | ||||||
|  |   updateConversation, | ||||||
|  | } from '@/utils/app/conversation'; | ||||||
|  | import { throttle } from '@/utils/data/throttle'; | ||||||
|  | 
 | ||||||
|  | import { ChatBody, Conversation, Message } from '@/types/chat'; | ||||||
|  | import { Plugin } from '@/types/plugin'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import Spinner from '../Spinner'; | ||||||
| import { ChatInput } from './ChatInput'; | import { ChatInput } from './ChatInput'; | ||||||
| import { ChatLoader } from './ChatLoader'; | import { ChatLoader } from './ChatLoader'; | ||||||
| import { ChatMessage } from './ChatMessage'; | import { ChatMessage } from './ChatMessage'; | ||||||
|  | @ -25,310 +34,466 @@ import { ModelSelect } from './ModelSelect'; | ||||||
| import { SystemPrompt } from './SystemPrompt'; | import { SystemPrompt } from './SystemPrompt'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   conversation: Conversation; |  | ||||||
|   models: OpenAIModel[]; |  | ||||||
|   apiKey: string; |  | ||||||
|   serverSideApiKeyIsSet: boolean; |  | ||||||
|   defaultModelId: OpenAIModelID; |  | ||||||
|   messageIsStreaming: boolean; |  | ||||||
|   modelError: ErrorMessage | null; |  | ||||||
|   loading: boolean; |  | ||||||
|   prompts: Prompt[]; |  | ||||||
|   onSend: ( |  | ||||||
|     message: Message, |  | ||||||
|     deleteCount: number, |  | ||||||
|     plugin: Plugin | null, |  | ||||||
|   ) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
|   onEditMessage: (message: Message, messageIndex: number) => void; |  | ||||||
|   stopConversationRef: MutableRefObject<boolean>; |   stopConversationRef: MutableRefObject<boolean>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Chat: FC<Props> = memo( | export const Chat = memo(({ stopConversationRef }: Props) => { | ||||||
|   ({ |   const { t } = useTranslation('chat'); | ||||||
|     conversation, |  | ||||||
|     models, |  | ||||||
|     apiKey, |  | ||||||
|     serverSideApiKeyIsSet, |  | ||||||
|     defaultModelId, |  | ||||||
|     messageIsStreaming, |  | ||||||
|     modelError, |  | ||||||
|     loading, |  | ||||||
|     prompts, |  | ||||||
|     onSend, |  | ||||||
|     onUpdateConversation, |  | ||||||
|     onEditMessage, |  | ||||||
|     stopConversationRef, |  | ||||||
|   }) => { |  | ||||||
|     const { t } = useTranslation('chat'); |  | ||||||
|     const [currentMessage, setCurrentMessage] = useState<Message>(); |  | ||||||
|     const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true); |  | ||||||
|     const [showSettings, setShowSettings] = useState<boolean>(false); |  | ||||||
|     const [showScrollDownButton, setShowScrollDownButton] = |  | ||||||
|       useState<boolean>(false); |  | ||||||
| 
 | 
 | ||||||
|     const messagesEndRef = useRef<HTMLDivElement>(null); |   const { | ||||||
|     const chatContainerRef = useRef<HTMLDivElement>(null); |     state: { | ||||||
|     const textareaRef = useRef<HTMLTextAreaElement>(null); |       selectedConversation, | ||||||
|  |       conversations, | ||||||
|  |       models, | ||||||
|  |       apiKey, | ||||||
|  |       pluginKeys, | ||||||
|  |       serverSideApiKeyIsSet, | ||||||
|  |       messageIsStreaming, | ||||||
|  |       modelError, | ||||||
|  |       loading, | ||||||
|  |       prompts, | ||||||
|  |     }, | ||||||
|  |     handleUpdateConversation, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |   } = useContext(HomeContext); | ||||||
| 
 | 
 | ||||||
|     const scrollToBottom = useCallback(() => { |   const [currentMessage, setCurrentMessage] = useState<Message>(); | ||||||
|       if (autoScrollEnabled) { |   const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true); | ||||||
|         messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |   const [showSettings, setShowSettings] = useState<boolean>(false); | ||||||
|         textareaRef.current?.focus(); |   const [showScrollDownButton, setShowScrollDownButton] = | ||||||
|       } |     useState<boolean>(false); | ||||||
|     }, [autoScrollEnabled]); |  | ||||||
| 
 | 
 | ||||||
|     const handleScroll = () => { |   const messagesEndRef = useRef<HTMLDivElement>(null); | ||||||
|       if (chatContainerRef.current) { |   const chatContainerRef = useRef<HTMLDivElement>(null); | ||||||
|         const { scrollTop, scrollHeight, clientHeight } = |   const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
|           chatContainerRef.current; |  | ||||||
|         const bottomTolerance = 30; |  | ||||||
| 
 | 
 | ||||||
|         if (scrollTop + clientHeight < scrollHeight - bottomTolerance) { |   const handleSend = useCallback( | ||||||
|           setAutoScrollEnabled(false); |     async (message: Message, deleteCount = 0, plugin: Plugin | null = null) => { | ||||||
|           setShowScrollDownButton(true); |       if (selectedConversation) { | ||||||
|         } else { |         let updatedConversation: Conversation; | ||||||
|           setAutoScrollEnabled(true); |         if (deleteCount) { | ||||||
|           setShowScrollDownButton(false); |           const updatedMessages = [...selectedConversation.messages]; | ||||||
|         } |           for (let i = 0; i < deleteCount; i++) { | ||||||
|       } |             updatedMessages.pop(); | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const handleScrollDown = () => { |  | ||||||
|       chatContainerRef.current?.scrollTo({ |  | ||||||
|         top: chatContainerRef.current.scrollHeight, |  | ||||||
|         behavior: 'smooth', |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const handleSettings = () => { |  | ||||||
|       setShowSettings(!showSettings); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const onClearAll = () => { |  | ||||||
|       if (confirm(t<string>('Are you sure you want to clear all messages?'))) { |  | ||||||
|         onUpdateConversation(conversation, { key: 'messages', value: [] }); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const scrollDown = () => { |  | ||||||
|       if (autoScrollEnabled) { |  | ||||||
|         messagesEndRef.current?.scrollIntoView(true); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|     const throttledScrollDown = throttle(scrollDown, 250); |  | ||||||
| 
 |  | ||||||
|     useEffect(() => { |  | ||||||
|       throttledScrollDown(); |  | ||||||
|       setCurrentMessage( |  | ||||||
|         conversation.messages[conversation.messages.length - 2], |  | ||||||
|       ); |  | ||||||
|     }, [conversation.messages, throttledScrollDown]); |  | ||||||
| 
 |  | ||||||
|     useEffect(() => { |  | ||||||
|       const observer = new IntersectionObserver( |  | ||||||
|         ([entry]) => { |  | ||||||
|           setAutoScrollEnabled(entry.isIntersecting); |  | ||||||
|           if (entry.isIntersecting) { |  | ||||||
|             textareaRef.current?.focus(); |  | ||||||
|           } |           } | ||||||
|         }, |           updatedConversation = { | ||||||
|         { |             ...selectedConversation, | ||||||
|           root: null, |             messages: [...updatedMessages, message], | ||||||
|           threshold: 0.5, |           }; | ||||||
|         }, |         } else { | ||||||
|       ); |           updatedConversation = { | ||||||
|       const messagesEndElement = messagesEndRef.current; |             ...selectedConversation, | ||||||
|       if (messagesEndElement) { |             messages: [...selectedConversation.messages, message], | ||||||
|         observer.observe(messagesEndElement); |           }; | ||||||
|       } |  | ||||||
|       return () => { |  | ||||||
|         if (messagesEndElement) { |  | ||||||
|           observer.unobserve(messagesEndElement); |  | ||||||
|         } |         } | ||||||
|       }; |         homeDispatch({ | ||||||
|     }, [messagesEndRef]); |           field: 'selectedConversation', | ||||||
|  |           value: updatedConversation, | ||||||
|  |         }); | ||||||
|  |         homeDispatch({ field: 'loading', value: true }); | ||||||
|  |         homeDispatch({ field: 'messageIsStreaming', value: true }); | ||||||
|  |         const chatBody: ChatBody = { | ||||||
|  |           model: updatedConversation.model, | ||||||
|  |           messages: updatedConversation.messages, | ||||||
|  |           key: apiKey, | ||||||
|  |           prompt: updatedConversation.prompt, | ||||||
|  |         }; | ||||||
|  |         const endpoint = getEndpoint(plugin); | ||||||
|  |         let body; | ||||||
|  |         if (!plugin) { | ||||||
|  |           body = JSON.stringify(chatBody); | ||||||
|  |         } else { | ||||||
|  |           body = JSON.stringify({ | ||||||
|  |             ...chatBody, | ||||||
|  |             googleAPIKey: pluginKeys | ||||||
|  |               .find((key) => key.pluginId === 'google-search') | ||||||
|  |               ?.requiredKeys.find((key) => key.key === 'GOOGLE_API_KEY')?.value, | ||||||
|  |             googleCSEId: pluginKeys | ||||||
|  |               .find((key) => key.pluginId === 'google-search') | ||||||
|  |               ?.requiredKeys.find((key) => key.key === 'GOOGLE_CSE_ID')?.value, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |         const controller = new AbortController(); | ||||||
|  |         const response = await fetch(endpoint, { | ||||||
|  |           method: 'POST', | ||||||
|  |           headers: { | ||||||
|  |             'Content-Type': 'application/json', | ||||||
|  |           }, | ||||||
|  |           signal: controller.signal, | ||||||
|  |           body, | ||||||
|  |         }); | ||||||
|  |         if (!response.ok) { | ||||||
|  |           homeDispatch({ field: 'loading', value: false }); | ||||||
|  |           homeDispatch({ field: 'messageIsStreaming', value: false }); | ||||||
|  |           toast.error(response.statusText); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const data = response.body; | ||||||
|  |         if (!data) { | ||||||
|  |           homeDispatch({ field: 'loading', value: false }); | ||||||
|  |           homeDispatch({ field: 'messageIsStreaming', value: false }); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (!plugin) { | ||||||
|  |           if (updatedConversation.messages.length === 1) { | ||||||
|  |             const { content } = message; | ||||||
|  |             const customName = | ||||||
|  |               content.length > 30 ? content.substring(0, 30) + '...' : content; | ||||||
|  |             updatedConversation = { | ||||||
|  |               ...updatedConversation, | ||||||
|  |               name: customName, | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  |           homeDispatch({ field: 'loading', value: false }); | ||||||
|  |           const reader = data.getReader(); | ||||||
|  |           const decoder = new TextDecoder(); | ||||||
|  |           let done = false; | ||||||
|  |           let isFirst = true; | ||||||
|  |           let text = ''; | ||||||
|  |           while (!done) { | ||||||
|  |             if (stopConversationRef.current === true) { | ||||||
|  |               controller.abort(); | ||||||
|  |               done = true; | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |             const { value, done: doneReading } = await reader.read(); | ||||||
|  |             done = doneReading; | ||||||
|  |             const chunkValue = decoder.decode(value); | ||||||
|  |             text += chunkValue; | ||||||
|  |             if (isFirst) { | ||||||
|  |               isFirst = false; | ||||||
|  |               const updatedMessages: Message[] = [ | ||||||
|  |                 ...updatedConversation.messages, | ||||||
|  |                 { role: 'assistant', content: chunkValue }, | ||||||
|  |               ]; | ||||||
|  |               updatedConversation = { | ||||||
|  |                 ...updatedConversation, | ||||||
|  |                 messages: updatedMessages, | ||||||
|  |               }; | ||||||
|  |               homeDispatch({ | ||||||
|  |                 field: 'selectedConversation', | ||||||
|  |                 value: updatedConversation, | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               const updatedMessages: Message[] = | ||||||
|  |                 updatedConversation.messages.map((message, index) => { | ||||||
|  |                   if (index === updatedConversation.messages.length - 1) { | ||||||
|  |                     return { | ||||||
|  |                       ...message, | ||||||
|  |                       content: text, | ||||||
|  |                     }; | ||||||
|  |                   } | ||||||
|  |                   return message; | ||||||
|  |                 }); | ||||||
|  |               updatedConversation = { | ||||||
|  |                 ...updatedConversation, | ||||||
|  |                 messages: updatedMessages, | ||||||
|  |               }; | ||||||
|  |               homeDispatch({ | ||||||
|  |                 field: 'selectedConversation', | ||||||
|  |                 value: 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); | ||||||
|  |           } | ||||||
|  |           homeDispatch({ field: 'conversations', value: updatedConversations }); | ||||||
|  |           saveConversations(updatedConversations); | ||||||
|  |           homeDispatch({ field: 'messageIsStreaming', value: false }); | ||||||
|  |         } else { | ||||||
|  |           const { answer } = await response.json(); | ||||||
|  |           const updatedMessages: Message[] = [ | ||||||
|  |             ...updatedConversation.messages, | ||||||
|  |             { role: 'assistant', content: answer }, | ||||||
|  |           ]; | ||||||
|  |           updatedConversation = { | ||||||
|  |             ...updatedConversation, | ||||||
|  |             messages: updatedMessages, | ||||||
|  |           }; | ||||||
|  |           homeDispatch({ | ||||||
|  |             field: 'selectedConversation', | ||||||
|  |             value: updateConversation, | ||||||
|  |           }); | ||||||
|  |           saveConversation(updatedConversation); | ||||||
|  |           const updatedConversations: Conversation[] = conversations.map( | ||||||
|  |             (conversation) => { | ||||||
|  |               if (conversation.id === selectedConversation.id) { | ||||||
|  |                 return updatedConversation; | ||||||
|  |               } | ||||||
|  |               return conversation; | ||||||
|  |             }, | ||||||
|  |           ); | ||||||
|  |           if (updatedConversations.length === 0) { | ||||||
|  |             updatedConversations.push(updatedConversation); | ||||||
|  |           } | ||||||
|  |           homeDispatch({ field: 'conversations', value: updatedConversations }); | ||||||
|  |           saveConversations(updatedConversations); | ||||||
|  |           homeDispatch({ field: 'loading', value: false }); | ||||||
|  |           homeDispatch({ field: 'messageIsStreaming', value: false }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [ | ||||||
|  |       apiKey, | ||||||
|  |       conversations, | ||||||
|  |       pluginKeys, | ||||||
|  |       selectedConversation, | ||||||
|  |       stopConversationRef, | ||||||
|  |     ], | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|     return ( |   const scrollToBottom = useCallback(() => { | ||||||
|       <div className="relative flex-1 overflow-hidden bg-white dark:bg-[#343541]"> |     if (autoScrollEnabled) { | ||||||
|         {!(apiKey || serverSideApiKeyIsSet) ? ( |       messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||||||
|           <div className="mx-auto flex h-full w-[300px] flex-col justify-center space-y-6 sm:w-[600px]"> |       textareaRef.current?.focus(); | ||||||
|             <div className="text-center text-4xl font-bold text-black dark:text-white"> |     } | ||||||
|               Welcome to Chatbot UI |   }, [autoScrollEnabled]); | ||||||
|             </div> | 
 | ||||||
|             <div className="text-center text-lg text-black dark:text-white"> |   const handleScroll = () => { | ||||||
|               <div className="mb-8">{`Chatbot UI is an open source clone of OpenAI's ChatGPT UI.`}</div> |     if (chatContainerRef.current) { | ||||||
|               <div className="mb-2 font-bold"> |       const { scrollTop, scrollHeight, clientHeight } = | ||||||
|                 Important: Chatbot UI is 100% unaffiliated with OpenAI. |         chatContainerRef.current; | ||||||
|               </div> |       const bottomTolerance = 30; | ||||||
|             </div> | 
 | ||||||
|             <div className="text-center text-gray-500 dark:text-gray-400"> |       if (scrollTop + clientHeight < scrollHeight - bottomTolerance) { | ||||||
|               <div className="mb-2"> |         setAutoScrollEnabled(false); | ||||||
|                 Chatbot UI allows you to plug in your API key to use this UI |         setShowScrollDownButton(true); | ||||||
|                 with their API. |       } else { | ||||||
|               </div> |         setAutoScrollEnabled(true); | ||||||
|               <div className="mb-2"> |         setShowScrollDownButton(false); | ||||||
|                 It is <span className="italic">only</span> used to communicate |       } | ||||||
|                 with their API. |     } | ||||||
|               </div> |   }; | ||||||
|               <div className="mb-2"> | 
 | ||||||
|                 {t( |   const handleScrollDown = () => { | ||||||
|                   'Please set your OpenAI API key in the bottom left of the sidebar.', |     chatContainerRef.current?.scrollTo({ | ||||||
|                 )} |       top: chatContainerRef.current.scrollHeight, | ||||||
|               </div> |       behavior: 'smooth', | ||||||
|               <div> |     }); | ||||||
|                 {t( |   }; | ||||||
|                   "If you don't have an OpenAI API key, you can get one here: ", | 
 | ||||||
|                 )} |   const handleSettings = () => { | ||||||
|                 <a |     setShowSettings(!showSettings); | ||||||
|                   href="https://platform.openai.com/account/api-keys" |   }; | ||||||
|                   target="_blank" | 
 | ||||||
|                   rel="noreferrer" |   const onClearAll = () => { | ||||||
|                   className="text-blue-500 hover:underline" |     if ( | ||||||
|                 > |       confirm(t<string>('Are you sure you want to clear all messages?')) && | ||||||
|                   openai.com |       selectedConversation | ||||||
|                 </a> |     ) { | ||||||
|               </div> |       handleUpdateConversation(selectedConversation, { | ||||||
|  |         key: 'messages', | ||||||
|  |         value: [], | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const scrollDown = () => { | ||||||
|  |     if (autoScrollEnabled) { | ||||||
|  |       messagesEndRef.current?.scrollIntoView(true); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   const throttledScrollDown = throttle(scrollDown, 250); | ||||||
|  | 
 | ||||||
|  |   // useEffect(() => {
 | ||||||
|  |   //   console.log('currentMessage', currentMessage);
 | ||||||
|  |   //   if (currentMessage) {
 | ||||||
|  |   //     handleSend(currentMessage);
 | ||||||
|  |   //     homeDispatch({ field: 'currentMessage', value: undefined });
 | ||||||
|  |   //   }
 | ||||||
|  |   // }, [currentMessage]);
 | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     throttledScrollDown(); | ||||||
|  |     selectedConversation && | ||||||
|  |       setCurrentMessage( | ||||||
|  |         selectedConversation.messages[selectedConversation.messages.length - 2], | ||||||
|  |       ); | ||||||
|  |   }, [selectedConversation, throttledScrollDown]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const observer = new IntersectionObserver( | ||||||
|  |       ([entry]) => { | ||||||
|  |         setAutoScrollEnabled(entry.isIntersecting); | ||||||
|  |         if (entry.isIntersecting) { | ||||||
|  |           textareaRef.current?.focus(); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         root: null, | ||||||
|  |         threshold: 0.5, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |     const messagesEndElement = messagesEndRef.current; | ||||||
|  |     if (messagesEndElement) { | ||||||
|  |       observer.observe(messagesEndElement); | ||||||
|  |     } | ||||||
|  |     return () => { | ||||||
|  |       if (messagesEndElement) { | ||||||
|  |         observer.unobserve(messagesEndElement); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }, [messagesEndRef]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="relative flex-1 overflow-hidden bg-white dark:bg-[#343541]"> | ||||||
|  |       {!(apiKey || serverSideApiKeyIsSet) ? ( | ||||||
|  |         <div className="mx-auto flex h-full w-[300px] flex-col justify-center space-y-6 sm:w-[600px]"> | ||||||
|  |           <div className="text-center text-4xl font-bold text-black dark:text-white"> | ||||||
|  |             Welcome to Chatbot UI | ||||||
|  |           </div> | ||||||
|  |           <div className="text-center text-lg text-black dark:text-white"> | ||||||
|  |             <div className="mb-8">{`Chatbot UI is an open source clone of OpenAI's ChatGPT UI.`}</div> | ||||||
|  |             <div className="mb-2 font-bold"> | ||||||
|  |               Important: Chatbot UI is 100% unaffiliated with OpenAI. | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         ) : modelError ? ( |           <div className="text-center text-gray-500 dark:text-gray-400"> | ||||||
|           <ErrorMessageDiv error={modelError} /> |             <div className="mb-2"> | ||||||
|         ) : ( |               Chatbot UI allows you to plug in your API key to use this UI with | ||||||
|           <> |               their API. | ||||||
|             <div |             </div> | ||||||
|               className="max-h-full overflow-x-hidden" |             <div className="mb-2"> | ||||||
|               ref={chatContainerRef} |               It is <span className="italic">only</span> used to communicate | ||||||
|               onScroll={handleScroll} |               with their API. | ||||||
|             > |             </div> | ||||||
|               {conversation.messages.length === 0 ? ( |             <div className="mb-2"> | ||||||
|                 <> |               {t( | ||||||
|                   <div className="mx-auto flex w-[350px] flex-col space-y-10 pt-12 sm:w-[600px]"> |                 'Please set your OpenAI API key in the bottom left of the sidebar.', | ||||||
|                     <div className="text-center text-3xl font-semibold text-gray-800 dark:text-gray-100"> |  | ||||||
|                       {models.length === 0 ? ( |  | ||||||
|                         <div> |  | ||||||
|                           <Spinner size="16px" className="mx-auto" /> |  | ||||||
|                         </div> |  | ||||||
|                       ) : ( |  | ||||||
|                         'Chatbot UI' |  | ||||||
|                       )} |  | ||||||
|                     </div> |  | ||||||
| 
 |  | ||||||
|                     {models.length > 0 && ( |  | ||||||
|                       <div className="flex h-full flex-col space-y-4 rounded-lg border border-neutral-200 p-4 dark:border-neutral-600"> |  | ||||||
|                         <ModelSelect |  | ||||||
|                           model={conversation.model} |  | ||||||
|                           models={models} |  | ||||||
|                           defaultModelId={defaultModelId} |  | ||||||
|                           onModelChange={(model) => |  | ||||||
|                             onUpdateConversation(conversation, { |  | ||||||
|                               key: 'model', |  | ||||||
|                               value: model, |  | ||||||
|                             }) |  | ||||||
|                           } |  | ||||||
|                         /> |  | ||||||
| 
 |  | ||||||
|                         <SystemPrompt |  | ||||||
|                           conversation={conversation} |  | ||||||
|                           prompts={prompts} |  | ||||||
|                           onChangePrompt={(prompt) => |  | ||||||
|                             onUpdateConversation(conversation, { |  | ||||||
|                               key: 'prompt', |  | ||||||
|                               value: prompt, |  | ||||||
|                             }) |  | ||||||
|                           } |  | ||||||
|                         /> |  | ||||||
|                       </div> |  | ||||||
|                     )} |  | ||||||
|                   </div> |  | ||||||
|                 </> |  | ||||||
|               ) : ( |  | ||||||
|                 <> |  | ||||||
|                   <div className="flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"> |  | ||||||
|                     {t('Model')}: {conversation.model.name} |  | ||||||
|                     <button |  | ||||||
|                       className="ml-2 cursor-pointer hover:opacity-50" |  | ||||||
|                       onClick={handleSettings} |  | ||||||
|                     > |  | ||||||
|                       <IconSettings size={18} /> |  | ||||||
|                     </button> |  | ||||||
|                     <button |  | ||||||
|                       className="ml-2 cursor-pointer hover:opacity-50" |  | ||||||
|                       onClick={onClearAll} |  | ||||||
|                     > |  | ||||||
|                       <IconClearAll size={18} /> |  | ||||||
|                     </button> |  | ||||||
|                   </div> |  | ||||||
|                   {showSettings && ( |  | ||||||
|                     <div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> |  | ||||||
|                       <div className="flex h-full flex-col space-y-4 border-b border-neutral-200 p-4 dark:border-neutral-600 md:rounded-lg md:border"> |  | ||||||
|                         <ModelSelect |  | ||||||
|                           model={conversation.model} |  | ||||||
|                           models={models} |  | ||||||
|                           defaultModelId={defaultModelId} |  | ||||||
|                           onModelChange={(model) => |  | ||||||
|                             onUpdateConversation(conversation, { |  | ||||||
|                               key: 'model', |  | ||||||
|                               value: model, |  | ||||||
|                             }) |  | ||||||
|                           } |  | ||||||
|                         /> |  | ||||||
|                       </div> |  | ||||||
|                     </div> |  | ||||||
|                   )} |  | ||||||
| 
 |  | ||||||
|                   {conversation.messages.map((message, index) => ( |  | ||||||
|                     <ChatMessage |  | ||||||
|                       key={index} |  | ||||||
|                       message={message} |  | ||||||
|                       messageIndex={index} |  | ||||||
|                       onEditMessage={onEditMessage} |  | ||||||
|                     /> |  | ||||||
|                   ))} |  | ||||||
| 
 |  | ||||||
|                   {loading && <ChatLoader />} |  | ||||||
| 
 |  | ||||||
|                   <div |  | ||||||
|                     className="h-[162px] bg-white dark:bg-[#343541]" |  | ||||||
|                     ref={messagesEndRef} |  | ||||||
|                   /> |  | ||||||
|                 </> |  | ||||||
|               )} |               )} | ||||||
|             </div> |             </div> | ||||||
| 
 |             <div> | ||||||
|             <ChatInput |               {t("If you don't have an OpenAI API key, you can get one here: ")} | ||||||
|               stopConversationRef={stopConversationRef} |               <a | ||||||
|               textareaRef={textareaRef} |                 href="https://platform.openai.com/account/api-keys" | ||||||
|               messageIsStreaming={messageIsStreaming} |                 target="_blank" | ||||||
|               conversationIsEmpty={conversation.messages.length === 0} |                 rel="noreferrer" | ||||||
|               model={conversation.model} |                 className="text-blue-500 hover:underline" | ||||||
|               prompts={prompts} |               > | ||||||
|               onSend={(message, plugin) => { |                 openai.com | ||||||
|                 setCurrentMessage(message); |               </a> | ||||||
|                 onSend(message, 0, plugin); |             </div> | ||||||
|               }} |  | ||||||
|               onRegenerate={() => { |  | ||||||
|                 if (currentMessage) { |  | ||||||
|                   onSend(currentMessage, 2, null); |  | ||||||
|                 } |  | ||||||
|               }} |  | ||||||
|             /> |  | ||||||
|           </> |  | ||||||
|         )} |  | ||||||
|         {showScrollDownButton && ( |  | ||||||
|           <div className="absolute bottom-0 right-0 mb-4 mr-4 pb-20"> |  | ||||||
|             <button |  | ||||||
|               className="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-300 text-gray-800 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-neutral-200" |  | ||||||
|               onClick={handleScrollDown} |  | ||||||
|             > |  | ||||||
|               <IconArrowDown size={18} /> |  | ||||||
|             </button> |  | ||||||
|           </div> |           </div> | ||||||
|         )} |         </div> | ||||||
|       </div> |       ) : modelError ? ( | ||||||
|     ); |         <ErrorMessageDiv error={modelError} /> | ||||||
|   }, |       ) : ( | ||||||
| ); |         <> | ||||||
|  |           <div | ||||||
|  |             className="max-h-full overflow-x-hidden" | ||||||
|  |             ref={chatContainerRef} | ||||||
|  |             onScroll={handleScroll} | ||||||
|  |           > | ||||||
|  |             {selectedConversation?.messages.length === 0 ? ( | ||||||
|  |               <> | ||||||
|  |                 <div className="mx-auto flex w-[350px] flex-col space-y-10 pt-12 sm:w-[600px]"> | ||||||
|  |                   <div className="text-center text-3xl font-semibold text-gray-800 dark:text-gray-100"> | ||||||
|  |                     {models.length === 0 ? ( | ||||||
|  |                       <div> | ||||||
|  |                         <Spinner size="16px" className="mx-auto" /> | ||||||
|  |                       </div> | ||||||
|  |                     ) : ( | ||||||
|  |                       'Chatbot UI' | ||||||
|  |                     )} | ||||||
|  |                   </div> | ||||||
|  | 
 | ||||||
|  |                   {models.length > 0 && ( | ||||||
|  |                     <div className="flex h-full flex-col space-y-4 rounded-lg border border-neutral-200 p-4 dark:border-neutral-600"> | ||||||
|  |                       <ModelSelect /> | ||||||
|  | 
 | ||||||
|  |                       <SystemPrompt | ||||||
|  |                         conversation={selectedConversation} | ||||||
|  |                         prompts={prompts} | ||||||
|  |                         onChangePrompt={(prompt) => | ||||||
|  |                           handleUpdateConversation(selectedConversation, { | ||||||
|  |                             key: 'prompt', | ||||||
|  |                             value: prompt, | ||||||
|  |                           }) | ||||||
|  |                         } | ||||||
|  |                       /> | ||||||
|  |                     </div> | ||||||
|  |                   )} | ||||||
|  |                 </div> | ||||||
|  |               </> | ||||||
|  |             ) : ( | ||||||
|  |               <> | ||||||
|  |                 <div className="flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"> | ||||||
|  |                   {t('Model')}: {selectedConversation?.model.name} | ||||||
|  |                   <button | ||||||
|  |                     className="ml-2 cursor-pointer hover:opacity-50" | ||||||
|  |                     onClick={handleSettings} | ||||||
|  |                   > | ||||||
|  |                     <IconSettings size={18} /> | ||||||
|  |                   </button> | ||||||
|  |                   <button | ||||||
|  |                     className="ml-2 cursor-pointer hover:opacity-50" | ||||||
|  |                     onClick={onClearAll} | ||||||
|  |                   > | ||||||
|  |                     <IconClearAll size={18} /> | ||||||
|  |                   </button> | ||||||
|  |                 </div> | ||||||
|  |                 {showSettings && ( | ||||||
|  |                   <div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> | ||||||
|  |                     <div className="flex h-full flex-col space-y-4 border-b border-neutral-200 p-4 dark:border-neutral-600 md:rounded-lg md:border"> | ||||||
|  |                       <ModelSelect /> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 )} | ||||||
|  | 
 | ||||||
|  |                 {selectedConversation?.messages.map((message, index) => ( | ||||||
|  |                   <ChatMessage | ||||||
|  |                     key={index} | ||||||
|  |                     message={message} | ||||||
|  |                     messageIndex={index} | ||||||
|  |                   /> | ||||||
|  |                 ))} | ||||||
|  | 
 | ||||||
|  |                 {loading && <ChatLoader />} | ||||||
|  | 
 | ||||||
|  |                 <div | ||||||
|  |                   className="h-[162px] bg-white dark:bg-[#343541]" | ||||||
|  |                   ref={messagesEndRef} | ||||||
|  |                 /> | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <ChatInput | ||||||
|  |             stopConversationRef={stopConversationRef} | ||||||
|  |             textareaRef={textareaRef} | ||||||
|  |             onSend={(message, plugin) => { | ||||||
|  |               setCurrentMessage(message); | ||||||
|  |               handleSend(message, 0, plugin); | ||||||
|  |             }} | ||||||
|  |             onRegenerate={() => { | ||||||
|  |               if (currentMessage) { | ||||||
|  |                 handleSend(currentMessage, 2, null); | ||||||
|  |               } | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|  |       {showScrollDownButton && ( | ||||||
|  |         <div className="absolute bottom-0 right-0 mb-4 mr-4 pb-20"> | ||||||
|  |           <button | ||||||
|  |             className="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-300 text-gray-800 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-neutral-200" | ||||||
|  |             onClick={handleScrollDown} | ||||||
|  |           > | ||||||
|  |             <IconArrowDown size={18} /> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }); | ||||||
| Chat.displayName = 'Chat'; | Chat.displayName = 'Chat'; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,3 @@ | ||||||
| import { Message } from '@/types/chat'; |  | ||||||
| import { OpenAIModel } from '@/types/openai'; |  | ||||||
| import { Plugin } from '@/types/plugin'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { | import { | ||||||
|   IconBolt, |   IconBolt, | ||||||
|   IconBrandGoogle, |   IconBrandGoogle, | ||||||
|  | @ -9,43 +5,49 @@ import { | ||||||
|   IconRepeat, |   IconRepeat, | ||||||
|   IconSend, |   IconSend, | ||||||
| } from '@tabler/icons-react'; | } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { | import { | ||||||
|   FC, |  | ||||||
|   KeyboardEvent, |   KeyboardEvent, | ||||||
|   MutableRefObject, |   MutableRefObject, | ||||||
|   useCallback, |   useCallback, | ||||||
|  |   useContext, | ||||||
|   useEffect, |   useEffect, | ||||||
|   useRef, |   useRef, | ||||||
|   useState, |   useState, | ||||||
| } from 'react'; | } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { Message } from '@/types/chat'; | ||||||
|  | import { Plugin } from '@/types/plugin'; | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
| import { PluginSelect } from './PluginSelect'; | import { PluginSelect } from './PluginSelect'; | ||||||
| import { PromptList } from './PromptList'; | import { PromptList } from './PromptList'; | ||||||
| import { VariableModal } from './VariableModal'; | import { VariableModal } from './VariableModal'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   messageIsStreaming: boolean; |  | ||||||
|   model: OpenAIModel; |  | ||||||
|   conversationIsEmpty: boolean; |  | ||||||
|   prompts: Prompt[]; |  | ||||||
|   onSend: (message: Message, plugin: Plugin | null) => 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>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatInput: FC<Props> = ({ | export const ChatInput = ({ | ||||||
|   messageIsStreaming, |  | ||||||
|   model, |  | ||||||
|   conversationIsEmpty, |  | ||||||
|   prompts, |  | ||||||
|   onSend, |   onSend, | ||||||
|   onRegenerate, |   onRegenerate, | ||||||
|   stopConversationRef, |   stopConversationRef, | ||||||
|   textareaRef, |   textareaRef, | ||||||
| }) => { | }: Props) => { | ||||||
|   const { t } = useTranslation('chat'); |   const { t } = useTranslation('chat'); | ||||||
| 
 | 
 | ||||||
|  |   const { | ||||||
|  |     state: { selectedConversation, messageIsStreaming, prompts }, | ||||||
|  | 
 | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|   const [content, setContent] = useState<string>(); |   const [content, setContent] = useState<string>(); | ||||||
|   const [isTyping, setIsTyping] = useState<boolean>(false); |   const [isTyping, setIsTyping] = useState<boolean>(false); | ||||||
|   const [showPromptList, setShowPromptList] = useState(false); |   const [showPromptList, setShowPromptList] = useState(false); | ||||||
|  | @ -64,9 +66,9 @@ export const ChatInput: FC<Props> = ({ | ||||||
| 
 | 
 | ||||||
|   const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |   const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|     const value = e.target.value; |     const value = e.target.value; | ||||||
|     const maxLength = model.maxLength; |     const maxLength = selectedConversation?.model.maxLength; | ||||||
| 
 | 
 | ||||||
|     if (value.length > maxLength) { |     if (maxLength && value.length > maxLength) { | ||||||
|       alert( |       alert( | ||||||
|         t( |         t( | ||||||
|           `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, |           `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, | ||||||
|  | @ -261,14 +263,16 @@ export const ChatInput: FC<Props> = ({ | ||||||
|           </button> |           </button> | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         {!messageIsStreaming && !conversationIsEmpty && ( |         {!messageIsStreaming && | ||||||
|           <button |           selectedConversation && | ||||||
|             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" |           selectedConversation.messages.length > 0 && ( | ||||||
|             onClick={onRegenerate} |             <button | ||||||
|           > |               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" | ||||||
|             <IconRepeat size={16} /> {t('Regenerate response')} |               onClick={onRegenerate} | ||||||
|           </button> |             > | ||||||
|         )} |               <IconRepeat size={16} /> {t('Regenerate response')} | ||||||
|  |             </button> | ||||||
|  |           )} | ||||||
| 
 | 
 | ||||||
|         <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 |           <button | ||||||
|  |  | ||||||
|  | @ -1,237 +1,271 @@ | ||||||
| import { Message } from '@/types/chat'; |  | ||||||
| import { | import { | ||||||
|   IconCheck, |   IconCheck, | ||||||
|   IconCopy, |   IconCopy, | ||||||
|   IconEdit, |   IconEdit, | ||||||
|   IconUser, |  | ||||||
|   IconRobot, |   IconRobot, | ||||||
|  |   IconUser, | ||||||
| } from '@tabler/icons-react'; | } from '@tabler/icons-react'; | ||||||
|  | import { FC, memo, useContext, useEffect, useRef, useState } from 'react'; | ||||||
|  | 
 | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { FC, memo, useEffect, useRef, useState } from 'react'; | 
 | ||||||
|  | import { updateConversation } from '@/utils/app/conversation'; | ||||||
|  | 
 | ||||||
|  | import { Message } from '@/types/chat'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import { CodeBlock } from '../Markdown/CodeBlock'; | ||||||
|  | import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown'; | ||||||
|  | 
 | ||||||
| import rehypeMathjax from 'rehype-mathjax'; | import rehypeMathjax from 'rehype-mathjax'; | ||||||
| import remarkGfm from 'remark-gfm'; | import remarkGfm from 'remark-gfm'; | ||||||
| import remarkMath from 'remark-math'; | import remarkMath from 'remark-math'; | ||||||
| import { CodeBlock } from '../Markdown/CodeBlock'; |  | ||||||
| import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown'; |  | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   message: Message; |   message: Message; | ||||||
|   messageIndex: number; |   messageIndex: number; | ||||||
|   onEditMessage: (message: Message, messageIndex: number) => void; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatMessage: FC<Props> = memo( | export const ChatMessage: FC<Props> = memo(({ message, messageIndex }) => { | ||||||
|   ({ message, messageIndex, onEditMessage }) => { |   const { t } = useTranslation('chat'); | ||||||
|     const { t } = useTranslation('chat'); |  | ||||||
|     const [isEditing, setIsEditing] = useState<boolean>(false); |  | ||||||
|     const [isTyping, setIsTyping] = useState<boolean>(false); |  | ||||||
|     const [messageContent, setMessageContent] = useState(message.content); |  | ||||||
|     const [messagedCopied, setMessageCopied] = useState(false); |  | ||||||
| 
 | 
 | ||||||
|     const textareaRef = useRef<HTMLTextAreaElement>(null); |   const { | ||||||
|  |     state: { selectedConversation, conversations }, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |   } = useContext(HomeContext); | ||||||
| 
 | 
 | ||||||
|     const toggleEditing = () => { |   const [isEditing, setIsEditing] = useState<boolean>(false); | ||||||
|       setIsEditing(!isEditing); |   const [isTyping, setIsTyping] = useState<boolean>(false); | ||||||
|     }; |   const [messageContent, setMessageContent] = useState(message.content); | ||||||
|  |   const [messagedCopied, setMessageCopied] = useState(false); | ||||||
| 
 | 
 | ||||||
|     const handleInputChange = ( |   const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
|       event: React.ChangeEvent<HTMLTextAreaElement>, | 
 | ||||||
|     ) => { |   const toggleEditing = () => { | ||||||
|       setMessageContent(event.target.value); |     setIsEditing(!isEditing); | ||||||
|       if (textareaRef.current) { |   }; | ||||||
|         textareaRef.current.style.height = 'inherit'; | 
 | ||||||
|         textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; |   const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|  |     setMessageContent(event.target.value); | ||||||
|  |     if (textareaRef.current) { | ||||||
|  |       textareaRef.current.style.height = 'inherit'; | ||||||
|  |       textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleEditMessage = () => { | ||||||
|  |     if (message.content != messageContent) { | ||||||
|  |       if (selectedConversation) { | ||||||
|  |         const updatedMessages = selectedConversation.messages | ||||||
|  |           .map((m, i) => { | ||||||
|  |             if (i < messageIndex) { | ||||||
|  |               return m; | ||||||
|  |             } | ||||||
|  |           }) | ||||||
|  |           .filter((m) => m) as Message[]; | ||||||
|  | 
 | ||||||
|  |         const updatedConversation = { | ||||||
|  |           ...selectedConversation, | ||||||
|  |           messages: updatedMessages, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const { single, all } = updateConversation( | ||||||
|  |           updatedConversation, | ||||||
|  |           conversations, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         homeDispatch({ field: 'selectedConversation', value: single }); | ||||||
|  |         homeDispatch({ field: 'conversations', value: all }); | ||||||
|  |         homeDispatch({ | ||||||
|  |           field: 'currentMessage', | ||||||
|  |           value: { ...message, content: messageContent }, | ||||||
|  |         }); | ||||||
|       } |       } | ||||||
|     }; |     } | ||||||
|  |     setIsEditing(false); | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|     const handleEditMessage = () => { |   const handlePressEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||||||
|       if (message.content != messageContent) { |     if (e.key === 'Enter' && !isTyping && !e.shiftKey) { | ||||||
|         onEditMessage({ ...message, content: messageContent }, messageIndex); |       e.preventDefault(); | ||||||
|       } |       handleEditMessage(); | ||||||
|       setIsEditing(false); |     } | ||||||
|     }; |   }; | ||||||
| 
 | 
 | ||||||
|     const handlePressEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { |   const copyOnClick = () => { | ||||||
|       if (e.key === 'Enter' && !isTyping && !e.shiftKey) { |     if (!navigator.clipboard) return; | ||||||
|         e.preventDefault(); |  | ||||||
|         handleEditMessage(); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
| 
 | 
 | ||||||
|     const copyOnClick = () => { |     navigator.clipboard.writeText(message.content).then(() => { | ||||||
|       if (!navigator.clipboard) return; |       setMessageCopied(true); | ||||||
|  |       setTimeout(() => { | ||||||
|  |         setMessageCopied(false); | ||||||
|  |       }, 2000); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|       navigator.clipboard.writeText(message.content).then(() => { |   useEffect(() => { | ||||||
|         setMessageCopied(true); |     if (textareaRef.current) { | ||||||
|         setTimeout(() => { |       textareaRef.current.style.height = 'inherit'; | ||||||
|           setMessageCopied(false); |       textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; | ||||||
|         }, 2000); |     } | ||||||
|       }); |   }, [isEditing]); | ||||||
|     }; |  | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |   return ( | ||||||
|       if (textareaRef.current) { |     <div | ||||||
|         textareaRef.current.style.height = 'inherit'; |       className={`group px-4 ${ | ||||||
|         textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; |         message.role === 'assistant' | ||||||
|       } |           ? 'border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100' | ||||||
|     }, [isEditing]); |           : 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100' | ||||||
|  |       }`}
 | ||||||
|  |       style={{ overflowWrap: 'anywhere' }} | ||||||
|  |     > | ||||||
|  |       <div className="relative 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"> | ||||||
|  |           {message.role === 'assistant' ? ( | ||||||
|  |             <IconRobot size={30} /> | ||||||
|  |           ) : ( | ||||||
|  |             <IconUser size={30} /> | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
| 
 | 
 | ||||||
|     return ( |         <div className="prose mt-[-2px] w-full dark:prose-invert"> | ||||||
|       <div |           {message.role === 'user' ? ( | ||||||
|         className={`group px-4 ${ |             <div className="flex w-full"> | ||||||
|           message.role === 'assistant' |               {isEditing ? ( | ||||||
|             ? 'border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100' |                 <div className="flex w-full flex-col"> | ||||||
|             : 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100' |                   <textarea | ||||||
|         }`}
 |                     ref={textareaRef} | ||||||
|         style={{ overflowWrap: 'anywhere' }} |                     className="w-full resize-none whitespace-pre-wrap border-none dark:bg-[#343541]" | ||||||
|       > |                     value={messageContent} | ||||||
|         <div className="relative 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"> |                     onChange={handleInputChange} | ||||||
|           <div className="min-w-[40px] text-right font-bold"> |                     onKeyDown={handlePressEnter} | ||||||
|             {message.role === 'assistant' ? ( |                     onCompositionStart={() => setIsTyping(true)} | ||||||
|               <IconRobot size={30} /> |                     onCompositionEnd={() => setIsTyping(false)} | ||||||
|             ) : ( |                     style={{ | ||||||
|               <IconUser size={30} /> |                       fontFamily: 'inherit', | ||||||
|             )} |                       fontSize: 'inherit', | ||||||
|           </div> |                       lineHeight: 'inherit', | ||||||
|  |                       padding: '0', | ||||||
|  |                       margin: '0', | ||||||
|  |                       overflow: 'hidden', | ||||||
|  |                     }} | ||||||
|  |                   /> | ||||||
| 
 | 
 | ||||||
|           <div className="prose mt-[-2px] w-full dark:prose-invert"> |                   <div className="mt-10 flex justify-center space-x-4"> | ||||||
|             {message.role === 'user' ? ( |                     <button | ||||||
|               <div className="flex w-full"> |                       className="h-[40px] rounded-md bg-blue-500 px-4 py-1 text-sm font-medium text-white enabled:hover:bg-blue-600 disabled:opacity-50" | ||||||
|                 {isEditing ? ( |                       onClick={handleEditMessage} | ||||||
|                   <div className="flex w-full flex-col"> |                       disabled={messageContent.trim().length <= 0} | ||||||
|                     <textarea |                     > | ||||||
|                       ref={textareaRef} |                       {t('Save & Submit')} | ||||||
|                       className="w-full resize-none whitespace-pre-wrap border-none dark:bg-[#343541]" |                     </button> | ||||||
|                       value={messageContent} |                     <button | ||||||
|                       onChange={handleInputChange} |                       className="h-[40px] rounded-md border border-neutral-300 px-4 py-1 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800" | ||||||
|                       onKeyDown={handlePressEnter} |                       onClick={() => { | ||||||
|                       onCompositionStart={() => setIsTyping(true)} |                         setMessageContent(message.content); | ||||||
|                       onCompositionEnd={() => setIsTyping(false)} |                         setIsEditing(false); | ||||||
|                       style={{ |  | ||||||
|                         fontFamily: 'inherit', |  | ||||||
|                         fontSize: 'inherit', |  | ||||||
|                         lineHeight: 'inherit', |  | ||||||
|                         padding: '0', |  | ||||||
|                         margin: '0', |  | ||||||
|                         overflow: 'hidden', |  | ||||||
|                       }} |                       }} | ||||||
|                     /> |                     > | ||||||
| 
 |                       {t('Cancel')} | ||||||
|                     <div className="mt-10 flex justify-center space-x-4"> |                     </button> | ||||||
|                       <button |  | ||||||
|                         className="h-[40px] rounded-md bg-blue-500 px-4 py-1 text-sm font-medium text-white enabled:hover:bg-blue-600 disabled:opacity-50" |  | ||||||
|                         onClick={handleEditMessage} |  | ||||||
|                         disabled={messageContent.trim().length <= 0} |  | ||||||
|                       > |  | ||||||
|                         {t('Save & Submit')} |  | ||||||
|                       </button> |  | ||||||
|                       <button |  | ||||||
|                         className="h-[40px] rounded-md border border-neutral-300 px-4 py-1 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800" |  | ||||||
|                         onClick={() => { |  | ||||||
|                           setMessageContent(message.content); |  | ||||||
|                           setIsEditing(false); |  | ||||||
|                         }} |  | ||||||
|                       > |  | ||||||
|                         {t('Cancel')} |  | ||||||
|                       </button> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |                   </div> | ||||||
|                 ) : ( |                 </div> | ||||||
|                   <div className="prose whitespace-pre-wrap dark:prose-invert"> |               ) : ( | ||||||
|                     {message.content} |                 <div className="prose whitespace-pre-wrap dark:prose-invert"> | ||||||
|                   </div> |                   {message.content} | ||||||
|                 )} |                 </div> | ||||||
|  |               )} | ||||||
| 
 | 
 | ||||||
|                 {(window.innerWidth < 640 || !isEditing) && ( |               {(window.innerWidth < 640 || !isEditing) && ( | ||||||
|                   <button |                 <button | ||||||
|                     className={`absolute translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300 ${ |                   className={`absolute translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300 ${ | ||||||
|                       window.innerWidth < 640 |                     window.innerWidth < 640 | ||||||
|                         ? 'bottom-1 right-3' |                       ? 'bottom-1 right-3' | ||||||
|                         : 'right-0 top-[26px]' |                       : 'right-0 top-[26px]' | ||||||
|                     } |                   } | ||||||
|                     `}
 |                     `}
 | ||||||
|                     onClick={toggleEditing} |                   onClick={toggleEditing} | ||||||
|  |                 > | ||||||
|  |                   <IconEdit size={20} /> | ||||||
|  |                 </button> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |           ) : ( | ||||||
|  |             <> | ||||||
|  |               <div | ||||||
|  |                 className={`absolute ${ | ||||||
|  |                   window.innerWidth < 640 | ||||||
|  |                     ? 'bottom-1 right-3' | ||||||
|  |                     : 'right-0 top-[26px] m-0' | ||||||
|  |                 }`}
 | ||||||
|  |               > | ||||||
|  |                 {messagedCopied ? ( | ||||||
|  |                   <IconCheck | ||||||
|  |                     size={20} | ||||||
|  |                     className="text-green-500 dark:text-green-400" | ||||||
|  |                   /> | ||||||
|  |                 ) : ( | ||||||
|  |                   <button | ||||||
|  |                     className="translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300" | ||||||
|  |                     onClick={copyOnClick} | ||||||
|                   > |                   > | ||||||
|                     <IconEdit size={20} /> |                     <IconCopy size={20} /> | ||||||
|                   </button> |                   </button> | ||||||
|                 )} |                 )} | ||||||
|               </div> |               </div> | ||||||
|             ) : ( |  | ||||||
|               <> |  | ||||||
|                 <div |  | ||||||
|                   className={`absolute ${ |  | ||||||
|                     window.innerWidth < 640 |  | ||||||
|                       ? 'bottom-1 right-3' |  | ||||||
|                       : 'right-0 top-[26px] m-0' |  | ||||||
|                   }`}
 |  | ||||||
|                 > |  | ||||||
|                   {messagedCopied ? ( |  | ||||||
|                     <IconCheck |  | ||||||
|                       size={20} |  | ||||||
|                       className="text-green-500 dark:text-green-400" |  | ||||||
|                     /> |  | ||||||
|                   ) : ( |  | ||||||
|                     <button |  | ||||||
|                       className="translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300" |  | ||||||
|                       onClick={copyOnClick} |  | ||||||
|                     > |  | ||||||
|                       <IconCopy size={20} /> |  | ||||||
|                     </button> |  | ||||||
|                   )} |  | ||||||
|                 </div> |  | ||||||
| 
 | 
 | ||||||
|                 <MemoizedReactMarkdown |               <MemoizedReactMarkdown | ||||||
|                   className="prose dark:prose-invert" |                 className="prose dark:prose-invert" | ||||||
|                   remarkPlugins={[remarkGfm, remarkMath]} |                 remarkPlugins={[remarkGfm, remarkMath]} | ||||||
|                   rehypePlugins={[rehypeMathjax]} |                 rehypePlugins={[rehypeMathjax]} | ||||||
|                   components={{ |                 components={{ | ||||||
|                     code({ node, inline, className, children, ...props }) { |                   code({ node, inline, className, children, ...props }) { | ||||||
|                       const match = /language-(\w+)/.exec(className || ''); |                     const match = /language-(\w+)/.exec(className || ''); | ||||||
| 
 | 
 | ||||||
|                       return !inline ? ( |                     return !inline ? ( | ||||||
|                         <CodeBlock |                       <CodeBlock | ||||||
|                           key={Math.random()} |                         key={Math.random()} | ||||||
|                           language={(match && match[1]) || ''} |                         language={(match && match[1]) || ''} | ||||||
|                           value={String(children).replace(/\n$/, '')} |                         value={String(children).replace(/\n$/, '')} | ||||||
|                           {...props} |                         {...props} | ||||||
|                         /> |                       /> | ||||||
|                       ) : ( |                     ) : ( | ||||||
|                         <code className={className} {...props}> |                       <code className={className} {...props}> | ||||||
|                           {children} |                         {children} | ||||||
|                         </code> |                       </code> | ||||||
|                       ); |                     ); | ||||||
|                     }, |                   }, | ||||||
|                     table({ children }) { |                   table({ children }) { | ||||||
|                       return ( |                     return ( | ||||||
|                         <table className="border-collapse border border-black px-3 py-1 dark:border-white"> |                       <table className="border-collapse border border-black px-3 py-1 dark:border-white"> | ||||||
|                           {children} |                         {children} | ||||||
|                         </table> |                       </table> | ||||||
|                       ); |                     ); | ||||||
|                     }, |                   }, | ||||||
|                     th({ children }) { |                   th({ children }) { | ||||||
|                       return ( |                     return ( | ||||||
|                         <th className="break-words border border-black bg-gray-500 px-3 py-1 text-white dark:border-white"> |                       <th className="break-words border border-black bg-gray-500 px-3 py-1 text-white dark:border-white"> | ||||||
|                           {children} |                         {children} | ||||||
|                         </th> |                       </th> | ||||||
|                       ); |                     ); | ||||||
|                     }, |                   }, | ||||||
|                     td({ children }) { |                   td({ children }) { | ||||||
|                       return ( |                     return ( | ||||||
|                         <td className="break-words border border-black px-3 py-1 dark:border-white"> |                       <td className="break-words border border-black px-3 py-1 dark:border-white"> | ||||||
|                           {children} |                         {children} | ||||||
|                         </td> |                       </td> | ||||||
|                       ); |                     ); | ||||||
|                     }, |                   }, | ||||||
|                   }} |                 }} | ||||||
|                 > |               > | ||||||
|                   {message.content} |                 {message.content} | ||||||
|                 </MemoizedReactMarkdown> |               </MemoizedReactMarkdown> | ||||||
|               </> |             </> | ||||||
|             )} |           )} | ||||||
|           </div> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     ); |     </div> | ||||||
|   }, |   ); | ||||||
| ); | }); | ||||||
| ChatMessage.displayName = 'ChatMessage'; | ChatMessage.displayName = 'ChatMessage'; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| import { ErrorMessage } from '@/types/error'; |  | ||||||
| import { IconCircleX } from '@tabler/icons-react'; | import { IconCircleX } from '@tabler/icons-react'; | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { ErrorMessage } from '@/types/error'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   error: ErrorMessage; |   error: ErrorMessage; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,23 +1,31 @@ | ||||||
| import { OpenAIModel, OpenAIModelID } from '@/types/openai'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { IconExternalLink } from '@tabler/icons-react'; | import { IconExternalLink } from '@tabler/icons-react'; | ||||||
| import { FC } from 'react'; | import { useContext } from 'react'; | ||||||
| 
 | 
 | ||||||
| interface Props { | import { useTranslation } from 'next-i18next'; | ||||||
|   model: OpenAIModel; |  | ||||||
|   models: OpenAIModel[]; |  | ||||||
|   defaultModelId: OpenAIModelID; |  | ||||||
|   onModelChange: (model: OpenAIModel) => void; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const ModelSelect: FC<Props> = ({ | import { OpenAIModel } from '@/types/openai'; | ||||||
|   model, | 
 | ||||||
|   models, | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|   defaultModelId, | 
 | ||||||
|   onModelChange, | export const ModelSelect = () => { | ||||||
| }) => { |  | ||||||
|   const { t } = useTranslation('chat'); |   const { t } = useTranslation('chat'); | ||||||
| 
 | 
 | ||||||
|  |   const { | ||||||
|  |     state: { selectedConversation, models, defaultModelId }, | ||||||
|  |     handleUpdateConversation, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | ||||||
|  |     selectedConversation && | ||||||
|  |       handleUpdateConversation(selectedConversation, { | ||||||
|  |         key: 'model', | ||||||
|  |         value: models.find( | ||||||
|  |           (model) => model.id === e.target.value, | ||||||
|  |         ) as OpenAIModel, | ||||||
|  |       }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col"> |     <div className="flex flex-col"> | ||||||
|       <label className="mb-2 text-left text-neutral-700 dark:text-neutral-400"> |       <label className="mb-2 text-left text-neutral-700 dark:text-neutral-400"> | ||||||
|  | @ -27,14 +35,8 @@ export const ModelSelect: FC<Props> = ({ | ||||||
|         <select |         <select | ||||||
|           className="w-full bg-transparent p-2" |           className="w-full bg-transparent p-2" | ||||||
|           placeholder={t('Select a model') || ''} |           placeholder={t('Select a model') || ''} | ||||||
|           value={model?.id || defaultModelId} |           value={selectedConversation?.model?.id || defaultModelId} | ||||||
|           onChange={(e) => { |           onChange={handleChange} | ||||||
|             onModelChange( |  | ||||||
|               models.find( |  | ||||||
|                 (model) => model.id === e.target.value, |  | ||||||
|               ) as OpenAIModel, |  | ||||||
|             ); |  | ||||||
|           }} |  | ||||||
|         > |         > | ||||||
|           {models.map((model) => ( |           {models.map((model) => ( | ||||||
|             <option |             <option | ||||||
|  | @ -50,8 +52,12 @@ export const ModelSelect: FC<Props> = ({ | ||||||
|         </select> |         </select> | ||||||
|       </div> |       </div> | ||||||
|       <div className="w-full mt-3 text-left text-neutral-700 dark:text-neutral-400 flex items-center"> |       <div className="w-full mt-3 text-left text-neutral-700 dark:text-neutral-400 flex items-center"> | ||||||
|         <a href="https://platform.openai.com/account/usage" target="_blank" className="flex items-center"> |         <a | ||||||
|           <IconExternalLink size={18} className={"inline mr-1"} /> |           href="https://platform.openai.com/account/usage" | ||||||
|  |           target="_blank" | ||||||
|  |           className="flex items-center" | ||||||
|  |         > | ||||||
|  |           <IconExternalLink size={18} className={'inline mr-1'} /> | ||||||
|           {t('View Account Usage')} |           {t('View Account Usage')} | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| import { Plugin, PluginList } from '@/types/plugin'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC, useEffect, useRef } from 'react'; | import { FC, useEffect, useRef } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { Plugin, PluginList } from '@/types/plugin'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   plugin: Plugin | null; |   plugin: Plugin | null; | ||||||
|   onPluginChange: (plugin: Plugin) => void; |   onPluginChange: (plugin: Plugin) => void; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { FC, MutableRefObject } from 'react'; | import { FC, MutableRefObject } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   prompts: Prompt[]; |   prompts: Prompt[]; | ||||||
|   activePromptIndex: number; |   activePromptIndex: number; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| import { IconRefresh } from '@tabler/icons-react'; | import { IconRefresh } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   onRegenerate: () => void; |   onRegenerate: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,8 +1,3 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { OpenAIModelID } from '@/types/openai'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { | import { | ||||||
|   FC, |   FC, | ||||||
|   KeyboardEvent, |   KeyboardEvent, | ||||||
|  | @ -11,6 +6,14 @@ import { | ||||||
|   useRef, |   useRef, | ||||||
|   useState, |   useState, | ||||||
| } from 'react'; | } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | ||||||
|  | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
| import { PromptList } from './PromptList'; | import { PromptList } from './PromptList'; | ||||||
| import { VariableModal } from './VariableModal'; | import { VariableModal } from './VariableModal'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   prompt: Prompt; |   prompt: Prompt; | ||||||
|   variables: string[]; |   variables: string[]; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | import { Dispatch, createContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { ActionType } from '@/hooks/useCreateReducer'; | ||||||
|  | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | import { SupportedExportFormats } from '@/types/export'; | ||||||
|  | import { PluginKey } from '@/types/plugin'; | ||||||
|  | 
 | ||||||
|  | import { ChatbarInitialState } from './Chatbar.state'; | ||||||
|  | 
 | ||||||
|  | export interface ChatbarContextProps { | ||||||
|  |   state: ChatbarInitialState; | ||||||
|  |   dispatch: Dispatch<ActionType<ChatbarInitialState>>; | ||||||
|  |   handleDeleteConversation: (conversation: Conversation) => void; | ||||||
|  |   handleClearConversations: () => void; | ||||||
|  |   handleExportData: () => void; | ||||||
|  |   handleImportConversations: (data: SupportedExportFormats) => void; | ||||||
|  |   handlePluginKeyChange: (pluginKey: PluginKey) => void; | ||||||
|  |   handleClearPluginKey: (pluginKey: PluginKey) => void; | ||||||
|  |   handleApiKeyChange: (apiKey: string) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const ChatbarContext = createContext<ChatbarContextProps>(undefined!); | ||||||
|  | 
 | ||||||
|  | export default ChatbarContext; | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | 
 | ||||||
|  | export interface ChatbarInitialState { | ||||||
|  |   searchTerm: string; | ||||||
|  |   filteredConversations: Conversation[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const initialState: ChatbarInitialState = { | ||||||
|  |   searchTerm: '', | ||||||
|  |   filteredConversations: [], | ||||||
|  | }; | ||||||
|  | @ -1,219 +1,237 @@ | ||||||
| import { Conversation } from '@/types/chat'; | import { useCallback, useContext, useEffect } from 'react'; | ||||||
| import { KeyValuePair } from '@/types/data'; | 
 | ||||||
| import { SupportedExportFormats } from '@/types/export'; |  | ||||||
| import { Folder } from '@/types/folder'; |  | ||||||
| import { PluginKey } from '@/types/plugin'; |  | ||||||
| import { IconFolderPlus, IconMessagesOff, IconPlus } from '@tabler/icons-react'; |  | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| import { FC, useEffect, useState } from 'react'; |  | ||||||
| import { ChatFolders } from '../Folders/Chat/ChatFolders'; |  | ||||||
| import { Search } from '../Sidebar/Search'; |  | ||||||
| import { ChatbarSettings } from './ChatbarSettings'; |  | ||||||
| import { Conversations } from './Conversations'; |  | ||||||
| 
 | 
 | ||||||
| interface Props { | import { useCreateReducer } from '@/hooks/useCreateReducer'; | ||||||
|   loading: boolean; |  | ||||||
|   conversations: Conversation[]; |  | ||||||
|   lightMode: 'light' | 'dark'; |  | ||||||
|   selectedConversation: Conversation; |  | ||||||
|   apiKey: string; |  | ||||||
|   serverSideApiKeyIsSet: boolean; |  | ||||||
|   pluginKeys: PluginKey[]; |  | ||||||
|   serverSidePluginKeysSet: boolean; |  | ||||||
|   folders: Folder[]; |  | ||||||
|   onCreateFolder: (name: string) => void; |  | ||||||
|   onDeleteFolder: (folderId: string) => void; |  | ||||||
|   onUpdateFolder: (folderId: string, name: string) => void; |  | ||||||
|   onNewConversation: () => void; |  | ||||||
|   onToggleLightMode: (mode: 'light' | 'dark') => void; |  | ||||||
|   onSelectConversation: (conversation: Conversation) => void; |  | ||||||
|   onDeleteConversation: (conversation: Conversation) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
|   onApiKeyChange: (apiKey: string) => void; |  | ||||||
|   onClearConversations: () => void; |  | ||||||
|   onExportConversations: () => void; |  | ||||||
|   onImportConversations: (data: SupportedExportFormats) => void; |  | ||||||
|   onPluginKeyChange: (pluginKey: PluginKey) => void; |  | ||||||
|   onClearPluginKey: (pluginKey: PluginKey) => void; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const Chatbar: FC<Props> = ({ | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | ||||||
|   loading, | import { saveConversation, saveConversations } from '@/utils/app/conversation'; | ||||||
|   conversations, | import { saveFolders } from '@/utils/app/folders'; | ||||||
|   lightMode, | import { exportData, importData } from '@/utils/app/importExport'; | ||||||
|   selectedConversation, | 
 | ||||||
|   apiKey, | import { Conversation } from '@/types/chat'; | ||||||
|   serverSideApiKeyIsSet, | import { LatestExportFormat, SupportedExportFormats } from '@/types/export'; | ||||||
|   pluginKeys, | import { OpenAIModels } from '@/types/openai'; | ||||||
|   serverSidePluginKeysSet, | import { PluginKey } from '@/types/plugin'; | ||||||
|   folders, | 
 | ||||||
|   onCreateFolder, | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|   onDeleteFolder, | 
 | ||||||
|   onUpdateFolder, | import { ChatFolders } from './components/ChatFolders'; | ||||||
|   onNewConversation, | import { ChatbarSettings } from './components/ChatbarSettings'; | ||||||
|   onToggleLightMode, | import { Conversations } from './components/Conversations'; | ||||||
|   onSelectConversation, | 
 | ||||||
|   onDeleteConversation, | import Sidebar from '../Sidebar'; | ||||||
|   onUpdateConversation, | import ChatbarContext from './Chatbar.context'; | ||||||
|   onApiKeyChange, | import { ChatbarInitialState, initialState } from './Chatbar.state'; | ||||||
|   onClearConversations, | 
 | ||||||
|   onExportConversations, | import { v4 as uuidv4 } from 'uuid'; | ||||||
|   onImportConversations, | 
 | ||||||
|   onPluginKeyChange, | export const Chatbar = () => { | ||||||
|   onClearPluginKey, |  | ||||||
| }) => { |  | ||||||
|   const { t } = useTranslation('sidebar'); |   const { t } = useTranslation('sidebar'); | ||||||
|   const [searchTerm, setSearchTerm] = useState<string>(''); |  | ||||||
|   const [filteredConversations, setFilteredConversations] = |  | ||||||
|     useState<Conversation[]>(conversations); |  | ||||||
| 
 | 
 | ||||||
|   const handleUpdateConversation = ( |   const chatBarContextValue = useCreateReducer<ChatbarInitialState>({ | ||||||
|     conversation: Conversation, |     initialState, | ||||||
|     data: KeyValuePair, |   }); | ||||||
|   ) => { | 
 | ||||||
|     onUpdateConversation(conversation, data); |   const { | ||||||
|     setSearchTerm(''); |     state: { conversations, showChatbar, defaultModelId, folders, pluginKeys }, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |     handleCreateFolder, | ||||||
|  |     handleNewConversation, | ||||||
|  |     handleUpdateConversation, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { searchTerm, filteredConversations }, | ||||||
|  |     dispatch: chatDispatch, | ||||||
|  |   } = chatBarContextValue; | ||||||
|  | 
 | ||||||
|  |   const handleApiKeyChange = useCallback( | ||||||
|  |     (apiKey: string) => { | ||||||
|  |       homeDispatch({ field: 'apiKey', value: apiKey }); | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem('apiKey', apiKey); | ||||||
|  |     }, | ||||||
|  |     [homeDispatch], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   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; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       homeDispatch({ field: 'pluginKeys', value: updatedPluginKeys }); | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys)); | ||||||
|  |     } else { | ||||||
|  |       homeDispatch({ field: 'pluginKeys', value: [...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) { | ||||||
|  |       homeDispatch({ field: 'pluginKeys', value: [] }); | ||||||
|  |       localStorage.removeItem('pluginKeys'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     homeDispatch({ field: 'pluginKeys', value: updatedPluginKeys }); | ||||||
|  | 
 | ||||||
|  |     localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleExportData = () => { | ||||||
|  |     exportData(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleImportConversations = (data: SupportedExportFormats) => { | ||||||
|  |     const { history, folders, prompts }: LatestExportFormat = importData(data); | ||||||
|  |     homeDispatch({ field: 'conversations', value: history }); | ||||||
|  |     homeDispatch({ | ||||||
|  |       field: 'selectedConversation', | ||||||
|  |       value: history[history.length - 1], | ||||||
|  |     }); | ||||||
|  |     homeDispatch({ field: 'folders', value: folders }); | ||||||
|  |     homeDispatch({ field: 'prompts', value: prompts }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleClearConversations = () => { | ||||||
|  |     defaultModelId && | ||||||
|  |       homeDispatch({ | ||||||
|  |         field: 'selectedConversation', | ||||||
|  |         value: { | ||||||
|  |           id: uuidv4(), | ||||||
|  |           name: 'New conversation', | ||||||
|  |           messages: [], | ||||||
|  |           model: OpenAIModels[defaultModelId], | ||||||
|  |           prompt: DEFAULT_SYSTEM_PROMPT, | ||||||
|  |           folderId: null, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |     homeDispatch({ field: 'conversations', value: [] }); | ||||||
|  | 
 | ||||||
|  |     localStorage.removeItem('conversationHistory'); | ||||||
|  |     localStorage.removeItem('selectedConversation'); | ||||||
|  | 
 | ||||||
|  |     const updatedFolders = folders.filter((f) => f.type !== 'chat'); | ||||||
|  | 
 | ||||||
|  |     homeDispatch({ field: 'folders', value: updatedFolders }); | ||||||
|  |     saveFolders(updatedFolders); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDeleteConversation = (conversation: Conversation) => { |   const handleDeleteConversation = (conversation: Conversation) => { | ||||||
|     onDeleteConversation(conversation); |     const updatedConversations = conversations.filter( | ||||||
|     setSearchTerm(''); |       (c) => c.id !== conversation.id, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     homeDispatch({ field: 'conversations', value: updatedConversations }); | ||||||
|  |     chatDispatch({ field: 'searchTerm', value: '' }); | ||||||
|  |     saveConversations(updatedConversations); | ||||||
|  | 
 | ||||||
|  |     if (updatedConversations.length > 0) { | ||||||
|  |       homeDispatch({ | ||||||
|  |         field: 'selectedConversation', | ||||||
|  |         value: updatedConversations[updatedConversations.length - 1], | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       saveConversation(updatedConversations[updatedConversations.length - 1]); | ||||||
|  |     } else { | ||||||
|  |       defaultModelId && | ||||||
|  |         homeDispatch({ | ||||||
|  |           field: 'selectedConversation', | ||||||
|  |           value: { | ||||||
|  |             id: uuidv4(), | ||||||
|  |             name: 'New conversation', | ||||||
|  |             messages: [], | ||||||
|  |             model: OpenAIModels[defaultModelId], | ||||||
|  |             prompt: DEFAULT_SYSTEM_PROMPT, | ||||||
|  |             folderId: null, | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |       localStorage.removeItem('selectedConversation'); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleToggleChatbar = () => { | ||||||
|  |     homeDispatch({ field: 'showChatbar', value: !showChatbar }); | ||||||
|  |     localStorage.setItem('showChatbar', JSON.stringify(!showChatbar)); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDrop = (e: any) => { |   const handleDrop = (e: any) => { | ||||||
|     if (e.dataTransfer) { |     if (e.dataTransfer) { | ||||||
|       const conversation = JSON.parse(e.dataTransfer.getData('conversation')); |       const conversation = JSON.parse(e.dataTransfer.getData('conversation')); | ||||||
|       onUpdateConversation(conversation, { key: 'folderId', value: 0 }); |       handleUpdateConversation(conversation, { key: 'folderId', value: 0 }); | ||||||
| 
 |       chatDispatch({ field: 'searchTerm', value: '' }); | ||||||
|       e.target.style.background = 'none'; |       e.target.style.background = 'none'; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const allowDrop = (e: any) => { |  | ||||||
|     e.preventDefault(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const highlightDrop = (e: any) => { |  | ||||||
|     e.target.style.background = '#343541'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const removeHighlight = (e: any) => { |  | ||||||
|     e.target.style.background = 'none'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (searchTerm) { |     if (searchTerm) { | ||||||
|       setFilteredConversations( |       chatDispatch({ | ||||||
|         conversations.filter((conversation) => { |         field: 'filteredConversations', | ||||||
|  |         value: conversations.filter((conversation) => { | ||||||
|           const searchable = |           const searchable = | ||||||
|             conversation.name.toLocaleLowerCase() + |             conversation.name.toLocaleLowerCase() + | ||||||
|             ' ' + |             ' ' + | ||||||
|             conversation.messages.map((message) => message.content).join(' '); |             conversation.messages.map((message) => message.content).join(' '); | ||||||
|           return searchable.toLowerCase().includes(searchTerm.toLowerCase()); |           return searchable.toLowerCase().includes(searchTerm.toLowerCase()); | ||||||
|         }), |         }), | ||||||
|       ); |       }); | ||||||
|     } else { |     } else { | ||||||
|       setFilteredConversations(conversations); |       chatDispatch({ | ||||||
|  |         field: 'filteredConversations', | ||||||
|  |         value: conversations, | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   }, [searchTerm, conversations]); |   }, [searchTerm, conversations]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <ChatbarContext.Provider | ||||||
|       className={`fixed top-0 bottom-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 transition-all sm:relative sm:top-0`} |       value={{ | ||||||
|  |         ...chatBarContextValue, | ||||||
|  |         handleDeleteConversation, | ||||||
|  |         handleClearConversations, | ||||||
|  |         handleImportConversations, | ||||||
|  |         handleExportData, | ||||||
|  |         handlePluginKeyChange, | ||||||
|  |         handleClearPluginKey, | ||||||
|  |         handleApiKeyChange, | ||||||
|  |       }} | ||||||
|     > |     > | ||||||
|       <div className="flex items-center"> |       <Sidebar<Conversation> | ||||||
|         <button |         side={'left'} | ||||||
|           className="flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10" |         isOpen={showChatbar} | ||||||
|           onClick={() => { |         addItemButtonTitle={t('New chat')} | ||||||
|             onNewConversation(); |         itemComponent={<Conversations conversations={filteredConversations} />} | ||||||
|             setSearchTerm(''); |         folderComponent={<ChatFolders searchTerm={searchTerm} />} | ||||||
|           }} |         items={filteredConversations} | ||||||
|         > |         searchTerm={searchTerm} | ||||||
|           <IconPlus size={18} /> |         handleSearchTerm={(searchTerm: string) => | ||||||
|           {t('New chat')} |           chatDispatch({ field: 'searchTerm', value: searchTerm }) | ||||||
|         </button> |         } | ||||||
| 
 |         toggleOpen={handleToggleChatbar} | ||||||
|         <button |         handleCreateItem={handleNewConversation} | ||||||
|           className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10" |         handleCreateFolder={() => handleCreateFolder(t('New folder'), 'chat')} | ||||||
|           onClick={() => onCreateFolder(t('New folder'))} |         handleDrop={handleDrop} | ||||||
|         > |         footerComponent={<ChatbarSettings />} | ||||||
|           <IconFolderPlus size={18} /> |  | ||||||
|         </button> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       {conversations.length > 1 && ( |  | ||||||
|         <Search |  | ||||||
|           placeholder="Search conversations..." |  | ||||||
|           searchTerm={searchTerm} |  | ||||||
|           onSearch={setSearchTerm} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       <div className="flex-grow overflow-auto"> |  | ||||||
|         {folders.length > 0 && ( |  | ||||||
|           <div className="flex border-b border-white/20 pb-2"> |  | ||||||
|             <ChatFolders |  | ||||||
|               searchTerm={searchTerm} |  | ||||||
|               conversations={filteredConversations.filter( |  | ||||||
|                 (conversation) => conversation.folderId, |  | ||||||
|               )} |  | ||||||
|               folders={folders} |  | ||||||
|               onDeleteFolder={onDeleteFolder} |  | ||||||
|               onUpdateFolder={onUpdateFolder} |  | ||||||
|               selectedConversation={selectedConversation} |  | ||||||
|               loading={loading} |  | ||||||
|               onSelectConversation={onSelectConversation} |  | ||||||
|               onDeleteConversation={handleDeleteConversation} |  | ||||||
|               onUpdateConversation={handleUpdateConversation} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
| 
 |  | ||||||
|         {conversations.length > 0 ? ( |  | ||||||
|           <div |  | ||||||
|             className="pt-2" |  | ||||||
|             onDrop={(e) => handleDrop(e)} |  | ||||||
|             onDragOver={allowDrop} |  | ||||||
|             onDragEnter={highlightDrop} |  | ||||||
|             onDragLeave={removeHighlight} |  | ||||||
|           > |  | ||||||
|             <Conversations |  | ||||||
|               loading={loading} |  | ||||||
|               conversations={filteredConversations.filter( |  | ||||||
|                 (conversation) => !conversation.folderId, |  | ||||||
|               )} |  | ||||||
|               selectedConversation={selectedConversation} |  | ||||||
|               onSelectConversation={onSelectConversation} |  | ||||||
|               onDeleteConversation={handleDeleteConversation} |  | ||||||
|               onUpdateConversation={handleUpdateConversation} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         ) : ( |  | ||||||
|           <div className="mt-8 flex flex-col items-center gap-3 text-sm leading-normal text-white opacity-50"> |  | ||||||
|             <IconMessagesOff /> |  | ||||||
|             {t('No conversations.')} |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <ChatbarSettings |  | ||||||
|         lightMode={lightMode} |  | ||||||
|         apiKey={apiKey} |  | ||||||
|         serverSideApiKeyIsSet={serverSideApiKeyIsSet} |  | ||||||
|         pluginKeys={pluginKeys} |  | ||||||
|         serverSidePluginKeysSet={serverSidePluginKeysSet} |  | ||||||
|         conversationsCount={conversations.length} |  | ||||||
|         onToggleLightMode={onToggleLightMode} |  | ||||||
|         onApiKeyChange={onApiKeyChange} |  | ||||||
|         onClearConversations={onClearConversations} |  | ||||||
|         onExportConversations={onExportConversations} |  | ||||||
|         onImportConversations={onImportConversations} |  | ||||||
|         onPluginKeyChange={onPluginKeyChange} |  | ||||||
|         onClearPluginKey={onClearPluginKey} |  | ||||||
|       /> |       /> | ||||||
|     </div> |     </ChatbarContext.Provider> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,82 +0,0 @@ | ||||||
| import { SupportedExportFormats } from '@/types/export'; |  | ||||||
| import { PluginKey } from '@/types/plugin'; |  | ||||||
| import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC } from 'react'; |  | ||||||
| import { Import } from '../Settings/Import'; |  | ||||||
| import { Key } from '../Settings/Key'; |  | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; |  | ||||||
| import { ClearConversations } from './ClearConversations'; |  | ||||||
| import { PluginKeys } from './PluginKeys'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   lightMode: 'light' | 'dark'; |  | ||||||
|   apiKey: string; |  | ||||||
|   serverSideApiKeyIsSet: boolean; |  | ||||||
|   pluginKeys: PluginKey[]; |  | ||||||
|   serverSidePluginKeysSet: boolean; |  | ||||||
|   conversationsCount: number; |  | ||||||
|   onToggleLightMode: (mode: 'light' | 'dark') => void; |  | ||||||
|   onApiKeyChange: (apiKey: string) => void; |  | ||||||
|   onClearConversations: () => void; |  | ||||||
|   onExportConversations: () => void; |  | ||||||
|   onImportConversations: (data: SupportedExportFormats) => void; |  | ||||||
|   onPluginKeyChange: (pluginKey: PluginKey) => void; |  | ||||||
|   onClearPluginKey: (pluginKey: PluginKey) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ChatbarSettings: FC<Props> = ({ |  | ||||||
|   lightMode, |  | ||||||
|   apiKey, |  | ||||||
|   serverSideApiKeyIsSet, |  | ||||||
|   pluginKeys, |  | ||||||
|   serverSidePluginKeysSet, |  | ||||||
|   conversationsCount, |  | ||||||
|   onToggleLightMode, |  | ||||||
|   onApiKeyChange, |  | ||||||
|   onClearConversations, |  | ||||||
|   onExportConversations, |  | ||||||
|   onImportConversations, |  | ||||||
|   onPluginKeyChange, |  | ||||||
|   onClearPluginKey, |  | ||||||
| }) => { |  | ||||||
|   const { t } = useTranslation('sidebar'); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div className="flex flex-col items-center space-y-1 border-t border-white/20 pt-1 text-sm"> |  | ||||||
|       {conversationsCount > 0 ? ( |  | ||||||
|         <ClearConversations onClearConversations={onClearConversations} /> |  | ||||||
|       ) : null} |  | ||||||
| 
 |  | ||||||
|       <Import onImport={onImportConversations} /> |  | ||||||
| 
 |  | ||||||
|       <SidebarButton |  | ||||||
|         text={t('Export data')} |  | ||||||
|         icon={<IconFileExport size={18} />} |  | ||||||
|         onClick={() => onExportConversations()} |  | ||||||
|       /> |  | ||||||
| 
 |  | ||||||
|       <SidebarButton |  | ||||||
|         text={lightMode === 'light' ? t('Dark mode') : t('Light mode')} |  | ||||||
|         icon={ |  | ||||||
|           lightMode === 'light' ? <IconMoon size={18} /> : <IconSun size={18} /> |  | ||||||
|         } |  | ||||||
|         onClick={() => |  | ||||||
|           onToggleLightMode(lightMode === 'light' ? 'dark' : 'light') |  | ||||||
|         } |  | ||||||
|       /> |  | ||||||
| 
 |  | ||||||
|       {!(serverSideApiKeyIsSet) ? ( |  | ||||||
|         <Key apiKey={apiKey} onApiKeyChange={onApiKeyChange} /> |  | ||||||
|       ) : null} |  | ||||||
| 
 |  | ||||||
|       {!(serverSidePluginKeysSet) ? ( |  | ||||||
|         <PluginKeys |  | ||||||
|           pluginKeys={pluginKeys} |  | ||||||
|           onPluginKeyChange={onPluginKeyChange} |  | ||||||
|           onClearPluginKey={onClearPluginKey} |  | ||||||
|         /> |  | ||||||
|       ) : null} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,163 +0,0 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { |  | ||||||
|   IconCheck, |  | ||||||
|   IconMessage, |  | ||||||
|   IconPencil, |  | ||||||
|   IconTrash, |  | ||||||
|   IconX, |  | ||||||
| } from '@tabler/icons-react'; |  | ||||||
| import { DragEvent, FC, KeyboardEvent, useEffect, useState } from 'react'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   selectedConversation: Conversation; |  | ||||||
|   conversation: Conversation; |  | ||||||
|   loading: boolean; |  | ||||||
|   onSelectConversation: (conversation: Conversation) => void; |  | ||||||
|   onDeleteConversation: (conversation: Conversation) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ConversationComponent: FC<Props> = ({ |  | ||||||
|   selectedConversation, |  | ||||||
|   conversation, |  | ||||||
|   loading, |  | ||||||
|   onSelectConversation, |  | ||||||
|   onDeleteConversation, |  | ||||||
|   onUpdateConversation, |  | ||||||
| }) => { |  | ||||||
|   const [isDeleting, setIsDeleting] = useState(false); |  | ||||||
|   const [isRenaming, setIsRenaming] = useState(false); |  | ||||||
|   const [renameValue, setRenameValue] = useState(''); |  | ||||||
| 
 |  | ||||||
|   const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => { |  | ||||||
|     if (e.key === 'Enter') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       handleRename(selectedConversation); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleDragStart = ( |  | ||||||
|     e: DragEvent<HTMLButtonElement>, |  | ||||||
|     conversation: Conversation, |  | ||||||
|   ) => { |  | ||||||
|     if (e.dataTransfer) { |  | ||||||
|       e.dataTransfer.setData('conversation', JSON.stringify(conversation)); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleRename = (conversation: Conversation) => { |  | ||||||
|     if (renameValue.trim().length > 0) { |  | ||||||
|       onUpdateConversation(conversation, { key: 'name', value: renameValue }); |  | ||||||
|       setRenameValue(''); |  | ||||||
|       setIsRenaming(false); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (isRenaming) { |  | ||||||
|       setIsDeleting(false); |  | ||||||
|     } else if (isDeleting) { |  | ||||||
|       setIsRenaming(false); |  | ||||||
|     } |  | ||||||
|   }, [isRenaming, isDeleting]); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div className="relative flex items-center"> |  | ||||||
|       {isRenaming && selectedConversation.id === conversation.id ? ( |  | ||||||
|         <div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3 rounded-lg"> |  | ||||||
|           <IconMessage size={18} /> |  | ||||||
|           <input |  | ||||||
|             className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100" |  | ||||||
|             type="text" |  | ||||||
|             value={renameValue} |  | ||||||
|             onChange={(e) => setRenameValue(e.target.value)} |  | ||||||
|             onKeyDown={handleEnterDown} |  | ||||||
|             autoFocus |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       ) : ( |  | ||||||
|         <button |  | ||||||
|           className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90 ${ |  | ||||||
|             loading ? 'disabled:cursor-not-allowed' : '' |  | ||||||
|           } ${ |  | ||||||
|             selectedConversation.id === conversation.id ? 'bg-[#343541]/90' : '' |  | ||||||
|           }`}
 |  | ||||||
|           onClick={() => onSelectConversation(conversation)} |  | ||||||
|           disabled={loading} |  | ||||||
|           draggable="true" |  | ||||||
|           onDragStart={(e) => handleDragStart(e, conversation)} |  | ||||||
|         > |  | ||||||
|           <IconMessage size={18} /> |  | ||||||
|           <div |  | ||||||
|             className={`relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3 ${ |  | ||||||
|               selectedConversation.id === conversation.id ? 'pr-12' : 'pr-1' |  | ||||||
|             }`}
 |  | ||||||
|           > |  | ||||||
|             {conversation.name} |  | ||||||
|           </div> |  | ||||||
|         </button> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       {(isDeleting || isRenaming) && |  | ||||||
|         selectedConversation.id === conversation.id && ( |  | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 if (isDeleting) { |  | ||||||
|                   onDeleteConversation(conversation); |  | ||||||
|                 } else if (isRenaming) { |  | ||||||
|                   handleRename(conversation); |  | ||||||
|                 } |  | ||||||
|                 setIsDeleting(false); |  | ||||||
|                 setIsRenaming(false); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconCheck size={18} /> |  | ||||||
|             </button> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsDeleting(false); |  | ||||||
|                 setIsRenaming(false); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconX size={18} /> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
| 
 |  | ||||||
|       {selectedConversation.id === conversation.id && |  | ||||||
|         !isDeleting && |  | ||||||
|         !isRenaming && ( |  | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsRenaming(true); |  | ||||||
|                 setRenameValue(selectedConversation.name); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconPencil size={18} /> |  | ||||||
|             </button> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsDeleting(true); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconTrash size={18} /> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,44 +0,0 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { FC } from 'react'; |  | ||||||
| import { ConversationComponent } from './Conversation'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   loading: boolean; |  | ||||||
|   conversations: Conversation[]; |  | ||||||
|   selectedConversation: Conversation; |  | ||||||
|   onSelectConversation: (conversation: Conversation) => void; |  | ||||||
|   onDeleteConversation: (conversation: Conversation) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const Conversations: FC<Props> = ({ |  | ||||||
|   loading, |  | ||||||
|   conversations, |  | ||||||
|   selectedConversation, |  | ||||||
|   onSelectConversation, |  | ||||||
|   onDeleteConversation, |  | ||||||
|   onUpdateConversation, |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="flex w-full flex-col gap-1"> |  | ||||||
|       {conversations |  | ||||||
|         .slice() |  | ||||||
|         .reverse() |  | ||||||
|         .map((conversation, index) => ( |  | ||||||
|           <ConversationComponent |  | ||||||
|             key={index} |  | ||||||
|             selectedConversation={selectedConversation} |  | ||||||
|             conversation={conversation} |  | ||||||
|             loading={loading} |  | ||||||
|             onSelectConversation={onSelectConversation} |  | ||||||
|             onDeleteConversation={onDeleteConversation} |  | ||||||
|             onUpdateConversation={onUpdateConversation} |  | ||||||
|           /> |  | ||||||
|         ))} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | import { useContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { FolderInterface } from '@/types/folder'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import Folder from '@/components/Folder'; | ||||||
|  | 
 | ||||||
|  | import { ConversationComponent } from './Conversation'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   searchTerm: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const ChatFolders = ({ searchTerm }: Props) => { | ||||||
|  |   const { | ||||||
|  |     state: { folders, conversations }, | ||||||
|  |     handleUpdateConversation, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const handleDrop = (e: any, folder: FolderInterface) => { | ||||||
|  |     if (e.dataTransfer) { | ||||||
|  |       const conversation = JSON.parse(e.dataTransfer.getData('conversation')); | ||||||
|  |       handleUpdateConversation(conversation, { | ||||||
|  |         key: 'folderId', | ||||||
|  |         value: folder.id, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const ChatFolders = (currentFolder: FolderInterface) => { | ||||||
|  |     return ( | ||||||
|  |       conversations && | ||||||
|  |       conversations | ||||||
|  |         .filter((conversation) => conversation.folderId) | ||||||
|  |         .map((conversation, index) => { | ||||||
|  |           if (conversation.folderId === currentFolder.id) { | ||||||
|  |             return ( | ||||||
|  |               <div key={index} className="ml-5 gap-2 border-l pl-2"> | ||||||
|  |                 <ConversationComponent conversation={conversation} /> | ||||||
|  |               </div> | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="flex w-full flex-col pt-2"> | ||||||
|  |       {folders | ||||||
|  |         .filter((folder) => folder.type === 'chat') | ||||||
|  |         .map((folder, index) => ( | ||||||
|  |           <Folder | ||||||
|  |             key={index} | ||||||
|  |             searchTerm={searchTerm} | ||||||
|  |             currentFolder={folder} | ||||||
|  |             handleDrop={handleDrop} | ||||||
|  |             folderComponent={ChatFolders(folder)} | ||||||
|  |           /> | ||||||
|  |         ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,71 @@ | ||||||
|  | import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react'; | ||||||
|  | import { useContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import { Import } from '../../Settings/Import'; | ||||||
|  | import { Key } from '../../Settings/Key'; | ||||||
|  | import { SidebarButton } from '../../Sidebar/SidebarButton'; | ||||||
|  | import ChatbarContext from '../Chatbar.context'; | ||||||
|  | import { ClearConversations } from './ClearConversations'; | ||||||
|  | import { PluginKeys } from './PluginKeys'; | ||||||
|  | 
 | ||||||
|  | export const ChatbarSettings = () => { | ||||||
|  |   const { t } = useTranslation('sidebar'); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { | ||||||
|  |       apiKey, | ||||||
|  |       lightMode, | ||||||
|  |       serverSideApiKeyIsSet, | ||||||
|  |       serverSidePluginKeysSet, | ||||||
|  |       conversations, | ||||||
|  |     }, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     handleClearConversations, | ||||||
|  |     handleImportConversations, | ||||||
|  |     handleExportData, | ||||||
|  | 
 | ||||||
|  |     handleApiKeyChange, | ||||||
|  |   } = useContext(ChatbarContext); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-col items-center space-y-1 border-t border-white/20 pt-1 text-sm"> | ||||||
|  |       {conversations.length > 0 ? ( | ||||||
|  |         <ClearConversations onClearConversations={handleClearConversations} /> | ||||||
|  |       ) : null} | ||||||
|  | 
 | ||||||
|  |       <Import onImport={handleImportConversations} /> | ||||||
|  | 
 | ||||||
|  |       <SidebarButton | ||||||
|  |         text={t('Export data')} | ||||||
|  |         icon={<IconFileExport size={18} />} | ||||||
|  |         onClick={() => handleExportData()} | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <SidebarButton | ||||||
|  |         text={lightMode === 'light' ? t('Dark mode') : t('Light mode')} | ||||||
|  |         icon={ | ||||||
|  |           lightMode === 'light' ? <IconMoon size={18} /> : <IconSun size={18} /> | ||||||
|  |         } | ||||||
|  |         onClick={() => | ||||||
|  |           homeDispatch({ | ||||||
|  |             field: 'lightMode', | ||||||
|  |             value: lightMode === 'light' ? 'dark' : 'light', | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       {!serverSideApiKeyIsSet ? ( | ||||||
|  |         <Key apiKey={apiKey} onApiKeyChange={handleApiKeyChange} /> | ||||||
|  |       ) : null} | ||||||
|  | 
 | ||||||
|  |       {!serverSidePluginKeysSet ? <PluginKeys /> : null} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| import { IconCheck, IconTrash, IconX } from '@tabler/icons-react'; | import { IconCheck, IconTrash, IconX } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC, useState } from 'react'; | import { FC, useState } from 'react'; | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { SidebarButton } from '@/components/Sidebar/SidebarButton'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   onClearConversations: () => void; |   onClearConversations: () => void; | ||||||
|  | @ -27,7 +29,7 @@ export const ClearConversations: FC<Props> = ({ onClearConversations }) => { | ||||||
| 
 | 
 | ||||||
|       <div className="flex w-[40px]"> |       <div className="flex w-[40px]"> | ||||||
|         <IconCheck |         <IconCheck | ||||||
|           className="ml-auto min-w-[20px] mr-1 text-neutral-400 hover:text-neutral-100" |           className="ml-auto mr-1 min-w-[20px] text-neutral-400 hover:text-neutral-100" | ||||||
|           size={18} |           size={18} | ||||||
|           onClick={(e) => { |           onClick={(e) => { | ||||||
|             e.stopPropagation(); |             e.stopPropagation(); | ||||||
|  | @ -0,0 +1,168 @@ | ||||||
|  | import { | ||||||
|  |   IconCheck, | ||||||
|  |   IconMessage, | ||||||
|  |   IconPencil, | ||||||
|  |   IconTrash, | ||||||
|  |   IconX, | ||||||
|  | } from '@tabler/icons-react'; | ||||||
|  | import { | ||||||
|  |   DragEvent, | ||||||
|  |   KeyboardEvent, | ||||||
|  |   MouseEventHandler, | ||||||
|  |   useContext, | ||||||
|  |   useEffect, | ||||||
|  |   useState, | ||||||
|  | } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import SidebarActionButton from '@/components/Buttons/SidebarActionButton'; | ||||||
|  | import ChatbarContext from '@/components/Chatbar/Chatbar.context'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   conversation: Conversation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const ConversationComponent = ({ conversation }: Props) => { | ||||||
|  |   const { | ||||||
|  |     state: { selectedConversation, messageIsStreaming }, | ||||||
|  |     handleSelectConversation, | ||||||
|  |     handleUpdateConversation, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { handleDeleteConversation } = useContext(ChatbarContext); | ||||||
|  | 
 | ||||||
|  |   const [isDeleting, setIsDeleting] = useState(false); | ||||||
|  |   const [isRenaming, setIsRenaming] = useState(false); | ||||||
|  |   const [renameValue, setRenameValue] = useState(''); | ||||||
|  | 
 | ||||||
|  |   const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => { | ||||||
|  |     if (e.key === 'Enter') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       selectedConversation && handleRename(selectedConversation); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDragStart = ( | ||||||
|  |     e: DragEvent<HTMLButtonElement>, | ||||||
|  |     conversation: Conversation, | ||||||
|  |   ) => { | ||||||
|  |     if (e.dataTransfer) { | ||||||
|  |       e.dataTransfer.setData('conversation', JSON.stringify(conversation)); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleRename = (conversation: Conversation) => { | ||||||
|  |     if (renameValue.trim().length > 0) { | ||||||
|  |       handleUpdateConversation(conversation, { | ||||||
|  |         key: 'name', | ||||||
|  |         value: renameValue, | ||||||
|  |       }); | ||||||
|  |       setRenameValue(''); | ||||||
|  |       setIsRenaming(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleConfirm: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     if (isDeleting) { | ||||||
|  |       handleDeleteConversation(conversation); | ||||||
|  |     } else if (isRenaming) { | ||||||
|  |       handleRename(conversation); | ||||||
|  |     } | ||||||
|  |     setIsDeleting(false); | ||||||
|  |     setIsRenaming(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleCancel: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     setIsDeleting(false); | ||||||
|  |     setIsRenaming(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleOpenRenameModal: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     setIsRenaming(true); | ||||||
|  |     selectedConversation && setRenameValue(selectedConversation.name); | ||||||
|  |   }; | ||||||
|  |   const handleOpenDeleteModal: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     setIsDeleting(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isRenaming) { | ||||||
|  |       setIsDeleting(false); | ||||||
|  |     } else if (isDeleting) { | ||||||
|  |       setIsRenaming(false); | ||||||
|  |     } | ||||||
|  |   }, [isRenaming, isDeleting]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="relative flex items-center"> | ||||||
|  |       {isRenaming && selectedConversation?.id === conversation.id ? ( | ||||||
|  |         <div className="flex w-full items-center gap-3 rounded-lg bg-[#343541]/90 p-3"> | ||||||
|  |           <IconMessage size={18} /> | ||||||
|  |           <input | ||||||
|  |             className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100" | ||||||
|  |             type="text" | ||||||
|  |             value={renameValue} | ||||||
|  |             onChange={(e) => setRenameValue(e.target.value)} | ||||||
|  |             onKeyDown={handleEnterDown} | ||||||
|  |             autoFocus | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       ) : ( | ||||||
|  |         <button | ||||||
|  |           className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90 ${ | ||||||
|  |             messageIsStreaming ? 'disabled:cursor-not-allowed' : '' | ||||||
|  |           } ${ | ||||||
|  |             selectedConversation?.id === conversation.id | ||||||
|  |               ? 'bg-[#343541]/90' | ||||||
|  |               : '' | ||||||
|  |           }`}
 | ||||||
|  |           onClick={() => handleSelectConversation(conversation)} | ||||||
|  |           disabled={messageIsStreaming} | ||||||
|  |           draggable="true" | ||||||
|  |           onDragStart={(e) => handleDragStart(e, conversation)} | ||||||
|  |         > | ||||||
|  |           <IconMessage size={18} /> | ||||||
|  |           <div | ||||||
|  |             className={`relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3 ${ | ||||||
|  |               selectedConversation?.id === conversation.id ? 'pr-12' : 'pr-1' | ||||||
|  |             }`}
 | ||||||
|  |           > | ||||||
|  |             {conversation.name} | ||||||
|  |           </div> | ||||||
|  |         </button> | ||||||
|  |       )} | ||||||
|  | 
 | ||||||
|  |       {(isDeleting || isRenaming) && | ||||||
|  |         selectedConversation?.id === conversation.id && ( | ||||||
|  |           <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|  |             <SidebarActionButton handleClick={handleConfirm}> | ||||||
|  |               <IconCheck size={18} /> | ||||||
|  |             </SidebarActionButton> | ||||||
|  |             <SidebarActionButton handleClick={handleCancel}> | ||||||
|  |               <IconX size={18} /> | ||||||
|  |             </SidebarActionButton> | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  | 
 | ||||||
|  |       {selectedConversation?.id === conversation.id && | ||||||
|  |         !isDeleting && | ||||||
|  |         !isRenaming && ( | ||||||
|  |           <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|  |             <SidebarActionButton handleClick={handleOpenRenameModal}> | ||||||
|  |               <IconPencil size={18} /> | ||||||
|  |             </SidebarActionButton> | ||||||
|  |             <SidebarActionButton handleClick={handleOpenDeleteModal}> | ||||||
|  |               <IconTrash size={18} /> | ||||||
|  |             </SidebarActionButton> | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | 
 | ||||||
|  | import { ConversationComponent } from './Conversation'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   conversations: Conversation[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Conversations = ({ conversations }: Props) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex w-full flex-col gap-1"> | ||||||
|  |       {conversations | ||||||
|  |         .filter((conversation) => !conversation.folderId) | ||||||
|  |         .slice() | ||||||
|  |         .reverse() | ||||||
|  |         .map((conversation, index) => ( | ||||||
|  |           <ConversationComponent key={index} conversation={conversation} /> | ||||||
|  |         ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -1,22 +1,25 @@ | ||||||
| import { PluginID, PluginKey } from '@/types/plugin'; |  | ||||||
| import { IconKey } from '@tabler/icons-react'; | import { IconKey } from '@tabler/icons-react'; | ||||||
| import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | import { KeyboardEvent, useContext, useEffect, useRef, useState } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; |  | ||||||
| 
 | 
 | ||||||
| interface Props { | import { PluginID, PluginKey } from '@/types/plugin'; | ||||||
|   pluginKeys: PluginKey[]; |  | ||||||
|   onPluginKeyChange: (pluginKey: PluginKey) => void; |  | ||||||
|   onClearPluginKey: (pluginKey: PluginKey) => void; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const PluginKeys: FC<Props> = ({ | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|   pluginKeys, | 
 | ||||||
|   onPluginKeyChange, | import { SidebarButton } from '@/components/Sidebar/SidebarButton'; | ||||||
|   onClearPluginKey, | 
 | ||||||
| }) => { | import ChatbarContext from '../Chatbar.context'; | ||||||
|  | 
 | ||||||
|  | export const PluginKeys = () => { | ||||||
|   const { t } = useTranslation('sidebar'); |   const { t } = useTranslation('sidebar'); | ||||||
| 
 | 
 | ||||||
|  |   const { | ||||||
|  |     state: { pluginKeys }, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { handlePluginKeyChange, handleClearPluginKey } = | ||||||
|  |     useContext(ChatbarContext); | ||||||
|  | 
 | ||||||
|   const [isChanging, setIsChanging] = useState(false); |   const [isChanging, setIsChanging] = useState(false); | ||||||
| 
 | 
 | ||||||
|   const modalRef = useRef<HTMLDivElement>(null); |   const modalRef = useRef<HTMLDivElement>(null); | ||||||
|  | @ -118,7 +121,7 @@ export const PluginKeys: FC<Props> = ({ | ||||||
|                             }), |                             }), | ||||||
|                           }; |                           }; | ||||||
| 
 | 
 | ||||||
|                           onPluginKeyChange(updatedPluginKey); |                           handlePluginKeyChange(updatedPluginKey); | ||||||
|                         } |                         } | ||||||
|                       } else { |                       } else { | ||||||
|                         const newPluginKey: PluginKey = { |                         const newPluginKey: PluginKey = { | ||||||
|  | @ -135,7 +138,7 @@ export const PluginKeys: FC<Props> = ({ | ||||||
|                           ], |                           ], | ||||||
|                         }; |                         }; | ||||||
| 
 | 
 | ||||||
|                         onPluginKeyChange(newPluginKey); |                         handlePluginKeyChange(newPluginKey); | ||||||
|                       } |                       } | ||||||
|                     }} |                     }} | ||||||
|                   /> |                   /> | ||||||
|  | @ -177,7 +180,7 @@ export const PluginKeys: FC<Props> = ({ | ||||||
|                             }), |                             }), | ||||||
|                           }; |                           }; | ||||||
| 
 | 
 | ||||||
|                           onPluginKeyChange(updatedPluginKey); |                           handlePluginKeyChange(updatedPluginKey); | ||||||
|                         } |                         } | ||||||
|                       } else { |                       } else { | ||||||
|                         const newPluginKey: PluginKey = { |                         const newPluginKey: PluginKey = { | ||||||
|  | @ -194,7 +197,7 @@ export const PluginKeys: FC<Props> = ({ | ||||||
|                           ], |                           ], | ||||||
|                         }; |                         }; | ||||||
| 
 | 
 | ||||||
|                         onPluginKeyChange(newPluginKey); |                         handlePluginKeyChange(newPluginKey); | ||||||
|                       } |                       } | ||||||
|                     }} |                     }} | ||||||
|                   /> |                   /> | ||||||
|  | @ -207,7 +210,7 @@ export const PluginKeys: FC<Props> = ({ | ||||||
|                       ); |                       ); | ||||||
| 
 | 
 | ||||||
|                       if (pluginKey) { |                       if (pluginKey) { | ||||||
|                         onClearPluginKey(pluginKey); |                         handleClearPluginKey(pluginKey); | ||||||
|                       } |                       } | ||||||
|                     }} |                     }} | ||||||
|                   > |                   > | ||||||
|  | @ -1,6 +1,3 @@ | ||||||
| import { PromptComponent } from '@/components/Promptbar/Prompt'; |  | ||||||
| import { Folder } from '@/types/folder'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { | import { | ||||||
|   IconCaretDown, |   IconCaretDown, | ||||||
|   IconCaretRight, |   IconCaretRight, | ||||||
|  | @ -9,29 +6,35 @@ import { | ||||||
|   IconTrash, |   IconTrash, | ||||||
|   IconX, |   IconX, | ||||||
| } from '@tabler/icons-react'; | } from '@tabler/icons-react'; | ||||||
| import { FC, KeyboardEvent, useEffect, useState } from 'react'; | import { | ||||||
|  |   KeyboardEvent, | ||||||
|  |   ReactElement, | ||||||
|  |   useContext, | ||||||
|  |   useEffect, | ||||||
|  |   useState, | ||||||
|  | } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { FolderInterface } from '@/types/folder'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import SidebarActionButton from '@/components/Buttons/SidebarActionButton'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  |   currentFolder: FolderInterface; | ||||||
|   searchTerm: string; |   searchTerm: string; | ||||||
|   prompts: Prompt[]; |   handleDrop: (e: any, folder: FolderInterface) => void; | ||||||
|   currentFolder: Folder; |   folderComponent: (ReactElement | undefined)[]; | ||||||
|   onDeleteFolder: (folder: string) => void; |  | ||||||
|   onUpdateFolder: (folder: string, name: string) => void; |  | ||||||
|   // prompt props
 |  | ||||||
|   onDeletePrompt: (prompt: Prompt) => void; |  | ||||||
|   onUpdatePrompt: (prompt: Prompt) => void; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const PromptFolder: FC<Props> = ({ | const Folder = ({ | ||||||
|   searchTerm, |  | ||||||
|   prompts, |  | ||||||
|   currentFolder, |   currentFolder, | ||||||
|   onDeleteFolder, |   searchTerm, | ||||||
|   onUpdateFolder, |   handleDrop, | ||||||
|   // prompt props
 |   folderComponent, | ||||||
|   onDeletePrompt, | }: Props) => { | ||||||
|   onUpdatePrompt, |   const { handleDeleteFolder, handleUpdateFolder } = useContext(HomeContext); | ||||||
| }) => { | 
 | ||||||
|   const [isDeleting, setIsDeleting] = useState(false); |   const [isDeleting, setIsDeleting] = useState(false); | ||||||
|   const [isRenaming, setIsRenaming] = useState(false); |   const [isRenaming, setIsRenaming] = useState(false); | ||||||
|   const [renameValue, setRenameValue] = useState(''); |   const [renameValue, setRenameValue] = useState(''); | ||||||
|  | @ -45,23 +48,16 @@ export const PromptFolder: FC<Props> = ({ | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleRename = () => { |   const handleRename = () => { | ||||||
|     onUpdateFolder(currentFolder.id, renameValue); |     handleUpdateFolder(currentFolder.id, renameValue); | ||||||
|     setRenameValue(''); |     setRenameValue(''); | ||||||
|     setIsRenaming(false); |     setIsRenaming(false); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDrop = (e: any, folder: Folder) => { |   const dropHandler = (e: any) => { | ||||||
|     if (e.dataTransfer) { |     if (e.dataTransfer) { | ||||||
|       setIsOpen(true); |       setIsOpen(true); | ||||||
| 
 | 
 | ||||||
|       const prompt = JSON.parse(e.dataTransfer.getData('prompt')); |       handleDrop(e, currentFolder); | ||||||
| 
 |  | ||||||
|       const updatedPrompt = { |  | ||||||
|         ...prompt, |  | ||||||
|         folderId: folder.id, |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       onUpdatePrompt(updatedPrompt); |  | ||||||
| 
 | 
 | ||||||
|       e.target.style.background = 'none'; |       e.target.style.background = 'none'; | ||||||
|     } |     } | ||||||
|  | @ -118,7 +114,7 @@ export const PromptFolder: FC<Props> = ({ | ||||||
|           <button |           <button | ||||||
|             className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90`} |             className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90`} | ||||||
|             onClick={() => setIsOpen(!isOpen)} |             onClick={() => setIsOpen(!isOpen)} | ||||||
|             onDrop={(e) => handleDrop(e, currentFolder)} |             onDrop={(e) => dropHandler(e)} | ||||||
|             onDragOver={allowDrop} |             onDragOver={allowDrop} | ||||||
|             onDragEnter={highlightDrop} |             onDragEnter={highlightDrop} | ||||||
|             onDragLeave={removeHighlight} |             onDragLeave={removeHighlight} | ||||||
|  | @ -137,13 +133,12 @@ export const PromptFolder: FC<Props> = ({ | ||||||
| 
 | 
 | ||||||
|         {(isDeleting || isRenaming) && ( |         {(isDeleting || isRenaming) && ( | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |           <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|             <button |             <SidebarActionButton | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |               handleClick={(e) => { | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |                 e.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|                 if (isDeleting) { |                 if (isDeleting) { | ||||||
|                   onDeleteFolder(currentFolder.id); |                   handleDeleteFolder(currentFolder.id); | ||||||
|                 } else if (isRenaming) { |                 } else if (isRenaming) { | ||||||
|                   handleRename(); |                   handleRename(); | ||||||
|                 } |                 } | ||||||
|  | @ -153,60 +148,45 @@ export const PromptFolder: FC<Props> = ({ | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <IconCheck size={18} /> |               <IconCheck size={18} /> | ||||||
|             </button> |             </SidebarActionButton> | ||||||
|             <button |             <SidebarActionButton | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |               handleClick={(e) => { | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |                 e.stopPropagation(); | ||||||
|                 setIsDeleting(false); |                 setIsDeleting(false); | ||||||
|                 setIsRenaming(false); |                 setIsRenaming(false); | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <IconX size={18} /> |               <IconX size={18} /> | ||||||
|             </button> |             </SidebarActionButton> | ||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         {!isDeleting && !isRenaming && ( |         {!isDeleting && !isRenaming && ( | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |           <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|             <button |             <SidebarActionButton | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |               handleClick={(e) => { | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |                 e.stopPropagation(); | ||||||
|                 setIsRenaming(true); |                 setIsRenaming(true); | ||||||
|                 setRenameValue(currentFolder.name); |                 setRenameValue(currentFolder.name); | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <IconPencil size={18} /> |               <IconPencil size={18} /> | ||||||
|             </button> |             </SidebarActionButton> | ||||||
|             <button |             <SidebarActionButton | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |               handleClick={(e) => { | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |                 e.stopPropagation(); | ||||||
|                 setIsDeleting(true); |                 setIsDeleting(true); | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <IconTrash size={18} /> |               <IconTrash size={18} /> | ||||||
|             </button> |             </SidebarActionButton> | ||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       {isOpen |       {isOpen ? folderComponent : null} | ||||||
|         ? prompts.map((prompt, index) => { |  | ||||||
|             if (prompt.folderId === currentFolder.id) { |  | ||||||
|               return ( |  | ||||||
|                 <div key={index} className="ml-5 gap-2 border-l pl-2"> |  | ||||||
|                   <PromptComponent |  | ||||||
|                     prompt={prompt} |  | ||||||
|                     onDeletePrompt={onDeletePrompt} |  | ||||||
|                     onUpdatePrompt={onUpdatePrompt} |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|               ); |  | ||||||
|             } |  | ||||||
|           }) |  | ||||||
|         : null} |  | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default Folder; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './Folder'; | ||||||
|  | @ -1,220 +0,0 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { Folder } from '@/types/folder'; |  | ||||||
| import { |  | ||||||
|   IconCaretDown, |  | ||||||
|   IconCaretRight, |  | ||||||
|   IconCheck, |  | ||||||
|   IconPencil, |  | ||||||
|   IconTrash, |  | ||||||
|   IconX, |  | ||||||
| } from '@tabler/icons-react'; |  | ||||||
| import { FC, KeyboardEvent, useEffect, useState } from 'react'; |  | ||||||
| import { ConversationComponent } from '../../Chatbar/Conversation'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   searchTerm: string; |  | ||||||
|   conversations: Conversation[]; |  | ||||||
|   currentFolder: Folder; |  | ||||||
|   onDeleteFolder: (folder: string) => void; |  | ||||||
|   onUpdateFolder: (folder: string, name: string) => void; |  | ||||||
|   // conversation props
 |  | ||||||
|   selectedConversation: Conversation; |  | ||||||
|   loading: boolean; |  | ||||||
|   onSelectConversation: (conversation: Conversation) => void; |  | ||||||
|   onDeleteConversation: (conversation: Conversation) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ChatFolder: FC<Props> = ({ |  | ||||||
|   searchTerm, |  | ||||||
|   conversations, |  | ||||||
|   currentFolder, |  | ||||||
|   onDeleteFolder, |  | ||||||
|   onUpdateFolder, |  | ||||||
|   // conversation props
 |  | ||||||
|   selectedConversation, |  | ||||||
|   loading, |  | ||||||
|   onSelectConversation, |  | ||||||
|   onDeleteConversation, |  | ||||||
|   onUpdateConversation, |  | ||||||
| }) => { |  | ||||||
|   const [isDeleting, setIsDeleting] = useState(false); |  | ||||||
|   const [isRenaming, setIsRenaming] = useState(false); |  | ||||||
|   const [renameValue, setRenameValue] = useState(''); |  | ||||||
|   const [isOpen, setIsOpen] = useState(false); |  | ||||||
| 
 |  | ||||||
|   const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => { |  | ||||||
|     if (e.key === 'Enter') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       handleRename(); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleRename = () => { |  | ||||||
|     onUpdateFolder(currentFolder.id, renameValue); |  | ||||||
|     setRenameValue(''); |  | ||||||
|     setIsRenaming(false); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleDrop = (e: any, folder: Folder) => { |  | ||||||
|     if (e.dataTransfer) { |  | ||||||
|       setIsOpen(true); |  | ||||||
| 
 |  | ||||||
|       const conversation = JSON.parse(e.dataTransfer.getData('conversation')); |  | ||||||
|       onUpdateConversation(conversation, { key: 'folderId', value: folder.id }); |  | ||||||
| 
 |  | ||||||
|       e.target.style.background = 'none'; |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const allowDrop = (e: any) => { |  | ||||||
|     e.preventDefault(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const highlightDrop = (e: any) => { |  | ||||||
|     e.target.style.background = '#343541'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const removeHighlight = (e: any) => { |  | ||||||
|     e.target.style.background = 'none'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (isRenaming) { |  | ||||||
|       setIsDeleting(false); |  | ||||||
|     } else if (isDeleting) { |  | ||||||
|       setIsRenaming(false); |  | ||||||
|     } |  | ||||||
|   }, [isRenaming, isDeleting]); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (searchTerm) { |  | ||||||
|       setIsOpen(true); |  | ||||||
|     } else { |  | ||||||
|       setIsOpen(false); |  | ||||||
|     } |  | ||||||
|   }, [searchTerm]); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <div className="relative flex items-center"> |  | ||||||
|         {isRenaming ? ( |  | ||||||
|           <div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3 rounded-lg"> |  | ||||||
|             {isOpen ? ( |  | ||||||
|               <IconCaretDown size={18} /> |  | ||||||
|             ) : ( |  | ||||||
|               <IconCaretRight size={18} /> |  | ||||||
|             )} |  | ||||||
| 
 |  | ||||||
|             <input |  | ||||||
|               className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100" |  | ||||||
|               type="text" |  | ||||||
|               value={renameValue} |  | ||||||
|               onChange={(e) => setRenameValue(e.target.value)} |  | ||||||
|               onKeyDown={handleEnterDown} |  | ||||||
|               autoFocus |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         ) : ( |  | ||||||
|           <button |  | ||||||
|             className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90`} |  | ||||||
|             onClick={() => setIsOpen(!isOpen)} |  | ||||||
|             onDrop={(e) => handleDrop(e, currentFolder)} |  | ||||||
|             onDragOver={allowDrop} |  | ||||||
|             onDragEnter={highlightDrop} |  | ||||||
|             onDragLeave={removeHighlight} |  | ||||||
|           > |  | ||||||
|             {isOpen ? ( |  | ||||||
|               <IconCaretDown size={18} /> |  | ||||||
|             ) : ( |  | ||||||
|               <IconCaretRight size={18} /> |  | ||||||
|             )} |  | ||||||
| 
 |  | ||||||
|             <div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3"> |  | ||||||
|               {currentFolder.name} |  | ||||||
|             </div> |  | ||||||
|           </button> |  | ||||||
|         )} |  | ||||||
| 
 |  | ||||||
|         {(isDeleting || isRenaming) && ( |  | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
| 
 |  | ||||||
|                 if (isDeleting) { |  | ||||||
|                   onDeleteFolder(currentFolder.id); |  | ||||||
|                 } else if (isRenaming) { |  | ||||||
|                   handleRename(); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 setIsDeleting(false); |  | ||||||
|                 setIsRenaming(false); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconCheck size={18} /> |  | ||||||
|             </button> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsDeleting(false); |  | ||||||
|                 setIsRenaming(false); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconX size={18} /> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
| 
 |  | ||||||
|         {!isDeleting && !isRenaming && ( |  | ||||||
|           <div className="absolute right-1 z-10 flex text-gray-300"> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsRenaming(true); |  | ||||||
|                 setRenameValue(currentFolder.name); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconPencil size={18} /> |  | ||||||
|             </button> |  | ||||||
|             <button |  | ||||||
|               className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|               onClick={(e) => { |  | ||||||
|                 e.stopPropagation(); |  | ||||||
|                 setIsDeleting(true); |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <IconTrash size={18} /> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       {isOpen |  | ||||||
|         ? conversations.map((conversation, index) => { |  | ||||||
|           if (conversation.folderId === currentFolder.id) { |  | ||||||
|             return ( |  | ||||||
|               <div key={index} className="ml-5 gap-2 border-l pl-2"> |  | ||||||
|                 <ConversationComponent |  | ||||||
|                   selectedConversation={selectedConversation} |  | ||||||
|                   conversation={conversation} |  | ||||||
|                   loading={loading} |  | ||||||
|                   onSelectConversation={onSelectConversation} |  | ||||||
|                   onDeleteConversation={onDeleteConversation} |  | ||||||
|                   onUpdateConversation={onUpdateConversation} |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         : null} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,57 +0,0 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { Folder } from '@/types/folder'; |  | ||||||
| import { FC } from 'react'; |  | ||||||
| import { ChatFolder } from './ChatFolder'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   searchTerm: string; |  | ||||||
|   conversations: Conversation[]; |  | ||||||
|   folders: Folder[]; |  | ||||||
|   onDeleteFolder: (folder: string) => void; |  | ||||||
|   onUpdateFolder: (folder: string, name: string) => void; |  | ||||||
|   // conversation props
 |  | ||||||
|   selectedConversation: Conversation; |  | ||||||
|   loading: boolean; |  | ||||||
|   onSelectConversation: (conversation: Conversation) => void; |  | ||||||
|   onDeleteConversation: (conversation: Conversation) => void; |  | ||||||
|   onUpdateConversation: ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ChatFolders: FC<Props> = ({ |  | ||||||
|   searchTerm, |  | ||||||
|   conversations, |  | ||||||
|   folders, |  | ||||||
|   onDeleteFolder, |  | ||||||
|   onUpdateFolder, |  | ||||||
|   // conversation props
 |  | ||||||
|   selectedConversation, |  | ||||||
|   loading, |  | ||||||
|   onSelectConversation, |  | ||||||
|   onDeleteConversation, |  | ||||||
|   onUpdateConversation, |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="flex w-full flex-col pt-2"> |  | ||||||
|       {folders.map((folder, index) => ( |  | ||||||
|         <ChatFolder |  | ||||||
|           key={index} |  | ||||||
|           searchTerm={searchTerm} |  | ||||||
|           conversations={conversations.filter((c) => c.folderId)} |  | ||||||
|           currentFolder={folder} |  | ||||||
|           onDeleteFolder={onDeleteFolder} |  | ||||||
|           onUpdateFolder={onUpdateFolder} |  | ||||||
|           // conversation props
 |  | ||||||
|           selectedConversation={selectedConversation} |  | ||||||
|           loading={loading} |  | ||||||
|           onSelectConversation={onSelectConversation} |  | ||||||
|           onDeleteConversation={onDeleteConversation} |  | ||||||
|           onUpdateConversation={onUpdateConversation} |  | ||||||
|         /> |  | ||||||
|       ))} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,44 +0,0 @@ | ||||||
| import { Folder } from '@/types/folder'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { FC } from 'react'; |  | ||||||
| import { PromptFolder } from './PromptFolder'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   searchTerm: string; |  | ||||||
|   prompts: Prompt[]; |  | ||||||
|   folders: Folder[]; |  | ||||||
|   onDeleteFolder: (folder: string) => void; |  | ||||||
|   onUpdateFolder: (folder: string, name: string) => void; |  | ||||||
|   // prompt props
 |  | ||||||
|   onDeletePrompt: (prompt: Prompt) => void; |  | ||||||
|   onUpdatePrompt: (prompt: Prompt) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const PromptFolders: FC<Props> = ({ |  | ||||||
|   searchTerm, |  | ||||||
|   prompts, |  | ||||||
|   folders, |  | ||||||
|   onDeleteFolder, |  | ||||||
|   onUpdateFolder, |  | ||||||
|   // prompt props
 |  | ||||||
|   onDeletePrompt, |  | ||||||
|   onUpdatePrompt, |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="flex w-full flex-col pt-2"> |  | ||||||
|       {folders.map((folder, index) => ( |  | ||||||
|         <PromptFolder |  | ||||||
|           key={index} |  | ||||||
|           searchTerm={searchTerm} |  | ||||||
|           prompts={prompts.filter((p) => p.folderId)} |  | ||||||
|           currentFolder={folder} |  | ||||||
|           onDeleteFolder={onDeleteFolder} |  | ||||||
|           onUpdateFolder={onUpdateFolder} |  | ||||||
|           // prompt props
 |  | ||||||
|           onDeletePrompt={onDeletePrompt} |  | ||||||
|           onUpdatePrompt={onUpdatePrompt} |  | ||||||
|         /> |  | ||||||
|       ))} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
|  | import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react'; | ||||||
|  | import { FC, memo, useState } from 'react'; | ||||||
|  | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; | ||||||
|  | import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
| import { | import { | ||||||
|   generateRandomString, |   generateRandomString, | ||||||
|   programmingLanguages, |   programmingLanguages, | ||||||
| } from '@/utils/app/codeblock'; | } from '@/utils/app/codeblock'; | ||||||
| import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC, memo, useState } from 'react'; |  | ||||||
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; |  | ||||||
| import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; |  | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   language: string; |   language: string; | ||||||
|  | @ -67,11 +69,7 @@ export const CodeBlock: FC<Props> = memo(({ language, value }) => { | ||||||
|             className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white" |             className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white" | ||||||
|             onClick={copyToClipboard} |             onClick={copyToClipboard} | ||||||
|           > |           > | ||||||
|             {isCopied ? ( |             {isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />} | ||||||
|               <IconCheck size={18} /> |  | ||||||
|             ) : ( |  | ||||||
|               <IconClipboard size={18} /> |  | ||||||
|             )} |  | ||||||
|             {isCopied ? t('Copied!') : t('Copy code')} |             {isCopied ? t('Copied!') : t('Copy code')} | ||||||
|           </button> |           </button> | ||||||
|           <button |           <button | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| import { Conversation } from '@/types/chat'; |  | ||||||
| import { IconPlus } from '@tabler/icons-react'; | import { IconPlus } from '@tabler/icons-react'; | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   selectedConversation: Conversation; |   selectedConversation: Conversation; | ||||||
|   onNewConversation: () => void; |   onNewConversation: () => void; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | import { Dispatch, createContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { ActionType } from '@/hooks/useCreateReducer'; | ||||||
|  | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | import { PromptbarInitialState } from './Promptbar.state'; | ||||||
|  | 
 | ||||||
|  | export interface PromptbarContextProps { | ||||||
|  |   state: PromptbarInitialState; | ||||||
|  |   dispatch: Dispatch<ActionType<PromptbarInitialState>>; | ||||||
|  |   handleCreatePrompt: () => void; | ||||||
|  |   handleDeletePrompt: (prompt: Prompt) => void; | ||||||
|  |   handleUpdatePrompt: (prompt: Prompt) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const PromptbarContext = createContext<PromptbarContextProps>(undefined!); | ||||||
|  | 
 | ||||||
|  | export default PromptbarContext; | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | export interface PromptbarInitialState { | ||||||
|  |   searchTerm: string; | ||||||
|  |   filteredPrompts: Prompt[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const initialState: PromptbarInitialState = { | ||||||
|  |   searchTerm: '', | ||||||
|  |   filteredPrompts: [], | ||||||
|  | }; | ||||||
|  | @ -1,50 +1,85 @@ | ||||||
| import { Folder } from '@/types/folder'; | import { useContext, useEffect, useState } from 'react'; | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { |  | ||||||
|   IconFolderPlus, |  | ||||||
|   IconMistOff, |  | ||||||
|   IconPlus, |  | ||||||
| } from '@tabler/icons-react'; |  | ||||||
| import { FC, useEffect, useState } from 'react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { PromptFolders } from '../Folders/Prompt/PromptFolders'; |  | ||||||
| import { Search } from '../Sidebar/Search'; |  | ||||||
| import { PromptbarSettings } from './PromptbarSettings'; |  | ||||||
| import { Prompts } from './Prompts'; |  | ||||||
| 
 | 
 | ||||||
| interface Props { | import { useCreateReducer } from '@/hooks/useCreateReducer'; | ||||||
|   prompts: Prompt[]; |  | ||||||
|   folders: Folder[]; |  | ||||||
|   onCreateFolder: (name: string) => void; |  | ||||||
|   onDeleteFolder: (folderId: string) => void; |  | ||||||
|   onUpdateFolder: (folderId: string, name: string) => void; |  | ||||||
|   onCreatePrompt: () => void; |  | ||||||
|   onUpdatePrompt: (prompt: Prompt) => void; |  | ||||||
|   onDeletePrompt: (prompt: Prompt) => void; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const Promptbar: FC<Props> = ({ | import { savePrompts } from '@/utils/app/prompts'; | ||||||
|   folders, | 
 | ||||||
|   prompts, | import { OpenAIModels } from '@/types/openai'; | ||||||
|   onCreateFolder, | import { Prompt } from '@/types/prompt'; | ||||||
|   onDeleteFolder, | 
 | ||||||
|   onUpdateFolder, | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|   onCreatePrompt, | 
 | ||||||
|   onUpdatePrompt, | import { PromptFolders } from './components/PromptFolders'; | ||||||
|   onDeletePrompt, | import { PromptbarSettings } from './components/PromptbarSettings'; | ||||||
| }) => { | import { Prompts } from './components/Prompts'; | ||||||
|  | 
 | ||||||
|  | import Sidebar from '../Sidebar'; | ||||||
|  | import PromptbarContext from './PromptBar.context'; | ||||||
|  | import { PromptbarInitialState, initialState } from './Promptbar.state'; | ||||||
|  | 
 | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
|  | 
 | ||||||
|  | const Promptbar = () => { | ||||||
|   const { t } = useTranslation('promptbar'); |   const { t } = useTranslation('promptbar'); | ||||||
|   const [searchTerm, setSearchTerm] = useState<string>(''); |  | ||||||
|   const [filteredPrompts, setFilteredPrompts] = useState<Prompt[]>(prompts); |  | ||||||
| 
 | 
 | ||||||
|   const handleUpdatePrompt = (prompt: Prompt) => { |   const promptBarContextValue = useCreateReducer<PromptbarInitialState>({ | ||||||
|     onUpdatePrompt(prompt); |     initialState, | ||||||
|     setSearchTerm(''); |   }); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { prompts, defaultModelId, showPromptbar }, | ||||||
|  |     dispatch: homeDispatch, | ||||||
|  |     handleCreateFolder, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { searchTerm, filteredPrompts }, | ||||||
|  |     dispatch: promptDispatch, | ||||||
|  |   } = promptBarContextValue; | ||||||
|  | 
 | ||||||
|  |   const handleTogglePromptbar = () => { | ||||||
|  |     homeDispatch({ field: 'showPromptbar', value: !showPromptbar }); | ||||||
|  |     localStorage.setItem('showPromptbar', JSON.stringify(!showPromptbar)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleCreatePrompt = () => { | ||||||
|  |     if (defaultModelId) { | ||||||
|  |       const newPrompt: Prompt = { | ||||||
|  |         id: uuidv4(), | ||||||
|  |         name: `Prompt ${prompts.length + 1}`, | ||||||
|  |         description: '', | ||||||
|  |         content: '', | ||||||
|  |         model: OpenAIModels[defaultModelId], | ||||||
|  |         folderId: null, | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const updatedPrompts = [...prompts, newPrompt]; | ||||||
|  | 
 | ||||||
|  |       homeDispatch({ field: 'prompts', value: updatedPrompts }); | ||||||
|  | 
 | ||||||
|  |       savePrompts(updatedPrompts); | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDeletePrompt = (prompt: Prompt) => { |   const handleDeletePrompt = (prompt: Prompt) => { | ||||||
|     onDeletePrompt(prompt); |     const updatedPrompts = prompts.filter((p) => p.id !== prompt.id); | ||||||
|     setSearchTerm(''); | 
 | ||||||
|  |     homeDispatch({ field: 'prompts', value: updatedPrompts }); | ||||||
|  |     savePrompts(updatedPrompts); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleUpdatePrompt = (prompt: Prompt) => { | ||||||
|  |     const updatedPrompts = prompts.map((p) => { | ||||||
|  |       if (p.id === prompt.id) { | ||||||
|  |         return prompt; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return p; | ||||||
|  |     }); | ||||||
|  |     homeDispatch({ field: 'prompts', value: updatedPrompts }); | ||||||
|  | 
 | ||||||
|  |     savePrompts(updatedPrompts); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDrop = (e: any) => { |   const handleDrop = (e: any) => { | ||||||
|  | @ -56,28 +91,17 @@ export const Promptbar: FC<Props> = ({ | ||||||
|         folderId: e.target.dataset.folderId, |         folderId: e.target.dataset.folderId, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       onUpdatePrompt(updatedPrompt); |       handleUpdatePrompt(updatedPrompt); | ||||||
| 
 | 
 | ||||||
|       e.target.style.background = 'none'; |       e.target.style.background = 'none'; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const allowDrop = (e: any) => { |  | ||||||
|     e.preventDefault(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const highlightDrop = (e: any) => { |  | ||||||
|     e.target.style.background = '#343541'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const removeHighlight = (e: any) => { |  | ||||||
|     e.target.style.background = 'none'; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (searchTerm) { |     if (searchTerm) { | ||||||
|       setFilteredPrompts( |       promptDispatch({ | ||||||
|         prompts.filter((prompt) => { |         field: 'filteredPrompts', | ||||||
|  |         value: prompts.filter((prompt) => { | ||||||
|           const searchable = |           const searchable = | ||||||
|             prompt.name.toLowerCase() + |             prompt.name.toLowerCase() + | ||||||
|             ' ' + |             ' ' + | ||||||
|  | @ -86,85 +110,43 @@ export const Promptbar: FC<Props> = ({ | ||||||
|             prompt.content.toLowerCase(); |             prompt.content.toLowerCase(); | ||||||
|           return searchable.includes(searchTerm.toLowerCase()); |           return searchable.includes(searchTerm.toLowerCase()); | ||||||
|         }), |         }), | ||||||
|       ); |       }); | ||||||
|     } else { |     } else { | ||||||
|       setFilteredPrompts(prompts); |       promptDispatch({ field: 'filteredPrompts', value: prompts }); | ||||||
|     } |     } | ||||||
|   }, [searchTerm, prompts]); |   }, [searchTerm, prompts]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <PromptbarContext.Provider | ||||||
|       className={`fixed top-0 right-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`} |       value={{ | ||||||
|  |         ...promptBarContextValue, | ||||||
|  |         handleCreatePrompt, | ||||||
|  |         handleDeletePrompt, | ||||||
|  |         handleUpdatePrompt, | ||||||
|  |       }} | ||||||
|     > |     > | ||||||
|       <div className="flex items-center"> |       <Sidebar<Prompt> | ||||||
|         <button |         side={'right'} | ||||||
|           className="text-sidebar flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-white transition-colors duration-200 hover:bg-gray-500/10" |         isOpen={showPromptbar} | ||||||
|           onClick={() => { |         addItemButtonTitle={t('New prompt')} | ||||||
|             onCreatePrompt(); |         itemComponent={ | ||||||
|             setSearchTerm(''); |           <Prompts | ||||||
|           }} |             prompts={filteredPrompts.filter((prompt) => !prompt.folderId)} | ||||||
|         > |           /> | ||||||
|           <IconPlus size={16} /> |         } | ||||||
|           {t('New prompt')} |         folderComponent={<PromptFolders />} | ||||||
|         </button> |         items={filteredPrompts} | ||||||
| 
 |         searchTerm={searchTerm} | ||||||
|         <button |         handleSearchTerm={(searchTerm: string) => | ||||||
|           className="flex items-center flex-shrink-0 gap-3 p-3 ml-2 text-sm text-white transition-colors duration-200 border rounded-md cursor-pointer border-white/20 hover:bg-gray-500/10" |           promptDispatch({ field: 'searchTerm', value: searchTerm }) | ||||||
|           onClick={() => onCreateFolder(t('New folder'))} |         } | ||||||
|         > |         toggleOpen={handleTogglePromptbar} | ||||||
|           <IconFolderPlus size={16} /> |         handleCreateItem={handleCreatePrompt} | ||||||
|         </button> |         handleCreateFolder={() => handleCreateFolder(t('New folder'), 'prompt')} | ||||||
|       </div> |         handleDrop={handleDrop} | ||||||
| 
 |       /> | ||||||
|       {prompts.length > 1 && ( |     </PromptbarContext.Provider> | ||||||
|         <Search |  | ||||||
|           placeholder={t('Search prompts...') || ''} |  | ||||||
|           searchTerm={searchTerm} |  | ||||||
|           onSearch={setSearchTerm} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
| 
 |  | ||||||
|       <div className="flex-grow overflow-auto"> |  | ||||||
|         {folders.length > 0 && ( |  | ||||||
|           <div className="flex pb-2 border-b border-white/20"> |  | ||||||
|             <PromptFolders |  | ||||||
|               searchTerm={searchTerm} |  | ||||||
|               prompts={filteredPrompts} |  | ||||||
|               folders={folders} |  | ||||||
|               onUpdateFolder={onUpdateFolder} |  | ||||||
|               onDeleteFolder={onDeleteFolder} |  | ||||||
|               // prompt props
 |  | ||||||
|               onDeletePrompt={handleDeletePrompt} |  | ||||||
|               onUpdatePrompt={handleUpdatePrompt} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
| 
 |  | ||||||
|         {prompts.length > 0 ? ( |  | ||||||
|           <div |  | ||||||
|             className="pt-2" |  | ||||||
|             onDrop={(e) => handleDrop(e)} |  | ||||||
|             onDragOver={allowDrop} |  | ||||||
|             onDragEnter={highlightDrop} |  | ||||||
|             onDragLeave={removeHighlight} |  | ||||||
|           > |  | ||||||
|             <Prompts |  | ||||||
|               prompts={filteredPrompts.filter((prompt) => !prompt.folderId)} |  | ||||||
|               onUpdatePrompt={handleUpdatePrompt} |  | ||||||
|               onDeletePrompt={handleDeletePrompt} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         ) : ( |  | ||||||
|           <div className="mt-8 text-center text-white opacity-50 select-none"> |  | ||||||
|             <IconMistOff className="mx-auto mb-3" /> |  | ||||||
|             <span className="text-[14px] leading-normal"> |  | ||||||
|               {t('No prompts.')} |  | ||||||
|             </span> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <PromptbarSettings /> |  | ||||||
|     </div> |  | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default Promptbar; | ||||||
|  |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { FC } from 'react'; |  | ||||||
| import { PromptComponent } from './Prompt'; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   prompts: Prompt[]; |  | ||||||
|   onUpdatePrompt: (prompt: Prompt) => void; |  | ||||||
|   onDeletePrompt: (prompt: Prompt) => void; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const Prompts: FC<Props> = ({ |  | ||||||
|   prompts, |  | ||||||
|   onUpdatePrompt, |  | ||||||
|   onDeletePrompt, |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <div className="flex w-full flex-col gap-1"> |  | ||||||
|       {prompts |  | ||||||
|         .slice() |  | ||||||
|         .reverse() |  | ||||||
|         .map((prompt, index) => ( |  | ||||||
|           <PromptComponent |  | ||||||
|             key={index} |  | ||||||
|             prompt={prompt} |  | ||||||
|             onUpdatePrompt={onUpdatePrompt} |  | ||||||
|             onDeletePrompt={onDeletePrompt} |  | ||||||
|           /> |  | ||||||
|         ))} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  | @ -1,29 +1,66 @@ | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { | import { | ||||||
|   IconBulbFilled, |   IconBulbFilled, | ||||||
|   IconCheck, |   IconCheck, | ||||||
|   IconTrash, |   IconTrash, | ||||||
|   IconX, |   IconX, | ||||||
| } from '@tabler/icons-react'; | } from '@tabler/icons-react'; | ||||||
| import { DragEvent, FC, useEffect, useState } from 'react'; | import { | ||||||
|  |   DragEvent, | ||||||
|  |   MouseEventHandler, | ||||||
|  |   useContext, | ||||||
|  |   useEffect, | ||||||
|  |   useState, | ||||||
|  | } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | import SidebarActionButton from '@/components/Buttons/SidebarActionButton'; | ||||||
|  | 
 | ||||||
|  | import PromptbarContext from '../PromptBar.context'; | ||||||
| import { PromptModal } from './PromptModal'; | import { PromptModal } from './PromptModal'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   prompt: Prompt; |   prompt: Prompt; | ||||||
|   onUpdatePrompt: (prompt: Prompt) => void; |  | ||||||
|   onDeletePrompt: (prompt: Prompt) => void; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const PromptComponent: FC<Props> = ({ | export const PromptComponent = ({ prompt }: Props) => { | ||||||
|   prompt, |   const { | ||||||
|   onUpdatePrompt, |     dispatch: promptDispatch, | ||||||
|   onDeletePrompt, |     handleUpdatePrompt, | ||||||
| }) => { |     handleDeletePrompt, | ||||||
|  |   } = useContext(PromptbarContext); | ||||||
|  | 
 | ||||||
|   const [showModal, setShowModal] = useState<boolean>(false); |   const [showModal, setShowModal] = useState<boolean>(false); | ||||||
|   const [isDeleting, setIsDeleting] = useState(false); |   const [isDeleting, setIsDeleting] = useState(false); | ||||||
|   const [isRenaming, setIsRenaming] = useState(false); |   const [isRenaming, setIsRenaming] = useState(false); | ||||||
|   const [renameValue, setRenameValue] = useState(''); |   const [renameValue, setRenameValue] = useState(''); | ||||||
| 
 | 
 | ||||||
|  |   const handleUpdate = (prompt: Prompt) => { | ||||||
|  |     handleUpdatePrompt(prompt); | ||||||
|  |     promptDispatch({ field: 'searchTerm', value: '' }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDelete: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |     if (isDeleting) { | ||||||
|  |       handleDeletePrompt(prompt); | ||||||
|  |       promptDispatch({ field: 'searchTerm', value: '' }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     setIsDeleting(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleCancelDelete: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     setIsDeleting(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleOpenDeleteModal: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     setIsDeleting(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const handleDragStart = (e: DragEvent<HTMLButtonElement>, prompt: Prompt) => { |   const handleDragStart = (e: DragEvent<HTMLButtonElement>, prompt: Prompt) => { | ||||||
|     if (e.dataTransfer) { |     if (e.dataTransfer) { | ||||||
|       e.dataTransfer.setData('prompt', JSON.stringify(prompt)); |       e.dataTransfer.setData('prompt', JSON.stringify(prompt)); | ||||||
|  | @ -63,44 +100,21 @@ export const PromptComponent: FC<Props> = ({ | ||||||
| 
 | 
 | ||||||
|       {(isDeleting || isRenaming) && ( |       {(isDeleting || isRenaming) && ( | ||||||
|         <div className="absolute right-1 z-10 flex text-gray-300"> |         <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|           <button |           <SidebarActionButton handleClick={handleDelete}> | ||||||
|             className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|             onClick={(e) => { |  | ||||||
|               e.stopPropagation(); |  | ||||||
| 
 |  | ||||||
|               if (isDeleting) { |  | ||||||
|                 onDeletePrompt(prompt); |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               setIsDeleting(false); |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             <IconCheck size={18} /> |             <IconCheck size={18} /> | ||||||
|           </button> |           </SidebarActionButton> | ||||||
| 
 | 
 | ||||||
|           <button |           <SidebarActionButton handleClick={handleCancelDelete}> | ||||||
|             className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|             onClick={(e) => { |  | ||||||
|               e.stopPropagation(); |  | ||||||
|               setIsDeleting(false); |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             <IconX size={18} /> |             <IconX size={18} /> | ||||||
|           </button> |           </SidebarActionButton> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {!isDeleting && !isRenaming && ( |       {!isDeleting && !isRenaming && ( | ||||||
|         <div className="absolute right-1 z-10 flex text-gray-300"> |         <div className="absolute right-1 z-10 flex text-gray-300"> | ||||||
|           <button |           <SidebarActionButton handleClick={handleOpenDeleteModal}> | ||||||
|             className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100" |  | ||||||
|             onClick={(e) => { |  | ||||||
|               e.stopPropagation(); |  | ||||||
|               setIsDeleting(true); |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             <IconTrash size={18} /> |             <IconTrash size={18} /> | ||||||
|           </button> |           </SidebarActionButton> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|  | @ -108,7 +122,7 @@ export const PromptComponent: FC<Props> = ({ | ||||||
|         <PromptModal |         <PromptModal | ||||||
|           prompt={prompt} |           prompt={prompt} | ||||||
|           onClose={() => setShowModal(false)} |           onClose={() => setShowModal(false)} | ||||||
|           onUpdatePrompt={onUpdatePrompt} |           onUpdatePrompt={handleUpdate} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|     </div> |     </div> | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | import { useContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { FolderInterface } from '@/types/folder'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from '@/pages/api/home/home.context'; | ||||||
|  | 
 | ||||||
|  | import Folder from '@/components/Folder'; | ||||||
|  | import { PromptComponent } from '@/components/Promptbar/components/Prompt'; | ||||||
|  | 
 | ||||||
|  | import PromptbarContext from '../PromptBar.context'; | ||||||
|  | 
 | ||||||
|  | export const PromptFolders = () => { | ||||||
|  |   const { | ||||||
|  |     state: { folders }, | ||||||
|  |   } = useContext(HomeContext); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { searchTerm, filteredPrompts }, | ||||||
|  |     handleUpdatePrompt, | ||||||
|  |   } = useContext(PromptbarContext); | ||||||
|  | 
 | ||||||
|  |   const handleDrop = (e: any, folder: FolderInterface) => { | ||||||
|  |     if (e.dataTransfer) { | ||||||
|  |       const prompt = JSON.parse(e.dataTransfer.getData('prompt')); | ||||||
|  | 
 | ||||||
|  |       const updatedPrompt = { | ||||||
|  |         ...prompt, | ||||||
|  |         folderId: folder.id, | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       handleUpdatePrompt(updatedPrompt); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const PromptFolders = (currentFolder: FolderInterface) => | ||||||
|  |     filteredPrompts | ||||||
|  |       .filter((p) => p.folderId) | ||||||
|  |       .map((prompt, index) => { | ||||||
|  |         if (prompt.folderId === currentFolder.id) { | ||||||
|  |           return ( | ||||||
|  |             <div key={index} className="ml-5 gap-2 border-l pl-2"> | ||||||
|  |               <PromptComponent prompt={prompt} /> | ||||||
|  |             </div> | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="flex w-full flex-col pt-2"> | ||||||
|  |       {folders | ||||||
|  |         .filter((folder) => folder.type === 'prompt') | ||||||
|  |         .map((folder, index) => ( | ||||||
|  |           <Folder | ||||||
|  |             key={index} | ||||||
|  |             searchTerm={searchTerm} | ||||||
|  |             currentFolder={folder} | ||||||
|  |             handleDrop={handleDrop} | ||||||
|  |             folderComponent={PromptFolders(folder)} | ||||||
|  |           /> | ||||||
|  |         ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | ||||||
|  | 
 | ||||||
| import { useTranslation } from 'next-i18next'; | import { useTranslation } from 'next-i18next'; | ||||||
| 
 | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   prompt: Prompt; |   prompt: Prompt; | ||||||
|   onClose: () => void; |   onClose: () => void; | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { FC } from "react"; | import { FC } from 'react'; | ||||||
| 
 | 
 | ||||||
| interface Props {} | interface Props {} | ||||||
| 
 | 
 | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | import { FC } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | import { PromptComponent } from './Prompt'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   prompts: Prompt[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Prompts: FC<Props> = ({ prompts }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex w-full flex-col gap-1"> | ||||||
|  |       {prompts | ||||||
|  |         .slice() | ||||||
|  |         .reverse() | ||||||
|  |         .map((prompt, index) => ( | ||||||
|  |           <PromptComponent key={index} prompt={prompt} /> | ||||||
|  |         ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './Promptbar'; | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import { IconX } from '@tabler/icons-react'; | import { IconX } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
| 
 | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   placeholder: string; |   placeholder: string; | ||||||
|   searchTerm: string; |   searchTerm: string; | ||||||
|   onSearch: (searchTerm: string) => void; |   onSearch: (searchTerm: string) => void; | ||||||
| } | } | ||||||
| 
 | const Search: FC<Props> = ({ placeholder, searchTerm, onSearch }) => { | ||||||
| export const Search: FC<Props> = ({ placeholder, searchTerm, onSearch }) => { |  | ||||||
|   const { t } = useTranslation('sidebar'); |   const { t } = useTranslation('sidebar'); | ||||||
| 
 | 
 | ||||||
|   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { |   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|  | @ -39,3 +39,5 @@ export const Search: FC<Props> = ({ placeholder, searchTerm, onSearch }) => { | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default Search; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './Search'; | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| import { SupportedExportFormats } from '@/types/export'; |  | ||||||
| import { IconFileImport } from '@tabler/icons-react'; | import { IconFileImport } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC } from 'react'; | import { FC } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { SupportedExportFormats } from '@/types/export'; | ||||||
|  | 
 | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; | import { SidebarButton } from '../Sidebar/SidebarButton'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import { IconCheck, IconKey, IconX } from '@tabler/icons-react'; | import { IconCheck, IconKey, IconX } from '@tabler/icons-react'; | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
| import { SidebarButton } from '../Sidebar/SidebarButton'; | import { SidebarButton } from '../Sidebar/SidebarButton'; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,125 @@ | ||||||
|  | import { IconFolderPlus, IconMistOff, IconPlus } from '@tabler/icons-react'; | ||||||
|  | import { ReactNode } from 'react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | 
 | ||||||
|  | import { | ||||||
|  |   CloseSidebarButton, | ||||||
|  |   OpenSidebarButton, | ||||||
|  | } from './components/OpenCloseButton'; | ||||||
|  | 
 | ||||||
|  | import Search from '../Search'; | ||||||
|  | 
 | ||||||
|  | interface Props<T> { | ||||||
|  |   isOpen: boolean; | ||||||
|  |   addItemButtonTitle: string; | ||||||
|  |   side: 'left' | 'right'; | ||||||
|  |   items: T[]; | ||||||
|  |   itemComponent: ReactNode; | ||||||
|  |   folderComponent: ReactNode; | ||||||
|  |   footerComponent?: ReactNode; | ||||||
|  |   searchTerm: string; | ||||||
|  |   handleSearchTerm: (searchTerm: string) => void; | ||||||
|  |   toggleOpen: () => void; | ||||||
|  |   handleCreateItem: () => void; | ||||||
|  |   handleCreateFolder: () => void; | ||||||
|  |   handleDrop: (e: any) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Sidebar = <T,>({ | ||||||
|  |   isOpen, | ||||||
|  |   addItemButtonTitle, | ||||||
|  |   side, | ||||||
|  |   items, | ||||||
|  |   itemComponent, | ||||||
|  |   folderComponent, | ||||||
|  |   footerComponent, | ||||||
|  |   searchTerm, | ||||||
|  |   handleSearchTerm, | ||||||
|  |   toggleOpen, | ||||||
|  |   handleCreateItem, | ||||||
|  |   handleCreateFolder, | ||||||
|  |   handleDrop, | ||||||
|  | }: Props<T>) => { | ||||||
|  |   const { t } = useTranslation('promptbar'); | ||||||
|  | 
 | ||||||
|  |   const allowDrop = (e: any) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const highlightDrop = (e: any) => { | ||||||
|  |     e.target.style.background = '#343541'; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const removeHighlight = (e: any) => { | ||||||
|  |     e.target.style.background = 'none'; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return isOpen ? ( | ||||||
|  |     <div> | ||||||
|  |       <div | ||||||
|  |         className={`fixed top-0  z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`} | ||||||
|  |       > | ||||||
|  |         <div className="flex items-center"> | ||||||
|  |           <button | ||||||
|  |             className="text-sidebar flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-white transition-colors duration-200 hover:bg-gray-500/10" | ||||||
|  |             onClick={() => { | ||||||
|  |               handleCreateItem(); | ||||||
|  |               handleSearchTerm(''); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             <IconPlus size={16} /> | ||||||
|  |             {addItemButtonTitle} | ||||||
|  |           </button> | ||||||
|  | 
 | ||||||
|  |           <button | ||||||
|  |             className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10" | ||||||
|  |             onClick={handleCreateFolder} | ||||||
|  |           > | ||||||
|  |             <IconFolderPlus size={16} /> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |         {items?.length > 0 && ( | ||||||
|  |           <Search | ||||||
|  |             placeholder={t('Search prompts...') || ''} | ||||||
|  |             searchTerm={searchTerm} | ||||||
|  |             onSearch={handleSearchTerm} | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|  | 
 | ||||||
|  |         <div className="flex-grow overflow-auto"> | ||||||
|  |           {items?.length > 0 && ( | ||||||
|  |             <div className="flex border-b border-white/20 pb-2"> | ||||||
|  |               {folderComponent} | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  | 
 | ||||||
|  |           {items?.length > 0 ? ( | ||||||
|  |             <div | ||||||
|  |               className="pt-2" | ||||||
|  |               onDrop={handleDrop} | ||||||
|  |               onDragOver={allowDrop} | ||||||
|  |               onDragEnter={highlightDrop} | ||||||
|  |               onDragLeave={removeHighlight} | ||||||
|  |             > | ||||||
|  |               {itemComponent} | ||||||
|  |             </div> | ||||||
|  |           ) : ( | ||||||
|  |             <div className="mt-8 select-none text-center text-white opacity-50"> | ||||||
|  |               <IconMistOff className="mx-auto mb-3" /> | ||||||
|  |               <span className="text-[14px] leading-normal"> | ||||||
|  |                 {t('No prompts.')} | ||||||
|  |               </span> | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |         {footerComponent} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <CloseSidebarButton onClick={toggleOpen} side={side} /> | ||||||
|  |     </div> | ||||||
|  |   ) : ( | ||||||
|  |     <OpenSidebarButton onClick={toggleOpen} side={side} /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default Sidebar; | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   onClick: any; | ||||||
|  |   side: 'left' | 'right'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const CloseSidebarButton = ({ onClick, side }: Props) => { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <button | ||||||
|  |         className={`fixed top-5 ${ | ||||||
|  |           side === 'right' ? 'right-[270px]' : 'left-[270px]' | ||||||
|  |         } z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:${ | ||||||
|  |           side === 'right' ? 'right-[270px]' : 'left-[270px]' | ||||||
|  |         } sm:h-8 sm:w-8 sm:text-neutral-700`}
 | ||||||
|  |         onClick={onClick} | ||||||
|  |       > | ||||||
|  |         {side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />} | ||||||
|  |       </button> | ||||||
|  |       <div | ||||||
|  |         onClick={onClick} | ||||||
|  |         className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden" | ||||||
|  |       ></div> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const OpenSidebarButton = ({ onClick, side }: Props) => { | ||||||
|  |   return ( | ||||||
|  |     <button | ||||||
|  |       className={`fixed top-2.5 ${ | ||||||
|  |         side === 'right' ? 'right-2' : 'left-2' | ||||||
|  |       } z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:${ | ||||||
|  |         side === 'right' ? 'right-2' : 'left-2' | ||||||
|  |       } sm:h-8 sm:w-8 sm:text-neutral-700`}
 | ||||||
|  |       onClick={onClick} | ||||||
|  |     > | ||||||
|  |       {side === 'right' ? <IconArrowBarLeft /> : <IconArrowBarRight />} | ||||||
|  |     </button> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './Sidebar'; | ||||||
|  | @ -5,7 +5,7 @@ interface Props { | ||||||
|   className?: string; |   className?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Spinner: FC<Props> = ({ size = '1em', className="" }) => { | const Spinner = ({ size = '1em', className = '' }: Props) => { | ||||||
|   return ( |   return ( | ||||||
|     <svg |     <svg | ||||||
|       stroke="currentColor" |       stroke="currentColor" | ||||||
|  | @ -30,3 +30,5 @@ export const Spinner: FC<Props> = ({ size = '1em', className="" }) => { | ||||||
|     </svg> |     </svg> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default Spinner; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default } from './Spinner'; | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | import { useMemo, useReducer } from 'react'; | ||||||
|  | 
 | ||||||
|  | // Extracts property names from initial state of reducer to allow typesafe dispatch objects
 | ||||||
|  | export type FieldNames<T> = { | ||||||
|  |   [K in keyof T]: T[K] extends string ? K : K; | ||||||
|  | }[keyof T]; | ||||||
|  | 
 | ||||||
|  | // Returns the Action Type for the dispatch object to be used for typing in things like context
 | ||||||
|  | export type ActionType<T> = | ||||||
|  |   | { type: 'reset' } | ||||||
|  |   | { type?: 'change'; field: FieldNames<T>; value: any }; | ||||||
|  | 
 | ||||||
|  | // Returns a typed dispatch and state
 | ||||||
|  | export const useCreateReducer = <T>({ initialState }: { initialState: T }) => { | ||||||
|  |   type Action = | ||||||
|  |     | { type: 'reset' } | ||||||
|  |     | { type?: 'change'; field: FieldNames<T>; value: any }; | ||||||
|  | 
 | ||||||
|  |   const reducer = (state: T, action: Action) => { | ||||||
|  |     if (!action.type) return { ...state, [action.field]: action.value }; | ||||||
|  | 
 | ||||||
|  |     if (action.type === 'reset') return initialState; | ||||||
|  | 
 | ||||||
|  |     throw new Error(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const [state, dispatch] = useReducer(reducer, initialState); | ||||||
|  | 
 | ||||||
|  |   return useMemo(() => ({ state, dispatch }), [state, dispatch]); | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,88 @@ | ||||||
|  | export type RequestModel = { | ||||||
|  |   params?: object; | ||||||
|  |   headers?: object; | ||||||
|  |   signal?: AbortSignal; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type RequestWithBodyModel = RequestModel & { | ||||||
|  |   body?: object | FormData; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const useFetch = () => { | ||||||
|  |   const handleFetch = async ( | ||||||
|  |     url: string, | ||||||
|  |     request: any, | ||||||
|  |     signal?: AbortSignal, | ||||||
|  |   ) => { | ||||||
|  |     const requestUrl = request?.params ? `${url}${request.params}` : url; | ||||||
|  | 
 | ||||||
|  |     const requestBody = request?.body | ||||||
|  |       ? request.body instanceof FormData | ||||||
|  |         ? { ...request, body: request.body } | ||||||
|  |         : { ...request, body: JSON.stringify(request.body) } | ||||||
|  |       : request; | ||||||
|  | 
 | ||||||
|  |     const headers = { | ||||||
|  |       ...(request?.headers | ||||||
|  |         ? request.headers | ||||||
|  |         : request?.body && request.body instanceof FormData | ||||||
|  |         ? {} | ||||||
|  |         : { 'Content-type': 'application/json' }), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return fetch(requestUrl, { ...requestBody, headers, signal }) | ||||||
|  |       .then((response) => { | ||||||
|  |         if (!response.ok) throw response; | ||||||
|  | 
 | ||||||
|  |         const contentType = response.headers.get('content-type'); | ||||||
|  |         const contentDisposition = response.headers.get('content-disposition'); | ||||||
|  | 
 | ||||||
|  |         const headers = response.headers; | ||||||
|  | 
 | ||||||
|  |         const result = | ||||||
|  |           contentType && | ||||||
|  |           (contentType?.indexOf('application/json') !== -1 || | ||||||
|  |             contentType?.indexOf('text/plain') !== -1) | ||||||
|  |             ? response.json() | ||||||
|  |             : contentDisposition?.indexOf('attachment') !== -1 | ||||||
|  |             ? response.blob() | ||||||
|  |             : response; | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |       }) | ||||||
|  |       .catch(async (err) => { | ||||||
|  |         const contentType = err.headers.get('content-type'); | ||||||
|  | 
 | ||||||
|  |         const errResult = | ||||||
|  |           contentType && contentType?.indexOf('application/problem+json') !== -1 | ||||||
|  |             ? await err.json() | ||||||
|  |             : err; | ||||||
|  | 
 | ||||||
|  |         throw errResult; | ||||||
|  |       }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     get: async <T>(url: string, request?: RequestModel): Promise<T> => { | ||||||
|  |       return handleFetch(url, { ...request, method: 'get' }); | ||||||
|  |     }, | ||||||
|  |     post: async <T>( | ||||||
|  |       url: string, | ||||||
|  |       request?: RequestWithBodyModel, | ||||||
|  |     ): Promise<T> => { | ||||||
|  |       return handleFetch(url, { ...request, method: 'post' }); | ||||||
|  |     }, | ||||||
|  |     put: async <T>(url: string, request?: RequestWithBodyModel): Promise<T> => { | ||||||
|  |       return handleFetch(url, { ...request, method: 'put' }); | ||||||
|  |     }, | ||||||
|  |     patch: async <T>( | ||||||
|  |       url: string, | ||||||
|  |       request?: RequestWithBodyModel, | ||||||
|  |     ): Promise<T> => { | ||||||
|  |       return handleFetch(url, { ...request, method: 'patch' }); | ||||||
|  |     }, | ||||||
|  |     delete: async <T>(url: string, request?: RequestModel): Promise<T> => { | ||||||
|  |       return handleFetch(url, { ...request, method: 'delete' }); | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | @ -1,24 +1,24 @@ | ||||||
| module.exports = { | module.exports = { | ||||||
|   i18n: { |   i18n: { | ||||||
|     defaultLocale: "en", |     defaultLocale: 'en', | ||||||
|     locales: [ |     locales: [ | ||||||
|       "bn", |       'bn', | ||||||
|       "de", |       'de', | ||||||
|       "en", |       'en', | ||||||
|       "es", |       'es', | ||||||
|       "fr", |       'fr', | ||||||
|       "he", |       'he', | ||||||
|       "id", |       'id', | ||||||
|       "it", |       'it', | ||||||
|       "ja", |       'ja', | ||||||
|       "ko", |       'ko', | ||||||
|       "pt", |       'pt', | ||||||
|       "ru", |       'ru', | ||||||
|       "sv", |       'sv', | ||||||
|       "te", |       'te', | ||||||
|       "vi", |       'vi', | ||||||
|       "zh", |       'zh', | ||||||
|       "ar", |       'ar', | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   localePath: |   localePath: | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -24,6 +24,7 @@ | ||||||
|     "react-hot-toast": "^2.4.0", |     "react-hot-toast": "^2.4.0", | ||||||
|     "react-i18next": "^12.2.0", |     "react-i18next": "^12.2.0", | ||||||
|     "react-markdown": "^8.0.5", |     "react-markdown": "^8.0.5", | ||||||
|  |     "react-query": "^3.39.3", | ||||||
|     "react-syntax-highlighter": "^15.5.0", |     "react-syntax-highlighter": "^15.5.0", | ||||||
|     "rehype-mathjax": "^4.0.2", |     "rehype-mathjax": "^4.0.2", | ||||||
|     "remark-gfm": "^3.0.1", |     "remark-gfm": "^3.0.1", | ||||||
|  | @ -33,6 +34,7 @@ | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@mozilla/readability": "^0.4.4", |     "@mozilla/readability": "^0.4.4", | ||||||
|     "@tailwindcss/typography": "^0.5.9", |     "@tailwindcss/typography": "^0.5.9", | ||||||
|  |     "@trivago/prettier-plugin-sort-imports": "^4.1.1", | ||||||
|     "@types/jsdom": "^21.1.1", |     "@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", | ||||||
|  |  | ||||||
|  | @ -1,16 +1,23 @@ | ||||||
| import '@/styles/globals.css'; | import { Toaster } from 'react-hot-toast'; | ||||||
|  | import { QueryClient, QueryClientProvider } from 'react-query'; | ||||||
|  | 
 | ||||||
| import { appWithTranslation } from 'next-i18next'; | import { appWithTranslation } from 'next-i18next'; | ||||||
| import type { AppProps } from 'next/app'; | import type { AppProps } from 'next/app'; | ||||||
| import { Inter } from 'next/font/google'; | import { Inter } from 'next/font/google'; | ||||||
| import { Toaster } from 'react-hot-toast'; | 
 | ||||||
|  | import '@/styles/globals.css'; | ||||||
| 
 | 
 | ||||||
| const inter = Inter({ subsets: ['latin'] }); | const inter = Inter({ subsets: ['latin'] }); | ||||||
| 
 | 
 | ||||||
| function App({ Component, pageProps }: AppProps<{}>) { | function App({ Component, pageProps }: AppProps<{}>) { | ||||||
|  |   const queryClient = new QueryClient(); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={inter.className}> |     <div className={inter.className}> | ||||||
|       <Toaster /> |       <Toaster /> | ||||||
|       <Component {...pageProps} /> |       <QueryClientProvider client={queryClient}> | ||||||
|  |         <Component {...pageProps} /> | ||||||
|  |       </QueryClientProvider> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Html, Head, Main, NextScript, DocumentProps } from 'next/document'; | import { DocumentProps, Head, Html, Main, NextScript } from 'next/document'; | ||||||
|  | 
 | ||||||
| import i18nextConfig from '../next-i18next.config'; | import i18nextConfig from '../next-i18next.config'; | ||||||
| 
 | 
 | ||||||
| type Props = DocumentProps & { | type Props = DocumentProps & { | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| import { ChatBody, Message } from '@/types/chat'; |  | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | ||||||
| import { OpenAIError, OpenAIStream } from '@/utils/server'; | import { OpenAIError, OpenAIStream } from '@/utils/server'; | ||||||
| import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json'; | 
 | ||||||
| import { Tiktoken, init } from '@dqbd/tiktoken/lite/init'; | import { ChatBody, Message } from '@/types/chat'; | ||||||
|  | 
 | ||||||
| // @ts-expect-error
 | // @ts-expect-error
 | ||||||
| import wasm from '../../node_modules/@dqbd/tiktoken/lite/tiktoken_bg.wasm?module'; | import wasm from '../../node_modules/@dqbd/tiktoken/lite/tiktoken_bg.wasm?module'; | ||||||
| 
 | 
 | ||||||
|  | import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json'; | ||||||
|  | import { Tiktoken, init } from '@dqbd/tiktoken/lite/init'; | ||||||
|  | 
 | ||||||
| export const config = { | export const config = { | ||||||
|   runtime: 'edge', |   runtime: 'edge', | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| import { Message } from '@/types/chat'; | import { NextApiRequest, NextApiResponse } from 'next'; | ||||||
| import { GoogleBody, GoogleSource } from '@/types/google'; | 
 | ||||||
| import { OPENAI_API_HOST } from '@/utils/app/const'; | import { OPENAI_API_HOST } from '@/utils/app/const'; | ||||||
| import { cleanSourceText } from '@/utils/server/google'; | import { cleanSourceText } from '@/utils/server/google'; | ||||||
|  | 
 | ||||||
|  | import { Message } from '@/types/chat'; | ||||||
|  | import { GoogleBody, GoogleSource } from '@/types/google'; | ||||||
|  | 
 | ||||||
| import { Readability } from '@mozilla/readability'; | import { Readability } from '@mozilla/readability'; | ||||||
| import endent from 'endent'; | import endent from 'endent'; | ||||||
| import jsdom, { JSDOM } from 'jsdom'; | import jsdom, { JSDOM } from 'jsdom'; | ||||||
| import { NextApiRequest, NextApiResponse } from 'next'; |  | ||||||
| 
 | 
 | ||||||
| const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => { | const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => { | ||||||
|   try { |   try { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | import { Dispatch, createContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { ActionType } from '@/hooks/useCreateReducer'; | ||||||
|  | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | import { KeyValuePair } from '@/types/data'; | ||||||
|  | import { FolderType } from '@/types/folder'; | ||||||
|  | 
 | ||||||
|  | import { HomeInitialState } from './home.state'; | ||||||
|  | 
 | ||||||
|  | export interface HomeContextProps { | ||||||
|  |   state: HomeInitialState; | ||||||
|  |   dispatch: Dispatch<ActionType<HomeInitialState>>; | ||||||
|  |   handleNewConversation: () => void; | ||||||
|  |   handleCreateFolder: (name: string, type: FolderType) => void; | ||||||
|  |   handleDeleteFolder: (folderId: string) => void; | ||||||
|  |   handleUpdateFolder: (folderId: string, name: string) => void; | ||||||
|  |   handleSelectConversation: (conversation: Conversation) => void; | ||||||
|  |   handleUpdateConversation: ( | ||||||
|  |     conversation: Conversation, | ||||||
|  |     data: KeyValuePair, | ||||||
|  |   ) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const HomeContext = createContext<HomeContextProps>(undefined!); | ||||||
|  | 
 | ||||||
|  | export default HomeContext; | ||||||
|  | @ -0,0 +1,52 @@ | ||||||
|  | import { Conversation, Message } from '@/types/chat'; | ||||||
|  | import { ErrorMessage } from '@/types/error'; | ||||||
|  | import { FolderInterface } from '@/types/folder'; | ||||||
|  | import { OpenAIModel, OpenAIModelID } from '@/types/openai'; | ||||||
|  | import { PluginKey } from '@/types/plugin'; | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | export interface HomeInitialState { | ||||||
|  |   apiKey: string; | ||||||
|  |   pluginKeys: PluginKey[]; | ||||||
|  |   loading: boolean; | ||||||
|  |   lightMode: 'light' | 'dark'; | ||||||
|  |   messageIsStreaming: boolean; | ||||||
|  |   modelError: ErrorMessage | null; | ||||||
|  |   models: OpenAIModel[]; | ||||||
|  |   folders: FolderInterface[]; | ||||||
|  |   conversations: Conversation[]; | ||||||
|  |   selectedConversation: Conversation | undefined; | ||||||
|  |   currentMessage: Message | undefined; | ||||||
|  |   prompts: Prompt[]; | ||||||
|  |   showChatbar: boolean; | ||||||
|  |   showPromptbar: boolean; | ||||||
|  |   currentFolder: FolderInterface | undefined; | ||||||
|  |   messageError: boolean; | ||||||
|  |   searchTerm: string; | ||||||
|  |   defaultModelId: OpenAIModelID | undefined; | ||||||
|  |   serverSideApiKeyIsSet: boolean; | ||||||
|  |   serverSidePluginKeysSet: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const initialState: HomeInitialState = { | ||||||
|  |   apiKey: '', | ||||||
|  |   loading: false, | ||||||
|  |   pluginKeys: [], | ||||||
|  |   lightMode: 'dark', | ||||||
|  |   messageIsStreaming: false, | ||||||
|  |   modelError: null, | ||||||
|  |   models: [], | ||||||
|  |   folders: [], | ||||||
|  |   conversations: [], | ||||||
|  |   selectedConversation: undefined, | ||||||
|  |   currentMessage: undefined, | ||||||
|  |   prompts: [], | ||||||
|  |   showPromptbar: true, | ||||||
|  |   showChatbar: true, | ||||||
|  |   currentFolder: undefined, | ||||||
|  |   messageError: false, | ||||||
|  |   searchTerm: '', | ||||||
|  |   defaultModelId: undefined, | ||||||
|  |   serverSideApiKeyIsSet: false, | ||||||
|  |   serverSidePluginKeysSet: false, | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,424 @@ | ||||||
|  | import { useEffect, useRef, useState } from 'react'; | ||||||
|  | import { useQuery } from 'react-query'; | ||||||
|  | 
 | ||||||
|  | import { GetServerSideProps } from 'next'; | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; | ||||||
|  | import Head from 'next/head'; | ||||||
|  | 
 | ||||||
|  | import { useCreateReducer } from '@/hooks/useCreateReducer'; | ||||||
|  | 
 | ||||||
|  | import useErrorService from '@/services/errorService'; | ||||||
|  | import useApiService from '@/services/useApiService'; | ||||||
|  | 
 | ||||||
|  | import { | ||||||
|  |   cleanConversationHistory, | ||||||
|  |   cleanSelectedConversation, | ||||||
|  | } from '@/utils/app/clean'; | ||||||
|  | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; | ||||||
|  | import { | ||||||
|  |   saveConversation, | ||||||
|  |   saveConversations, | ||||||
|  |   updateConversation, | ||||||
|  | } from '@/utils/app/conversation'; | ||||||
|  | import { saveFolders } from '@/utils/app/folders'; | ||||||
|  | import { savePrompts } from '@/utils/app/prompts'; | ||||||
|  | 
 | ||||||
|  | import { Conversation } from '@/types/chat'; | ||||||
|  | import { KeyValuePair } from '@/types/data'; | ||||||
|  | import { FolderInterface, FolderType } from '@/types/folder'; | ||||||
|  | import { OpenAIModelID, OpenAIModels, fallbackModelID } from '@/types/openai'; | ||||||
|  | import { Prompt } from '@/types/prompt'; | ||||||
|  | 
 | ||||||
|  | import { Chat } from '@/components/Chat/Chat'; | ||||||
|  | import { Chatbar } from '@/components/Chatbar/Chatbar'; | ||||||
|  | import { Navbar } from '@/components/Mobile/Navbar'; | ||||||
|  | import Promptbar from '@/components/Promptbar'; | ||||||
|  | 
 | ||||||
|  | import HomeContext from './home.context'; | ||||||
|  | import { HomeInitialState, initialState } from './home.state'; | ||||||
|  | 
 | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   serverSideApiKeyIsSet: boolean; | ||||||
|  |   serverSidePluginKeysSet: boolean; | ||||||
|  |   defaultModelId: OpenAIModelID; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Home = ({ | ||||||
|  |   serverSideApiKeyIsSet, | ||||||
|  |   serverSidePluginKeysSet, | ||||||
|  |   defaultModelId, | ||||||
|  | }: Props) => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|  |   const { getModels } = useApiService(); | ||||||
|  |   const { getModelsError } = useErrorService(); | ||||||
|  |   const [initialRender, setInitialRender] = useState<boolean>(true); | ||||||
|  | 
 | ||||||
|  |   const contextValue = useCreateReducer<HomeInitialState>({ | ||||||
|  |     initialState, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     state: { | ||||||
|  |       apiKey, | ||||||
|  |       lightMode, | ||||||
|  |       folders, | ||||||
|  |       conversations, | ||||||
|  |       selectedConversation, | ||||||
|  | 
 | ||||||
|  |       prompts, | ||||||
|  |     }, | ||||||
|  |     dispatch, | ||||||
|  |   } = contextValue; | ||||||
|  | 
 | ||||||
|  |   const stopConversationRef = useRef<boolean>(false); | ||||||
|  | 
 | ||||||
|  |   const { data, error, refetch } = useQuery( | ||||||
|  |     ['GetModels', apiKey, serverSideApiKeyIsSet], | ||||||
|  |     ({ signal }) => { | ||||||
|  |       if (!apiKey && !serverSideApiKeyIsSet) return null; | ||||||
|  | 
 | ||||||
|  |       return getModels( | ||||||
|  |         { | ||||||
|  |           key: apiKey, | ||||||
|  |         }, | ||||||
|  |         signal, | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     { enabled: true, refetchOnMount: false }, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (data) dispatch({ field: 'models', value: data }); | ||||||
|  |   }, [data, dispatch]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     dispatch({ field: 'modelError', value: getModelsError(error) }); | ||||||
|  |   }, [dispatch, error, getModelsError]); | ||||||
|  | 
 | ||||||
|  |   // FETCH MODELS ----------------------------------------------
 | ||||||
|  | 
 | ||||||
|  |   const handleSelectConversation = (conversation: Conversation) => { | ||||||
|  |     dispatch({ | ||||||
|  |       field: 'selectedConversation', | ||||||
|  |       value: conversation, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     saveConversation(conversation); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // FOLDER OPERATIONS  --------------------------------------------
 | ||||||
|  | 
 | ||||||
|  |   const handleCreateFolder = (name: string, type: FolderType) => { | ||||||
|  |     const newFolder: FolderInterface = { | ||||||
|  |       id: uuidv4(), | ||||||
|  |       name, | ||||||
|  |       type, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const updatedFolders = [...folders, newFolder]; | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'folders', value: updatedFolders }); | ||||||
|  |     saveFolders(updatedFolders); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDeleteFolder = (folderId: string) => { | ||||||
|  |     const updatedFolders = folders.filter((f) => f.id !== folderId); | ||||||
|  |     dispatch({ field: 'folders', value: updatedFolders }); | ||||||
|  |     saveFolders(updatedFolders); | ||||||
|  | 
 | ||||||
|  |     const updatedConversations: Conversation[] = conversations.map((c) => { | ||||||
|  |       if (c.folderId === folderId) { | ||||||
|  |         return { | ||||||
|  |           ...c, | ||||||
|  |           folderId: null, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return c; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'conversations', value: updatedConversations }); | ||||||
|  |     saveConversations(updatedConversations); | ||||||
|  | 
 | ||||||
|  |     const updatedPrompts: Prompt[] = prompts.map((p) => { | ||||||
|  |       if (p.folderId === folderId) { | ||||||
|  |         return { | ||||||
|  |           ...p, | ||||||
|  |           folderId: null, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return p; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'prompts', value: updatedPrompts }); | ||||||
|  |     savePrompts(updatedPrompts); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleUpdateFolder = (folderId: string, name: string) => { | ||||||
|  |     const updatedFolders = folders.map((f) => { | ||||||
|  |       if (f.id === folderId) { | ||||||
|  |         return { | ||||||
|  |           ...f, | ||||||
|  |           name, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return f; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'folders', value: updatedFolders }); | ||||||
|  | 
 | ||||||
|  |     saveFolders(updatedFolders); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // CONVERSATION OPERATIONS  --------------------------------------------
 | ||||||
|  | 
 | ||||||
|  |   const handleNewConversation = () => { | ||||||
|  |     const lastConversation = conversations[conversations.length - 1]; | ||||||
|  | 
 | ||||||
|  |     const newConversation: Conversation = { | ||||||
|  |       id: uuidv4(), | ||||||
|  |       name: `${t('New Conversation')}`, | ||||||
|  |       messages: [], | ||||||
|  |       model: lastConversation?.model || { | ||||||
|  |         id: OpenAIModels[defaultModelId].id, | ||||||
|  |         name: OpenAIModels[defaultModelId].name, | ||||||
|  |         maxLength: OpenAIModels[defaultModelId].maxLength, | ||||||
|  |         tokenLimit: OpenAIModels[defaultModelId].tokenLimit, | ||||||
|  |       }, | ||||||
|  |       prompt: DEFAULT_SYSTEM_PROMPT, | ||||||
|  |       folderId: null, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const updatedConversations = [...conversations, newConversation]; | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'selectedConversation', value: newConversation }); | ||||||
|  |     dispatch({ field: 'conversations', value: updatedConversations }); | ||||||
|  | 
 | ||||||
|  |     saveConversation(newConversation); | ||||||
|  |     saveConversations(updatedConversations); | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'loading', value: false }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleUpdateConversation = ( | ||||||
|  |     conversation: Conversation, | ||||||
|  |     data: KeyValuePair, | ||||||
|  |   ) => { | ||||||
|  |     const updatedConversation = { | ||||||
|  |       ...conversation, | ||||||
|  |       [data.key]: data.value, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const { single, all } = updateConversation( | ||||||
|  |       updatedConversation, | ||||||
|  |       conversations, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     dispatch({ field: 'selectedConversation', value: single }); | ||||||
|  |     dispatch({ field: 'conversations', value: all }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // EFFECTS  --------------------------------------------
 | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (window.innerWidth < 640) { | ||||||
|  |       dispatch({ field: 'showChatbar', value: false }); | ||||||
|  |     } | ||||||
|  |   }, [selectedConversation]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     defaultModelId && | ||||||
|  |       dispatch({ field: 'defaultModelId', value: defaultModelId }); | ||||||
|  |     serverSideApiKeyIsSet && | ||||||
|  |       dispatch({ | ||||||
|  |         field: 'serverSideApiKeyIsSet', | ||||||
|  |         value: serverSideApiKeyIsSet, | ||||||
|  |       }); | ||||||
|  |     serverSidePluginKeysSet && | ||||||
|  |       dispatch({ | ||||||
|  |         field: 'serverSidePluginKeysSet', | ||||||
|  |         value: serverSidePluginKeysSet, | ||||||
|  |       }); | ||||||
|  |   }, [defaultModelId, serverSideApiKeyIsSet, serverSidePluginKeysSet]); | ||||||
|  | 
 | ||||||
|  |   // ON LOAD --------------------------------------------
 | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     console.log('initialize', serverSideApiKeyIsSet); | ||||||
|  |     const theme = localStorage.getItem('theme'); | ||||||
|  |     if (theme) { | ||||||
|  |       dispatch({ field: 'lightMode', value: theme as 'dark' | 'light' }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const apiKey = localStorage.getItem('apiKey'); | ||||||
|  | 
 | ||||||
|  |     if (serverSideApiKeyIsSet) { | ||||||
|  |       console.log('trigger key', apiKey); | ||||||
|  |       dispatch({ field: 'apiKey', value: '' }); | ||||||
|  | 
 | ||||||
|  |       localStorage.removeItem('apiKey'); | ||||||
|  |     } else if (apiKey) { | ||||||
|  |       dispatch({ field: 'apiKey', value: apiKey }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const pluginKeys = localStorage.getItem('pluginKeys'); | ||||||
|  |     if (serverSidePluginKeysSet) { | ||||||
|  |       dispatch({ field: 'pluginKeys', value: [] }); | ||||||
|  |       localStorage.removeItem('pluginKeys'); | ||||||
|  |     } else if (pluginKeys) { | ||||||
|  |       dispatch({ field: 'pluginKeys', value: pluginKeys }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (window.innerWidth < 640) { | ||||||
|  |       dispatch({ field: 'showChatbar', value: false }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const showChatbar = localStorage.getItem('showChatbar'); | ||||||
|  |     if (showChatbar) { | ||||||
|  |       dispatch({ field: 'showChatbar', value: showChatbar === 'true' }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const showPromptbar = localStorage.getItem('showPromptbar'); | ||||||
|  |     if (showPromptbar) { | ||||||
|  |       dispatch({ field: 'showPromptbar', value: showPromptbar === 'true' }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const folders = localStorage.getItem('folders'); | ||||||
|  |     if (folders) { | ||||||
|  |       dispatch({ field: 'folders', value: JSON.parse(folders) }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const prompts = localStorage.getItem('prompts'); | ||||||
|  |     if (prompts) { | ||||||
|  |       dispatch({ field: 'prompts', value: JSON.parse(prompts) }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const conversationHistory = localStorage.getItem('conversationHistory'); | ||||||
|  |     if (conversationHistory) { | ||||||
|  |       const parsedConversationHistory: Conversation[] = | ||||||
|  |         JSON.parse(conversationHistory); | ||||||
|  |       const cleanedConversationHistory = cleanConversationHistory( | ||||||
|  |         parsedConversationHistory, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       dispatch({ field: 'conversations', value: cleanedConversationHistory }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const selectedConversation = localStorage.getItem('selectedConversation'); | ||||||
|  |     if (selectedConversation) { | ||||||
|  |       const parsedSelectedConversation: Conversation = | ||||||
|  |         JSON.parse(selectedConversation); | ||||||
|  |       const cleanedSelectedConversation = cleanSelectedConversation( | ||||||
|  |         parsedSelectedConversation, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       dispatch({ | ||||||
|  |         field: 'selectedConversation', | ||||||
|  |         value: cleanedSelectedConversation, | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       dispatch({ | ||||||
|  |         field: 'selectedConversation', | ||||||
|  |         value: { | ||||||
|  |           id: uuidv4(), | ||||||
|  |           name: 'New conversation', | ||||||
|  |           messages: [], | ||||||
|  |           model: OpenAIModels[defaultModelId], | ||||||
|  |           prompt: DEFAULT_SYSTEM_PROMPT, | ||||||
|  |           folderId: null, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [ | ||||||
|  |     defaultModelId, | ||||||
|  |     dispatch, | ||||||
|  |     serverSideApiKeyIsSet, | ||||||
|  |     serverSidePluginKeysSet, | ||||||
|  |   ]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <HomeContext.Provider | ||||||
|  |       value={{ | ||||||
|  |         ...contextValue, | ||||||
|  |         handleNewConversation, | ||||||
|  |         handleCreateFolder, | ||||||
|  |         handleDeleteFolder, | ||||||
|  |         handleUpdateFolder, | ||||||
|  |         handleSelectConversation, | ||||||
|  |         handleUpdateConversation, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <Head> | ||||||
|  |         <title>Chatbot UI</title> | ||||||
|  |         <meta name="description" content="ChatGPT but better." /> | ||||||
|  |         <meta | ||||||
|  |           name="viewport" | ||||||
|  |           content="height=device-height ,width=device-width, initial-scale=1, user-scalable=no" | ||||||
|  |         /> | ||||||
|  |         <link rel="icon" href="/favicon.ico" /> | ||||||
|  |       </Head> | ||||||
|  |       {selectedConversation && ( | ||||||
|  |         <main | ||||||
|  |           className={`flex h-screen w-screen flex-col text-sm text-white dark:text-white ${lightMode}`} | ||||||
|  |         > | ||||||
|  |           <div className="fixed top-0 w-full sm:hidden"> | ||||||
|  |             <Navbar | ||||||
|  |               selectedConversation={selectedConversation} | ||||||
|  |               onNewConversation={handleNewConversation} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <div className="flex h-full w-full pt-[48px] sm:pt-0"> | ||||||
|  |             <Chatbar /> | ||||||
|  | 
 | ||||||
|  |             <div className="flex flex-1"> | ||||||
|  |               <Chat stopConversationRef={stopConversationRef} /> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <Promptbar /> | ||||||
|  |           </div> | ||||||
|  |         </main> | ||||||
|  |       )} | ||||||
|  |     </HomeContext.Provider> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | export default Home; | ||||||
|  | 
 | ||||||
|  | export const getServerSideProps: GetServerSideProps = async ({ locale }) => { | ||||||
|  |   const defaultModelId = | ||||||
|  |     (process.env.DEFAULT_MODEL && | ||||||
|  |       Object.values(OpenAIModelID).includes( | ||||||
|  |         process.env.DEFAULT_MODEL as OpenAIModelID, | ||||||
|  |       ) && | ||||||
|  |       process.env.DEFAULT_MODEL) || | ||||||
|  |     fallbackModelID; | ||||||
|  | 
 | ||||||
|  |   let serverSidePluginKeysSet = false; | ||||||
|  | 
 | ||||||
|  |   const googleApiKey = process.env.GOOGLE_API_KEY; | ||||||
|  |   const googleCSEId = process.env.GOOGLE_CSE_ID; | ||||||
|  | 
 | ||||||
|  |   if (googleApiKey && googleCSEId) { | ||||||
|  |     serverSidePluginKeysSet = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     props: { | ||||||
|  |       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, | ||||||
|  |       defaultModelId, | ||||||
|  |       serverSidePluginKeysSet, | ||||||
|  |       ...(await serverSideTranslations(locale ?? 'en', [ | ||||||
|  |         'common', | ||||||
|  |         'chat', | ||||||
|  |         'sidebar', | ||||||
|  |         'markdown', | ||||||
|  |         'promptbar', | ||||||
|  |       ])), | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | export { default, getServerSideProps } from './home'; | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai'; |  | ||||||
| import { OPENAI_API_HOST } from '@/utils/app/const'; | import { OPENAI_API_HOST } from '@/utils/app/const'; | ||||||
| 
 | 
 | ||||||
|  | import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai'; | ||||||
|  | 
 | ||||||
| export const config = { | export const config = { | ||||||
|   runtime: 'edge', |   runtime: 'edge', | ||||||
| }; | }; | ||||||
|  | @ -17,7 +18,7 @@ const handler = async (req: Request): Promise<Response> => { | ||||||
|         Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`, |         Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`, | ||||||
|         ...(process.env.OPENAI_ORGANIZATION && { |         ...(process.env.OPENAI_ORGANIZATION && { | ||||||
|           'OpenAI-Organization': process.env.OPENAI_ORGANIZATION, |           'OpenAI-Organization': process.env.OPENAI_ORGANIZATION, | ||||||
|         }) |         }), | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										896
									
								
								pages/index.tsx
								
								
								
								
							
							
						
						
									
										896
									
								
								pages/index.tsx
								
								
								
								
							|  | @ -1,895 +1 @@ | ||||||
| import { Chat } from '@/components/Chat/Chat'; | export { default, getServerSideProps } from './api/home'; | ||||||
| import { Chatbar } from '@/components/Chatbar/Chatbar'; |  | ||||||
| import { Navbar } from '@/components/Mobile/Navbar'; |  | ||||||
| import { Promptbar } from '@/components/Promptbar/Promptbar'; |  | ||||||
| import { ChatBody, Conversation, Message } from '@/types/chat'; |  | ||||||
| import { KeyValuePair } from '@/types/data'; |  | ||||||
| import { ErrorMessage } from '@/types/error'; |  | ||||||
| import { LatestExportFormat, SupportedExportFormats } from '@/types/export'; |  | ||||||
| import { Folder, FolderType } from '@/types/folder'; |  | ||||||
| import { |  | ||||||
|   OpenAIModel, |  | ||||||
|   OpenAIModelID, |  | ||||||
|   OpenAIModels, |  | ||||||
|   fallbackModelID, |  | ||||||
| } from '@/types/openai'; |  | ||||||
| import { Plugin, PluginKey } from '@/types/plugin'; |  | ||||||
| import { Prompt } from '@/types/prompt'; |  | ||||||
| import { getEndpoint } from '@/utils/app/api'; |  | ||||||
| import { |  | ||||||
|   cleanConversationHistory, |  | ||||||
|   cleanSelectedConversation, |  | ||||||
| } from '@/utils/app/clean'; |  | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; |  | ||||||
| import { |  | ||||||
|   saveConversation, |  | ||||||
|   saveConversations, |  | ||||||
|   updateConversation, |  | ||||||
| } from '@/utils/app/conversation'; |  | ||||||
| import { saveFolders } from '@/utils/app/folders'; |  | ||||||
| import { exportData, importData } from '@/utils/app/importExport'; |  | ||||||
| import { savePrompts } from '@/utils/app/prompts'; |  | ||||||
| import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react'; |  | ||||||
| import { GetServerSideProps } from 'next'; |  | ||||||
| import { useTranslation } from 'next-i18next'; |  | ||||||
| import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; |  | ||||||
| import Head from 'next/head'; |  | ||||||
| import { useEffect, useRef, useState } from 'react'; |  | ||||||
| import toast from 'react-hot-toast'; |  | ||||||
| import { v4 as uuidv4 } from 'uuid'; |  | ||||||
| 
 |  | ||||||
| interface HomeProps { |  | ||||||
|   serverSideApiKeyIsSet: boolean; |  | ||||||
|   serverSidePluginKeysSet: boolean; |  | ||||||
|   defaultModelId: OpenAIModelID; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const Home: React.FC<HomeProps> = ({ |  | ||||||
|   serverSideApiKeyIsSet, |  | ||||||
|   serverSidePluginKeysSet, |  | ||||||
|   defaultModelId, |  | ||||||
| }) => { |  | ||||||
|   const { t } = useTranslation('chat'); |  | ||||||
| 
 |  | ||||||
|   // STATE ----------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const [apiKey, setApiKey] = useState<string>(''); |  | ||||||
|   const [pluginKeys, setPluginKeys] = useState<PluginKey[]>([]); |  | ||||||
|   const [loading, setLoading] = useState<boolean>(false); |  | ||||||
|   const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark'); |  | ||||||
|   const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false); |  | ||||||
| 
 |  | ||||||
|   const [modelError, setModelError] = useState<ErrorMessage | null>(null); |  | ||||||
| 
 |  | ||||||
|   const [models, setModels] = useState<OpenAIModel[]>([]); |  | ||||||
| 
 |  | ||||||
|   const [folders, setFolders] = useState<Folder[]>([]); |  | ||||||
| 
 |  | ||||||
|   const [conversations, setConversations] = useState<Conversation[]>([]); |  | ||||||
|   const [selectedConversation, setSelectedConversation] = |  | ||||||
|     useState<Conversation>(); |  | ||||||
|   const [currentMessage, setCurrentMessage] = useState<Message>(); |  | ||||||
| 
 |  | ||||||
|   const [showSidebar, setShowSidebar] = useState<boolean>(true); |  | ||||||
| 
 |  | ||||||
|   const [prompts, setPrompts] = useState<Prompt[]>([]); |  | ||||||
|   const [showPromptbar, setShowPromptbar] = useState<boolean>(true); |  | ||||||
| 
 |  | ||||||
|   // REFS ----------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const stopConversationRef = useRef<boolean>(false); |  | ||||||
| 
 |  | ||||||
|   // FETCH RESPONSE ----------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const handleSend = async ( |  | ||||||
|     message: Message, |  | ||||||
|     deleteCount = 0, |  | ||||||
|     plugin: Plugin | null = null, |  | ||||||
|   ) => { |  | ||||||
|     if (selectedConversation) { |  | ||||||
|       let updatedConversation: Conversation; |  | ||||||
| 
 |  | ||||||
|       if (deleteCount) { |  | ||||||
|         const updatedMessages = [...selectedConversation.messages]; |  | ||||||
|         for (let i = 0; i < deleteCount; i++) { |  | ||||||
|           updatedMessages.pop(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         updatedConversation = { |  | ||||||
|           ...selectedConversation, |  | ||||||
|           messages: [...updatedMessages, message], |  | ||||||
|         }; |  | ||||||
|       } else { |  | ||||||
|         updatedConversation = { |  | ||||||
|           ...selectedConversation, |  | ||||||
|           messages: [...selectedConversation.messages, message], |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       setSelectedConversation(updatedConversation); |  | ||||||
|       setLoading(true); |  | ||||||
|       setMessageIsStreaming(true); |  | ||||||
| 
 |  | ||||||
|       const chatBody: ChatBody = { |  | ||||||
|         model: updatedConversation.model, |  | ||||||
|         messages: updatedConversation.messages, |  | ||||||
|         key: apiKey, |  | ||||||
|         prompt: updatedConversation.prompt, |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       const endpoint = getEndpoint(plugin); |  | ||||||
|       let body; |  | ||||||
| 
 |  | ||||||
|       if (!plugin) { |  | ||||||
|         body = JSON.stringify(chatBody); |  | ||||||
|       } else { |  | ||||||
|         body = JSON.stringify({ |  | ||||||
|           ...chatBody, |  | ||||||
|           googleAPIKey: pluginKeys |  | ||||||
|             .find((key) => key.pluginId === 'google-search') |  | ||||||
|             ?.requiredKeys.find((key) => key.key === 'GOOGLE_API_KEY')?.value, |  | ||||||
|           googleCSEId: pluginKeys |  | ||||||
|             .find((key) => key.pluginId === 'google-search') |  | ||||||
|             ?.requiredKeys.find((key) => key.key === 'GOOGLE_CSE_ID')?.value, |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const controller = new AbortController(); |  | ||||||
|       const response = await fetch(endpoint, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|           'Content-Type': 'application/json', |  | ||||||
|         }, |  | ||||||
|         signal: controller.signal, |  | ||||||
|         body, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (!response.ok) { |  | ||||||
|         setLoading(false); |  | ||||||
|         setMessageIsStreaming(false); |  | ||||||
|         toast.error(response.statusText); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const data = response.body; |  | ||||||
| 
 |  | ||||||
|       if (!data) { |  | ||||||
|         setLoading(false); |  | ||||||
|         setMessageIsStreaming(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!plugin) { |  | ||||||
|         if (updatedConversation.messages.length === 1) { |  | ||||||
|           const { content } = message; |  | ||||||
|           const customName = |  | ||||||
|             content.length > 30 ? content.substring(0, 30) + '...' : content; |  | ||||||
| 
 |  | ||||||
|           updatedConversation = { |  | ||||||
|             ...updatedConversation, |  | ||||||
|             name: customName, |  | ||||||
|           }; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         setLoading(false); |  | ||||||
| 
 |  | ||||||
|         const reader = data.getReader(); |  | ||||||
|         const decoder = new TextDecoder(); |  | ||||||
|         let done = false; |  | ||||||
|         let isFirst = true; |  | ||||||
|         let text = ''; |  | ||||||
| 
 |  | ||||||
|         while (!done) { |  | ||||||
|           if (stopConversationRef.current === true) { |  | ||||||
|             controller.abort(); |  | ||||||
|             done = true; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|           const { value, done: doneReading } = await reader.read(); |  | ||||||
|           done = doneReading; |  | ||||||
|           const chunkValue = decoder.decode(value); |  | ||||||
| 
 |  | ||||||
|           text += chunkValue; |  | ||||||
| 
 |  | ||||||
|           if (isFirst) { |  | ||||||
|             isFirst = false; |  | ||||||
|             const updatedMessages: Message[] = [ |  | ||||||
|               ...updatedConversation.messages, |  | ||||||
|               { role: 'assistant', content: chunkValue }, |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             updatedConversation = { |  | ||||||
|               ...updatedConversation, |  | ||||||
|               messages: updatedMessages, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             setSelectedConversation(updatedConversation); |  | ||||||
|           } else { |  | ||||||
|             const updatedMessages: Message[] = updatedConversation.messages.map( |  | ||||||
|               (message, index) => { |  | ||||||
|                 if (index === updatedConversation.messages.length - 1) { |  | ||||||
|                   return { |  | ||||||
|                     ...message, |  | ||||||
|                     content: text, |  | ||||||
|                   }; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 return message; |  | ||||||
|               }, |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             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); |  | ||||||
| 
 |  | ||||||
|         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); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // FETCH MODELS ----------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const fetchModels = async (key: string) => { |  | ||||||
|     const error = { |  | ||||||
|       title: t('Error fetching models.'), |  | ||||||
|       code: null, |  | ||||||
|       messageLines: [ |  | ||||||
|         t( |  | ||||||
|           'Make sure your OpenAI API key is set in the bottom left of the sidebar.', |  | ||||||
|         ), |  | ||||||
|         t('If you completed this step, OpenAI may be experiencing issues.'), |  | ||||||
|       ], |  | ||||||
|     } as ErrorMessage; |  | ||||||
| 
 |  | ||||||
|     const response = await fetch('/api/models', { |  | ||||||
|       method: 'POST', |  | ||||||
|       headers: { |  | ||||||
|         'Content-Type': 'application/json', |  | ||||||
|       }, |  | ||||||
|       body: JSON.stringify({ |  | ||||||
|         key, |  | ||||||
|       }), |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (!response.ok) { |  | ||||||
|       try { |  | ||||||
|         const data = await response.json(); |  | ||||||
|         Object.assign(error, { |  | ||||||
|           code: data.error?.code, |  | ||||||
|           messageLines: [data.error?.message], |  | ||||||
|         }); |  | ||||||
|       } catch (e) {} |  | ||||||
|       setModelError(error); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const data = await response.json(); |  | ||||||
| 
 |  | ||||||
|     if (!data) { |  | ||||||
|       setModelError(error); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     setModels(data); |  | ||||||
|     setModelError(null); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // BASIC HANDLERS --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const handleLightMode = (mode: 'dark' | 'light') => { |  | ||||||
|     setLightMode(mode); |  | ||||||
|     localStorage.setItem('theme', mode); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleApiKeyChange = (apiKey: string) => { |  | ||||||
|     setApiKey(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 = () => { |  | ||||||
|     setShowSidebar(!showSidebar); |  | ||||||
|     localStorage.setItem('showChatbar', JSON.stringify(!showSidebar)); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleTogglePromptbar = () => { |  | ||||||
|     setShowPromptbar(!showPromptbar); |  | ||||||
|     localStorage.setItem('showPromptbar', JSON.stringify(!showPromptbar)); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleExportData = () => { |  | ||||||
|     exportData(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleImportConversations = (data: SupportedExportFormats) => { |  | ||||||
|     const { history, folders, prompts }: LatestExportFormat = importData(data); |  | ||||||
| 
 |  | ||||||
|     setConversations(history); |  | ||||||
|     setSelectedConversation(history[history.length - 1]); |  | ||||||
|     setFolders(folders); |  | ||||||
|     setPrompts(prompts); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleSelectConversation = (conversation: Conversation) => { |  | ||||||
|     setSelectedConversation(conversation); |  | ||||||
|     saveConversation(conversation); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // FOLDER OPERATIONS  --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const handleCreateFolder = (name: string, type: FolderType) => { |  | ||||||
|     const newFolder: Folder = { |  | ||||||
|       id: uuidv4(), |  | ||||||
|       name, |  | ||||||
|       type, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const updatedFolders = [...folders, newFolder]; |  | ||||||
| 
 |  | ||||||
|     setFolders(updatedFolders); |  | ||||||
|     saveFolders(updatedFolders); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleDeleteFolder = (folderId: string) => { |  | ||||||
|     const updatedFolders = folders.filter((f) => f.id !== folderId); |  | ||||||
|     setFolders(updatedFolders); |  | ||||||
|     saveFolders(updatedFolders); |  | ||||||
| 
 |  | ||||||
|     const updatedConversations: Conversation[] = conversations.map((c) => { |  | ||||||
|       if (c.folderId === folderId) { |  | ||||||
|         return { |  | ||||||
|           ...c, |  | ||||||
|           folderId: null, |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return c; |  | ||||||
|     }); |  | ||||||
|     setConversations(updatedConversations); |  | ||||||
|     saveConversations(updatedConversations); |  | ||||||
| 
 |  | ||||||
|     const updatedPrompts: Prompt[] = prompts.map((p) => { |  | ||||||
|       if (p.folderId === folderId) { |  | ||||||
|         return { |  | ||||||
|           ...p, |  | ||||||
|           folderId: null, |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return p; |  | ||||||
|     }); |  | ||||||
|     setPrompts(updatedPrompts); |  | ||||||
|     savePrompts(updatedPrompts); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleUpdateFolder = (folderId: string, name: string) => { |  | ||||||
|     const updatedFolders = folders.map((f) => { |  | ||||||
|       if (f.id === folderId) { |  | ||||||
|         return { |  | ||||||
|           ...f, |  | ||||||
|           name, |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return f; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     setFolders(updatedFolders); |  | ||||||
|     saveFolders(updatedFolders); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // CONVERSATION OPERATIONS  --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const handleNewConversation = () => { |  | ||||||
|     const lastConversation = conversations[conversations.length - 1]; |  | ||||||
| 
 |  | ||||||
|     const newConversation: Conversation = { |  | ||||||
|       id: uuidv4(), |  | ||||||
|       name: `${t('New Conversation')}`, |  | ||||||
|       messages: [], |  | ||||||
|       model: lastConversation?.model || { |  | ||||||
|         id: OpenAIModels[defaultModelId].id, |  | ||||||
|         name: OpenAIModels[defaultModelId].name, |  | ||||||
|         maxLength: OpenAIModels[defaultModelId].maxLength, |  | ||||||
|         tokenLimit: OpenAIModels[defaultModelId].tokenLimit, |  | ||||||
|       }, |  | ||||||
|       prompt: DEFAULT_SYSTEM_PROMPT, |  | ||||||
|       folderId: null, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const updatedConversations = [...conversations, newConversation]; |  | ||||||
| 
 |  | ||||||
|     setSelectedConversation(newConversation); |  | ||||||
|     setConversations(updatedConversations); |  | ||||||
| 
 |  | ||||||
|     saveConversation(newConversation); |  | ||||||
|     saveConversations(updatedConversations); |  | ||||||
| 
 |  | ||||||
|     setLoading(false); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleDeleteConversation = (conversation: Conversation) => { |  | ||||||
|     const updatedConversations = conversations.filter( |  | ||||||
|       (c) => c.id !== conversation.id, |  | ||||||
|     ); |  | ||||||
|     setConversations(updatedConversations); |  | ||||||
|     saveConversations(updatedConversations); |  | ||||||
| 
 |  | ||||||
|     if (updatedConversations.length > 0) { |  | ||||||
|       setSelectedConversation( |  | ||||||
|         updatedConversations[updatedConversations.length - 1], |  | ||||||
|       ); |  | ||||||
|       saveConversation(updatedConversations[updatedConversations.length - 1]); |  | ||||||
|     } else { |  | ||||||
|       setSelectedConversation({ |  | ||||||
|         id: uuidv4(), |  | ||||||
|         name: 'New conversation', |  | ||||||
|         messages: [], |  | ||||||
|         model: OpenAIModels[defaultModelId], |  | ||||||
|         prompt: DEFAULT_SYSTEM_PROMPT, |  | ||||||
|         folderId: null, |  | ||||||
|       }); |  | ||||||
|       localStorage.removeItem('selectedConversation'); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleUpdateConversation = ( |  | ||||||
|     conversation: Conversation, |  | ||||||
|     data: KeyValuePair, |  | ||||||
|   ) => { |  | ||||||
|     const updatedConversation = { |  | ||||||
|       ...conversation, |  | ||||||
|       [data.key]: data.value, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const { single, all } = updateConversation( |  | ||||||
|       updatedConversation, |  | ||||||
|       conversations, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     setSelectedConversation(single); |  | ||||||
|     setConversations(all); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleClearConversations = () => { |  | ||||||
|     setConversations([]); |  | ||||||
|     localStorage.removeItem('conversationHistory'); |  | ||||||
| 
 |  | ||||||
|     setSelectedConversation({ |  | ||||||
|       id: uuidv4(), |  | ||||||
|       name: 'New conversation', |  | ||||||
|       messages: [], |  | ||||||
|       model: OpenAIModels[defaultModelId], |  | ||||||
|       prompt: DEFAULT_SYSTEM_PROMPT, |  | ||||||
|       folderId: null, |  | ||||||
|     }); |  | ||||||
|     localStorage.removeItem('selectedConversation'); |  | ||||||
| 
 |  | ||||||
|     const updatedFolders = folders.filter((f) => f.type !== 'chat'); |  | ||||||
|     setFolders(updatedFolders); |  | ||||||
|     saveFolders(updatedFolders); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleEditMessage = (message: Message, messageIndex: number) => { |  | ||||||
|     if (selectedConversation) { |  | ||||||
|       const updatedMessages = selectedConversation.messages |  | ||||||
|         .map((m, i) => { |  | ||||||
|           if (i < messageIndex) { |  | ||||||
|             return m; |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         .filter((m) => m) as Message[]; |  | ||||||
| 
 |  | ||||||
|       const updatedConversation = { |  | ||||||
|         ...selectedConversation, |  | ||||||
|         messages: updatedMessages, |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       const { single, all } = updateConversation( |  | ||||||
|         updatedConversation, |  | ||||||
|         conversations, |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       setSelectedConversation(single); |  | ||||||
|       setConversations(all); |  | ||||||
| 
 |  | ||||||
|       setCurrentMessage(message); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // PROMPT OPERATIONS --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   const handleCreatePrompt = () => { |  | ||||||
|     const newPrompt: Prompt = { |  | ||||||
|       id: uuidv4(), |  | ||||||
|       name: `Prompt ${prompts.length + 1}`, |  | ||||||
|       description: '', |  | ||||||
|       content: '', |  | ||||||
|       model: OpenAIModels[defaultModelId], |  | ||||||
|       folderId: null, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const updatedPrompts = [...prompts, newPrompt]; |  | ||||||
| 
 |  | ||||||
|     setPrompts(updatedPrompts); |  | ||||||
|     savePrompts(updatedPrompts); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleUpdatePrompt = (prompt: Prompt) => { |  | ||||||
|     const updatedPrompts = prompts.map((p) => { |  | ||||||
|       if (p.id === prompt.id) { |  | ||||||
|         return prompt; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return p; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     setPrompts(updatedPrompts); |  | ||||||
|     savePrompts(updatedPrompts); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleDeletePrompt = (prompt: Prompt) => { |  | ||||||
|     const updatedPrompts = prompts.filter((p) => p.id !== prompt.id); |  | ||||||
|     setPrompts(updatedPrompts); |  | ||||||
|     savePrompts(updatedPrompts); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // EFFECTS  --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (currentMessage) { |  | ||||||
|       handleSend(currentMessage); |  | ||||||
|       setCurrentMessage(undefined); |  | ||||||
|     } |  | ||||||
|   }, [currentMessage]); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (window.innerWidth < 640) { |  | ||||||
|       setShowSidebar(false); |  | ||||||
|     } |  | ||||||
|   }, [selectedConversation]); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (apiKey) { |  | ||||||
|       fetchModels(apiKey); |  | ||||||
|     } |  | ||||||
|   }, [apiKey]); |  | ||||||
| 
 |  | ||||||
|   // ON LOAD --------------------------------------------
 |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     const theme = localStorage.getItem('theme'); |  | ||||||
|     if (theme) { |  | ||||||
|       setLightMode(theme as 'dark' | 'light'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const apiKey = localStorage.getItem('apiKey'); |  | ||||||
|     if (serverSideApiKeyIsSet) { |  | ||||||
|       fetchModels(''); |  | ||||||
|       setApiKey(''); |  | ||||||
|       localStorage.removeItem('apiKey'); |  | ||||||
|     } else if (apiKey) { |  | ||||||
|       setApiKey(apiKey); |  | ||||||
|       fetchModels(apiKey); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const pluginKeys = localStorage.getItem('pluginKeys'); |  | ||||||
|     if (serverSidePluginKeysSet) { |  | ||||||
|       setPluginKeys([]); |  | ||||||
|       localStorage.removeItem('pluginKeys'); |  | ||||||
|     } else if (pluginKeys) { |  | ||||||
|       setPluginKeys(JSON.parse(pluginKeys)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (window.innerWidth < 640) { |  | ||||||
|       setShowSidebar(false); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const showChatbar = localStorage.getItem('showChatbar'); |  | ||||||
|     if (showChatbar) { |  | ||||||
|       setShowSidebar(showChatbar === 'true'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const showPromptbar = localStorage.getItem('showPromptbar'); |  | ||||||
|     if (showPromptbar) { |  | ||||||
|       setShowPromptbar(showPromptbar === 'true'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const folders = localStorage.getItem('folders'); |  | ||||||
|     if (folders) { |  | ||||||
|       setFolders(JSON.parse(folders)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const prompts = localStorage.getItem('prompts'); |  | ||||||
|     if (prompts) { |  | ||||||
|       setPrompts(JSON.parse(prompts)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const conversationHistory = localStorage.getItem('conversationHistory'); |  | ||||||
|     if (conversationHistory) { |  | ||||||
|       const parsedConversationHistory: Conversation[] = |  | ||||||
|         JSON.parse(conversationHistory); |  | ||||||
|       const cleanedConversationHistory = cleanConversationHistory( |  | ||||||
|         parsedConversationHistory, |  | ||||||
|       ); |  | ||||||
|       setConversations(cleanedConversationHistory); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const selectedConversation = localStorage.getItem('selectedConversation'); |  | ||||||
|     if (selectedConversation) { |  | ||||||
|       const parsedSelectedConversation: Conversation = |  | ||||||
|         JSON.parse(selectedConversation); |  | ||||||
|       const cleanedSelectedConversation = cleanSelectedConversation( |  | ||||||
|         parsedSelectedConversation, |  | ||||||
|       ); |  | ||||||
|       setSelectedConversation(cleanedSelectedConversation); |  | ||||||
|     } else { |  | ||||||
|       setSelectedConversation({ |  | ||||||
|         id: uuidv4(), |  | ||||||
|         name: 'New conversation', |  | ||||||
|         messages: [], |  | ||||||
|         model: OpenAIModels[defaultModelId], |  | ||||||
|         prompt: DEFAULT_SYSTEM_PROMPT, |  | ||||||
|         folderId: null, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }, [serverSideApiKeyIsSet]); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <Head> |  | ||||||
|         <title>Chatbot UI</title> |  | ||||||
|         <meta name="description" content="ChatGPT but better." /> |  | ||||||
|         <meta |  | ||||||
|           name="viewport" |  | ||||||
|           content="height=device-height ,width=device-width, initial-scale=1, user-scalable=no" |  | ||||||
|         /> |  | ||||||
|         <link rel="icon" href="/favicon.ico" /> |  | ||||||
|       </Head> |  | ||||||
|       {selectedConversation && ( |  | ||||||
|         <main |  | ||||||
|           className={`flex h-screen w-screen flex-col text-sm text-white dark:text-white ${lightMode}`} |  | ||||||
|         > |  | ||||||
|           <div className="fixed top-0 w-full sm:hidden"> |  | ||||||
|             <Navbar |  | ||||||
|               selectedConversation={selectedConversation} |  | ||||||
|               onNewConversation={handleNewConversation} |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <div className="flex h-full w-full pt-[48px] sm:pt-0"> |  | ||||||
|             {showSidebar ? ( |  | ||||||
|               <div> |  | ||||||
|                 <Chatbar |  | ||||||
|                   loading={messageIsStreaming} |  | ||||||
|                   conversations={conversations} |  | ||||||
|                   lightMode={lightMode} |  | ||||||
|                   selectedConversation={selectedConversation} |  | ||||||
|                   apiKey={apiKey} |  | ||||||
|                   serverSideApiKeyIsSet={serverSideApiKeyIsSet} |  | ||||||
|                   pluginKeys={pluginKeys} |  | ||||||
|                   serverSidePluginKeysSet={serverSidePluginKeysSet} |  | ||||||
|                   folders={folders.filter((folder) => folder.type === 'chat')} |  | ||||||
|                   onToggleLightMode={handleLightMode} |  | ||||||
|                   onCreateFolder={(name) => handleCreateFolder(name, 'chat')} |  | ||||||
|                   onDeleteFolder={handleDeleteFolder} |  | ||||||
|                   onUpdateFolder={handleUpdateFolder} |  | ||||||
|                   onNewConversation={handleNewConversation} |  | ||||||
|                   onSelectConversation={handleSelectConversation} |  | ||||||
|                   onDeleteConversation={handleDeleteConversation} |  | ||||||
|                   onUpdateConversation={handleUpdateConversation} |  | ||||||
|                   onApiKeyChange={handleApiKeyChange} |  | ||||||
|                   onClearConversations={handleClearConversations} |  | ||||||
|                   onExportConversations={handleExportData} |  | ||||||
|                   onImportConversations={handleImportConversations} |  | ||||||
|                   onPluginKeyChange={handlePluginKeyChange} |  | ||||||
|                   onClearPluginKey={handleClearPluginKey} |  | ||||||
|                 /> |  | ||||||
| 
 |  | ||||||
|                 <button |  | ||||||
|                   className="fixed top-5 left-[270px] z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-[270px] sm:h-8 sm:w-8 sm:text-neutral-700" |  | ||||||
|                   onClick={handleToggleChatbar} |  | ||||||
|                 > |  | ||||||
|                   <IconArrowBarLeft /> |  | ||||||
|                 </button> |  | ||||||
|                 <div |  | ||||||
|                   onClick={handleToggleChatbar} |  | ||||||
|                   className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden" |  | ||||||
|                 ></div> |  | ||||||
|               </div> |  | ||||||
|             ) : ( |  | ||||||
|               <button |  | ||||||
|                 className="fixed top-2.5 left-4 z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-4 sm:h-8 sm:w-8 sm:text-neutral-700" |  | ||||||
|                 onClick={handleToggleChatbar} |  | ||||||
|               > |  | ||||||
|                 <IconArrowBarRight /> |  | ||||||
|               </button> |  | ||||||
|             )} |  | ||||||
| 
 |  | ||||||
|             <div className="flex flex-1"> |  | ||||||
|               <Chat |  | ||||||
|                 conversation={selectedConversation} |  | ||||||
|                 messageIsStreaming={messageIsStreaming} |  | ||||||
|                 apiKey={apiKey} |  | ||||||
|                 serverSideApiKeyIsSet={serverSideApiKeyIsSet} |  | ||||||
|                 defaultModelId={defaultModelId} |  | ||||||
|                 modelError={modelError} |  | ||||||
|                 models={models} |  | ||||||
|                 loading={loading} |  | ||||||
|                 prompts={prompts} |  | ||||||
|                 onSend={handleSend} |  | ||||||
|                 onUpdateConversation={handleUpdateConversation} |  | ||||||
|                 onEditMessage={handleEditMessage} |  | ||||||
|                 stopConversationRef={stopConversationRef} |  | ||||||
|               /> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             {showPromptbar ? ( |  | ||||||
|               <div> |  | ||||||
|                 <Promptbar |  | ||||||
|                   prompts={prompts} |  | ||||||
|                   folders={folders.filter((folder) => folder.type === 'prompt')} |  | ||||||
|                   onCreatePrompt={handleCreatePrompt} |  | ||||||
|                   onUpdatePrompt={handleUpdatePrompt} |  | ||||||
|                   onDeletePrompt={handleDeletePrompt} |  | ||||||
|                   onCreateFolder={(name) => handleCreateFolder(name, 'prompt')} |  | ||||||
|                   onDeleteFolder={handleDeleteFolder} |  | ||||||
|                   onUpdateFolder={handleUpdateFolder} |  | ||||||
|                 /> |  | ||||||
|                 <button |  | ||||||
|                   className="fixed top-5 right-[270px] z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-[270px] sm:h-8 sm:w-8 sm:text-neutral-700" |  | ||||||
|                   onClick={handleTogglePromptbar} |  | ||||||
|                 > |  | ||||||
|                   <IconArrowBarRight /> |  | ||||||
|                 </button> |  | ||||||
|                 <div |  | ||||||
|                   onClick={handleTogglePromptbar} |  | ||||||
|                   className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden" |  | ||||||
|                 ></div> |  | ||||||
|               </div> |  | ||||||
|             ) : ( |  | ||||||
|               <button |  | ||||||
|                 className="fixed top-2.5 right-4 z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-4 sm:h-8 sm:w-8 sm:text-neutral-700" |  | ||||||
|                 onClick={handleTogglePromptbar} |  | ||||||
|               > |  | ||||||
|                 <IconArrowBarLeft /> |  | ||||||
|               </button> |  | ||||||
|             )} |  | ||||||
|           </div> |  | ||||||
|         </main> |  | ||||||
|       )} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| export default Home; |  | ||||||
| 
 |  | ||||||
| export const getServerSideProps: GetServerSideProps = async ({ locale }) => { |  | ||||||
|   const defaultModelId = |  | ||||||
|     (process.env.DEFAULT_MODEL && |  | ||||||
|       Object.values(OpenAIModelID).includes( |  | ||||||
|         process.env.DEFAULT_MODEL as OpenAIModelID, |  | ||||||
|       ) && |  | ||||||
|       process.env.DEFAULT_MODEL) || |  | ||||||
|     fallbackModelID; |  | ||||||
| 
 |  | ||||||
|   let serverSidePluginKeysSet = false; |  | ||||||
| 
 |  | ||||||
|   const googleApiKey = process.env.GOOGLE_API_KEY; |  | ||||||
|   const googleCSEId = process.env.GOOGLE_CSE_ID; |  | ||||||
| 
 |  | ||||||
|   if (googleApiKey && googleCSEId) { |  | ||||||
|     serverSidePluginKeysSet = true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     props: { |  | ||||||
|       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, |  | ||||||
|       defaultModelId, |  | ||||||
|       serverSidePluginKeysSet, |  | ||||||
|       ...(await serverSideTranslations(locale ?? 'en', [ |  | ||||||
|         'common', |  | ||||||
|         'chat', |  | ||||||
|         'sidebar', |  | ||||||
|         'markdown', |  | ||||||
|         'promptbar', |  | ||||||
|       ])), |  | ||||||
|     }, |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
|  | @ -1,5 +1,25 @@ | ||||||
| module.exports = { | module.exports = { | ||||||
|   trailingComma: 'all', |   trailingComma: 'all', | ||||||
|   singleQuote: true, |   singleQuote: true, | ||||||
|   plugins: [require('prettier-plugin-tailwindcss')] |   plugins: [ | ||||||
|  |     'prettier-plugin-tailwindcss', | ||||||
|  |     '@trivago/prettier-plugin-sort-imports', | ||||||
|  |   ], | ||||||
|  |   importOrder: [ | ||||||
|  |     'react', // React
 | ||||||
|  |     '^react-.*$', // React-related imports
 | ||||||
|  |     '^next', // Next-related imports
 | ||||||
|  |     '^next-.*$', // Next-related imports
 | ||||||
|  |     '^next/.*$', // Next-related imports
 | ||||||
|  |     '^.*/hooks/.*$', // Hooks
 | ||||||
|  |     '^.*/services/.*$', // Services
 | ||||||
|  |     '^.*/utils/.*$', // Utils
 | ||||||
|  |     '^.*/types/.*$', // Types
 | ||||||
|  |     '^.*/pages/.*$', // Components
 | ||||||
|  |     '^.*/components/.*$', // Components
 | ||||||
|  |     '^[./]', // Other imports
 | ||||||
|  |     '.*', // Any uncaught imports
 | ||||||
|  |   ], | ||||||
|  |   importOrderSeparation: true, | ||||||
|  |   importOrderSortSpecifiers: true, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -23,14 +23,8 @@ | ||||||
| 
 | 
 | ||||||
|   "click if using a .env.local file": ".env.local انقر إذا كنت تستخدم ملف", |   "click if using a .env.local file": ".env.local انقر إذا كنت تستخدم ملف", | ||||||
| 
 | 
 | ||||||
|    |  | ||||||
|   "Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "حرفًا {{maxLength}}  حد الرسالة هو {{valueLength}} لقد أدخلت ", |   "Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "حرفًا {{maxLength}}  حد الرسالة هو {{valueLength}} لقد أدخلت ", | ||||||
| 
 | 
 | ||||||
|   |  | ||||||
| 
 |  | ||||||
|    |  | ||||||
|    |  | ||||||
|    |  | ||||||
|   "Please enter a message": "يرجى إدخال رسالة", |   "Please enter a message": "يرجى إدخال رسالة", | ||||||
|   "Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI هي مجموعة متقدمة للدردشة تستخدم", |   "Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI هي مجموعة متقدمة للدردشة تستخدم", | ||||||
|   "Are you sure you want to clear all messages?": "هل أنت متأكد أنك تريد مسح كافة الرسائل؟" |   "Are you sure you want to clear all messages?": "هل أنت متأكد أنك تريد مسح كافة الرسائل؟" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| { | { | ||||||
| "Copy code": "نسخ الكود", |   "Copy code": "نسخ الكود", | ||||||
| "Copied!": "تم النسخ!", |   "Copied!": "تم النسخ!", | ||||||
| "Enter file name": "أدخل اسم الملف" |   "Enter file name": "أدخل اسم الملف" | ||||||
| } | } | ||||||
|  | @ -9,4 +9,4 @@ | ||||||
|   "Prompt": "مطلب", |   "Prompt": "مطلب", | ||||||
|   "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "محتوى المطلب. استخدم {{}} للإشارة إلى متغير. مثال: {{الاسم}} هي {{صفة}} {{اسم}}", |   "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "محتوى المطلب. استخدم {{}} للإشارة إلى متغير. مثال: {{الاسم}} هي {{صفة}} {{اسم}}", | ||||||
|   "Save": "حفظ" |   "Save": "حفظ" | ||||||
|   } | } | ||||||
|  |  | ||||||
|  | @ -1,28 +1,28 @@ | ||||||
| { | { | ||||||
|     "OpenAI API Key Required": "Memerlukan Kunci API OpenAI", |   "OpenAI API Key Required": "Memerlukan Kunci API OpenAI", | ||||||
|     "Please set your OpenAI API key in the bottom left of the sidebar.": "Silakan atur kunci API OpenAI Anda di bagian kiri bawah bilah sisi.", |   "Please set your OpenAI API key in the bottom left of the sidebar.": "Silakan atur kunci API OpenAI Anda di bagian kiri bawah bilah sisi.", | ||||||
|     "Stop Generating": "Berhenti Menghasilkan", |   "Stop Generating": "Berhenti Menghasilkan", | ||||||
|     "Prompt limit is {{maxLength}} characters": "Batas karakter untuk prompt adalah {{maxLength}} karakter", |   "Prompt limit is {{maxLength}} characters": "Batas karakter untuk prompt adalah {{maxLength}} karakter", | ||||||
|     "System Prompt": "Prompt Sistem", |   "System Prompt": "Prompt Sistem", | ||||||
|     "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Anda adalah ChatGPT, model bahasa besar yang dilatih oleh OpenAI. Ikuti instruksi pengguna dengan hati-hati. Balas menggunakan markdown.", |   "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Anda adalah ChatGPT, model bahasa besar yang dilatih oleh OpenAI. Ikuti instruksi pengguna dengan hati-hati. Balas menggunakan markdown.", | ||||||
|     "Enter a prompt": "Masukkan sebuah prompt", |   "Enter a prompt": "Masukkan sebuah prompt", | ||||||
|     "Regenerate response": "Hasilkan kembali respons", |   "Regenerate response": "Hasilkan kembali respons", | ||||||
|     "Sorry, there was an error.": "Maaf, terjadi kesalahan.", |   "Sorry, there was an error.": "Maaf, terjadi kesalahan.", | ||||||
|     "Model": "Model", |   "Model": "Model", | ||||||
|     "Conversation": "Percakapan", |   "Conversation": "Percakapan", | ||||||
|     "OR": "ATAU", |   "OR": "ATAU", | ||||||
|     "Loading...": "Memuat...", |   "Loading...": "Memuat...", | ||||||
|     "Type a message...": "Ketik sebuah pesan...", |   "Type a message...": "Ketik sebuah pesan...", | ||||||
|     "Error fetching models.": "Kesalahan dalam mengambil model.", |   "Error fetching models.": "Kesalahan dalam mengambil model.", | ||||||
|     "AI": "AI", |   "AI": "AI", | ||||||
|     "You": "Anda", |   "You": "Anda", | ||||||
|     "Cancel": "Cancel", |   "Cancel": "Cancel", | ||||||
|     "Save & Submit": "Save & Submit", |   "Save & Submit": "Save & Submit", | ||||||
|     "Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Pastikan kunci API OpenAI Anda diatur di bagian kiri bawah bilah sisi.", |   "Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Pastikan kunci API OpenAI Anda diatur di bagian kiri bawah bilah sisi.", | ||||||
|     "If you completed this step, OpenAI may be experiencing issues.": "Jika Anda telah menyelesaikan langkah ini, OpenAI mungkin mengalami masalah.", |   "If you completed this step, OpenAI may be experiencing issues.": "Jika Anda telah menyelesaikan langkah ini, OpenAI mungkin mengalami masalah.", | ||||||
|     "click if using a .env.local file": "klik jika menggunakan file .env.local", |   "click if using a .env.local file": "klik jika menggunakan file .env.local", | ||||||
|     "Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "Batas karakter untuk pesan adalah {{maxLength}} karakter. Anda telah memasukkan {{valueLength}} karakter.", |   "Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "Batas karakter untuk pesan adalah {{maxLength}} karakter. Anda telah memasukkan {{valueLength}} karakter.", | ||||||
|     "Please enter a message": "Silakan masukkan sebuah pesan", |   "Please enter a message": "Silakan masukkan sebuah pesan", | ||||||
|     "Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI adalah kit chatbot canggih untuk model obrolan OpenAI yang bertujuan meniru antarmuka dan fungsionalitas ChatGPT.", |   "Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI adalah kit chatbot canggih untuk model obrolan OpenAI yang bertujuan meniru antarmuka dan fungsionalitas ChatGPT.", | ||||||
|     "Are you sure you want to clear all messages?": "Apakah Anda yakin ingin menghapus semua pesan?" |   "Are you sure you want to clear all messages?": "Apakah Anda yakin ingin menghapus semua pesan?" | ||||||
| } | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| { | { | ||||||
|     "Copy code": "Salin kode", |   "Copy code": "Salin kode", | ||||||
|     "Copied!": "Kode disalin!", |   "Copied!": "Kode disalin!", | ||||||
|     "Enter file name": "Masukkan nama file" |   "Enter file name": "Masukkan nama file" | ||||||
| } | } | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | import { useMemo } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useTranslation } from 'next-i18next'; | ||||||
|  | 
 | ||||||
|  | import { ErrorMessage } from '@/types/error'; | ||||||
|  | 
 | ||||||
|  | const useErrorService = () => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     getModelsError: useMemo( | ||||||
|  |       () => (error: any) => { | ||||||
|  |         return !error | ||||||
|  |           ? null | ||||||
|  |           : ({ | ||||||
|  |               title: t('Error fetching models.'), | ||||||
|  |               code: error.status || 'unknown', | ||||||
|  |               messageLines: error.statusText | ||||||
|  |                 ? [error.statusText] | ||||||
|  |                 : [ | ||||||
|  |                     t( | ||||||
|  |                       'Make sure your OpenAI API key is set in the bottom left of the sidebar.', | ||||||
|  |                     ), | ||||||
|  |                     t( | ||||||
|  |                       'If you completed this step, OpenAI may be experiencing issues.', | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |             } as ErrorMessage); | ||||||
|  |       }, | ||||||
|  |       [t], | ||||||
|  |     ), | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default useErrorService; | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | import { useCallback } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useFetch } from '@/hooks/useFetch'; | ||||||
|  | 
 | ||||||
|  | export interface GetModelsRequestProps { | ||||||
|  |   key: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const useApiService = () => { | ||||||
|  |   const fetchService = useFetch(); | ||||||
|  | 
 | ||||||
|  |   // const getModels = useCallback(
 | ||||||
|  |   // 	(
 | ||||||
|  |   // 		params: GetManagementRoutineInstanceDetailedParams,
 | ||||||
|  |   // 		signal?: AbortSignal
 | ||||||
|  |   // 	) => {
 | ||||||
|  |   // 		return fetchService.get<GetManagementRoutineInstanceDetailed>(
 | ||||||
|  |   // 			`/v1/ManagementRoutines/${params.managementRoutineId}/instances/${params.instanceId
 | ||||||
|  |   // 			}?sensorGroupIds=${params.sensorGroupId ?? ''}`,
 | ||||||
|  |   // 			{
 | ||||||
|  |   // 				signal,
 | ||||||
|  |   // 			}
 | ||||||
|  |   // 		);
 | ||||||
|  |   // 	},
 | ||||||
|  |   // 	[fetchService]
 | ||||||
|  |   // );
 | ||||||
|  | 
 | ||||||
|  |   const getModels = useCallback( | ||||||
|  |     (params: GetModelsRequestProps, signal?: AbortSignal) => { | ||||||
|  |       return fetchService.post<GetModelsRequestProps>(`/api/models`, { | ||||||
|  |         body: { key: params.key }, | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         signal, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     [fetchService], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     getModels, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default useApiService; | ||||||
|  | @ -11,8 +11,8 @@ module.exports = { | ||||||
|   }, |   }, | ||||||
|   variants: { |   variants: { | ||||||
|     extend: { |     extend: { | ||||||
|       visibility: ["group-hover"], |       visibility: ['group-hover'], | ||||||
|     }, |     }, | ||||||
|    }, |   }, | ||||||
|   plugins: [require('@tailwindcss/typography')], |   plugins: [require('@tailwindcss/typography')], | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Conversation, Message } from './chat'; | import { Conversation, Message } from './chat'; | ||||||
| import { Folder } from './folder'; | import { FolderInterface } from './folder'; | ||||||
| import { OpenAIModel } from './openai'; | import { OpenAIModel } from './openai'; | ||||||
| import { Prompt } from './prompt'; | import { Prompt } from './prompt'; | ||||||
| 
 | 
 | ||||||
|  | @ -34,12 +34,12 @@ export interface ExportFormatV2 { | ||||||
| export interface ExportFormatV3 { | export interface ExportFormatV3 { | ||||||
|   version: 3; |   version: 3; | ||||||
|   history: Conversation[]; |   history: Conversation[]; | ||||||
|   folders: Folder[]; |   folders: FolderInterface[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ExportFormatV4 { | export interface ExportFormatV4 { | ||||||
|   version: 4; |   version: 4; | ||||||
|   history: Conversation[]; |   history: Conversation[]; | ||||||
|   folders: Folder[]; |   folders: FolderInterface[]; | ||||||
|   prompts: Prompt[] |   prompts: Prompt[]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| export interface Folder { | export interface FolderInterface { | ||||||
|   id: string; |   id: string; | ||||||
|   name: string; |   name: string; | ||||||
|   type: FolderType; |   type: FolderType; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Conversation } from './chat'; | import { Conversation } from './chat'; | ||||||
| import { Folder } from './folder'; | import { FolderInterface } from './folder'; | ||||||
| import { PluginKey } from './plugin'; | import { PluginKey } from './plugin'; | ||||||
| import { Prompt } from './prompt'; | import { Prompt } from './prompt'; | ||||||
| 
 | 
 | ||||||
|  | @ -10,7 +10,7 @@ export interface LocalStorage { | ||||||
|   selectedConversation: Conversation; |   selectedConversation: Conversation; | ||||||
|   theme: 'light' | 'dark'; |   theme: 'light' | 'dark'; | ||||||
|   // added folders (3/23/23)
 |   // added folders (3/23/23)
 | ||||||
|   folders: Folder[]; |   folders: FolderInterface[]; | ||||||
|   // added prompts (3/26/23)
 |   // added prompts (3/26/23)
 | ||||||
|   prompts: Prompt[]; |   prompts: Prompt[]; | ||||||
|   // added showChatbar and showPromptbar (3/26/23)
 |   // added showChatbar and showPromptbar (3/26/23)
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Conversation } from '@/types/chat'; | import { Conversation } from '@/types/chat'; | ||||||
| import { OpenAIModelID, OpenAIModels } from '@/types/openai'; | import { OpenAIModelID, OpenAIModels } from '@/types/openai'; | ||||||
|  | 
 | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from './const'; | import { DEFAULT_SYSTEM_PROMPT } from './const'; | ||||||
| 
 | 
 | ||||||
| export const cleanSelectedConversation = (conversation: Conversation) => { | export const cleanSelectedConversation = (conversation: Conversation) => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| export const DEFAULT_SYSTEM_PROMPT = | export const DEFAULT_SYSTEM_PROMPT = | ||||||
|   process.env.DEFAULT_SYSTEM_PROMPT || "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown."; |   process.env.DEFAULT_SYSTEM_PROMPT || | ||||||
|  |   "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown."; | ||||||
| 
 | 
 | ||||||
| export const OPENAI_API_HOST = | export const OPENAI_API_HOST = | ||||||
|   process.env.OPENAI_API_HOST || 'https://api.openai.com'; |   process.env.OPENAI_API_HOST || 'https://api.openai.com'; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Folder } from '@/types/folder'; | import { FolderInterface } from '@/types/folder'; | ||||||
| 
 | 
 | ||||||
| export const saveFolders = (folders: Folder[]) => { | export const saveFolders = (folders: FolderInterface[]) => { | ||||||
|   localStorage.setItem('folders', JSON.stringify(folders)); |   localStorage.setItem('folders', JSON.stringify(folders)); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { | ||||||
|   LatestExportFormat, |   LatestExportFormat, | ||||||
|   SupportedExportFormats, |   SupportedExportFormats, | ||||||
| } from '@/types/export'; | } from '@/types/export'; | ||||||
|  | 
 | ||||||
| import { cleanConversationHistory } from './clean'; | import { cleanConversationHistory } from './clean'; | ||||||
| 
 | 
 | ||||||
| export function isExportFormatV1(obj: any): obj is ExportFormatV1 { | export function isExportFormatV1(obj: any): obj is ExportFormatV1 { | ||||||
|  | @ -50,14 +51,13 @@ export function cleanData(data: SupportedExportFormats): LatestExportFormat { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (isExportFormatV3(data)) { |   if (isExportFormatV3(data)) { | ||||||
|     return {...data, version: 4, prompts: []}; |     return { ...data, version: 4, prompts: [] }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if(isExportFormatV4(data)){ |   if (isExportFormatV4(data)) { | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   throw new Error('Unsupported data format'); |   throw new Error('Unsupported data format'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -81,7 +81,7 @@ export const exportData = () => { | ||||||
|     folders = JSON.parse(folders); |     folders = JSON.parse(folders); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if(prompts){ |   if (prompts) { | ||||||
|     prompts = JSON.parse(prompts); |     prompts = JSON.parse(prompts); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -110,7 +110,7 @@ export const importData = ( | ||||||
|   data: SupportedExportFormats, |   data: SupportedExportFormats, | ||||||
| ): LatestExportFormat => { | ): LatestExportFormat => { | ||||||
|   const cleanedData = cleanData(data); |   const cleanedData = cleanData(data); | ||||||
|   const { history,folders, prompts } = cleanedData; |   const { history, folders, prompts } = cleanedData; | ||||||
| 
 | 
 | ||||||
|   const conversations = history; |   const conversations = history; | ||||||
|   localStorage.setItem('conversationHistory', JSON.stringify(conversations)); |   localStorage.setItem('conversationHistory', JSON.stringify(conversations)); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | export function throttle<T extends (...args: any[]) => any>( | ||||||
|  |   func: T, | ||||||
|  |   limit: number, | ||||||
|  | ): T { | ||||||
|  |   let lastFunc: ReturnType<typeof setTimeout>; | ||||||
|  |   let lastRan: number; | ||||||
|  | 
 | ||||||
|  |   return ((...args) => { | ||||||
|  |     if (!lastRan) { | ||||||
|  |       func(...args); | ||||||
|  |       lastRan = Date.now(); | ||||||
|  |     } else { | ||||||
|  |       clearTimeout(lastFunc); | ||||||
|  |       lastFunc = setTimeout(() => { | ||||||
|  |         if (Date.now() - lastRan >= limit) { | ||||||
|  |           func(...args); | ||||||
|  |           lastRan = Date.now(); | ||||||
|  |         } | ||||||
|  |       }, limit - (Date.now() - lastRan)); | ||||||
|  |     } | ||||||
|  |   }) as T; | ||||||
|  | } | ||||||
|  | @ -1,19 +0,0 @@ | ||||||
| export function throttle<T extends (...args: any[]) => any>(func: T, limit: number): T { |  | ||||||
|     let lastFunc: ReturnType<typeof setTimeout>; |  | ||||||
|     let lastRan: number; |  | ||||||
| 
 |  | ||||||
|     return ((...args) => { |  | ||||||
|         if (!lastRan) { |  | ||||||
|             func(...args); |  | ||||||
|             lastRan = Date.now(); |  | ||||||
|         } else { |  | ||||||
|             clearTimeout(lastFunc); |  | ||||||
|             lastFunc = setTimeout(() => { |  | ||||||
|                 if (Date.now() - lastRan >= limit) { |  | ||||||
|                     func(...args); |  | ||||||
|                     lastRan = Date.now(); |  | ||||||
|                 } |  | ||||||
|             }, limit - (Date.now() - lastRan)); |  | ||||||
|         } |  | ||||||
|     }) as T; |  | ||||||
| } |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| import { Message } from '@/types/chat'; | import { Message } from '@/types/chat'; | ||||||
| import { OpenAIModel } from '@/types/openai'; | import { OpenAIModel } from '@/types/openai'; | ||||||
|  | 
 | ||||||
|  | import { OPENAI_API_HOST } from '../app/const'; | ||||||
|  | 
 | ||||||
| import { | import { | ||||||
|   createParser, |  | ||||||
|   ParsedEvent, |   ParsedEvent, | ||||||
|   ReconnectInterval, |   ReconnectInterval, | ||||||
|  |   createParser, | ||||||
| } from 'eventsource-parser'; | } from 'eventsource-parser'; | ||||||
| import { OPENAI_API_HOST } from '../app/const'; |  | ||||||
| 
 | 
 | ||||||
| export class OpenAIError extends Error { | export class OpenAIError extends Error { | ||||||
|   type: string; |   type: string; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { defineConfig } from 'vite'; |  | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
|  | import { defineConfig } from 'vite'; | ||||||
| 
 | 
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   resolve: { |   resolve: { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue