1
+ // client/src/a11y/LiveAnnouncer.tsx
1
2
import React , { useState , useCallback , useRef , useEffect , useMemo } from 'react' ;
2
3
import { findLastSeparatorIndex } from 'librechat-data-provider' ;
3
4
import type { AnnounceOptions } from '~/Providers/AnnouncerContext' ;
@@ -15,30 +16,35 @@ interface AnnouncementItem {
15
16
isAssertive : boolean ;
16
17
}
17
18
18
- const CHUNK_SIZE = 50 ;
19
- const MIN_ANNOUNCEMENT_DELAY = 400 ;
19
+ /** Chunk size for processing text */
20
+ const CHUNK_SIZE = 200 ;
21
+ /** Minimum delay between announcements */
22
+ const MIN_ANNOUNCEMENT_DELAY = 1000 ;
23
+ /** Delay before clearing the live region */
24
+ const CLEAR_DELAY = 5000 ;
20
25
/** Regex to remove *, `, and _ from message text */
21
26
const replacementRegex = / [ * ` _ ] / g;
22
27
23
28
const LiveAnnouncer : React . FC < LiveAnnouncerProps > = ( { children } ) => {
24
- const [ politeMessageId , setPoliteMessageId ] = useState ( '' ) ;
25
- const [ assertiveMessageId , setAssertiveMessageId ] = useState ( '' ) ;
26
- const [ announcePoliteMessage , setAnnouncePoliteMessage ] = useState ( '' ) ;
27
- const [ announceAssertiveMessage , setAnnounceAssertiveMessage ] = useState ( '' ) ;
29
+ const [ statusMessage , setStatusMessage ] = useState ( '' ) ;
30
+ const [ responseMessage , setResponseMessage ] = useState ( '' ) ;
28
31
29
32
const counterRef = useRef ( 0 ) ;
30
33
const isAnnouncingRef = useRef ( false ) ;
31
34
const politeProcessedTextRef = useRef ( '' ) ;
32
35
const queueRef = useRef < AnnouncementItem [ ] > ( [ ] ) ;
33
36
const timeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
37
+ const lastAnnouncementTimeRef = useRef ( 0 ) ;
34
38
35
39
const localize = useLocalize ( ) ;
36
40
41
+ /** Generates a unique ID for announcement messages */
37
42
const generateUniqueId = ( prefix : string ) => {
38
43
counterRef . current += 1 ;
39
44
return `${ prefix } -${ counterRef . current } ` ;
40
45
} ;
41
46
47
+ /** Processes the text in chunks and returns a chunk of text */
42
48
const processChunks = ( text : string , processedTextRef : React . MutableRefObject < string > ) => {
43
49
const remainingText = text . slice ( processedTextRef . current . length ) ;
44
50
@@ -73,59 +79,76 @@ const LiveAnnouncer: React.FC<LiveAnnouncerProps> = ({ children }) => {
73
79
[ localize ] ,
74
80
) ;
75
81
76
- const announceNextInQueue = useCallback ( ( ) => {
77
- if ( queueRef . current . length > 0 && ! isAnnouncingRef . current ) {
78
- isAnnouncingRef . current = true ;
79
- const nextAnnouncement = queueRef . current . shift ( ) ;
80
- if ( nextAnnouncement ) {
81
- const { message : _msg , id, isAssertive } = nextAnnouncement ;
82
- const setMessage = isAssertive ? setAnnounceAssertiveMessage : setAnnouncePoliteMessage ;
83
- const setMessageId = isAssertive ? setAssertiveMessageId : setPoliteMessageId ;
84
-
85
- setMessage ( '' ) ;
86
- setMessageId ( '' ) ;
87
-
88
- /* Force a re-render before setting the new message */
89
- setTimeout ( ( ) => {
90
- const message = ( events [ _msg ] ?? _msg ) . replace ( replacementRegex , '' ) ;
91
- setMessage ( message ) ;
92
- setMessageId ( id ) ;
93
-
94
- if ( timeoutRef . current ) {
95
- clearTimeout ( timeoutRef . current ) ;
96
- }
82
+ const announceMessage = useCallback (
83
+ ( message : string , isAssertive : boolean ) => {
84
+ const setMessage = isAssertive ? setStatusMessage : setResponseMessage ;
85
+ setMessage ( message ) ;
97
86
98
- timeoutRef . current = setTimeout ( ( ) => {
99
- isAnnouncingRef . current = false ;
100
- announceNextInQueue ( ) ;
101
- } , MIN_ANNOUNCEMENT_DELAY ) ;
102
- } , 0 ) ;
87
+ if ( timeoutRef . current ) {
88
+ clearTimeout ( timeoutRef . current ) ;
103
89
}
104
- }
105
- } , [ events ] ) ;
90
+
91
+ lastAnnouncementTimeRef . current = Date . now ( ) ;
92
+ isAnnouncingRef . current = true ;
93
+
94
+ timeoutRef . current = setTimeout (
95
+ ( ) => {
96
+ isAnnouncingRef . current = false ;
97
+ setMessage ( '' ) ; // Clear the message after a delay
98
+ if ( queueRef . current . length > 0 ) {
99
+ const nextAnnouncement = queueRef . current . shift ( ) ;
100
+ if ( nextAnnouncement ) {
101
+ const { message : _msg , isAssertive } = nextAnnouncement ;
102
+ const nextMessage = ( events [ _msg ] ?? _msg ) . replace ( replacementRegex , '' ) ;
103
+ announceMessage ( nextMessage , isAssertive ) ;
104
+ }
105
+ }
106
+ } ,
107
+ isAssertive ? MIN_ANNOUNCEMENT_DELAY : CLEAR_DELAY ,
108
+ ) ;
109
+ } ,
110
+ [ events ] ,
111
+ ) ;
106
112
107
113
const addToQueue = useCallback (
108
114
( item : AnnouncementItem ) => {
109
- queueRef . current . push ( item ) ;
110
- announceNextInQueue ( ) ;
115
+ if ( item . isAssertive ) {
116
+ /* For assertive messages, clear the queue and announce immediately */
117
+ queueRef . current = [ ] ;
118
+ const { message : _msg , isAssertive } = item ;
119
+ const message = ( events [ _msg ] ?? _msg ) . replace ( replacementRegex , '' ) ;
120
+ announceMessage ( message , isAssertive ) ;
121
+ } else {
122
+ queueRef . current . push ( item ) ;
123
+ if ( ! isAnnouncingRef . current ) {
124
+ const nextAnnouncement = queueRef . current . shift ( ) ;
125
+ if ( nextAnnouncement ) {
126
+ const { message : _msg , isAssertive } = nextAnnouncement ;
127
+ const message = ( events [ _msg ] ?? _msg ) . replace ( replacementRegex , '' ) ;
128
+ announceMessage ( message , isAssertive ) ;
129
+ }
130
+ }
131
+ }
111
132
} ,
112
- [ announceNextInQueue ] ,
133
+ [ events , announceMessage ] ,
113
134
) ;
114
135
136
+ /** Announces a polite message */
115
137
const announcePolite = useCallback (
116
138
( { message, id, isStream = false , isComplete = false } : AnnounceOptions ) => {
117
139
const announcementId = id ?? generateUniqueId ( 'polite' ) ;
118
- if ( isStream ) {
140
+ if ( isStream || isComplete ) {
119
141
const chunk = processChunks ( message , politeProcessedTextRef ) ;
120
142
if ( chunk ) {
121
143
addToQueue ( { message : chunk , id : announcementId , isAssertive : false } ) ;
122
144
}
123
- } else if ( isComplete ) {
124
- const remainingText = message . slice ( politeProcessedTextRef . current . length ) ;
125
- if ( remainingText . trim ( ) ) {
126
- addToQueue ( { message : remainingText . trim ( ) , id : announcementId , isAssertive : false } ) ;
145
+ if ( isComplete ) {
146
+ const remainingText = message . slice ( politeProcessedTextRef . current . length ) ;
147
+ if ( remainingText . trim ( ) ) {
148
+ addToQueue ( { message : remainingText . trim ( ) , id : announcementId , isAssertive : false } ) ;
149
+ }
150
+ politeProcessedTextRef . current = '' ;
127
151
}
128
- politeProcessedTextRef . current = '' ;
129
152
} else {
130
153
addToQueue ( { message, id : announcementId , isAssertive : false } ) ;
131
154
politeProcessedTextRef . current = '' ;
@@ -134,6 +157,7 @@ const LiveAnnouncer: React.FC<LiveAnnouncerProps> = ({ children }) => {
134
157
[ addToQueue ] ,
135
158
) ;
136
159
160
+ /** Announces an assertive message */
137
161
const announceAssertive = useCallback (
138
162
( { message, id } : AnnounceOptions ) => {
139
163
const announcementId = id ?? generateUniqueId ( 'assertive' ) ;
@@ -160,12 +184,7 @@ const LiveAnnouncer: React.FC<LiveAnnouncerProps> = ({ children }) => {
160
184
return (
161
185
< AnnouncerContext . Provider value = { contextValue } >
162
186
{ children }
163
- < Announcer
164
- assertiveMessage = { announceAssertiveMessage }
165
- assertiveMessageId = { assertiveMessageId }
166
- politeMessage = { announcePoliteMessage }
167
- politeMessageId = { politeMessageId }
168
- />
187
+ < Announcer statusMessage = { statusMessage } responseMessage = { responseMessage } />
169
188
</ AnnouncerContext . Provider >
170
189
) ;
171
190
} ;
0 commit comments