@meistrari/chat-nuxt 1.10.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 (82) 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/header.d.vue.ts +4 -4
  19. package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +4 -4
  20. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +20 -20
  21. package/dist/runtime/components/chat/mobile/shell/shell.vue +1 -1
  22. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +20 -20
  23. package/dist/runtime/components/chat/topbar.d.vue.ts +8 -8
  24. package/dist/runtime/components/chat/topbar.vue.d.ts +8 -8
  25. package/dist/runtime/composables/useConversationGroups.d.ts +12 -0
  26. package/dist/runtime/composables/useConversationGroups.js +160 -0
  27. package/dist/runtime/composables/useConversationItem.d.ts +48 -0
  28. package/dist/runtime/composables/useConversationItem.js +176 -0
  29. package/dist/runtime/composables/useConversationList.d.ts +128 -0
  30. package/dist/runtime/composables/useConversationList.js +553 -0
  31. package/dist/runtime/composables/useConversations.d.ts +2 -0
  32. package/dist/runtime/composables/useConversations.js +246 -107
  33. package/dist/runtime/composables/usePendingConversationGroup.d.ts +5 -0
  34. package/dist/runtime/composables/usePendingConversationGroup.js +33 -0
  35. package/dist/runtime/embed/components/ChatConfigurationModal.d.vue.ts +1 -1
  36. package/dist/runtime/embed/components/ChatConfigurationModal.vue.d.ts +1 -1
  37. package/dist/runtime/embed/components/ChatEmbedInner.vue +24 -5
  38. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.d.vue.ts +4 -4
  39. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.vue.d.ts +4 -4
  40. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +2 -2
  41. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +2 -2
  42. package/dist/runtime/server/api/chat/conversations/[id]/group.patch.d.ts +1 -0
  43. package/dist/runtime/server/api/chat/conversations/[id]/group.patch.js +1 -0
  44. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.delete.d.ts +1 -0
  45. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.delete.js +1 -0
  46. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.patch.d.ts +1 -0
  47. package/dist/runtime/server/api/chat/conversations/groups/[groupId]/index.patch.js +1 -0
  48. package/dist/runtime/server/api/chat/conversations/groups/index.get.d.ts +1 -0
  49. package/dist/runtime/server/api/chat/conversations/groups/index.get.js +1 -0
  50. package/dist/runtime/server/api/chat/conversations/groups/index.post.d.ts +1 -0
  51. package/dist/runtime/server/api/chat/conversations/groups/index.post.js +1 -0
  52. package/dist/runtime/server/api/chat/conversations/metadata.get.d.ts +1 -0
  53. package/dist/runtime/server/api/chat/conversations/metadata.get.js +1 -0
  54. package/dist/runtime/server/api/conversations/[id]/duplicate.post.js +1 -0
  55. package/dist/runtime/server/api/conversations/[id]/group.patch.d.ts +2 -0
  56. package/dist/runtime/server/api/conversations/[id]/group.patch.js +46 -0
  57. package/dist/runtime/server/api/conversations/groups/[groupId]/index.delete.d.ts +4 -0
  58. package/dist/runtime/server/api/conversations/groups/[groupId]/index.delete.js +25 -0
  59. package/dist/runtime/server/api/conversations/groups/[groupId]/index.patch.d.ts +2 -0
  60. package/dist/runtime/server/api/conversations/groups/[groupId]/index.patch.js +30 -0
  61. package/dist/runtime/server/api/conversations/groups/index.get.d.ts +2 -0
  62. package/dist/runtime/server/api/conversations/groups/index.get.js +13 -0
  63. package/dist/runtime/server/api/conversations/groups/index.post.d.ts +2 -0
  64. package/dist/runtime/server/api/conversations/groups/index.post.js +26 -0
  65. package/dist/runtime/server/api/conversations/index.post.js +1 -0
  66. package/dist/runtime/server/api/conversations/metadata.get.d.ts +2 -0
  67. package/dist/runtime/server/api/conversations/metadata.get.js +20 -0
  68. package/dist/runtime/server/db/schema/conversation-groups.d.ts +143 -0
  69. package/dist/runtime/server/db/schema/conversation-groups.js +20 -0
  70. package/dist/runtime/server/db/schema/conversations.d.ts +17 -0
  71. package/dist/runtime/server/db/schema/conversations.js +3 -0
  72. package/dist/runtime/server/db/schema/index.d.ts +1 -0
  73. package/dist/runtime/server/db/schema/index.js +1 -0
  74. package/dist/runtime/server/utils/conversation-group.d.ts +4 -0
  75. package/dist/runtime/server/utils/conversation-group.js +48 -0
  76. package/dist/runtime/types/chat.d.ts +6 -1
  77. package/dist/runtime/utils/conversation-groups.d.ts +18 -0
  78. package/dist/runtime/utils/conversation-groups.js +30 -0
  79. package/drizzle/0016_dry_aaron_stack.sql +16 -0
  80. package/drizzle/meta/0016_snapshot.json +887 -0
  81. package/drizzle/meta/_journal.json +7 -0
  82. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import * as Sentry from "@sentry/nuxt";
