🎙️ a11y: Screen Reader Support for Dynamic Content Updates (#3625)

* WIP: first pass, hooks

* wip: isStream arg

* feat: first pass, dynamic content updates, screen reader announcements

* chore: unrelated, styling redundancy
This commit is contained in:
Danny Avila 2024-08-13 03:04:27 -04:00 committed by GitHub
parent 05696233a9
commit 6655304753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 353 additions and 54 deletions

View file

@ -28,6 +28,7 @@ import {
import useContentHandler from '~/hooks/SSE/useContentHandler';
import type { TGenTitleMutation } from '~/data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLiveAnnouncer } from '~/Providers';
type TSyncData = {
sync: boolean;
@ -64,6 +65,7 @@ export default function useEventHandlers({
resetLatestMessage,
}: EventHandlerParams) {
const queryClient = useQueryClient();
const { announcePolite, announceAssertive } = useLiveAnnouncer();
const { conversationId: paramId } = useParams();
const { token } = useAuthContext();
@ -71,7 +73,7 @@ export default function useEventHandlers({
const contentHandler = useContentHandler({ setMessages, getMessages });
const messageHandler = useCallback(
(data: string, submission: TSubmission) => {
(data: string | undefined, submission: TSubmission) => {
const {
messages,
userMessage,
@ -80,13 +82,20 @@ export default function useEventHandlers({
initialResponse,
isRegenerate = false,
} = submission;
const text = data ?? '';
if (text.length > 0) {
announcePolite({
message: text,
isStream: true,
});
}
if (isRegenerate) {
setMessages([
...messages,
{
...initialResponse,
text: data,
text,
plugin: plugin ?? null,
plugins: plugins ?? [],
// unfinished: true
@ -98,7 +107,7 @@ export default function useEventHandlers({
userMessage,
{
...initialResponse,
text: data,
text,
plugin: plugin ?? null,
plugins: plugins ?? [],
// unfinished: true
@ -106,7 +115,7 @@ export default function useEventHandlers({
]);
}
},
[setMessages],
[setMessages, announcePolite],
);
const cancelHandler = useCallback(
@ -136,7 +145,7 @@ export default function useEventHandlers({
}
// refresh title
if (genTitle && isNewConvo && requestMessage?.parentMessageId === Constants.NO_PARENT) {
if (genTitle && isNewConvo && requestMessage.parentMessageId === Constants.NO_PARENT) {
setTimeout(() => {
genTitle.mutate({ conversationId: convoUpdate.conversationId as string });
}, 2500);
@ -238,8 +247,8 @@ export default function useEventHandlers({
const { messages, userMessage, isRegenerate = false } = submission;
const initialResponse = {
...submission.initialResponse,
parentMessageId: userMessage?.messageId,
messageId: userMessage?.messageId + '_',
parentMessageId: userMessage.messageId,
messageId: userMessage.messageId + '_',
};
if (isRegenerate) {
setMessages([...messages, initialResponse]);
@ -248,12 +257,16 @@ export default function useEventHandlers({
}
const { conversationId, parentMessageId } = userMessage;
announceAssertive({
message: 'The AI is generating a response.',
id: `ai-generating-${Date.now()}`,
});
let update = {} as TConversation;
if (setConversation && !isAddedRequest) {
setConversation((prevState) => {
let title = prevState?.title;
const parentId = isRegenerate ? userMessage?.overrideParentMessageId : parentMessageId;
const parentId = isRegenerate ? userMessage.overrideParentMessageId : parentMessageId;
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
const cachedConvo = getConversationById(convos, conversationId);
@ -295,7 +308,14 @@ export default function useEventHandlers({
scrollToEnd();
},
[setMessages, setConversation, queryClient, isAddedRequest, resetLatestMessage],
[
setMessages,
setConversation,
queryClient,
isAddedRequest,
resetLatestMessage,
announceAssertive,
],
);
const finalHandler = useCallback(
@ -304,7 +324,7 @@ export default function useEventHandlers({
const { messages, conversation: submissionConvo, isRegenerate = false } = submission;
setShowStopButton(false);
setCompleted((prev) => new Set(prev.add(submission?.initialResponse?.messageId)));
setCompleted((prev) => new Set(prev.add(submission.initialResponse.messageId)));
const currentMessages = getMessages();
// Early return if messages are empty; i.e., the user navigated away
@ -312,6 +332,19 @@ export default function useEventHandlers({
return setIsSubmitting(false);
}
/* a11y announcements */
announcePolite({
message: '',
isComplete: true,
});
setTimeout(() => {
announcePolite({
message: 'The AI has finished generating a response.',
id: `ai-finished-${Date.now()}`,
});
}, 100);
// update the messages; if assistants endpoint, client doesn't receive responseMessage
if (runMessages) {
setMessages([...runMessages]);
@ -367,6 +400,7 @@ export default function useEventHandlers({
setMessages,
setCompleted,
isAddedRequest,
announcePolite,
setConversation,
setIsSubmitting,
setShowStopButton,
@ -379,7 +413,7 @@ export default function useEventHandlers({
setCompleted((prev) => new Set(prev.add(initialResponse.messageId)));
const conversationId = userMessage?.conversationId ?? submission?.conversationId;
const conversationId = userMessage.conversationId ?? submission.conversationId;
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
const metadata = data['responseMessage'] ?? data;
@ -387,7 +421,7 @@ export default function useEventHandlers({
...initialResponse,
...metadata,
error: true,
parentMessageId: userMessage?.messageId,
parentMessageId: userMessage.messageId,
};
if (!errorMessage.messageId) {
@ -408,7 +442,7 @@ export default function useEventHandlers({
if (newConversation) {
newConversation({
template: { conversationId: convoId },
preset: tPresetSchema.parse(submission?.conversation),
preset: tPresetSchema.parse(submission.conversation),
});
}
setIsSubmitting(false);
@ -422,7 +456,7 @@ export default function useEventHandlers({
if (newConversation) {
newConversation({
template: { conversationId: convoId },
preset: tPresetSchema.parse(submission?.conversation),
preset: tPresetSchema.parse(submission.conversation),
});
}
setIsSubmitting(false);
@ -438,14 +472,14 @@ export default function useEventHandlers({
const errorResponse = tMessageSchema.parse({
...data,
error: true,
parentMessageId: userMessage?.messageId,
parentMessageId: userMessage.messageId,
});
setMessages([...messages, userMessage, errorResponse]);
if (data.conversationId && paramId === 'new' && newConversation) {
newConversation({
template: { conversationId: data.conversationId },
preset: tPresetSchema.parse(submission?.conversation),
preset: tPresetSchema.parse(submission.conversation),
});
}
@ -459,7 +493,7 @@ export default function useEventHandlers({
async (conversationId = '', submission: TSubmission, messages?: TMessage[]) => {
const runAbortKey = `${conversationId}:${messages?.[messages.length - 1]?.messageId ?? ''}`;
console.log({ conversationId, submission, messages, runAbortKey });
const { endpoint: _endpoint, endpointType } = submission?.conversation || {};
const { endpoint: _endpoint, endpointType } = submission.conversation || {};
const endpoint = endpointType ?? _endpoint;
try {
const response = await fetch(`${EndpointURLs[endpoint ?? '']}/abort`, {
@ -513,7 +547,7 @@ export default function useEventHandlers({
console.error(error);
const convoId = conversationId ?? v4();
const text =
submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : '';
submission.initialResponse.text.length > 45 ? submission.initialResponse.text : '';
const errorMessage = {
...submission,
...submission.initialResponse,
@ -526,7 +560,7 @@ export default function useEventHandlers({
if (newConversation) {
newConversation({
template: { conversationId: convoId },
preset: tPresetSchema.parse(submission?.conversation),
preset: tPresetSchema.parse(submission.conversation),
});
}
setIsSubmitting(false);