add react-hot-toast and surface OpenAI API errors to users (#328)
This commit is contained in:
		
							parent
							
								
									23ad285a4b
								
							
						
					
					
						commit
						b7b6bbaaca
					
				|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -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,7 +49,11 @@ 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); | ||||||
|     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 }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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({ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue