From bebfffb2d93e2aad6fbebc408453b3ff444ac6d7 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Sun, 7 Jan 2024 13:49:59 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix:=20Custom=20Endpoin?= =?UTF-8?q?t=20issues,=20Improve=20SSE=20Response=20Handling=20(#1510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(custom): prevent presets using removed custom endpoints from causing frontend errors * refactor(abortMiddleware): send 204 status when abortController is not found/active, set expected header `application/json` when not set * fix(useSSE): general improvements: - Add endpointType to fetch URL in useSSE hook - use EndpointURLs enum - handle 204 response by setting `data` to initiated response - add better error handling UX, make clear when there is an explicit error --- api/server/middleware/abortMiddleware.js | 4 +- client/src/components/Chat/Landing.tsx | 4 +- .../Chat/Menus/Endpoints/MenuItem.tsx | 4 +- .../Chat/Menus/Presets/PresetItems.tsx | 18 +++++--- client/src/hooks/useSSE.ts | 46 +++++++++++++++++-- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index 811963174c..cc9b9fc051 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -14,7 +14,7 @@ async function abortMessage(req, res) { } if (!abortControllers.has(abortKey) && !res.headersSent) { - return res.status(404).send({ message: 'Request not found' }); + return res.status(204).send({ message: 'Request not found' }); } const { abortController } = abortControllers.get(abortKey); @@ -26,6 +26,8 @@ async function abortMessage(req, res) { return sendMessage(res, finalEvent); } + res.setHeader('Content-Type', 'application/json'); + res.send(JSON.stringify(finalEvent)); } diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index 4831374084..1cc5df08e2 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -23,6 +23,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) { const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL'); const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown'; + const Icon = icons[iconKey]; return (
@@ -31,7 +32,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
{endpoint && - icons[iconKey]({ + Icon && + Icon({ size: 41, context: 'landing', className: 'h-2/3 w-2/3', diff --git a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx index 74ab6f3765..3d37d60b4e 100644 --- a/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/MenuItem.tsx @@ -82,7 +82,7 @@ const MenuItem: FC = ({
- { + {Icon && ( = ({ className="icon-md shrink-0 dark:text-white" iconURL={getEndpointField(endpointsConfig, endpoint, 'iconURL')} /> - } + )}
{title}
{description}
diff --git a/client/src/components/Chat/Menus/Presets/PresetItems.tsx b/client/src/components/Chat/Menus/Presets/PresetItems.tsx index b440b069d0..b0ed2b792f 100644 --- a/client/src/components/Chat/Menus/Presets/PresetItems.tsx +++ b/client/src/components/Chat/Menus/Presets/PresetItems.tsx @@ -97,7 +97,8 @@ const PresetItems: FC<{ const iconKey = getEndpointField(endpointsConfig, preset.endpoint, 'type') ? 'unknown' - : preset.endpoint ?? 'unknown'; + : preset.endpointType ?? preset.endpoint ?? 'unknown'; + const Icon = icons[iconKey]; return ( @@ -109,12 +110,15 @@ const PresetItems: FC<{ title={getPresetTitle(preset)} disableHover={true} onClick={() => onSelectPreset(preset)} - icon={icons[iconKey]({ - context: 'menu-item', - iconURL: getEndpointField(endpointsConfig, preset.endpoint, 'iconURL'), - className: 'icon-md mr-1 dark:text-white', - endpoint: preset.endpoint, - })} + icon={ + Icon && + Icon({ + context: 'menu-item', + iconURL: getEndpointField(endpointsConfig, preset.endpoint, 'iconURL'), + className: 'icon-md mr-1 dark:text-white', + endpoint: preset.endpoint, + }) + } selected={false} data-testid={`preset-item-${preset}`} > diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index 0cc7957f5b..0a6ca109d3 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'; import { /* @ts-ignore */ SSE, + EndpointURLs, createPayload, tMessageSchema, tConvoUpdateSchema, @@ -268,10 +269,11 @@ export default function useSSE(submission: TSubmission | null, index = 0) { const abortConversation = (conversationId = '', submission: TSubmission) => { console.log(submission); - const { endpoint } = submission?.conversation || {}; + const { endpoint: _endpoint, endpointType } = submission?.conversation || {}; + const endpoint = endpointType ?? _endpoint; let res: Response; - fetch(`/api/ask/${endpoint}/abort`, { + fetch(`${EndpointURLs[endpoint ?? '']}/abort`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -283,7 +285,29 @@ export default function useSSE(submission: TSubmission | null, index = 0) { }) .then((response) => { res = response; - return response.json(); + // Check if the response is JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return response.json(); + } else if (response.status === 204) { + const responseMessage = { + ...submission.initialResponse, + text: submission.initialResponse.text.replace( + '', + '', + ), + }; + + return { + requestMessage: submission.message, + responseMessage: responseMessage, + conversation: submission.conversation, + }; + } else { + throw new Error( + 'Unexpected response from server; Status: ' + res.status + ' ' + res.statusText, + ); + } }) .then((data) => { console.log('aborted', data); @@ -295,6 +319,22 @@ export default function useSSE(submission: TSubmission | null, index = 0) { .catch((error) => { console.error('Error aborting request'); console.error(error); + const convoId = conversationId ?? v4(); + + const text = + submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : ''; + + const errorMessage = { + ...submission, + ...submission.initialResponse, + text: text ?? error.message ?? 'Error cancelling request', + unfinished: !!text.length, + error: true, + }; + + const errorResponse = tMessageSchema.parse(errorMessage); + setMessages([...submission.messages, submission.message, errorResponse]); + newConversation({ template: { conversationId: convoId } }); setIsSubmitting(false); }); return;