diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 15d324074..a4c14c843 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -5,7 +5,7 @@ permissions: on: schedule: - - cron: "0 * * * *" # every hour + - cron: "0 0 * * *" # every day workflow_dispatch: jobs: diff --git a/README.md b/README.md index 9f54194ab..90ed7d42f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. ## 最新动态 - 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 +- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com ## Get Started @@ -167,7 +168,13 @@ Specify OpenAI organization ID. > Default: Empty -If you do not want users to input their own API key, set this environment variable to 1. +If you do not want users to input their own API key, set this value to 1. + +### `DISABLE_GPT4` (optional) + +> Default: Empty + +If you do not want users to use GPT-4, set this value to 1. ## Development @@ -255,6 +262,9 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [@WingCH](https://github.com/WingCH) [@jtung4](https://github.com/jtung4) [@micozhu](https://github.com/micozhu) +[@jhansion](https://github.com/jhansion) +[@Sha1rholder](https://github.com/Sha1rholder) +[@AnsonHyq](https://github.com/AnsonHyq) ### Contributor diff --git a/README_CN.md b/README_CN.md index 60302be63..1e987542d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -64,7 +64,7 @@ code1,code2,code3 ## 环境变量 -> 本项目大多数配置项都通过环境变量来设置。 +> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。 ### `OPENAI_API_KEY` (必填项) @@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 +### `DISABLE_GPT4` (可选) + +如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。 + ## 开发 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 62c8d60fb..2b3bcbf20 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { getServerSideConfig } from "../../config/server"; @@ -9,6 +9,7 @@ const serverConfig = getServerSideConfig(); const DANGER_CONFIG = { needCode: serverConfig.needCode, hideUserApiKey: serverConfig.hideUserApiKey, + enableGPT4: serverConfig.enableGPT4, }; declare global { diff --git a/app/command.ts b/app/command.ts new file mode 100644 index 000000000..40bad92b3 --- /dev/null +++ b/app/command.ts @@ -0,0 +1,28 @@ +import { useSearchParams } from "react-router-dom"; + +type Command = (param: string) => void; +interface Commands { + fill?: Command; + submit?: Command; + mask?: Command; +} + +export function useCommand(commands: Commands = {}) { + const [searchParams, setSearchParams] = useSearchParams(); + + if (commands === undefined) return; + + let shouldUpdate = false; + searchParams.forEach((param, name) => { + const commandName = name as keyof Commands; + if (typeof commands[commandName] === "function") { + commands[commandName]!(param); + searchParams.delete(name); + shouldUpdate = true; + } + }); + + if (shouldUpdate) { + setSearchParams(searchParams); + } +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ca51a06af..54def01cf 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -26,12 +26,10 @@ import { SubmitKey, useChatStore, BOT_HELLO, - ROLES, createMessage, useAccessStore, Theme, useAppConfig, - ModelConfig, DEFAULT_TOPIC, } from "../store"; @@ -55,14 +53,11 @@ import chatStyle from "./chat.module.scss"; import { ListItem, Modal, showModal } from "./ui-lib"; import { useLocation, useNavigate } from "react-router-dom"; -import { Path } from "../constant"; +import { LAST_INPUT_KEY, Path } from "../constant"; import { Avatar } from "./emoji"; import { MaskAvatar, MaskConfig } from "./mask"; -import { - DEFAULT_MASK_AVATAR, - DEFAULT_MASK_ID, - useMaskStore, -} from "../store/mask"; +import { useMaskStore } from "../store/mask"; +import { useCommand } from "../command"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -409,7 +404,6 @@ export function Chat() { const inputRef = useRef(null); const [userInput, setUserInput] = useState(""); - const [beforeInput, setBeforeInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); @@ -478,12 +472,11 @@ export function Chat() { } }; - // submit user input - const onUserSubmit = () => { + const doSubmit = (userInput: string) => { if (userInput.trim() === "") return; setIsLoading(true); chatStore.onUserInput(userInput).then(() => setIsLoading(false)); - setBeforeInput(userInput); + localStorage.setItem(LAST_INPUT_KEY, userInput); setUserInput(""); setPromptHints([]); if (!isMobileScreen) inputRef.current?.focus(); @@ -497,23 +490,18 @@ export function Chat() { // check if should send message const onInputKeyDown = (e: React.KeyboardEvent) => { - // if ArrowUp and no userInput + // if ArrowUp and no userInput, fill with last input if (e.key === "ArrowUp" && userInput.length <= 0) { - setUserInput(beforeInput); + setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); e.preventDefault(); return; } if (shouldSubmit(e)) { - onUserSubmit(); + doSubmit(userInput); e.preventDefault(); } }; const onRightClick = (e: any, message: Message) => { - // auto fill user input - if (message.role === "user") { - setUserInput(message.content); - } - // copy to clipboard if (selectOrCopy(e.currentTarget, message.content)) { e.preventDefault(); @@ -618,6 +606,13 @@ export function Chat() { const isChat = location.pathname === Path.Chat; const autoFocus = !isMobileScreen || isChat; // only focus in chat page + useCommand({ + fill: setUserInput, + submit: (text) => { + doSubmit(text); + }, + }); + return (
@@ -816,7 +811,7 @@ export function Chat() { text={Locale.Chat.Send} className={styles["chat-input-send"]} type="primary" - onClick={onUserSubmit} + onClick={() => doSubmit(userInput)} />
diff --git a/app/components/home.tsx b/app/components/home.tsx index a83a77982..4c3d0a646 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -23,6 +23,7 @@ import { } from "react-router-dom"; import { SideBar } from "./sidebar"; import { useAppConfig } from "../store/config"; +import { useMaskStore } from "../store/mask"; export function Loading(props: { noLogo?: boolean }) { return ( diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 9e71bb048..fb37fdc43 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -12,15 +12,21 @@ import mermaid from "mermaid"; import LoadingIcon from "../icons/three-dots.svg"; import React from "react"; -export function Mermaid(props: { code: string }) { +export function Mermaid(props: { code: string; onError: () => void }) { const ref = useRef(null); useEffect(() => { if (props.code && ref.current) { - mermaid.run({ - nodes: [ref.current], - }); + mermaid + .run({ + nodes: [ref.current], + }) + .catch((e) => { + props.onError(); + console.error("[Mermaid] ", e.message); + }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.code]); function viewSvgInNewWindow() { @@ -38,7 +44,7 @@ export function Mermaid(props: { code: string }) { return (
viewSvgInNewWindow()} > @@ -60,7 +66,7 @@ export function PreCode(props: { children: any }) { }, [props.children]); if (mermaidCode) { - return ; + return setMermaidCode("")} />; } return ( @@ -147,7 +153,7 @@ export function Markdown( } }; - checkInView(); + setTimeout(() => checkInView(), 1); return (
void; + remove: () => void; +}) { + const [focusingInput, setFocusingInput] = useState(false); + + return ( +
+ {!focusingInput && ( + + )} + setFocusingInput(true)} + onBlur={() => setFocusingInput(false)} + onInput={(e) => + props.update({ + ...props.prompt, + content: e.currentTarget.value as any, + }) + } + /> + {!focusingInput && ( + } + className={chatStyle["context-delete-button"]} + onClick={() => props.remove()} + bordered + /> + )} +
+ ); +} + export function ContextPrompts(props: { context: Message[]; updateContext: (updater: (context: Message[]) => void) => void; @@ -128,42 +181,12 @@ export function ContextPrompts(props: { <>
{context.map((c, i) => ( -
- - - updateContextPrompt(i, { - ...c, - content: e.currentTarget.value as any, - }) - } - /> - } - className={chatStyle["context-delete-button"]} - onClick={() => removeContextPrompt(i)} - bordered - /> -
+ updateContextPrompt(i, prompt)} + remove={() => removeContextPrompt(i)} + /> ))}
@@ -174,7 +197,7 @@ export function ContextPrompts(props: { className={chatStyle["context-prompt-button"]} onClick={() => addContextPrompt({ - role: "system", + role: "user", content: "", date: "", }) diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 32c2f5c0d..fe9319e0f 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -1,4 +1,3 @@ -import styles from "./settings.module.scss"; import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store"; import Locale from "../locales"; diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index 42612e0ad..81858fb02 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -13,6 +13,7 @@ import { Mask, useMaskStore } from "../store/mask"; import Locale from "../locales"; import { useAppConfig, useChatStore } from "../store"; import { MaskAvatar } from "./mask"; +import { useCommand } from "../command"; function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { const xmin = Math.max(aRect.x, bRect.x); @@ -108,9 +109,20 @@ export function NewChat() { const startChat = (mask?: Mask) => { chatStore.newSession(mask); - navigate(Path.Chat); + setTimeout(() => navigate(Path.Chat), 1); }; + useCommand({ + mask: (id) => { + try { + const mask = maskStore.get(parseInt(id)); + startChat(mask ?? undefined); + } catch { + console.error("[New Chat] failed to create chat from mask id=", id); + } + }, + }); + return (
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index f257a3ca4..1eac17c16 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -60,18 +60,13 @@ .user-prompt-buttons { display: flex; align-items: center; + column-gap: 2px; .user-prompt-button { - height: 100%; - - &:not(:last-child) { - margin-right: 5px; - } + //height: 100%; + padding: 7px; } } } } - - .user-prompt-actions { - } } diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 531a451c6..70e21e32d 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -32,6 +32,28 @@ const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { loading: () => null, }); +function useHotKey() { + const chatStore = useChatStore(); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.metaKey || e.altKey || e.ctrlKey) { + const n = chatStore.sessions.length; + const limit = (x: number) => (x + n) % n; + const i = chatStore.currentSessionIndex; + if (e.key === "ArrowUp") { + chatStore.selectSession(limit(i - 1)); + } else if (e.key === "ArrowDown") { + chatStore.selectSession(limit(i + 1)); + } + } + }; + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }); +} + function useDragSideBar() { const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); @@ -86,9 +108,10 @@ export function SideBar(props: { className?: string }) { // drag side bar const { onDragMouseDown, shouldNarrow } = useDragSideBar(); const navigate = useNavigate(); - const config = useAppConfig(); + useHotKey(); + return (
{ proxyUrl: process.env.PROXY_URL, isVercel: !!process.env.VERCEL, hideUserApiKey: !!process.env.HIDE_USER_API_KEY, + enableGPT4: !process.env.DISABLE_GPT4, }; }; diff --git a/app/constant.ts b/app/constant.ts index fed20cafa..d0f9fc743 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -38,3 +38,5 @@ export const MIN_SIDEBAR_WIDTH = 230; export const NARROW_SIDEBAR_WIDTH = 100; export const ACCESS_CODE_PREFIX = "ak-"; + +export const LAST_INPUT_KEY = "last-input"; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 9af3d86e8..7f4d19fad 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -4,7 +4,7 @@ const cn = { WIP: "该功能仍在开发中……", Error: { Unauthorized: - "现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。", + "访问密码不正确或为空,请前往[设置](/#/settings)页输入正确的访问密码,或者填入你自己的 OpenAI API Key。", }, ChatItem: { ChatItemCount: (count: number) => `${count} 条对话`, @@ -78,6 +78,7 @@ const cn = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "头像", @@ -148,7 +149,7 @@ const cn = { }, AccessCode: { Title: "访问密码", - SubTitle: "已开启加密访问", + SubTitle: "管理员已开启加密访问", Placeholder: "请输入访问密码", }, Model: "模型 (model)", diff --git a/app/locales/de.ts b/app/locales/de.ts index 048e575c5..56202722d 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -81,6 +81,7 @@ const de: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "Avatar", diff --git a/app/locales/en.ts b/app/locales/en.ts index 02aa8aa7d..afe7974f3 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -80,6 +80,7 @@ const en: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "Avatar", diff --git a/app/locales/es.ts b/app/locales/es.ts index 46d18a547..df28075eb 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -80,6 +80,7 @@ const es: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "Avatar", diff --git a/app/locales/index.ts b/app/locales/index.ts index 40f0a1ade..dee6f795b 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -6,6 +6,7 @@ import IT from "./it"; import TR from "./tr"; import JP from "./jp"; import DE from "./de"; +import VI from "./vi"; export type { LocaleType } from "./cn"; @@ -18,6 +19,7 @@ export const AllLangs = [ "tr", "jp", "de", + "vi", ] as const; export type Lang = (typeof AllLangs)[number]; @@ -79,4 +81,5 @@ export default { tr: TR, jp: JP, de: DE, + vi: VI, }[getLang()] as typeof CN; diff --git a/app/locales/it.ts b/app/locales/it.ts index ee9a2c2bc..abf655f0d 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -80,6 +80,7 @@ const it: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "Avatar", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index fb693cf5b..de03f9fdc 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -80,6 +80,7 @@ const jp: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "アバター", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 5eb4fe3e4..6793beb95 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -80,6 +80,7 @@ const tr: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "Avatar", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 0095ef38e..8a7364d56 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -78,6 +78,7 @@ const tw: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", + vi: "Vietnamese", }, }, Avatar: "大頭貼", diff --git a/app/locales/vi.ts b/app/locales/vi.ts new file mode 100644 index 000000000..66d4a4d58 --- /dev/null +++ b/app/locales/vi.ts @@ -0,0 +1,241 @@ +import { SubmitKey } from "../store/config"; +import type { LocaleType } from "./index"; + +const vi: LocaleType = { + WIP: "Coming Soon...", + Error: { + Unauthorized: + "Truy cập chưa xác thực, vui lòng nhập mã truy cập trong trang cài đặt.", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} tin nhắn`, + }, + Chat: { + SubTitle: (count: number) => `${count} tin nhắn với ChatGPT`, + Actions: { + ChatList: "Xem danh sách chat", + CompressedHistory: "Nén tin nhắn trong quá khứ", + Export: "Xuất tất cả tin nhắn dưới dạng Markdown", + Copy: "Sao chép", + Stop: "Dừng", + Retry: "Thử lại", + Delete: "Xóa", + }, + Rename: "Đổi tên", + Typing: "Đang nhập…", + Input: (submitKey: string) => { + var inputHints = `${submitKey} để gửi`; + if (submitKey === String(SubmitKey.Enter)) { + inputHints += ", Shift + Enter để xuống dòng"; + } + return inputHints + ", / để tìm kiếm mẫu gợi ý"; + }, + Send: "Gửi", + Config: { + Reset: "Khôi phục cài đặt gốc", + SaveAs: "Lưu dưới dạng Mẫu", + }, + }, + Export: { + Title: "Tất cả tin nhắn", + Copy: "Sao chép tất cả", + Download: "Tải xuống", + MessageFromYou: "Tin nhắn của bạn", + MessageFromChatGPT: "Tin nhắn từ ChatGPT", + }, + Memory: { + Title: "Lịch sử tin nhắn", + EmptyContent: "Chưa có tin nhắn", + Send: "Gửi tin nhắn trong quá khứ", + Copy: "Sao chép tin nhắn trong quá khứ", + Reset: "Đặt lại phiên", + ResetConfirm: + "Đặt lại sẽ xóa toàn bộ lịch sử trò chuyện hiện tại và bộ nhớ. Bạn có chắc chắn muốn đặt lại không?", + }, + Home: { + NewChat: "Cuộc trò chuyện mới", + DeleteChat: "Xác nhận xóa các cuộc trò chuyện đã chọn?", + DeleteToast: "Đã xóa cuộc trò chuyện", + Revert: "Khôi phục", + }, + Settings: { + Title: "Cài đặt", + SubTitle: "Tất cả cài đặt", + Actions: { + ClearAll: "Xóa toàn bộ dữ liệu", + ResetAll: "Khôi phục cài đặt gốc", + Close: "Đóng", + ConfirmResetAll: "Bạn chắc chắn muốn thiết lập lại tất cả cài đặt?", + ConfirmClearAll: "Bạn chắc chắn muốn thiết lập lại tất cả dữ liệu?", + }, + Lang: { + Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "Tất cả ngôn ngữ", + Options: { + cn: "简体中文", + en: "English", + tw: "繁體中文", + es: "Español", + it: "Italiano", + tr: "Türkçe", + jp: "日本語", + de: "Deutsch", + vi: "Vietnamese", + }, + }, + Avatar: "Ảnh đại diện", + FontSize: { + Title: "Font chữ", + SubTitle: "Thay đổi font chữ của nội dung trò chuyện", + }, + Update: { + Version: (x: string) => `Phiên bản: ${x}`, + IsLatest: "Phiên bản mới nhất", + CheckUpdate: "Kiểm tra bản cập nhật", + IsChecking: "Kiểm tra bản cập nhật...", + FoundUpdate: (x: string) => `Phát hiện phiên bản mới: ${x}`, + GoToUpdate: "Cập nhật", + }, + SendKey: "Phím gửi", + Theme: "Theme", + TightBorder: "Chế độ không viền", + SendPreviewBubble: { + Title: "Gửi bong bóng xem trước", + SubTitle: "Xem trước nội dung markdown bằng bong bóng", + }, + Mask: { + Title: "Mask Splash Screen", + SubTitle: "Chớp màn hình khi bắt đầu cuộc trò chuyện mới", + }, + Prompt: { + Disable: { + Title: "Vô hiệu hóa chức năng tự động hoàn thành", + SubTitle: "Nhập / để kích hoạt chức năng tự động hoàn thành", + }, + List: "Danh sách mẫu gợi ý", + ListCount: (builtin: number, custom: number) => + `${builtin} có sẵn, ${custom} do người dùng xác định`, + Edit: "Chỉnh sửa", + Modal: { + Title: "Danh sách mẫu gợi ý", + Add: "Thêm", + Search: "Tìm kiếm mẫu", + }, + EditModal: { + Title: "Chỉnh sửa mẫu", + }, + }, + HistoryCount: { + Title: "Số lượng tin nhắn đính kèm", + SubTitle: "Số lượng tin nhắn trong quá khứ được gửi kèm theo mỗi yêu cầu", + }, + CompressThreshold: { + Title: "Ngưỡng nén lịch sử tin nhắn", + SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng", + }, + Token: { + Title: "API Key", + SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập", + Placeholder: "OpenAI API Key", + }, + Usage: { + Title: "Hạn mức tài khoản", + SubTitle(used: any, total: any) { + return `Đã sử dụng $${used} trong tháng này, hạn mức $${total}`; + }, + IsChecking: "Đang kiểm tra...", + Check: "Kiểm tra", + NoAccess: "Nhập API Key để kiểm tra hạn mức", + }, + AccessCode: { + Title: "Mã truy cập", + SubTitle: "Đã bật kiểm soát truy cập", + Placeholder: "Nhập mã truy cập", + }, + Model: "Mô hình", + Temperature: { + Title: "Tính ngẫu nhiên (temperature)", + SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên", + }, + MaxTokens: { + Title: "Giới hạn số lượng token (max_tokens)", + SubTitle: "Số lượng token tối đa được sử dụng trong mỗi lần tương tác", + }, + PresencePenlty: { + Title: "Chủ đề mới (presence_penalty)", + SubTitle: "Giá trị càng lớn tăng khả năng mở rộng sang các chủ đề mới", + }, + }, + Store: { + DefaultTopic: "Cuộc trò chuyện mới", + BotHello: "Xin chào! Mình có thể giúp gì cho bạn?", + Error: "Có lỗi xảy ra, vui lòng thử lại sau.", + Prompt: { + History: (content: string) => + "Tóm tắt ngắn gọn cuộc trò chuyện giữa người dùng và AI: " + content, + Topic: + "Sử dụng 4 đến 5 từ tóm tắt cuộc trò chuyện này mà không có phần mở đầu, dấu chấm câu, dấu ngoặc kép, dấu chấm, ký hiệu hoặc văn bản bổ sung nào. Loại bỏ các dấu ngoặc kép kèm theo.", + Summarize: + "Tóm tắt cuộc trò chuyện này một cách ngắn gọn trong 200 từ hoặc ít hơn để sử dụng làm gợi ý cho ngữ cảnh tiếp theo.", + }, + }, + Copy: { + Success: "Sao chép vào bộ nhớ tạm", + Failed: + "Sao chép không thành công, vui lòng cấp quyền truy cập vào bộ nhớ tạm", + }, + Context: { + Toast: (x: any) => `Sử dụng ${x} tin nhắn chứa ngữ cảnh`, + Edit: "Thiết lập ngữ cảnh và bộ nhớ", + Add: "Thêm tin nhắn", + }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Mẫu", + Page: { + Title: "Mẫu trò chuyện", + SubTitle: (count: number) => `${count} mẫu`, + Search: "Tìm kiếm mẫu", + Create: "Tạo", + }, + Item: { + Info: (count: number) => `${count} tin nhắn`, + Chat: "Chat", + View: "Xem trước", + Edit: "Chỉnh sửa", + Delete: "Xóa", + DeleteConfirm: "Xác nhận xóa?", + }, + EditModal: { + Title: (readonly: boolean) => + `Chỉnh sửa mẫu ${readonly ? "(chỉ xem)" : ""}`, + Download: "Tải xuống", + Clone: "Tạo bản sao", + }, + Config: { + Avatar: "Ảnh đại diện bot", + Name: "Tên bot", + }, + }, + NewChat: { + Return: "Quay lại", + Skip: "Bỏ qua", + Title: "Chọn 1 biểu tượng", + SubTitle: "Bắt đầu trò chuyện ẩn sau lớp mặt nạ", + More: "Tìm thêm", + NotShow: "Không hiển thị lại", + ConfirmNoShow: "Xác nhận tắt? Bạn có thể bật lại trong phần cài đặt.", + }, + + UI: { + Confirm: "Xác nhận", + Cancel: "Hủy", + Close: "Đóng", + Create: "Tạo", + Edit: "Chỉnh sửa", + }, +}; + +export default vi; diff --git a/app/store/access.ts b/app/store/access.ts index 51290d0a7..79b7b9900 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { StoreKey } from "../constant"; import { BOT_HELLO } from "./chat"; +import { ALL_MODELS } from "./config"; export interface AccessControlStore { accessCode: string; @@ -60,6 +61,14 @@ export const useAccessStore = create()( console.log("[Config] got config from server", res); set(() => ({ ...res })); + if (!res.enableGPT4) { + ALL_MODELS.forEach((model) => { + if (model.name.startsWith("gpt-4")) { + (model as any).available = false; + } + }); + } + if ((res as any).botHello) { BOT_HELLO.content = (res as any).botHello; } diff --git a/app/store/chat.ts b/app/store/chat.ts index 5abd81298..cb11087d4 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -7,11 +7,11 @@ import { requestChatStream, requestWithPrompt, } from "../requests"; -import { isMobileScreen, trimTopic } from "../utils"; +import { trimTopic } from "../utils"; import Locale from "../locales"; import { showToast } from "../components/ui-lib"; -import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config"; +import { ModelType } from "./config"; import { createEmptyMask, Mask } from "./mask"; import { StoreKey } from "../constant"; @@ -180,8 +180,9 @@ export const useChatStore = create()( const sessions = get().sessions.slice(); sessions.splice(index, 1); + const currentIndex = get().currentSessionIndex; let nextIndex = Math.min( - get().currentSessionIndex, + currentIndex - Number(index < currentIndex), sessions.length - 1, ); @@ -251,9 +252,20 @@ export const useChatStore = create()( model: modelConfig.model, }); + const systemInfo = createMessage({ + role: "system", + content: `IMPRTANT: You are a virtual assistant powered by the ${ + modelConfig.model + } model, now time is ${new Date().toLocaleString()}}`, + id: botMessage.id! + 1, + }); + // get recent messages + const systemMessages = [systemInfo]; const recentMessages = get().getMessagesWithMemory(); - const sendMessages = recentMessages.concat(userMessage); + const sendMessages = systemMessages.concat( + recentMessages.concat(userMessage), + ); const sessionIndex = get().currentSessionIndex; const messageIndex = get().currentSession().messages.length + 1; diff --git a/app/store/config.ts b/app/store/config.ts index d87da42ba..e7e6555ed 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -76,6 +76,26 @@ export const ALL_MODELS = [ name: "gpt-3.5-turbo-0301", available: true, }, + { + name: "qwen-v1", // 通义千问 + available: false, + }, + { + name: "ernie", // 文心一言 + available: false, + }, + { + name: "spark", // 讯飞星火 + available: false, + }, + { + name: "llama", // llama + available: false, + }, + { + name: "chatglm", // chatglm-6b + available: false, + }, ] as const; export type ModelType = (typeof ALL_MODELS)[number]["name"]; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index f849516a0..1ae908be5 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -248,6 +248,10 @@ div.math { display: flex; align-items: center; justify-content: center; + + @media screen and (max-width: 600px) { + align-items: flex-end; + } } .link { diff --git a/app/utils.ts b/app/utils.ts index 43ea796e5..a272d5684 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -158,15 +158,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) { const width = getDomContentWidth(dom); measureDom.style.width = width + "px"; - measureDom.innerText = dom.value.trim().length > 0 ? dom.value : "1"; - - const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1); + measureDom.innerText = dom.value !== "" ? dom.value : "1"; + const endWithEmptyLine = dom.value.endsWith("\n"); const height = parseFloat(window.getComputedStyle(measureDom).height); const singleLineHeight = parseFloat( window.getComputedStyle(singleLineDom).height, ); - const rows = Math.round(height / singleLineHeight) + lineWrapCount; + const rows = + Math.round(height / singleLineHeight) + (endWithEmptyLine ? 1 : 0); return rows; } diff --git a/docs/cloudflare-pages-cn.md b/docs/cloudflare-pages-cn.md new file mode 100644 index 000000000..2f9a99f2d --- /dev/null +++ b/docs/cloudflare-pages-cn.md @@ -0,0 +1,39 @@ +# Cloudflare Pages 部署指南 + +## 如何新建项目 +在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 + +1. 点击 "Create a project"。 +2. 选择 "Connect to Git"。 +3. 关联 Cloudflare Pages 和你的 GitHub 账号。 +4. 选中你 fork 的此项目。 +5. 点击 "Begin setup"。 +6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。 +7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 +8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: + ``` + npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify + ``` +9. 对于 "Build output directory",使用默认值并且不要修改。 +10. 不要修改 "Root Directory"。 +11. 对于 "Environment variables",点击 ">" 然后点击 "Add variable"。按照以下信息填写: + + - `NODE_VERSION=20.1` + - `NEXT_TELEMETRY_DISABLE=1` + - `OPENAI_API_KEY=你自己的API Key` + - `YARN_VERSION=1.22.19` + - `PHP_VERSION=7.4` + + 根据实际需要,可以选择填写以下选项: + + - `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` + - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` + - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` + - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` + +12. 点击 "Save and Deploy"。 +13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 +14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 +15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 +16. 前往 "Deployments",点击 "Retry deployment"。 +17. Enjoy. \ No newline at end of file diff --git a/docs/cloudflare-pages-en.md b/docs/cloudflare-pages-en.md new file mode 100644 index 000000000..ee8ff6a6b --- /dev/null +++ b/docs/cloudflare-pages-en.md @@ -0,0 +1,38 @@ +# Cloudflare Pages Deployment Guide + +## How to create a new project +Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. + +1. Click "Create a project". +2. Choose "Connect to Git". +3. Connect Cloudflare Pages to your GitHub account. +4. Select the forked project. +5. Click "Begin setup". +6. For "Project name" and "Production branch", use the default values or change them as needed. +7. In "Build Settings", choose the "Framework presets" option and select "Next.js". +8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: + ``` + npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify + ``` +9. For "Build output directory", use the default value and do not modify it. +10. Do not modify "Root Directory". +11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: + - `NODE_VERSION=20.1` + - `NEXT_TELEMETRY_DISABLE=1` + - `OPENAI_API_KEY=your_own_API_key` + - `YARN_VERSION=1.22.19` + - `PHP_VERSION=7.4` + + Optionally fill in the following based on your needs: + + - `CODE= Optional, access passwords, multiple passwords can be separated by commas` + - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` + - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` + - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` + +12. Click "Save and Deploy". +13. Click "Cancel deployment" because you need to fill in Compatibility flags. +14. Go to "Build settings", "Functions", and find "Compatibility flags". +15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". +16. Go to "Deployments" and click "Retry deployment". +17. Enjoy. \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 3f7c2fb6b..c62f88409 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,7 +5,12 @@ const nextConfig = { appDir: true, }, async rewrites() { - const ret = []; + const ret = [ + { + source: "/api/proxy/:path*", + destination: "https://api.openai.com/:path*", + }, + ]; const apiUrl = process.env.API_URL; if (apiUrl) {