@lowdefy/blocks-antd-x 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,784 +12,48 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import React, { useRef, useMemo, useEffect, useState, useCallback } from 'react';
16
- import { useChat } from '@ai-sdk/react';
17
- import { lastAssistantMessageIsCompleteWithApprovalResponses, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
18
- import { FileCard, Prompts, Sender } from '@ant-design/x';
19
- import { Button } from 'antd';
20
- import { PaperClipOutlined } from '@ant-design/icons';
21
- import { isReserved, setKey, type } from '@lowdefy/helpers';
22
- import getLegacyObjectUrl from '@lowdefy/blocks-files/utils/getLegacyObjectUrl.js';
23
- import getUploadPolicy from '@lowdefy/blocks-files/utils/getUploadPolicy.js';
24
- import uploadFile from '@lowdefy/blocks-files/utils/uploadFile.js';
25
- import { getFileCardType, getFileCardIcon } from './fileCardUtils.js';
26
- import DrawerWrapper from './DrawerWrapper.js';
27
- import createLowdefyChatTransport from './LowdefyChatTransport.js';
28
- import MessageList from './MessageList.js';
29
- import useAgentEvents, { collectExternalEventIds } from './useAgentEvents.js';
30
- import WelcomeScreen from './WelcomeScreen.js';
31
- function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageId, properties }) {
32
- const { agentId, urlQuery, sharedState, welcome, messageDisplay, sender, conversationId, feedbackValues: storedFeedbackValues, messages: externalMessages, display, drawer: drawerConfig, suggestions } = properties;
33
- const senderRef = useRef(null);
34
- const finishMetaRef = useRef(null);
35
- const fileInputRef = useRef(null);
36
- const [attachedFiles, setAttachedFiles] = useState([]);
37
- const [dragOver, setDragOver] = useState(false);
38
- // Controlled composer value, so a starter prompt (or the setInput method) can
39
- // fill the box, and a send can leave typed text in place when it is rejected.
40
- const [inputValue, setInputValue] = useState('');
41
- // Mirror the operator-evaluated sharedState object into a ref so transport.body()
42
- // sees the freshest value at send time without re-constructing the transport.
43
- const sharedStateRef = useRef(null);
44
- sharedStateRef.current = type.isObject(sharedState) && Object.keys(sharedState).length > 0 ? sharedState : null;
45
- const attachmentsConfig = sender?.attachments;
46
- const uploadPolicyRequestId = attachmentsConfig?.uploadPolicyRequestId ?? attachmentsConfig?.s3PostPolicyRequestId;
47
- const downloadPolicyRequestId = attachmentsConfig?.downloadPolicyRequestId;
48
- const switchConfigs = sender?.switches ?? [];
49
- const [headerOpen, setHeaderOpen] = useState(sender?.header?.open ?? true);
50
- const [switchState, setSwitchState] = useState(()=>{
51
- const initial = {};
52
- for (const sw of switchConfigs){
53
- initial[sw.key] = sw.default ?? false;
54
- }
55
- return initial;
56
- });
57
- // When no conversationId is supplied, mint a stable session id so every turn
58
- // (including a welcome prompt sent before the app sets the prop) posts a
59
- // consistent id. App-supplied ids remain authoritative.
60
- const mintedIdRef = useRef(null);
61
- if (!conversationId && !mintedIdRef.current) {
62
- mintedIdRef.current = crypto.randomUUID();
63
- }
64
- const effectiveConversationId = conversationId ?? mintedIdRef.current;
65
- const urlQueryKey = JSON.stringify(urlQuery ?? null);
66
- const transport = useMemo(()=>createLowdefyChatTransport({
67
- pageId,
68
- agentId,
69
- conversationId: effectiveConversationId,
70
- urlQuery,
71
- sharedStateRef
72
- }), [
73
- pageId,
74
- agentId,
75
- effectiveConversationId,
76
- urlQueryKey
77
- ]);
78
- const bubbleListRef = useRef(null);
79
- const { messages, sendMessage, status, stop, addToolApprovalResponse, addToolOutput, setMessages, regenerate, clearError } = useChat({
80
- // Key the Chat instance by conversation: without an id, useChat creates its
81
- // Chat once per mount and CAPTURES that transport — the transport rebuilt by
82
- // the useMemo above on a conversationId change is silently ignored, so every
83
- // send posts under the mount-time conversationId. Continuing a conversation
84
- // the app selected later then persists the whole transcript under the stale
85
- // id (a duplicate conversation doc). Keying by id makes useChat swap Chat
86
- // instances — and adopt the rebuilt transport — when the conversation
87
- // changes; the clear-on-id-change and external-message-sync effects below
88
- // compose with the swap unchanged.
89
- id: effectiveConversationId,
90
- transport,
91
- experimental_throttle: 50,
92
- sendAutomaticallyWhen: (args)=>lastAssistantMessageIsCompleteWithToolCalls(args) || lastAssistantMessageIsCompleteWithApprovalResponses(args),
93
- async onToolCall ({ toolCall }) {
94
- if (toolCall.dynamic) return;
95
- if (toolCall.toolName === 'update-page-state') {
96
- try {
97
- const updates = toolCall.input?.updates ?? {};
98
- // Allowlist writes against the keys currently exposed via sharedState —
99
- // the tool's description tells the agent "keys must match field names
100
- // visible in shared state", but we enforce it here so a hallucinated
101
- // key can't mutate arbitrary page state.
102
- const allowedKeys = new Set(Object.keys(sharedStateRef.current ?? {}));
103
- const writable = {};
104
- const ignored = [];
105
- for (const [key, value] of Object.entries(updates)){
106
- if (!allowedKeys.has(key)) {
107
- ignored.push(key);
108
- continue;
109
- }
110
- // Same policy as a non-allowlisted key: drop it and report it back
111
- // so the model can self-correct on the next turn.
112
- if (isReserved(key)) {
113
- ignored.push(key);
114
- continue;
115
- }
116
- setKey(writable, key, value);
117
- }
118
- if (Object.keys(writable).length > 0) {
119
- await methods.triggerEvent({
120
- name: '__updatePageState',
121
- event: writable
122
- });
123
- }
124
- addToolOutput({
125
- tool: 'update-page-state',
126
- toolCallId: toolCall.toolCallId,
127
- output: {
128
- ok: true,
129
- written: Object.keys(writable),
130
- ...ignored.length > 0 ? {
131
- ignored
132
- } : {}
133
- }
134
- });
135
- } catch (err) {
136
- addToolOutput({
137
- tool: 'update-page-state',
138
- toolCallId: toolCall.toolCallId,
139
- state: 'output-error',
140
- errorText: err.message
141
- });
142
- }
143
- }
144
- },
145
- onError: (error)=>{
146
- // A request that fails after the stream's `start` chunk leaves the
147
- // assistant shell the SDK pushed on that chunk — id set, parts: [] — in
148
- // the message list. Left there, every later send fails UIMessage
149
- // validation ("Message must contain at least one part") and the
150
- // conversation is dead until a reload. Drop empty shells so the user can
151
- // simply try again; there is nothing in them to show or to send.
152
- setMessages((prev)=>prev.filter((m)=>m.role !== 'assistant' || (m.parts?.length ?? 0) > 0));
153
- methods.triggerEvent({
154
- name: 'onError',
155
- event: {
156
- message: error.message
157
- }
158
- });
159
- },
160
- onFinish: (options)=>{
161
- finishMetaRef.current = {
162
- finishReason: options.finishReason,
163
- isAbort: options.isAbort,
164
- isDisconnect: options.isDisconnect
165
- };
166
- },
167
- onData: (dataPart)=>{
168
- methods.triggerEvent({
169
- name: 'onDataPart',
170
- event: {
171
- type: dataPart.type,
172
- data: dataPart.data,
173
- id: dataPart.id
174
- }
175
- });
176
- }
177
- });
178
- // The control is otherwise write-only: it reports a rating and immediately forgets it,
179
- // so the thumb un-highlights on the next render and the message looks unrated. This holds
180
- // what was clicked THIS visit; the block still persists nothing itself.
181
- const [feedbackValues, setFeedbackValues] = useState({});
182
- // Ratings the app already knows about, keyed by message id — what a reload or a
183
- // conversation switch would otherwise lose, since the block stores nothing. A click this
184
- // visit wins over the stored value, so the thumb responds immediately and a rating the
185
- // user has just withdrawn is not re-lit by a stale prop.
186
- // Not memoised: the property arrives from operators that rebuild it every render, so a
187
- // dependency on it would miss every time — the same reason the message sync below
188
- // compares by count and id rather than by reference.
189
- const effectiveFeedbackValues = {
190
- ...storedFeedbackValues ?? {},
191
- ...feedbackValues
192
- };
193
- // Clear messages when conversationId changes so the new conversation starts clean.
194
- // Developers load saved messages via the messages property if needed.
195
- const prevConversationIdRef = useRef(effectiveConversationId);
196
- useEffect(()=>{
197
- if (effectiveConversationId !== prevConversationIdRef.current) {
198
- prevConversationIdRef.current = effectiveConversationId;
199
- setMessages([]);
200
- // Ratings clicked in the thread being left must not carry over: they are keyed by
201
- // message id, and the incoming conversation supplies its own through feedbackValues.
202
- setFeedbackValues({});
203
- }
204
- }, [
205
- effectiveConversationId,
206
- setMessages
207
- ]);
208
- // Sync external messages when provided — undefined means "not provided" (no sync),
209
- // null means "clear messages", array means "load these messages".
210
- // Compare by count + last ID to avoid re-syncing on every Lowdefy re-render
211
- // (operators like _state create new array references even when data hasn't changed).
212
- const prevExternalRef = useRef({
213
- count: 0,
214
- lastId: null
215
- });
216
- // Event-dedup ids of externally loaded messages. Restored history must not
217
- // replay onToolCall / onToolResult / onUserMessage / onTitleGenerated side
218
- // effects — useAgentEvents suppresses ids in this ref.
219
- const externalIdsRef = useRef(null);
220
- useEffect(()=>{
221
- if (type.isUndefined(externalMessages)) return;
222
- const msgs = externalMessages ?? [];
223
- const count = msgs.length;
224
- const lastId = count > 0 ? msgs[count - 1]?.id : null;
225
- if (count !== prevExternalRef.current.count || lastId !== prevExternalRef.current.lastId) {
226
- prevExternalRef.current = {
227
- count,
228
- lastId
229
- };
230
- externalIdsRef.current = collectExternalEventIds(msgs);
231
- setMessages(msgs);
232
- }
233
- }, [
234
- externalMessages,
235
- setMessages
236
- ]);
237
- // Register CallMethod methods so YAML actions can control the chat.
238
- useEffect(()=>{
239
- methods.registerMethod('regenerate', (args)=>{
240
- regenerate(args?.messageId ? {
241
- messageId: args.messageId
242
- } : undefined);
243
- });
244
- methods.registerMethod('setMessages', (args)=>{
245
- const msgs = args?.messages ?? [];
246
- externalIdsRef.current = collectExternalEventIds(msgs);
247
- setMessages(msgs);
248
- });
249
- methods.registerMethod('sendMessage', (args)=>{
250
- if (args?.text) {
251
- sendMessage({
252
- text: args.text,
253
- ...args.files ? {
254
- files: args.files
255
- } : {},
256
- ...args.metadata ? {
257
- metadata: args.metadata
258
- } : {}
259
- });
260
- }
261
- });
262
- methods.registerMethod('setInput', (args)=>{
263
- setInputValue(typeof args?.text === 'string' ? args.text : '');
264
- });
265
- methods.registerMethod('clearMessages', ()=>{
266
- setMessages([]);
267
- });
268
- methods.registerMethod('deleteMessage', (args)=>{
269
- if (args?.messageId) {
270
- setMessages((prev)=>prev.filter((m)=>m.id !== args.messageId));
271
- }
272
- });
273
- methods.registerMethod('stop', ()=>{
274
- stop();
275
- });
276
- methods.registerMethod('clearError', ()=>{
277
- clearError();
278
- });
279
- methods.registerMethod('scrollToBottom', ()=>{
280
- bubbleListRef.current?.scrollTo({
281
- top: 'bottom'
282
- });
283
- });
284
- methods.registerEvent({
285
- name: '__updatePageState',
286
- actions: [
287
- {
288
- id: 'setState',
289
- type: 'SetState',
290
- params: {
291
- _event: true
292
- }
293
- }
294
- ]
295
- });
296
- if (uploadPolicyRequestId) {
297
- if (attachmentsConfig?.s3PostPolicyRequestId) {
298
- console.warn('AgentChat attachments property "s3PostPolicyRequestId" is deprecated. Use "uploadPolicyRequestId" instead.');
299
- }
300
- methods.registerEvent({
301
- name: '__getUploadPolicy',
302
- actions: [
303
- {
304
- id: '__getUploadPolicy',
305
- type: 'Request',
306
- params: [
307
- uploadPolicyRequestId
308
- ]
309
- }
310
- ]
311
- });
312
- if (downloadPolicyRequestId) {
313
- methods.registerEvent({
314
- name: '__getDownloadPolicy',
315
- actions: [
316
- {
317
- id: '__getDownloadPolicy',
318
- type: 'Request',
319
- params: [
320
- downloadPolicyRequestId
321
- ]
322
- }
323
- ]
324
- });
325
- }
326
- }
327
- }, []);
328
- useAgentEvents({
329
- messages,
330
- status,
331
- methods,
332
- finishMetaRef,
333
- conversationId: effectiveConversationId,
334
- externalIdsRef
335
- });
336
- const isEmpty = messages.length === 0;
337
- const isBusy = status === 'streaming' || status === 'submitted';
338
- function fileToContentPart(file) {
339
- return new Promise((resolve)=>{
340
- const reader = new FileReader();
341
- reader.onload = ()=>{
342
- resolve({
343
- type: 'file',
344
- url: reader.result,
345
- mediaType: file.type,
346
- filename: file.name
347
- });
348
- };
349
- reader.readAsDataURL(file);
350
- });
351
- }
352
- async function uploadFileToStorage(file) {
353
- const { name, type: fileType } = file;
354
- const descriptor = await getUploadPolicy({
355
- methods,
356
- file
357
- });
358
- await uploadFile({
359
- descriptor,
360
- file
361
- });
362
- // The model provider fetches this URL, so resolve it through the download
363
- // request when configured (stable public URL or signed URL). Otherwise
364
- // fall back to the legacy unsigned object URL from the descriptor.
365
- let objectUrl;
366
- if (downloadPolicyRequestId) {
367
- const response = await methods.triggerEvent({
368
- name: '__getDownloadPolicy',
369
- event: {
370
- file: {
371
- bucket: descriptor.bucket,
372
- key: descriptor.key,
373
- name,
374
- type: fileType
375
- }
376
- }
377
- });
378
- if (response.success !== true) {
379
- throw new Error('Download policy request failed.');
380
- }
381
- objectUrl = response.responses.__getDownloadPolicy.response[0];
382
- } else {
383
- objectUrl = getLegacyObjectUrl({
384
- descriptor
385
- });
386
- }
387
- return {
388
- url: objectUrl,
389
- mediaType: fileType,
390
- filename: name
391
- };
392
- }
393
- // One intake for every way a file arrives — the paperclip picker, a
394
- // clipboard paste, a drag-and-drop — so the accept list and size cap apply
395
- // to all of them, not just the picker's native dialog. Pasted clipboard
396
- // images all arrive as "image.png" (every browser), and the attached list
397
- // is keyed by name + size, so those get a unique name.
398
- function acceptsFile(file) {
399
- const accept = attachmentsConfig?.accept;
400
- if (!accept) return true;
401
- const name = (file.name || '').toLowerCase();
402
- const mime = (file.type || '').toLowerCase();
403
- return accept.split(',').map((t)=>t.trim().toLowerCase()).filter(Boolean).some((t)=>{
404
- if (t.startsWith('.')) return name.endsWith(t);
405
- if (t.endsWith('/*')) return mime.startsWith(t.slice(0, -1));
406
- return mime === t;
407
- });
408
- }
409
- function addFiles(incoming, { pasted = false } = {}) {
410
- const files = Array.from(incoming ?? []).filter((f)=>acceptsFile(f) && !(attachmentsConfig?.maxSize && f.size > attachmentsConfig.maxSize));
411
- if (files.length === 0) return;
412
- const named = files.map((f)=>{
413
- if (!pasted || !/^image\.\w+$/i.test(f.name)) return f;
414
- const ext = f.name.split('.').pop();
415
- return new File([
416
- f
417
- ], `pasted-${Date.now()}.${ext}`, {
418
- type: f.type
419
- });
420
- });
421
- setAttachedFiles((prev)=>[
422
- ...prev,
423
- ...named
424
- ]);
425
- }
426
- function dragHasFiles(e) {
427
- return Array.from(e.dataTransfer?.types ?? []).includes('Files');
428
- }
429
- async function handleSend(text) {
430
- if (!text.trim() && attachedFiles.length === 0) return;
431
- const filesMeta = attachedFiles.map((file)=>({
432
- name: file.name,
433
- size: file.size,
434
- type: file.type
435
- }));
436
- // onBeforeSend — validation and cancellation only
437
- const response = await methods.triggerEvent({
438
- name: 'onBeforeSend',
439
- event: {
440
- text,
441
- files: filesMeta,
442
- messages,
443
- switches: switchState
444
- }
445
- });
446
- if (response.success === false) return;
447
- if (attachedFiles.length > 0) {
448
- const parts = [
449
- {
450
- type: 'text',
451
- text
452
- }
453
- ];
454
- if (uploadPolicyRequestId) {
455
- try {
456
- const uploadResults = await Promise.all(attachedFiles.map((file)=>uploadFileToStorage(file)));
457
- for (const result of uploadResults){
458
- parts.push({
459
- type: 'file',
460
- url: result.url,
461
- mediaType: result.mediaType,
462
- filename: result.filename
463
- });
464
- }
465
- } catch (error) {
466
- methods.triggerEvent({
467
- name: 'onError',
468
- event: {
469
- message: error.message ?? 'File upload failed.'
470
- }
471
- });
472
- return;
473
- }
474
- } else {
475
- for (const file of attachedFiles){
476
- const part = await fileToContentPart(file);
477
- parts.push(part);
478
- }
479
- }
480
- sendMessage({
481
- parts
482
- });
483
- setAttachedFiles([]);
484
- } else {
485
- sendMessage({
486
- text
487
- });
488
- }
489
- // Empty the composer here, after the sends and downstream of both the
490
- // onBeforeSend cancellation return and the upload await, so a send that was
491
- // rejected or failed to upload leaves the user's text in the box. Resetting
492
- // from the Sender's onSubmit handler instead would silently lose typed input
493
- // on every rejected send.
494
- setInputValue('');
495
- }
496
- function handleStop() {
497
- stop();
498
- methods.triggerEvent({
499
- name: 'onStop',
500
- event: {
501
- messages
502
- }
503
- });
504
- }
505
- function handlePromptClick(prompt) {
506
- sendMessage({
507
- text: prompt.label
508
- });
509
- }
510
- // A two-track welcome starter fills the composer instead of sending, so a
511
- // generic shipped default is an editable first draft rather than a message the
512
- // user never meant to send. Reuses the controlled Sender value.
513
- function handleWelcomePromptFill(text) {
514
- setInputValue(typeof text === 'string' ? text : '');
515
- }
516
- function handleSuggestionClick(suggestion) {
517
- methods.triggerEvent({
518
- name: 'onSuggestionClick',
519
- event: {
520
- suggestion
521
- }
522
- });
523
- sendMessage({
524
- text: suggestion.label
525
- });
526
- }
527
- function handleSwitchChange(key, checked) {
528
- setSwitchState((prev)=>({
529
- ...prev,
530
- [key]: checked
531
- }));
532
- methods.triggerEvent({
533
- name: 'onSwitchChange',
534
- event: {
535
- key,
536
- checked
537
- }
538
- });
539
- }
540
- const agentSuggestions = useMemo(()=>{
541
- const lastAssistant = [
542
- ...messages
543
- ].reverse().find((m)=>m.role === 'assistant');
544
- if (!lastAssistant?.parts) return null;
545
- const suggestionPart = [
546
- ...lastAssistant.parts
547
- ].reverse().find((p)=>p.type === 'data-suggestions');
548
- return suggestionPart?.data?.items ?? null;
549
- }, [
550
- messages
551
- ]);
552
- const activeSuggestions = agentSuggestions ?? suggestions;
553
- // A link in an answer was a plain anchor: a full browser navigation out of the
554
- // conversation, with no way for an app to do anything else with it. Default is only
555
- // prevented when the app actually handles onLinkClick — otherwise the link keeps
556
- // navigating, so wiring nothing changes nothing.
557
- //
558
- // Modified and non-primary clicks are never intercepted: open-in-new-tab is a
559
- // reasonable thing to do with a citation, and preventing it would be a regression.
560
- const interceptLinks = Boolean(events?.onLinkClick);
561
- // Stable identity: MessageBubble memoises its markdown component map on this, and the map
562
- // is rebuilt on every streaming chunk otherwise — remounting every link in the answer as
563
- // it streams.
564
- const handleLinkClick = useCallback(({ href, text, domEvent })=>{
565
- if (!interceptLinks || domEvent.defaultPrevented || domEvent.button !== 0 || domEvent.metaKey || domEvent.ctrlKey || domEvent.shiftKey || domEvent.altKey) {
566
- return;
567
- }
568
- // Prevented synchronously: Link checks defaultPrevented immediately after calling
569
- // this, and an external anchor's default fires before any async work could run.
570
- domEvent.preventDefault();
571
- methods.triggerEvent({
572
- name: 'onLinkClick',
573
- event: {
574
- href,
575
- text
576
- }
577
- });
578
- }, [
579
- interceptLinks,
580
- methods
581
- ]);
582
- function handleFeedback({ messageId, rating }) {
583
- setFeedbackValues((prev)=>({
584
- ...prev,
585
- [messageId]: rating
586
- }));
587
- const message = messages.find((msg)=>msg.id === messageId);
588
- const messageContent = message?.parts?.filter((part)=>part.type === 'text').map((part)=>part.text).join('') ?? '';
589
- methods.triggerEvent({
590
- name: 'onFeedback',
591
- event: {
592
- messageId,
593
- messageContent,
594
- rating
595
- }
596
- });
597
- }
598
- function handleRegenerate({ messageId }) {
599
- methods.triggerEvent({
600
- name: 'onRegenerate',
601
- event: {
602
- messageId,
603
- messages
604
- }
605
- });
606
- regenerate({
607
- messageId
608
- });
609
- }
610
- function handleEditMessage({ messageId, originalContent, newContent }) {
611
- methods.triggerEvent({
612
- name: 'onEditMessage',
613
- event: {
614
- messageId,
615
- originalContent,
616
- newContent,
617
- messages
618
- }
619
- });
620
- const messageIndex = messages.findIndex((m)=>m.id === messageId);
621
- if (messageIndex >= 0) {
622
- setMessages((prev)=>prev.slice(0, messageIndex));
623
- sendMessage({
624
- text: newContent
625
- });
626
- }
627
- }
628
- function handleDelete({ messageId }) {
629
- const message = messages.find((m)=>m.id === messageId);
630
- const textContent = message?.parts?.filter((p)=>p.type === 'text').map((p)=>p.text).join('') ?? '';
631
- methods.triggerEvent({
632
- name: 'onDeleteMessage',
633
- event: {
634
- messageId,
635
- content: textContent,
636
- messages
637
- }
638
- });
639
- setMessages((prev)=>prev.filter((m)=>m.id !== messageId));
640
- }
641
- const chatContent = /*#__PURE__*/ React.createElement("div", {
642
- id: blockId,
15
+ */ import React, { useState } from 'react';
16
+ import { createLazyBlock } from '@lowdefy/block-utils';
17
+ import DrawerLauncher from './DrawerLauncher.js';
18
+ import meta from './meta.js';
19
+ // Painted until the implementation loads. Drawer mode shows the same launcher button the
20
+ // implementation renders, so first paint has it; inline mode holds the chat's box so the
21
+ // page does not shift when the chat arrives.
22
+ function AgentChatFallback({ blockId, components, onDrawerOpenRequest, properties }) {
23
+ if (properties.display === 'drawer') {
24
+ return /*#__PURE__*/ React.createElement(DrawerLauncher, {
25
+ blockId: blockId,
26
+ config: properties.drawer,
27
+ Icon: components.Icon,
28
+ onClick: onDrawerOpenRequest
29
+ });
30
+ }
31
+ return /*#__PURE__*/ React.createElement("div", {
643
32
  style: {
644
- display: 'flex',
645
- flexDirection: 'column',
646
- height: display === 'drawer' ? '100%' : properties.height ?? 'calc(100dvh - 170px)',
33
+ height: properties.height ?? 'calc(100dvh - 170px)',
647
34
  maxWidth: properties.maxWidth ?? 800,
648
35
  margin: '0 auto',
649
36
  width: '100%'
650
37
  }
651
- }, /*#__PURE__*/ React.createElement("div", {
652
- style: {
653
- flex: 1,
654
- minHeight: 0,
655
- padding: '16px 0'
656
- }
657
- }, isEmpty && !welcome?.tracks ? /*#__PURE__*/ React.createElement(WelcomeScreen, {
658
- config: welcome,
659
- onPromptClick: handlePromptClick
660
- }) : /*#__PURE__*/ React.createElement(MessageList, {
661
- ref: bubbleListRef,
662
- messages: messages,
663
- isStreaming: isBusy,
664
- config: messageDisplay,
665
- welcome: welcome,
666
- onWelcomePromptFill: handleWelcomePromptFill,
667
- addToolApprovalResponse: addToolApprovalResponse,
668
- onFeedback: handleFeedback,
669
- onLinkClick: handleLinkClick,
670
- Link: Link,
671
- feedbackValues: effectiveFeedbackValues,
672
- onRegenerate: handleRegenerate,
673
- onDelete: handleDelete,
674
- onEditMessage: handleEditMessage,
675
- translate: methods.translate
676
- })), !isEmpty && activeSuggestions && activeSuggestions.length > 0 && !isBusy && /*#__PURE__*/ React.createElement("div", {
677
- style: {
678
- padding: '0 16px 8px'
679
- }
680
- }, /*#__PURE__*/ React.createElement(Prompts, {
681
- items: activeSuggestions.map((s, i)=>({
682
- key: s.key ?? `suggestion-${i}`,
683
- label: s.label,
684
- description: s.description
685
- })),
686
- onItemClick: ({ data })=>handleSuggestionClick(data),
687
- wrap: true
688
- })), /*#__PURE__*/ React.createElement("div", {
689
- style: {
690
- padding: '8px 16px 24px',
691
- borderRadius: 12,
692
- outline: dragOver ? '2px dashed currentColor' : 'none',
693
- outlineOffset: -6,
694
- transition: 'outline-color 120ms'
695
- },
696
- // Drag-in lands in the same intake as the picker and paste. Only
697
- // react to drags carrying files, so text selections dragged across
698
- // the composer are left to the textarea.
699
- onDragOver: attachmentsConfig?.enabled ? (e)=>{
700
- if (!dragHasFiles(e)) return;
701
- e.preventDefault();
702
- if (!dragOver) setDragOver(true);
703
- } : undefined,
704
- onDragLeave: attachmentsConfig?.enabled ? ()=>setDragOver(false) : undefined,
705
- onDrop: attachmentsConfig?.enabled ? (e)=>{
706
- if (!dragHasFiles(e)) return;
707
- e.preventDefault();
708
- setDragOver(false);
709
- addFiles(e.dataTransfer.files);
710
- } : undefined
711
- }, attachmentsConfig?.enabled && attachedFiles.length > 0 && /*#__PURE__*/ React.createElement(FileCard.List, {
712
- style: {
713
- marginBottom: 4
714
- },
715
- items: attachedFiles.map((file, i)=>{
716
- const cardType = getFileCardType(file.type);
717
- return {
718
- key: `${i}`,
719
- name: file.name,
720
- byte: file.size,
721
- type: cardType,
722
- icon: getFileCardIcon(file.type, file.name),
723
- src: cardType === 'image' ? URL.createObjectURL(file) : undefined
724
- };
725
- }),
726
- removable: true,
727
- onRemove: (item)=>{
728
- setAttachedFiles((prev)=>{
729
- const idx = prev.findIndex((f)=>f.name === item.name && f.size === item.byte);
730
- return idx !== -1 ? prev.filter((_, j)=>j !== idx) : prev;
731
- });
732
- },
733
- overflow: "wrap",
734
- size: "small"
735
- }), attachmentsConfig?.enabled && /*#__PURE__*/ React.createElement("input", {
736
- ref: fileInputRef,
737
- type: "file",
738
- style: {
739
- display: 'none'
740
- },
741
- accept: attachmentsConfig.accept,
742
- multiple: true,
743
- onChange: (e)=>{
744
- addFiles(e.target.files);
745
- e.target.value = '';
746
- }
747
- }), /*#__PURE__*/ React.createElement(Sender, {
748
- ref: senderRef,
749
- value: inputValue,
750
- onChange: setInputValue,
751
- placeholder: sender?.placeholder ?? methods.translate('agent.sender.placeholder'),
752
- submitType: sender?.submitType ?? 'enter',
753
- allowSpeech: sender?.allowSpeech ?? false,
754
- onSubmit: handleSend,
755
- // Clipboard paste (a screenshot, a copied file) attaches like the
756
- // paperclip does.
757
- onPasteFile: attachmentsConfig?.enabled ? (files)=>addFiles(files, {
758
- pasted: true
759
- }) : undefined,
760
- onCancel: handleStop,
761
- loading: isBusy,
762
- prefix: attachmentsConfig?.enabled ? /*#__PURE__*/ React.createElement(Button, {
763
- type: "text",
764
- icon: /*#__PURE__*/ React.createElement(PaperClipOutlined, null),
765
- onClick: ()=>fileInputRef.current?.click()
766
- }) : undefined,
767
- header: sender?.header ? /*#__PURE__*/ React.createElement(Sender.Header, {
768
- title: sender.header.title,
769
- closable: sender.header.closable ?? true,
770
- open: headerOpen,
771
- onOpenChange: setHeaderOpen
772
- }, sender.header.content) : undefined,
773
- footer: switchConfigs.length > 0 ? switchConfigs.map((sw)=>/*#__PURE__*/ React.createElement(Sender.Switch, {
774
- key: sw.key,
775
- value: switchState[sw.key] ?? false,
776
- onChange: (checked)=>handleSwitchChange(sw.key, checked),
777
- icon: sw.icon ? /*#__PURE__*/ React.createElement(Icon, {
778
- blockId: `${blockId}_switch_${sw.key}_icon`,
779
- events: {},
780
- properties: sw.icon
781
- }) : undefined
782
- }, sw.label)) : undefined
783
- })));
784
- if (display === 'drawer') {
785
- return /*#__PURE__*/ React.createElement(DrawerWrapper, {
786
- config: drawerConfig
787
- }, chatContent);
788
- }
789
- return chatContent;
38
+ });
39
+ }
40
+ const LazyAgentChat = createLazyBlock({
41
+ load: ()=>import('./AgentChat.lazy.js'),
42
+ meta,
43
+ Fallback: AgentChatFallback
44
+ });
45
+ // Holds a launcher click made before the implementation loaded, so the implementation can
46
+ // mount with its drawer already open instead of dropping the click. The click needs no
47
+ // preload: the fallback only shows while the mounted wrapper's load is already in flight.
48
+ function AgentChat(props) {
49
+ const [drawerOpenRequested, setDrawerOpenRequested] = useState(false);
50
+ return /*#__PURE__*/ React.createElement(LazyAgentChat, {
51
+ ...props,
52
+ drawerOpenRequested: drawerOpenRequested,
53
+ onDrawerOpenRequest: ()=>setDrawerOpenRequested(true)
54
+ });
790
55
  }
791
- AgentChat.meta = {
792
- category: 'display',
793
- icons: []
794
- };
56
+ AgentChat.displayName = 'AgentChat';
57
+ AgentChat.meta = LazyAgentChat.meta;
58
+ AgentChat.preload = LazyAgentChat.preload;
795
59
  export default AgentChat;