@meistrari/chat-nuxt 4.4.1-rc.1 → 4.5.0-rc.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.
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.0",
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_6: {}, __VLS_80: {}, __VLS_93: {
23
23
  message: useChatAuth;
24
- }, __VLS_127: {}, __VLS_129: {}, __VLS_210: {
24
+ }, __VLS_105: {}, __VLS_133: {}, __VLS_142: {}, __VLS_206: {}, __VLS_225: {
25
25
  message: useChatAuth;
26
- };
26
+ }, __VLS_237: {};
27
27
  type __VLS_Slots = {} & {
28
- 'sidebar-bottom'?: (props: typeof __VLS_11) => any;
28
+ 'sidebar-bottom'?: (props: typeof __VLS_6) => any;
29
+ } & {
30
+ 'composer-bottom'?: (props: typeof __VLS_80) => any;
31
+ } & {
32
+ 'failed-message-actions'?: (props: typeof __VLS_93) => any;
33
+ } & {
34
+ 'composer-bottom'?: (props: typeof __VLS_105) => any;
35
+ } & {
36
+ 'topbar-leading'?: (props: typeof __VLS_133) => any;
29
37
  } & {
30
- 'failed-message-actions'?: (props: typeof __VLS_96) => any;
38
+ 'topbar-leading'?: (props: typeof __VLS_142) => any;
31
39
  } & {
32
- 'topbar-leading'?: (props: typeof __VLS_127) => any;
40
+ 'composer-bottom'?: (props: typeof __VLS_206) => any;
33
41
  } & {
34
- 'topbar-leading'?: (props: typeof __VLS_129) => any;
42
+ 'failed-message-actions'?: (props: typeof __VLS_225) => any;
35
43
  } & {
36
- 'failed-message-actions'?: (props: typeof __VLS_210) => any;
44
+ 'composer-bottom'?: (props: typeof __VLS_237) => 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);
@@ -548,6 +554,7 @@ watch(
548
554
  { immediate: true }
549
555
  );
550
556
  const currentUser = computed(() => props.user ?? authUser.value ?? null);
