1
1
// client/src/a11y/LiveAnnouncer.tsx
2
2
import React , { useState , useCallback , useRef , useEffect , useMemo } from 'react' ;
3
- import { findLastSeparatorIndex } from 'librechat-data-provider' ;
4
3
import type { AnnounceOptions } from '~/Providers/AnnouncerContext' ;
5
4
import AnnouncerContext from '~/Providers/AnnouncerContext' ;
6
5
import useLocalize from '~/hooks/useLocalize' ;
@@ -10,161 +9,53 @@ interface LiveAnnouncerProps {
10
9
children : React . ReactNode ;
11
10
}
12
11
13
- interface AnnouncementItem {
14
- message : string ;
15
- id : string ;
16
- isAssertive : boolean ;
17
- }
18
-
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 ;
25
- /** Regex to remove *, `, and _ from message text */
26
- const replacementRegex = / [ * ` _ ] / g;
27
-
28
12
const LiveAnnouncer : React . FC < LiveAnnouncerProps > = ( { children } ) => {
29
13
const [ statusMessage , setStatusMessage ] = useState ( '' ) ;
30
- const [ responseMessage , setResponseMessage ] = useState ( '' ) ;
14
+ const [ logMessage , setLogMessage ] = useState ( '' ) ;
31
15
32
- const counterRef = useRef ( 0 ) ;
33
- const isAnnouncingRef = useRef ( false ) ;
34
- const politeProcessedTextRef = useRef ( '' ) ;
35
- const queueRef = useRef < AnnouncementItem [ ] > ( [ ] ) ;
36
- const timeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
37
- const lastAnnouncementTimeRef = useRef ( 0 ) ;
16
+ const statusTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
38
17
39
18
const localize = useLocalize ( ) ;
40
19
41
- /** Generates a unique ID for announcement messages */
42
- const generateUniqueId = ( prefix : string ) => {
43
- counterRef . current += 1 ;
44
- return `${ prefix } -${ counterRef . current } ` ;
45
- } ;
46
-
47
- /** Processes the text in chunks and returns a chunk of text */
48
- const processChunks = ( text : string , processedTextRef : React . MutableRefObject < string > ) => {
49
- const remainingText = text . slice ( processedTextRef . current . length ) ;
50
-
51
- if ( remainingText . length < CHUNK_SIZE ) {
52
- return '' ; /* Not enough characters to process */
53
- }
54
-
55
- let separatorIndex = - 1 ;
56
- let startIndex = CHUNK_SIZE ;
57
-
58
- while ( separatorIndex === - 1 && startIndex <= remainingText . length ) {
59
- separatorIndex = findLastSeparatorIndex ( remainingText . slice ( startIndex ) ) ;
60
- if ( separatorIndex !== - 1 ) {
61
- separatorIndex += startIndex ; /* Adjust the index to account for the starting position */
62
- } else {
63
- startIndex += CHUNK_SIZE ; /* Move the starting position by another CHUNK_SIZE characters */
64
- }
65
- }
66
-
67
- if ( separatorIndex === - 1 ) {
68
- return '' ; /* No separator found, wait for more text */
69
- }
70
-
71
- const chunkText = remainingText . slice ( 0 , separatorIndex + 1 ) ;
72
- processedTextRef . current += chunkText ;
73
- return chunkText . trim ( ) ;
74
- } ;
75
-
76
- /** Localized event announcements, i.e., "the AI is replying, finished, etc." */
77
20
const events : Record < string , string | undefined > = useMemo (
78
- ( ) => ( { start : localize ( 'com_a11y_start' ) , end : localize ( 'com_a11y_end' ) } ) ,
21
+ ( ) => ( {
22
+ start : localize ( 'com_a11y_start' ) ,
23
+ end : localize ( 'com_a11y_end' ) ,
24
+ composing : localize ( 'com_a11y_ai_composing' ) ,
25
+ } ) ,
79
26
[ localize ] ,
80
27
) ;
81
28
82
- const announceMessage = useCallback (
83
- ( message : string , isAssertive : boolean ) => {
84
- const setMessage = isAssertive ? setStatusMessage : setResponseMessage ;
85
- setMessage ( message ) ;
29
+ const announceStatus = useCallback ( ( message : string ) => {
30
+ if ( statusTimeoutRef . current ) {
31
+ clearTimeout ( statusTimeoutRef . current ) ;
32
+ }
86
33
87
- if ( timeoutRef . current ) {
88
- clearTimeout ( timeoutRef . current ) ;
89
- }
34
+ setStatusMessage ( message ) ;
90
35
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
- ) ;
36
+ statusTimeoutRef . current = setTimeout ( ( ) => {
37
+ setStatusMessage ( '' ) ;
38
+ } , 1000 ) ;
39
+ } , [ ] ) ;
112
40
113
- const addToQueue = useCallback (
114
- ( item : AnnouncementItem ) => {
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
- }
132
- } ,
133
- [ events , announceMessage ] ,
134
- ) ;
41
+ const announceLog = useCallback ( ( message : string ) => {
42
+ setLogMessage ( message ) ;
43
+ } , [ ] ) ;
135
44
136
- /** Announces a polite message */
137
45
const announcePolite = useCallback (
138
- ( { message, id, isStream = false , isComplete = false } : AnnounceOptions ) => {
139
- const announcementId = id ?? generateUniqueId ( 'polite' ) ;
140
- if ( isStream || isComplete ) {
141
- const chunk = processChunks ( message , politeProcessedTextRef ) ;
142
- if ( chunk ) {
143
- addToQueue ( { message : chunk , id : announcementId , isAssertive : false } ) ;
144
- }
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 = '' ;
151
- }
46
+ ( { message, isStatus = false } : AnnounceOptions ) => {
47
+ const finalMessage = ( events [ message ] ?? message ) . replace ( / [ * ` _ ] / g, '' ) ;
48
+
49
+ if ( isStatus ) {
50
+ announceStatus ( finalMessage ) ;
152
51
} else {
153
- addToQueue ( { message, id : announcementId , isAssertive : false } ) ;
154
- politeProcessedTextRef . current = '' ;
52
+ announceLog ( finalMessage ) ;
155
53
}
156
54
} ,
157
- [ addToQueue ] ,
55
+ [ events , announceStatus , announceLog ] ,
158
56
) ;
159
57
160
- /** Announces an assertive message */
161
- const announceAssertive = useCallback (
162
- ( { message, id } : AnnounceOptions ) => {
163
- const announcementId = id ?? generateUniqueId ( 'assertive' ) ;
164
- addToQueue ( { message, id : announcementId , isAssertive : true } ) ;
165
- } ,
166
- [ addToQueue ] ,
167
- ) ;
58
+ const announceAssertive = announcePolite ;
168
59
169
60
const contextValue = {
170
61
announcePolite,
@@ -173,18 +64,16 @@ const LiveAnnouncer: React.FC<LiveAnnouncerProps> = ({ children }) => {
173
64
174
65
useEffect ( ( ) => {
175
66
return ( ) => {
176
- queueRef . current = [ ] ;
177
- isAnnouncingRef . current = false ;
178
- if ( timeoutRef . current ) {
179
- clearTimeout ( timeoutRef . current ) ;
67
+ if ( statusTimeoutRef . current ) {
68
+ clearTimeout ( statusTimeoutRef . current ) ;
180
69
}
181
70
} ;
182
71
} , [ ] ) ;
183
72
184
73
return (
185
74
< AnnouncerContext . Provider value = { contextValue } >
186
75
{ children }
187
- < Announcer statusMessage = { statusMessage } responseMessage = { responseMessage } />
76
+ < Announcer statusMessage = { statusMessage } logMessage = { logMessage } />
188
77
</ AnnouncerContext . Provider >
189
78
) ;
190
79
} ;
0 commit comments