Merge remote-tracking branch 'upstream/main'

This commit is contained in:
ZhaoLiu 2023-05-10 14:47:44 +08:00
commit 651ba4b45b
34 changed files with 585 additions and 103 deletions

View File

@ -5,7 +5,7 @@ permissions:
on:
schedule:
- cron: "0 * * * *" # every hour
- cron: "0 0 * * *" # every day
workflow_dispatch:
jobs:

View File

@ -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

View File

@ -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 服务器。

View File

@ -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 {

28
app/command.ts Normal file
View File

@ -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);
}
}

View File

@ -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: () => <LoadingIcon />,
@ -409,7 +404,6 @@ export function Chat() {
const inputRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
// 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 (
<div className={styles.chat} key={session.id}>
<div className="window-header">
@ -816,7 +811,7 @@ export function Chat() {
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
type="primary"
onClick={onUserSubmit}
onClick={() => doSubmit(userInput)}
/>
</div>
</div>

View File

@ -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 (

View File

@ -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<HTMLDivElement>(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 (
<div
className="no-dark"
style={{ cursor: "pointer" }}
style={{ cursor: "pointer", overflow: "auto" }}
ref={ref}
onClick={() => viewSvgInNewWindow()}
>
@ -60,7 +66,7 @@ export function PreCode(props: { children: any }) {
}, [props.children]);
if (mermaidCode) {
return <Mermaid code={mermaidCode} />;
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
}
return (
@ -147,7 +153,7 @@ export function Markdown(
}
};
checkInView();
setTimeout(() => checkInView(), 1);
return (
<div

View File

@ -14,13 +14,13 @@ import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useEffect, useState } from "react";
import { useState } from "react";
import { downloadAs, readFromFile } from "../utils";
import { Updater } from "../api/openai/typing";
import { ModelConfigList } from "./model-config";
@ -106,6 +106,59 @@ export function MaskConfig(props: {
);
}
function ContextPromptItem(props: {
prompt: Message;
update: (prompt: Message) => void;
remove: () => void;
}) {
const [focusingInput, setFocusingInput] = useState(false);
return (
<div className={chatStyle["context-prompt-row"]}>
{!focusingInput && (
<select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
props.update({
...props.prompt,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
)}
<Input
value={props.prompt.content}
type="text"
className={chatStyle["context-content"]}
rows={focusingInput ? 5 : 1}
onFocus={() => setFocusingInput(true)}
onBlur={() => setFocusingInput(false)}
onInput={(e) =>
props.update({
...props.prompt,
content: e.currentTarget.value as any,
})
}
/>
{!focusingInput && (
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => props.remove()}
bordered
/>
)}
</div>
);
}
export function ContextPrompts(props: {
context: Message[];
updateContext: (updater: (context: Message[]) => void) => void;
@ -128,42 +181,12 @@ export function ContextPrompts(props: {
<>
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
{context.map((c, i) => (
<div className={chatStyle["context-prompt-row"]} key={i}>
<select
value={c.role}
className={chatStyle["context-role"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
<Input
value={c.content}
type="text"
className={chatStyle["context-content"]}
rows={1}
onInput={(e) =>
updateContextPrompt(i, {
...c,
content: e.currentTarget.value as any,
})
}
/>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => removeContextPrompt(i)}
bordered
/>
</div>
<ContextPromptItem
key={i}
prompt={c}
update={(prompt) => updateContextPrompt(i, prompt)}
remove={() => removeContextPrompt(i)}
/>
))}
<div className={chatStyle["context-prompt-row"]}>
@ -174,7 +197,7 @@ export function ContextPrompts(props: {
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "system",
role: "user",
content: "",
date: "",
})

View File

@ -1,4 +1,3 @@
import styles from "./settings.module.scss";
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";

View File

@ -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 (
<div className={styles["new-chat"]}>
<div className={styles["mask-header"]}>

View File

@ -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 {
}
}

View File

@ -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 (
<div
className={`${styles.sidebar} ${props.className} ${

View File

@ -124,6 +124,18 @@
}
}
@media screen and (max-width: 600px) {
.modal-container {
width: 100vw;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.modal-content {
max-height: 50vh;
}
}
}
.show {
opacity: 1;
transition: all ease 0.3s;
@ -191,13 +203,3 @@
resize: none;
min-width: 50px;
}
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;
.modal-content {
max-height: 50vh;
}
}
}

View File

@ -8,6 +8,7 @@ declare global {
PROXY_URL?: string;
VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
}
}
}
@ -40,5 +41,6 @@ export const getServerSideConfig = () => {
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
enableGPT4: !process.env.DISABLE_GPT4,
};
};

View File

@ -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";

View File

@ -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)",

View File

@ -81,6 +81,7 @@ const de: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "Avatar",

View File

@ -80,6 +80,7 @@ const en: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "Avatar",

View File

@ -80,6 +80,7 @@ const es: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "Avatar",

View File

@ -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;

View File

@ -80,6 +80,7 @@ const it: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "Avatar",

View File

@ -80,6 +80,7 @@ const jp: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "アバター",

View File

@ -80,6 +80,7 @@ const tr: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "Avatar",

View File

@ -78,6 +78,7 @@ const tw: LocaleType = {
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Vietnamese",
},
},
Avatar: "大頭貼",

241
app/locales/vi.ts Normal file
View File

@ -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;

View File

@ -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<AccessControlStore>()(
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;
}

View File

@ -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<ChatStore>()(
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<ChatStore>()(
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;

View File

@ -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"];

View File

@ -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 {

View File

@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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) {