557
+ const emptyStateLabels = computed(() => resolveEmptyStateLabels(embedConfig?.labels.value, currentUser.value?.name));
551
558
  const conversationCreator = computed(
552
559
  () => resolveConversationCreator(currentConversation.value?.userId, {
553
560
  currentUser: currentUser.value,
@@ -920,48 +927,18 @@ async function handleDelete() {
920
927
  :inert="sidebarCollapsed || void 0"
921
928
  h-full
922
929
  flex="~ col"
923
- bg-neutral-50
930
+ bg
931
+ border
924
932
  :style="{ width: `${sidebarWidth}px` }"
925
933
  :class="
926
- sidebarPosition === 'right' ? 'border-l-0.5px border-l-neutral-200' : 'border-r-0.5px border-r-neutral-200'
934
+ sidebarPosition === 'right' ? 'border-l-0.5px' : 'border-r-0.5px'
927
935
  "
928
936
  >
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
937
  <div
962
938
  min-h-0
963
939
  overflow-hidden
964
- px-4px
940
+ pt-12px
941
+ px-6px
965
942
  :class="$slots['sidebar-bottom'] ? '' : 'flex-1'"
966
943
  :style="
967
944
  $slots['sidebar-bottom'] ? { flexGrow: 100 - sidebarBottomHeight, flexBasis: 0 } : void 0
@@ -991,7 +968,7 @@ async function handleDelete() {
991
968
  </div>
992
969
 
993
970
  <button
994
- v-if="sidebarCollapsible"
971
+ v-if="sidebarCollapsible && sidebarToggle === 'edge'"
995
972
  type="button"
996
973
  class="chat-sidebar__toggle"
997
974
  :aria-label="
@@ -1091,8 +1068,8 @@ async function handleDelete() {
1091
1068
  <template #empty>
1092
1069
  <TelaChatEmptyState
1093
1070
  compact
1094
- :title="`Ol\xE1, ${currentUser?.name?.split(' ')[0] ?? 'usu\xE1rio'}`"
1095
- description="Como posso te ajudar hoje?"
1071
+ :title="emptyStateLabels.title"
1072
+ :description="emptyStateLabels.description"
1096
1073
  >
1097
1074
  <TelaChatSuggestions v-if="resolvedSuggestions.length">
1098
1075
  <TelaChatSuggestion
@@ -1105,7 +1082,7 @@ async function handleDelete() {
1105
1082
  </TelaChatSuggestion>
1106
1083
  </TelaChatSuggestions>
1107
1084
 
1108
- <TelaChatPromptInputHints size="sm" :hints="PROMPT_INPUT_HINTS" />
1085
+ <TelaChatPromptInputHints v-if="resolvedFeatures.showPromptInputHints" size="sm" :hints="PROMPT_INPUT_HINTS" />
1109
1086
 
1110
1087
  <template #input>
1111
1088
  <ChatMessageInput
@@ -1132,6 +1109,9 @@ async function handleDelete() {
1132
1109
  @send="handleSend"
1133
1110
  @cancel="handleCancel"
1134
1111
  />
1112
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full>
1113
+ <slot name="composer-bottom" />
1114
+ </div>
1135
1115
  </template>
1136
1116
  </TelaChatEmptyState>
1137
1117
  </template>
@@ -1181,6 +1161,9 @@ async function handleDelete() {
1181
1161
  @send="handleSend"
1182
1162
  @cancel="handleCancel"
1183
1163
  />
1164
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full max-w-700px mx-auto mt--12px>
1165
+ <slot name="composer-bottom" />
1166
+ </div>
1184
1167
  </div>
1185
1168
  </ChatMobileShell>
1186
1169
 
@@ -1211,15 +1194,17 @@ async function handleDelete() {
1211
1194
  @delete="showDeleteModal = true"
1212
1195
  @tab-change="handleTabChange"
1213
1196
  >
1214
- <template v-if="$slots['topbar-leading']" #leading>
1197
+ <template v-if="hasTopbarLeading" #leading>
1198
+ <ChatSidebarToggle v-if="showTopbarSidebarToggle" :open="!sidebarCollapsed" @toggle="toggleSidebarCollapsed" />
1215
1199
  <slot name="topbar-leading" />
1216
1200
  </template>
1217
1201
  </ChatTopbar>
1218
1202
  <div
1219
- v-else-if="$slots['topbar-leading'] && !isMobile && !compact"
1220
- absolute top-0 z-10 h-56px flex items-center pl-16px
1203
+ v-else-if="hasTopbarLeading && !isMobile && !compact"
1204
+ absolute top-0 z-10 h-56px flex items-center gap-8px pl-16px
1221
1205
  :style="{ left: 'var(--chat-panel-offset)' }"
1222
1206
  >
1207
+ <ChatSidebarToggle v-if="showTopbarSidebarToggle" :open="!sidebarCollapsed" @toggle="toggleSidebarCollapsed" />
1223
1208
  <slot name="topbar-leading" />
1224
1209
  </div>
1225
1210
 
@@ -1316,8 +1301,8 @@ async function handleDelete() {
1316
1301
  <TelaChatEmptyState
1317
1302
  v-else-if="!isMobile && !hasMessages"
1318
1303
  :compact="compact"
1319
- :title="`Ol\xE1, ${currentUser?.name?.split(' ')[0] ?? 'usu\xE1rio'}`"
1320
- description="Como posso te ajudar hoje?"
1304
+ :title="emptyStateLabels.title"
1305
+ :description="emptyStateLabels.description"
1321
1306
  :class="currentConversation && !compact ? 'pt-64px' : ''"
1322
1307
  >
1323
1308
  <TelaChatSuggestions v-if="resolvedSuggestions.length">
@@ -1332,6 +1317,7 @@ async function handleDelete() {
1332
1317
  </TelaChatSuggestions>
1333
1318
 
1334
1319
  <TelaChatPromptInputHints
1320
+ v-if="resolvedFeatures.showPromptInputHints"
1335
1321
  :size="compact ? 'sm' : 'md'"
1336
1322
  :hints="PROMPT_INPUT_HINTS"
1337
1323
  />
@@ -1361,6 +1347,9 @@ async function handleDelete() {
1361
1347
  @send="handleSend"
1362
1348
  @cancel="handleCancel"
1363
1349
  />
1350
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full>
1351
+ <slot name="composer-bottom" />
1352
+ </div>
1364
1353
  </template>
1365
1354
  </TelaChatEmptyState>
1366
1355
 
@@ -1413,6 +1402,9 @@ async function handleDelete() {
1413
1402
  @send="handleSend"
1414
1403
  @cancel="handleCancel"
1415
1404
  />
1405
+ <div v-if="$slots['composer-bottom']" data-chat-composer-bottom w-full max-w-700px mx-auto mt--12px>
1406
+ <slot name="composer-bottom" />
1407
+ </div>
1416
1408
  </div>
1417
1409
  </TelaChatBody>
1418
1410
 
@@ -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_6: {}, __VLS_80: {}, __VLS_93: {
23
23
  message: useChatAuth;
24
- }, __VLS_127: {}, __VLS_129: {}, __VLS_210: {
24
+ }, __VLS_105: {}, __VLS_133: {}, __VLS_142: {}, __VLS_206: {}, __VLS_225: {
25
25
  message: useChatAuth;
26
- };
26
+ }, __VLS_237: {};
27
27
  type __VLS_Slots = {} & {
28
- 'sidebar-bottom'?: (props: typeof __VLS_11) => any;
28
+ 'sidebar-bottom'?: (props: typeof __VLS_6) => any;
29
+ } & {
30
+ 'composer-bottom'?: (props: typeof __VLS_80) => any;
31
+ } & {
32
+ 'failed-message-actions'?: (props: typeof __VLS_93) => any;
33
+ } & {
34
+ 'composer-bottom'?: (props: typeof __VLS_105) => any;
35
+ } & {
36
+ 'topbar-leading'?: (props: typeof __VLS_133) => any;
29
37
  } & {
30
- 'failed-message-actions'?: (props: typeof __VLS_96) => any;
38
+ 'topbar-leading'?: (props: typeof __VLS_142) => any;
31
39
  } & {
32
- 'topbar-leading'?: (props: typeof __VLS_127) => any;
40
+ 'composer-bottom'?: (props: typeof __VLS_206) => any;
33
41
  } & {
34
- 'topbar-leading'?: (props: typeof __VLS_129) => any;
42
+ 'failed-message-actions'?: (props: typeof __VLS_225) => any;
35
43
  } & {
36
- 'failed-message-actions'?: (props: typeof __VLS_210) => any;
44
+ 'composer-bottom'?: (props: typeof __VLS_237) => 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. */
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.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {