diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 52feae50e..15d324074 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -31,3 +31,10 @@ jobs:
# Set test_mode true to run tests instead of the true action!!
test_mode: false
+
+ - name: Sync check
+ if: failure()
+ run: |
+ echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
+ echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
+ exit 1
diff --git a/README.md b/README.md
index b2d1e48ce..3b6308be9 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,15 @@
(null);
const [autoScroll, setAutoScroll] = useState(true);
+ const scrollToBottom = () => {
+ const dom = scrollRef.current;
+ if (dom) {
+ setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
+ }
+ };
// auto scroll
useLayoutEffect(() => {
- const dom = scrollRef.current;
- if (dom && autoScroll) {
- setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
- }
+ autoScroll && scrollToBottom();
});
return {
scrollRef,
autoScroll,
setAutoScroll,
+ scrollToBottom,
};
}
+export function ChatActions(props: {
+ showPromptModal: () => void;
+ scrollToBottom: () => void;
+ hitBottom: boolean;
+}) {
+ const chatStore = useChatStore();
+
+ // switch themes
+ const theme = chatStore.config.theme;
+ function nextTheme() {
+ const themes = [Theme.Auto, Theme.Light, Theme.Dark];
+ const themeIndex = themes.indexOf(theme);
+ const nextIndex = (themeIndex + 1) % themes.length;
+ const nextTheme = themes[nextIndex];
+ chatStore.updateConfig((config) => (config.theme = nextTheme));
+ }
+
+ // stop all responses
+ const couldStop = ControllerPool.hasPending();
+ const stopAll = () => ControllerPool.stopAll();
+
+ return (
+
+ {couldStop && (
+
+
+
+ )}
+ {!props.hitBottom && (
+
+
+
+ )}
+ {props.hitBottom && (
+
+
+
+ )}
+
+
+ {theme === Theme.Auto ? (
+
+ ) : theme === Theme.Light ? (
+
+ ) : theme === Theme.Dark ? (
+
+ ) : null}
+
+
+ );
+}
+
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
@@ -357,11 +437,10 @@ export function Chat(props: {
const [beforeInput, setBeforeInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
- const { scrollRef, setAutoScroll } = useScrollToBottom();
+ const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(false);
const onChatBodyScroll = (e: HTMLElement) => {
- setAutoScroll(false);
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
setHitBottom(isTouchBottom);
};
@@ -383,16 +462,6 @@ export function Chat(props: {
inputRef.current?.focus();
};
- const scrollInput = () => {
- const dom = inputRef.current;
- if (!dom) return;
- const paddingBottomNum: number = parseInt(
- window.getComputedStyle(dom).paddingBottom,
- 10,
- );
- dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
- };
-
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
@@ -417,7 +486,6 @@ export function Chat(props: {
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
- scrollInput();
setUserInput(text);
const n = text.trim().length;
@@ -475,21 +543,44 @@ export function Chat(props: {
}
};
- const onResend = (botIndex: number) => {
+ const findLastUesrIndex = (messageId: number) => {
// find last user input message and resend
- for (let i = botIndex; i >= 0; i -= 1) {
- if (messages[i].role === "user") {
- setIsLoading(true);
- chatStore
- .onUserInput(messages[i].content)
- .then(() => setIsLoading(false));
- chatStore.updateCurrentSession((session) =>
- session.messages.splice(i, 2),
- );
- inputRef.current?.focus();
- return;
+ let lastUserMessageIndex: number | null = null;
+ for (let i = 0; i < session.messages.length; i += 1) {
+ const message = session.messages[i];
+ if (message.id === messageId) {
+ break;
+ }
+ if (message.role === "user") {
+ lastUserMessageIndex = i;
}
}
+
+ return lastUserMessageIndex;
+ };
+
+ const deleteMessage = (userIndex: number) => {
+ chatStore.updateCurrentSession((session) =>
+ session.messages.splice(userIndex, 2),
+ );
+ };
+
+ const onDelete = (botMessageId: number) => {
+ const userIndex = findLastUesrIndex(botMessageId);
+ if (userIndex === null) return;
+ deleteMessage(userIndex);
+ };
+
+ const onResend = (botMessageId: number) => {
+ // find last user input message and resend
+ const userIndex = findLastUesrIndex(botMessageId);
+ if (userIndex === null) return;
+
+ setIsLoading(true);
+ const content = session.messages[userIndex].content;
+ deleteMessage(userIndex);
+ chatStore.onUserInput(content).then(() => setIsLoading(false));
+ inputRef.current?.focus();
};
const config = useChatStore((state) => state.config);
@@ -541,6 +632,13 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
+ const renameSession = () => {
+ const newTopic = prompt(Locale.Chat.Rename, session.topic);
+ if (newTopic && newTopic !== session.topic) {
+ chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
+ }
+ };
+
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
@@ -554,14 +652,7 @@ export function Chat(props: {
{
- const newTopic = prompt(Locale.Chat.Rename, session.topic);
- if (newTopic && newTopic !== session.topic) {
- chatStore.updateCurrentSession(
- (session) => (session.topic = newTopic!),
- );
- }
- }}
+ onClickCapture={renameSession}
>
{session.topic}
@@ -578,6 +669,7 @@ export function Chat(props: {
onClick={props?.showSideBar}
/>
+
{!isMobileScreen() && (
)}
+
+
+ }
+ bordered
+ onClick={renameSession}
+ />
+
+
}
@@ -678,12 +779,20 @@ export function Chat(props: {
{Locale.Chat.Actions.Stop}
) : (
- onResend(i)}
- >
- {Locale.Chat.Actions.Retry}
-
+ <>
+ onDelete(message.id ?? i)}
+ >
+ {Locale.Chat.Actions.Delete}
+
+ onResend(message.id ?? i)}
+ >
+ {Locale.Chat.Actions.Retry}
+
+ >
)}
)}
- {(message.preview || message.content.length === 0) &&
- !isUser ? (
-
- ) : (
- onRightClick(e, message)}
- onDoubleClickCapture={() => {
- if (!isMobileScreen()) return;
- setUserInput(message.content);
- }}
- >
-
-
- )}
+ onRightClick(e, message)}
+ onDoubleClickCapture={() => {
+ if (!isMobileScreen()) return;
+ setUserInput(message.content);
+ }}
+ fontSize={fontSize}
+ parentRef={scrollRef}
+ />
{!isUser && !message.preview && (
@@ -726,6 +833,12 @@ export function Chat(props: {
+
+
setShowPromptModal(true)}
+ scrollToBottom={scrollToBottom}
+ hitBottom={hitBottom}
+ />
);
}
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss
index 830e1baeb..b7f095580 100644
--- a/app/components/settings.module.scss
+++ b/app/components/settings.module.scss
@@ -32,3 +32,63 @@
min-width: 80%;
}
}
+
+.user-prompt-modal {
+ min-height: 40vh;
+
+ .user-prompt-search {
+ width: 100%;
+ max-width: 100%;
+ margin-bottom: 10px;
+ background-color: var(--gray);
+ }
+
+ .user-prompt-list {
+ padding: 10px 0;
+
+ .user-prompt-item {
+ margin-bottom: 10px;
+ widows: 100%;
+
+ .user-prompt-header {
+ display: flex;
+ widows: 100%;
+ margin-bottom: 5px;
+
+ .user-prompt-title {
+ flex-grow: 1;
+ max-width: 100%;
+ margin-right: 5px;
+ padding: 5px;
+ font-size: 12px;
+ text-align: left;
+ }
+
+ .user-prompt-buttons {
+ display: flex;
+ align-items: center;
+
+ .user-prompt-button {
+ height: 100%;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+
+ .user-prompt-content {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 5px;
+ margin-right: 10px;
+ font-size: 12px;
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .user-prompt-actions {
+ }
+}
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index 5bb435f20..376b350e3 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useMemo, HTMLProps } from "react";
+import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
@@ -6,12 +6,13 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
+import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
-import { List, ListItem, Popover, showToast } from "./ui-lib";
+import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -26,14 +27,114 @@ import {
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
-import { getEmojiUrl } from "../utils";
+import { copyToClipboard, getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
-import { SearchService, usePromptStore } from "../store/prompt";
-import { requestUsage } from "../requests";
+import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
+function UserPromptModal(props: { onClose?: () => void }) {
+ const promptStore = usePromptStore();
+ const userPrompts = promptStore.getUserPrompts();
+ const builtinPrompts = SearchService.builtinPrompts;
+ const allPrompts = userPrompts.concat(builtinPrompts);
+ const [searchInput, setSearchInput] = useState("");
+ const [searchPrompts, setSearchPrompts] = useState([]);
+ const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
+
+ useEffect(() => {
+ if (searchInput.length > 0) {
+ const searchResult = SearchService.search(searchInput);
+ setSearchPrompts(searchResult);
+ } else {
+ setSearchPrompts([]);
+ }
+ }, [searchInput]);
+
+ return (
+
+
props.onClose?.()}
+ actions={[
+ promptStore.add({ title: "", content: "" })}
+ icon={}
+ bordered
+ text={Locale.Settings.Prompt.Modal.Add}
+ />,
+ ]}
+ >
+
+
setSearchInput(e.currentTarget.value)}
+ >
+
+
+ {prompts.map((v, _) => (
+
+
+
{
+ if (v.isUser) {
+ promptStore.updateUserPrompts(
+ v.id!,
+ (prompt) => (prompt.title = e.currentTarget.value),
+ );
+ }
+ }}
+ >
+
+
+ {v.isUser && (
+ }
+ bordered
+ className={styles["user-prompt-button"]}
+ onClick={() => promptStore.remove(v.id!)}
+ />
+ )}
+ }
+ bordered
+ className={styles["user-prompt-button"]}
+ onClick={() => copyToClipboard(v.content)}
+ />
+
+
+
{
+ if (v.isUser) {
+ promptStore.updateUserPrompts(
+ v.id!,
+ (prompt) => (prompt.content = e.currentTarget.value),
+ );
+ }
+ }}
+ />
+
+ ))}
+
+
+
+
+ );
+}
+
function SettingItem(props: {
title: string;
subTitle?: string;
@@ -99,18 +200,16 @@ export function Settings(props: { closeSettings: () => void }) {
});
}
- const [usage, setUsage] = useState<{
- used?: number;
- subscription?: number;
- }>();
+ const usage = {
+ used: updateStore.used,
+ subscription: updateStore.subscription,
+ };
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
- requestUsage()
- .then((res) => setUsage(res))
- .finally(() => {
- setLoadingUsage(false);
- });
+ updateStore.updateUsage().finally(() => {
+ setLoadingUsage(false);
+ });
}
const accessStore = useAccessStore();
@@ -122,10 +221,12 @@ export function Settings(props: { closeSettings: () => void }) {
const promptStore = usePromptStore();
const builtinCount = SearchService.count.builtin;
- const customCount = promptStore.prompts.size ?? 0;
+ const customCount = promptStore.getUserPrompts().length ?? 0;
+ const [shouldShowPromptModal, setShowPromptModal] = useState(false);
const showUsage = accessStore.isAuthorized();
useEffect(() => {
+ // checks per minutes
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -470,7 +571,7 @@ export function Settings(props: { closeSettings: () => void }) {
}
text={Locale.Settings.Prompt.Edit}
- onClick={() => showToast(Locale.WIP)}
+ onClick={() => setShowPromptModal(true)}
/>
@@ -556,6 +657,10 @@ export function Settings(props: { closeSettings: () => void }) {
>
+
+ {shouldShowPromptModal && (
+ setShowPromptModal(false)} />
+ )}
);
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss
index 457c55049..8965c06a0 100644
--- a/app/components/ui-lib.module.scss
+++ b/app/components/ui-lib.module.scss
@@ -53,7 +53,7 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
- width: 50vw;
+ width: 60vw;
animation: slide-in ease 0.3s;
--modal-padding: 20px;
diff --git a/app/global.d.ts b/app/global.d.ts
new file mode 100644
index 000000000..bd1c062de
--- /dev/null
+++ b/app/global.d.ts
@@ -0,0 +1,11 @@
+declare module "*.jpg";
+declare module "*.png";
+declare module "*.woff2";
+declare module "*.woff";
+declare module "*.ttf";
+declare module "*.scss" {
+ const content: Record
;
+ export default content;
+}
+
+declare module "*.svg";
diff --git a/app/icons/auto.svg b/app/icons/auto.svg
new file mode 100644
index 000000000..6745dfbd0
--- /dev/null
+++ b/app/icons/auto.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/bottom.svg b/app/icons/bottom.svg
new file mode 100644
index 000000000..e2cfba2c7
--- /dev/null
+++ b/app/icons/bottom.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/dark.svg b/app/icons/dark.svg
new file mode 100644
index 000000000..3eebc373e
--- /dev/null
+++ b/app/icons/dark.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/light.svg b/app/icons/light.svg
new file mode 100644
index 000000000..22cfa1fff
--- /dev/null
+++ b/app/icons/light.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/pause.svg b/app/icons/pause.svg
new file mode 100644
index 000000000..382f7a939
--- /dev/null
+++ b/app/icons/pause.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/rename.svg b/app/icons/rename.svg
new file mode 100644
index 000000000..cee69eb8d
--- /dev/null
+++ b/app/icons/rename.svg
@@ -0,0 +1 @@
+
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index d89912233..c2260ef4a 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -18,6 +18,7 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
+ Delete: "删除",
},
Rename: "重命名对话",
Typing: "正在输入…",
@@ -62,10 +63,10 @@ const cn = {
ResetAll: "重置所有选项",
Close: "关闭",
ConfirmResetAll: {
- Confirm: "Are you sure you want to reset all configurations?",
+ Confirm: "确认清除所有配置?",
},
ConfirmClearAll: {
- Confirm: "Are you sure you want to reset all chat?",
+ Confirm: "确认清除所有聊天记录?",
},
},
Lang: {
@@ -78,6 +79,7 @@ const cn = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "头像",
@@ -107,6 +109,11 @@ const cn = {
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom} 条`,
Edit: "编辑",
+ Modal: {
+ Title: "提示词列表",
+ Add: "增加一条",
+ Search: "搜尋提示詞",
+ },
},
HistoryCount: {
Title: "附带历史消息数",
diff --git a/app/locales/de.ts b/app/locales/de.ts
new file mode 100644
index 000000000..e71abfaf7
--- /dev/null
+++ b/app/locales/de.ts
@@ -0,0 +1,189 @@
+import { SubmitKey } from "../store/app";
+import type { LocaleType } from "./index";
+
+const de: LocaleType = {
+ WIP: "In Bearbeitung...",
+ Error: {
+ Unauthorized:
+ "Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
+ },
+ ChatItem: {
+ ChatItemCount: (count: number) => `${count} Nachrichten`,
+ },
+ Chat: {
+ SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
+ Actions: {
+ ChatList: "Zur Chat-Liste gehen",
+ CompressedHistory: "Komprimierter Gedächtnis-Prompt",
+ Export: "Alle Nachrichten als Markdown exportieren",
+ Copy: "Kopieren",
+ Stop: "Stop",
+ Retry: "Wiederholen",
+ Delete: "Delete",
+ },
+ Rename: "Chat umbenennen",
+ Typing: "Tippen...",
+ Input: (submitKey: string) => {
+ var inputHints = `${submitKey} um zu Senden`;
+ if (submitKey === String(SubmitKey.Enter)) {
+ inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
+ }
+ return inputHints + ", / zum Durchsuchen von Prompts";
+ },
+ Send: "Senden",
+ },
+ Export: {
+ Title: "Alle Nachrichten",
+ Copy: "Alles kopieren",
+ Download: "Herunterladen",
+ MessageFromYou: "Deine Nachricht",
+ MessageFromChatGPT: "Nachricht von ChatGPT",
+ },
+ Memory: {
+ Title: "Gedächtnis-Prompt",
+ EmptyContent: "Noch nichts.",
+ Send: "Gedächtnis senden",
+ Copy: "Gedächtnis kopieren",
+ Reset: "Sitzung zurücksetzen",
+ ResetConfirm:
+ "Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
+ },
+ Home: {
+ NewChat: "Neuer Chat",
+ DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
+ DeleteToast: "Chat gelöscht",
+ Revert: "Zurücksetzen",
+ },
+ Settings: {
+ Title: "Einstellungen",
+ SubTitle: "Alle Einstellungen",
+ Actions: {
+ ClearAll: "Alle Daten löschen",
+ ResetAll: "Alle Einstellungen zurücksetzen",
+ Close: "Schließen",
+ ConfirmResetAll: {
+ Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?",
+ },
+ },
+ Lang: {
+ Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
+ Options: {
+ cn: "简体中文",
+ en: "English",
+ tw: "繁體中文",
+ es: "Español",
+ it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
+ },
+ },
+ Avatar: "Avatar",
+ FontSize: {
+ Title: "Schriftgröße",
+ SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
+ },
+ Update: {
+ Version: (x: string) => `Version: ${x}`,
+ IsLatest: "Neueste Version",
+ CheckUpdate: "Update prüfen",
+ IsChecking: "Update wird geprüft...",
+ FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
+ GoToUpdate: "Aktualisieren",
+ },
+ SendKey: "Senden-Taste",
+ Theme: "Erscheinungsbild",
+ TightBorder: "Enger Rahmen",
+ SendPreviewBubble: "Vorschau-Bubble senden",
+ Prompt: {
+ Disable: {
+ Title: "Autovervollständigung deaktivieren",
+ SubTitle: "Autovervollständigung mit / starten",
+ },
+ List: "Prompt-Liste",
+ ListCount: (builtin: number, custom: number) =>
+ `${builtin} integriert, ${custom} benutzerdefiniert`,
+ Edit: "Bearbeiten",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
+ },
+ HistoryCount: {
+ Title: "Anzahl der angehängten Nachrichten",
+ SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
+ },
+ CompressThreshold: {
+ Title: "Schwellenwert für Verlaufskomprimierung",
+ SubTitle:
+ "Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
+ },
+ Token: {
+ Title: "API-Schlüssel",
+ SubTitle:
+ "Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
+ Placeholder: "OpenAI API-Schlüssel",
+ },
+ Usage: {
+ Title: "Kontostand",
+ SubTitle(used: any, total: any) {
+ return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
+ },
+ IsChecking: "Wird überprüft...",
+ Check: "Erneut prüfen",
+ NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
+ },
+ AccessCode: {
+ Title: "Zugangscode",
+ SubTitle: "Zugangskontrolle aktiviert",
+ Placeholder: "Zugangscode erforderlich",
+ },
+ Model: "Modell",
+ Temperature: {
+ Title: "Temperature", //Temperatur
+ SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
+ },
+ MaxTokens: {
+ Title: "Max Tokens", //Maximale Token
+ SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
+ },
+ PresencePenlty: {
+ Title: "Presence Penalty", //Anwesenheitsstrafe
+ SubTitle:
+ "Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
+ },
+ },
+ Store: {
+ DefaultTopic: "Neues Gespräch",
+ BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
+ Error:
+ "Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
+ Prompt: {
+ History: (content: string) =>
+ "Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
+ content,
+ Topic:
+ "Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
+ Summarize:
+ "Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
+ },
+ ConfirmClearAll:
+ "Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?",
+ },
+ Copy: {
+ Success: "In die Zwischenablage kopiert",
+ Failed:
+ "Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
+ },
+ Context: {
+ Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
+ Edit: "Kontext- und Gedächtnis-Prompts",
+ Add: "Hinzufügen",
+ },
+};
+
+export default de;
diff --git a/app/locales/en.ts b/app/locales/en.ts
index bd417aa84..20e569606 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -19,6 +19,7 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
+ Delete: "Delete",
},
Rename: "Rename Chat",
Typing: "Typing…",
@@ -77,6 +78,7 @@ const en: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const en: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Attached Messages Count",
@@ -126,7 +133,7 @@ const en: LocaleType = {
return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
- Check: "Check Again",
+ Check: "Check",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {
diff --git a/app/locales/es.ts b/app/locales/es.ts
index 88bcd2012..e2a9eb211 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
+ Delete: "Delete",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@@ -77,6 +78,7 @@ const es: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const es: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",
diff --git a/app/locales/index.ts b/app/locales/index.ts
index dff1e6614..389304f85 100644
--- a/app/locales/index.ts
+++ b/app/locales/index.ts
@@ -5,10 +5,20 @@ import ES from "./es";
import IT from "./it";
import TR from "./tr";
import JP from "./jp";
+import DE from "./de";
export type { LocaleType } from "./cn";
-export const AllLangs = ["en", "cn", "tw", "es", "it", "tr", "jp"] as const;
+export const AllLangs = [
+ "en",
+ "cn",
+ "tw",
+ "es",
+ "it",
+ "tr",
+ "jp",
+ "de",
+] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang";
@@ -44,21 +54,13 @@ export function getLang(): Lang {
const lang = getLanguage();
- if (lang.includes("zh") || lang.includes("cn")) {
- return "cn";
- } else if (lang.includes("tw")) {
- return "tw";
- } else if (lang.includes("es")) {
- return "es";
- } else if (lang.includes("it")) {
- return "it";
- } else if (lang.includes("tr")) {
- return "tr";
- } else if (lang.includes("jp")) {
- return "jp";
- } else {
- return "en";
+ for (const option of AllLangs) {
+ if (lang.includes(option)) {
+ return option;
+ }
}
+
+ return "en";
}
export function changeLang(lang: Lang) {
@@ -66,6 +68,13 @@ export function changeLang(lang: Lang) {
location.reload();
}
-export default { en: EN, cn: CN, tw: TW, es: ES, it: IT, tr: TR, jp: JP }[
- getLang()
-];
+export default {
+ en: EN,
+ cn: CN,
+ tw: TW,
+ es: ES,
+ it: IT,
+ tr: TR,
+ jp: JP,
+ de: DE,
+}[getLang()] as typeof CN;
diff --git a/app/locales/it.ts b/app/locales/it.ts
index 3cd768fed..f0453b5c3 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -19,6 +19,7 @@ const it: LocaleType = {
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
+ Delete: "Delete",
},
Rename: "Rinomina Chat",
Typing: "Typing…",
@@ -45,12 +46,12 @@ const it: LocaleType = {
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
- "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
+ "Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
- DeleteToast: "Chat Deleted",
+ DeleteToast: "Chat Cancellata",
Revert: "Revert",
},
Settings: {
@@ -77,6 +78,7 @@ const it: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -93,9 +95,9 @@ const it: LocaleType = {
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
- Theme: "tema",
- TightBorder: "Bordi stretti",
- SendPreviewBubble: "Invia l'anteprima della bolla",
+ Theme: "Tema",
+ TightBorder: "Schermo intero",
+ SendPreviewBubble: "Anteprima di digitazione",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
@@ -105,6 +107,11 @@ const it: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Modifica",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Conteggio dei messaggi allegati",
@@ -116,7 +123,7 @@ const it: LocaleType = {
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
- Title: "Chiave API",
+ Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
@@ -124,7 +131,7 @@ const it: LocaleType = {
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
- return `Usato in questo mese $${used}, subscription $${total}`;
+ return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",
diff --git a/app/locales/jp.ts b/app/locales/jp.ts
index 50ac21609..a793b5fe0 100644
--- a/app/locales/jp.ts
+++ b/app/locales/jp.ts
@@ -18,6 +18,7 @@ const jp = {
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
+ Delete: "Delete",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
@@ -76,6 +77,7 @@ const jp = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "アバター",
@@ -106,6 +108,11 @@ const jp = {
ListCount: (builtin: number, custom: number) =>
`組み込み ${builtin} 件、ユーザー定義 ${custom} 件`,
Edit: "編集",
+ Modal: {
+ Title: "提示词列表",
+ Add: "增加一条",
+ Search: "搜尋提示詞",
+ },
},
HistoryCount: {
Title: "履歴メッセージ数を添付",
@@ -177,6 +184,4 @@ const jp = {
},
};
-export type LocaleType = typeof jp;
-
export default jp;
diff --git a/app/locales/tr.ts b/app/locales/tr.ts
index 708d2d7d5..04a846245 100644
--- a/app/locales/tr.ts
+++ b/app/locales/tr.ts
@@ -19,6 +19,7 @@ const tr: LocaleType = {
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
+ Delete: "Delete",
},
Rename: "Sohbeti Yeniden Adlandır",
Typing: "Yazıyor…",
@@ -77,6 +78,7 @@ const tr: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -105,6 +107,11 @@ const tr: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
Edit: "Düzenle",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Ekli Mesaj Sayısı",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 77975b896..2fbb2e477 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -18,6 +18,7 @@ const tw: LocaleType = {
Copy: "複製",
Stop: "停止",
Retry: "重試",
+ Delete: "刪除",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@@ -75,6 +76,7 @@ const tw: LocaleType = {
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "大頭貼",
@@ -103,6 +105,11 @@ const tw: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
Edit: "編輯",
+ Modal: {
+ Title: "提示詞列表",
+ Add: "增加一條",
+ Search: "搜索提示词",
+ },
},
HistoryCount: {
Title: "附帶歷史訊息數",
@@ -152,9 +159,10 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
- Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
+ Topic:
+ "Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
Summarize:
- "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
+ "Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
diff --git a/app/requests.ts b/app/requests.ts
index 6d6666058..834c00d43 100644
--- a/app/requests.ts
+++ b/app/requests.ts
@@ -2,7 +2,7 @@ import type { ChatRequest, ChatResponse } from "./api/openai/typing";
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
import { showToast } from "./components/ui-lib";
-const TIME_OUT_MS = 30000;
+const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
@@ -114,6 +114,10 @@ export async function requestUsage() {
response.total_usage = Math.round(response.total_usage) / 100;
}
+ if (total.hard_limit_usd) {
+ total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
+ }
+
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
@@ -167,7 +171,6 @@ export async function requestChatStream(
options?.onController?.(controller);
while (true) {
- // handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
@@ -189,8 +192,8 @@ export async function requestChatStream(
finish();
} else if (res.status === 401) {
- console.error("Anauthorized");
- options?.onError(new Error("Anauthorized"), res.status);
+ console.error("Unauthorized");
+ options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
@@ -235,6 +238,14 @@ export const ControllerPool = {
controller?.abort();
},
+ stopAll() {
+ Object.values(this.controllers).forEach((v) => v.abort());
+ },
+
+ hasPending() {
+ return Object.values(this.controllers).length > 0;
+ },
+
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
diff --git a/app/store/app.ts b/app/store/app.ts
index efc4ce36a..df56ce129 100644
--- a/app/store/app.ts
+++ b/app/store/app.ts
@@ -386,6 +386,7 @@ export const useChatStore = create()(
const botMessage: Message = createMessage({
role: "assistant",
streaming: true,
+ id: userMessage.id! + 1,
});
// get recent messages
@@ -421,7 +422,7 @@ export const useChatStore = create()(
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
- } else {
+ } else if (!error.message.includes("aborted")) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
diff --git a/app/store/prompt.ts b/app/store/prompt.ts
index d0dd454ac..8d754ff5d 100644
--- a/app/store/prompt.ts
+++ b/app/store/prompt.ts
@@ -5,62 +5,74 @@ import { getLang } from "../locales";
export interface Prompt {
id?: number;
+ isUser?: boolean;
title: string;
content: string;
}
export interface PromptStore {
+ counter: number;
latestId: number;
- prompts: Map;
+ prompts: Record;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
+
+ getUserPrompts: () => Prompt[];
+ updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
- engine: new Fuse([], { keys: ["title"] }),
+ builtinEngine: new Fuse([], { keys: ["title"] }),
+ userEngine: new Fuse([], { keys: ["title"] }),
count: {
builtin: 0,
},
- allBuiltInPrompts: [] as Prompt[],
+ allPrompts: [] as Prompt[],
+ builtinPrompts: [] as Prompt[],
- init(prompts: Prompt[]) {
+ init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
if (this.ready) {
return;
}
- this.allBuiltInPrompts = prompts;
- this.engine.setCollection(prompts);
+ this.allPrompts = userPrompts.concat(builtinPrompts);
+ this.builtinPrompts = builtinPrompts.slice();
+ this.builtinEngine.setCollection(builtinPrompts);
+ this.userEngine.setCollection(userPrompts);
this.ready = true;
},
remove(id: number) {
- this.engine.remove((doc) => doc.id === id);
+ this.userEngine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
- this.engine.add(prompt);
+ this.userEngine.add(prompt);
},
search(text: string) {
- const results = this.engine.search(text);
- return results.map((v) => v.item);
+ const userResults = this.userEngine.search(text);
+ const builtinResults = this.builtinEngine.search(text);
+ return userResults.concat(builtinResults).map((v) => v.item);
},
};
export const usePromptStore = create()(
persist(
(set, get) => ({
+ counter: 0,
latestId: 0,
- prompts: new Map(),
+ prompts: {},
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
- prompts.set(prompt.id, prompt);
+ prompt.isUser = true;
+ prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
@@ -72,19 +84,40 @@ export const usePromptStore = create()(
remove(id) {
const prompts = get().prompts;
- prompts.delete(id);
+ delete prompts[id];
SearchService.remove(id);
set(() => ({
prompts,
+ counter: get().counter + 1,
}));
},
+ getUserPrompts() {
+ const userPrompts = Object.values(get().prompts ?? {});
+ userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
+ return userPrompts;
+ },
+
+ updateUserPrompts(id: number, updater) {
+ const prompt = get().prompts[id] ?? {
+ title: "",
+ content: "",
+ id,
+ };
+
+ SearchService.remove(id);
+ updater(prompt);
+ const prompts = get().prompts;
+ prompts[id] = prompt;
+ set(() => ({ prompts }));
+ SearchService.add(prompt);
+ },
+
search(text) {
if (text.length === 0) {
- // return all prompts
- const userPrompts = get().prompts?.values?.() ?? [];
- return SearchService.allBuiltInPrompts.concat([...userPrompts]);
+ // return all rompts
+ return SearchService.allPrompts.concat([...get().getUserPrompts()]);
}
return SearchService.search(text) as Prompt[];
},
@@ -104,24 +137,27 @@ export const usePromptStore = create()(
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
- const builtinPrompts = fetchPrompts
- .map((promptList: PromptList) => {
+ const builtinPrompts = fetchPrompts.map(
+ (promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
+ id: Math.random(),
title,
content,
} as Prompt),
);
- })
- .concat([...(state?.prompts?.values() ?? [])]);
-
- const allPromptsForSearch = builtinPrompts.reduce(
- (pre, cur) => pre.concat(cur),
- [],
+ },
);
+
+ const userPrompts =
+ usePromptStore.getState().getUserPrompts() ?? [];
+
+ const allPromptsForSearch = builtinPrompts
+ .reduce((pre, cur) => pre.concat(cur), [])
+ .filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
- SearchService.init(allPromptsForSearch);
+ SearchService.init(allPromptsForSearch, userPrompts);
});
},
},
diff --git a/app/store/update.ts b/app/store/update.ts
index efcdc8a7b..47b190b88 100644
--- a/app/store/update.ts
+++ b/app/store/update.ts
@@ -1,13 +1,19 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
+import { requestUsage } from "../requests";
export interface UpdateStore {
lastUpdate: number;
remoteVersion: string;
+ used?: number;
+ subscription?: number;
+ lastUpdateUsage: number;
+
version: string;
- getLatestVersion: (force: boolean) => Promise;
+ getLatestVersion: (force?: boolean) => Promise;
+ updateUsage: (force?: boolean) => Promise;
}
export const UPDATE_KEY = "chat-update";
@@ -26,22 +32,27 @@ function queryMeta(key: string, defaultValue?: string): string {
return ret;
}
+const ONE_MINUTE = 60 * 1000;
+
export const useUpdateStore = create()(
persist(
(set, get) => ({
lastUpdate: 0,
remoteVersion: "",
+ lastUpdateUsage: 0,
+
version: "unknown",
async getLatestVersion(force = false) {
- set(() => ({ version: queryMeta("version") }));
+ set(() => ({ version: queryMeta("version") ?? "unknown" }));
- const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
- const shouldFetch = force || overTenMins;
- if (!shouldFetch) {
- return get().version ?? "unknown";
- }
+ const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
+ if (!force && !overTenMins) return;
+
+ set(() => ({
+ lastUpdate: Date.now(),
+ }));
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
@@ -49,14 +60,26 @@ export const useUpdateStore = create()(
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({
- lastUpdate: Date.now(),
remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
- return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
- return get().version ?? "";
+ }
+ },
+
+ async updateUsage(force = false) {
+ const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
+ if (!overOneMinute && !force) return;
+
+ set(() => ({
+ lastUpdateUsage: Date.now(),
+ }));
+
+ const usage = await requestUsage();
+
+ if (usage) {
+ set(() => usage);
}
},
}),
diff --git a/app/styles/globals.scss b/app/styles/globals.scss
index 53902d935..37c662288 100644
--- a/app/styles/globals.scss
+++ b/app/styles/globals.scss
@@ -1,4 +1,6 @@
@mixin light {
+ --theme: light;
+
/* color */
--white: white;
--black: rgb(48, 48, 48);
@@ -18,6 +20,8 @@
}
@mixin dark {
+ --theme: dark;
+
/* color */
--white: rgb(30, 30, 30);
--black: rgb(187, 187, 187);
@@ -31,6 +35,10 @@
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
+
+ div:not(.no-dark) > svg {
+ filter: invert(0.5);
+ }
}
.light {
@@ -132,6 +140,7 @@ label {
input {
text-align: center;
+ font-family: inherit;
}
input[type="checkbox"] {
@@ -216,6 +225,7 @@ input[type="password"] {
color: var(--black);
padding: 0 10px;
max-width: 50%;
+ font-family: inherit;
}
div.math {
@@ -282,10 +292,6 @@ pre {
.clickable {
cursor: pointer;
- div:not(.no-dark) > svg {
- filter: invert(0.5);
- }
-
&:hover {
filter: brightness(0.9);
}
diff --git a/app/utils.ts b/app/utils.ts
index 5c2b06975..0e4a8eaea 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -120,3 +120,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
return rows;
}
+
+export function getCSSVar(varName: string) {
+ return getComputedStyle(document.body).getPropertyValue(varName).trim();
+}
diff --git a/docs/faq.en.md b/docs/faq-en.md
similarity index 100%
rename from docs/faq.en.md
rename to docs/faq-en.md
diff --git a/docs/images/enable-actions-sync.jpg b/docs/images/enable-actions-sync.jpg
new file mode 100644
index 000000000..4a69da928
Binary files /dev/null and b/docs/images/enable-actions-sync.jpg differ
diff --git a/docs/images/enable-actions.jpg b/docs/images/enable-actions.jpg
new file mode 100644
index 000000000..a4f4f0f1f
Binary files /dev/null and b/docs/images/enable-actions.jpg differ
diff --git a/docs/images/vercel/vercel-create-1.jpg b/docs/images/vercel/vercel-create-1.jpg
new file mode 100644
index 000000000..f0bbd0028
Binary files /dev/null and b/docs/images/vercel/vercel-create-1.jpg differ
diff --git a/docs/images/vercel/vercel-create-2.jpg b/docs/images/vercel/vercel-create-2.jpg
new file mode 100644
index 000000000..157768a88
Binary files /dev/null and b/docs/images/vercel/vercel-create-2.jpg differ
diff --git a/docs/images/vercel/vercel-create-3.jpg b/docs/images/vercel/vercel-create-3.jpg
new file mode 100644
index 000000000..2eaae1f9f
Binary files /dev/null and b/docs/images/vercel/vercel-create-3.jpg differ
diff --git a/docs/images/vercel/vercel-env-edit.jpg b/docs/images/vercel/vercel-env-edit.jpg
new file mode 100644
index 000000000..5b115935e
Binary files /dev/null and b/docs/images/vercel/vercel-env-edit.jpg differ
diff --git a/docs/images/vercel/vercel-redeploy.jpg b/docs/images/vercel/vercel-redeploy.jpg
new file mode 100644
index 000000000..ee3483fa7
Binary files /dev/null and b/docs/images/vercel/vercel-redeploy.jpg differ
diff --git a/docs/vercel-cn.md b/docs/vercel-cn.md
new file mode 100644
index 000000000..c49229694
--- /dev/null
+++ b/docs/vercel-cn.md
@@ -0,0 +1,39 @@
+# Vercel 的使用说明
+
+## 如何新建项目
+当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。
+
+
+1. 进入 Vercel 控制台首页;
+2. 点击 Add New;
+3. 选择 Project。
+
+
+1. 在 Import Git Repository 处,搜索 chatgpt-next-web;
+2. 选中新 fork 的项目,点击 Import。
+
+
+1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
+2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量;
+3. 填入环境变量对应的值;
+4. 点击 Add 确认增加环境变量;
+5. 请确保你添加了 OPENAI_API_KEY,否则无法使用;
+6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。
+
+## 如何增加自定义域名
+[TODO]
+
+## 如何更改环境变量
+
+1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮;
+2. 点击左侧的 Environment Variables;
+3. 点击已有条目的右侧按钮;
+4. 选择 Edit 进行编辑,然后保存即可。
+
+⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效!
+
+## 如何重新部署
+
+1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮;
+2. 选择列表最顶部一条的右侧按钮;
+3. 点击 Redeploy 即可重新部署。
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index 703b42bcd..28172baef 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -8,6 +8,17 @@ export const config = {
const serverConfig = getServerSideConfig();
+function getIP(req: NextRequest) {
+ let ip = req.ip ?? req.headers.get("x-real-ip");
+ const forwardedFor = req.headers.get("x-forwarded-for");
+
+ if (!ip && forwardedFor) {
+ ip = forwardedFor.split(",").at(0) ?? "";
+ }
+
+ return ip;
+}
+
export function middleware(req: NextRequest) {
//console.log("req:",req)
const accessCode = req.headers.get("access-code");
@@ -17,6 +28,8 @@ export function middleware(req: NextRequest) {
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
+ console.log("[User IP] ", getIP(req));
+ console.log("[Time] ", new Date().toLocaleString());
if (!accessCode) {
return NextResponse.json(
diff --git a/package.json b/package.json
index a8d46440a..250d6ed4f 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,8 @@
"start": "next start",
"lint": "next lint",
"fetch": "node ./scripts/fetch-prompts.mjs",
- "prepare": "husky install"
+ "prepare": "husky install",
+ "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
},
"dependencies": {
"@hello-pangea/dnd": "^16.2.0",
@@ -19,7 +20,7 @@
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
- "next": "^13.2.3",
+ "next": "^13.3.1-canary.8",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
diff --git a/scripts/.gitignore b/scripts/.gitignore
new file mode 100644
index 000000000..80fe56c37
--- /dev/null
+++ b/scripts/.gitignore
@@ -0,0 +1 @@
+proxychains.conf
diff --git a/scripts/init-proxy.sh b/scripts/init-proxy.sh
new file mode 100644
index 000000000..acba064f4
--- /dev/null
+++ b/scripts/init-proxy.sh
@@ -0,0 +1,5 @@
+dir="$(dirname "$0")"
+config=$dir/proxychains.conf
+host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
+cp $dir/proxychains.template.conf $config
+sed -i "\$s/.*/http $host_ip 7890/" $config
diff --git a/scripts/proxychains.template.conf b/scripts/proxychains.template.conf
new file mode 100644
index 000000000..e78b96a68
--- /dev/null
+++ b/scripts/proxychains.template.conf
@@ -0,0 +1,12 @@
+strict_chain
+proxy_dns
+
+remote_dns_subnet 224
+
+tcp_read_time_out 15000
+tcp_connect_time_out 8000
+
+localnet 127.0.0.0/255.0.0.0
+
+[ProxyList]
+socks4 127.0.0.1 9050
diff --git a/yarn.lock b/yarn.lock
index eaf5168d8..9090096ef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1099,10 +1099,10 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
-"@next/env@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.4.tgz#8b763700262b2445140a44a8c8d088cef676dbae"
- integrity sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==
+"@next/env@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
+ integrity sha512-xZfNu7yq3OfiC4rkGuGMcqb25se+ZHRqajSdny8dp+nZzkNSK1SHuNT3W8faI+KGk6dqzO/zAdHR9YrqnQlCAg==
"@next/eslint-plugin-next@13.2.3":
version "13.2.3"
@@ -1111,70 +1111,50 @@
dependencies:
glob "7.1.7"
-"@next/swc-android-arm-eabi@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz#758d0403771e549f9cee71cbabc0cb16a6c947c0"
- integrity sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==
+"@next/swc-darwin-arm64@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1-canary.8.tgz#66786ba76d37c210c184739624c6f84eaf2dc52b"
+ integrity sha512-BLbvhcaSzwuXbREOmJiqAdXVD7Jl9830hDY5ZTTNg7hXqEZgoMg2LxAEmtaaBMVZRfDQjd5bH3QPBV8fbG4UKg==
-"@next/swc-android-arm64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz#834d586523045110d5602e0c8aae9028835ac427"
- integrity sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==
+"@next/swc-darwin-x64@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1-canary.8.tgz#289296bd3cc55db7fef42037eb89ce4a6260ba31"
+ integrity sha512-n4tJKPIvFTZshS1TVWrsqaW7h9VW+BmguO/AlZ3Q3NJ9hWxC5L4lxn2T6CTQ4M30Gf+t5u+dPzYLQ5IDtJFnFQ==
-"@next/swc-darwin-arm64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz#5006fca179a36ef3a24d293abadec7438dbb48c6"
- integrity sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==
+"@next/swc-linux-arm64-gnu@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1-canary.8.tgz#dc79e8005849b6482241b460abdce9334665c766"
+ integrity sha512-AxnsgZ56whwVAeejyEZMk8xc8Vapwzb3Zn0YdZzPCR42WKfkcSkM+AWfq33zUOZnjvCmQBDyfHIo4CURVweR6g==
-"@next/swc-darwin-x64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz#6549c7c04322766acc3264ccdb3e1b43fcaf7946"
- integrity sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==
+"@next/swc-linux-arm64-musl@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1-canary.8.tgz#f70873add4aad7ced36f760d1640adc008b7dc03"
+ integrity sha512-zc7rzhtrHMWZ/phvjCNplHGo+ZLembjtluI5J8Xl4iwQQCyZwAtnmQhs37/zkdi6dHZou+wcFBZWRz14awRDBw==
-"@next/swc-freebsd-x64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz#0bbe28979e3e868debc2cc06e45e186ce195b7f4"
- integrity sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==
+"@next/swc-linux-x64-gnu@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1-canary.8.tgz#fe81b8033628c6cf74e154f2db8c8c7f1593008f"
+ integrity sha512-vNbFDiuZ9fWmcznlilDbflZLb04evWPUQlyDT7Tqjd964PlSIaaX3tr64pdYjJOljDaqTr2Kbx0YW74mWF/PEw==
-"@next/swc-linux-arm-gnueabihf@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz#1d28d2203f5a7427d6e7119d7bcb5fc40959fb3e"
- integrity sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==
+"@next/swc-linux-x64-musl@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1-canary.8.tgz#ada4585046a7937f96f2d39fc4aaca12826dde5f"
+ integrity sha512-/FVBPJEBDZYCNraocRWtd5ObAgNi9VFnzJYGYDYIj4jKkFRWWm/CaWu9A7toQACC/JDy262uPyDPathXT9BAqQ==
-"@next/swc-linux-arm64-gnu@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz#eb26448190948cdf4c44b8f34110a3ecea32f1d0"
- integrity sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==
+"@next/swc-win32-arm64-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1-canary.8.tgz#21b4f6c4be61845759753df9313bd9bcbb241969"
+ integrity sha512-8jMwRCeI26yVZLPwG0AjOi4b1yqSeqYmbHA7r+dqiV0OgFdYjnbyHU1FmiKDaC5SnnJN6LWV2Qjer9GDD0Kcuw==
-"@next/swc-linux-arm64-musl@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz#c4227c0acd94a420bb14924820710e6284d234d3"
- integrity sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==
+"@next/swc-win32-ia32-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1-canary.8.tgz#e23192e1d1b1a32b0eb805363b02360c5b523a77"
+ integrity sha512-kcYB9iSEikFhv0I9uQDdgQ2lm8i3O8LA+GhnED9e5VtURBwOSwED7c6ZpaRQBYSPgnEA9/xiJVChICE/I7Ig1g==
-"@next/swc-linux-x64-gnu@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz#6bcb540944ee9b0209b33bfc23b240c2044dfc3e"
- integrity sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==
-
-"@next/swc-linux-x64-musl@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz#ce21e43251eaf09a09df39372b2c3e38028c30ff"
- integrity sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==
-
-"@next/swc-win32-arm64-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz#68220063d8e5e082f5465498675640dedb670ff1"
- integrity sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==
-
-"@next/swc-win32-ia32-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz#7c120ab54a081be9566df310bed834f168252990"
- integrity sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==
-
-"@next/swc-win32-x64-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz#5abda92fe12b9829bf7951c4a221282c56041144"
- integrity sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==
+"@next/swc-win32-x64-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1-canary.8.tgz#a3f29404955cba2193de5e74fd5d9fcfdcb0ab51"
+ integrity sha512-UKrGHonKVWBNg+HI4J8pXE6Jjjl8GwjhygFau71s8M0+jSy99y5Y+nGH9EmMNWKNvrObukyYvrs6OsAusKdCqw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1737,6 +1717,13 @@ browserslist@^4.21.3, browserslist@^4.21.5:
node-releases "^2.0.8"
update-browserslist-db "^1.0.10"
+busboy@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
+ integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
+ dependencies:
+ streamsearch "^1.1.0"
+
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -3944,30 +3931,27 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
-next@^13.2.3:
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/next/-/next-13.2.4.tgz#2363330392b0f7da02ab41301f60857ffa7f67d6"
- integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==
+next@^13.3.1-canary.8:
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/next/-/next-13.3.1-canary.8.tgz#f0846e5eada1491884326786a0749d5adc04c24d"
+ integrity sha512-z4QUgyAN+hSWSEqb4pvGvC3iRktE6NH2DVLU4AvfqNYpzP+prePiJC8HN/cJpFhGW9YbhyRLi5FliDC631OOag==
dependencies:
- "@next/env" "13.2.4"
+ "@next/env" "13.3.1-canary.8"
"@swc/helpers" "0.4.14"
+ busboy "1.6.0"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.1.1"
optionalDependencies:
- "@next/swc-android-arm-eabi" "13.2.4"
- "@next/swc-android-arm64" "13.2.4"
- "@next/swc-darwin-arm64" "13.2.4"
- "@next/swc-darwin-x64" "13.2.4"
- "@next/swc-freebsd-x64" "13.2.4"
- "@next/swc-linux-arm-gnueabihf" "13.2.4"
- "@next/swc-linux-arm64-gnu" "13.2.4"
- "@next/swc-linux-arm64-musl" "13.2.4"
- "@next/swc-linux-x64-gnu" "13.2.4"
- "@next/swc-linux-x64-musl" "13.2.4"
- "@next/swc-win32-arm64-msvc" "13.2.4"
- "@next/swc-win32-ia32-msvc" "13.2.4"
- "@next/swc-win32-x64-msvc" "13.2.4"
+ "@next/swc-darwin-arm64" "13.3.1-canary.8"
+ "@next/swc-darwin-x64" "13.3.1-canary.8"
+ "@next/swc-linux-arm64-gnu" "13.3.1-canary.8"
+ "@next/swc-linux-arm64-musl" "13.3.1-canary.8"
+ "@next/swc-linux-x64-gnu" "13.3.1-canary.8"
+ "@next/swc-linux-x64-musl" "13.3.1-canary.8"
+ "@next/swc-win32-arm64-msvc" "13.3.1-canary.8"
+ "@next/swc-win32-ia32-msvc" "13.3.1-canary.8"
+ "@next/swc-win32-x64-msvc" "13.3.1-canary.8"
node-domexception@^1.0.0:
version "1.0.0"
@@ -4682,6 +4666,11 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
+streamsearch@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
+ integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
+
string-argv@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"