2
2
  import { useChatAuth } from "#chat-auth";
3
+ import { DEFAULT_MODEL } from "../types/chat.js";
3
4
  import { resolveChatStateScope } from "../utils/tela-chat.js";
5
+ const CONVERSATION_CREATOR_FILTERS = ["all", "mine"];
4
6
  function sortConversationsByUpdatedAt(conversations) {
5
7
  return [...conversations].sort(
6
8
  (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
@@ -33,6 +35,23 @@ function setBucketValue(bucket, key, value) {
33
35
  [key]: value
34
36
  };
35
37
  }
38
+ function removeBucketValue(bucket, key) {
39
+ const nextValue = { ...bucket.value };
40
+ delete nextValue[key];
41
+ bucket.value = nextValue;
42
+ }
43
+ function hasBucketValue(bucket, key) {
44
+ return Object.prototype.hasOwnProperty.call(bucket.value, key);
45
+ }
46
+ function conversationBucketKeyForRuntime(runtimeScope, filter) {
47
+ return `${runtimeScope}:creator:${filter}`;
48
+ }
49
+ function createOptimisticConversationId() {
50
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
51
+ return `optimistic-${crypto.randomUUID()}`;
52
+ }
53
+ return `optimistic-${Date.now()}-${Math.random().toString(36).slice(2)}`;
54
+ }
36
55
  export function useConversations() {
37
56
  const chatApi = useChatApi();
38
57
  const embedConfig = useEmbedConfig();
@@ -44,6 +63,9 @@ export function useConversations() {
44
63
  const mutationVersionBuckets = useState("conversations-mutation-version", () => ({}));
45
64
  const deletedIdBuckets = useState("conversations-deleted-ids", () => ({}));
46
65
  const mutatedIdBuckets = useState("conversations-mutated-ids", () => ({}));
66
+ const metadataBuckets = useState("conversations-metadata", () => ({}));
67
+ const metadataLoadingBuckets = useState("conversations-metadata-loading", () => ({}));
68
+ const metadataFetchedBuckets = useState("conversations-metadata-fetched", () => ({}));
47
69
  const conversationCreatorFilter = useState(
48
70
  "conversation-creator-filter",
49
71
  () => embedConfig?.defaultConversationCreatorFilter?.value ?? "all"
@@ -54,17 +76,10 @@ export function useConversations() {
54
76
  embedConfig?.telaAgentId.value,
55
77
  embedConfig?.conversationScope?.value
56
78
  ));
57
- const workspaceScope = computed(() => `${runtimeScope.value}:creator:${conversationCreatorFilter.value}`);
58
- const conversationBucketKey = (filter) => `${runtimeScope.value}:creator:${filter}`;
59
- const conversationCreatorFilterCounts = computed(() => {
60
- const allConversations = conversationBuckets.value[conversationBucketKey("all")] ?? [];
61
- const mineConversations = conversationBuckets.value[conversationBucketKey("mine")];
62
- const authUserId = authUser.value?.id;
63
- return {
64
- all: allConversations.length,
65
- mine: mineConversations?.length ?? (authUserId ? allConversations.filter((conversation) => conversation.userId === authUserId).length : 0)
66
- };
67
- });
79
+ const workspaceScope = computed(() => conversationBucketKeyForRuntime(runtimeScope.value, conversationCreatorFilter.value));
80
+ const conversationCreatorFilterCounts = computed(
81
+ () => metadataBuckets.value[runtimeScope.value]?.counts ?? { all: 0, mine: 0 }
82
+ );
68
83
  const conversations = computed({
69
84
  get: () => conversationBuckets.value[workspaceScope.value] ?? [],
70
85
  set: (value) => {
@@ -74,33 +89,6 @@ export function useConversations() {
74
89
  };
75
90
  }
76
91
  });
77
- const mutationVersion = computed({
78
- get: () => mutationVersionBuckets.value[workspaceScope.value] ?? 0,
79
- set: (value) => {
80
- mutationVersionBuckets.value = {
81
- ...mutationVersionBuckets.value,
82
- [workspaceScope.value]: value
83
- };
84
- }
85
- });
86
- const deletedConversationIds = computed({
87
- get: () => deletedIdBuckets.value[workspaceScope.value] ?? [],
88
- set: (value) => {
89
- deletedIdBuckets.value = {
90
- ...deletedIdBuckets.value,
91
- [workspaceScope.value]: value
92
- };
93
- }
94
- });
95
- const mutatedConversationIds = computed({
96
- get: () => mutatedIdBuckets.value[workspaceScope.value] ?? [],
97
- set: (value) => {
98
- mutatedIdBuckets.value = {
99
- ...mutatedIdBuckets.value,
100
- [workspaceScope.value]: value
101
- };
102
- }
103
- });
104
92
  const loading = computed({
105
93
  get: () => loadingBuckets.value[workspaceScope.value] ?? false,
106
94
  set: (value) => {
@@ -119,60 +107,152 @@ export function useConversations() {
119
107
  };
120
108
  }
121
109
  });
122
- function markMutated(id) {
123
- mutationVersion.value = mutationVersion.value + 1;
110
+ function markScopeMutated(scope, id) {
111
+ setBucketValue(mutationVersionBuckets, scope, bucketValue(mutationVersionBuckets, scope, 0) + 1);
124
112
  if (id) {
125
- mutatedConversationIds.value = [.../* @__PURE__ */ new Set([...mutatedConversationIds.value, id])];
113
+ setBucketValue(mutatedIdBuckets, scope, [
114
+ .../* @__PURE__ */ new Set([...bucketValue(mutatedIdBuckets, scope, []), id])
115
+ ]);
126
116
  }
127
117
  }
128
- function commitConversations(nextConversations, mutatedId) {
129
- conversations.value = sortConversationsByUpdatedAt(nextConversations);
130
- markMutated(mutatedId);
131
- }
132
118
  function commitConversationsToScope(scope, nextConversations, mutatedId) {
133
119
  setBucketValue(conversationBuckets, scope, sortConversationsByUpdatedAt(nextConversations));
134
- setBucketValue(mutationVersionBuckets, scope, bucketValue(mutationVersionBuckets, scope, 0) + 1);
135
- if (mutatedId) {
136
- setBucketValue(mutatedIdBuckets, scope, [
137
- .../* @__PURE__ */ new Set([...bucketValue(mutatedIdBuckets, scope, []), mutatedId])
138
- ]);
120
+ markScopeMutated(scope, mutatedId);
121
+ }
122
+ function activeAndLoadedRuntimeConversationScopes(scope) {
123
+ const activeScope = runtimeScope.value === scope ? [workspaceScope.value] : [];
124
+ const loadedScopes = CONVERSATION_CREATOR_FILTERS.map((filter) => conversationBucketKeyForRuntime(scope, filter)).filter((key) => hasBucketValue(conversationBuckets, key));
125
+ return [.../* @__PURE__ */ new Set([...activeScope, ...loadedScopes])];
126
+ }
127
+ function findConversationInLoadedBuckets(id, runtimeScopeKey) {
128
+ for (const scope of activeAndLoadedRuntimeConversationScopes(runtimeScopeKey)) {
129
+ const found = bucketValue(conversationBuckets, scope, []).find((conversation) => conversation.id === id);
130
+ if (found) {
131
+ return found;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ function snapshotConversationBuckets(scopes) {
137
+ return Object.fromEntries(scopes.map((scope) => [
138
+ scope,
139
+ hasBucketValue(conversationBuckets, scope) ? [...bucketValue(conversationBuckets, scope, [])] : void 0
140
+ ]));
141
+ }
142
+ function restoreConversationBuckets(snapshot, mutatedId) {
143
+ for (const [scope, previousConversations] of Object.entries(snapshot)) {
144
+ if (previousConversations) {
145
+ commitConversationsToScope(scope, previousConversations, mutatedId);
146
+ } else {
147
+ removeBucketValue(conversationBuckets, scope);
148
+ markScopeMutated(scope, mutatedId);
149
+ }
139
150
  }
140
151
  }
141
- function upsertCreatedConversation(conversation) {
142
- const allScope = conversationBucketKey("all");
143
- const allConversations = bucketValue(conversationBuckets, allScope, []);
144
- commitConversationsToScope(
145
- allScope,
146
- [conversation, ...allConversations.filter((item) => item.id !== conversation.id)],
147
- conversation.id
148
- );
149
- if (authUser.value?.id === conversation.userId || conversationCreatorFilter.value === "mine") {
150
- const mineScope = conversationBucketKey("mine");
151
- const mineConversations = bucketValue(conversationBuckets, mineScope, []);
152
+ function upsertConversationInLoadedBuckets(conversation, runtimeScopeKey, replacedId) {
153
+ for (const scope of activeAndLoadedRuntimeConversationScopes(runtimeScopeKey)) {
154
+ const filter = CONVERSATION_CREATOR_FILTERS.find((item) => scope === conversationBucketKeyForRuntime(runtimeScopeKey, item));
155
+ if (!filter || filter === "mine" && conversation.userId !== authUser.value?.id)
156
+ continue;
157
+ const currentConversations = bucketValue(conversationBuckets, scope, []);
152
158
  commitConversationsToScope(
153
- mineScope,
154
- [conversation, ...mineConversations.filter((item) => item.id !== conversation.id)],
159
+ scope,
160
+ [
161
+ conversation,
162
+ ...currentConversations.filter((item) => item.id !== conversation.id && item.id !== replacedId)
163
+ ],
155
164
  conversation.id
156
165
  );
157
166
  }
158
167
  }
159
- function addDeletedConversationId(id) {
160
- deletedConversationIds.value = [.../* @__PURE__ */ new Set([...deletedConversationIds.value, id])];
161
- markMutated();
168
+ function updateConversationInLoadedBuckets(updated, runtimeScopeKey = runtimeScope.value) {
169
+ for (const scope of activeAndLoadedRuntimeConversationScopes(runtimeScopeKey)) {
170
+ const currentConversations = bucketValue(conversationBuckets, scope, []);
171
+ const index = currentConversations.findIndex((conversation) => conversation.id === updated.id);
172
+ if (index === -1)
173
+ continue;
174
+ const nextConversations = [...currentConversations];
175
+ nextConversations[index] = updated;
176
+ commitConversationsToScope(scope, nextConversations, updated.id);
177
+ }
162
178
  }
163
- function removeDeletedConversationId(id) {
164
- deletedConversationIds.value = deletedConversationIds.value.filter((deletedId) => deletedId !== id);
165
- markMutated();
179
+ function removeConversationFromLoadedBuckets(id, runtimeScopeKey) {
180
+ for (const scope of activeAndLoadedRuntimeConversationScopes(runtimeScopeKey)) {
181
+ const currentConversations = bucketValue(conversationBuckets, scope, []);
182
+ if (currentConversations.some((conversation) => conversation.id === id)) {
183
+ commitConversationsToScope(
184
+ scope,
185
+ currentConversations.filter((conversation) => conversation.id !== id),
186
+ id
187
+ );
188
+ }
189
+ }
166
190
  }
167
- function cloneAppliedSettingsSnapshot(snapshot) {
168
- if (!snapshot)
169
- return null;
170
- return JSON.parse(JSON.stringify(snapshot));
191
+ function addDeletedConversationIdToScopes(id, scopes) {
192
+ for (const scope of scopes) {
193
+ setBucketValue(deletedIdBuckets, scope, [
194
+ .../* @__PURE__ */ new Set([...bucketValue(deletedIdBuckets, scope, []), id])
195
+ ]);
196
+ markScopeMutated(scope);
197
+ }
198
+ }
199
+ function removeDeletedConversationIdFromScopes(id, scopes) {
200
+ for (const scope of scopes) {
201
+ setBucketValue(
202
+ deletedIdBuckets,
203
+ scope,
204
+ bucketValue(deletedIdBuckets, scope, []).filter((deletedId) => deletedId !== id)
205
+ );
206
+ markScopeMutated(scope);
207
+ }
208
+ }
209
+ function snapshotConversationMetadata(scope) {
210
+ const metadata = metadataBuckets.value[scope];
211
+ return metadata ? { counts: { ...metadata.counts } } : void 0;
171
212
  }
213
+ function restoreConversationMetadata(scope, metadata) {
214
+ if (metadata) {
215
+ setBucketValue(metadataBuckets, scope, metadata);
216
+ } else {
217
+ removeBucketValue(metadataBuckets, scope);
218
+ }
219
+ }
220
+ function updateConversationCounts(scope, conversation, delta) {
221
+ const counts = metadataBuckets.value[scope]?.counts ?? { all: 0, mine: 0 };
222
+ setBucketValue(metadataBuckets, scope, {
223
+ counts: {
224
+ all: Math.max(0, counts.all + delta),
225
+ mine: Math.max(0, counts.mine + (conversation.userId === authUser.value?.id ? delta : 0))
226
+ }
227
+ });
228
+ }
229
+ const fetchConversationMetadata = async (options = {}) => {
230
+ const requestScope = runtimeScope.value;
231
+ if (bucketValue(metadataLoadingBuckets, requestScope, false) || bucketValue(metadataFetchedBuckets, requestScope, false) && !options.force) {
232
+ return;
233
+ }
234
+ setBucketValue(metadataLoadingBuckets, requestScope, true);
235
+ try {
236
+ const metadata = await $fetch(
237
+ chatApi.path("/conversations/metadata"),
238
+ chatApi.withChatHeaders()
239
+ );
240
+ setBucketValue(metadataBuckets, requestScope, metadata);
241
+ setBucketValue(metadataFetchedBuckets, requestScope, true);
242
+ } catch (err) {
243
+ const message = err instanceof Error ? err.message : "Erro ao carregar metadados das conversas";
244
+ setBucketValue(errorBuckets, workspaceScope.value, message);
245
+ Sentry.captureException(err);
246
+ } finally {
247
+ setBucketValue(metadataLoadingBuckets, requestScope, false);
248
+ }
249
+ };
172
250
  const fetchConversations = async (options = {}) => {
173
251
  const requestScope = workspaceScope.value;
174
252
  const requestCreatorFilter = conversationCreatorFilter.value;
253
+ const metadataPromise = fetchConversationMetadata({ force: options.force });
175
254
  if (bucketValue(loadingBuckets, requestScope, false) || bucketValue(fetchedBuckets, requestScope, false) && !options.force) {
255
+ await metadataPromise;
176
256
  return;
177
257
  }
178
258
  setBucketValue(loadingBuckets, requestScope, true);
@@ -186,6 +266,7 @@ export function useConversations() {
186
266
  const currentDeletedIds = bucketValue(deletedIdBuckets, requestScope, []);
187
267
  const currentMutatedIds = bucketValue(mutatedIdBuckets, requestScope, []);
188
268
  const visibleData = data.filter((conversation) => !currentDeletedIds.includes(conversation.id));
269
+ await metadataPromise;
189
270
  if (bucketValue(mutationVersionBuckets, requestScope, 0) === mutationVersionAtStart) {
190
271
  setBucketValue(conversationBuckets, requestScope, sortConversationsByUpdatedAt(visibleData));
191
272
  setBucketValue(mutatedIdBuckets, requestScope, []);
@@ -211,6 +292,27 @@ export function useConversations() {
211
292
  };
212
293
  const createConversation = async (options) => {
213
294
  error.value = null;
295
+ const requestRuntimeScope = runtimeScope.value;
296
+ const metadataSnapshot = snapshotConversationMetadata(requestRuntimeScope);
297
+ const now = /* @__PURE__ */ new Date();
298
+ const optimisticConversation = {
299
+ id: createOptimisticConversationId(),
300
+ workspaceId: embedConfig?.workspaceId.value || activeOrganization.value?.id || "",
301
+ groupId: null,
302
+ userId: authUser.value?.id ?? "",
303
+ title: "Nova conversa",
304
+ threadSessionId: null,
305
+ model: isTelaAgentMode.value ? null : options?.model ?? DEFAULT_MODEL,
306
+ appliedSettingsAt: null,
307
+ appliedSettingsSnapshot: null,
308
+ telaAgentId: embedConfig?.telaAgentId.value ?? null,
309
+ createdBy: authUser.value?.email ?? null,
310
+ conversationScope: embedConfig?.conversationScope?.value ?? null,
311
+ createdAt: now,
312
+ updatedAt: now
313
+ };
314
+ upsertConversationInLoadedBuckets(optimisticConversation, requestRuntimeScope);
315
+ updateConversationCounts(requestRuntimeScope, optimisticConversation, 1);
214
316
  try {
215
317
  const conversation = await $fetch(
216
318
  chatApi.path("/conversations"),
@@ -219,9 +321,11 @@ export function useConversations() {
219
321
  body: !isTelaAgentMode.value && options?.model ? { model: options.model } : void 0
220
322
  })
221
323
  );
222
- upsertCreatedConversation(conversation);
324
+ upsertConversationInLoadedBuckets(conversation, requestRuntimeScope, optimisticConversation.id);
223
325
  return conversation;
224
326
  } catch (err) {
327
+ removeConversationFromLoadedBuckets(optimisticConversation.id, requestRuntimeScope);
328
+ restoreConversationMetadata(requestRuntimeScope, metadataSnapshot);
225
329
  const message = err instanceof Error ? err.message : "Erro ao criar conversa";
226
330
  error.value = message;
227
331
  return null;
@@ -230,9 +334,16 @@ export function useConversations() {
230
334
  const deleteConversation = async (id) => {
231
335
  error.value = null;
232
336
  const statusToast = useStatusToast();
233
- const deletedConversation = conversations.value.find((c) => c.id === id);
234
- addDeletedConversationId(id);
235
- commitConversations(conversations.value.filter((c) => c.id !== id), id);
337
+ const requestRuntimeScope = runtimeScope.value;
338
+ const affectedScopes = activeAndLoadedRuntimeConversationScopes(requestRuntimeScope);
339
+ const conversationSnapshot = snapshotConversationBuckets(affectedScopes);
340
+ const metadataSnapshot = snapshotConversationMetadata(requestRuntimeScope);
341
+ const deletedConversation = findConversationInLoadedBuckets(id, requestRuntimeScope);
342
+ addDeletedConversationIdToScopes(id, affectedScopes);
343
+ removeConversationFromLoadedBuckets(id, requestRuntimeScope);
344
+ if (deletedConversation) {
345
+ updateConversationCounts(requestRuntimeScope, deletedConversation, -1);
346
+ }
236
347
  try {
237
348
  await $fetch(
238
349
  chatApi.path(`/conversations/${id}`),
@@ -246,10 +357,9 @@ export function useConversations() {
246
357
  });
247
358
  return true;
248
359
  } catch (err) {
249
- removeDeletedConversationId(id);
250
- if (deletedConversation) {
251
- commitConversations([...conversations.value, deletedConversation], deletedConversation.id);
252
- }
360
+ removeDeletedConversationIdFromScopes(id, affectedScopes);
361
+ restoreConversationBuckets(conversationSnapshot, id);
362
+ restoreConversationMetadata(requestRuntimeScope, metadataSnapshot);
253
363
  const message = err instanceof Error ? err.message : "Erro ao deletar conversa";
254
364
  error.value = message;
255
365
  statusToast.update({
@@ -260,45 +370,70 @@ export function useConversations() {
260
370
  }
261
371
  };
262
372
  const updateConversationInList = (updated) => {
263
- const index = conversations.value.findIndex((c) => c.id === updated.id);
264
- if (index !== -1) {
265
- const nextConversations = [...conversations.value];
266
- nextConversations[index] = {
267
- id: updated.id,
268
- workspaceId: updated.workspaceId,
269
- userId: updated.userId,
270
- title: updated.title,
271
- threadSessionId: updated.threadSessionId,
272
- model: updated.model,
273
- appliedSettingsAt: updated.appliedSettingsAt,
274
- appliedSettingsSnapshot: cloneAppliedSettingsSnapshot(updated.appliedSettingsSnapshot),
275
- telaAgentId: updated.telaAgentId,
276
- createdBy: updated.createdBy,
277
- conversationScope: updated.conversationScope,
278
- createdAt: updated.createdAt,
279
- updatedAt: updated.updatedAt
280
- };
281
- commitConversations(nextConversations, updated.id);
373
+ updateConversationInLoadedBuckets(updated);
374
+ };
375
+ const moveConversationToGroup = async (id, groupId) => {
376
+ error.value = null;
377
+ const requestRuntimeScope = runtimeScope.value;
378
+ const affectedScopes = activeAndLoadedRuntimeConversationScopes(requestRuntimeScope);
379
+ const conversationSnapshot = snapshotConversationBuckets(affectedScopes);
380
+ const conversation = findConversationInLoadedBuckets(id, requestRuntimeScope);
381
+ if (!conversation)
382
+ return false;
383
+ updateConversationInLoadedBuckets({ ...conversation, groupId }, requestRuntimeScope);
384
+ try {
385
+ const updated = await $fetch(
386
+ chatApi.path(`/conversations/${id}/group`),
387
+ chatApi.withChatHeaders({
388
+ method: "PATCH",
389
+ body: { groupId }
390
+ })
391
+ );
392
+ updateConversationInLoadedBuckets(updated, requestRuntimeScope);
393
+ return true;
394
+ } catch (err) {
395
+ restoreConversationBuckets(conversationSnapshot, id);
396
+ const message = err instanceof Error ? err.message : "Erro ao mover conversa";
397
+ error.value = message;
398
+ Sentry.captureException(err);
399
+ return false;
282
400
  }
283
401
  };
402
+ const clearGroupFromConversations = (groupId) => {
403
+ const affectedScopes = activeAndLoadedRuntimeConversationScopes(runtimeScope.value).filter((scope) => bucketValue(conversationBuckets, scope, []).some((conversation) => conversation.groupId === groupId));
404
+ const conversationSnapshot = snapshotConversationBuckets(affectedScopes);
405
+ for (const scope of affectedScopes) {
406
+ const currentConversations = bucketValue(conversationBuckets, scope, []);
407
+ commitConversationsToScope(
408
+ scope,
409
+ currentConversations.map(
410
+ (conversation) => conversation.groupId === groupId ? { ...conversation, groupId: null } : conversation
411
+ )
412
+ );
413
+ }
414
+ return () => restoreConversationBuckets(conversationSnapshot);
415
+ };
284
416
  const renameConversation = async (id, title) => {
285
417
  error.value = null;
286
- const conversation = conversations.value.find((c) => c.id === id);
418
+ const requestRuntimeScope = runtimeScope.value;
419
+ const affectedScopes = activeAndLoadedRuntimeConversationScopes(requestRuntimeScope);
420
+ const conversationSnapshot = snapshotConversationBuckets(affectedScopes);
421
+ const conversation = findConversationInLoadedBuckets(id, requestRuntimeScope);
287
422
  if (!conversation)
288
423
  return false;
289
- const previousTitle = conversation.title;
290
- updateConversationInList({ ...conversation, title, updatedAt: /* @__PURE__ */ new Date() });
424
+ updateConversationInLoadedBuckets({ ...conversation, title, updatedAt: /* @__PURE__ */ new Date() }, requestRuntimeScope);
291
425
  try {
292
- await $fetch(
426
+ const updated = await $fetch(
293
427
  chatApi.path(`/conversations/${id}`),
294
428
  chatApi.withChatHeaders({
295
429
  method: "PATCH",
296
430
  body: { title }
297
431
  })
298
432
  );
433
+ updateConversationInLoadedBuckets(updated, requestRuntimeScope);
299
434
  return true;
300
435
  } catch (err) {
301
- updateConversationInList({ ...conversation, title: previousTitle });
436
+ restoreConversationBuckets(conversationSnapshot, id);
302
437
  const message = err instanceof Error ? err.message : "Erro ao renomear conversa";
303
438
  error.value = message;
304
439
  Sentry.captureException(err);
@@ -308,6 +443,7 @@ export function useConversations() {
308
443
  const duplicateConversation = async (id) => {
309
444
  error.value = null;
310
445
  const statusToast = useStatusToast();
446
+ const requestRuntimeScope = runtimeScope.value;
311
447
  try {
312
448
  const duplicated = await $fetch(
313
449
  chatApi.path(`/conversations/${id}/duplicate`),
@@ -315,7 +451,8 @@ export function useConversations() {
315
451
  method: "POST"
316
452
  })
317
453
  );
318
- upsertCreatedConversation(duplicated);
454
+ upsertConversationInLoadedBuckets(duplicated, requestRuntimeScope);
455
+ updateConversationCounts(requestRuntimeScope, duplicated, 1);
319
456
  statusToast.update({
320
457
  text: "Conversa duplicada",
321
458
  icon: "i-ph-check"
@@ -339,9 +476,11 @@ export function useConversations() {
339
476
  createConversation,
340
477
  deleteConversation,
341
478
  renameConversation,
479
+ moveConversationToGroup,
342
480
  duplicateConversation,
343
481
  conversationCreatorFilter,
344
482
  conversationCreatorFilterCounts,
345
- updateConversationInList
483
+ updateConversationInList,
484
+ clearGroupFromConversations
346
485
  };
347
486
  }
@@ -0,0 +1,5 @@
1
+ export declare function usePendingConversationGroup(): {
2
+ pendingConversationGroupId: any;
3
+ setPendingConversationGroup: (groupId?: string | null) => void;
4
+ clearPendingConversationGroup: () => void;
5
+ };
@@ -0,0 +1,33 @@
1
+ import { useChatAuth } from "#chat-auth";
2
+ import { resolveChatStateScope } from "../utils/tela-chat.js";
3
+ const PENDING_CONVERSATION_GROUP_STATE_KEY = "chat-pending-conversation-group";
4
+ export function usePendingConversationGroup() {
5
+ const embedConfig = useEmbedConfig();
6
+ const { activeOrganization } = useChatAuth();
7
+ const pendingConversationGroups = useState(
8
+ PENDING_CONVERSATION_GROUP_STATE_KEY,
9
+ () => ({})
10
+ );
11
+ const runtimeScope = computed(() => resolveChatStateScope(
12
+ embedConfig?.workspaceId.value || activeOrganization.value?.id,
13
+ embedConfig?.telaAgentId.value,
14
+ embedConfig?.conversationScope?.value
15
+ ));
16
+ const pendingConversationGroupId = computed(
17
+ () => pendingConversationGroups.value[runtimeScope.value] ?? null
18
+ );
19
+ function setPendingConversationGroup(groupId) {
20
+ pendingConversationGroups.value = {
21
+ ...pendingConversationGroups.value,
22
+ [runtimeScope.value]: groupId ?? null
23
+ };
24
+ }
25
+ function clearPendingConversationGroup() {
26
+ setPendingConversationGroup(null);
27
+ }
28
+ return {
29
+ pendingConversationGroupId,
30
+ setPendingConversationGroup,
31
+ clearPendingConversationGroup
32
+ };
33
+ }
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
18
  onSave?: ((payload: ChatConfigurationSavePayload) => any) | undefined;
19
19
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
20
20
  }>, {
21
- initialTab: ChatConfigurationTab;
22
21
  saving: boolean;
22
+ initialTab: ChatConfigurationTab;
23
23
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
24
  declare const _default: typeof __VLS_export;
25
25
  export default _default;
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
18
  onSave?: ((payload: ChatConfigurationSavePayload) => any) | undefined;
19
19
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
20
20
  }>, {
21
- initialTab: ChatConfigurationTab;
22
21
  saving: boolean;
22
+ initialTab: ChatConfigurationTab;
23
23
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
24
  declare const _default: typeof __VLS_export;
25
25
  export default _default;
@@ -25,7 +25,7 @@ const chatAction = useChatAction();
25
25
  const { user: authUser, activeOrganization, logout } = useChatAuth();
26
26
  const { getMember, getMemberImage } = useWorkspaceMembers();
27
27
  const statusToast = useStatusToast();
28
- const { conversations, loading: conversationsLoading, fetchConversations, createConversation, deleteConversation, renameConversation, duplicateConversation, updateConversationInList } = useConversations();
28
+ const { conversations, loading: conversationsLoading, fetchConversations, createConversation, deleteConversation, renameConversation, duplicateConversation, moveConversationToGroup, updateConversationInList } = useConversations();
29
29
  const { settings: workspaceSettingsState, fetchSettings } = useWorkspaceSettings();
30
30
  const { metadata: telaAgentMetadata, loading: telaAgentMetadataLoading, fetchAgentMetadata } = useTelaAgentMetadata();
31
31
  const isTelaAgentMode = computed(() => !!embedConfig?.telaAgentId.value);
@@ -46,6 +46,11 @@ const activeConversationId = ref(
46
46
  );
47
47
  const loadingConversationId = ref(null);
48
48
  const isReady = ref(false);
49
+ const {
50
+ pendingConversationGroupId,
51
+ setPendingConversationGroup,
52
+ clearPendingConversationGroup
53
+ } = usePendingConversationGroup();
49
54
  const resolvedFeatures = computed(() => resolveChatFeatures(props.features));
50
55
  const dropZoneRef = ref(null);
51
56
  const activeTab = ref("chat");
@@ -154,6 +159,7 @@ async function openConversation(nextConversationId) {
154
159
  if (!normalizedConversationId) {
155
160
  return;
156
161
  }
162
+ clearPendingConversationGroup();
157
163
  if (activeConversationId.value !== normalizedConversationId) {
158
164
  closePreview();
159
165
  }
@@ -165,6 +171,9 @@ watch(() => props.conversationId, async (nextConversationId) => {
165
171
  return;
166
172
  }
167
173
  const normalizedConversationId = normalizeConversationId(nextConversationId);
174
+ if (normalizedConversationId) {
175
+ clearPendingConversationGroup();
176
+ }
168
177
  if (!isReady.value) {
169
178
  syncConversationId(normalizedConversationId);
170
179
  return;
@@ -272,7 +281,14 @@ async function handleSend(content, files, model, agentInputs) {
272
281
  const conversation = await createConversation(isTelaAgentMode.value ? void 0 : { model: model ?? selectedModel.value });
273
282
  if (!conversation)
274
283
  return;
275
- setConversation(conversation);
284
+ const groupId = pendingConversationGroupId.value;
285
+ clearPendingConversationGroup();
286
+ if (groupId && await moveConversationToGroup(conversation.id, groupId)) {
287
+ const groupedConversation = { ...conversation, groupId };
288
+ setConversation(groupedConversation);
289
+ } else {
290
+ setConversation(conversation);
291
+ }
276
292
  createdConversationId = conversation.id;
277
293
  }
278
294
  if (!currentConversation.value)
@@ -286,7 +302,8 @@ async function handleSend(content, files, model, agentInputs) {
286
302
  async function handleRetry(messageId) {
287
303
  await retryMessage(messageId);
288
304
  }
289
- async function handleNewConversation() {
305
+ async function handleNewConversation(groupId) {
306
+ setPendingConversationGroup(groupId);
290
307
  resetConversation();
291
308
  }
292
309
  const workspaceSheetOpen = ref(false);
@@ -407,10 +424,10 @@ async function handleDelete() {
407
424
  w-240px h-full flex="~ col" flex-shrink-0
408
425
  bg-neutral-50 b-r=".5px neutral-200"
409
426
  >
410
- <div px-12px pt-16px pb-8px flex="~ col" gap-8px>
427
+ <div px-12px pt-16px pb-8px flex gap-8px>
411
428
  <button
412
429
  flex items-center justify-center gap-6px
413
- h-32px px-12px w-full
430
+ h-32px px-12px flex-1 min-w-0
414
431
  bg-white b=".5px neutral-300" rounded-10px
415
432
  hover:bg-neutral-50 transition-colors cursor-pointer
416
433
  @click="handleNewConversation"
@@ -420,6 +437,7 @@ async function handleDelete() {
420
437
  Nova conversa
421
438
  </span>
422
439
  </button>
440
+ <ChatConversationNewGroupButton />
423
441
  </div>
424
442
 
425
443
  <div flex-1 overflow-y-auto px-4px>
@@ -429,6 +447,7 @@ async function handleDelete() {
429
447
  :get-member-image="getMemberImage"
430
448
  :select-conversation="openConversation"
431
449
  :reset-conversation="handleNewConversation"
450
+ :start-conversation="handleNewConversation"
432
451
  :loading="conversationsLoading"
433
452
  />
434
453
  </div>
@@ -8,15 +8,15 @@ type __VLS_Props = {
8
8
  getFileType: (fileName: string) => string;
9
9
  };
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
11
- "update:searchQuery": (value: string) => any;
12
11
  handleFileSelect: (event: Event) => any;
13
- removePendingFile: (file: File) => any;
14
12
  removeContextFile: (url: string) => any;
13
+ removePendingFile: (file: File) => any;
14
+ "update:searchQuery": (value: string) => any;
15
15
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
- "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
17
16
  onHandleFileSelect?: ((event: Event) => any) | undefined;
18
- onRemovePendingFile?: ((file: File) => any) | undefined;
19
17
  onRemoveContextFile?: ((url: string) => any) | undefined;
18
+ onRemovePendingFile?: ((file: File) => any) | undefined;
19
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
20
20
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
21
  declare const _default: typeof __VLS_export;
22
22
  export default _default;