feat: Add i18n support for Chinese language (#142)
* feat: Add i18n support for Chinese language * fix: locale not working in Docker environment
This commit is contained in:
		
							parent
							
								
									932853f1ba
								
							
						
					
					
						commit
						92eab6c634
					
				|  | @ -19,6 +19,8 @@ COPY --from=dependencies /app/node_modules ./node_modules | ||||||
| COPY --from=build /app/.next ./.next | COPY --from=build /app/.next ./.next | ||||||
| COPY --from=build /app/public ./public | COPY --from=build /app/public ./public | ||||||
| COPY --from=build /app/package*.json ./ | COPY --from=build /app/package*.json ./ | ||||||
|  | COPY --from=build /app/next.config.js ./next.config.js | ||||||
|  | COPY --from=build /app/next-i18next.config.js ./next-i18next.config.js | ||||||
| 
 | 
 | ||||||
| # Expose the port the app will run on | # Expose the port the app will run on | ||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types"; | import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types"; | ||||||
| import { FC, MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; | import { FC, MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { ChatInput } from "./ChatInput"; | import { ChatInput } from "./ChatInput"; | ||||||
| import { ChatLoader } from "./ChatLoader"; | import { ChatLoader } from "./ChatLoader"; | ||||||
| import { ChatMessage } from "./ChatMessage"; | import { ChatMessage } from "./ChatMessage"; | ||||||
|  | @ -23,6 +24,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKeyIsSet, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onEditMessage, stopConversationRef }) => { | export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKeyIsSet, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onEditMessage, stopConversationRef }) => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|   const [currentMessage, setCurrentMessage] = useState<Message>(); |   const [currentMessage, setCurrentMessage] = useState<Message>(); | ||||||
|   const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); |   const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); | ||||||
| 
 | 
 | ||||||
|  | @ -71,14 +73,14 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKey | ||||||
|     <div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white"> |     <div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white"> | ||||||
|       {!(apiKey || serverSideApiKeyIsSet) ? ( |       {!(apiKey || serverSideApiKeyIsSet) ? ( | ||||||
|         <div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6"> |         <div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6"> | ||||||
|           <div className="text-2xl font-semibold text-center text-gray-800 dark:text-gray-100">OpenAI API Key Required</div> |           <div className="text-2xl font-semibold text-center text-gray-800 dark:text-gray-100">{t('OpenAI API Key Required')}</div> | ||||||
|           <div className="text-center text-gray-500 dark:text-gray-400">Please set your OpenAI API key in the bottom left of the sidebar.</div> |           <div className="text-center text-gray-500 dark:text-gray-400">{t('Please set your OpenAI API key in the bottom left of the sidebar.')}</div> | ||||||
|         </div> |         </div> | ||||||
|       ) : modelError ? ( |       ) : modelError ? ( | ||||||
|         <div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6"> |         <div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6"> | ||||||
|           <div className="text-center text-red-500">Error fetching models.</div> |           <div className="text-center text-red-500">{t('Error fetching models.')}</div> | ||||||
|           <div className="text-center text-red-500">Make sure your OpenAI API key is set in the bottom left of the sidebar.</div> |           <div className="text-center text-red-500">{t('Make sure your OpenAI API key is set in the bottom left of the sidebar.')}</div> | ||||||
|           <div className="text-center text-red-500">If you completed this step, OpenAI may be experiencing issues.</div> |           <div className="text-center text-red-500">{t('If you completed this step, OpenAI may be experiencing issues.')}</div> | ||||||
|         </div> |         </div> | ||||||
|       ) : ( |       ) : ( | ||||||
|         <> |         <> | ||||||
|  | @ -89,7 +91,7 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKey | ||||||
|             {conversation.messages.length === 0 ? ( |             {conversation.messages.length === 0 ? ( | ||||||
|               <> |               <> | ||||||
|                 <div className="flex flex-col mx-auto pt-12 space-y-10 w-[350px] sm:w-[600px]"> |                 <div className="flex flex-col mx-auto pt-12 space-y-10 w-[350px] sm:w-[600px]"> | ||||||
|                   <div className="text-4xl font-semibold text-center text-gray-800 dark:text-gray-100">{models.length === 0 ? "Loading..." : "Chatbot UI"}</div> |                   <div className="text-4xl font-semibold text-center text-gray-800 dark:text-gray-100">{models.length === 0 ? t("Loading...") : "Chatbot UI"}</div> | ||||||
| 
 | 
 | ||||||
|                   {models.length > 0 && ( |                   {models.length > 0 && ( | ||||||
|                     <div className="flex flex-col h-full space-y-4 border p-4 rounded border-neutral-500"> |                     <div className="flex flex-col h-full space-y-4 border p-4 rounded border-neutral-500"> | ||||||
|  | @ -109,7 +111,7 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKey | ||||||
|               </> |               </> | ||||||
|             ) : ( |             ) : ( | ||||||
|               <> |               <> | ||||||
|                 <div className="flex justify-center py-2 text-neutral-500 bg-neutral-100 dark:bg-[#444654] dark:text-neutral-200 text-sm border border-b-neutral-300 dark:border-none">Model: {conversation.model.name}</div> |                 <div className="flex justify-center py-2 text-neutral-500 bg-neutral-100 dark:bg-[#444654] dark:text-neutral-200 text-sm border border-b-neutral-300 dark:border-none">{t('Model')}: {conversation.model.name}</div> | ||||||
| 
 | 
 | ||||||
|                 {conversation.messages.map((message, index) => ( |                 {conversation.messages.map((message, index) => ( | ||||||
|                   <ChatMessage |                   <ChatMessage | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { Message, OpenAIModel, OpenAIModelID } from "@/types"; | import { Message, OpenAIModel, OpenAIModelID } from "@/types"; | ||||||
| import { IconPlayerStop, IconRepeat, IconSend } from "@tabler/icons-react"; | import { IconPlayerStop, IconRepeat, IconSend } from "@tabler/icons-react"; | ||||||
| import { FC, KeyboardEvent, MutableRefObject, useEffect, useState } from "react"; | import { FC, KeyboardEvent, MutableRefObject, useEffect, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   messageIsStreaming: boolean; |   messageIsStreaming: boolean; | ||||||
|  | @ -13,6 +14,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSend, onRegenerate, stopConversationRef, textareaRef }) => { | export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSend, onRegenerate, stopConversationRef, textareaRef }) => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|   const [content, setContent] = useState<string>(); |   const [content, setContent] = useState<string>(); | ||||||
|   const [isTyping, setIsTyping] = useState<boolean>(false); |   const [isTyping, setIsTyping] = useState<boolean>(false); | ||||||
| 
 | 
 | ||||||
|  | @ -21,7 +23,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe | ||||||
|     const maxLength = model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000; |     const maxLength = model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000; | ||||||
| 
 | 
 | ||||||
|     if (value.length > maxLength) { |     if (value.length > maxLength) { | ||||||
|       alert(`Message limit is ${maxLength} characters. You have entered ${value.length} characters.`); |       alert(t(`Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, { maxLength, valueLength: value.length })); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +36,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!content) { |     if (!content) { | ||||||
|       alert("Please enter a message"); |       alert(t("Please enter a message")); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -88,7 +90,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe | ||||||
|               size={16} |               size={16} | ||||||
|               className="inline-block mb-[2px]" |               className="inline-block mb-[2px]" | ||||||
|             />{" "} |             />{" "} | ||||||
|             Stop Generating |             {t('Stop Generating')} | ||||||
|           </button> |           </button> | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|  | @ -115,7 +117,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe | ||||||
|               maxHeight: "400px", |               maxHeight: "400px", | ||||||
|               overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}` |               overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}` | ||||||
|             }} |             }} | ||||||
|             placeholder="Type a message..." |             placeholder={t("Type a message...") || ''} | ||||||
|             value={content} |             value={content} | ||||||
|             rows={1} |             rows={1} | ||||||
|             onCompositionStart={() => setIsTyping(true)} |             onCompositionStart={() => setIsTyping(true)} | ||||||
|  | @ -144,7 +146,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe | ||||||
|         > |         > | ||||||
|           ChatBot UI |           ChatBot UI | ||||||
|         </a> |         </a> | ||||||
|         . Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality. |         . {t("Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.")} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { Message } from "@/types"; | import { Message } from "@/types"; | ||||||
| import { IconEdit } from "@tabler/icons-react"; | import { IconEdit } from "@tabler/icons-react"; | ||||||
| import { FC, useEffect, useRef, useState } from "react"; | import { FC, useEffect, useRef, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import ReactMarkdown from "react-markdown"; | import ReactMarkdown from "react-markdown"; | ||||||
| import remarkGfm from "remark-gfm"; | import remarkGfm from "remark-gfm"; | ||||||
| import { CodeBlock } from "../Markdown/CodeBlock"; | import { CodeBlock } from "../Markdown/CodeBlock"; | ||||||
|  | @ -13,6 +14,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage }) => { | export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage }) => { | ||||||
|  |   const { t } = useTranslation('chat'); | ||||||
|   const [isEditing, setIsEditing] = useState<boolean>(false); |   const [isEditing, setIsEditing] = useState<boolean>(false); | ||||||
|   const [isHovering, setIsHovering] = useState<boolean>(false); |   const [isHovering, setIsHovering] = useState<boolean>(false); | ||||||
|   const [messageContent, setMessageContent] = useState(message.content); |   const [messageContent, setMessageContent] = useState(message.content); | ||||||
|  | @ -60,7 +62,7 @@ export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEdi | ||||||
|       onMouseLeave={() => setIsHovering(false)} |       onMouseLeave={() => setIsHovering(false)} | ||||||
|     > |     > | ||||||
|       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto relative"> |       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto relative"> | ||||||
|         <div className="font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div> |         <div className="font-bold min-w-[40px]">{message.role === "assistant" ? t("AI") : t("You")}:</div> | ||||||
| 
 | 
 | ||||||
|         <div className="prose dark:prose-invert mt-[-2px] w-full"> |         <div className="prose dark:prose-invert mt-[-2px] w-full"> | ||||||
|           {message.role === "user" ? ( |           {message.role === "user" ? ( | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { OpenAIModel } from "@/types"; | import { OpenAIModel } from "@/types"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   model: OpenAIModel; |   model: OpenAIModel; | ||||||
|  | @ -8,12 +9,13 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => { | export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => { | ||||||
|  |   const {t} = useTranslation('chat') | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col"> |     <div className="flex flex-col"> | ||||||
|       <label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label> |       <label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">{t('Model')}</label> | ||||||
|       <select |       <select | ||||||
|         className="w-full p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900 cursor-pointer" |         className="w-full p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900 cursor-pointer" | ||||||
|         placeholder="Select a model" |         placeholder={t("Select a model") || ''} | ||||||
|         value={model.id} |         value={model.id} | ||||||
|         onChange={(e) => { |         onChange={(e) => { | ||||||
|           onModelChange(models.find((model) => model.id === e.target.value) as OpenAIModel); |           onModelChange(models.find((model) => model.id === e.target.value) as OpenAIModel); | ||||||
|  |  | ||||||
|  | @ -1,20 +1,22 @@ | ||||||
| import { IconRefresh } from "@tabler/icons-react"; | import { IconRefresh } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   onRegenerate: () => void; |   onRegenerate: () => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Regenerate: FC<Props> = ({ onRegenerate }) => { | export const Regenerate: FC<Props> = ({ onRegenerate }) => { | ||||||
|  |   const { t }  = useTranslation('chat') | ||||||
|   return ( |   return ( | ||||||
|     <div className="fixed sm:absolute bottom-4 sm:bottom-8 w-full sm:w-1/2 px-2 left-0 sm:left-[280px] lg:left-[200px] right-0 ml-auto mr-auto"> |     <div className="fixed sm:absolute bottom-4 sm:bottom-8 w-full sm:w-1/2 px-2 left-0 sm:left-[280px] lg:left-[200px] right-0 ml-auto mr-auto"> | ||||||
|       <div className="text-center mb-4 text-red-500">Sorry, there was an error.</div> |       <div className="text-center mb-4 text-red-500">{t('Sorry, there was an error.')}</div> | ||||||
|       <button |       <button | ||||||
|         className="flex items-center justify-center w-full h-12 bg-neutral-100 dark:bg-[#444654] text-neutral-500 dark:text-neutral-200 text-sm font-semibold rounded-lg border border-b-neutral-300 dark:border-none" |         className="flex items-center justify-center w-full h-12 bg-neutral-100 dark:bg-[#444654] text-neutral-500 dark:text-neutral-200 text-sm font-semibold rounded-lg border border-b-neutral-300 dark:border-none" | ||||||
|         onClick={onRegenerate} |         onClick={onRegenerate} | ||||||
|       > |       > | ||||||
|         <IconRefresh className="mr-2" /> |         <IconRefresh className="mr-2" /> | ||||||
|         <div>Regenerate response</div> |         <div>{t('Regenerate response')}</div> | ||||||
|       </button> |       </button> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { Conversation } from "@/types"; | import { Conversation } from "@/types"; | ||||||
| import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const"; | import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const"; | ||||||
| import { FC, useEffect, useRef, useState } from "react"; | import { FC, useEffect, useRef, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   conversation: Conversation; |   conversation: Conversation; | ||||||
|  | @ -8,6 +9,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => { | export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => { | ||||||
|  |   const { t } = useTranslation('chat') | ||||||
|   const [value, setValue] = useState<string>(""); |   const [value, setValue] = useState<string>(""); | ||||||
| 
 | 
 | ||||||
|   const textareaRef = useRef<HTMLTextAreaElement>(null); |   const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
|  | @ -17,7 +19,7 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => { | ||||||
|     const maxLength = 4000; |     const maxLength = 4000; | ||||||
| 
 | 
 | ||||||
|     if (value.length > maxLength) { |     if (value.length > maxLength) { | ||||||
|       alert(`Prompt limit is ${maxLength} characters`); |       alert(t(`Prompt limit is {{maxLength}} characters`, { maxLength })); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -45,7 +47,7 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => { | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col"> |     <div className="flex flex-col"> | ||||||
|       <label className="text-left dark:text-neutral-400 text-neutral-700 mb-2">System Prompt</label> |       <label className="text-left dark:text-neutral-400 text-neutral-700 mb-2">{t('System Prompt')}</label> | ||||||
|       <textarea |       <textarea | ||||||
|         ref={textareaRef} |         ref={textareaRef} | ||||||
|         className="w-full rounded-lg px-4 py-2 focus:outline-none dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-500 shadow text-neutral-900" |         className="w-full rounded-lg px-4 py-2 focus:outline-none dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-500 shadow text-neutral-900" | ||||||
|  | @ -55,8 +57,8 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => { | ||||||
|           maxHeight: "300px", |           maxHeight: "300px", | ||||||
|           overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}` |           overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}` | ||||||
|         }} |         }} | ||||||
|         placeholder="Enter a prompt" |         placeholder={t("Enter a prompt") || ''} | ||||||
|         value={value} |         value={t(value) || ''} | ||||||
|         rows={1} |         rows={1} | ||||||
|         onChange={handleChange} |         onChange={handleChange} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { generateRandomString, programmingLanguages } from "@/utils/app/codeblock"; | import { generateRandomString, programmingLanguages } from "@/utils/app/codeblock"; | ||||||
| import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react"; | import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react"; | ||||||
| import { FC, useState } from "react"; | import { FC, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; | ||||||
| import { oneDark, oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism"; | import { oneDark, oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism"; | ||||||
| 
 | 
 | ||||||
|  | @ -11,6 +12,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => { | export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => { | ||||||
|  |   const { t } = useTranslation('markdown'); | ||||||
|   const [isCopied, setIsCopied] = useState<Boolean>(false); |   const [isCopied, setIsCopied] = useState<Boolean>(false); | ||||||
| 
 | 
 | ||||||
|   const copyToClipboard = () => { |   const copyToClipboard = () => { | ||||||
|  | @ -29,7 +31,7 @@ export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => { | ||||||
|   const downloadAsFile = () => { |   const downloadAsFile = () => { | ||||||
|     const fileExtension = programmingLanguages[language] || ".file"; |     const fileExtension = programmingLanguages[language] || ".file"; | ||||||
|     const suggestedFileName = `file-${generateRandomString(3, true)}${fileExtension}`; |     const suggestedFileName = `file-${generateRandomString(3, true)}${fileExtension}`; | ||||||
|     const fileName = window.prompt("Enter file name", suggestedFileName); |     const fileName = window.prompt(t("Enter file name") || '', suggestedFileName); | ||||||
| 
 | 
 | ||||||
|     if (!fileName) { |     if (!fileName) { | ||||||
|       // user pressed cancel on prompt
 |       // user pressed cancel on prompt
 | ||||||
|  | @ -57,7 +59,7 @@ export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => { | ||||||
|             onClick={copyToClipboard} |             onClick={copyToClipboard} | ||||||
|           > |           > | ||||||
|             {isCopied ? <IconCheck size={18} className="mr-1.5"/> : <IconClipboard size={18} className="mr-1.5"/>} |             {isCopied ? <IconCheck size={18} className="mr-1.5"/> : <IconClipboard size={18} className="mr-1.5"/>} | ||||||
|             {isCopied ? "Copied!" : "Copy code"} |             {isCopied ? t("Copied!") : t("Copy code")} | ||||||
|           </button> |           </button> | ||||||
|           <button |           <button | ||||||
|             className="text-white bg-none py-0.5 pl-2 rounded focus:outline-none text-xs flex items-center" |             className="text-white bg-none py-0.5 pl-2 rounded focus:outline-none text-xs flex items-center" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { IconCheck, IconTrash, IconX } from "@tabler/icons-react"; | import { IconCheck, IconTrash, IconX } from "@tabler/icons-react"; | ||||||
| import { FC, useState } from "react"; | import { FC, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { SidebarButton } from "./SidebarButton"; | import { SidebarButton } from "./SidebarButton"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  | @ -9,6 +10,8 @@ interface Props { | ||||||
| export const ClearConversations: FC<Props> = ({ onClearConversations }) => { | export const ClearConversations: FC<Props> = ({ onClearConversations }) => { | ||||||
|   const [isConfirming, setIsConfirming] = useState<boolean>(false); |   const [isConfirming, setIsConfirming] = useState<boolean>(false); | ||||||
| 
 | 
 | ||||||
|  |   const { t } = useTranslation('sidebar') | ||||||
|  | 
 | ||||||
|   const handleClearConversations = () => { |   const handleClearConversations = () => { | ||||||
|     onClearConversations(); |     onClearConversations(); | ||||||
|     setIsConfirming(false); |     setIsConfirming(false); | ||||||
|  | @ -18,7 +21,7 @@ export const ClearConversations: FC<Props> = ({ onClearConversations }) => { | ||||||
|     <div className="flex hover:bg-[#343541] py-3 px-3 rounded-md cursor-pointer w-full items-center"> |     <div className="flex hover:bg-[#343541] py-3 px-3 rounded-md cursor-pointer w-full items-center"> | ||||||
|       <IconTrash size={16} /> |       <IconTrash size={16} /> | ||||||
| 
 | 
 | ||||||
|       <div className="ml-3 flex-1 text-left text-white">Are you sure?</div> |       <div className="ml-3 flex-1 text-left text-white">{t('Are you sure?')}</div> | ||||||
| 
 | 
 | ||||||
|       <div className="flex w-[40px]"> |       <div className="flex w-[40px]"> | ||||||
|         <IconCheck |         <IconCheck | ||||||
|  | @ -42,7 +45,7 @@ export const ClearConversations: FC<Props> = ({ onClearConversations }) => { | ||||||
|     </div> |     </div> | ||||||
|   ) : ( |   ) : ( | ||||||
|     <SidebarButton |     <SidebarButton | ||||||
|       text="Clear conversations" |       text={t("Clear conversations")} | ||||||
|       icon={<IconTrash size={16} />} |       icon={<IconTrash size={16} />} | ||||||
|       onClick={() => setIsConfirming(true)} |       onClick={() => setIsConfirming(true)} | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|  | @ -3,12 +3,14 @@ import { cleanConversationHistory } from "@/utils/app/clean"; | ||||||
| import { IconFileImport } from "@tabler/icons-react"; | import { IconFileImport } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
| import { SidebarButton } from "./SidebarButton"; | import { SidebarButton } from "./SidebarButton"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   onImport: (data: { conversations: Conversation[]; folders: ChatFolder[] }) => void; |   onImport: (data: { conversations: Conversation[]; folders: ChatFolder[] }) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Import: FC<Props> = ({ onImport }) => { | export const Import: FC<Props> = ({ onImport }) => { | ||||||
|  |   const { t}  = useTranslation('sidebar') | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <input |       <input | ||||||
|  | @ -36,7 +38,7 @@ export const Import: FC<Props> = ({ onImport }) => { | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <SidebarButton |       <SidebarButton | ||||||
|         text="Import conversations" |         text={t("Import conversations")} | ||||||
|         icon={<IconFileImport size={16} />} |         icon={<IconFileImport size={16} />} | ||||||
|         onClick={() => { |         onClick={() => { | ||||||
|           const importFile = document.querySelector("#import-file") as HTMLInputElement; |           const importFile = document.querySelector("#import-file") as HTMLInputElement; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { IconCheck, IconKey, IconX } from "@tabler/icons-react"; | import { IconCheck, IconKey, IconX } from "@tabler/icons-react"; | ||||||
| import { FC, KeyboardEvent, useState } from "react"; | import { FC, KeyboardEvent, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { SidebarButton } from "./SidebarButton"; | import { SidebarButton } from "./SidebarButton"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  | @ -8,6 +9,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => { | export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => { | ||||||
|  |   const { t } = useTranslation('sidebar'); | ||||||
|   const [isChanging, setIsChanging] = useState(false); |   const [isChanging, setIsChanging] = useState(false); | ||||||
|   const [newKey, setNewKey] = useState(apiKey); |   const [newKey, setNewKey] = useState(apiKey); | ||||||
| 
 | 
 | ||||||
|  | @ -58,7 +60,7 @@ export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => { | ||||||
|     </div> |     </div> | ||||||
|   ) : ( |   ) : ( | ||||||
|     <SidebarButton |     <SidebarButton | ||||||
|       text="OpenAI API Key" |       text={t("OpenAI API Key")} | ||||||
|       icon={<IconKey size={16} />} |       icon={<IconKey size={16} />} | ||||||
|       onClick={() => setIsChanging(true)} |       onClick={() => setIsChanging(true)} | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { IconX } from "@tabler/icons-react"; | import { IconX } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   searchTerm: string; |   searchTerm: string; | ||||||
|  | @ -7,6 +8,8 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Search: FC<Props> = ({ searchTerm, onSearch }) => { | export const Search: FC<Props> = ({ searchTerm, onSearch }) => { | ||||||
|  |   const { t } = useTranslation('sidebar'); | ||||||
|  | 
 | ||||||
|   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { |   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     onSearch(e.target.value); |     onSearch(e.target.value); | ||||||
|   }; |   }; | ||||||
|  | @ -20,7 +23,7 @@ export const Search: FC<Props> = ({ searchTerm, onSearch }) => { | ||||||
|       <input |       <input | ||||||
|         className="flex-1 w-full pr-10 bg-[#202123] border border-neutral-600 text-sm rounded-md px-4 py-3 text-white" |         className="flex-1 w-full pr-10 bg-[#202123] border border-neutral-600 text-sm rounded-md px-4 py-3 text-white" | ||||||
|         type="text" |         type="text" | ||||||
|         placeholder="Search conversations..." |         placeholder={t('Search conversations...') || ''} | ||||||
|         value={searchTerm} |         value={searchTerm} | ||||||
|         onChange={handleSearchChange} |         onChange={handleSearchChange} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { ChatFolder, Conversation, KeyValuePair } from "@/types"; | import { ChatFolder, Conversation, KeyValuePair } from "@/types"; | ||||||
| import { IconArrowBarLeft, IconFolderPlus, IconPlus } from "@tabler/icons-react"; | import { IconArrowBarLeft, IconFolderPlus, IconPlus } from "@tabler/icons-react"; | ||||||
| import { FC, useEffect, useState } from "react"; | import { FC, useEffect, useState } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { Conversations } from "./Conversations"; | import { Conversations } from "./Conversations"; | ||||||
| import { Folders } from "./Folders"; | import { Folders } from "./Folders"; | ||||||
| import { Search } from "./Search"; | import { Search } from "./Search"; | ||||||
|  | @ -29,6 +30,8 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, folders, onCreateFolder, onDeleteFolder, onUpdateFolder, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => { | export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, folders, onCreateFolder, onDeleteFolder, onUpdateFolder, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => { | ||||||
|  | 
 | ||||||
|  |   const { t } = useTranslation('sidebar'); | ||||||
|   const [searchTerm, setSearchTerm] = useState<string>(""); |   const [searchTerm, setSearchTerm] = useState<string>(""); | ||||||
|   const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations); |   const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations); | ||||||
| 
 | 
 | ||||||
|  | @ -87,12 +90,12 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|           <IconPlus size={16} /> |           <IconPlus size={16} /> | ||||||
|           New chat |           {t('New chat')} | ||||||
|         </button> |         </button> | ||||||
| 
 | 
 | ||||||
|         <button |         <button | ||||||
|           className="ml-2 flex gap-3 p-3 items-center rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20" |           className="ml-2 flex gap-3 p-3 items-center rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20" | ||||||
|           onClick={() => onCreateFolder("New folder")} |           onClick={() => onCreateFolder(t("New folder"))} | ||||||
|         > |         > | ||||||
|           <IconFolderPlus size={16} /> |           <IconFolderPlus size={16} /> | ||||||
|         </button> |         </button> | ||||||
|  | @ -148,7 +151,7 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected | ||||||
|           </div> |           </div> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <div className="mt-4 text-white text-center"> |           <div className="mt-4 text-white text-center"> | ||||||
|             <div>No conversations.</div> |             <div>{t('No conversations.')}</div> | ||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { ChatFolder, Conversation } from "@/types"; | import { ChatFolder, Conversation } from "@/types"; | ||||||
| import { IconFileExport, IconMoon, IconSun } from "@tabler/icons-react"; | import { IconFileExport, IconMoon, IconSun } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| import { ClearConversations } from "./ClearConversations"; | import { ClearConversations } from "./ClearConversations"; | ||||||
| import { Import } from "./Import"; | import { Import } from "./Import"; | ||||||
| import { Key } from "./Key"; | import { Key } from "./Key"; | ||||||
|  | @ -17,6 +18,7 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => { | export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => { | ||||||
|  |   const { t} = useTranslation('sidebar') | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1"> |     <div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1"> | ||||||
|       <ClearConversations onClearConversations={onClearConversations} /> |       <ClearConversations onClearConversations={onClearConversations} /> | ||||||
|  | @ -24,13 +26,13 @@ export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMod | ||||||
|       <Import onImport={onImportConversations} /> |       <Import onImport={onImportConversations} /> | ||||||
| 
 | 
 | ||||||
|       <SidebarButton |       <SidebarButton | ||||||
|         text="Export conversations" |         text={t("Export conversations")} | ||||||
|         icon={<IconFileExport size={16} />} |         icon={<IconFileExport size={16} />} | ||||||
|         onClick={() => onExportConversations()} |         onClick={() => onExportConversations()} | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <SidebarButton |       <SidebarButton | ||||||
|         text={lightMode === "light" ? "Dark mode" : "Light mode"} |         text={lightMode === "light" ? t("Dark mode") : t("Light mode")} | ||||||
|         icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />} |         icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />} | ||||||
|         onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")} |         onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | const path = require("path"); | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |   i18n: { | ||||||
|  |     defaultLocale: "en", | ||||||
|  |     locales: ["en", "zh"], | ||||||
|  |   }, | ||||||
|  |   localePath: | ||||||
|  |     typeof window === "undefined" | ||||||
|  |       ? require("path").resolve("./public/locales") | ||||||
|  |       : "/public/locales", | ||||||
|  | }; | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
|  | const { i18n } = require('./next-i18next.config') | ||||||
|  | 
 | ||||||
| /** @type {import('next').NextConfig} */ | /** @type {import('next').NextConfig} */ | ||||||
| const nextConfig = { | const nextConfig = { | ||||||
|  |   i18n, | ||||||
|   reactStrictMode: true, |   reactStrictMode: true, | ||||||
| 
 | 
 | ||||||
|   webpack(config, { isServer, dev }) { |   webpack(config, { isServer, dev }) { | ||||||
|  |  | ||||||
|  | @ -16,10 +16,13 @@ | ||||||
|         "eslint": "8.36.0", |         "eslint": "8.36.0", | ||||||
|         "eslint-config-next": "13.2.4", |         "eslint-config-next": "13.2.4", | ||||||
|         "eventsource-parser": "^0.1.0", |         "eventsource-parser": "^0.1.0", | ||||||
|  |         "i18next": "^22.4.13", | ||||||
|         "next": "13.2.4", |         "next": "13.2.4", | ||||||
|  |         "next-i18next": "^13.2.2", | ||||||
|         "openai": "^3.2.1", |         "openai": "^3.2.1", | ||||||
|         "react": "18.2.0", |         "react": "18.2.0", | ||||||
|         "react-dom": "18.2.0", |         "react-dom": "18.2.0", | ||||||
|  |         "react-i18next": "^12.2.0", | ||||||
|         "react-markdown": "^8.0.5", |         "react-markdown": "^8.0.5", | ||||||
|         "react-syntax-highlighter": "^15.5.0", |         "react-syntax-highlighter": "^15.5.0", | ||||||
|         "remark-gfm": "^3.0.1", |         "remark-gfm": "^3.0.1", | ||||||
|  | @ -472,6 +475,15 @@ | ||||||
|         "@types/unist": "*" |         "@types/unist": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@types/hoist-non-react-statics": { | ||||||
|  |       "version": "3.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", | ||||||
|  |       "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "hoist-non-react-statics": "^3.3.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@types/json5": { |     "node_modules/@types/json5": { | ||||||
|       "version": "0.0.29", |       "version": "0.0.29", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", |       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||||
|  | @ -1166,6 +1178,12 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | ||||||
|       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" |       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/core-js": { | ||||||
|  |       "version": "3.29.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", | ||||||
|  |       "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", | ||||||
|  |       "hasInstallScript": true | ||||||
|  |     }, | ||||||
|     "node_modules/cross-spawn": { |     "node_modules/cross-spawn": { | ||||||
|       "version": "7.0.3", |       "version": "7.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", |       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", | ||||||
|  | @ -2467,6 +2485,35 @@ | ||||||
|         "node": "*" |         "node": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/hoist-non-react-statics": { | ||||||
|  |       "version": "3.3.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", | ||||||
|  |       "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "react-is": "^16.7.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/html-parse-stringify": { | ||||||
|  |       "version": "3.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", | ||||||
|  |       "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "void-elements": "3.1.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/i18next": { | ||||||
|  |       "version": "22.4.13", | ||||||
|  |       "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.13.tgz", | ||||||
|  |       "integrity": "sha512-GX7flMHRRqQA0I1yGLmaZ4Hwt1JfLqagk8QPDPZsqekbKtXsuIngSVWM/s3SLgNkrEXjA+0sMGNuOEkkmyqmWg==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/runtime": "^7.20.6" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/i18next-fs-backend": { | ||||||
|  |       "version": "2.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", | ||||||
|  |       "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" | ||||||
|  |     }, | ||||||
|     "node_modules/ignore": { |     "node_modules/ignore": { | ||||||
|       "version": "5.2.4", |       "version": "5.2.4", | ||||||
|       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", |       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", | ||||||
|  | @ -3996,6 +4043,27 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/next-i18next": { | ||||||
|  |       "version": "13.2.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.2.2.tgz", | ||||||
|  |       "integrity": "sha512-t0WU6K+HJoq2nVQ0n6OiiEZja9GyMqtDSU74FmOafgk4ljns+iZ18bsNJiI8rOUXfFfkW96ea1N7D5kbMyT+PA==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/runtime": "^7.20.13", | ||||||
|  |         "@types/hoist-non-react-statics": "^3.3.1", | ||||||
|  |         "core-js": "^3", | ||||||
|  |         "hoist-non-react-statics": "^3.3.2", | ||||||
|  |         "i18next-fs-backend": "^2.1.1" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "i18next": "^22.0.6", | ||||||
|  |         "next": ">= 12.0.0", | ||||||
|  |         "react": ">= 17.0.2", | ||||||
|  |         "react-i18next": "^12.2.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/next/node_modules/postcss": { |     "node_modules/next/node_modules/postcss": { | ||||||
|       "version": "8.4.14", |       "version": "8.4.14", | ||||||
|       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", |       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", | ||||||
|  | @ -4556,6 +4624,27 @@ | ||||||
|         "react": "^18.2.0" |         "react": "^18.2.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/react-i18next": { | ||||||
|  |       "version": "12.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", | ||||||
|  |       "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/runtime": "^7.20.6", | ||||||
|  |         "html-parse-stringify": "^3.0.1" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "i18next": ">= 19.0.0", | ||||||
|  |         "react": ">= 16.8.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "react-dom": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "react-native": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/react-is": { |     "node_modules/react-is": { | ||||||
|       "version": "16.13.1", |       "version": "16.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", |       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||||
|  | @ -5422,6 +5511,14 @@ | ||||||
|         "url": "https://opencollective.com/unified" |         "url": "https://opencollective.com/unified" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/void-elements": { | ||||||
|  |       "version": "3.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", | ||||||
|  |       "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/which": { |     "node_modules/which": { | ||||||
|       "version": "2.0.2", |       "version": "2.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", |       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", | ||||||
|  | @ -5802,6 +5899,15 @@ | ||||||
|         "@types/unist": "*" |         "@types/unist": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "@types/hoist-non-react-statics": { | ||||||
|  |       "version": "3.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", | ||||||
|  |       "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", | ||||||
|  |       "requires": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "hoist-non-react-statics": "^3.3.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "@types/json5": { |     "@types/json5": { | ||||||
|       "version": "0.0.29", |       "version": "0.0.29", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", |       "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", | ||||||
|  | @ -6276,6 +6382,11 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | ||||||
|       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" |       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" | ||||||
|     }, |     }, | ||||||
|  |     "core-js": { | ||||||
|  |       "version": "3.29.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", | ||||||
|  |       "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==" | ||||||
|  |     }, | ||||||
|     "cross-spawn": { |     "cross-spawn": { | ||||||
|       "version": "7.0.3", |       "version": "7.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", |       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", | ||||||
|  | @ -7218,6 +7329,35 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", |       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", | ||||||
|       "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" |       "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" | ||||||
|     }, |     }, | ||||||
|  |     "hoist-non-react-statics": { | ||||||
|  |       "version": "3.3.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", | ||||||
|  |       "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", | ||||||
|  |       "requires": { | ||||||
|  |         "react-is": "^16.7.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "html-parse-stringify": { | ||||||
|  |       "version": "3.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", | ||||||
|  |       "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", | ||||||
|  |       "requires": { | ||||||
|  |         "void-elements": "3.1.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "i18next": { | ||||||
|  |       "version": "22.4.13", | ||||||
|  |       "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.13.tgz", | ||||||
|  |       "integrity": "sha512-GX7flMHRRqQA0I1yGLmaZ4Hwt1JfLqagk8QPDPZsqekbKtXsuIngSVWM/s3SLgNkrEXjA+0sMGNuOEkkmyqmWg==", | ||||||
|  |       "requires": { | ||||||
|  |         "@babel/runtime": "^7.20.6" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "i18next-fs-backend": { | ||||||
|  |       "version": "2.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", | ||||||
|  |       "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" | ||||||
|  |     }, | ||||||
|     "ignore": { |     "ignore": { | ||||||
|       "version": "5.2.4", |       "version": "5.2.4", | ||||||
|       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", |       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", | ||||||
|  | @ -8194,6 +8334,18 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "next-i18next": { | ||||||
|  |       "version": "13.2.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.2.2.tgz", | ||||||
|  |       "integrity": "sha512-t0WU6K+HJoq2nVQ0n6OiiEZja9GyMqtDSU74FmOafgk4ljns+iZ18bsNJiI8rOUXfFfkW96ea1N7D5kbMyT+PA==", | ||||||
|  |       "requires": { | ||||||
|  |         "@babel/runtime": "^7.20.13", | ||||||
|  |         "@types/hoist-non-react-statics": "^3.3.1", | ||||||
|  |         "core-js": "^3", | ||||||
|  |         "hoist-non-react-statics": "^3.3.2", | ||||||
|  |         "i18next-fs-backend": "^2.1.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node-releases": { |     "node-releases": { | ||||||
|       "version": "2.0.10", |       "version": "2.0.10", | ||||||
|       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", |       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", | ||||||
|  | @ -8534,6 +8686,15 @@ | ||||||
|         "scheduler": "^0.23.0" |         "scheduler": "^0.23.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "react-i18next": { | ||||||
|  |       "version": "12.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", | ||||||
|  |       "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==", | ||||||
|  |       "requires": { | ||||||
|  |         "@babel/runtime": "^7.20.6", | ||||||
|  |         "html-parse-stringify": "^3.0.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "react-is": { |     "react-is": { | ||||||
|       "version": "16.13.1", |       "version": "16.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", |       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||||
|  | @ -9134,6 +9295,11 @@ | ||||||
|         "unist-util-stringify-position": "^3.0.0" |         "unist-util-stringify-position": "^3.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "void-elements": { | ||||||
|  |       "version": "3.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", | ||||||
|  |       "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" | ||||||
|  |     }, | ||||||
|     "which": { |     "which": { | ||||||
|       "version": "2.0.2", |       "version": "2.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", |       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", | ||||||
|  |  | ||||||
|  | @ -17,10 +17,13 @@ | ||||||
|     "eslint": "8.36.0", |     "eslint": "8.36.0", | ||||||
|     "eslint-config-next": "13.2.4", |     "eslint-config-next": "13.2.4", | ||||||
|     "eventsource-parser": "^0.1.0", |     "eventsource-parser": "^0.1.0", | ||||||
|  |     "i18next": "^22.4.13", | ||||||
|     "next": "13.2.4", |     "next": "13.2.4", | ||||||
|  |     "next-i18next": "^13.2.2", | ||||||
|     "openai": "^3.2.1", |     "openai": "^3.2.1", | ||||||
|     "react": "18.2.0", |     "react": "18.2.0", | ||||||
|     "react-dom": "18.2.0", |     "react-dom": "18.2.0", | ||||||
|  |     "react-i18next": "^12.2.0", | ||||||
|     "react-markdown": "^8.0.5", |     "react-markdown": "^8.0.5", | ||||||
|     "react-syntax-highlighter": "^15.5.0", |     "react-syntax-highlighter": "^15.5.0", | ||||||
|     "remark-gfm": "^3.0.1", |     "remark-gfm": "^3.0.1", | ||||||
|  |  | ||||||
|  | @ -1,13 +1,16 @@ | ||||||
| import "@/styles/globals.css"; | import "@/styles/globals.css"; | ||||||
|  | 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"; | ||||||
| 
 | 
 | ||||||
| const inter = Inter({ subsets: ["latin"] }); | const inter = Inter({ subsets: ["latin"] }); | ||||||
| 
 | 
 | ||||||
| export default function App({ Component, pageProps }: AppProps<{}>) { | function App({ Component, pageProps }: AppProps<{}>) { | ||||||
|   return ( |   return ( | ||||||
|     <main className={inter.className}> |     <main className={inter.className}> | ||||||
|       <Component {...pageProps} /> |       <Component {...pageProps} /> | ||||||
|     </main> |     </main> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export default appWithTranslation(App); | ||||||
|  | @ -1,8 +1,16 @@ | ||||||
| import { Html, Head, Main, NextScript } from 'next/document' | import { Html, Head, Main, NextScript, DocumentProps } from 'next/document' | ||||||
|  | import i18nextConfig from '../next-i18next.config' | ||||||
| 
 | 
 | ||||||
| export default function Document() { | type Props = DocumentProps & { | ||||||
|  |   // add custom document props
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Document(props: Props) { | ||||||
|  |   const currentLocale = | ||||||
|  |       props.__NEXT_DATA__.locale ?? | ||||||
|  |       i18nextConfig.i18n.defaultLocale | ||||||
|   return ( |   return ( | ||||||
|     <Html lang="en"> |     <Html lang={currentLocale}> | ||||||
|       <Head> |       <Head> | ||||||
|         <meta name="apple-mobile-web-app-capable" content="yes"/> |         <meta name="apple-mobile-web-app-capable" content="yes"/> | ||||||
|         <meta name="apple-mobile-web-app-title" content="Chatbot UI"></meta> |         <meta name="apple-mobile-web-app-title" content="Chatbot UI"></meta> | ||||||
|  |  | ||||||
|  | @ -11,12 +11,15 @@ import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react"; | ||||||
| import { GetServerSideProps } from "next"; | import { GetServerSideProps } from "next"; | ||||||
| import Head from "next/head"; | import Head from "next/head"; | ||||||
| import { useEffect, useRef, useState } from "react"; | import { useEffect, useRef, useState } from "react"; | ||||||
|  | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' | ||||||
|  | import { useTranslation } from "next-i18next"; | ||||||
| 
 | 
 | ||||||
| interface HomeProps { | interface HomeProps { | ||||||
|   serverSideApiKeyIsSet: boolean; |   serverSideApiKeyIsSet: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => { | const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => { | ||||||
|  |   const { t } = useTranslation('chat') | ||||||
|   const [folders, setFolders] = useState<ChatFolder[]>([]); |   const [folders, setFolders] = useState<ChatFolder[]>([]); | ||||||
|   const [conversations, setConversations] = useState<Conversation[]>([]); |   const [conversations, setConversations] = useState<Conversation[]>([]); | ||||||
|   const [selectedConversation, setSelectedConversation] = useState<Conversation>(); |   const [selectedConversation, setSelectedConversation] = useState<Conversation>(); | ||||||
|  | @ -282,7 +285,7 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => { | ||||||
| 
 | 
 | ||||||
|     const newConversation: Conversation = { |     const newConversation: Conversation = { | ||||||
|       id: lastConversation ? lastConversation.id + 1 : 1, |       id: lastConversation ? lastConversation.id + 1 : 1, | ||||||
|       name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`, |       name: `${t('Conversation')} ${lastConversation ? lastConversation.id + 1 : 1}`, | ||||||
|       messages: [], |       messages: [], | ||||||
|       model: OpenAIModels[OpenAIModelID.GPT_3_5], |       model: OpenAIModels[OpenAIModelID.GPT_3_5], | ||||||
|       prompt: DEFAULT_SYSTEM_PROMPT, |       prompt: DEFAULT_SYSTEM_PROMPT, | ||||||
|  | @ -532,10 +535,16 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => { | ||||||
| }; | }; | ||||||
| export default Home; | export default Home; | ||||||
| 
 | 
 | ||||||
| export const getServerSideProps: GetServerSideProps = async () => { | export const getServerSideProps: GetServerSideProps = async ({ locale }) => { | ||||||
|   return { |   return { | ||||||
|     props: { |     props: { | ||||||
|       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY |       serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, | ||||||
|  |       ...(await serverSideTranslations(locale ?? 'en', [ | ||||||
|  |         'common', | ||||||
|  |         'chat', | ||||||
|  |         'sidebar', | ||||||
|  |         'markdown', | ||||||
|  |       ])), | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | {} | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | { | ||||||
|  |   "OpenAI API Key Required": "需要 OpenAI API 密钥", | ||||||
|  |   "Please set your OpenAI API key in the bottom left of the sidebar.": "请在侧边栏左下角设置您的 OpenAI API 密钥。", | ||||||
|  |   "Stop Generating": "停止生成", | ||||||
|  |   "Prompt limit is {{maxLength}} characters": "提示字数限制为 {{maxLength}} 个字符", | ||||||
|  |   "System Prompt": "系统提示", | ||||||
|  |   "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "你是 ChatGPT,一个由 OpenAI 训练的大型语言模型。请仔细遵循用户的指示。使用 Markdown 格式进行回应。", | ||||||
|  |   "Enter a prompt": "输入一个提示", | ||||||
|  |   "Regenerate response": "重新生成回应", | ||||||
|  |   "Sorry, there was an error.": "抱歉,出现了错误。", | ||||||
|  |   "Model": "模型", | ||||||
|  |   "Conversation": "对话", | ||||||
|  |   "OR": "或", | ||||||
|  |   "Loading...": "加载中...", | ||||||
|  |   "Type a message...": "输入一条消息...", | ||||||
|  |   "Error fetching models.": "获取模型时出错。", | ||||||
|  |   "AI": "AI", | ||||||
|  |   "You": "你", | ||||||
|  |   "Make sure your OpenAI API key is set in the bottom left of the sidebar.": "请确保您的 OpenAI API 密钥已在侧边栏左下角设置。", | ||||||
|  |   "If you completed this step, OpenAI may be experiencing issues.": "如果您已完成此步骤,OpenAI 可能遇到了问题。", | ||||||
|  |   "click if using a .env.local file": "如果使用 .env.local 文件,请点击", | ||||||
|  |   "Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "消息字数限制为 {{maxLength}} 个字符。您已输入 {{valueLength}} 个字符。", | ||||||
|  |   "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 是一个高级聊天机器人工具包,旨在模仿 OpenAI 聊天模型的 ChatGPT 界面和功能。" | ||||||
|  | } | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | {} | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | { | ||||||
|  |   "Copy code": "复制代码", | ||||||
|  |   "Copied!": "已复制!", | ||||||
|  |   "Enter file name": "输入文件名" | ||||||
|  | } | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | { | ||||||
|  |   "New folder": "新建文件夹", | ||||||
|  |   "New chat": "新建聊天", | ||||||
|  |   "No conversations.": "无对话。", | ||||||
|  |   "Search conversations...": "搜索对话...", | ||||||
|  |   "OpenAI API Key": "OpenAI API 密钥", | ||||||
|  |   "Import conversations": "导入对话", | ||||||
|  |   "Are you sure?": "确定吗?", | ||||||
|  |   "Clear conversations": "清空对话", | ||||||
|  |   "Export conversations": "导出对话", | ||||||
|  |   "Dark mode": "深色模式", | ||||||
|  |   "Light mode": "浅色模式" | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue