@@ -6,6 +6,15 @@ function truncate(string, maxLength = 32) {
66 return string . length > maxLength ? string . slice ( 0 , maxLength ) + '…' : string ;
77}
88
9+ const getCookie = ( name ) => {
10+ return document . cookie
11+ . split ( ';' )
12+ . map ( ( cookie ) => cookie . trim ( ) )
13+ . filter ( ( cookie ) => cookie . startsWith ( `${ name } =` ) )
14+ . map ( ( cookie ) => decodeURIComponent ( cookie . split ( '=' ) [ 1 ] ) )
15+ . shift ( )
16+ }
17+
918const getLangCode = async ( args ) => {
1019 return language
1120}
@@ -142,22 +151,34 @@ const openContactModal = async (args) => {
142151
143152 const submitButton = contactModal . querySelector ( '#chatbot-contact-submit' )
144153
145- $ ( submitButton ) . click ( async ( ) => {
146- const payload = {
147- subject : subjectInput . value ,
148- message : messageInput . value
149- }
154+ $ ( submitButton ) . off ( 'click' ) . on ( 'click' , async ( ) => {
155+ submitButton . disabled = true
150156
151- await fetch ( url , {
152- method : 'POST' ,
153- body : JSON . stringify ( payload ) ,
154- headers : {
155- 'Content-Type' : 'application/json' ,
156- 'X-CSRFToken' : Cookies . get ( 'csrftoken' )
157+ try {
158+ const payload = {
159+ subject : subjectInput . value ,
160+ message : messageInput . value
161+ }
162+
163+ const response = await fetch ( url , {
164+ method : 'POST' ,
165+ body : JSON . stringify ( payload ) ,
166+ headers : {
167+ 'Content-Type' : 'application/json' ,
168+ 'X-CSRFToken' : getCookie ( 'csrftoken' )
169+ }
170+ } )
171+
172+ if ( ! response . ok ) {
173+ throw new Error ( `Failed to send contact email (${ response . status } )` )
157174 }
158- } )
159175
160- $ ( contactModal ) . modal ( 'hide' )
176+ $ ( contactModal ) . modal ( 'hide' )
177+ } catch ( error ) {
178+ console . error ( error )
179+ } finally {
180+ submitButton . disabled = false
181+ }
161182 } )
162183
163184 $ ( contactModal ) . modal ( 'show' )
@@ -184,41 +205,124 @@ const copilotEventHandler = async (event) => {
184205
185206window . copilotEventHandler = copilotEventHandler
186207
187- document . addEventListener ( "DOMContentLoaded" , ( ) => {
188- const observer = new MutationObserver ( ( mutations , obs ) => {
189- const copilot = document . getElementById ( "chainlit-copilot" )
190- const shadow = copilot . shadowRoot
208+ const observedShadows = new WeakSet ( )
209+
210+ const ensureDialogAccessibility = ( {
211+ container,
212+ titleText,
213+ descriptionText,
214+ titleId,
215+ descriptionId
216+ } ) => {
217+ if ( ! container ) {
218+ return
219+ }
220+
221+ const resolvedTitleId = titleId || `${ container . id || 'dialog' } -title`
222+ const resolvedDescriptionId =
223+ descriptionId || `${ container . id || 'dialog' } -description`
224+
225+ if ( ! container . querySelector ( `#${ resolvedTitleId } ` ) ) {
226+ const dialogTitle = document . createElement ( 'h2' )
227+ dialogTitle . id = resolvedTitleId
228+ dialogTitle . setAttribute ( 'data-radix-dialog-title' , '' )
229+ dialogTitle . textContent = titleText
230+ dialogTitle . classList . add ( 'sr-only' )
191231
192- const modal = shadow . getElementById ( "new-chat-dialog" )
193- const confirmButton = shadow . getElementById ( "confirm" )
232+ container . prepend ( dialogTitle )
233+ }
234+
235+ if ( ! container . getAttribute ( 'aria-labelledby' ) ) {
236+ container . setAttribute ( 'aria-labelledby' , resolvedTitleId )
237+ }
238+
239+ if ( descriptionText ) {
240+ if ( ! container . querySelector ( `#${ resolvedDescriptionId } ` ) ) {
241+ const dialogDescription = document . createElement ( 'p' )
242+ dialogDescription . id = resolvedDescriptionId
243+ dialogDescription . textContent = descriptionText
244+ dialogDescription . classList . add ( 'sr-only' )
245+
246+ container . prepend ( dialogDescription )
247+ }
248+
249+ if ( ! container . getAttribute ( 'aria-describedby' ) ) {
250+ container . setAttribute ( 'aria-describedby' , resolvedDescriptionId )
251+ }
252+ }
253+ }
194254
195- if ( modal && confirmButton && ! confirmButton . dataset . hasHandler ) {
196- const handler = async ( event ) => {
197- event . stopPropagation ( )
255+ const patchNewChatDialog = ( shadow ) => {
256+ const modal = shadow . getElementById ( "new-chat-dialog" )
257+ const confirmButton = shadow . getElementById ( "confirm" )
198258
259+ if ( ! modal || ! confirmButton ) {
260+ return
261+ }
262+
263+ const content = modal . matches ?. ( '[data-radix-dialog-content]' )
264+ ? modal
265+ : modal . querySelector ?. ( '[data-radix-dialog-content]' ) || modal
266+
267+ ensureDialogAccessibility ( {
268+ container : content ,
269+ titleText : gettext ( 'Start a new chat' ) ,
270+ descriptionText : gettext (
271+ 'This will reset the current conversation and start a new chat.'
272+ ) ,
273+ titleId : 'chainlit-new-chat-title' ,
274+ descriptionId : 'chainlit-new-chat-description'
275+ } )
276+
277+ if ( ! confirmButton . dataset . hasHandler ) {
278+ confirmButton . dataset . hasHandler = "true"
279+
280+ confirmButton . addEventListener (
281+ "click" ,
282+ ( ) => {
199283 window . sendChainlitMessage ( {
200284 type : "system_message" ,
201285 output : "" ,
202286 metadata : {
203- " action" : "reset_history" ,
204- " project" : parseInt ( projectId )
287+ action : "reset_history" ,
288+ project : projectId
205289 }
206290 } )
291+ } ,
292+ { capture : true }
293+ )
294+ }
295+ }
207296
208- // remove this listener so we don’t fire again
209- confirmButton . removeEventListener ( "click" , handler )
297+ const applyCopilotPatches = ( ) => {
298+ const copilot = document . getElementById ( "chainlit-copilot" )
299+ if ( ! copilot ) {
300+ return
301+ }
210302
211- // trigger the original click (React handles it)
212- setTimeout ( ( ) => confirmButton . click ( ) , 500 )
303+ const shadow = copilot . shadowRoot
213304
214- // mark handler as attached to avoid duplicates
215- confirmButton . dataset . hasHandler = "true"
216- }
305+ if ( ! shadow ) {
306+ return
307+ }
217308
218- // attach the listener
219- confirmButton . addEventListener ( "click" , handler )
220- }
221- } )
309+ patchNewChatDialog ( shadow )
310+
311+ if ( ! observedShadows . has ( shadow ) ) {
312+ const shadowObserver = new MutationObserver ( ( ) => {
313+ patchNewChatDialog ( shadow )
314+ } )
315+
316+ shadowObserver . observe ( shadow , { childList : true , subtree : true } )
317+ observedShadows . add ( shadow )
318+ }
319+ }
320+
321+ document . addEventListener ( "DOMContentLoaded" , ( ) => {
322+ const observer = new MutationObserver ( applyCopilotPatches )
323+
324+ // Run once in case the widget is already rendered before we start observing.
325+ applyCopilotPatches ( )
222326
223327 observer . observe ( document . body , { childList : true , subtree : true } )
224- } ) ;
328+ } ) ;
0 commit comments