mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	feat: support i18n
This commit is contained in:
		@@ -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)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user