From ec43f4e6ab07dd3c03c7b59045320f3896151b3b Mon Sep 17 00:00:00 2001 From: Gan-Xing <41600413+Gan-Xing@users.noreply.github.com> Date: Thu, 11 May 2023 16:31:42 +0800 Subject: [PATCH 01/24] Update README.md Change My Deployment URL --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90ed7d42f..e0267ec28 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. [演示](https://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/236402186-fa76e930-64f5-47ae-b967-b0f04b1fbf56.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FGan-Xing%2FChatGPT-Next-Web%2F&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) + [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) From f2b81a2f23d3695293577b286ebd42b5678edb7c Mon Sep 17 00:00:00 2001 From: "599153574@qq.com" Date: Fri, 12 May 2023 07:51:04 +0800 Subject: [PATCH 02/24] =?UTF-8?q?add=20french=20translation=20-=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B3=95=E8=AF=AD=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/locales/cn.ts | 1 + app/locales/cs.ts | 14 +-- app/locales/de.ts | 1 + app/locales/en.ts | 1 + app/locales/es.ts | 1 + app/locales/fr.ts | 251 +++++++++++++++++++++++++++++++++++++++++++ app/locales/index.ts | 3 + app/locales/it.ts | 1 + app/locales/jp.ts | 1 + app/locales/ru.ts | 96 +++++++++-------- app/locales/tr.ts | 1 + app/locales/tw.ts | 1 + app/locales/vi.ts | 1 + 13 files changed, 321 insertions(+), 52 deletions(-) create mode 100644 app/locales/fr.ts diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 0cf3b55ec..e586708d3 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -73,6 +73,7 @@ const cn = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index ab9b39a53..ca2c23046 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -70,11 +70,12 @@ const cs: LocaleType = { }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` - All: "Všechny jazyky", - Options: { + All: "Všechny jazyky", + Options: { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", @@ -166,8 +167,7 @@ const cs: LocaleType = { }, PresencePenlty: { Title: "Přítomnostní korekce", - SubTitle: - "Větší hodnota zvyšuje pravděpodobnost nových témat.", + SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.", }, }, Store: { @@ -182,7 +182,7 @@ const cs: LocaleType = { "Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.", Summarize: "Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.", - }, + }, }, Copy: { Success: "Zkopírováno do schránky", @@ -231,7 +231,7 @@ const cs: LocaleType = { More: "Najít více", NotShow: "Nezobrazovat znovu", ConfirmNoShow: "Potvrdit zakázání?Můžete jej povolit později v nastavení.", -}, + }, UI: { Confirm: "Potvrdit", @@ -239,7 +239,7 @@ const cs: LocaleType = { Close: "Zavřít", Create: "Vytvořit", Edit: "Upravit", - } + }, }; export default cs; diff --git a/app/locales/de.ts b/app/locales/de.ts index 8cfe3be45..dd888e7aa 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -76,6 +76,7 @@ const de: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/en.ts b/app/locales/en.ts index 420f16905..ba1381722 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -75,6 +75,7 @@ const en: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/es.ts b/app/locales/es.ts index 5f66071ce..9b6070f65 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -75,6 +75,7 @@ const es: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/fr.ts b/app/locales/fr.ts new file mode 100644 index 000000000..797958514 --- /dev/null +++ b/app/locales/fr.ts @@ -0,0 +1,251 @@ +import { SubmitKey } from "../store/config"; +import type { LocaleType } from "./index"; + +const fr: LocaleType = { + WIP: "Prochainement...", + Error: { + Unauthorized: + "Accès non autorisé, veuillez saisir le code d'accès dans la page des paramètres.", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} messages en total`, + }, + Chat: { + SubTitle: (count: number) => `${count} messages échangés avec ChatGPT`, + Actions: { + ChatList: "Aller à la liste de discussion", + CompressedHistory: "Mémoire d'historique compressée Prompt", + Export: "Exporter tous les messages en tant que Markdown", + Copy: "Copier", + Stop: "Arrêter", + Retry: "Réessayer", + Delete: "Supprimer", + }, + Rename: "Renommer la conversation", + Typing: "En train d'écrire…", + Input: (submitKey: string) => { + var inputHints = `Appuyez sur ${submitKey} pour envoyer`; + if (submitKey === String(SubmitKey.Enter)) { + inputHints += ", Shift + Enter pour insérer un saut de ligne"; + } + return inputHints + ", / pour rechercher des prompts"; + }, + Send: "Envoyer", + Config: { + Reset: "Restaurer les paramètres par défaut", + SaveAs: "Enregistrer en tant que masque", + }, + }, + Export: { + Title: "Tous les messages", + Copy: "Tout sélectionner", + Download: "Télécharger", + MessageFromYou: "Message de votre part", + MessageFromChatGPT: "Message de ChatGPT", + }, + Memory: { + Title: "Prompt mémoire", + EmptyContent: "Rien encore.", + Send: "Envoyer la mémoire", + Copy: "Copier la mémoire", + Reset: "Réinitialiser la session", + ResetConfirm: + "La réinitialisation supprimera l'historique de la conversation actuelle ainsi que la mémoire de l'historique. Êtes-vous sûr de vouloir procéder à la réinitialisation?", + }, + Home: { + NewChat: "Nouvelle discussion", + DeleteChat: "Confirmer la suppression de la conversation sélectionnée ?", + DeleteToast: "Conversation supprimée", + Revert: "Revenir en arrière", + }, + Settings: { + Title: "Paramètres", + SubTitle: "Toutes les configurations", + Actions: { + ClearAll: "Effacer toutes les données", + ResetAll: "Réinitialiser les configurations", + Close: "Fermer", + ConfirmResetAll: + "Êtes-vous sûr de vouloir réinitialiser toutes les configurations?", + ConfirmClearAll: "Êtes-vous sûr de vouloir supprimer toutes les données?", + }, + Lang: { + Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language` + All: "Toutes les langues", + Options: { + cn: "简体中文", + en: "English", + tw: "繁體中文", + fr: "Français", + es: "Español", + it: "Italiano", + tr: "Türkçe", + jp: "日本語", + de: "Deutsch", + vi: "Vietnamese", + ru: "Русский", + cs: "Čeština", + }, + }, + + Avatar: "Avatar", + FontSize: { + Title: "Taille des polices", + SubTitle: "Ajuste la taille de police du contenu de la conversation", + }, + Update: { + Version: (x: string) => `Version : ${x}`, + IsLatest: "Dernière version", + CheckUpdate: "Vérifier la mise à jour", + IsChecking: "Vérification de la mise à jour...", + FoundUpdate: (x: string) => `Nouvelle version disponible : ${x}`, + GoToUpdate: "Mise à jour", + }, + SendKey: "Clé d'envoi", + Theme: "Thème", + TightBorder: "Bordure serrée", + SendPreviewBubble: { + Title: "Aperçu de l'envoi dans une bulle", + SubTitle: "Aperçu du Markdown dans une bulle", + }, + Mask: { + Title: "Écran de masque", + SubTitle: + "Afficher un écran de masque avant de démarrer une nouvelle discussion", + }, + Prompt: { + Disable: { + Title: "Désactiver la saisie semi-automatique", + SubTitle: "Appuyez sur / pour activer la saisie semi-automatique", + }, + List: "Liste de prompts", + ListCount: (builtin: number, custom: number) => + `${builtin} intégré, ${custom} personnalisé`, + Edit: "Modifier", + Modal: { + Title: "Liste de prompts", + Add: "Ajouter un élément", + Search: "Rechercher des prompts", + }, + EditModal: { + Title: "Modifier le prompt", + }, + }, + HistoryCount: { + Title: "Nombre de messages joints", + SubTitle: "Nombre de messages envoyés attachés par demande", + }, + CompressThreshold: { + Title: "Seuil de compression de l'historique", + SubTitle: + "Comprimera si la longueur des messages non compressés dépasse cette valeur", + }, + Token: { + Title: "Clé API", + SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès", + Placeholder: "Clé OpenAI API", + }, + Usage: { + Title: "Solde du compte", + SubTitle(used: any, total: any) { + return `Épuisé ce mois-ci $${used}, abonnement $${total}`; + }, + IsChecking: "Vérification...", + Check: "Vérifier", + NoAccess: "Entrez la clé API pour vérifier le solde", + }, + AccessCode: { + Title: "Code d'accès", + SubTitle: "Contrôle d'accès activé", + Placeholder: "Code d'accès requis", + }, + Model: "Modèle", + Temperature: { + Title: "Température", + SubTitle: "Une valeur plus élevée rendra les réponses plus aléatoires", + }, + MaxTokens: { + Title: "Max Tokens", + SubTitle: "Longueur maximale des tokens d'entrée et des tokens générés", + }, + PresencePenlty: { + Title: "Pénalité de présence", + SubTitle: + "Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets", + }, + }, + Store: { + DefaultTopic: "Nouvelle conversation", + BotHello: "Bonjour ! Comment puis-je vous aider aujourd'hui ?", + Error: "Quelque chose s'est mal passé, veuillez réessayer plus tard.", + Prompt: { + History: (content: string) => + "Ceci est un résumé de l'historique des discussions entre l'IA et l'utilisateur : " + + content, + Topic: + "Veuillez générer un titre de quatre à cinq mots résumant notre conversation sans introduction, ponctuation, guillemets, points, symboles ou texte supplémentaire. Supprimez les guillemets inclus.", + Summarize: + "Résumez brièvement nos discussions en 200 mots ou moins pour les utiliser comme prompt de contexte futur.", + }, + }, + Copy: { + Success: "Copié dans le presse-papiers", + Failed: + "La copie a échoué, veuillez accorder l'autorisation d'accès au presse-papiers", + }, + Context: { + Toast: (x: any) => `Avec ${x} contextes de prompts`, + Edit: "Contextes et mémoires de prompts", + Add: "Ajouter un prompt", + }, + Plugin: { + Name: "Extension", + }, + Mask: { + Name: "Masque", + Page: { + Title: "Modèle de prompt", + SubTitle: (count: number) => `${count} modèles de prompts`, + Search: "Rechercher des modèles", + Create: "Créer", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Discussion", + View: "Vue", + Edit: "Modifier", + Delete: "Supprimer", + DeleteConfirm: "Confirmer la suppression?", + }, + EditModal: { + Title: (readonly: boolean) => + `Modifier le modèle de prompt ${readonly ? "(en lecture seule)" : ""}`, + Download: "Télécharger", + Clone: "Dupliquer", + }, + Config: { + Avatar: "Avatar du bot", + Name: "Nom du bot", + }, + }, + NewChat: { + Return: "Retour", + Skip: "Passer", + Title: "Choisir un masque", + SubTitle: "Discutez avec l'âme derrière le masque", + More: "En savoir plus", + NotShow: "Ne pas afficher à nouveau", + ConfirmNoShow: + "Confirmez-vous vouloir désactiver cela? Vous pouvez le réactiver plus tard dans les paramètres.", + }, + + UI: { + Confirm: "Confirmer", + Cancel: "Annuler", + Close: "Fermer", + Create: "Créer", + Edit: "Éditer", + }, +}; + +export default fr; diff --git a/app/locales/index.ts b/app/locales/index.ts index e41dfcdf7..ac90f7707 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -1,6 +1,7 @@ import CN from "./cn"; import EN from "./en"; import TW from "./tw"; +import FR from "./fr"; import ES from "./es"; import IT from "./it"; import TR from "./tr"; @@ -16,6 +17,7 @@ export const AllLangs = [ "en", "cn", "tw", + "fr", "es", "it", "tr", @@ -80,6 +82,7 @@ export default { en: EN, cn: CN, tw: TW, + fr: FR, es: ES, it: IT, tr: TR, diff --git a/app/locales/it.ts b/app/locales/it.ts index f9daa7a37..313343704 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -75,6 +75,7 @@ const it: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 526574b30..a55734a6c 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -75,6 +75,7 @@ const jp: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index 437a54b2b..49079b743 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -75,6 +75,7 @@ const ru: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", @@ -85,51 +86,52 @@ const ru: LocaleType = { cs: "Čeština", }, }, - Avatar: "Аватар", - FontSize: { - Title: "Размер шрифта", - SubTitle: "Настроить размер шрифта контента чата", + Avatar: "Аватар", + FontSize: { + Title: "Размер шрифта", + SubTitle: "Настроить размер шрифта контента чата", + }, + Update: { + Version: (x: string) => `Версия: ${x}`, + IsLatest: "Последняя версия", + CheckUpdate: "Проверить обновление", + IsChecking: "Проверка обновления...", + FoundUpdate: (x: string) => `Найдена новая версия: ${x}`, + GoToUpdate: "Обновить", + }, + SendKey: "Клавиша отправки", + Theme: "Тема", + TightBorder: "Узкая граница", + SendPreviewBubble: { + Title: "Отправить предпросмотр", + SubTitle: "Предварительный просмотр markdown в пузыре", + }, + Mask: { + Title: "Экран заставки маски", + SubTitle: "Показывать экран заставки маски перед началом нового чата", + }, + Prompt: { + Disable: { + Title: "Отключить автозаполнение", + SubTitle: "Ввод / для запуска автозаполнения", }, - Update: { - Version: (x: string) => `Версия: ${x}`, - IsLatest: "Последняя версия", - CheckUpdate: "Проверить обновление", - IsChecking: "Проверка обновления...", - FoundUpdate: (x: string) => `Найдена новая версия: ${x}`, - GoToUpdate: "Обновить", + List: "Список подсказок", + ListCount: (builtin: number, custom: number) => + `${builtin} встроенных, ${custom} пользовательских`, + Edit: "Редактировать", + Modal: { + Title: "Список подсказок", + Add: "Добавить", + Search: "Поиск подсказок", }, - SendKey: "Клавиша отправки", - Theme: "Тема", - TightBorder: "Узкая граница", - SendPreviewBubble: { - Title: "Отправить предпросмотр", - SubTitle: "Предварительный просмотр markdown в пузыре", + EditModal: { + Title: "Редактировать подсказку", }, - Mask: { - Title: "Экран заставки маски", - SubTitle: "Показывать экран заставки маски перед началом нового чата", - }, - Prompt: { - Disable: { - Title: "Отключить автозаполнение", - SubTitle: "Ввод / для запуска автозаполнения", - }, - List: "Список подсказок", - ListCount: (builtin: number, custom: number) => - `${builtin} встроенных, ${custom} пользовательских`, - Edit: "Редактировать", - Modal: { - Title: "Список подсказок", - Add: "Добавить", - Search: "Поиск подсказок", - }, - EditModal: { - Title: "Редактировать подсказку", - }, - }, - HistoryCount: { - Title: "Количество прикрепляемых сообщений", - SubTitle: "Количество отправляемых сообщений, прикрепляемых к каждому запросу", + }, + HistoryCount: { + Title: "Количество прикрепляемых сообщений", + SubTitle: + "Количество отправляемых сообщений, прикрепляемых к каждому запросу", }, CompressThreshold: { Title: "Порог сжатия истории", @@ -186,7 +188,8 @@ const ru: LocaleType = { }, Copy: { Success: "Скопировано в буфер обмена", - Failed: "Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена", + Failed: + "Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена", }, Context: { Toast: (x: any) => `С ${x} контекстными подсказками`, @@ -214,7 +217,9 @@ const ru: LocaleType = { }, EditModal: { Title: (readonly: boolean) => - `Редактирование шаблона подсказки ${readonly ? "(только для чтения)" : ""}`, + `Редактирование шаблона подсказки ${ + readonly ? "(только для чтения)" : "" + }`, Download: "Скачать", Clone: "Клонировать", }, @@ -230,7 +235,8 @@ const ru: LocaleType = { SubTitle: "Общайтесь с душой за маской", More: "Найти еще", NotShow: "Не показывать снова", - ConfirmNoShow: "Подтвердите отключение? Вы можете включить это позже в настройках.", + ConfirmNoShow: + "Подтвердите отключение? Вы можете включить это позже в настройках.", }, UI: { diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 42c3f78eb..88e76543c 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -75,6 +75,7 @@ const tr: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index a8dbf91a5..d1ab127a5 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -73,6 +73,7 @@ const tw: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index d3be61058..f8b5bebc3 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -75,6 +75,7 @@ const vi: LocaleType = { cn: "简体中文", en: "English", tw: "繁體中文", + fr: "Français", es: "Español", it: "Italiano", tr: "Türkçe", From 881cf082c2928af5956d6d169f11d4ca3c420056 Mon Sep 17 00:00:00 2001 From: Gan-Xing <41600413+Gan-Xing@users.noreply.github.com> Date: Fri, 12 May 2023 10:57:32 +0800 Subject: [PATCH 03/24] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 多国语言支持增加法语 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0267ec28..4d9c84652 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - 预制角色功能(面具),方便地创建、分享和调试你的个性化对话 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 -- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch +- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 ## 开发计划 From 377579e80278fcad9135d500cf30f3d5f13cc117 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 12 May 2023 17:19:40 +0800 Subject: [PATCH 04/24] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 90ed7d42f..b36f4a4a7 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [@jhansion](https://github.com/jhansion) [@Sha1rholder](https://github.com/Sha1rholder) [@AnsonHyq](https://github.com/AnsonHyq) +[@synwith](https://github.com/synwith) ### Contributor From 93c9974019002b57d8184c23d70f68390be316c0 Mon Sep 17 00:00:00 2001 From: wsw <944627549@qq.com> Date: Fri, 12 May 2023 17:54:40 +0800 Subject: [PATCH 05/24] feat: scrolling effect when switching chat windows --- app/components/chat-list.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 02ea086b2..c1365182c 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -16,6 +16,7 @@ import { Link, useNavigate } from "react-router-dom"; import { Path } from "../constant"; import { MaskAvatar } from "./mask"; import { Mask } from "../store/mask"; +import { useRef, useEffect } from "react"; export function ChatItem(props: { onClick?: () => void; @@ -29,6 +30,14 @@ export function ChatItem(props: { narrow?: boolean; mask: Mask; }) { + const draggableRef = useRef(null); + useEffect(() => { + if (props.selected && draggableRef.current) { + draggableRef.current?.scrollIntoView({ + block: "center", + }); + } + }, [props.selected]); return ( {(provided) => ( @@ -37,7 +46,10 @@ export function ChatItem(props: { props.selected && styles["chat-item-selected"] }`} onClick={props.onClick} - ref={provided.innerRef} + ref={(ele) => { + draggableRef.current = ele; + provided.innerRef(ele); + }} {...provided.draggableProps} {...provided.dragHandleProps} title={`${props.title}\n${Locale.ChatItem.ChatItemCount( From 170936a96ef9b59ac8a95d0201c34ef6d9438644 Mon Sep 17 00:00:00 2001 From: Yorun <547747006@qq.com> Date: Fri, 12 May 2023 18:47:41 +0800 Subject: [PATCH 06/24] fix: the theme-color selector --- app/components/home.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 4c3d0a646..6b34a5a1b 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -64,17 +64,17 @@ export function useSwitchTheme() { } const metaDescriptionDark = document.querySelector( - 'meta[name="theme-color"][media]', + 'meta[name="theme-color"][media*="dark"]', ); const metaDescriptionLight = document.querySelector( - 'meta[name="theme-color"]:not([media])', + 'meta[name="theme-color"][media*="light"]', ); if (config.theme === "auto") { metaDescriptionDark?.setAttribute("content", "#151515"); metaDescriptionLight?.setAttribute("content", "#fafafa"); } else { - const themeColor = getCSSVar("--themeColor"); + const themeColor = getCSSVar("--theme-color"); metaDescriptionDark?.setAttribute("content", themeColor); metaDescriptionLight?.setAttribute("content", themeColor); } From dd5604f5d9d21a5e10f9d4609072d7f118463072 Mon Sep 17 00:00:00 2001 From: Yorun <547747006@qq.com> Date: Fri, 12 May 2023 19:23:49 +0800 Subject: [PATCH 07/24] style: move nextjs supported meta tags to metadata --- app/layout.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index f2e765ae0..d08f87e44 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,11 +9,19 @@ const buildConfig = getBuildConfig(); export const metadata = { title: "ChatGPT Next Web", description: "Your personal ChatGPT Chat Bot.", + viewport: { + width: "device-width", + initialScale: 1, + maximumScale: 1, + }, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#fafafa" }, + { media: "(prefers-color-scheme: dark)", color: "#151515" }, + ], appleWebApp: { title: "ChatGPT Next Web", statusBarStyle: "default", }, - viewport: "width=device-width, initial-scale=1, maximum-scale=1", }; export default function RootLayout({ @@ -24,22 +32,12 @@ export default function RootLayout({ return ( - - From dc3fa6c780797b8b969e6b9b485a97e2f1ce6311 Mon Sep 17 00:00:00 2001 From: PaRaD1SE98 Date: Fri, 12 May 2023 22:46:22 +0900 Subject: [PATCH 08/24] Update settings.tsx fix: typo upater -> updater --- app/components/settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 2e08c251e..eb83d8905 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -573,9 +573,9 @@ export function Settings() { { + updateConfig={(updater) => { const modelConfig = { ...config.modelConfig }; - upater(modelConfig); + updater(modelConfig); config.update((config) => (config.modelConfig = modelConfig)); }} /> From a0e192b6e4ed5717e8bcd9ec787a012c73e0a9e2 Mon Sep 17 00:00:00 2001 From: "ShengYan, Zhang" Date: Sat, 13 May 2023 07:23:14 +0800 Subject: [PATCH 09/24] fix: show Vitenamese in it's own language --- app/locales/cn.ts | 2 +- app/locales/cs.ts | 15 ++++---- app/locales/de.ts | 2 +- app/locales/en.ts | 2 +- app/locales/es.ts | 2 +- app/locales/it.ts | 2 +- app/locales/jp.ts | 2 +- app/locales/ru.ts | 97 +++++++++++++++++++++++++---------------------- app/locales/tr.ts | 2 +- app/locales/tw.ts | 2 +- app/locales/vi.ts | 4 +- 11 files changed, 68 insertions(+), 64 deletions(-) diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 0cf3b55ec..b955a7d2b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -78,7 +78,7 @@ const cn = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/cs.ts b/app/locales/cs.ts index ab9b39a53..6d614575e 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -70,8 +70,8 @@ const cs: LocaleType = { }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` - All: "Všechny jazyky", - Options: { + All: "Všechny jazyky", + Options: { cn: "简体中文", en: "English", tw: "繁體中文", @@ -80,7 +80,7 @@ const cs: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, @@ -166,8 +166,7 @@ const cs: LocaleType = { }, PresencePenlty: { Title: "Přítomnostní korekce", - SubTitle: - "Větší hodnota zvyšuje pravděpodobnost nových témat.", + SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.", }, }, Store: { @@ -182,7 +181,7 @@ const cs: LocaleType = { "Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.", Summarize: "Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.", - }, + }, }, Copy: { Success: "Zkopírováno do schránky", @@ -231,7 +230,7 @@ const cs: LocaleType = { More: "Najít více", NotShow: "Nezobrazovat znovu", ConfirmNoShow: "Potvrdit zakázání?Můžete jej povolit později v nastavení.", -}, + }, UI: { Confirm: "Potvrdit", @@ -239,7 +238,7 @@ const cs: LocaleType = { Close: "Zavřít", Create: "Vytvořit", Edit: "Upravit", - } + }, }; export default cs; diff --git a/app/locales/de.ts b/app/locales/de.ts index 8cfe3be45..02510070c 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -81,7 +81,7 @@ const de: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/en.ts b/app/locales/en.ts index 420f16905..b17408bd4 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -80,7 +80,7 @@ const en: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/es.ts b/app/locales/es.ts index 5f66071ce..b80faf23c 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -80,7 +80,7 @@ const es: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/it.ts b/app/locales/it.ts index f9daa7a37..e71121662 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -80,7 +80,7 @@ const it: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 526574b30..dbb9980fe 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -80,7 +80,7 @@ const jp: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/ru.ts b/app/locales/ru.ts index 437a54b2b..6770f5213 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -80,56 +80,57 @@ const ru: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, }, - Avatar: "Аватар", - FontSize: { - Title: "Размер шрифта", - SubTitle: "Настроить размер шрифта контента чата", + Avatar: "Аватар", + FontSize: { + Title: "Размер шрифта", + SubTitle: "Настроить размер шрифта контента чата", + }, + Update: { + Version: (x: string) => `Версия: ${x}`, + IsLatest: "Последняя версия", + CheckUpdate: "Проверить обновление", + IsChecking: "Проверка обновления...", + FoundUpdate: (x: string) => `Найдена новая версия: ${x}`, + GoToUpdate: "Обновить", + }, + SendKey: "Клавиша отправки", + Theme: "Тема", + TightBorder: "Узкая граница", + SendPreviewBubble: { + Title: "Отправить предпросмотр", + SubTitle: "Предварительный просмотр markdown в пузыре", + }, + Mask: { + Title: "Экран заставки маски", + SubTitle: "Показывать экран заставки маски перед началом нового чата", + }, + Prompt: { + Disable: { + Title: "Отключить автозаполнение", + SubTitle: "Ввод / для запуска автозаполнения", }, - Update: { - Version: (x: string) => `Версия: ${x}`, - IsLatest: "Последняя версия", - CheckUpdate: "Проверить обновление", - IsChecking: "Проверка обновления...", - FoundUpdate: (x: string) => `Найдена новая версия: ${x}`, - GoToUpdate: "Обновить", + List: "Список подсказок", + ListCount: (builtin: number, custom: number) => + `${builtin} встроенных, ${custom} пользовательских`, + Edit: "Редактировать", + Modal: { + Title: "Список подсказок", + Add: "Добавить", + Search: "Поиск подсказок", }, - SendKey: "Клавиша отправки", - Theme: "Тема", - TightBorder: "Узкая граница", - SendPreviewBubble: { - Title: "Отправить предпросмотр", - SubTitle: "Предварительный просмотр markdown в пузыре", + EditModal: { + Title: "Редактировать подсказку", }, - Mask: { - Title: "Экран заставки маски", - SubTitle: "Показывать экран заставки маски перед началом нового чата", - }, - Prompt: { - Disable: { - Title: "Отключить автозаполнение", - SubTitle: "Ввод / для запуска автозаполнения", - }, - List: "Список подсказок", - ListCount: (builtin: number, custom: number) => - `${builtin} встроенных, ${custom} пользовательских`, - Edit: "Редактировать", - Modal: { - Title: "Список подсказок", - Add: "Добавить", - Search: "Поиск подсказок", - }, - EditModal: { - Title: "Редактировать подсказку", - }, - }, - HistoryCount: { - Title: "Количество прикрепляемых сообщений", - SubTitle: "Количество отправляемых сообщений, прикрепляемых к каждому запросу", + }, + HistoryCount: { + Title: "Количество прикрепляемых сообщений", + SubTitle: + "Количество отправляемых сообщений, прикрепляемых к каждому запросу", }, CompressThreshold: { Title: "Порог сжатия истории", @@ -186,7 +187,8 @@ const ru: LocaleType = { }, Copy: { Success: "Скопировано в буфер обмена", - Failed: "Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена", + Failed: + "Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена", }, Context: { Toast: (x: any) => `С ${x} контекстными подсказками`, @@ -214,7 +216,9 @@ const ru: LocaleType = { }, EditModal: { Title: (readonly: boolean) => - `Редактирование шаблона подсказки ${readonly ? "(только для чтения)" : ""}`, + `Редактирование шаблона подсказки ${ + readonly ? "(только для чтения)" : "" + }`, Download: "Скачать", Clone: "Клонировать", }, @@ -230,7 +234,8 @@ const ru: LocaleType = { SubTitle: "Общайтесь с душой за маской", More: "Найти еще", NotShow: "Не показывать снова", - ConfirmNoShow: "Подтвердите отключение? Вы можете включить это позже в настройках.", + ConfirmNoShow: + "Подтвердите отключение? Вы можете включить это позже в настройках.", }, UI: { diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 42c3f78eb..e27f28580 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -80,7 +80,7 @@ const tr: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/tw.ts b/app/locales/tw.ts index a8dbf91a5..e115a0f6a 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -78,7 +78,7 @@ const tw: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, diff --git a/app/locales/vi.ts b/app/locales/vi.ts index d3be61058..9568a125a 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -2,7 +2,7 @@ import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const vi: LocaleType = { - WIP: "Coming Soon...", + WIP: "Sắp ra mắt...", 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.", @@ -80,7 +80,7 @@ const vi: LocaleType = { tr: "Türkçe", jp: "日本語", de: "Deutsch", - vi: "Vietnamese", + vi: "Tiếng Việt", ru: "Русский", cs: "Čeština", }, From f07e4fc87fe7a53a0d0b18f3c2a07c653014e5d9 Mon Sep 17 00:00:00 2001 From: "ShengYan, Zhang" Date: Sat, 13 May 2023 09:02:15 +0800 Subject: [PATCH 10/24] docs: add supported languages to README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b36f4a4a7..a3c10a339 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - New in v2: create, share and debug your chat tools with prompt templates (mask) - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) - Automatically compresses chat history to support long conversations while also saving your tokens -- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch +- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština ## Roadmap @@ -62,7 +62,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - 预制角色功能(面具),方便地创建、分享和调试你的个性化对话 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 -- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch +- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 ## 开发计划 From 5b9b120fa6d7823976ff68dd6147a7e002aa7c42 Mon Sep 17 00:00:00 2001 From: InitialXKO <45725592+InitialXKO@users.noreply.github.com> Date: Sat, 13 May 2023 09:36:04 +0800 Subject: [PATCH 11/24] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E7=94=9F?= =?UTF-8?q?=E5=9B=BE=E9=9D=A2=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/masks/cn.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/app/masks/cn.ts b/app/masks/cn.ts index 757a35ee9..86af414bb 100644 --- a/app/masks/cn.ts +++ b/app/masks/cn.ts @@ -1,6 +1,38 @@ import { BuiltinMask } from "./typing"; -export const CN_MASKS: BuiltinMask[] = [ +export const CN_MASKS: BuiltinMask[] = [{ + avatar:"gpt-bot", + name:"文生图", + context: [ + {role:"system", + content:"You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block.", + date:"", + }, + {role:"user", + content:"Can you draw some pictures for me?", + date:"", + }, + {role:"assistant", + content:"Sure, what do you want me to draw?", + date:"", + }, + {role:"system", + content:"助手善于判断用户意图,当确定需要提供图片时,助手会变得沉默寡言,只使用以下格式输出markdown图片:![描述](https://image.pollinations.ai/prompt/描述),因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示,以大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记,因为那样只会渲染出代码块或原始块而不是图片。", + date:"", + }, + ], + modelConfig:{ + model:"gpt-3.5-turbo", + temperature:1, + max_tokens:2000, + presence_penalty:0, + sendMemory:true, + historyMessageCount:32, + compressMessageLengthThreshold:1000, + }, + lang:"cn", + builtin:true, + }, { avatar: "1f638", name: "文案写手", From a9f000e7ef7f0713d474f4e8a8801ce4ee7bc19f Mon Sep 17 00:00:00 2001 From: PaRaD1SE98 Date: Sun, 14 May 2023 01:24:20 +0900 Subject: [PATCH 12/24] remove error messages in toBeSummarizedMsgs --- app/store/chat.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index cb11087d4..b8cdecb60 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -423,7 +423,10 @@ export const useChatStore = create()( let toBeSummarizedMsgs = session.messages.slice( session.lastSummarizeIndex, ); - + + // remove error messages if any + toBeSummarizedMsgs = toBeSummarizedMsgs.filter((msg) => !msg.isError); + const historyMsgLength = countMessages(toBeSummarizedMsgs); if (historyMsgLength > modelConfig?.max_tokens ?? 4000) { From ff2589c97f00abe30d389cc5e71a6b63c561a6c1 Mon Sep 17 00:00:00 2001 From: PaRaD1SE98 Date: Sun, 14 May 2023 02:34:32 +0900 Subject: [PATCH 13/24] remove error messages for chat title summary --- app/store/chat.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index b8cdecb60..d9a4af781 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -402,14 +402,17 @@ export const useChatStore = create()( summarizeSession() { const session = get().currentSession(); + + // remove error messages if any + const cleanMessages = session.messages.filter((msg) => !msg.isError); // should summarize topic after chating more than 50 words const SUMMARIZE_MIN_LEN = 50; if ( session.topic === DEFAULT_TOPIC && - countMessages(session.messages) >= SUMMARIZE_MIN_LEN + countMessages(cleanMessages) >= SUMMARIZE_MIN_LEN ) { - requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, { + requestWithPrompt(cleanMessages, Locale.Store.Prompt.Topic, { model: "gpt-3.5-turbo", }).then((res) => { get().updateCurrentSession( @@ -420,12 +423,9 @@ export const useChatStore = create()( } const modelConfig = session.mask.modelConfig; - let toBeSummarizedMsgs = session.messages.slice( + let toBeSummarizedMsgs = cleanMessages.slice( session.lastSummarizeIndex, ); - - // remove error messages if any - toBeSummarizedMsgs = toBeSummarizedMsgs.filter((msg) => !msg.isError); const historyMsgLength = countMessages(toBeSummarizedMsgs); From 6da3aab046d85825cf195bf2074465471f7fc481 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 14 May 2023 02:21:35 +0800 Subject: [PATCH 14/24] fix: #1423 should not scroll right when dragging side bar items --- app/components/home.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 247d70b9e..1ce95af8f 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -186,7 +186,7 @@ .chat-item-delete { position: absolute; top: 10px; - right: -20px; + right: 0; transition: all ease 0.3s; opacity: 0; cursor: pointer; @@ -194,7 +194,7 @@ .chat-item:hover > .chat-item-delete { opacity: 0.5; - right: 10px; + transform: translateX(-10px); } .chat-item:hover > .chat-item-delete:hover { From bd90caa99d1501bbbd75cc722e185e9266973d9b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 14 May 2023 23:00:17 +0800 Subject: [PATCH 15/24] refactor: llm client api --- app/client/api.ts | 109 +++++++++++++++++++++++++++++ app/client/controller.ts | 37 ++++++++++ app/client/platforms/openai.ts | 124 +++++++++++++++++++++++++++++++++ app/constant.ts | 2 + app/requests.ts | 22 ------ app/store/chat.ts | 1 + package.json | 1 + yarn.lock | 5 ++ 8 files changed, 279 insertions(+), 22 deletions(-) create mode 100644 app/client/api.ts create mode 100644 app/client/controller.ts create mode 100644 app/client/platforms/openai.ts diff --git a/app/client/api.ts b/app/client/api.ts new file mode 100644 index 000000000..103e95e53 --- /dev/null +++ b/app/client/api.ts @@ -0,0 +1,109 @@ +import { fetchEventSource } from "@microsoft/fetch-event-source"; +import { ACCESS_CODE_PREFIX } from "../constant"; +import { ModelType, useAccessStore } from "../store"; +import { ChatGPTApi } from "./platforms/openai"; + +export enum MessageRole { + System = "system", + User = "user", + Assistant = "assistant", +} + +export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; +export type ChatModel = ModelType; + +export interface Message { + role: MessageRole; + content: string; +} + +export interface LLMConfig { + temperature?: number; + topP?: number; + stream?: boolean; + presencePenalty?: number; + frequencyPenalty?: number; +} + +export interface ChatOptions { + messages: Message[]; + model: ChatModel; + config: LLMConfig; + + onUpdate: (message: string, chunk: string) => void; + onFinish: (message: string) => void; + onError: (err: Error) => void; + onUnAuth: () => void; +} + +export interface LLMUsage { + used: number; + total: number; +} + +export abstract class LLMApi { + abstract chat(options: ChatOptions): Promise; + abstract usage(): Promise; +} + +export class ClientApi { + public llm: LLMApi; + + constructor() { + this.llm = new ChatGPTApi(); + } + + headers() { + const accessStore = useAccessStore.getState(); + let headers: Record = {}; + + const makeBearer = (token: string) => `Bearer ${token.trim()}`; + const validString = (x: string) => x && x.length > 0; + + // use user's api key first + if (validString(accessStore.token)) { + headers.Authorization = makeBearer(accessStore.token); + } else if ( + accessStore.enabledAccessControl() && + validString(accessStore.accessCode) + ) { + headers.Authorization = makeBearer( + ACCESS_CODE_PREFIX + accessStore.accessCode, + ); + } + + return headers; + } + + config() {} + + prompts() {} + + masks() {} +} + +export const api = new ClientApi(); + +export function getHeaders() { + const accessStore = useAccessStore.getState(); + let headers: Record = { + "Content-Type": "application/json", + }; + + const makeBearer = (token: string) => `Bearer ${token.trim()}`; + const validString = (x: string) => x && x.length > 0; + + // use user's api key first + if (validString(accessStore.token)) { + headers.Authorization = makeBearer(accessStore.token); + } else if ( + accessStore.enabledAccessControl() && + validString(accessStore.accessCode) + ) { + headers.Authorization = makeBearer( + ACCESS_CODE_PREFIX + accessStore.accessCode, + ); + } + + return headers; +} diff --git a/app/client/controller.ts b/app/client/controller.ts new file mode 100644 index 000000000..86cb99e7f --- /dev/null +++ b/app/client/controller.ts @@ -0,0 +1,37 @@ +// To store message streaming controller +export const ChatControllerPool = { + controllers: {} as Record, + + addController( + sessionIndex: number, + messageId: number, + controller: AbortController, + ) { + const key = this.key(sessionIndex, messageId); + this.controllers[key] = controller; + return key; + }, + + stop(sessionIndex: number, messageId: number) { + const key = this.key(sessionIndex, messageId); + const controller = this.controllers[key]; + 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]; + }, + + key(sessionIndex: number, messageIndex: number) { + return `${sessionIndex},${messageIndex}`; + }, +}; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts new file mode 100644 index 000000000..7d4d94da6 --- /dev/null +++ b/app/client/platforms/openai.ts @@ -0,0 +1,124 @@ +import { REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { + EventStreamContentType, + fetchEventSource, +} from "@microsoft/fetch-event-source"; +import { ChatOptions, LLMApi, LLMUsage } from "../api"; + +export class ChatGPTApi implements LLMApi { + public ChatPath = "v1/chat/completions"; + + path(path: string): string { + const openaiUrl = useAccessStore.getState().openaiUrl; + if (openaiUrl.endsWith("/")) openaiUrl.slice(0, openaiUrl.length - 1); + return [openaiUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + async chat(options: ChatOptions) { + const messages = options.messages.map((v) => ({ + role: v.role, + content: v.content, + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.model, + }, + }; + + const requestPayload = { + messages, + stream: options.config.stream, + model: modelConfig.model, + temperature: modelConfig.temperature, + presence_penalty: modelConfig.presence_penalty, + }; + + console.log("[Request] openai payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + + try { + const chatPath = this.path(this.ChatPath); + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + }; + + // make a fetch request + const reqestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + if (shouldStream) { + let responseText = ""; + + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + if ( + res.ok && + res.headers.get("Content-Type") === EventStreamContentType + ) { + return; + } + + if (res.status === 401) { + // TODO: Unauthorized 401 + responseText += "\n\n"; + } else if (res.status !== 200) { + console.error("[Request] response", res); + throw new Error("[Request] server error"); + } + }, + onmessage: (ev) => { + if (ev.data === "[DONE]") { + return options.onFinish(responseText); + } + try { + const resJson = JSON.parse(ev.data); + const message = this.extractMessage(resJson); + responseText += message; + options.onUpdate(responseText, message); + } catch (e) { + console.error("[Request] stream error", e); + options.onError(e as Error); + } + }, + onclose() { + options.onError(new Error("stream closed unexpected")); + }, + onerror(err) { + options.onError(err); + }, + }); + } else { + const res = await fetch(chatPath, chatPayload); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + + clearTimeout(reqestTimeoutId); + } catch (e) { + console.log("[Request] failed to make a chat reqeust", e); + options.onError(e as Error); + } + } + async usage() { + return { + used: 0, + total: 0, + } as LLMUsage; + } +} diff --git a/app/constant.ts b/app/constant.ts index d0f9fc743..577c0af69 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -40,3 +40,5 @@ export const NARROW_SIDEBAR_WIDTH = 100; export const ACCESS_CODE_PREFIX = "ak-"; export const LAST_INPUT_KEY = "last-input"; + +export const REQUEST_TIMEOUT_MS = 60000; diff --git a/app/requests.ts b/app/requests.ts index d9750a5b7..df81b4f9a 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -43,28 +43,6 @@ const makeRequestParam = ( }; }; -export function getHeaders() { - const accessStore = useAccessStore.getState(); - let headers: Record = {}; - - const makeBearer = (token: string) => `Bearer ${token.trim()}`; - const validString = (x: string) => x && x.length > 0; - - // use user's api key first - if (validString(accessStore.token)) { - headers.Authorization = makeBearer(accessStore.token); - } else if ( - accessStore.enabledAccessControl() && - validString(accessStore.accessCode) - ) { - headers.Authorization = makeBearer( - ACCESS_CODE_PREFIX + accessStore.accessCode, - ); - } - - return headers; -} - export function requestOpenaiClient(path: string) { const openaiUrl = useAccessStore.getState().openaiUrl; return (body: any, method = "POST") => diff --git a/app/store/chat.ts b/app/store/chat.ts index cb11087d4..17cf77072 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -14,6 +14,7 @@ import { showToast } from "../components/ui-lib"; import { ModelType } from "./config"; import { createEmptyMask, Mask } from "./mask"; import { StoreKey } from "../constant"; +import { api } from "../client/api"; export type Message = ChatCompletionResponseMessage & { date: string; diff --git a/package.json b/package.json index 2f194174f..6b13b9b6c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@hello-pangea/dnd": "^16.2.0", + "@microsoft/fetch-event-source": "^2.0.1", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "emoji-picker-react": "^4.4.7", diff --git a/yarn.lock b/yarn.lock index 22610c6af..a6695acba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,6 +1111,11 @@ dependencies: "@types/react" ">=16.0.0" +"@microsoft/fetch-event-source@^2.0.1": + version "2.0.1" + resolved "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" + integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== + "@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" From 03163d6a61856dbe52f156d89da80a2ce9f7cb79 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 14 May 2023 23:25:22 +0800 Subject: [PATCH 16/24] fix: #1444 async load google fonts --- app/components/home.tsx | 13 ++++++++++++- app/layout.tsx | 5 ----- next.config.mjs | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 6b34a5a1b..810c9fa12 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -23,7 +23,6 @@ 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 ( @@ -91,12 +90,24 @@ const useHasHydrated = () => { return hasHydrated; }; +const loadAsyncGoogleFont = () => { + const linkEl = document.createElement("link"); + linkEl.rel = "stylesheet"; + linkEl.href = + "/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"; + document.head.appendChild(linkEl); +}; + function Screen() { const config = useAppConfig(); const location = useLocation(); const isHome = location.pathname === Path.Home; const isMobileScreen = useMobileScreen(); + useEffect(() => { + loadAsyncGoogleFont(); + }, []); + return (
- - {children} diff --git a/next.config.mjs b/next.config.mjs index c62f88409..da23fd21b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,10 @@ const nextConfig = { source: "/api/proxy/:path*", destination: "https://api.openai.com/:path*", }, + { + source: "/google-fonts/:path*", + destination: "https://fonts.googleapis.com/:path*", + }, ]; const apiUrl = process.env.API_URL; From a3de277c437275519bcb45ce30ba58b7561a4e53 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 15 May 2023 01:33:46 +0800 Subject: [PATCH 17/24] refactor: #1000 #1179 api layer for client-side only mode and local models --- app/api/openai/[...path]/route.ts | 77 +-------- app/api/openai/typing.ts | 9 - app/client/api.ts | 50 ++---- app/client/platforms/openai.ts | 158 ++++++++++++------ app/components/chat.tsx | 16 +- app/components/mask.tsx | 17 +- app/requests.ts | 263 ------------------------------ app/store/access.ts | 2 +- app/store/chat.ts | 146 +++++++++-------- app/store/mask.ts | 4 +- app/store/update.ts | 18 +- app/typing.ts | 1 + app/utils/format.ts | 8 + package.json | 3 - yarn.lock | 68 -------- 15 files changed, 247 insertions(+), 593 deletions(-) delete mode 100644 app/api/openai/typing.ts delete mode 100644 app/requests.ts create mode 100644 app/typing.ts create mode 100644 app/utils/format.ts diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts index 1ca103c64..981749e7e 100644 --- a/app/api/openai/[...path]/route.ts +++ b/app/api/openai/[...path]/route.ts @@ -1,49 +1,8 @@ -import { createParser } from "eventsource-parser"; +import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "../../auth"; import { requestOpenai } from "../../common"; -async function createStream(res: Response) { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - const stream = new ReadableStream({ - async start(controller) { - function onParse(event: any) { - if (event.type === "event") { - const data = event.data; - // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream - if (data === "[DONE]") { - controller.close(); - return; - } - try { - const json = JSON.parse(data); - const text = json.choices[0].delta.content; - const queue = encoder.encode(text); - controller.enqueue(queue); - } catch (e) { - controller.error(e); - } - } - } - - const parser = createParser(onParse); - for await (const chunk of res.body as any) { - parser.feed(decoder.decode(chunk, { stream: true })); - } - }, - }); - return stream; -} - -function formatResponse(msg: any) { - const jsonMsg = ["```json\n", JSON.stringify(msg, null, " "), "\n```"].join( - "", - ); - return new Response(jsonMsg); -} - async function handle( req: NextRequest, { params }: { params: { path: string[] } }, @@ -58,40 +17,10 @@ async function handle( } try { - const api = await requestOpenai(req); - - const contentType = api.headers.get("Content-Type") ?? ""; - - // streaming response - if (contentType.includes("stream")) { - const stream = await createStream(api); - const res = new Response(stream); - res.headers.set("Content-Type", contentType); - return res; - } - - // try to parse error msg - try { - const mayBeErrorBody = await api.json(); - if (mayBeErrorBody.error) { - console.error("[OpenAI Response] ", mayBeErrorBody); - return formatResponse(mayBeErrorBody); - } else { - const res = new Response(JSON.stringify(mayBeErrorBody)); - res.headers.set("Content-Type", "application/json"); - res.headers.set("Cache-Control", "no-cache"); - return res; - } - } catch (e) { - console.error("[OpenAI Parse] ", e); - return formatResponse({ - msg: "invalid response from openai server", - error: e, - }); - } + return await requestOpenai(req); } catch (e) { console.error("[OpenAI] ", e); - return formatResponse(e); + return NextResponse.json(prettyObject(e)); } } diff --git a/app/api/openai/typing.ts b/app/api/openai/typing.ts deleted file mode 100644 index 2286d2312..000000000 --- a/app/api/openai/typing.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { - CreateChatCompletionRequest, - CreateChatCompletionResponse, -} from "openai"; - -export type ChatRequest = CreateChatCompletionRequest; -export type ChatResponse = CreateChatCompletionResponse; - -export type Updater = (updater: (value: T) => void) => void; diff --git a/app/client/api.ts b/app/client/api.ts index 103e95e53..c76fab57f 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,39 +1,35 @@ -import { fetchEventSource } from "@microsoft/fetch-event-source"; import { ACCESS_CODE_PREFIX } from "../constant"; -import { ModelType, useAccessStore } from "../store"; +import { ModelConfig, ModelType, useAccessStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; -export enum MessageRole { - System = "system", - User = "user", - Assistant = "assistant", -} +export const ROLES = ["system", "user", "assistant"] as const; +export type MessageRole = (typeof ROLES)[number]; export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; export type ChatModel = ModelType; -export interface Message { +export interface RequestMessage { role: MessageRole; content: string; } export interface LLMConfig { + model: string; temperature?: number; - topP?: number; + top_p?: number; stream?: boolean; - presencePenalty?: number; - frequencyPenalty?: number; + presence_penalty?: number; + frequency_penalty?: number; } export interface ChatOptions { - messages: Message[]; - model: ChatModel; + messages: RequestMessage[]; config: LLMConfig; - onUpdate: (message: string, chunk: string) => void; + onUpdate?: (message: string, chunk: string) => void; onFinish: (message: string) => void; - onError: (err: Error) => void; - onUnAuth: () => void; + onError?: (err: Error) => void; + onController?: (controller: AbortController) => void; } export interface LLMUsage { @@ -53,28 +49,6 @@ export class ClientApi { this.llm = new ChatGPTApi(); } - headers() { - const accessStore = useAccessStore.getState(); - let headers: Record = {}; - - const makeBearer = (token: string) => `Bearer ${token.trim()}`; - const validString = (x: string) => x && x.length > 0; - - // use user's api key first - if (validString(accessStore.token)) { - headers.Authorization = makeBearer(accessStore.token); - } else if ( - accessStore.enabledAccessControl() && - validString(accessStore.accessCode) - ) { - headers.Authorization = makeBearer( - ACCESS_CODE_PREFIX + accessStore.accessCode, - ); - } - - return headers; - } - config() {} prompts() {} diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 7d4d94da6..4b8c1cf43 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,13 +1,13 @@ import { REQUEST_TIMEOUT_MS } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; -import { - EventStreamContentType, - fetchEventSource, -} from "@microsoft/fetch-event-source"; -import { ChatOptions, LLMApi, LLMUsage } from "../api"; + +import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; +import Locale from "../../locales"; export class ChatGPTApi implements LLMApi { public ChatPath = "v1/chat/completions"; + public UsagePath = "dashboard/billing/usage"; + public SubsPath = "dashboard/billing/subscription"; path(path: string): string { const openaiUrl = useAccessStore.getState().openaiUrl; @@ -29,7 +29,7 @@ export class ChatGPTApi implements LLMApi { ...useAppConfig.getState().modelConfig, ...useChatStore.getState().currentSession().mask.modelConfig, ...{ - model: options.model, + model: options.config.model, }, }; @@ -45,6 +45,7 @@ export class ChatGPTApi implements LLMApi { const shouldStream = !!options.config.stream; const controller = new AbortController(); + options.onController?.(controller); try { const chatPath = this.path(this.ChatPath); @@ -52,6 +53,7 @@ export class ChatGPTApi implements LLMApi { method: "POST", body: JSON.stringify(requestPayload), signal: controller.signal, + headers: getHeaders(), }; // make a fetch request @@ -59,66 +61,128 @@ export class ChatGPTApi implements LLMApi { () => controller.abort(), REQUEST_TIMEOUT_MS, ); + if (shouldStream) { let responseText = ""; - fetchEventSource(chatPath, { - ...chatPayload, - async onopen(res) { - if ( - res.ok && - res.headers.get("Content-Type") === EventStreamContentType - ) { - return; - } + const finish = () => { + options.onFinish(responseText); + }; - if (res.status === 401) { - // TODO: Unauthorized 401 - responseText += "\n\n"; - } else if (res.status !== 200) { - console.error("[Request] response", res); - throw new Error("[Request] server error"); + const res = await fetch(chatPath, chatPayload); + clearTimeout(reqestTimeoutId); + + if (res.status === 401) { + responseText += "\n\n" + Locale.Error.Unauthorized; + return finish(); + } + + if ( + !res.ok || + !res.headers.get("Content-Type")?.includes("stream") || + !res.body + ) { + return options.onError?.(new Error()); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + return finish(); + } + + const chunk = decoder.decode(value); + const lines = chunk.split("data: "); + + for (const line of lines) { + const text = line.trim(); + if (line.startsWith("[DONE]")) { + return finish(); } - }, - onmessage: (ev) => { - if (ev.data === "[DONE]") { - return options.onFinish(responseText); + if (text.length === 0) continue; + const json = JSON.parse(text); + const delta = json.choices[0].delta.content; + if (delta) { + responseText += delta; + options.onUpdate?.(responseText, delta); } - try { - const resJson = JSON.parse(ev.data); - const message = this.extractMessage(resJson); - responseText += message; - options.onUpdate(responseText, message); - } catch (e) { - console.error("[Request] stream error", e); - options.onError(e as Error); - } - }, - onclose() { - options.onError(new Error("stream closed unexpected")); - }, - onerror(err) { - options.onError(err); - }, - }); + } + } } else { const res = await fetch(chatPath, chatPayload); + clearTimeout(reqestTimeoutId); const resJson = await res.json(); const message = this.extractMessage(resJson); options.onFinish(message); } - - clearTimeout(reqestTimeoutId); } catch (e) { console.log("[Request] failed to make a chat reqeust", e); - options.onError(e as Error); + options.onError?.(e as Error); } } async usage() { + const formatDate = (d: Date) => + `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d + .getDate() + .toString() + .padStart(2, "0")}`; + const ONE_DAY = 1 * 24 * 60 * 60 * 1000; + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startDate = formatDate(startOfMonth); + const endDate = formatDate(new Date(Date.now() + ONE_DAY)); + + const [used, subs] = await Promise.all([ + fetch( + this.path( + `${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`, + ), + { + method: "GET", + headers: getHeaders(), + }, + ), + fetch(this.path(this.SubsPath), { + method: "GET", + headers: getHeaders(), + }), + ]); + + if (!used.ok || !subs.ok || used.status === 401) { + throw new Error(Locale.Error.Unauthorized); + } + + const response = (await used.json()) as { + total_usage?: number; + error?: { + type: string; + message: string; + }; + }; + + const total = (await subs.json()) as { + hard_limit_usd?: number; + }; + + if (response.error && response.error.type) { + throw Error(response.error.message); + } + + if (response.total_usage) { + 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: 0, - total: 0, + used: response.total_usage, + total: total.hard_limit_usd, } as LLMUsage; } } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index d38990372..94baf1b66 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -22,7 +22,7 @@ import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; import { - Message, + ChatMessage, SubmitKey, useChatStore, BOT_HELLO, @@ -43,7 +43,7 @@ import { import dynamic from "next/dynamic"; -import { ControllerPool } from "../requests"; +import { ChatControllerPool } from "../client/controller"; import { Prompt, usePromptStore } from "../store/prompt"; import Locale from "../locales"; @@ -63,7 +63,7 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); -function exportMessages(messages: Message[], topic: string) { +function exportMessages(messages: ChatMessage[], topic: string) { const mdText = `# ${topic}\n\n` + messages @@ -331,8 +331,8 @@ export function ChatActions(props: { } // stop all responses - const couldStop = ControllerPool.hasPending(); - const stopAll = () => ControllerPool.stopAll(); + const couldStop = ChatControllerPool.hasPending(); + const stopAll = () => ChatControllerPool.stopAll(); return (
@@ -394,7 +394,7 @@ export function ChatActions(props: { } export function Chat() { - type RenderMessage = Message & { preview?: boolean }; + type RenderMessage = ChatMessage & { preview?: boolean }; const chatStore = useChatStore(); const [session, sessionIndex] = useChatStore((state) => [ @@ -487,7 +487,7 @@ export function Chat() { // stop response const onUserStop = (messageId: number) => { - ControllerPool.stop(sessionIndex, messageId); + ChatControllerPool.stop(sessionIndex, messageId); }; // check if should send message @@ -507,7 +507,7 @@ export function Chat() { e.preventDefault(); } }; - const onRightClick = (e: any, message: Message) => { + const onRightClick = (e: any, message: ChatMessage) => { // copy to clipboard if (selectOrCopy(e.currentTarget, message.content)) { e.preventDefault(); diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 13ffb9ef6..adb5d448b 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -13,7 +13,8 @@ import EyeIcon from "../icons/eye.svg"; import CopyIcon from "../icons/copy.svg"; import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask"; -import { Message, ModelConfig, ROLES, useChatStore } from "../store"; +import { ChatMessage, ModelConfig, useChatStore } from "../store"; +import { ROLES } from "../client/api"; import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib"; import { Avatar, AvatarPicker } from "./emoji"; import Locale, { AllLangs, Lang } from "../locales"; @@ -22,7 +23,7 @@ import { useNavigate } from "react-router-dom"; import chatStyle from "./chat.module.scss"; import { useState } from "react"; import { downloadAs, readFromFile } from "../utils"; -import { Updater } from "../api/openai/typing"; +import { Updater } from "../typing"; import { ModelConfigList } from "./model-config"; import { FileName, Path } from "../constant"; import { BUILTIN_MASK_STORE } from "../masks"; @@ -107,8 +108,8 @@ export function MaskConfig(props: { } function ContextPromptItem(props: { - prompt: Message; - update: (prompt: Message) => void; + prompt: ChatMessage; + update: (prompt: ChatMessage) => void; remove: () => void; }) { const [focusingInput, setFocusingInput] = useState(false); @@ -160,12 +161,12 @@ function ContextPromptItem(props: { } export function ContextPrompts(props: { - context: Message[]; - updateContext: (updater: (context: Message[]) => void) => void; + context: ChatMessage[]; + updateContext: (updater: (context: ChatMessage[]) => void) => void; }) { const context = props.context; - const addContextPrompt = (prompt: Message) => { + const addContextPrompt = (prompt: ChatMessage) => { props.updateContext((context) => context.push(prompt)); }; @@ -173,7 +174,7 @@ export function ContextPrompts(props: { props.updateContext((context) => context.splice(i, 1)); }; - const updateContextPrompt = (i: number, prompt: Message) => { + const updateContextPrompt = (i: number, prompt: ChatMessage) => { props.updateContext((context) => (context[i] = prompt)); }; diff --git a/app/requests.ts b/app/requests.ts deleted file mode 100644 index df81b4f9a..000000000 --- a/app/requests.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { ChatRequest, ChatResponse } from "./api/openai/typing"; -import { - Message, - ModelConfig, - ModelType, - useAccessStore, - useAppConfig, - useChatStore, -} from "./store"; -import { showToast } from "./components/ui-lib"; -import { ACCESS_CODE_PREFIX } from "./constant"; - -const TIME_OUT_MS = 60000; - -const makeRequestParam = ( - messages: Message[], - options?: { - stream?: boolean; - overrideModel?: ModelType; - }, -): ChatRequest => { - let sendMessages = messages.map((v) => ({ - role: v.role, - content: v.content, - })); - - const modelConfig = { - ...useAppConfig.getState().modelConfig, - ...useChatStore.getState().currentSession().mask.modelConfig, - }; - - // override model config - if (options?.overrideModel) { - modelConfig.model = options.overrideModel; - } - - return { - messages: sendMessages, - stream: options?.stream, - model: modelConfig.model, - temperature: modelConfig.temperature, - presence_penalty: modelConfig.presence_penalty, - }; -}; - -export function requestOpenaiClient(path: string) { - const openaiUrl = useAccessStore.getState().openaiUrl; - return (body: any, method = "POST") => - fetch(openaiUrl + path, { - method, - body: body && JSON.stringify(body), - headers: getHeaders(), - }); -} - -export async function requestChat( - messages: Message[], - options?: { - model?: ModelType; - }, -) { - const req: ChatRequest = makeRequestParam(messages, { - overrideModel: options?.model, - }); - - const res = await requestOpenaiClient("v1/chat/completions")(req); - - try { - const response = (await res.json()) as ChatResponse; - return response; - } catch (error) { - console.error("[Request Chat] ", error, res.body); - } -} - -export async function requestUsage() { - const formatDate = (d: Date) => - `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d - .getDate() - .toString() - .padStart(2, "0")}`; - const ONE_DAY = 1 * 24 * 60 * 60 * 1000; - const now = new Date(); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const startDate = formatDate(startOfMonth); - const endDate = formatDate(new Date(Date.now() + ONE_DAY)); - - const [used, subs] = await Promise.all([ - requestOpenaiClient( - `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`, - )(null, "GET"), - requestOpenaiClient("dashboard/billing/subscription")(null, "GET"), - ]); - - const response = (await used.json()) as { - total_usage?: number; - error?: { - type: string; - message: string; - }; - }; - - const total = (await subs.json()) as { - hard_limit_usd?: number; - }; - - if (response.error && response.error.type) { - showToast(response.error.message); - return; - } - - if (response.total_usage) { - 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, - }; -} - -export async function requestChatStream( - messages: Message[], - options?: { - modelConfig?: ModelConfig; - overrideModel?: ModelType; - onMessage: (message: string, done: boolean) => void; - onError: (error: Error, statusCode?: number) => void; - onController?: (controller: AbortController) => void; - }, -) { - const req = makeRequestParam(messages, { - stream: true, - overrideModel: options?.overrideModel, - }); - - console.log("[Request] ", req); - - const controller = new AbortController(); - const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS); - - try { - const openaiUrl = useAccessStore.getState().openaiUrl; - const res = await fetch(openaiUrl + "v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...getHeaders(), - }, - body: JSON.stringify(req), - signal: controller.signal, - }); - - clearTimeout(reqTimeoutId); - - let responseText = ""; - - const finish = () => { - options?.onMessage(responseText, true); - controller.abort(); - }; - - if (res.ok) { - const reader = res.body?.getReader(); - const decoder = new TextDecoder(); - - options?.onController?.(controller); - - while (true) { - const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS); - const content = await reader?.read(); - clearTimeout(resTimeoutId); - - if (!content || !content.value) { - break; - } - - const text = decoder.decode(content.value, { stream: true }); - responseText += text; - - const done = content.done; - options?.onMessage(responseText, false); - - if (done) { - break; - } - } - - finish(); - } else if (res.status === 401) { - 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); - } - } catch (err) { - console.error("NetWork Error", err); - options?.onError(err as Error); - } -} - -export async function requestWithPrompt( - messages: Message[], - prompt: string, - options?: { - model?: ModelType; - }, -) { - messages = messages.concat([ - { - role: "user", - content: prompt, - date: new Date().toLocaleString(), - }, - ]); - - const res = await requestChat(messages, options); - - return res?.choices?.at(0)?.message?.content ?? ""; -} - -// To store message streaming controller -export const ControllerPool = { - controllers: {} as Record, - - addController( - sessionIndex: number, - messageId: number, - controller: AbortController, - ) { - const key = this.key(sessionIndex, messageId); - this.controllers[key] = controller; - return key; - }, - - stop(sessionIndex: number, messageId: number) { - const key = this.key(sessionIndex, messageId); - const controller = this.controllers[key]; - 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]; - }, - - key(sessionIndex: number, messageIndex: number) { - return `${sessionIndex},${messageIndex}`; - }, -}; diff --git a/app/store/access.ts b/app/store/access.ts index 4e870b616..91049846b 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { StoreKey } from "../constant"; -import { getHeaders } from "../requests"; +import { getHeaders } from "../client/api"; import { BOT_HELLO } from "./chat"; import { ALL_MODELS } from "./config"; diff --git a/app/store/chat.ts b/app/store/chat.ts index 17cf77072..9c58c852d 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,12 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { type ChatCompletionResponseMessage } from "openai"; -import { - ControllerPool, - requestChatStream, - requestWithPrompt, -} from "../requests"; import { trimTopic } from "../utils"; import Locale from "../locales"; @@ -14,9 +8,11 @@ import { showToast } from "../components/ui-lib"; import { ModelType } from "./config"; import { createEmptyMask, Mask } from "./mask"; import { StoreKey } from "../constant"; -import { api } from "../client/api"; +import { api, RequestMessage } from "../client/api"; +import { ChatControllerPool } from "../client/controller"; +import { prettyObject } from "../utils/format"; -export type Message = ChatCompletionResponseMessage & { +export type ChatMessage = RequestMessage & { date: string; streaming?: boolean; isError?: boolean; @@ -24,7 +20,7 @@ export type Message = ChatCompletionResponseMessage & { model?: ModelType; }; -export function createMessage(override: Partial): Message { +export function createMessage(override: Partial): ChatMessage { return { id: Date.now(), date: new Date().toLocaleString(), @@ -34,8 +30,6 @@ export function createMessage(override: Partial): Message { }; } -export const ROLES: Message["role"][] = ["system", "user", "assistant"]; - export interface ChatStat { tokenCount: number; wordCount: number; @@ -48,7 +42,7 @@ export interface ChatSession { topic: string; memoryPrompt: string; - messages: Message[]; + messages: ChatMessage[]; stat: ChatStat; lastUpdate: number; lastSummarizeIndex: number; @@ -57,7 +51,7 @@ export interface ChatSession { } export const DEFAULT_TOPIC = Locale.Store.DefaultTopic; -export const BOT_HELLO: Message = createMessage({ +export const BOT_HELLO: ChatMessage = createMessage({ role: "assistant", content: Locale.Store.BotHello, }); @@ -89,24 +83,24 @@ interface ChatStore { newSession: (mask?: Mask) => void; deleteSession: (index: number) => void; currentSession: () => ChatSession; - onNewMessage: (message: Message) => void; + onNewMessage: (message: ChatMessage) => void; onUserInput: (content: string) => Promise; summarizeSession: () => void; - updateStat: (message: Message) => void; + updateStat: (message: ChatMessage) => void; updateCurrentSession: (updater: (session: ChatSession) => void) => void; updateMessage: ( sessionIndex: number, messageIndex: number, - updater: (message?: Message) => void, + updater: (message?: ChatMessage) => void, ) => void; resetSession: () => void; - getMessagesWithMemory: () => Message[]; - getMemoryPrompt: () => Message; + getMessagesWithMemory: () => ChatMessage[]; + getMemoryPrompt: () => ChatMessage; clearAllData: () => void; } -function countMessages(msgs: Message[]) { +function countMessages(msgs: ChatMessage[]) { return msgs.reduce((pre, cur) => pre + cur.content.length, 0); } @@ -241,12 +235,12 @@ export const useChatStore = create()( const session = get().currentSession(); const modelConfig = session.mask.modelConfig; - const userMessage: Message = createMessage({ + const userMessage: ChatMessage = createMessage({ role: "user", content, }); - const botMessage: Message = createMessage({ + const botMessage: ChatMessage = createMessage({ role: "assistant", streaming: true, id: userMessage.id! + 1, @@ -278,45 +272,54 @@ export const useChatStore = create()( // make request console.log("[User Input] ", sendMessages); - requestChatStream(sendMessages, { - onMessage(content, done) { - // stream response - if (done) { - botMessage.streaming = false; - botMessage.content = content; - get().onNewMessage(botMessage); - ControllerPool.remove( - sessionIndex, - botMessage.id ?? messageIndex, - ); - } else { - botMessage.content = content; - set(() => ({})); - } + api.llm.chat({ + messages: sendMessages, + config: { ...modelConfig, stream: true }, + onUpdate(message) { + botMessage.streaming = true; + botMessage.content = message; + set(() => ({})); }, - onError(error, statusCode) { + onFinish(message) { + botMessage.streaming = false; + botMessage.content = message; + get().onNewMessage(botMessage); + ChatControllerPool.remove( + sessionIndex, + botMessage.id ?? messageIndex, + ); + set(() => ({})); + }, + onError(error) { const isAborted = error.message.includes("aborted"); - if (statusCode === 401) { - botMessage.content = Locale.Error.Unauthorized; - } else if (!isAborted) { + if ( + botMessage.content !== Locale.Error.Unauthorized && + !isAborted + ) { botMessage.content += "\n\n" + Locale.Store.Error; + } else if (botMessage.content.length === 0) { + botMessage.content = prettyObject(error); } botMessage.streaming = false; userMessage.isError = !isAborted; botMessage.isError = !isAborted; set(() => ({})); - ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex); + ChatControllerPool.remove( + sessionIndex, + botMessage.id ?? messageIndex, + ); + + console.error("[Chat] error ", error); }, onController(controller) { // collect controller for stop/retry - ControllerPool.addController( + ChatControllerPool.addController( sessionIndex, botMessage.id ?? messageIndex, controller, ); }, - modelConfig: { ...modelConfig }, }); }, @@ -330,7 +333,7 @@ export const useChatStore = create()( ? Locale.Store.Prompt.History(session.memoryPrompt) : "", date: "", - } as Message; + } as ChatMessage; }, getMessagesWithMemory() { @@ -385,7 +388,7 @@ export const useChatStore = create()( updateMessage( sessionIndex: number, messageIndex: number, - updater: (message?: Message) => void, + updater: (message?: ChatMessage) => void, ) { const sessions = get().sessions; const session = sessions.at(sessionIndex); @@ -410,13 +413,24 @@ export const useChatStore = create()( session.topic === DEFAULT_TOPIC && countMessages(session.messages) >= SUMMARIZE_MIN_LEN ) { - requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, { - model: "gpt-3.5-turbo", - }).then((res) => { - get().updateCurrentSession( - (session) => - (session.topic = res ? trimTopic(res) : DEFAULT_TOPIC), - ); + const topicMessages = session.messages.concat( + createMessage({ + role: "user", + content: Locale.Store.Prompt.Topic, + }), + ); + api.llm.chat({ + messages: topicMessages, + config: { + model: "gpt-3.5-turbo", + }, + onFinish(message) { + get().updateCurrentSession( + (session) => + (session.topic = + message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC), + ); + }, }); } @@ -450,26 +464,24 @@ export const useChatStore = create()( historyMsgLength > modelConfig.compressMessageLengthThreshold && session.mask.modelConfig.sendMemory ) { - requestChatStream( - toBeSummarizedMsgs.concat({ + api.llm.chat({ + messages: toBeSummarizedMsgs.concat({ role: "system", content: Locale.Store.Prompt.Summarize, date: "", }), - { - overrideModel: "gpt-3.5-turbo", - onMessage(message, done) { - session.memoryPrompt = message; - if (done) { - console.log("[Memory] ", session.memoryPrompt); - session.lastSummarizeIndex = lastSummarizeIndex; - } - }, - onError(error) { - console.error("[Summarize] ", error); - }, + config: { ...modelConfig, stream: true }, + onUpdate(message) { + session.memoryPrompt = message; }, - ); + onFinish(message) { + console.log("[Memory] ", message); + session.lastSummarizeIndex = lastSummarizeIndex; + }, + onError(err) { + console.error("[Summarize] ", err); + }, + }); } }, diff --git a/app/store/mask.ts b/app/store/mask.ts index 98bd47021..efd774ebe 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { BUILTIN_MASKS } from "../masks"; import { getLang, Lang } from "../locales"; -import { DEFAULT_TOPIC, Message } from "./chat"; +import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { ModelConfig, ModelType, useAppConfig } from "./config"; import { StoreKey } from "../constant"; @@ -10,7 +10,7 @@ export type Mask = { id: number; avatar: string; name: string; - context: Message[]; + context: ChatMessage[]; modelConfig: ModelConfig; lang: Lang; builtin: boolean; diff --git a/app/store/update.ts b/app/store/update.ts index 8d8808220..00a2edda1 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -1,7 +1,8 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; -import { requestUsage } from "../requests"; +import { FETCH_COMMIT_URL, StoreKey } from "../constant"; +import { api } from "../client/api"; +import { showToast } from "../components/ui-lib"; export interface UpdateStore { lastUpdate: number; @@ -73,10 +74,17 @@ export const useUpdateStore = create()( lastUpdateUsage: Date.now(), })); - const usage = await requestUsage(); + try { + const usage = await api.llm.usage(); - if (usage) { - set(() => usage); + if (usage) { + set(() => ({ + used: usage.used, + subscription: usage.total, + })); + } + } catch (e) { + showToast((e as Error).message); } }, }), diff --git a/app/typing.ts b/app/typing.ts new file mode 100644 index 000000000..25e474abf --- /dev/null +++ b/app/typing.ts @@ -0,0 +1 @@ +export type Updater = (updater: (value: T) => void) => void; diff --git a/app/utils/format.ts b/app/utils/format.ts new file mode 100644 index 000000000..1f71f4f00 --- /dev/null +++ b/app/utils/format.ts @@ -0,0 +1,8 @@ +export function prettyObject(msg: any) { + const prettyMsg = [ + "```json\n", + JSON.stringify(msg, null, " "), + "\n```", + ].join(""); + return prettyMsg; +} diff --git a/package.json b/package.json index 6b13b9b6c..914ec60fb 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,13 @@ }, "dependencies": { "@hello-pangea/dnd": "^16.2.0", - "@microsoft/fetch-event-source": "^2.0.1", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "emoji-picker-react": "^4.4.7", - "eventsource-parser": "^0.1.0", "fuse.js": "^6.6.2", "mermaid": "^10.1.0", "next": "^13.3.1-canary.8", "node-fetch": "^3.3.1", - "openai": "^3.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.5", diff --git a/yarn.lock b/yarn.lock index a6695acba..2b5c690cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,11 +1111,6 @@ dependencies: "@types/react" ">=16.0.0" -"@microsoft/fetch-event-source@^2.0.1": - version "2.0.1" - resolved "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" - integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== - "@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" @@ -1643,11 +1638,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -1658,13 +1648,6 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== -axios@^0.26.0: - version "0.26.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== - dependencies: - follow-redirects "^1.14.8" - axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" @@ -1885,13 +1868,6 @@ colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -2376,11 +2352,6 @@ delaunator@5: dependencies: robust-predicates "^3.0.0" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -2821,11 +2792,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventsource-parser@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-0.1.0.tgz#4a6b84751ca8e704040e6f7f50e7d77344fa1b7c" - integrity sha512-M9QjFtEIkwytUarnx113HGmgtk52LSn3jNAtnWKi3V+b9rqSfQeVdLsaD5AG/O4IrGQwmAAHBIsqbmURPTd2rA== - execa@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" @@ -2934,11 +2900,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.8: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -2946,15 +2907,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - format@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" @@ -4271,18 +4223,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4493,14 +4433,6 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openai@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" - integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== - dependencies: - axios "^0.26.0" - form-data "^4.0.0" - optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" From e9335d9508c21710a8855b7cd0fd0b29ed90d252 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 15 May 2023 01:45:31 +0800 Subject: [PATCH 18/24] chore: upgrade nextjs to 13.4.2 --- package.json | 2 +- yarn.lock | 124 +++++++++++++++++++++++++++------------------------ 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 914ec60fb..07ba977ea 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "emoji-picker-react": "^4.4.7", "fuse.js": "^6.6.2", "mermaid": "^10.1.0", - "next": "^13.3.1-canary.8", + "next": "^13.4.2", "node-fetch": "^3.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 2b5c690cd..5240d7e77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,10 +1111,10 @@ dependencies: "@types/react" ">=16.0.0" -"@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/env@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/env/-/env-13.4.2.tgz#cf3ebfd523a33d8404c1216e02ac8d856a73170e" + integrity sha512-Wqvo7lDeS0KGwtwg9TT9wKQ8raelmUxt+TQKWvG/xKfcmDXNOtCuaszcfCF8JzlBG1q0VhpI6CKaRMbVPMDWgw== "@next/eslint-plugin-next@13.2.3": version "13.2.3" @@ -1123,50 +1123,50 @@ dependencies: glob "7.1.7" -"@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-darwin-arm64@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.2.tgz#d0b497df972bd02eee3bc823d6a76c2cc8b733ef" + integrity sha512-6BBlqGu3ewgJflv9iLCwO1v1hqlecaIH2AotpKfVUEzUxuuDNJQZ2a4KLb4MBl8T9/vca1YuWhSqtbF6ZuUJJw== -"@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-x64@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.2.tgz#09a800bed8dfe4beec4cbf14092f9c22db24470b" + integrity sha512-iZuYr7ZvGLPjPmfhhMl0ISm+z8EiyLBC1bLyFwGBxkWmPXqdJ60mzuTaDSr5WezDwv0fz32HB7JHmRC6JVHSZg== -"@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-linux-arm64-gnu@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.2.tgz#b7ade28834564120b0b25ffa0b79d75982d290bc" + integrity sha512-2xVabFtIge6BJTcJrW8YuUnYTuQjh4jEuRuS2mscyNVOj6zUZkom3CQg+egKOoS+zh2rrro66ffSKIS+ztFJTg== -"@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-linux-arm64-musl@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.2.tgz#f5420548234d35251630ddaa2e9a7dc32337a887" + integrity sha512-wKRCQ27xCUJx5d6IivfjYGq8oVngqIhlhSAJntgXLt7Uo9sRT/3EppMHqUZRfyuNBTbykEre1s5166z+pvRB5A== -"@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-x64-gnu@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.2.tgz#0241dc011d73f08df9d9998cffdfcf08d1971520" + integrity sha512-NpCa+UVhhuNeaFVUP1Bftm0uqtvLWq2JTm7+Ta48+2Uqj2mNXrDIvyn1DY/ZEfmW/1yvGBRaUAv9zkMkMRixQA== -"@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-x64-musl@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.2.tgz#fd35919e2b64b1c739583145799fefd594ef5d63" + integrity sha512-ZWVC72x0lW4aj44e3khvBrj2oSYj1bD0jESmyah3zG/3DplEy/FOtYkMzbMjHTdDSheso7zH8GIlW6CDQnKhmQ== -"@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-win32-arm64-msvc@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.2.tgz#fa95d2dbb97707c130a868a1bd7e83e64bedf4c6" + integrity sha512-pLT+OWYpzJig5K4VKhLttlIfBcVZfr2+Xbjra0Tjs83NQSkFS+y7xx+YhCwvpEmXYLIvaggj2ONPyjbiigOvHQ== -"@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-win32-ia32-msvc@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.2.tgz#31a98e61d3cda92ec2293c50df7cb5280fc63697" + integrity sha512-dhpiksQCyGca4WY0fJyzK3FxMDFoqMb0Cn+uDB+9GYjpU2K5//UGPQlCwiK4JHxuhg8oLMag5Nf3/IPSJNG8jw== -"@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== +"@next/swc-win32-x64-msvc@13.4.2": + version "13.4.2" + resolved "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.2.tgz#8435ab6087046355f5de07122d3097949e8fab10" + integrity sha512-O7bort1Vld00cu8g0jHZq3cbSTUNMohOEvYqsqE10+yfohhdPHzvzO+ziJRz4Dyyr/fYKREwS7gR4JC0soSOMw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1317,10 +1317,10 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.4.14": - version "0.4.14" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" - integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== +"@swc/helpers@0.5.1": + version "0.5.1" + resolved "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" + integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== dependencies: tslib "^2.4.0" @@ -4270,27 +4270,28 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -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== +next@^13.4.2: + version "13.4.2" + resolved "https://registry.npmmirror.com/next/-/next-13.4.2.tgz#972f73a794f2c61729facedc79c49b22bdc89f0c" + integrity sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg== dependencies: - "@next/env" "13.3.1-canary.8" - "@swc/helpers" "0.4.14" + "@next/env" "13.4.2" + "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.1.1" + zod "3.21.4" optionalDependencies: - "@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" + "@next/swc-darwin-arm64" "13.4.2" + "@next/swc-darwin-x64" "13.4.2" + "@next/swc-linux-arm64-gnu" "13.4.2" + "@next/swc-linux-arm64-musl" "13.4.2" + "@next/swc-linux-x64-gnu" "13.4.2" + "@next/swc-linux-x64-musl" "13.4.2" + "@next/swc-win32-arm64-msvc" "13.4.2" + "@next/swc-win32-ia32-msvc" "13.4.2" + "@next/swc-win32-x64-msvc" "13.4.2" node-domexception@^1.0.0: version "1.0.0" @@ -5584,6 +5585,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@3.21.4: + version "3.21.4" + resolved "https://registry.npmmirror.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zustand@^4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.6.tgz#ce7804eb75361af0461a2d0536b65461ec5de86f" From 5979bdd48e6d9c936f1e62d2e49f926d9f2d83b3 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 15 May 2023 01:55:45 +0800 Subject: [PATCH 19/24] fixup --- app/client/platforms/openai.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4b8c1cf43..5058fa0ae 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -103,11 +103,15 @@ export class ChatGPTApi implements LLMApi { return finish(); } if (text.length === 0) continue; - const json = JSON.parse(text); - const delta = json.choices[0].delta.content; - if (delta) { - responseText += delta; - options.onUpdate?.(responseText, delta); + try { + const json = JSON.parse(text); + const delta = json.choices[0].delta.content; + if (delta) { + responseText += delta; + options.onUpdate?.(responseText, delta); + } + } catch (e) { + console.error("[Request] parse error", text, chunk); } } } From 9e602eb5755a9fe6d912501c087cdd9cbf19abd9 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 15 May 2023 02:01:50 +0800 Subject: [PATCH 20/24] fixup: decode in stream mode --- app/client/platforms/openai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 5058fa0ae..4bdf9e056 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -94,7 +94,7 @@ export class ChatGPTApi implements LLMApi { return finish(); } - const chunk = decoder.decode(value); + const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split("data: "); for (const line of lines) { From b357e2ecef6f42c4cec433ec20a0dea3c73072c0 Mon Sep 17 00:00:00 2001 From: PaRaD1SE98 Date: Mon, 15 May 2023 10:03:11 +0900 Subject: [PATCH 21/24] fix: typo IMPRTANT -> IMPORTANT --- app/store/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 8fa32724e..9257d2633 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -249,7 +249,7 @@ export const useChatStore = create()( const systemInfo = createMessage({ role: "system", - content: `IMPRTANT: You are a virtual assistant powered by the ${ + content: `IMPORTANT: You are a virtual assistant powered by the ${ modelConfig.model } model, now time is ${new Date().toLocaleString()}}`, id: botMessage.id! + 1, From 8b0cf7d248bd3582c619f9337f711076caa75532 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 16 May 2023 00:22:11 +0800 Subject: [PATCH 22/24] fix: #1509 openai url split --- app/client/platforms/openai.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4bdf9e056..a69e8e3ce 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -10,8 +10,10 @@ export class ChatGPTApi implements LLMApi { public SubsPath = "dashboard/billing/subscription"; path(path: string): string { - const openaiUrl = useAccessStore.getState().openaiUrl; - if (openaiUrl.endsWith("/")) openaiUrl.slice(0, openaiUrl.length - 1); + let openaiUrl = useAccessStore.getState().openaiUrl; + if (openaiUrl.endsWith("/")) { + openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); + } return [openaiUrl, path].join("/"); } From aed6b349507dce2bdca77756db52bca88db268a9 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 16 May 2023 01:25:16 +0800 Subject: [PATCH 23/24] fix: #1498 missing text caused by streaming --- app/client/platforms/openai.ts | 57 +++++++++++++--------------------- package.json | 1 + yarn.lock | 5 +++ 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index a69e8e3ce..cc1ecb919 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -3,6 +3,7 @@ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import Locale from "../../locales"; +import { fetchEventSource } from "@microsoft/fetch-event-source"; export class ChatGPTApi implements LLMApi { public ChatPath = "v1/chat/completions"; @@ -71,40 +72,20 @@ export class ChatGPTApi implements LLMApi { options.onFinish(responseText); }; - const res = await fetch(chatPath, chatPayload); - clearTimeout(reqestTimeoutId); - - if (res.status === 401) { - responseText += "\n\n" + Locale.Error.Unauthorized; - return finish(); - } - - if ( - !res.ok || - !res.headers.get("Content-Type")?.includes("stream") || - !res.body - ) { - return options.onError?.(new Error()); - } - - const reader = res.body.getReader(); - const decoder = new TextDecoder("utf-8"); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - return finish(); - } - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split("data: "); - - for (const line of lines) { - const text = line.trim(); - if (line.startsWith("[DONE]")) { + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(reqestTimeoutId); + if (res.status === 401) { + responseText += "\n\n" + Locale.Error.Unauthorized; return finish(); } - if (text.length === 0) continue; + }, + onmessage(msg) { + if (msg.data === "[DONE]") { + return finish(); + } + const text = msg.data; try { const json = JSON.parse(text); const delta = json.choices[0].delta.content; @@ -113,10 +94,16 @@ export class ChatGPTApi implements LLMApi { options.onUpdate?.(responseText, delta); } } catch (e) { - console.error("[Request] parse error", text, chunk); + console.error("[Request] parse error", text, msg); } - } - } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + }, + }); } else { const res = await fetch(chatPath, chatPayload); clearTimeout(reqestTimeoutId); diff --git a/package.json b/package.json index 07ba977ea..f9d3c3c72 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@hello-pangea/dnd": "^16.2.0", + "@microsoft/fetch-event-source": "^2.0.1", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "emoji-picker-react": "^4.4.7", diff --git a/yarn.lock b/yarn.lock index 5240d7e77..e54a69e48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,6 +1111,11 @@ dependencies: "@types/react" ">=16.0.0" +"@microsoft/fetch-event-source@^2.0.1": + version "2.0.1" + resolved "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" + integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== + "@next/env@13.4.2": version "13.4.2" resolved "https://registry.npmmirror.com/@next/env/-/env-13.4.2.tgz#cf3ebfd523a33d8404c1216e02ac8d856a73170e" From 71cbf86b2c8e21407045ba848ba38237134b837d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 16 May 2023 01:58:58 +0800 Subject: [PATCH 24/24] fixup: add more error info --- app/api/auth.ts | 5 ++--- app/client/platforms/openai.ts | 13 +++++++++++++ app/store/chat.ts | 4 +--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/api/auth.ts b/app/api/auth.ts index 1005c5fff..62fcd2262 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -43,8 +43,7 @@ export function auth(req: NextRequest) { if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { return { error: true, - needAccessCode: true, - msg: "Please go settings page and fill your access code.", + msg: !accessCode ? "empty access code" : "wrong access code", }; } @@ -58,7 +57,7 @@ export function auth(req: NextRequest) { console.log("[Auth] admin did not provide an api key"); return { error: true, - msg: "Empty Api Key", + msg: "admin did not provide an api key", }; } } else { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index cc1ecb919..99f365202 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -4,6 +4,7 @@ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import Locale from "../../locales"; import { fetchEventSource } from "@microsoft/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; export class ChatGPTApi implements LLMApi { public ChatPath = "v1/chat/completions"; @@ -72,12 +73,24 @@ export class ChatGPTApi implements LLMApi { options.onFinish(responseText); }; + controller.signal.onabort = finish; + fetchEventSource(chatPath, { ...chatPayload, async onopen(res) { clearTimeout(reqestTimeoutId); if (res.status === 401) { + let extraInfo = { error: undefined }; + try { + extraInfo = await res.clone().json(); + } catch {} + responseText += "\n\n" + Locale.Error.Unauthorized; + + if (extraInfo.error) { + responseText += "\n\n" + prettyObject(extraInfo); + } + return finish(); } }, diff --git a/app/store/chat.ts b/app/store/chat.ts index 9257d2633..9bb9a8039 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -296,9 +296,7 @@ export const useChatStore = create()( botMessage.content !== Locale.Error.Unauthorized && !isAborted ) { - botMessage.content += "\n\n" + Locale.Store.Error; - } else if (botMessage.content.length === 0) { - botMessage.content = prettyObject(error); + botMessage.content += "\n\n" + prettyObject(error); } botMessage.streaming = false; userMessage.isError = !isAborted;