@@ -86,9 +86,16 @@ export class PlaygroundContent extends SignalWatcher(
86
86
@state ( )
87
87
accessor sessions : CopilotSessionType [ ] = [ ] ;
88
88
89
+ @state ( )
90
+ accessor sharedInputValue : string = '' ;
91
+
89
92
private rootSessionId : string | undefined = undefined ;
90
93
91
- private readonly _getSessions = async ( ) => {
94
+ private isUpdatingTextareas = false ;
95
+
96
+ private isSending = false ;
97
+
98
+ private readonly getSessions = async ( ) => {
92
99
const sessions =
93
100
( await AIProvider . session ?. getSessions (
94
101
this . doc . workspace . id ,
@@ -107,7 +114,7 @@ export class PlaygroundContent extends SignalWatcher(
107
114
} ) ;
108
115
if ( rootSessionId ) {
109
116
this . rootSessionId = rootSessionId ;
110
- const forkSession = await this . _forkSession ( rootSessionId ) ;
117
+ const forkSession = await this . forkSession ( rootSessionId ) ;
111
118
if ( forkSession ) {
112
119
this . sessions = [ forkSession ] ;
113
120
}
@@ -120,15 +127,15 @@ export class PlaygroundContent extends SignalWatcher(
120
127
if ( childSessions . length > 0 ) {
121
128
this . sessions = childSessions ;
122
129
} else {
123
- const forkSession = await this . _forkSession ( rootSession . id ) ;
130
+ const forkSession = await this . forkSession ( rootSession . id ) ;
124
131
if ( forkSession ) {
125
132
this . sessions = [ forkSession ] ;
126
133
}
127
134
}
128
135
}
129
136
} ;
130
137
131
- private readonly _forkSession = async ( parentSessionId : string ) => {
138
+ private readonly forkSession = async ( parentSessionId : string ) => {
132
139
const forkSessionId = await AIProvider . forkChat ?.( {
133
140
workspaceId : this . doc . workspace . id ,
134
141
docId : this . doc . id ,
@@ -144,19 +151,171 @@ export class PlaygroundContent extends SignalWatcher(
144
151
) ;
145
152
} ;
146
153
147
- private readonly _addChat = async ( ) => {
154
+ private readonly addChat = async ( ) => {
148
155
if ( ! this . rootSessionId ) {
149
156
return ;
150
157
}
151
- const forkSession = await this . _forkSession ( this . rootSessionId ) ;
158
+ const forkSession = await this . forkSession ( this . rootSessionId ) ;
152
159
if ( forkSession ) {
153
160
this . sessions = [ ...this . sessions , forkSession ] ;
154
161
}
155
162
} ;
156
163
164
+ private setupTextareaSync ( ) {
165
+ const observer = new MutationObserver ( ( ) => {
166
+ this . syncAllTextareas ( ) ;
167
+ this . syncAllSendButtons ( ) ;
168
+ } ) ;
169
+ observer . observe ( this , {
170
+ childList : true ,
171
+ subtree : true ,
172
+ } ) ;
173
+ this . _disposables . add ( ( ) => observer . disconnect ( ) ) ;
174
+ this . syncAllTextareas ( ) ;
175
+ this . syncAllSendButtons ( ) ;
176
+ }
177
+
178
+ private syncAllTextareas ( ) {
179
+ const textareas = this . getAllTextareas ( ) ;
180
+ textareas . forEach ( textarea => {
181
+ this . setupTextareaListeners ( textarea ) ;
182
+ } ) ;
183
+ }
184
+
185
+ private getAllTextareas ( ) : HTMLTextAreaElement [ ] {
186
+ return Array . from (
187
+ this . querySelectorAll (
188
+ 'ai-chat-input textarea[data-testid="chat-panel-input"]'
189
+ )
190
+ ) as HTMLTextAreaElement [ ] ;
191
+ }
192
+
193
+ private setupTextareaListeners ( textarea : HTMLTextAreaElement ) {
194
+ if ( textarea . dataset . synced ) return ;
195
+
196
+ textarea . dataset . synced = 'true' ;
197
+
198
+ const handleInput = ( event : Event ) => {
199
+ if ( this . isUpdatingTextareas ) return ;
200
+
201
+ const target = event . target as HTMLTextAreaElement ;
202
+ const newValue = target . value ;
203
+
204
+ if ( newValue !== this . sharedInputValue ) {
205
+ this . sharedInputValue = newValue ;
206
+ this . updateOtherTextareas ( target , newValue ) ;
207
+ }
208
+ } ;
209
+
210
+ // paste need delay to ensure the content is fully processed
211
+ const handlePaste = ( event : ClipboardEvent ) => {
212
+ if ( this . isUpdatingTextareas ) return ;
213
+
214
+ const target = event . target as HTMLTextAreaElement ;
215
+ setTimeout ( ( ) => {
216
+ const newValue = target . value ;
217
+ if ( newValue !== this . sharedInputValue ) {
218
+ this . sharedInputValue = newValue ;
219
+ this . updateOtherTextareas ( target , newValue ) ;
220
+ }
221
+ } , 0 ) ;
222
+ } ;
223
+
224
+ textarea . addEventListener ( 'input' , handleInput ) ;
225
+ textarea . addEventListener ( 'paste' , handlePaste ) ;
226
+
227
+ this . _disposables . add ( ( ) => {
228
+ textarea . removeEventListener ( 'input' , handleInput ) ;
229
+ textarea . removeEventListener ( 'paste' , handlePaste ) ;
230
+ } ) ;
231
+ }
232
+
233
+ private updateOtherTextareas (
234
+ sourceTextarea : HTMLTextAreaElement ,
235
+ newValue : string
236
+ ) {
237
+ this . isUpdatingTextareas = true ;
238
+
239
+ const textareas = this . getAllTextareas ( ) ;
240
+ textareas . forEach ( textarea => {
241
+ if ( textarea !== sourceTextarea && textarea . value !== newValue ) {
242
+ textarea . value = newValue ;
243
+ this . triggerInputEvent ( textarea ) ;
244
+ }
245
+ } ) ;
246
+
247
+ this . isUpdatingTextareas = false ;
248
+ }
249
+
250
+ private triggerInputEvent ( textarea : HTMLTextAreaElement ) {
251
+ const inputEvent = new Event ( 'input' , { bubbles : true } ) ;
252
+ textarea . dispatchEvent ( inputEvent ) ;
253
+ }
254
+
255
+ private syncAllSendButtons ( ) {
256
+ const sendButtons = this . getAllSendButtons ( ) ;
257
+ sendButtons . forEach ( button => {
258
+ this . setupSendButtonListener ( button ) ;
259
+ } ) ;
260
+ }
261
+
262
+ private getAllSendButtons ( ) : HTMLElement [ ] {
263
+ return Array . from (
264
+ this . querySelectorAll ( '[data-testid="chat-panel-send"]' )
265
+ ) as HTMLElement [ ] ;
266
+ }
267
+
268
+ private setupSendButtonListener ( button : HTMLElement ) {
269
+ if ( button . dataset . syncSetup ) return ;
270
+
271
+ button . dataset . syncSetup = 'true' ;
272
+
273
+ const handleSendClick = async ( _event : MouseEvent ) => {
274
+ if ( this . isSending ) {
275
+ return ;
276
+ }
277
+ this . isSending = true ;
278
+ try {
279
+ await this . triggerOtherSendButtons ( button ) ;
280
+ } finally {
281
+ this . isSending = false ;
282
+ }
283
+ } ;
284
+
285
+ button . addEventListener ( 'click' , handleSendClick ) ;
286
+
287
+ this . _disposables . add ( ( ) => {
288
+ button . removeEventListener ( 'click' , handleSendClick ) ;
289
+ } ) ;
290
+ }
291
+
292
+ private async triggerOtherSendButtons ( sourceButton : HTMLElement ) {
293
+ const allSendButtons = this . getAllSendButtons ( ) ;
294
+ const otherButtons = allSendButtons . filter (
295
+ button => button !== sourceButton
296
+ ) ;
297
+
298
+ const clickPromises = otherButtons . map ( async button => {
299
+ try {
300
+ const clickEvent = new MouseEvent ( 'click' , {
301
+ bubbles : true ,
302
+ cancelable : true ,
303
+ view : window ,
304
+ } ) ;
305
+
306
+ button . dispatchEvent ( clickEvent ) ;
307
+ } catch ( error ) {
308
+ console . error ( error ) ;
309
+ }
310
+ } ) ;
311
+
312
+ await Promise . allSettled ( clickPromises ) ;
313
+ }
314
+
157
315
override connectedCallback ( ) {
158
316
super . connectedCallback ( ) ;
159
- this . _getSessions ( ) . catch ( console . error ) ;
317
+ this . getSessions ( ) . catch ( console . error ) ;
318
+ this . setupTextareaSync ( ) ;
160
319
}
161
320
162
321
override render ( ) {
@@ -179,7 +338,7 @@ export class PlaygroundContent extends SignalWatcher(
179
338
.extensions=${ this . extensions }
180
339
.affineFeatureFlagService=${ this . affineFeatureFlagService }
181
340
.session=${ session }
182
- .addChat=${ this . _addChat }
341
+ .addChat=${ this . addChat }
183
342
> </ playground-chat >
184
343
</ div >
185
344
`
0 commit comments