diff --git a/package-lock.json b/package-lock.json index 1cc2afb..68aa793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "openai": "^3.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-hot-toast": "^2.4.0", "react-i18next": "^12.2.0", "react-markdown": "^8.0.5", "react-syntax-highlighter": "^15.5.0", @@ -3455,6 +3456,14 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/goober": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz", + "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6394,6 +6403,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz", + "integrity": "sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-i18next": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", @@ -10613,6 +10637,12 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "goober": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz", + "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==", + "requires": {} + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -12517,6 +12547,14 @@ "scheduler": "^0.23.0" } }, + "react-hot-toast": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz", + "integrity": "sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==", + "requires": { + "goober": "^2.1.10" + } + }, "react-i18next": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", diff --git a/package.json b/package.json index ed70cb1..029753c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "openai": "^3.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-hot-toast": "^2.4.0", "react-i18next": "^12.2.0", "react-markdown": "^8.0.5", "react-syntax-highlighter": "^15.5.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 388cbb5..1cf463f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,12 +2,14 @@ import '@/styles/globals.css'; import { appWithTranslation } from 'next-i18next'; import type { AppProps } from 'next/app'; import { Inter } from 'next/font/google'; +import { Toaster } from 'react-hot-toast'; const inter = Inter({ subsets: ['latin'] }); function App({ Component, pageProps }: AppProps<{}>) { return (
+
); diff --git a/pages/api/chat.ts b/pages/api/chat.ts index 09c0aaf..4083fd9 100644 --- a/pages/api/chat.ts +++ b/pages/api/chat.ts @@ -1,6 +1,6 @@ import { ChatBody, Message } from '@/types/chat'; import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; -import { OpenAIStream } from '@/utils/server'; +import { OpenAIError, OpenAIStream } from '@/utils/server'; import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json'; import { init, Tiktoken } from '@dqbd/tiktoken/lite/init'; // @ts-expect-error @@ -49,7 +49,11 @@ const handler = async (req: Request): Promise => { return new Response(stream); } catch (error) { console.error(error); - return new Response('Error', { status: 500 }); + if (error instanceof OpenAIError) { + return new Response('Error', { status: 500, statusText: error.message }); + } else { + return new Response('Error', { status: 500 }); + } } }; diff --git a/pages/index.tsx b/pages/index.tsx index ea205b5..9d906d3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -34,6 +34,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import Head from 'next/head'; import { useEffect, useRef, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import toast from 'react-hot-toast'; interface HomeProps { serverSideApiKeyIsSet: boolean; @@ -120,6 +121,7 @@ const Home: React.FC = ({ if (!response.ok) { setLoading(false); setMessageIsStreaming(false); + toast.error(response.statusText); return; } diff --git a/utils/server/index.ts b/utils/server/index.ts index 9366af8..8ab473d 100644 --- a/utils/server/index.ts +++ b/utils/server/index.ts @@ -7,6 +7,20 @@ import { } from 'eventsource-parser'; import { OPENAI_API_HOST } from '../app/const'; +export class OpenAIError extends Error { + type: string; + param: string; + code: string; + + constructor(message: string, type: string, param: string, code: string) { + super(message); + this.name = 'OpenAIError'; + this.type = type; + this.param = param; + this.code = code; + } +} + export const OpenAIStream = async ( model: OpenAIModel, systemPrompt: string, @@ -41,9 +55,21 @@ export const OpenAIStream = async ( const decoder = new TextDecoder(); if (res.status !== 200) { - const statusText = res.statusText; - const result = await res.body?.getReader().read(); - throw new Error(`OpenAI API returned an error: ${decoder.decode(result?.value) || statusText}`); + const result = await res.json(); + if (result.error) { + throw new OpenAIError( + result.error.message, + result.error.type, + result.error.param, + result.error.code, + ); + } else { + throw new Error( + `OpenAI API returned an error: ${ + decoder.decode(result?.value) || result.statusText + }`, + ); + } } const stream = new ReadableStream({