@meistrari/chat-nuxt 4.4.1-rc.1 → 4.5.0-rc.1

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.
Files changed (25) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/conversations/components/chat-conversation-creator-filter.vue +22 -11
  3. package/dist/runtime/conversations/components/chat-conversation-list.vue +245 -159
  4. package/dist/runtime/embed/components/chat-embed-inner.d.vue.ts +16 -8
  5. package/dist/runtime/embed/components/chat-embed-inner.vue +48 -45
  6. package/dist/runtime/embed/components/chat-embed-inner.vue.d.ts +16 -8
  7. package/dist/runtime/embed/components/chat-embed.d.vue.ts +4 -2
  8. package/dist/runtime/embed/components/chat-embed.vue +3 -0
  9. package/dist/runtime/embed/components/chat-embed.vue.d.ts +4 -2
  10. package/dist/runtime/embed/components/chat-sidebar-toggle.d.vue.ts +10 -0
  11. package/dist/runtime/embed/components/chat-sidebar-toggle.vue +49 -0
  12. package/dist/runtime/embed/components/chat-sidebar-toggle.vue.d.ts +10 -0
  13. package/dist/runtime/embed/components/meistrari-chat-embed.d.vue.ts +2 -0
  14. package/dist/runtime/embed/components/meistrari-chat-embed.vue +3 -0
  15. package/dist/runtime/embed/components/meistrari-chat-embed.vue.d.ts +2 -0
  16. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +6 -2
  17. package/dist/runtime/embed/empty-state-labels.d.ts +5 -0
  18. package/dist/runtime/embed/empty-state-labels.js +9 -0
  19. package/dist/runtime/embed/feature-config.d.ts +3 -0
  20. package/dist/runtime/embed/feature-config.js +2 -1
  21. package/dist/runtime/embed/types.d.ts +13 -0
  22. package/dist/runtime/lightweight/messages/composables/chat.d.ts +1 -0
  23. package/dist/runtime/messages/composables/chat.d.ts +1 -0
  24. package/dist/runtime/messages/composables/chat.js +48 -13
  25. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.4.1-rc.1",
4
+ "version": "4.5.0-rc.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,14 +1,13 @@
1
1
  <script setup>
2
2
  import { useConversations } from "../composables/conversations";
3
- import { conversationCreatorFilterOptions } from "../../types/schemas/chat/ui.js";
4
3
  import { hasConversationCreatorFilterResults } from "../creator-filter";
5
4
  import { useEmbedConfig } from "../../embed/composables/embed-config";
5
+ const FILTER_OPTIONS = [
6
+ { value: "all", label: "Todas" },
7
+ { value: "mine", label: "Minhas" }
8
+ ];
6
9
  const embedConfig = useEmbedConfig();
7
10
  const { conversationCreatorFilter, conversationCreatorFilterCounts } = useConversations();
8
- const filterOptions = computed(() => conversationCreatorFilterOptions.map((option) => ({
9
- value: option.value,
10
- label: `${option.label} ${conversationCreatorFilterCounts.value[option.value]}`
11
- })));
12
11
  const showConversationCreatorFilter = computed(
13
12
  () => !embedConfig?.hideConversationCreatorFilter.value && hasConversationCreatorFilterResults(conversationCreatorFilterCounts.value)
14
13
  );
