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/public ./public
|
||||
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 3000
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types";
|
||||
import { FC, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { ChatLoader } from "./ChatLoader";
|
||||
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 }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||
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">
|
||||
{!(apiKey || serverSideApiKeyIsSet) ? (
|
||||
<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-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-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">{t('Please set your OpenAI API key in the bottom left of the sidebar.')}</div>
|
||||
</div>
|
||||
) : modelError ? (
|
||||
<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">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('Error fetching models.')}</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">{t('If you completed this step, OpenAI may be experiencing issues.')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
@ -89,7 +91,7 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, serverSideApiKey
|
|||
{conversation.messages.length === 0 ? (
|
||||
<>
|
||||
<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 && (
|
||||
<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) => (
|
||||
<ChatMessage
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Message, OpenAIModel, OpenAIModelID } from "@/types";
|
||||
import { IconPlayerStop, IconRepeat, IconSend } from "@tabler/icons-react";
|
||||
import { FC, KeyboardEvent, MutableRefObject, useEffect, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
messageIsStreaming: boolean;
|
||||
|
@ -13,6 +14,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSend, onRegenerate, stopConversationRef, textareaRef }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [content, setContent] = useState<string>();
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -34,7 +36,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe
|
|||
}
|
||||
|
||||
if (!content) {
|
||||
alert("Please enter a message");
|
||||
alert(t("Please enter a message"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -88,7 +90,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe
|
|||
size={16}
|
||||
className="inline-block mb-[2px]"
|
||||
/>{" "}
|
||||
Stop Generating
|
||||
{t('Stop Generating')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
@ -115,7 +117,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe
|
|||
maxHeight: "400px",
|
||||
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}`
|
||||
}}
|
||||
placeholder="Type a message..."
|
||||
placeholder={t("Type a message...") || ''}
|
||||
value={content}
|
||||
rows={1}
|
||||
onCompositionStart={() => setIsTyping(true)}
|
||||
|
@ -144,7 +146,7 @@ export const ChatInput: FC<Props> = ({ messageIsStreaming, model, messages, onSe
|
|||
>
|
||||
ChatBot UI
|
||||
</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>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Message } from "@/types";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeBlock } from "../Markdown/CodeBlock";
|
||||
|
@ -13,6 +14,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||
const [messageContent, setMessageContent] = useState(message.content);
|
||||
|
@ -60,7 +62,7 @@ export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEdi
|
|||
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="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">
|
||||
{message.role === "user" ? (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { OpenAIModel } from "@/types";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
model: OpenAIModel;
|
||||
|
@ -8,12 +9,13 @@ interface Props {
|
|||
}
|
||||
|
||||
export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => {
|
||||
const {t} = useTranslation('chat')
|
||||
return (
|
||||
<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
|
||||
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}
|
||||
onChange={(e) => {
|
||||
onModelChange(models.find((model) => model.id === e.target.value) as OpenAIModel);
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import { IconRefresh } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
onRegenerate: () => void;
|
||||
}
|
||||
|
||||
export const Regenerate: FC<Props> = ({ onRegenerate }) => {
|
||||
const { t } = useTranslation('chat')
|
||||
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="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
|
||||
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}
|
||||
>
|
||||
<IconRefresh className="mr-2" />
|
||||
<div>Regenerate response</div>
|
||||
<div>{t('Regenerate response')}</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Conversation } from "@/types";
|
||||
import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
conversation: Conversation;
|
||||
|
@ -8,6 +9,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||
const { t } = useTranslation('chat')
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
@ -17,7 +19,7 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
|||
const maxLength = 4000;
|
||||
|
||||
if (value.length > maxLength) {
|
||||
alert(`Prompt limit is ${maxLength} characters`);
|
||||
alert(t(`Prompt limit is {{maxLength}} characters`, { maxLength }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -45,7 +47,7 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
|||
|
||||
return (
|
||||
<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
|
||||
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"
|
||||
|
@ -55,8 +57,8 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
|||
maxHeight: "300px",
|
||||
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}`
|
||||
}}
|
||||
placeholder="Enter a prompt"
|
||||
value={value}
|
||||
placeholder={t("Enter a prompt") || ''}
|
||||
value={t(value) || ''}
|
||||
rows={1}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { generateRandomString, programmingLanguages } from "@/utils/app/codeblock";
|
||||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import { FC, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
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 }) => {
|
||||
const { t } = useTranslation('markdown');
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
|
@ -29,7 +31,7 @@ export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => {
|
|||
const downloadAsFile = () => {
|
||||
const fileExtension = programmingLanguages[language] || ".file";
|
||||
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) {
|
||||
// user pressed cancel on prompt
|
||||
|
@ -57,7 +59,7 @@ export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => {
|
|||
onClick={copyToClipboard}
|
||||
>
|
||||
{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
|
||||
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 { FC, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { SidebarButton } from "./SidebarButton";
|
||||
|
||||
interface Props {
|
||||
|
@ -9,6 +10,8 @@ interface Props {
|
|||
export const ClearConversations: FC<Props> = ({ onClearConversations }) => {
|
||||
const [isConfirming, setIsConfirming] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation('sidebar')
|
||||
|
||||
const handleClearConversations = () => {
|
||||
onClearConversations();
|
||||
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">
|
||||
<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]">
|
||||
<IconCheck
|
||||
|
@ -42,7 +45,7 @@ export const ClearConversations: FC<Props> = ({ onClearConversations }) => {
|
|||
</div>
|
||||
) : (
|
||||
<SidebarButton
|
||||
text="Clear conversations"
|
||||
text={t("Clear conversations")}
|
||||
icon={<IconTrash size={16} />}
|
||||
onClick={() => setIsConfirming(true)}
|
||||
/>
|
||||
|
|
|
@ -3,12 +3,14 @@ import { cleanConversationHistory } from "@/utils/app/clean";
|
|||
import { IconFileImport } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { SidebarButton } from "./SidebarButton";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
onImport: (data: { conversations: Conversation[]; folders: ChatFolder[] }) => void;
|
||||
}
|
||||
|
||||
export const Import: FC<Props> = ({ onImport }) => {
|
||||
const { t} = useTranslation('sidebar')
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
|
@ -36,7 +38,7 @@ export const Import: FC<Props> = ({ onImport }) => {
|
|||
/>
|
||||
|
||||
<SidebarButton
|
||||
text="Import conversations"
|
||||
text={t("Import conversations")}
|
||||
icon={<IconFileImport size={16} />}
|
||||
onClick={() => {
|
||||
const importFile = document.querySelector("#import-file") as HTMLInputElement;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { IconCheck, IconKey, IconX } from "@tabler/icons-react";
|
||||
import { FC, KeyboardEvent, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { SidebarButton } from "./SidebarButton";
|
||||
|
||||
interface Props {
|
||||
|
@ -8,6 +9,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
||||
const { t } = useTranslation('sidebar');
|
||||
const [isChanging, setIsChanging] = useState(false);
|
||||
const [newKey, setNewKey] = useState(apiKey);
|
||||
|
||||
|
@ -58,7 +60,7 @@ export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
|||
</div>
|
||||
) : (
|
||||
<SidebarButton
|
||||
text="OpenAI API Key"
|
||||
text={t("OpenAI API Key")}
|
||||
icon={<IconKey size={16} />}
|
||||
onClick={() => setIsChanging(true)}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { IconX } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface Props {
|
||||
searchTerm: string;
|
||||
|
@ -7,6 +8,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export const Search: FC<Props> = ({ searchTerm, onSearch }) => {
|
||||
const { t } = useTranslation('sidebar');
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onSearch(e.target.value);
|
||||
};
|
||||
|
@ -20,7 +23,7 @@ export const Search: FC<Props> = ({ searchTerm, onSearch }) => {
|
|||
<input
|
||||
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"
|
||||
placeholder="Search conversations..."
|
||||
placeholder={t('Search conversations...') || ''}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ChatFolder, Conversation, KeyValuePair } from "@/types";
|
||||
import { IconArrowBarLeft, IconFolderPlus, IconPlus } from "@tabler/icons-react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Conversations } from "./Conversations";
|
||||
import { Folders } from "./Folders";
|
||||
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 }) => {
|
||||
|
||||
const { t } = useTranslation('sidebar');
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations);
|
||||
|
||||
|
@ -87,12 +90,12 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
|||
}}
|
||||
>
|
||||
<IconPlus size={16} />
|
||||
New chat
|
||||
{t('New chat')}
|
||||
</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"
|
||||
onClick={() => onCreateFolder("New folder")}
|
||||
onClick={() => onCreateFolder(t("New folder"))}
|
||||
>
|
||||
<IconFolderPlus size={16} />
|
||||
</button>
|
||||
|
@ -148,7 +151,7 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
|||
</div>
|
||||
) : (
|
||||
<div className="mt-4 text-white text-center">
|
||||
<div>No conversations.</div>
|
||||
<div>{t('No conversations.')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ChatFolder, Conversation } from "@/types";
|
||||
import { IconFileExport, IconMoon, IconSun } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { ClearConversations } from "./ClearConversations";
|
||||
import { Import } from "./Import";
|
||||
import { Key } from "./Key";
|
||||
|
@ -17,6 +18,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => {
|
||||
const { t} = useTranslation('sidebar')
|
||||
return (
|
||||
<div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1">
|
||||
<ClearConversations onClearConversations={onClearConversations} />
|
||||
|
@ -24,13 +26,13 @@ export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMod
|
|||
<Import onImport={onImportConversations} />
|
||||
|
||||
<SidebarButton
|
||||
text="Export conversations"
|
||||
text={t("Export conversations")}
|
||||
icon={<IconFileExport size={16} />}
|
||||
onClick={() => onExportConversations()}
|
||||
/>
|
||||
|
||||
<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} />}
|
||||
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} */
|
||||
const nextConfig = {
|
||||
i18n,
|
||||
reactStrictMode: true,
|
||||
|
||||
webpack(config, { isServer, dev }) {
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"i18next": "^22.4.13",
|
||||
"next": "13.2.4",
|
||||
"next-i18next": "^13.2.2",
|
||||
"openai": "^3.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
@ -472,6 +475,15 @@
|
|||
"@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": {
|
||||
"version": "0.0.29",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -2467,6 +2485,35 @@
|
|||
"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": {
|
||||
"version": "5.2.4",
|
||||
"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": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
|
@ -4556,6 +4624,27 @@
|
|||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -5422,6 +5511,14 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
@ -5802,6 +5899,15 @@
|
|||
"@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": {
|
||||
"version": "0.0.29",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "7.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.2.4",
|
||||
"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": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
|
||||
|
@ -8534,6 +8686,15 @@
|
|||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -9134,6 +9295,11 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"i18next": "^22.4.13",
|
||||
"next": "13.2.4",
|
||||
"next-i18next": "^13.2.2",
|
||||
"openai": "^3.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import "@/styles/globals.css";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps<{}>) {
|
||||
function App({ Component, pageProps }: AppProps<{}>) {
|
||||
return (
|
||||
<main className={inter.className}>
|
||||
<Component {...pageProps} />
|
||||
</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 (
|
||||
<Html lang="en">
|
||||
<Html lang={currentLocale}>
|
||||
<Head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
<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 Head from "next/head";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
interface HomeProps {
|
||||
serverSideApiKeyIsSet: boolean;
|
||||
}
|
||||
|
||||
const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||
const { t } = useTranslation('chat')
|
||||
const [folders, setFolders] = useState<ChatFolder[]>([]);
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation>();
|
||||
|
@ -282,7 +285,7 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
|||
|
||||
const newConversation: Conversation = {
|
||||
id: lastConversation ? lastConversation.id + 1 : 1,
|
||||
name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`,
|
||||
name: `${t('Conversation')} ${lastConversation ? lastConversation.id + 1 : 1}`,
|
||||
messages: [],
|
||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
|
@ -532,10 +535,16 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
|||
};
|
||||
export default Home;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
return {
|
||||
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