@meistrari/chat-nuxt 1.11.0 → 1.12.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.
Files changed (72) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +15 -0
  3. package/dist/runtime/components/chat/conversation-item.d.vue.ts +6 -1
  4. package/dist/runtime/components/chat/conversation-item.vue +30 -141
  5. package/dist/runtime/components/chat/conversation-item.vue.d.ts +6 -1
  6. package/dist/runtime/components/chat/conversation-list.d.vue.ts +1 -0
  7. package/dist/runtime/components/chat/conversation-list.vue +300 -72
  8. package/dist/runtime/components/chat/conversation-list.vue.d.ts +1 -0
  9. package/dist/runtime/components/chat/conversation-new-group-button.d.vue.ts +7 -0
  10. package/dist/runtime/components/chat/conversation-new-group-button.vue +90 -0
  11. package/dist/runtime/components/chat/conversation-new-group-button.vue.d.ts +7 -0
  12. package/dist/runtime/components/chat/conversation-new-group-popover-content.d.vue.ts +23 -0
  13. package/dist/runtime/components/chat/conversation-new-group-popover-content.vue +75 -0
  14. package/dist/runtime/components/chat/conversation-new-group-popover-content.vue.d.ts +23 -0
  15. package/dist/runtime/components/chat/mobile/shell/drawer.d.vue.ts +2 -2
  16. package/dist/runtime/components/chat/mobile/shell/drawer.vue +370 -41
  17. package/dist/runtime/components/chat/mobile/shell/drawer.vue.d.ts +2 -2
  18. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +6 -6
  19. package/dist/runtime/components/chat/mobile/shell/shell.vue +1 -1
  20. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +6 -6
  21. package/dist/runtime/components/chat/topbar.d.vue.ts +2 -2
  22. package/dist/runtime/components/chat/topbar.vue.d.ts +2 -2
  23. package/dist/runtime/composables/useConversationGroups.d.ts +12 -0
  24. package/dist/runtime/composables/useConversationGroups.js +160 -0
  25. package/dist/runtime/composables/useConversationItem.d.ts +48 -0
  26. package/dist/runtime/composables/useConversationItem.js +176 -0
  27. package/dist/runtime/composables/useConversationList.d.ts +128 -0
  28. package/dist/runtime/composables/useConversationList.js +553 -0
  29. package/dist/runtime/composables/useConversations.d.ts +2 -0
  30. package/dist/runtime/composables/useConversations.js +45 -1
  31. package/dist/runtime/composables/usePendingConversationGroup.d.ts +5 -0
  32. package/dist/runtime/composables/usePendingConversationGroup.js +33 -0
  33. package/dist/runtime/embed/components/ChatConfigurationModal.d.vue.ts +1 -1
  34. package/dist/runtime/embed/components/ChatConfigurationModal.vue.d.ts +1 -1
  35. package/dist/runtime/embed/components/ChatEmbedInner.vue +24 -5
  36. package/dist/runtime/server/api/chat/conversations/[id]/group.patch.d.ts +1 -0
  37. package/dist/runtime/server/api/chat/conversations/[id]/group.patch.js +1 -0
  38. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.delete.d.ts +1 -0
  39. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.delete.js +1 -0
  40. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.patch.d.ts +1 -0
  41. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.patch.js +1 -0
  42. package/dist/runtime/server/api/chat/conversations/groups/index.get.d.ts +1 -0
  43. package/dist/runtime/server/api/chat/conversations/groups/index.get.js +1 -0
  44. package/dist/runtime/server/api/chat/conversations/groups/index.post.d.ts +1 -0
  45. package/dist/runtime/server/api/chat/conversations/groups/index.post.js +1 -0
  46. package/dist/runtime/server/api/conversations/[id]/duplicate.post.js +1 -0
  47. package/dist/runtime/server/api/conversations/[id]/group.patch.d.ts +2 -0
  48. package/dist/runtime/server/api/conversations/[id]/group.patch.js +46 -0
  49. package/dist/runtime/server/api/conversations/groups/[groupId]/index.delete.d.ts +4 -0
  50. package/dist/runtime/server/api/conversations/groups/[groupId]/index.delete.js +25 -0
  51. package/dist/runtime/server/api/conversations/groups/[groupId]/index.patch.d.ts +2 -0
  52. package/dist/runtime/server/api/conversations/groups/[groupId]/index.patch.js +30 -0
  53. package/dist/runtime/server/api/conversations/groups/index.get.d.ts +2 -0
  54. package/dist/runtime/server/api/conversations/groups/index.get.js +13 -0
  55. package/dist/runtime/server/api/conversations/groups/index.post.d.ts +2 -0
  56. package/dist/runtime/server/api/conversations/groups/index.post.js +26 -0
  57. package/dist/runtime/server/api/conversations/index.post.js +1 -0
  58. package/dist/runtime/server/db/schema/conversation-groups.d.ts +143 -0
  59. package/dist/runtime/server/db/schema/conversation-groups.js +20 -0
  60. package/dist/runtime/server/db/schema/conversations.d.ts +17 -0
  61. package/dist/runtime/server/db/schema/conversations.js +3 -0
  62. package/dist/runtime/server/db/schema/index.d.ts +1 -0
  63. package/dist/runtime/server/db/schema/index.js +1 -0
  64. package/dist/runtime/server/utils/conversation-group.d.ts +4 -0
  65. package/dist/runtime/server/utils/conversation-group.js +48 -0
  66. package/dist/runtime/types/chat.d.ts +2 -1
  67. package/dist/runtime/utils/conversation-groups.d.ts +18 -0
  68. package/dist/runtime/utils/conversation-groups.js +30 -0
  69. package/drizzle/0016_dry_aaron_stack.sql +16 -0
  70. package/drizzle/meta/0016_snapshot.json +887 -0
  71. package/drizzle/meta/_journal.json +7 -0
  72. package/package.json +1 -1
@@ -12,11 +12,11 @@ type __VLS_Props = {
12
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
13
  select: (id: string) => any;
14
14
  close: () => any;
15
- newConvo: () => any;
15
+ newConvo: (groupId?: string | null | undefined) => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onSelect?: ((id: string) => any) | undefined;
18
18
  onClose?: (() => any) | undefined;
19
- onNewConvo?: (() => any) | undefined;
19
+ onNewConvo?: ((groupId?: string | null | undefined) => any) | undefined;
20
20
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
21
  declare const _default: typeof __VLS_export;
22
22
  export default _default;
@@ -1,7 +1,10 @@
1
1
  <script setup>
2
+ import { useChatAuth } from "#chat-auth";
2
3
  import { formatRelativeTime } from "../../../../utils/format-time";
3
4
  import { conversationListSkeletonRows } from "../../../../utils/conversation-list-skeleton";
4
5
  import { filterConversationsByTitle } from "../../../../utils/conversation-search";
6
+ import { groupConversationsByGroup } from "../../../../utils/conversation-groups";
7
+ import { resolveChatStateScope } from "../../../../utils/tela-chat";
5
8
  const props = defineProps({
6
9
  open: { type: Boolean, required: true },
7
10
  conversations: { type: null, required: true },
@@ -12,11 +15,28 @@ const props = defineProps({
12
15
  userImage: { type: String, required: false }
13
16
  });
14
17
  const emit = defineEmits(["close", "select", "newConvo"]);
18
+ const DRAG_OPEN_GROUP_DELAY_MS = 400;
15
19
  const initial = computed(() => {
16
20
  const source = props.userName ?? props.userEmail ?? "U";
17
21
  return source.charAt(0).toUpperCase();
18
22
  });
19
23
  const conversationSearch = ref("");
24
+ const draggedConversationId = ref(null);
25
+ const dropTargetGroupId = ref(void 0);
26
+ const openGroupIds = ref(/* @__PURE__ */ new Set());
27
+ const groupsReady = ref(false);
28
+ let dragOpenGroupTimeout = null;
29
+ let dragOpenGroupId = null;
30
+ const embedConfig = useEmbedConfig();
31
+ const { activeOrganization } = useChatAuth();
32
+ const { groups, fetchGroups } = useConversationGroups();
33
+ const { moveConversationToGroup } = useConversations();
34
+ const runtimeScope = computed(() => resolveChatStateScope(
35
+ embedConfig?.workspaceId.value || activeOrganization.value?.id,
36
+ embedConfig?.telaAgentId.value,
37
+ embedConfig?.conversationScope?.value
38
+ ));
39
+ const openGroupsStorageKey = computed(() => `conversation-group-open:${runtimeScope.value}`);
20
40
  const filteredConversations = computed(
21
41
  () => filterConversationsByTitle(props.conversations, conversationSearch.value, {
22
42
  fallbackTitle: "Nova conversa",
@@ -24,14 +44,179 @@ const filteredConversations = computed(
24
44
  })
25
45
  );
26
46
  const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
47
+ const filteredGroupedConversations = computed(() => groupConversationsByGroup(filteredConversations.value, groups.value));
48
+ const allGroupedConversations = computed(() => groupConversationsByGroup(props.conversations, groups.value));
49
+ const groupedConversations = computed(() => {
50
+ if (!hasConversationSearchQuery.value)
51
+ return filteredGroupedConversations.value;
52
+ const allItemsByGroupId = new Map(
53
+ allGroupedConversations.value.groups.map((groupBucket) => [groupBucket.group.id, groupBucket.items])
54
+ );
55
+ return {
56
+ groups: filteredGroupedConversations.value.groups.map(
57
+ (groupBucket) => doesGroupMatchSearch(groupBucket.group.name) ? {
58
+ ...groupBucket,
59
+ items: allItemsByGroupId.get(groupBucket.group.id) ?? []
60
+ } : groupBucket
61
+ ),
62
+ unfiled: filteredGroupedConversations.value.unfiled
63
+ };
64
+ });
65
+ const visibleGroupBuckets = computed(() => groupedConversations.value.groups.filter((groupBucket) => {
66
+ if (!hasConversationSearchQuery.value)
67
+ return true;
68
+ return groupBucket.items.length > 0 || doesGroupMatchSearch(groupBucket.group.name);
69
+ }).map((groupBucket) => {
70
+ const open = isGroupOpen(groupBucket.group.id) || hasConversationSearchQuery.value && (groupBucket.items.length > 0 || doesGroupMatchSearch(groupBucket.group.name));
71
+ return {
72
+ ...groupBucket,
73
+ open,
74
+ folderIcon: open ? "i-ph-folder-open" : "i-ph-folder",
75
+ conversationCountLabel: groupBucket.items.length > 0 ? String(groupBucket.items.length) : null,
76
+ showNewConversationAction: !hasConversationSearchQuery.value,
77
+ showSearchEmpty: hasConversationSearchQuery.value && groupBucket.items.length === 0
78
+ };
79
+ }));
80
+ const showGroupCreationPrompt = computed(
81
+ () => groupsReady.value && !hasConversationSearchQuery.value && groups.value.length === 0
82
+ );
83
+ const hasConversationContent = computed(() => {
84
+ if (hasConversationSearchQuery.value)
85
+ return visibleGroupBuckets.value.length > 0 || groupedConversations.value.unfiled.length > 0;
86
+ return showGroupCreationPrompt.value || groups.value.length > 0 || filteredConversations.value.length > 0;
87
+ });
88
+ const shouldShowUngroupedHeader = computed(() => visibleGroupBuckets.value.length > 0);
89
+ const ungroupedConversationLabel = computed(() => `Conversas \xB7 ${groupedConversations.value.unfiled.length}`);
90
+ const activeGroupId = computed(() => {
91
+ const currentConversation = props.conversations.find((conversation) => conversation.id === props.activeId);
92
+ return currentConversation?.groupId ?? null;
93
+ });
94
+ onMounted(() => {
95
+ void Promise.resolve(fetchGroups()).finally(() => {
96
+ groupsReady.value = true;
97
+ });
98
+ try {
99
+ const stored = localStorage.getItem(openGroupsStorageKey.value);
100
+ if (stored) {
101
+ openGroupIds.value = new Set(JSON.parse(stored));
102
+ }
103
+ } catch {
104
+ }
105
+ });
106
+ watch(openGroupIds, (groupIds) => {
107
+ if (typeof localStorage === "undefined")
108
+ return;
109
+ try {
110
+ localStorage.setItem(openGroupsStorageKey.value, JSON.stringify([...groupIds]));
111
+ } catch {
112
+ }
113
+ });
114
+ watch(activeGroupId, (groupId) => {
115
+ if (groupId) {
116
+ setGroupOpen(groupId, true);
117
+ }
118
+ }, { immediate: true });
119
+ onBeforeUnmount(() => {
120
+ clearDragOpenGroup();
121
+ });
122
+ function isGroupOpen(groupId) {
123
+ return openGroupIds.value.has(groupId);
124
+ }
125
+ function setGroupOpen(groupId, open) {
126
+ const nextOpenGroupIds = new Set(openGroupIds.value);
127
+ if (open) {
128
+ nextOpenGroupIds.add(groupId);
129
+ } else {
130
+ nextOpenGroupIds.delete(groupId);
131
+ }
132
+ openGroupIds.value = nextOpenGroupIds;
133
+ }
134
+ function toggleGroup(groupId) {
135
+ setGroupOpen(groupId, !isGroupOpen(groupId));
136
+ }
137
+ function doesGroupMatchSearch(groupName) {
138
+ const query = conversationSearch.value.trim().toLowerCase();
139
+ return query.length > 0 && groupName.toLowerCase().includes(query);
140
+ }
27
141
  function handleSelect(id) {
28
142
  emit("select", id);
29
143
  emit("close");
30
144
  }
31
- function handleNew() {
32
- emit("newConvo");
145
+ function handleNew(groupId) {
146
+ if (groupId) {
147
+ setGroupOpen(groupId, true);
148
+ }
149
+ emit("newConvo", groupId);
33
150
  emit("close");
34
151
  }
152
+ function handleConversationDragStart(event, id) {
153
+ draggedConversationId.value = id;
154
+ event.dataTransfer?.setData("text/plain", id);
155
+ if (event.dataTransfer) {
156
+ event.dataTransfer.effectAllowed = "move";
157
+ }
158
+ }
159
+ function clearDragState() {
160
+ draggedConversationId.value = null;
161
+ dropTargetGroupId.value = void 0;
162
+ clearDragOpenGroup();
163
+ }
164
+ function clearDragOpenGroup() {
165
+ if (dragOpenGroupTimeout) {
166
+ clearTimeout(dragOpenGroupTimeout);
167
+ }
168
+ dragOpenGroupTimeout = null;
169
+ dragOpenGroupId = null;
170
+ }
171
+ function scheduleDragOpenGroup(groupId) {
172
+ if (!groupId || isGroupOpen(groupId)) {
173
+ clearDragOpenGroup();
174
+ return;
175
+ }
176
+ if (dragOpenGroupId === groupId)
177
+ return;
178
+ clearDragOpenGroup();
179
+ dragOpenGroupId = groupId;
180
+ dragOpenGroupTimeout = setTimeout(() => {
181
+ setGroupOpen(groupId, true);
182
+ clearDragOpenGroup();
183
+ }, DRAG_OPEN_GROUP_DELAY_MS);
184
+ }
185
+ function handleDragEnter(groupId) {
186
+ if (!draggedConversationId.value)
187
+ return;
188
+ dropTargetGroupId.value = groupId;
189
+ scheduleDragOpenGroup(groupId);
190
+ }
191
+ function handleDragOver(event, groupId) {
192
+ if (!draggedConversationId.value)
193
+ return;
194
+ if (event.dataTransfer) {
195
+ event.dataTransfer.dropEffect = "move";
196
+ }
197
+ dropTargetGroupId.value = groupId;
198
+ scheduleDragOpenGroup(groupId);
199
+ }
200
+ function handleDragLeave(event, groupId) {
201
+ const currentTarget = event.currentTarget;
202
+ const relatedTarget = event.relatedTarget;
203
+ if (currentTarget && relatedTarget && currentTarget.contains(relatedTarget))
204
+ return;
205
+ if (dropTargetGroupId.value === groupId) {
206
+ dropTargetGroupId.value = void 0;
207
+ clearDragOpenGroup();
208
+ }
209
+ }
210
+ function isDropTarget(groupId) {
211
+ return draggedConversationId.value !== null && dropTargetGroupId.value === groupId;
212
+ }
213
+ async function handleConversationDrop(event, groupId) {
214
+ const conversationId = event.dataTransfer?.getData("text/plain") || draggedConversationId.value;
215
+ clearDragState();
216
+ if (!conversationId)
217
+ return;
218
+ await moveConversationToGroup(conversationId, groupId);
219
+ }
35
220
  </script>
36
221
 
37
222
  <template>
@@ -84,32 +269,26 @@ function handleNew() {
84
269
  </div>
85
270
 
86
271
  <div p-12px flex="~ col" gap-8px>
87
- <button
88
- flex items-center justify-center gap-6px
89
- w-full px-12px py-10px rounded-10px
90
- bg-white b=".5px neutral-300" shadow-sm
91
- class="hover:bg-neutral-50"
92
- transition-colors cursor-pointer
93
- @click="handleNew"
94
- >
95
- <span i-ph-chat-circle text-16px text-neutral-900 />
96
- <span text-14px font-580 text-neutral-900 leading-18px tracking="-.15px">
97
- Nova conversa
98
- </span>
99
- </button>
272
+ <div flex gap-8px>
273
+ <button
274
+ flex items-center justify-center gap-6px
275
+ flex-1 min-w-0 px-12px py-10px rounded-10px
276
+ bg-white b=".5px neutral-300" shadow-sm
277
+ class="hover:bg-neutral-50"
278
+ transition-colors cursor-pointer
279
+ @click="handleNew()"
280
+ >
281
+ <span i-ph-chat-circle text-16px text-neutral-900 />
282
+ <span text-14px font-580 text-neutral-900 leading-18px tracking="-.15px">
283
+ Nova conversa
284
+ </span>
285
+ </button>
286
+ <ChatConversationNewGroupButton />
287
+ </div>
100
288
  <ChatConversationSearch v-model="conversationSearch" />
101
289
  <ChatConversationCreatorFilter />
102
290
  </div>
103
291
 
104
- <div px-14px pt-4px pb-4px>
105
- <span
106
- text-12px font-medium uppercase text-neutral-400
107
- style="letter-spacing: 0.5px"
108
- >
109
- {{ hasConversationSearchQuery ? "Resultados" : "Recentes" }}
110
- </span>
111
- </div>
112
-
113
292
  <div flex-1 overflow-y-auto px-8px pb-8px>
114
293
  <div v-if="loading && conversations.length === 0" flex="~ col" gap-1px py-4px>
115
294
  <div
@@ -120,35 +299,185 @@ function handleNew() {
120
299
  <TelaSkeleton rounded-6px bg-neutral-200 :style="{ width: row.width, height: '13px' }" />
121
300
  </div>
122
301
  </div>
123
- <div v-else-if="filteredConversations.length === 0" p-16px text-center>
302
+ <div v-else-if="!hasConversationContent" p-16px text-center>
124
303
  <p text-14px text-neutral-400>
125
304
  {{ hasConversationSearchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda" }}
126
305
  </p>
127
306
  </div>
128
- <div v-else flex="~ col" gap-1px>
129
- <button
130
- v-for="conv in filteredConversations"
131
- :key="conv.id"
132
- flex="~ col" gap-2px
133
- w-full px-12px py-10px rounded-10px text-left
134
- transition-colors cursor-pointer
135
- :class="conv.id === activeId ? 'bg-neutral-100' : 'hover:bg-neutral-50'"
136
- @click="handleSelect(conv.id)"
307
+ <div v-else flex="~ col" gap-8px>
308
+ <div
309
+ v-if="showGroupCreationPrompt"
310
+ mx-4px
311
+ flex="~ col" gap-10px
137
312
  >
138
- <div flex items-center gap-8px w-full>
139
- <span flex-1 text-14px font-medium text-neutral-900 leading-18px truncate>
140
- {{ conv.title || "Nova conversa" }}
313
+ <div h-20px flex items-center px-4px>
314
+ <span text-12px font-semibold text-neutral-400 uppercase style="letter-spacing: 0">
315
+ PASTAS
141
316
  </span>
142
- <span text-12px text-neutral-400 leading-16px shrink-0>
143
- {{ formatRelativeTime(conv.updatedAt) }}
317
+ </div>
318
+ <div flex="~ col" gap-6px>
319
+ <div
320
+ h-36px px-10px
321
+ flex items-center gap-10px
322
+ rounded-10px
323
+ b=".5px dashed neutral-200"
324
+ >
325
+ <span i-ph-folder text-16px text-neutral-400 shrink-0 />
326
+ <span h-10px w-120px rounded-full bg-neutral-100 />
327
+ </div>
328
+ <div
329
+ h-36px px-10px
330
+ flex items-center gap-10px
331
+ rounded-10px
332
+ b=".5px dashed neutral-200"
333
+ >
334
+ <span i-ph-folder text-16px text-neutral-400 shrink-0 />
335
+ <span h-10px w-84px rounded-full bg-neutral-100 />
336
+ </div>
337
+ </div>
338
+ <p px-4px text-14px text-neutral-500 leading-20px>
339
+ Suas pastas aparecem aqui. Crie uma para começar a agrupar conversas.
340
+ </p>
341
+ <ChatConversationNewGroupButton label="Criar pasta" variant="empty-state" />
342
+ </div>
343
+
344
+ <div v-if="visibleGroupBuckets.length > 0" flex="~ col" gap-2px>
345
+ <div
346
+ v-for="groupBucket in visibleGroupBuckets"
347
+ :key="groupBucket.group.id"
348
+ flex="~ col"
349
+ rounded-10px
350
+ transition-colors
351
+ :class="isDropTarget(groupBucket.group.id) ? 'bg-neutral-100' : ''"
352
+ @dragenter.prevent="handleDragEnter(groupBucket.group.id)"
353
+ @dragover.prevent="handleDragOver($event, groupBucket.group.id)"
354
+ @dragleave="handleDragLeave($event, groupBucket.group.id)"
355
+ @drop.prevent="handleConversationDrop($event, groupBucket.group.id)"
356
+ >
357
+ <div rounded-10px>
358
+ <button
359
+ type="button"
360
+ flex items-center gap-8px
361
+ w-full h-40px px-10px rounded-10px text-left
362
+ transition-colors cursor-pointer
363
+ class="hover:bg-neutral-50"
364
+ @click="toggleGroup(groupBucket.group.id)"
365
+ >
366
+ <span
367
+ i-ph-caret-right
368
+ text-12px text-neutral-400
369
+ transition-transform
370
+ :class="groupBucket.open && 'rotate-90'"
371
+ />
372
+ <span
373
+ :class="groupBucket.folderIcon"
374
+ text-16px text-neutral-500 shrink-0
375
+ />
376
+ <span flex-1 text-14px font-semibold text-neutral-900 leading-18px truncate>
377
+ {{ groupBucket.group.name }}
378
+ </span>
379
+ <span v-if="groupBucket.conversationCountLabel" text-12px font-medium text-neutral-400 tabular-nums>
380
+ {{ groupBucket.conversationCountLabel }}
381
+ </span>
382
+ </button>
383
+ </div>
384
+
385
+ <Transition name="group-slide">
386
+ <div v-if="groupBucket.open" class="group-slide-panel">
387
+ <div class="group-slide__inner">
388
+ <div flex="~ col" gap-1px ml-18px>
389
+ <button
390
+ v-for="conv in groupBucket.items"
391
+ :key="conv.id"
392
+ draggable="true"
393
+ flex="~ col" gap-2px
394
+ w-full px-12px py-10px rounded-10px text-left
395
+ transition-colors cursor-pointer
396
+ :class="conv.id === activeId ? 'bg-neutral-100' : 'hover:bg-neutral-50'"
397
+ @click="handleSelect(conv.id)"
398
+ @dragstart="handleConversationDragStart($event, conv.id)"
399
+ @dragend="clearDragState"
400
+ >
401
+ <div flex items-center gap-8px w-full>
402
+ <span flex-1 text-14px font-medium text-neutral-900 leading-18px truncate>
403
+ {{ conv.title || "Nova conversa" }}
404
+ </span>
405
+ <span text-12px text-neutral-400 leading-16px shrink-0>
406
+ {{ formatRelativeTime(conv.updatedAt) }}
407
+ </span>
408
+ </div>
409
+ </button>
410
+ <button
411
+ v-if="groupBucket.showNewConversationAction"
412
+ type="button"
413
+ flex items-center gap-8px
414
+ w-full px-12px py-10px rounded-10px text-left
415
+ transition-colors cursor-pointer
416
+ class="hover:bg-neutral-50"
417
+ @click="handleNew(groupBucket.group.id)"
418
+ >
419
+ <span i-ph-plus text-14px text-neutral-500 shrink-0 />
420
+ <span text-14px font-medium text-neutral-500 leading-18px>
421
+ Nova conversa
422
+ </span>
423
+ </button>
424
+ <div
425
+ v-else-if="groupBucket.showSearchEmpty"
426
+ px-12px py-10px
427
+ >
428
+ <span text-13px text-neutral-400 leading-17px>
429
+ Sem resultados
430
+ </span>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ </Transition>
436
+ </div>
437
+ </div>
438
+
439
+ <div
440
+ flex="~ col" gap-1px
441
+ rounded-10px
442
+ transition-colors
443
+ :class="isDropTarget(null) ? 'bg-neutral-50' : ''"
444
+ @dragenter.prevent="handleDragEnter(null)"
445
+ @dragover.prevent="handleDragOver($event, null)"
446
+ @dragleave="handleDragLeave($event, null)"
447
+ @drop.prevent="handleConversationDrop($event, null)"
448
+ >
449
+ <div v-if="shouldShowUngroupedHeader" px-6px pt-4px pb-2px>
450
+ <span text-12px font-semibold uppercase text-neutral-400 style="letter-spacing: 0">
451
+ {{ ungroupedConversationLabel }}
144
452
  </span>
145
453
  </div>
146
- </button>
454
+ <button
455
+ v-for="conv in groupedConversations.unfiled"
456
+ :key="conv.id"
457
+ draggable="true"
458
+ flex="~ col" gap-2px
459
+ w-full px-12px py-10px rounded-10px text-left
460
+ transition-colors cursor-pointer
461
+ :class="conv.id === activeId ? 'bg-neutral-100' : 'hover:bg-neutral-50'"
462
+ @click="handleSelect(conv.id)"
463
+ @dragstart="handleConversationDragStart($event, conv.id)"
464
+ @dragend="clearDragState"
465
+ >
466
+ <div flex items-center gap-8px w-full>
467
+ <span flex-1 text-14px font-medium text-neutral-900 leading-18px truncate>
468
+ {{ conv.title || "Nova conversa" }}
469
+ </span>
470
+ <span text-12px text-neutral-400 leading-16px shrink-0>
471
+ {{ formatRelativeTime(conv.updatedAt) }}
472
+ </span>
473
+ </div>
474
+ </button>
475
+ </div>
147
476
  </div>
148
477
  </div>
149
478
  </div>
150
479
  </template>
151
480
 
152
481
  <style scoped>
153
- .mobile-drawer{box-shadow:0 3px 12px rgba(103,127,148,.08),0 12px 52px rgba(103,127,148,.16);transform:translateX(-100%);transition:transform .26s cubic-bezier(.2,.8,.2,1);width:82%}.mobile-drawer--open{transform:translateX(0)}.scrim-enter-active,.scrim-leave-active{transition:opacity .2s linear}.scrim-enter-from,.scrim-leave-to{opacity:0}
482
+ .mobile-drawer{box-shadow:0 3px 12px rgba(103,127,148,.08),0 12px 52px rgba(103,127,148,.16);transform:translateX(-100%);transition:transform .26s cubic-bezier(.2,.8,.2,1);width:82%}.mobile-drawer--open{transform:translateX(0)}.scrim-enter-active,.scrim-leave-active{transition:opacity .2s linear}.scrim-enter-from,.scrim-leave-to{opacity:0}.group-slide-panel{display:grid;grid-template-rows:1fr}.group-slide-enter-active,.group-slide-leave-active{overflow:hidden;transition:grid-template-rows .18s cubic-bezier(.2,.8,.2,1),opacity .14s ease,transform .18s cubic-bezier(.2,.8,.2,1)}.group-slide-enter-from,.group-slide-leave-to{grid-template-rows:0fr;opacity:0;transform:translateY(-4px)}.group-slide-enter-to,.group-slide-leave-from{grid-template-rows:1fr;opacity:1;transform:translateY(0)}.group-slide__inner{min-height:0;overflow:hidden}
154
483
  </style>
@@ -12,11 +12,11 @@ type __VLS_Props = {
12
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
13
  select: (id: string) => any;
14
14
  close: () => any;
15
- newConvo: () => any;
15
+ newConvo: (groupId?: string | null | undefined) => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onSelect?: ((id: string) => any) | undefined;
18
18
  onClose?: (() => any) | undefined;
19
- onNewConvo?: (() => any) | undefined;
19
+ onNewConvo?: ((groupId?: string | null | undefined) => any) | undefined;
20
20
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
21
  declare const _default: typeof __VLS_export;
22
22
  export default _default;
@@ -50,10 +50,11 @@ type __VLS_Slots = {} & {
50
50
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
51
51
  delete: () => any;
52
52
  cancel: () => any;
53
- selectConversation: (id: string) => any;
54
53
  rename: () => any;
55
54
  duplicate: () => any;
56
55
  export: () => any;
56
+ copyLink: () => any;
57
+ selectConversation: (id: string) => any;
57
58
  send: (content: string, files: FileUploadInput[], model?: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | undefined, agentInputs?: ({
58
59
  type: "file";
59
60
  name: string;
@@ -70,10 +71,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
70
71
  "update:editTitle": (value: string) => any;
71
72
  saveTitle: () => any;
72
73
  cancelEditing: () => any;
73
- copyLink: () => any;
74
74
  debug: () => any;
75
75
  tabChange: (tab: TopbarTab) => any;
76
- newConversation: () => any;
76
+ newConversation: (groupId?: string | null | undefined) => any;
77
77
  switchWorkspace: () => any;
78
78
  signout: () => any;
79
79
  dismissOutdatedBanner: () => any;
@@ -82,10 +82,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
82
82
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
83
83
  onDelete?: (() => any) | undefined;
84
84
  onCancel?: (() => any) | undefined;
85
- onSelectConversation?: ((id: string) => any) | undefined;
86
85
  onRename?: (() => any) | undefined;
87
86
  onDuplicate?: (() => any) | undefined;
88
87
  onExport?: (() => any) | undefined;
88
+ onCopyLink?: (() => any) | undefined;
89
+ onSelectConversation?: ((id: string) => any) | undefined;
89
90
  onSend?: ((content: string, files: FileUploadInput[], model?: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | undefined, agentInputs?: ({
90
91
  type: "file";
91
92
  name: string;
@@ -102,10 +103,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
102
103
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
103
104
  onSaveTitle?: (() => any) | undefined;
104
105
  onCancelEditing?: (() => any) | undefined;
105
- onCopyLink?: (() => any) | undefined;
106
106
  onDebug?: (() => any) | undefined;
107
107
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
108
- onNewConversation?: (() => any) | undefined;
108
+ onNewConversation?: ((groupId?: string | null | undefined) => any) | undefined;
109
109
  onSwitchWorkspace?: (() => any) | undefined;
110
110
  onSignout?: (() => any) | undefined;
111
111
  onDismissOutdatedBanner?: (() => any) | undefined;
@@ -148,7 +148,7 @@ function startNewConversationFromOutdatedSettings() {
148
148
  :user-image="currentUser?.image ?? void 0"
149
149
  @close="setDrawerOpen(false)"
150
150
  @select="emit('selectConversation', $event)"
151
- @new-convo="emit('newConversation')"
151
+ @new-convo="emit('newConversation', $event)"
152
152
  />
153
153
 
154
154
  <ChatMobileBottomSheet
@@ -50,10 +50,11 @@ type __VLS_Slots = {} & {
50
50
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
51
51
  delete: () => any;
52
52
  cancel: () => any;
53
- selectConversation: (id: string) => any;
54
53
  rename: () => any;
55
54
  duplicate: () => any;
56
55
  export: () => any;
56
+ copyLink: () => any;
57
+ selectConversation: (id: string) => any;
57
58
  send: (content: string, files: FileUploadInput[], model?: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | undefined, agentInputs?: ({
58
59
  type: "file";
59
60
  name: string;
@@ -70,10 +71,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
70
71
  "update:editTitle": (value: string) => any;
71
72
  saveTitle: () => any;
72
73
  cancelEditing: () => any;
73
- copyLink: () => any;
74
74
  debug: () => any;
75
75
  tabChange: (tab: TopbarTab) => any;
76
- newConversation: () => any;
76
+ newConversation: (groupId?: string | null | undefined) => any;
77
77
  switchWorkspace: () => any;
78
78
  signout: () => any;
79
79
  dismissOutdatedBanner: () => any;
@@ -82,10 +82,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
82
82
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
83
83
  onDelete?: (() => any) | undefined;
84
84
  onCancel?: (() => any) | undefined;
85
- onSelectConversation?: ((id: string) => any) | undefined;
86
85
  onRename?: (() => any) | undefined;
87
86
  onDuplicate?: (() => any) | undefined;
88
87
  onExport?: (() => any) | undefined;
88
+ onCopyLink?: (() => any) | undefined;
89
+ onSelectConversation?: ((id: string) => any) | undefined;
89
90
  onSend?: ((content: string, files: FileUploadInput[], model?: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | undefined, agentInputs?: ({
90
91
  type: "file";
91
92
  name: string;
@@ -102,10 +103,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
102
103
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
103
104
  onSaveTitle?: (() => any) | undefined;
104
105
  onCancelEditing?: (() => any) | undefined;
105
- onCopyLink?: (() => any) | undefined;
106
106
  onDebug?: (() => any) | undefined;
107
107
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
108
- onNewConversation?: (() => any) | undefined;
108
+ onNewConversation?: ((groupId?: string | null | undefined) => any) | undefined;
109
109
  onSwitchWorkspace?: (() => any) | undefined;
110
110
  onSignout?: (() => any) | undefined;
111
111
  onDismissOutdatedBanner?: (() => any) | undefined;
@@ -19,11 +19,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
19
  rename: () => any;
20
20
  duplicate: () => any;
21
21
  export: () => any;
22
+ copyLink: () => any;
22
23
  "update:isEditing": (value: boolean) => any;
23
24
  "update:editTitle": (value: string) => any;
24
25
  saveTitle: () => any;
25
26
  cancelEditing: () => any;
26
- copyLink: () => any;
27
27
  debug: () => any;
28
28
  tabChange: (tab: TopbarTab) => any;
29
29
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
@@ -31,11 +31,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
31
31
  onRename?: (() => any) | undefined;
32
32
  onDuplicate?: (() => any) | undefined;
33
33
  onExport?: (() => any) | undefined;
34
+ onCopyLink?: (() => any) | undefined;
34
35
  "onUpdate:isEditing"?: ((value: boolean) => any) | undefined;
35
36
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
36
37
  onSaveTitle?: (() => any) | undefined;
37
38
  onCancelEditing?: (() => any) | undefined;
38
- onCopyLink?: (() => any) | undefined;
39
39
  onDebug?: (() => any) | undefined;
40
40
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
41
41
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -19,11 +19,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
19
  rename: () => any;
20
20
  duplicate: () => any;
21
21
  export: () => any;
22
+ copyLink: () => any;
22
23
  "update:isEditing": (value: boolean) => any;
23
24
  "update:editTitle": (value: string) => any;
24
25
  saveTitle: () => any;
25
26
  cancelEditing: () => any;
26
- copyLink: () => any;
27
27
  debug: () => any;
28
28
  tabChange: (tab: TopbarTab) => any;
29
29
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
@@ -31,11 +31,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
31
31
  onRename?: (() => any) | undefined;
32
32
  onDuplicate?: (() => any) | undefined;
33
33
  onExport?: (() => any) | undefined;
34
+ onCopyLink?: (() => any) | undefined;
34
35
  "onUpdate:isEditing"?: ((value: boolean) => any) | undefined;
35
36
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
36
37
  onSaveTitle?: (() => any) | undefined;
37
38
  onCancelEditing?: (() => any) | undefined;
38
- onCopyLink?: (() => any) | undefined;
39
39
  onDebug?: (() => any) | undefined;
40
40
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
41
41
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;