forked from XiaoMo/ChatGPT-Next-Web
feat: support i18n
This commit is contained in:
parent
ce5abac9fb
commit
7cd170b933
@ -23,6 +23,7 @@ import DownloadIcon from "../icons/download.svg";
|
|||||||
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
|
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
|
||||||
import { showModal } from "./ui-lib";
|
import { showModal } from "./ui-lib";
|
||||||
import { copyToClipboard, downloadAs, isIOS } from "../utils";
|
import { copyToClipboard, downloadAs, isIOS } from "../utils";
|
||||||
|
import Locale from '../locales'
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ export function ChatItem(props: {
|
|||||||
>
|
>
|
||||||
<div className={styles["chat-item-title"]}>{props.title}</div>
|
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||||
<div className={styles["chat-item-info"]}>
|
<div className={styles["chat-item-info"]}>
|
||||||
<div className={styles["chat-item-count"]}>{props.count} 条对话</div>
|
<div className={styles["chat-item-count"]}>{Locale.ChatItem.ChatItemCount(props.count)}</div>
|
||||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||||
@ -200,7 +201,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<div className={styles["window-header-title"]}>
|
<div className={styles["window-header-title"]}>
|
||||||
<div className={styles["window-header-main-title"]}>{session.topic}</div>
|
<div className={styles["window-header-main-title"]}>{session.topic}</div>
|
||||||
<div className={styles["window-header-sub-title"]}>
|
<div className={styles["window-header-sub-title"]}>
|
||||||
与 ChatGPT 的 {session.messages.length} 条对话
|
{Locale.Chat.SubTitle(session.messages.length)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
@ -208,7 +209,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<MenuIcon />}
|
icon={<MenuIcon />}
|
||||||
bordered
|
bordered
|
||||||
title="查看消息列表"
|
title={Locale.Chat.Actions.ChatList}
|
||||||
onClick={props?.showSideBar}
|
onClick={props?.showSideBar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -216,7 +217,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<BrainIcon />}
|
icon={<BrainIcon />}
|
||||||
bordered
|
bordered
|
||||||
title="查看压缩后的历史 Prompt"
|
title={Locale.Chat.Actions.CompressedHistory}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showMemoryPrompt(session)
|
showMemoryPrompt(session)
|
||||||
}}
|
}}
|
||||||
@ -226,7 +227,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<ExportIcon />}
|
icon={<ExportIcon />}
|
||||||
bordered
|
bordered
|
||||||
title="导出聊天记录"
|
title={Locale.Chat.Actions.Export}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
exportMessages(session.messages, session.topic)
|
exportMessages(session.messages, session.topic)
|
||||||
}}
|
}}
|
||||||
@ -251,7 +252,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<Avatar role={message.role} />
|
<Avatar role={message.role} />
|
||||||
</div>
|
</div>
|
||||||
{(message.preview || message.streaming) && (
|
{(message.preview || message.streaming) && (
|
||||||
<div className={styles["chat-message-status"]}>正在输入…</div>
|
<div className={styles["chat-message-status"]}>{Locale.Chat.Typing}</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles["chat-message-item"]}>
|
<div className={styles["chat-message-item"]}>
|
||||||
{(message.preview || message.content.length === 0) &&
|
{(message.preview || message.content.length === 0) &&
|
||||||
@ -283,7 +284,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<div className={styles["chat-input-panel-inner"]}>
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
<textarea
|
<textarea
|
||||||
className={styles["chat-input"]}
|
className={styles["chat-input"]}
|
||||||
placeholder={`输入消息,${submitKey} 发送`}
|
placeholder={Locale.Chat.Input(submitKey)}
|
||||||
rows={3}
|
rows={3}
|
||||||
onInput={(e) => setUserInput(e.currentTarget.value)}
|
onInput={(e) => setUserInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
@ -291,7 +292,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<SendWhiteIcon />}
|
icon={<SendWhiteIcon />}
|
||||||
text={"发送"}
|
text={Locale.Chat.Send}
|
||||||
className={styles["chat-input-send"] + " no-dark"}
|
className={styles["chat-input-send"] + " no-dark"}
|
||||||
onClick={onUserSubmit}
|
onClick={onUserSubmit}
|
||||||
/>
|
/>
|
||||||
@ -322,21 +323,21 @@ function exportMessages(messages: Message[], topic: string) {
|
|||||||
const filename = `${topic}.md`
|
const filename = `${topic}.md`
|
||||||
|
|
||||||
showModal({
|
showModal({
|
||||||
title: "导出聊天记录为 Markdown", children: <div className="markdown-body">
|
title: Locale.Export.Title, children: <div className="markdown-body">
|
||||||
<pre className={styles['export-content']}>{mdText}</pre>
|
<pre className={styles['export-content']}>{mdText}</pre>
|
||||||
</div>, actions: [
|
</div>, actions: [
|
||||||
<IconButton key="copy" icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(mdText)} />,
|
<IconButton key="copy" icon={<CopyIcon />} bordered text={Locale.Export.Copy} onClick={() => copyToClipboard(mdText)} />,
|
||||||
<IconButton key="download" icon={<DownloadIcon />} bordered text="下载文件" onClick={() => downloadAs(mdText, filename)} />
|
<IconButton key="download" icon={<DownloadIcon />} bordered text={Locale.Export.Download} onClick={() => downloadAs(mdText, filename)} />
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMemoryPrompt(session: ChatSession) {
|
function showMemoryPrompt(session: ChatSession) {
|
||||||
showModal({
|
showModal({
|
||||||
title: `上下文记忆 Prompt (${session.lastSummarizeIndex} of ${session.messages.length})`, children: <div className="markdown-body">
|
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`, children: <div className="markdown-body">
|
||||||
<pre className={styles['export-content']}>{session.memoryPrompt || '无'}</pre>
|
<pre className={styles['export-content']}>{session.memoryPrompt || Locale.Memory.EmptyContent}</pre>
|
||||||
</div>, actions: [
|
</div>, actions: [
|
||||||
<IconButton key="copy" icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(session.memoryPrompt)} />,
|
<IconButton key="copy" icon={<CopyIcon />} bordered text={Locale.Memory.Copy} onClick={() => copyToClipboard(session.memoryPrompt)} />,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -405,7 +406,7 @@ export function Home() {
|
|||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
text={"新的聊天"}
|
text={Locale.Home.NewChat}
|
||||||
onClick={createNewSession}
|
onClick={createNewSession}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react";
|
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
||||||
|
|
||||||
import styles from "./settings.module.scss";
|
import styles from "./settings.module.scss";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ import { IconButton } from "./button";
|
|||||||
import { SubmitKey, useChatStore, Theme } from "../store";
|
import { SubmitKey, useChatStore, Theme } from "../store";
|
||||||
import { Avatar } from "./home";
|
import { Avatar } from "./home";
|
||||||
|
|
||||||
|
import Locale, { changeLang, getLang } from '../locales'
|
||||||
|
|
||||||
export function Settings(props: { closeSettings: () => void }) {
|
export function Settings(props: { closeSettings: () => void }) {
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [
|
const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [
|
||||||
@ -27,8 +29,8 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<>
|
<>
|
||||||
<div className={styles["window-header"]}>
|
<div className={styles["window-header"]}>
|
||||||
<div className={styles["window-header-title"]}>
|
<div className={styles["window-header-title"]}>
|
||||||
<div className={styles["window-header-main-title"]}>设置</div>
|
<div className={styles["window-header-main-title"]}>{Locale.Settings.Title}</div>
|
||||||
<div className={styles["window-header-sub-title"]}>设置选项</div>
|
<div className={styles["window-header-sub-title"]}>{Locale.Settings.SubTitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -36,7 +38,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
icon={<ClearIcon />}
|
icon={<ClearIcon />}
|
||||||
onClick={clearAllData}
|
onClick={clearAllData}
|
||||||
bordered
|
bordered
|
||||||
title="清除所有数据"
|
title={Locale.Settings.Actions.ClearAll}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -44,7 +46,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
icon={<ResetIcon />}
|
icon={<ResetIcon />}
|
||||||
onClick={resetConfig}
|
onClick={resetConfig}
|
||||||
bordered
|
bordered
|
||||||
title="重置所有选项"
|
title={Locale.Settings.Actions.ResetAll}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -52,7 +54,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
icon={<CloseIcon />}
|
icon={<CloseIcon />}
|
||||||
onClick={props.closeSettings}
|
onClick={props.closeSettings}
|
||||||
bordered
|
bordered
|
||||||
title="关闭"
|
title={Locale.Settings.Actions.Close}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -60,7 +62,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<div className={styles["settings"]}>
|
<div className={styles["settings"]}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>头像</div>
|
<div className={styles["settings-title"]}>{Locale.Settings.Avatar}</div>
|
||||||
<Popover
|
<Popover
|
||||||
onClose={() => setShowEmojiPicker(false)}
|
onClose={() => setShowEmojiPicker(false)}
|
||||||
content={
|
content={
|
||||||
@ -85,7 +87,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>发送键</div>
|
<div className={styles["settings-title"]}>{Locale.Settings.SendKey}</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<select
|
<select
|
||||||
value={config.submitKey}
|
value={config.submitKey}
|
||||||
@ -106,7 +108,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>主题</div>
|
<div className={styles["settings-title"]}>{Locale.Settings.Theme}</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<select
|
<select
|
||||||
value={config.theme}
|
value={config.theme}
|
||||||
@ -126,7 +128,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>紧凑边框</div>
|
<div className={styles["settings-title"]}>{Locale.Settings.TightBorder}</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={config.tightBorder}
|
checked={config.tightBorder}
|
||||||
@ -137,10 +139,30 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<div className={styles["settings-title"]}>{Locale.Settings.Lang.Name}</div>
|
||||||
|
<div className="">
|
||||||
|
<select
|
||||||
|
value={getLang()}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeLang(e.target.value as any)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value='en' key='en'>
|
||||||
|
{Locale.Settings.Lang.Options.en}
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<option value='cn' key='cn'>
|
||||||
|
{Locale.Settings.Lang.Options.cn}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>附带历史消息数</div>
|
<div className={styles["settings-title"]}>{Locale.Settings.HistoryCount}</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
title={config.historyMessageCount.toString()}
|
title={config.historyMessageCount.toString()}
|
||||||
@ -159,7 +181,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>
|
<div className={styles["settings-title"]}>
|
||||||
历史消息压缩长度阈值
|
{Locale.Settings.CompressThreshold}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -173,21 +195,6 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<div className={styles["settings-title"]}>
|
|
||||||
上下文中包含机器人消息
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={config.sendBotMessages}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateConfig(
|
|
||||||
(config) => (config.sendBotMessages = e.currentTarget.checked)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
></input>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
71
app/locales/cn.ts
Normal file
71
app/locales/cn.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
const cn = {
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: '查看消息列表',
|
||||||
|
CompressedHistory: '查看压缩后的历史 Prompt',
|
||||||
|
Export: '导出聊天记录',
|
||||||
|
},
|
||||||
|
Typing: '正在输入…',
|
||||||
|
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
|
||||||
|
Send: '发送',
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: '导出聊天记录为 Markdown',
|
||||||
|
Copy: '全部复制',
|
||||||
|
Download: '下载文件',
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: '上下文记忆 Prompt',
|
||||||
|
EmptyContent: '尚未记忆',
|
||||||
|
Copy: '全部复制',
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: '新的聊天',
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: '设置',
|
||||||
|
SubTitle: '设置选项',
|
||||||
|
Actions: {
|
||||||
|
ClearAll: '清除所有数据',
|
||||||
|
ResetAll: '重置所有选项',
|
||||||
|
Close: '关闭',
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: 'Language',
|
||||||
|
Options: {
|
||||||
|
cn: '中文',
|
||||||
|
en: 'English'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Avatar: '头像',
|
||||||
|
SendKey: '发送键',
|
||||||
|
Theme: '主题',
|
||||||
|
TightBorder: '紧凑边框',
|
||||||
|
HistoryCount: '附带历史消息数',
|
||||||
|
CompressThreshold: '历史消息长度压缩阈值',
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: '新的聊天',
|
||||||
|
BotHello: '有什么可以帮你的吗',
|
||||||
|
Error: '出错了,稍后重试吧',
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) => '这是 ai 和用户的历史聊天总结作为前情提要:' + content,
|
||||||
|
Topic: "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
|
||||||
|
Summarize: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内',
|
||||||
|
},
|
||||||
|
ConfirmClearAll: '确认清除所有聊天、设置数据?',
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: '已写入剪切板',
|
||||||
|
Failed: '复制失败,请赋予剪切板权限',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocaleType = typeof cn;
|
||||||
|
|
||||||
|
export default cn;
|
70
app/locales/en.ts
Normal file
70
app/locales/en.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { LocaleType } from './index'
|
||||||
|
|
||||||
|
const en: LocaleType = {
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} messages`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} messages with ChatGPT`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: 'Go To Chat List',
|
||||||
|
CompressedHistory: 'Compressed History Memory Prompt',
|
||||||
|
Export: 'Export All Messages as Markdown',
|
||||||
|
},
|
||||||
|
Typing: 'Typing…',
|
||||||
|
Input: (submitKey: string) => `Type something and press ${submitKey} to send`,
|
||||||
|
Send: 'Send',
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: 'All Messages',
|
||||||
|
Copy: 'Copy All',
|
||||||
|
Download: 'Download',
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: 'Memory Prompt',
|
||||||
|
EmptyContent: 'Nothing yet.',
|
||||||
|
Copy: 'Copy All',
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: 'New Chat',
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: 'Settings',
|
||||||
|
SubTitle: 'All Settings',
|
||||||
|
Actions: {
|
||||||
|
ClearAll: 'Clear All Data',
|
||||||
|
ResetAll: 'Reset All Settings',
|
||||||
|
Close: 'Close',
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: '语言',
|
||||||
|
Options: {
|
||||||
|
cn: '中文',
|
||||||
|
en: 'English'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Avatar: 'Avatar',
|
||||||
|
SendKey: 'Send Key',
|
||||||
|
Theme: 'Theme',
|
||||||
|
TightBorder: 'Tight Border',
|
||||||
|
HistoryCount: 'History Message Count',
|
||||||
|
CompressThreshold: 'Message Compression Threshold',
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: 'New Conversation',
|
||||||
|
BotHello: 'Hello! How can I assist you today?',
|
||||||
|
Error: 'Something went wrong, please try again later.',
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) => 'This is a summary of the chat history between the AI and the user as a recap: ' + content,
|
||||||
|
Topic: "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.",
|
||||||
|
Summarize: 'Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.',
|
||||||
|
},
|
||||||
|
ConfirmClearAll: 'Confirm to clear all chat and setting data?',
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: 'Copied to clipboard',
|
||||||
|
Failed: 'Copy failed, please grant permission to access clipboard',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default en;
|
30
app/locales/index.ts
Normal file
30
app/locales/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import CN from './cn'
|
||||||
|
import EN from './en'
|
||||||
|
|
||||||
|
export type { LocaleType } from './cn'
|
||||||
|
|
||||||
|
type Lang = 'en' | 'cn'
|
||||||
|
|
||||||
|
const LANG_KEY = 'lang'
|
||||||
|
export function getLang(): Lang {
|
||||||
|
const savedLang = localStorage?.getItem(LANG_KEY)
|
||||||
|
|
||||||
|
if (['en', 'cn'].includes(savedLang ?? '')) {
|
||||||
|
return savedLang as Lang
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = navigator.language.toLowerCase()
|
||||||
|
|
||||||
|
if (lang.includes('zh') || lang.includes('cn')) {
|
||||||
|
return 'cn'
|
||||||
|
} else {
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeLang(lang: Lang) {
|
||||||
|
localStorage.setItem(LANG_KEY, lang)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { en: EN, cn: CN }[getLang()]
|
18
app/store.ts
18
app/store.ts
@ -2,9 +2,11 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
import { type ChatCompletionResponseMessage } from "openai";
|
import { type ChatCompletionResponseMessage } from "openai";
|
||||||
import { requestChat, requestChatStream, requestWithPrompt } from "./requests";
|
import { requestChatStream, requestWithPrompt } from "./requests";
|
||||||
import { trimTopic } from "./utils";
|
import { trimTopic } from "./utils";
|
||||||
|
|
||||||
|
import Locale from './locales'
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
@ -60,7 +62,7 @@ export interface ChatSession {
|
|||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TOPIC = "新的聊天";
|
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
|
||||||
|
|
||||||
function createEmptySession(): ChatSession {
|
function createEmptySession(): ChatSession {
|
||||||
const createDate = new Date().toLocaleString();
|
const createDate = new Date().toLocaleString();
|
||||||
@ -72,7 +74,7 @@ function createEmptySession(): ChatSession {
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "有什么可以帮你的吗",
|
content: Locale.Store.BotHello,
|
||||||
date: createDate,
|
date: createDate,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -234,7 +236,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
botMessage.content += "\n\n出错了,稍后重试吧";
|
botMessage.content += "\n\n" + Locale.Store.Error;
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
set(() => ({}));
|
set(() => ({}));
|
||||||
},
|
},
|
||||||
@ -247,7 +249,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: '这是 ai 和用户的历史聊天总结作为前情提要:' + session.memoryPrompt,
|
content: Locale.Store.Prompt.History(session.memoryPrompt),
|
||||||
date: ''
|
date: ''
|
||||||
} as Message
|
} as Message
|
||||||
},
|
},
|
||||||
@ -286,7 +288,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
// should summarize topic
|
// should summarize topic
|
||||||
requestWithPrompt(
|
requestWithPrompt(
|
||||||
session.messages,
|
session.messages,
|
||||||
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”"
|
Locale.Store.Prompt.Topic
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
get().updateCurrentSession(
|
get().updateCurrentSession(
|
||||||
(session) => (session.topic = trimTopic(res))
|
(session) => (session.topic = trimTopic(res))
|
||||||
@ -312,7 +314,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
if (historyMsgLength > config.compressMessageLengthThreshold) {
|
if (historyMsgLength > config.compressMessageLengthThreshold) {
|
||||||
requestChatStream(toBeSummarizedMsgs.concat({
|
requestChatStream(toBeSummarizedMsgs.concat({
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内',
|
content: Locale.Store.Prompt.Summarize,
|
||||||
date: ''
|
date: ''
|
||||||
}), {
|
}), {
|
||||||
filterBot: false,
|
filterBot: false,
|
||||||
@ -345,7 +347,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearAllData() {
|
clearAllData() {
|
||||||
if (confirm('确认清除所有聊天、设置数据?')) {
|
if (confirm(Locale.Store.ConfirmClearAll)) {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import Locale from './locales'
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
const s = topic.split("");
|
const s = topic.split("");
|
||||||
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
||||||
@ -12,9 +14,9 @@ export function trimTopic(topic: string) {
|
|||||||
|
|
||||||
export function copyToClipboard(text: string) {
|
export function copyToClipboard(text: string) {
|
||||||
navigator.clipboard.writeText(text).then(res => {
|
navigator.clipboard.writeText(text).then(res => {
|
||||||
alert('复制成功')
|
alert(Locale.Copy.Success)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
alert('复制失败,请赋予剪切板权限')
|
alert(Locale.Copy.Failed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user