add react-hot-toast and surface OpenAI API errors to users (#328)

This commit is contained in:
Jason Banich 2023-04-01 22:05:07 -07:00 committed by GitHub
parent 23ad285a4b
commit b7b6bbaaca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 5 deletions

38
package-lock.json generated
View File

@ -17,6 +17,7 @@
"openai": "^3.2.1", "openai": "^3.2.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"react-i18next": "^12.2.0", "react-i18next": "^12.2.0",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
@ -3455,6 +3456,14 @@
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true "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": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -6394,6 +6403,21 @@
"react": "^18.2.0" "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": { "node_modules/react-i18next": {
"version": "12.2.0", "version": "12.2.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz",
@ -10613,6 +10637,12 @@
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true "dev": true
}, },
"goober": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz",
"integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==",
"requires": {}
},
"gopd": { "gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -12517,6 +12547,14 @@
"scheduler": "^0.23.0" "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": { "react-i18next": {
"version": "12.2.0", "version": "12.2.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz",

View File

@ -21,6 +21,7 @@
"openai": "^3.2.1", "openai": "^3.2.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"react-i18next": "^12.2.0", "react-i18next": "^12.2.0",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",

View File

@ -2,12 +2,14 @@ import '@/styles/globals.css';
import { appWithTranslation } from 'next-i18next'; import { appWithTranslation } from 'next-i18next';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { Toaster } from 'react-hot-toast';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
function App({ Component, pageProps }: AppProps<{}>) { function App({ Component, pageProps }: AppProps<{}>) {
return ( return (
<main className={inter.className}> <main className={inter.className}>
<Toaster />
<Component {...pageProps} /> <Component {...pageProps} />
</main> </main>
); );

View File

@ -1,6 +1,6 @@
import { ChatBody, Message } from '@/types/chat'; import { ChatBody, Message } from '@/types/chat';
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; 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 tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
import { init, Tiktoken } from '@dqbd/tiktoken/lite/init'; import { init, Tiktoken } from '@dqbd/tiktoken/lite/init';
// @ts-expect-error // @ts-expect-error
@ -49,8 +49,12 @@ const handler = async (req: Request): Promise<Response> => {
return new Response(stream); return new Response(stream);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (error instanceof OpenAIError) {
return new Response('Error', { status: 500, statusText: error.message });
} else {
return new Response('Error', { status: 500 }); return new Response('Error', { status: 500 });
} }
}
}; };
export default handler; export default handler;

View File

@ -34,6 +34,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head'; import Head from 'next/head';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import toast from 'react-hot-toast';
interface HomeProps { interface HomeProps {
serverSideApiKeyIsSet: boolean; serverSideApiKeyIsSet: boolean;
@ -120,6 +121,7 @@ const Home: React.FC<HomeProps> = ({
if (!response.ok) { if (!response.ok) {
setLoading(false); setLoading(false);
setMessageIsStreaming(false); setMessageIsStreaming(false);
toast.error(response.statusText);
return; return;
} }

View File

@ -7,6 +7,20 @@ import {
} from 'eventsource-parser'; } from 'eventsource-parser';
import { OPENAI_API_HOST } from '../app/const'; 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 ( export const OpenAIStream = async (
model: OpenAIModel, model: OpenAIModel,
systemPrompt: string, systemPrompt: string,
@ -41,9 +55,21 @@ export const OpenAIStream = async (
const decoder = new TextDecoder(); const decoder = new TextDecoder();
if (res.status !== 200) { if (res.status !== 200) {
const statusText = res.statusText; const result = await res.json();
const result = await res.body?.getReader().read(); if (result.error) {
throw new Error(`OpenAI API returned an error: ${decoder.decode(result?.value) || statusText}`); 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({ const stream = new ReadableStream({