@@ -23,13 +22,25 @@ function handleFilterUpdate(value) {
23
22
  </script>
24
23
 
25
24
  <template>
26
- <TelaSegmentToggle
25
+ <TelaSelectMenu
27
26
  v-if="showConversationCreatorFilter"
28
27
  :model-value="conversationCreatorFilter"
29
- :options="filterOptions"
30
- class="w-full"
31
- buttons-class="flex-1 min-w-0 px-8px"
32
- aria-label="Filtrar conversas por criador"
28
+ :options="FILTER_OPTIONS"
29
+ content-class="min-w-130px! rounded-16px!"
30
+ enable-portal
33
31
  @update:model-value="handleFilterUpdate"
34
- />
32
+ >
33
+ <template #trigger>
34
+ <!-- Styled to match the sibling "+" icon button; the caret is hidden. -->
35
+ <TelaSelectMenuTrigger
36
+ aria-label="Filtrar conversas por criador"
37
+ class="size-22px! p-3px! gap-0! justify-center! rounded-6px! border-0! bg-transparent! shadow-none! text-icon-tertiary hover:bg-muted! hover:text-primary data-[state=open]:bg-muted! data-[state=open]:text-primary [&>:last-child]:hidden"
38
+ >
39
+ <TelaIcon
40
+ name="i-ph-sliders-horizontal"
41
+ size="16px"
42
+ />
43
+ </TelaSelectMenuTrigger>
44
+ </template>
45
+ </TelaSelectMenu>
35
46
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { useVirtualList } from "@vueuse/core";
2
+ import { usePreferredReducedMotion, useVirtualList } from "@vueuse/core";
3
3
  import { useConversationList } from "../composables/conversation-list";
4
4
  const props = defineProps({
5
5
  conversations: { type: null, required: true },
@@ -55,6 +55,46 @@ const ROW_HEIGHT = 32;
55
55
  const HEADING_HEIGHT = 28;
56
56
  const ERROR_ROW_HEIGHT = 20;
57
57
  const SECTION_GAP = 10;
58
+ const FOLDER_TOGGLE_DURATION = 270;
59
+ const togglingFolder = shallowRef(null);
60
+ const reducedMotion = usePreferredReducedMotion();
61
+ let togglingFrame = 0;
62
+ function easeOutCubic(t) {
63
+ return 1 - (1 - t) ** 3;
64
+ }
65
+ function stopFolderAnimation() {
66
+ if (togglingFrame)
67
+ cancelAnimationFrame(togglingFrame);
68
+ togglingFrame = 0;
69
+ togglingFolder.value = null;
70
+ }
71
+ function animateFolder(groupId, from, to) {
72
+ stopFolderAnimation();
73
+ if (typeof requestAnimationFrame === "undefined" || reducedMotion.value === "reduce")
74
+ return;
75
+ const startedAt = Date.now();
76
+ const tick = () => {
77
+ const t = Math.min((Date.now() - startedAt) / FOLDER_TOGGLE_DURATION, 1);
78
+ if (t >= 1) {
79
+ togglingFrame = 0;
80
+ togglingFolder.value = null;
81
+ return;
82
+ }
83
+ togglingFolder.value = { groupId, progress: from + (to - from) * easeOutCubic(t) };
84
+ togglingFrame = requestAnimationFrame(tick);
85
+ };
86
+ tick();
87
+ }
88
+ function folderProgress(groupId) {
89
+ return togglingFolder.value?.groupId === groupId ? togglingFolder.value.progress : null;
90
+ }
91
+ function handleToggleGroup(groupId) {
92
+ const wasOpen = groupItems.value.find((group) => group.group.id === groupId)?.open ?? false;
93
+ const from = folderProgress(groupId) ?? (wasOpen ? 1 : 0);
94
+ toggleGroup(groupId);
95
+ animateFolder(groupId, from, wasOpen ? 0 : 1);
96
+ }
97
+ onBeforeUnmount(stopFolderAnimation);
58
98
  const rows = computed(() => {
59
99
  const result = [];
60
100
  if (groupItems.value.length > 0)
@@ -64,14 +104,18 @@ const rows = computed(() => {
64
104
  result.push({ type: "group", key: `group:${groupId}`, height: ROW_HEIGHT, groupId, groupBucket });
65
105
  if (groupBucket.isRenaming && renameGroupError.value)
66
106
  result.push({ type: "error", key: `error:${groupId}`, height: ERROR_ROW_HEIGHT, groupId, groupBucket });
67
- if (!groupBucket.open)
107
+ const progress = folderProgress(groupId);
108
+ if (!groupBucket.open && progress === null)
68
109
  continue;
69
- for (const conversation of groupBucket.items)
70
- result.push({ type: "conversation", key: conversation.id, height: ROW_HEIGHT, groupId, conversation });
110
+ const children = groupBucket.items.map((conversation) => ({ type: "conversation", key: conversation.id, height: ROW_HEIGHT, groupId, conversation }));
71
111
  if (groupBucket.showNewConversationAction)
72
- result.push({ type: "new", key: `new:${groupId}`, height: ROW_HEIGHT, groupId, groupBucket });
112
+ children.push({ type: "new", key: `new:${groupId}`, height: ROW_HEIGHT, groupId, groupBucket });
73
113
  else if (groupBucket.showSearchEmpty)
74
- result.push({ type: "empty", key: `empty:${groupId}`, height: ROW_HEIGHT, groupId, groupBucket });
114
+ children.push({ type: "empty", key: `empty:${groupId}`, height: ROW_HEIGHT, groupId, groupBucket });
115
+ const scale = progress ?? 1;
116
+ const clipHeight = children.length * ROW_HEIGHT * scale;
117
+ for (const child of children)
118
+ result.push({ ...child, height: ROW_HEIGHT * scale, layoutHeight: ROW_HEIGHT, clipHeight });
75
119
  }
76
120
  if (groupItems.value.length > 0)
77
121
  result.push({ type: "spacer", key: "spacer", height: SECTION_GAP, groupId: null });
@@ -79,8 +123,20 @@ const rows = computed(() => {
79
123
  for (const conversation of ungroupedConversations.value)
80
124
  result.push({ type: "conversation", key: conversation.id, height: ROW_HEIGHT, groupId: null, conversation });
81
125
  let offset = 0;
126
+ let clip = null;
82
127
  return result.map((row) => {
83
- const positionedRow = { ...row, offset };
128
+ let clipTop;
129
+ let localOffset = 0;
130
+ if (row.clipHeight !== void 0 && row.groupId) {
131
+ if (clip?.groupId !== row.groupId)
132
+ clip = { groupId: row.groupId, top: offset, localOffset: 0 };
133
+ clipTop = clip.top;
134
+ localOffset = clip.localOffset;
135
+ clip.localOffset += row.layoutHeight ?? row.height;
136
+ } else {
137
+ clip = null;
138
+ }
139
+ const positionedRow = { ...row, offset, clipTop, localOffset };
84
140
  offset += row.height;
85
141
  return positionedRow;
86
142
  });
@@ -100,6 +156,25 @@ const renderedRows = computed(() => {
100
156
  }
101
157
  return [...indexes].sort((a, b) => a - b).map((index) => ({ ...rows.value[index], index }));
102
158
  });
159
+ const renderedBlocks = computed(() => {
160
+ const blocks = [];
161
+ const clipBlocks = /* @__PURE__ */ new Map();
162
+ for (const row of renderedRows.value) {
163
+ if (row.clipTop === void 0 || !row.groupId) {
164
+ blocks.push({ key: row.key, top: row.offset, height: row.height, clip: false, opacity: 1, rows: [row] });
165
+ continue;
166
+ }
167
+ const existing = clipBlocks.get(row.groupId);
168
+ if (existing) {
169
+ existing.rows.push(row);
170
+ continue;
171
+ }
172
+ const block = { key: `clip:${row.groupId}`, top: row.clipTop, height: row.clipHeight ?? 0, clip: true, opacity: folderProgress(row.groupId) ?? 1, rows: [row] };
173
+ clipBlocks.set(row.groupId, block);
174
+ blocks.push(block);
175
+ }
176
+ return blocks;
177
+ });
103
178
  const totalHeight = computed(() => {
104
179
  const last = rows.value.at(-1);
105
180
  return last ? last.offset + last.height : 0;
@@ -205,176 +280,187 @@ function dropTargetClass(row) {
205
280
  </p>
206
281
  </div>
207
282
 
208
- <div v-else v-bind="containerProps" flex-1 min-h-0 mt-4px>
283
+ <div v-else v-bind="containerProps" flex-1 min-h-0 mt-4px no-scrollbar>
209
284
  <div relative :style="{ height: `${totalHeight}px` }">
210
285
  <div
211
- v-for="row in renderedRows"
212
- :key="row.key"
213
- :style="{ height: `${row.height}px`, top: `${row.offset}px` }"
286
+ v-for="block in renderedBlocks"
287
+ :key="block.key"
214
288
  absolute inset-x-0
215
- :data-row-index="row.index"
216
- rounded-8px
217
- :class="dropTargetClass(row)"
218
- @keydown="handleRowKeydown($event, row.index)"
219
- @focusin="retainedRowKey = row.key"
220
- @pointerdown="retainedRowKey = row.key"
221
- @dragstart.capture="draggedRowKey = row.key"
222
- @dragend.capture="draggedRowKey = null"
223
- v-on="dropListeners(row)"
289
+ :class="block.clip ? 'overflow-hidden' : ''"
290
+ :style="{ top: `${block.top}px`, height: `${block.height}px`, opacity: block.opacity }"
224
291
  >
225
- <div v-if="row.type === 'folders'" h-full flex items-center justify-between pl-12px pr-2px>
226
- <span body-12-medium text-tertiary>
227
- Pastas
228
- </span>
229
- <TelaIconButton
230
- icon="i-ph-plus"
231
- color="secondary"
232
- size="sm"
233
- button-class="p-3px! rounded-6px!"
234
- aria-label="Nova pasta"
235
- @click="createNewGroup"
236
- />
237
- </div>
238
- <div v-else-if="row.type === 'ungrouped'" h-full flex items-center justify-between pl-12px pr-2px>
239
- <span body-12-medium text-secondary>
240
- {{ ungroupedConversationLabel }}
241
- </span>
242
- <TelaIconButton
243
- icon="i-ph-plus"
244
- color="secondary"
245
- size="sm"
246
- button-class="p-3px! rounded-6px!"
247
- aria-label="Nova conversa"
248
- @click="startUngroupedConversation"
249
- />
250
- </div>
251
292
  <div
252
- v-if="row.type === 'group'"
253
- pl-12px pr-4px
254
- h-30px
293
+ v-for="row in block.rows"
294
+ :key="row.key"
295
+ :style="{ height: `${row.layoutHeight ?? row.height}px`, top: `${row.localOffset}px` }"
296
+ absolute inset-x-0
297
+ :data-row-index="row.index"
255
298
  rounded-8px
256
- class="group hover:bg-subtle"
257
- :data-active="row.groupBucket.open && !row.groupBucket.dropTarget"
299
+ :class="dropTargetClass(row)"
300
+ @keydown="handleRowKeydown($event, row.index)"
301
+ @focusin="retainedRowKey = row.key"
302
+ @pointerdown="retainedRowKey = row.key"
303
+ @dragstart.capture="draggedRowKey = row.key"
304
+ @dragend.capture="draggedRowKey = null"
305
+ v-on="dropListeners(row)"
258
306
  >
259
- <div flex items-center justify-between h-full gap-8px>
260
- <template v-if="row.groupBucket.isRenaming">
261
- <TelaIcon
262
- :name="row.groupBucket.folderIcon"
263
- size="16px"
264
- color="icon-secondary"
265
- shrink-0
307
+ <div v-if="row.type === 'folders'" h-full flex items-center justify-between pl-12px pr-2px>
308
+ <span body-12-medium text-tertiary>
309
+ Pastas
310
+ </span>
311
+ <TelaIconButton
312
+ icon="i-ph-plus"
313
+ color="secondary"
314
+ size="sm"
315
+ button-class="p-3px! rounded-6px!"
316
+ aria-label="Nova pasta"
317
+ @click="createNewGroup"
318
+ />
319
+ </div>
320
+ <div v-else-if="row.type === 'ungrouped'" h-full flex items-center justify-between pl-12px pr-2px>
321
+ <span body-12-medium text-tertiary>
322
+ {{ ungroupedConversationLabel }}
323
+ </span>
324
+ <div flex items-center gap-2px>
325
+ <ChatConversationCreatorFilter />
326
+ <TelaIconButton
327
+ icon="i-ph-plus"
328
+ color="secondary"
329
+ size="sm"
330
+ button-class="p-3px! rounded-6px!"
331
+ aria-label="Nova conversa"
332
+ @click="startUngroupedConversation"
266
333
  />
267
- <div relative flex-1 min-w-0 flex items-center>
268
- <input
269
- :ref="(element) => setRenameGroupInputRef(element, row.groupBucket.group.id)"
270
- v-model="renameGroupName"
271
- type="text"
272
- autocomplete="off"
273
- maxlength="80"
274
- aria-label="Nome da pasta"
275
- w-full min-w-0
276
- bg-transparent border-none outline-none
277
- body-14-medium text-taupe-600
278
- p-0
279
- :disabled="groupActionLoading"
280
- @blur="handleRenameGroupBlur"
281
- @keydown.enter.prevent="submitRenameGroup"
282
- @keydown.escape.prevent.stop="cancelRenameGroup"
283
- @click.stop
284
- @dblclick.stop
285
- >
286
- <div
287
- aria-hidden="true"
288
- absolute inset-x-0 bottom--0.5px h-0.5px pointer-events-none
289
- :class="renameGroupError ? 'bg-red-500' : 'bg-border'"
290
- />
291
- </div>
292
- </template>
293
-
294
- <template v-else>
295
- <button
296
- type="button"
297
- h-full flex-1 min-w-0
298
- flex items-center gap-8px
299
- text-left
300
- cursor-pointer
301
- :aria-expanded="row.groupBucket.open"
302
- @click="toggleGroup(row.groupBucket.group.id)"
303
- >
334
+ </div>
335
+ </div>
336
+ <div
337
+ v-if="row.type === 'group'"
338
+ pl-12px pr-4px
339
+ h-30px
340
+ rounded-8px
341
+ class="group hover:bg-subtle"
342
+ :data-active="row.groupBucket.open && !row.groupBucket.dropTarget"
343
+ >
344
+ <div flex items-center justify-between h-full gap-8px>
345
+ <template v-if="row.groupBucket.isRenaming">
304
346
  <TelaIcon
305
347
  :name="row.groupBucket.folderIcon"
306
- size="16px"
307
- color="icon-tertiary"
348
+ size="18px"
349
+ color="icon-secondary"
308
350
  shrink-0
309
351
  />
310
- <span min-w-0 truncate body-14-medium text-taupe-700>
311
- {{ row.groupBucket.group.name }}
312
- </span>
313
- </button>
352
+ <div relative flex-1 min-w-0 flex items-center>
353
+ <input
354
+ :ref="(element) => setRenameGroupInputRef(element, row.groupBucket.group.id)"
355
+ v-model="renameGroupName"
356
+ type="text"
357
+ autocomplete="off"
358
+ maxlength="80"
359
+ aria-label="Nome da pasta"
360
+ w-full min-w-0
361
+ bg-transparent border-none outline-none
362
+ body-14-medium text-taupe-600
363
+ p-0
364
+ :disabled="groupActionLoading"
365
+ @blur="handleRenameGroupBlur"
366
+ @keydown.enter.prevent="submitRenameGroup"
367
+ @keydown.escape.prevent.stop="cancelRenameGroup"
368
+ @click.stop
369
+ @dblclick.stop
370
+ >
371
+ <div
372
+ aria-hidden="true"
373
+ absolute inset-x-0 bottom--0.5px h-0.5px pointer-events-none
374
+ :class="renameGroupError ? 'bg-red-500' : 'bg-border'"
375
+ />
376
+ </div>
377
+ </template>
314
378
 
315
- <div
316
- flex items-center
317
- class="opacity-0 group-hover:opacity-100 focus-within:opacity-100 [&:has([data-state=open])]:opacity-100"
318
- >
319
- <TelaDropdownMenu
320
- :items="row.groupBucket.menuItems"
321
- content-class="chat-sidebar-floating"
379
+ <template v-else>
380
+ <button
381
+ type="button"
382
+ h-full flex-1 min-w-0
383
+ flex items-center gap-8px
384
+ text-left
385
+ cursor-pointer
386
+ :aria-expanded="row.groupBucket.open"
387
+ @click="handleToggleGroup(row.groupBucket.group.id)"
322
388
  >
323
- <TelaIconButton
324
- icon="i-ph-dots-three"
325
- color="secondary"
326
- size="3xs"
327
- aria-label="Ações da pasta"
328
- class="group hover:!bg-lowered data-[state=open]:!bg-lowered"
329
- icon-class="group-hover:icon-tertiary group-data-[state=open]:icon-tertiary"
330
- @click.prevent.stop
389
+ <TelaIcon
390
+ :name="row.groupBucket.folderIcon"
391
+ size="16px"
392
+ color="icon-tertiary"
393
+ shrink-0
331
394
  />
332
- </TelaDropdownMenu>
333
- </div>
334
- </template>
395
+ <span min-w-0 truncate body-14-medium text-taupe-700>
396
+ {{ row.groupBucket.group.name }}
397
+ </span>
398
+ </button>
399
+
400
+ <div
401
+ flex items-center
402
+ class="opacity-0 group-hover:opacity-100 focus-within:opacity-100 [&:has([data-state=open])]:opacity-100"
403
+ >
404
+ <TelaDropdownMenu
405
+ :items="row.groupBucket.menuItems"
406
+ content-class="chat-sidebar-floating"
407
+ >
408
+ <TelaIconButton
409
+ icon="i-ph-dots-three"
410
+ color="secondary"
411
+ size="3xs"
412
+ aria-label="Ações da pasta"
413
+ class="group hover:!bg-lowered data-[state=open]:!bg-lowered"
414
+ icon-class="group-hover:icon-tertiary group-data-[state=open]:icon-tertiary"
415
+ @click.prevent.stop
416
+ />
417
+ </TelaDropdownMenu>
418
+ </div>
419
+ </template>
420
+ </div>
335
421
  </div>
336
- </div>
337
422
 
338
- <div v-if="row.type === 'error'" ml-44px mr-8px>
339
- <span block truncate text-12px text-red-600 leading-16px aria-live="polite" :title="renameGroupError ?? void 0">
340
- {{ renameGroupError }}
341
- </span>
342
- </div>
343
- <ChatConversationItem
344
- v-if="row.type === 'conversation'"
345
- v-bind="getConversationItemProps(row.conversation)"
346
- :item-class="row.groupId ? 'ml-24px' : void 0"
347
- @delete="handleDelete"
348
- @rename="handleRename"
349
- @duplicate="handleDuplicate"
350
- @export="handleExport"
351
- @drag-start="handleConversationDragStart"
352
- @drag-end="clearDragState"
353
- />
354
- <button
355
- v-if="row.type === 'new'"
356
- type="button"
357
- relative h-30px pl-12px w-full
358
- flex items-center justify-between
359
- rounded-8px
360
- text-left
361
- hover:bg-subtle
362
- cursor-pointer
363
- @click="startConversationInGroup(row.groupBucket.group.id)"
364
- >
365
- <span body-14-medium text-taupe-500 ml-24px>
366
- Nova conversa
367
- </span>
368
- </button>
369
- <div v-if="row.type === 'empty'" h-full flex items-center ml-36px>
370
- <span body-14-medium text-tertiary>Sem resultados</span>
423
+ <div v-if="row.type === 'error'" ml-44px mr-8px>
424
+ <span block truncate text-12px text-red-600 leading-16px aria-live="polite" :title="renameGroupError ?? void 0">
425
+ {{ renameGroupError }}
426
+ </span>
427
+ </div>
428
+ <ChatConversationItem
429
+ v-if="row.type === 'conversation'"
430
+ v-bind="getConversationItemProps(row.conversation)"
431
+ :item-class="row.groupId ? 'ml-24px' : void 0"
432
+ @delete="handleDelete"
433
+ @rename="handleRename"
434
+ @duplicate="handleDuplicate"
435
+ @export="handleExport"
436
+ @drag-start="handleConversationDragStart"
437
+ @drag-end="clearDragState"
438
+ />
439
+ <button
440
+ v-if="row.type === 'new'"
441
+ type="button"
442
+ relative h-30px pl-12px w-full
443
+ flex items-center justify-between
444
+ rounded-8px
445
+ text-left
446
+ hover:bg-subtle
447
+ cursor-pointer
448
+ @click="startConversationInGroup(row.groupBucket.group.id)"
449
+ >
450
+ <span body-14-medium text-taupe-500 ml-24px>
451
+ Nova conversa
452
+ </span>
453
+ </button>
454
+ <div v-if="row.type === 'empty'" h-full flex items-center ml-36px>
455
+ <span body-14-medium text-tertiary>Sem resultados</span>
456
+ </div>
457
+ <div
458
+ v-if="isNestedRow(row)"
459
+ aria-hidden="true"
460
+ absolute top-0 left-19px w-0.5px bg-border pointer-events-none
461
+ :class="isLastNestedRow(row.index) ? 'h-30px' : 'h-full'"
462
+ />
371
463
  </div>
372
- <div
373
- v-if="isNestedRow(row)"
374
- aria-hidden="true"
375
- absolute top-0 left-19px w-0.5px bg-border pointer-events-none
376
- :class="isLastNestedRow(row.index) ? 'h-30px' : 'h-full'"
377
- />
378
464
  </div>
379
465
  </div>
380
466
  </div>
@@ -19,21 +19,29 @@ type __VLS_Props = {
19
19
  user?: EmbedUser | null;
20
20
  features?: Partial<ChatFeatureConfig>;
21
21
  };
22
- declare var __VLS_11: {}, __VLS_96: {
22
+ declare var __VLS_14: {}, __VLS_88: {}, __VLS_101: {
23
23
  message: useChatAuth;
24
- }, __VLS_127: {}, __VLS_129: {}, __VLS_210: {
24
+ }, __VLS_113: {}, __VLS_141: {}, __VLS_150: {}, __VLS_214: {}, __VLS_233: {
25
25
  message: useChatAuth;
26
- };
26
+ }, __VLS_245: {};
27
27
  type __VLS_Slots = {} & {
28
- 'sidebar-bottom'?: (props: typeof __VLS_11) => any;
28
+ 'sidebar-bottom'?: (props: typeof __VLS_14) => any;
29
+ } & {
30
+ 'composer-bottom'?: (props: typeof __VLS_88) => any;
31
+ } & {
32
+ 'failed-message-actions'?: (props: typeof __VLS_101) => any;
33
+ } & {
34
+ 'composer-bottom'?: (props: typeof __VLS_113) => any;
35
+ } & {
36
+ 'topbar-leading'?: (props: typeof __VLS_141) => any;
29
37
  } & {
30
- 'failed-message-actions'?: (props: typeof __VLS_96) => any;
38
+ 'topbar-leading'?: (props: typeof __VLS_150) => any;
31
39
  } & {
32
- 'topbar-leading'?: (props: typeof __VLS_127) => any;
40
+ 'composer-bottom'?: (props: typeof __VLS_214) => any;
33
41
  } & {
34
- 'topbar-leading'?: (props: typeof __VLS_129) => any;
42
+ 'failed-message-actions'?: (props: typeof __VLS_233) => any;
35
43
  } & {
36
- 'failed-message-actions'?: (props: typeof __VLS_210) => any;
44
+ 'composer-bottom'?: (props: typeof __VLS_245) => any;
37
45
  };
38
46
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
39
47
  "update:conversationId": (value: string | null) => any;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { inject, provide } from "vue";
2
+ import { inject, provide, useSlots } from "vue";
3
3
  import { useChatHarnessAdapter } from "../composables/harness-adapter";
4
4
  import ChatMessageList from "../../messages/components/chat-message-list.vue";
5
5
  import { CHAT_COMPOSER_EXTENSION_KEY } from "../../types/composer";
@@ -11,6 +11,7 @@ import { exportConversationToMarkdown } from "../../conversations/export";
11
11
  import { CHAT_MOBILE_BREAKPOINT } from "../../shared/breakpoints";
12
12
  import ChatMobileFilePreview from "./mobile/chat-mobile-file-preview.vue";
13
13
  import ChatMobileShell from "./mobile/chat-mobile-shell.vue";
14
+ import ChatSidebarToggle from "./chat-sidebar-toggle.vue";
14
15
  import ChatMessageInput from "../../messages/components/chat-message-input.vue";
15
16
  import { normalizeConversationId } from "../../conversations/scope";
16
17
  import { resolveChatFeatures } from "../feature-config";
@@ -32,6 +33,7 @@ import { useConversationFiles } from "../../files/composables/conversation-files
32
33
  import { useConversationFilesPanel } from "../../files/composables/conversation-files-panel";
33
34
  import { useConversations } from "../../conversations/composables/conversations";
34
35
  import { useEmbedConfig } from "../composables/embed-config";
36
+ import { resolveEmptyStateLabels } from "../empty-state-labels";
35
37
  import { useAgentWake } from "../composables/use-agent-wake";
36
38
  import { useFileDrop } from "../../files/composables/file-drop";
37
39
  import { useFilePreviewPanel } from "../../files/composables/file-preview-panel";
@@ -68,6 +70,7 @@ const isSidebarCollapsed = ref(props.sidebar?.defaultCollapsed ?? false);
68
70
  const sidebarCollapsed = computed(
69
71
  () => sidebarCollapsible.value && isSidebarCollapsed.value
70
72
  );
73
+ const sidebarToggle = computed(() => props.sidebar?.toggle ?? "edge");
71
74
  const sidebarToggleIcon = computed(() => {
72
75
  const pointsRight = sidebarPosition.value === "right" ? !sidebarCollapsed.value : sidebarCollapsed.value;
73
76
  return pointsRight ? "i-ph-caret-right" : "i-ph-caret-left";
@@ -177,6 +180,9 @@ function applySuggestion(prompt) {
177
180
  }
178
181
  const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
179
182
  const isMobile = breakpoints.smaller("mobile");
183
+ const showTopbarSidebarToggle = computed(() => sidebarCollapsible.value && sidebarToggle.value === "topbar" && !props.hideSidebar && !isMobile.value && !props.compact);
184
+ const slots = useSlots();
185
+ const hasTopbarLeading = computed(() => showTopbarSidebarToggle.value || !!slots["topbar-leading"]);
180
186
  const drawerOpen = ref(false);
181
187
  function restoreQueuedSend(send) {
182
188
  messageInputRef.value?.restoreDraft(send);
@@ -197,6 +203,7 @@ const {
197
203
  generatingTitle,
198
204
  animatedTitle,
199
205
  composerLocked,
206
+ isPolling,
200
207
  outboundOccupied,
201
208
  sessionReady,
202
209
  agentSessionRewakeable,
@@ -215,6 +222,7 @@ const {
215
222
  retryMessage,
216
223
  cancelConversation,
217
224
  startPolling,
225
+ reconnect,
218
226
  clearConversation,
219
227
  error: chatError
220
228
  } = createChat({
@@ -548,6 +556,7 @@ watch(
548
556
  { immediate: true }
549
557
  );
550
558
  const currentUser = computed(() => props.user ?? authUser.value ?? null);
559
+ const emptyStateLabels = computed(() => resolveEmptyStateLabels(embedConfig?.labels.value, currentUser.value?.name));
551
560
  const conversationCreator = computed(
552
561
  () => resolveConversationCreator(currentConversation.value?.userId, {
553
562
  currentUser: currentUser.value,
@@ -899,6 +908,15 @@ async function handleDelete() {
899
908
  rounded-8px bg-error text-error p-12px body-14-regular
900
909
  >
901
910
  {{ chatError }}
911
+ <TelaButton
912
+ v-if="composerLocked && !isPolling"
913
+ variant="ghost-danger"
914
+ size="sm"
915
+ icon="i-ph-arrow-clockwise"
916
+ @click="reconnect"
917
+ >
918
+ Reconectar
919
+ </TelaButton>
902
920
  </div>
903
921
  <!-- Sidebar -->
904
922
  <div
@@ -920,48 +938,18 @@ async function handleDelete() {
920
938
  :inert="sidebarCollapsed || void 0"
921
939
  h-full
922
940
  flex="~ col"
923
- bg-neutral-50
941
+ bg
942
+ border
924
943
  :style="{ width: `${sidebarWidth}px` }"
925
944
  :class="
926
- sidebarPosition === 'right' ? 'border-l-0.5px border-l-neutral-200' : 'border-r-0.5px border-r-neutral-200'
945
+ sidebarPosition === 'right' ? 'border-l-0.5px' : 'border-r-0.5px'
927
946
  "
928
947
  >
929
- <div px-12px pt-16px pb-8px flex gap-8px>
930
- <button
931
- flex
932
- items-center
933
- justify-center
934
- gap-6px
935
- h-32px
936
- px-12px
937
- flex-1
938
- min-w-0
939
- bg-white
940
- b=".5px neutral-300"
941
- rounded-10px
942
- hover:bg-neutral-50
943
- transition-colors
944
- cursor-pointer
945
- @click="() => handleNewConversation()"
946
- >
947
- <span i-ph-chat-circle text-12px text-gray-900 />
948
- <span
949
- text-14px
950
- font-semibold
951
- text-neutral-900
952
- tracking="-.15px"
953
- leading-18px
954
- >
955
- Nova conversa
956
- </span>
957
- </button>
958
- <ChatConversationNewGroupButton />
959
- </div>
960
-
961
948
  <div
962
949
  min-h-0
963
950
  overflow-hidden
964
- px-4px
951
+ pt-12px
952
+ px-6px
965
953
  :class="$slots['sidebar-bottom'] ? '' : 'flex-1'"
966
954
  :style="
967
955
  $slots['sidebar-bottom'] ? { flexGrow: 100 - sidebarBottomHeight, flexBasis: 0 } : void 0
@@ -991,7 +979,7 @@ async function handleDelete() {
991
979
  </div>
992
980
 
993
981
  <button
994
- v-if="sidebarCollapsible"
982
+ v-if="sidebarCollapsible && sidebarToggle === 'edge'"
995
983
  type="button"
996
984
  class="chat-sidebar__toggle"
997
985
  :aria-label="
@@ -1091,8 +1079,8 @@ async function handleDelete() {
1091
1079
  <template #empty>
1092
1080
  <TelaChatEmptyState
1093
1081
  compact
1094
- :title="`Ol\xE1, ${currentUser?.name?.split(' ')[0] ?? 'usu\xE1rio'}`"
1095
- description="Como posso te ajudar hoje?"
1082
+ :title="emptyStateLabels.title"
1083
+ :description="emptyStateLabels.description"
1096
1084
  >
1097
1085
  <TelaChatSuggestions v-if="resolvedSuggestions.length">
1098
1086
  <TelaChatSuggestion
@@ -1105,7 +1093,7 @@ async function handleDelete() {
1105
1093
  </TelaChatSuggestion>
1106
1094
  </TelaChatSuggestions>
1107
1095
 
1108
- <TelaChatPromptInputHints size="sm" :hints="PROMPT_INPUT_HINTS" />
1096
+ <TelaChatPromptInputHints v-if="resolvedFeatures.showPromptInputHints" size="sm" :hints="PROMPT_INPUT_HINTS" />
1109
1097
 
1110
1098
  <template #input>
1111
1099
  <ChatMessageInput
@@ -1132,6 +1120,9 @@ async function handleDelete() {
1132
1120
  @send="handleSend"
1133
1121
  @cancel="handleCancel"
1134
1122
  />
1123
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full>
1124
+ <slot name="composer-bottom" />
1125
+ </div>
1135
1126
  </template>
1136
1127
  </TelaChatEmptyState>
1137
1128
  </template>
@@ -1181,6 +1172,9 @@ async function handleDelete() {
1181
1172
  @send="handleSend"
1182
1173
  @cancel="handleCancel"
1183
1174
  />
1175
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full max-w-700px mx-auto mt--12px>
1176
+ <slot name="composer-bottom" />
1177
+ </div>
1184
1178
  </div>
1185
1179
  </ChatMobileShell>
1186
1180
 
@@ -1211,15 +1205,17 @@ async function handleDelete() {
1211
1205
  @delete="showDeleteModal = true"
1212
1206
  @tab-change="handleTabChange"
1213
1207
  >
1214
- <template v-if="$slots['topbar-leading']" #leading>
1208
+ <template v-if="hasTopbarLeading" #leading>
1209
+ <ChatSidebarToggle v-if="showTopbarSidebarToggle" :open="!sidebarCollapsed" @toggle="toggleSidebarCollapsed" />
1215
1210
  <slot name="topbar-leading" />
1216
1211
  </template>
1217
1212
  </ChatTopbar>
1218
1213
  <div
1219
- v-else-if="$slots['topbar-leading'] && !isMobile && !compact"
1220
- absolute top-0 z-10 h-56px flex items-center pl-16px
1214
+ v-else-if="hasTopbarLeading && !isMobile && !compact"
1215
+ absolute top-0 z-10 h-56px flex items-center gap-8px pl-16px
1221
1216
  :style="{ left: 'var(--chat-panel-offset)' }"
1222
1217
  >
1218
+ <ChatSidebarToggle v-if="showTopbarSidebarToggle" :open="!sidebarCollapsed" @toggle="toggleSidebarCollapsed" />
1223
1219
  <slot name="topbar-leading" />
1224
1220
  </div>
1225
1221
 
@@ -1316,8 +1312,8 @@ async function handleDelete() {
1316
1312
  <TelaChatEmptyState
1317
1313
  v-else-if="!isMobile && !hasMessages"
1318
1314
  :compact="compact"
1319
- :title="`Ol\xE1, ${currentUser?.name?.split(' ')[0] ?? 'usu\xE1rio'}`"
1320
- description="Como posso te ajudar hoje?"
1315
+ :title="emptyStateLabels.title"
1316
+ :description="emptyStateLabels.description"
1321
1317
  :class="currentConversation && !compact ? 'pt-64px' : ''"
1322
1318
  >
1323
1319
  <TelaChatSuggestions v-if="resolvedSuggestions.length">
@@ -1332,6 +1328,7 @@ async function handleDelete() {
1332
1328
  </TelaChatSuggestions>
1333
1329
 
1334
1330
  <TelaChatPromptInputHints
1331
+ v-if="resolvedFeatures.showPromptInputHints"
1335
1332
  :size="compact ? 'sm' : 'md'"
1336
1333
  :hints="PROMPT_INPUT_HINTS"
1337
1334
  />
@@ -1361,6 +1358,9 @@ async function handleDelete() {
1361
1358
  @send="handleSend"
1362
1359
  @cancel="handleCancel"
1363
1360
  />
1361
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full>
1362
+ <slot name="composer-bottom" />
1363
+ </div>
1364
1364
  </template>
1365
1365
  </TelaChatEmptyState>
1366
1366
 
@@ -1413,6 +1413,9 @@ async function handleDelete() {
1413
1413
  @send="handleSend"
1414
1414
  @cancel="handleCancel"
1415
1415
  />
1416
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full max-w-700px mx-auto mt--12px>
1417
+ <slot name="composer-bottom" />
1418
+ </div>
1416
1419
  </div>
1417
1420
  </TelaChatBody>
1418
1421
 
@@ -19,21 +19,29 @@ type __VLS_Props = {
19
19
  user?: EmbedUser | null;
20
20
  features?: Partial<ChatFeatureConfig>;
21
21
  };
22
- declare var __VLS_11: {}, __VLS_96: {
22
+ declare var __VLS_14: {}, __VLS_88: {}, __VLS_101: {
23
23
  message: useChatAuth;
24
- }, __VLS_127: {}, __VLS_129: {}, __VLS_210: {
24
+ }, __VLS_113: {}, __VLS_141: {}, __VLS_150: {}, __VLS_214: {}, __VLS_233: {
25
25
  message: useChatAuth;
26
- };
26
+ }, __VLS_245: {};
27
27
  type __VLS_Slots = {} & {
28
- 'sidebar-bottom'?: (props: typeof __VLS_11) => any;
28
+ 'sidebar-bottom'?: (props: typeof __VLS_14) => any;
29
+ } & {
30
+ 'composer-bottom'?: (props: typeof __VLS_88) => any;
31
+ } & {
32
+ 'failed-message-actions'?: (props: typeof __VLS_101) => any;
33
+ } & {
34
+ 'composer-bottom'?: (props: typeof __VLS_113) => any;
35
+ } & {
36
+ 'topbar-leading'?: (props: typeof __VLS_141) => any;
29
37
  } & {
30
- 'failed-message-actions'?: (props: typeof __VLS_96) => any;
38
+ 'topbar-leading'?: (props: typeof __VLS_150) => any;
31
39
  } & {
32
- 'topbar-leading'?: (props: typeof __VLS_127) => any;
40
+ 'composer-bottom'?: (props: typeof __VLS_214) => any;
33
41
  } & {
34
- 'topbar-leading'?: (props: typeof __VLS_129) => any;
42
+ 'failed-message-actions'?: (props: typeof __VLS_233) => any;
35
43
  } & {
36
- 'failed-message-actions'?: (props: typeof __VLS_210) => any;
44
+ 'composer-bottom'?: (props: typeof __VLS_245) => any;
37
45
  };
38
46
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
39
47
  "update:conversationId": (value: string | null) => any;
@@ -1,10 +1,12 @@
1
- declare var __VLS_26: {}, __VLS_29: {}, __VLS_32: any;
1
+ declare var __VLS_26: {}, __VLS_29: {}, __VLS_32: {}, __VLS_35: any;
2
2
  type __VLS_Slots = {} & {
3
3
  'sidebar-bottom'?: (props: typeof __VLS_26) => any;
4
4
  } & {
5
5
  'topbar-leading'?: (props: typeof __VLS_29) => any;
6
6
  } & {
7
- 'failed-message-actions'?: (props: typeof __VLS_32) => any;
7
+ 'composer-bottom'?: (props: typeof __VLS_32) => any;
8
+ } & {
9
+ 'failed-message-actions'?: (props: typeof __VLS_35) => any;
8
10
  };
9
11
  declare const __VLS_base: import("vue").DefineSetupFnComponent<Record<string, any>, {}, {}, Record<string, any> & {}, import("vue").PublicProps>;
10
12
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -177,6 +177,9 @@ provideChatMessageTerminal((payload) => {
177
177
  <template v-if="$slots['topbar-leading']" #topbar-leading>
178
178
  <slot name="topbar-leading" />
179
179
  </template>
180
+ <template v-if="$slots['composer-bottom']" #composer-bottom>
181
+ <slot name="composer-bottom" />
182
+ </template>
180
183
  <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
181
184
  <slot name="failed-message-actions" v-bind="slotProps" />
182
185
  </template>
@@ -1,10 +1,12 @@
1
- declare var __VLS_26: {}, __VLS_29: {}, __VLS_32: any;
1
+ declare var __VLS_26: {}, __VLS_29: {}, __VLS_32: {}, __VLS_35: any;
2
2
  type __VLS_Slots = {} & {
3
3
  'sidebar-bottom'?: (props: typeof __VLS_26) => any;
4
4
  } & {
5
5
  'topbar-leading'?: (props: typeof __VLS_29) => any;
6
6
  } & {
7
- 'failed-message-actions'?: (props: typeof __VLS_32) => any;
7
+ 'composer-bottom'?: (props: typeof __VLS_32) => any;
8
+ } & {
9
+ 'failed-message-actions'?: (props: typeof __VLS_35) => any;
8
10
  };
9
11
  declare const __VLS_base: import("vue").DefineSetupFnComponent<Record<string, any>, {}, {}, Record<string, any> & {}, import("vue").PublicProps>;
10
12
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
+ toggle: () => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ onToggle?: (() => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,49 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ open: { type: Boolean, required: true }
4
+ });
5
+ const emit = defineEmits(["toggle"]);
6
+ const panelWidth = computed(() => props.open ? 6.5 : 4.5);
7
+ </script>
8
+
9
+ <template>
10
+ <button
11
+ type="button"
12
+ :aria-label="open ? 'Recolher conversas' : 'Expandir conversas'"
13
+ :aria-expanded="open"
14
+ h-32px w-32px
15
+ flex items-center justify-center
16
+ flex-shrink-0
17
+ rounded-8px
18
+ text-icon-tertiary
19
+ hover:bg-muted hover:text-icon-secondary
20
+ cursor-pointer
21
+ @click="emit('toggle')"
22
+ >
23
+ <svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
24
+ <defs>
25
+ <clipPath id="chat-sidebar-toggle-clip">
26
+ <rect x="0.5" y="0.5" width="15" height="13" rx="3.5" />
27
+ </clipPath>
28
+ </defs>
29
+ <rect
30
+ x="0.5"
31
+ y="0.5"
32
+ :width="panelWidth"
33
+ height="13"
34
+ fill="#D9D9D9"
35
+ clip-path="url(#chat-sidebar-toggle-clip)"
36
+ :style="{ opacity: open ? 1 : 0, transition: 'width 0.2s ease-out, opacity 0.2s ease-out' }"
37
+ />
38
+ <rect x="0.5" y="0.5" width="15" height="13" rx="3.5" stroke="currentColor" />
39
+ <rect
40
+ :x="panelWidth - 0.5"
41
+ y="0.5"
42
+ width="1"
43
+ height="13"
44
+ fill="currentColor"
45
+ :style="{ transition: 'x 0.2s ease-out' }"
46
+ />
47
+ </svg>
48
+ </button>
49
+ </template>
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
+ toggle: () => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ onToggle?: (() => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -26,6 +26,8 @@ type MeistrariChatEmbedSlots = SlotsType<{
26
26
  'sidebar-bottom'?: () => any;
27
27
  /** Rendered at the start of the conversation topbar, before the title. Page variant only. */
28
28
  'topbar-leading'?: () => any;
29
+ /** Rendered right under the prompt input, flush with it, on the empty state and in a conversation. Page variant only. */
30
+ 'composer-bottom'?: () => any;
29
31
  /** Rendered inside a failed assistant message's error box, next to "Tentar novamente"; receives the failed message. */
30
32
  'failed-message-actions'?: (props: { message: Message }) => any;
31
33
  }>;
@@ -121,6 +121,9 @@ const widgetEmbedProps = computed(() => ({
121
121
  <template v-if="$slots['topbar-leading']" #topbar-leading>
122
122
  <slot name="topbar-leading" />
123
123
  </template>
124
+ <template v-if="$slots['composer-bottom']" #composer-bottom>
125
+ <slot name="composer-bottom" />
126
+ </template>
124
127
  <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
125
128
  <slot name="failed-message-actions" v-bind="slotProps" />
126
129
  </template>
@@ -26,6 +26,8 @@ type MeistrariChatEmbedSlots = SlotsType<{
26
26
  'sidebar-bottom'?: () => any;
27
27
  /** Rendered at the start of the conversation topbar, before the title. Page variant only. */
28
28
  'topbar-leading'?: () => any;
29
+ /** Rendered right under the prompt input, flush with it, on the empty state and in a conversation. Page variant only. */
30
+ 'composer-bottom'?: () => any;
29
31
  /** Rendered inside a failed assistant message's error box, next to "Tentar novamente"; receives the failed message. */
30
32
  'failed-message-actions'?: (props: { message: Message }) => any;
31
33
  }>;
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
2
  import ChatMessageInput from "../../../messages/components/chat-message-input.vue";
3
+ import { useEmbedConfig } from "../../composables/embed-config";
4
+ import { resolveEmptyStateLabels } from "../../empty-state-labels";
3
5
  import ChatMobileFilesView from "./chat-mobile-files-view.vue";
4
6
  import ChatMobileHomeHeader from "./chat-mobile-home-header.vue";
5
7
  import ChatMobileHomeRecents from "./chat-mobile-home-recents.vue";
@@ -47,6 +49,8 @@ const props = defineProps({
47
49
  workspaceSheetOpen: { type: Boolean, required: true }
48
50
  });
49
51
  const emit = defineEmits(["cancel", "cancelEditing", "copyLink", "debug", "delete", "dismissOutdatedBanner", "duplicate", "export", "newConversation", "rename", "saveTitle", "selectConversation", "send", "settings", "signout", "switchWorkspace", "tabChange", "update:drawerOpen", "update:editTitle", "update:workspaceSheetOpen"]);
52
+ const embedConfig = useEmbedConfig();
53
+ const emptyStateLabels = computed(() => resolveEmptyStateLabels(embedConfig?.labels.value, props.currentUser?.name));
50
54
  const selectedModel = defineModel("selectedModel", { type: [String, null], ...{ default: null } });
51
55
  const selectedReasoningEffort = defineModel("selectedReasoningEffort", { type: null, ...{ default: null } });
52
56
  const renameInputRef = ref(null);
@@ -215,10 +219,10 @@ function startNewConversationFromOutdatedSettings() {
215
219
  <div flex="~ col 1" justify-center>
216
220
  <div px-20px pb-20px text-center>
217
221
  <h1 text-24px font-580 text-neutral-900 leading-28px tracking="-.4px">
218
- Olá, {{ currentUser?.name?.split(" ")[0] ?? "usu\xE1rio" }}
222
+ {{ emptyStateLabels.title }}
219
223
  </h1>
220
224
  <p text-14px text-neutral-500 leading-18px mt-6px>
221
- Como posso te ajudar hoje?
225
+ {{ emptyStateLabels.description }}
222
226
  </p>
223
227
  </div>
224
228
 
@@ -0,0 +1,5 @@
1
+ import type { ChatLabels } from './types.js';
2
+ export declare function resolveEmptyStateLabels(labels: Partial<ChatLabels> | undefined, userName: string | null | undefined): {
3
+ title: string;
4
+ description: string;
5
+ };
@@ -0,0 +1,9 @@
1
+ const DEFAULT_EMPTY_STATE_TITLE = "Ol\xE1, {name}";
2
+ const DEFAULT_EMPTY_STATE_DESCRIPTION = "Como posso te ajudar hoje?";
3
+ export function resolveEmptyStateLabels(labels, userName) {
4
+ const firstName = userName?.split(" ")[0] || "usu\xE1rio";
5
+ return {
6
+ title: (labels?.emptyStateTitle ?? DEFAULT_EMPTY_STATE_TITLE).replaceAll("{name}", firstName),
7
+ description: labels?.emptyStateDescription ?? DEFAULT_EMPTY_STATE_DESCRIPTION
8
+ };
9
+ }
@@ -10,6 +10,8 @@ export type ChatFeatureConfig = {
10
10
  showTtft?: boolean;
11
11
  /** Reliable outbound delivery and sandbox readiness handling. Defaults to true. */
12
12
  chatOutboundQueue?: boolean;
13
+ /** `@` / `/` shortcut hints below the empty-state composer. Defaults to true. */
14
+ showPromptInputHints?: boolean;
13
15
  };
14
16
  export type ResolvedChatFeatureConfig = {
15
17
  showUsageTab: boolean;
@@ -17,5 +19,6 @@ export type ResolvedChatFeatureConfig = {
17
19
  showCancelButton: boolean;
18
20
  showTtft: boolean;
19
21
  chatOutboundQueue: boolean;
22
+ showPromptInputHints: boolean;
20
23
  };
21
24
  export declare function resolveChatFeatures(features?: Partial<ChatFeatureConfig>): ResolvedChatFeatureConfig;
@@ -4,6 +4,7 @@ export function resolveChatFeatures(features) {
4
4
  showDebugOption: features?.showDebugOption ?? true,
5
5
  showCancelButton: features?.showCancelButton ?? true,
6
6
  showTtft: features?.showTtft ?? true,
7
- chatOutboundQueue: features?.chatOutboundQueue ?? true
7
+ chatOutboundQueue: features?.chatOutboundQueue ?? true,
8
+ showPromptInputHints: features?.showPromptInputHints ?? true
8
9
  };
9
10
  }
@@ -15,6 +15,13 @@ export type ChatLabels = {
15
15
  conversationSearchPlaceholder: string;
16
16
  /** Clear-button tooltip in the sidebar and mobile drawer search. Defaults to `Limpar`. */
17
17
  conversationSearchClear: string;
18
+ /**
19
+ * Empty-state heading. `{name}` is replaced with the user's first name
20
+ * (or `usuário` when unknown). Defaults to `Olá, {name}`.
21
+ */
22
+ emptyStateTitle: string;
23
+ /** Empty-state subheading. Defaults to `Como posso te ajudar hoje?`. */
24
+ emptyStateDescription: string;
18
25
  };
19
26
  export type ChatSuggestion = {
20
27
  /** Short label rendered on the suggestion pill. */
@@ -45,6 +52,12 @@ export type ChatSidebarConfig = {
45
52
  collapsible?: boolean;
46
53
  /** Start collapsed on first render when `collapsible` is set. Defaults to false. */
47
54
  defaultCollapsed?: boolean;
55
+ /**
56
+ * Where the collapse control lives when `collapsible` is set: `'edge'` is the handle on the
57
+ * sidebar's inner edge; `'topbar'` is chat-app's sidebar toggle at the start of the
58
+ * conversation topbar (desktop only). Defaults to `'edge'`.
59
+ */
60
+ toggle?: 'edge' | 'topbar';
48
61
  };
49
62
  export type ChatEmbedSharedProps = {
50
63
  /** Let the embed synchronize its conversation with the browser URL and title. Defaults to true. */
@@ -35,6 +35,7 @@ export declare function useChat(options?: Parameters<typeof useSharedChat>[0]):
35
35
  retryMessage: (messageId: string, prepare?: () => Promise<void>) => Promise<boolean>;
36
36
  cancelConversation: () => Promise<boolean>;
37
37
  startPolling: () => void;
38
+ reconnect: () => void;
38
39
  stopPolling: () => void;
39
40
  clearConversation: () => void;
40
41
  clearConversationThread: () => Promise<boolean>;
@@ -57,6 +57,7 @@ export declare function useChat(options?: {
57
57
  retryMessage: (messageId: string, prepare?: () => Promise<void>) => Promise<boolean>;
58
58
  cancelConversation: () => Promise<boolean>;
59
59
  startPolling: () => void;
60
+ reconnect: () => void;
60
61
  stopPolling: () => void;
61
62
  clearConversation: () => void;
62
63
  clearConversationThread: () => Promise<boolean>;
@@ -28,6 +28,12 @@ import { usePolling } from "../../shared/composables/polling.js";
28
28
  import { ChatMessageStatus, terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
29
29
  import { useDirectStream } from "./use-direct-stream.js";
30
30
  import { useOutboundQueue } from "./outbound-queue.js";
31
+ const POLL_OUTAGE_NOTICE_FAILURES = 5;
32
+ const POLL_OUTAGE_BUDGET_MS = 6e4;
33
+ const POLL_INTERVAL_MS = 500;
34
+ const POLL_MAX_BACKOFF_MS = 5e3;
35
+ const CHAT_REQUEST_TIMEOUT_MS = 15e3;
36
+ const CHAT_UNAVAILABLE_TEXT = "Chat temporariamente indispon\xEDvel. Tente novamente.";
31
37
  const WORKSPACE_AGENT_NOT_READY_TEXT = "O agente deste workspace ainda n\xE3o foi configurado no Tela. Abra as configura\xE7\xF5es do workspace para criar o agente.";
32
38
  export function resolveTelaAgentInputs(messageInputs, configuredInputs) {
33
39
  const inputs = messageInputs ?? configuredInputs;
@@ -241,6 +247,9 @@ export function useChat(options = {}, adapter = {}) {
241
247
  }
242
248
  }
243
249
  let pollGeneration = 0;
250
+ let consecutivePollFailures = 0;
251
+ let pollOutageStartedAt = null;
252
+ const pollInterval = ref(POLL_INTERVAL_MS);
244
253
  const pollMessages = async () => {
245
254
  if (!currentConversation.value)
246
255
  return;
@@ -251,14 +260,17 @@ export function useChat(options = {}, adapter = {}) {
251
260
  try {
252
261
  const runtime = chatApi.conversationV2Enabled ? await $fetch(
253
262
  chatApi.runtimePath(`/conversations/${conversationId}/runtime`),
254
- chatApi.withChatHeaders()
263
+ chatApi.withChatHeaders({ timeout: CHAT_REQUEST_TIMEOUT_MS, retry: 0 })
255
264
  ) : null;
256
265
  const response = await $fetch(
257
266
  chatApi.path(`/conversations/${conversationId}/messages`),
258
- chatApi.withChatHeaders()
267
+ chatApi.withChatHeaders({ timeout: CHAT_REQUEST_TIMEOUT_MS, retry: 0 })
259
268
  );
260
269
  if (generation !== pollGeneration || currentConversation.value?.id !== conversationId)
261
270
  return;
271
+ resetPollOutage();
272
+ if (error.value === CHAT_UNAVAILABLE_TEXT)
273
+ error.value = null;
262
274
  if (runtime)
263
275
  runtimeBlocked.value = runtime.conversation.runtimeState === "blocked";
264
276
  if (response.agentSession) {
@@ -332,16 +344,28 @@ export function useChat(options = {}, adapter = {}) {
332
344
  Sentry.captureException(err);
333
345
  if (generation !== pollGeneration)
334
346
  return;
335
- const errorRecord = err && typeof err === "object" ? err : null;
336
- const statusCode = typeof errorRecord?.statusCode === "number" ? errorRecord.statusCode : typeof errorRecord?.status === "number" ? errorRecord.status : typeof errorRecord?.response?.status === "number" ? errorRecord.response.status : null;
337
- if (statusCode && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429) {
347
+ const statusCode = getChatApiErrorStatus(err);
348
+ const terminalError = statusCode === 501 || statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429;
349
+ consecutivePollFailures++;
350
+ pollOutageStartedAt ??= Date.now();
351
+ pollInterval.value = Math.min(POLL_INTERVAL_MS * 2 ** consecutivePollFailures, POLL_MAX_BACKOFF_MS);
352
+ if (!terminalError && consecutivePollFailures >= POLL_OUTAGE_NOTICE_FAILURES)
353
+ error.value = CHAT_UNAVAILABLE_TEXT;
354
+ const outageExhausted = Date.now() - pollOutageStartedAt >= POLL_OUTAGE_BUDGET_MS;
355
+ if (terminalError || outageExhausted) {
356
+ const message = statusCode === 501 || !terminalError ? CHAT_UNAVAILABLE_TEXT : getChatApiErrorMessage(err) ?? "Erro ao atualizar mensagens";
357
+ if (!terminalError || statusCode === 501 || outbound.occupied.value) {
358
+ error.value = message;
359
+ stopPolling();
360
+ return;
361
+ }
338
362
  const previousMessages = messages.value;
339
- const failedMessages = markPendingAssistantMessagesFailed(previousMessages).map((message) => {
340
- if (message.role !== "assistant" || message.status !== ChatMessageStatus.Completed || !pendingStreamReconciliationIds.value.has(message.id)) {
341
- return message;
363
+ const failedMessages = markPendingAssistantMessagesFailed(previousMessages).map((message2) => {
364
+ if (message2.role !== "assistant" || message2.status !== ChatMessageStatus.Completed || !pendingStreamReconciliationIds.value.has(message2.id)) {
365
+ return message2;
342
366
  }
343
367
  return {
344
- ...message,
368
+ ...message2,
345
369
  content: GENERATION_FAILED_TEXT,
346
370
  status: ChatMessageStatus.Failed
347
371
  };
@@ -352,20 +376,30 @@ export function useChat(options = {}, adapter = {}) {
352
376
  messages.value = failedMessages;
353
377
  for (const terminalMessage of terminalMessages)
354
378
  chatMessageTerminal.emit(terminalMessage);
355
- error.value = getChatApiErrorMessage(err) ?? "Erro ao atualizar mensagens";
379
+ error.value = message;
356
380
  stopPolling();
357
381
  }
358
382
  }
359
383
  };
360
384
  const { isPolling, start: beginPolling, stop: endPolling } = usePolling({
361
385
  callback: pollMessages,
362
- interval: 500,
386
+ interval: pollInterval,
363
387
  stopCondition
364
388
  });
389
+ function resetPollOutage() {
390
+ consecutivePollFailures = 0;
391
+ pollOutageStartedAt = null;
392
+ pollInterval.value = POLL_INTERVAL_MS;
393
+ }
365
394
  function startPolling() {
395
+ resetPollOutage();
366
396
  pollGeneration++;
367
397
  beginPolling();
368
398
  }
399
+ function reconnect() {
400
+ error.value = null;
401
+ startPolling();
402
+ }
369
403
  function stopPolling() {
370
404
  pollGeneration++;
371
405
  endPolling();
@@ -614,7 +648,7 @@ export function useChat(options = {}, adapter = {}) {
614
648
  v2AdmissionRequested = true;
615
649
  const accepted = await $fetch(
616
650
  chatApi.runtimePath(`/conversations/${conversationId2}/messages`),
617
- chatApi.withChatHeaders({ method: "POST", body: v2Body, retry: 1 })
651
+ chatApi.withChatHeaders({ method: "POST", body: v2Body, retry: 1, timeout: CHAT_REQUEST_TIMEOUT_MS })
618
652
  );
619
653
  response = accepted;
620
654
  if (currentConversation.value?.id !== conversationId2)
@@ -661,7 +695,7 @@ export function useChat(options = {}, adapter = {}) {
661
695
  } catch (err) {
662
696
  const code = getChatApiErrorCode(err);
663
697
  const status = getChatApiErrorStatus(err);
664
- if (v2AdmissionRequested && !controller.signal.aborted && (status === null || status === 408 || status === 429 || status >= 500)) {
698
+ if (v2AdmissionRequested && !controller.signal.aborted && (status === null || status === 408 || status === 429 || status >= 500 && status !== 501)) {
665
699
  const attempts = (queuedSend?.attempts ?? 0) + 1;
666
700
  const queuedAt = queuedSend?.queuedAt ?? Date.now();
667
701
  if (outboundQueueEnabled.value) {
@@ -1029,6 +1063,7 @@ export function useChat(options = {}, adapter = {}) {
1029
1063
  retryMessage,
1030
1064
  cancelConversation,
1031
1065
  startPolling,
1066
+ reconnect,
1032
1067
  stopPolling,
1033
1068
  clearConversation,
1034
1069
  clearConversationThread,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "4.4.1-rc.1",
3
+ "version": "4.5.0-rc.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {