@meistrari/chat-nuxt 1.8.1 → 1.10.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 (46) hide show
  1. package/README.md +1 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/MeistrariChatEmbed.vue +3 -0
  4. package/dist/runtime/components/chat/conversation-creator-filter.d.vue.ts +3 -0
  5. package/dist/runtime/components/chat/conversation-creator-filter.vue +95 -0
  6. package/dist/runtime/components/chat/conversation-creator-filter.vue.d.ts +3 -0
  7. package/dist/runtime/components/chat/conversation-item.d.vue.ts +1 -0
  8. package/dist/runtime/components/chat/conversation-item.vue +142 -63
  9. package/dist/runtime/components/chat/conversation-item.vue.d.ts +1 -0
  10. package/dist/runtime/components/chat/conversation-list.d.vue.ts +2 -0
  11. package/dist/runtime/components/chat/conversation-list.vue +35 -9
  12. package/dist/runtime/components/chat/conversation-list.vue.d.ts +2 -0
  13. package/dist/runtime/components/chat/conversation-search.d.vue.ts +16 -0
  14. package/dist/runtime/components/chat/conversation-search.vue +35 -0
  15. package/dist/runtime/components/chat/conversation-search.vue.d.ts +16 -0
  16. package/dist/runtime/components/chat/mobile/shell/drawer.vue +16 -5
  17. package/dist/runtime/components/chat/mobile/shell/header.d.vue.ts +4 -4
  18. package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +4 -4
  19. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +14 -14
  20. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +14 -14
  21. package/dist/runtime/components/chat/topbar.d.vue.ts +8 -8
  22. package/dist/runtime/components/chat/topbar.vue +29 -11
  23. package/dist/runtime/components/chat/topbar.vue.d.ts +8 -8
  24. package/dist/runtime/composables/useConversations.d.ts +2 -0
  25. package/dist/runtime/composables/useConversations.js +81 -36
  26. package/dist/runtime/composables/useEmbedConfig.d.ts +3 -0
  27. package/dist/runtime/composables/useEmbedConfig.js +4 -0
  28. package/dist/runtime/embed/components/ChatEmbed.vue +2 -0
  29. package/dist/runtime/embed/components/ChatEmbedInner.vue +11 -30
  30. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.d.vue.ts +4 -4
  31. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.vue.d.ts +4 -4
  32. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +2 -2
  33. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +2 -2
  34. package/dist/runtime/server/api/conversations/index.get.js +5 -4
  35. package/dist/runtime/server/utils/conversation-list.d.ts +4 -0
  36. package/dist/runtime/server/utils/conversation-list.js +28 -0
  37. package/dist/runtime/types/chat.d.ts +8 -0
  38. package/dist/runtime/types/chat.js +4 -0
  39. package/dist/runtime/types/embed.d.ts +3 -0
  40. package/dist/runtime/utils/conversation-creator-filter.d.ts +5 -0
  41. package/dist/runtime/utils/conversation-creator-filter.js +24 -0
  42. package/dist/runtime/utils/conversation-search.d.ts +8 -0
  43. package/dist/runtime/utils/conversation-search.js +11 -0
  44. package/dist/runtime/utils/conversation-title.d.ts +1 -0
  45. package/dist/runtime/utils/conversation-title.js +6 -0
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -220,6 +220,7 @@ Key props for embedding:
220
220
  | `conversationId` | `string \| null` | `null` | Controlled conversation (supports v-model) |
221
221
  | `initialConversationId` | `string \| null` | `null` | Open this conversation on mount |
222
222
  | `conversationScope` | `string \| null` | `null` | Isolate conversation history by technical scope within the current workspace and runtime |
223
+ | `defaultConversationCreatorFilter` | `'all' \| 'mine'` | `'all'` | Initial sidebar creator filter |
223
224
  | `telaAgentId` | `string` | — | Use Tela agent mode for this embed |
224
225
  | `telaAgentInputs` | `TelaAgentExecutionInput[] \| null` | `undefined` | Inputs sent with each Tela agent run; hides the variables panel when non-null |
225
226
  | `workspaceSettings` | `WorkspaceSettings \| null` | `null` | Override settings from host app |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "1.8.1",
4
+ "version": "1.10.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -3,6 +3,7 @@ const props = defineProps({
3
3
  conversationId: { type: [String, null], required: false },
4
4
  initialConversationId: { type: [String, null], required: false },
5
5
  conversationScope: { type: [String, null], required: false },
6
+ defaultConversationCreatorFilter: { type: String, required: false },
6
7
  hideSidebar: { type: Boolean, required: false },
7
8
  hideSettings: { type: Boolean, required: false },
8
9
  loadingMessages: { type: [Array, null], required: false },
@@ -27,6 +28,7 @@ const hideSettings = computed(() => props.hideSettings ?? false);
27
28
  :conversation-id="conversationId"
28
29
  :initial-conversation-id="initialConversationId"
29
30
  :conversation-scope="conversationScope"
31
+ :default-conversation-creator-filter="defaultConversationCreatorFilter"
30
32
  :hide-sidebar="hideSidebar"
31
33
  :hide-settings="hideSettings"
32
34
  :features="features"
@@ -44,6 +46,7 @@ const hideSettings = computed(() => props.hideSettings ?? false);
44
46
  :conversation-id="conversationId"
45
47
  :initial-conversation-id="initialConversationId"
46
48
  :conversation-scope="conversationScope"
49
+ :default-conversation-creator-filter="defaultConversationCreatorFilter"
47
50
  :hide-sidebar="hideSidebar"
48
51
  :hide-settings="hideSettings"
49
52
  :features="features"
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,95 @@
1
+ <script setup>
2
+ import { nextTick } from "vue";
3
+ import { CONVERSATION_CREATOR_FILTER_OPTIONS } from "../../types/chat";
4
+ import {
5
+ hasConversationCreatorFilterResults,
6
+ resolveConversationCreatorFilterKeyboardTarget
7
+ } from "../../utils/conversation-creator-filter";
8
+ const { conversationCreatorFilter, conversationCreatorFilterCounts, fetchConversations } = useConversations();
9
+ const isUpdating = ref(false);
10
+ const filterButtonElements = ref([]);
11
+ const filterOptions = computed(() => CONVERSATION_CREATOR_FILTER_OPTIONS.map((option) => ({
12
+ ...option,
13
+ count: conversationCreatorFilterCounts.value[option.value]
14
+ })));
15
+ const showConversationCreatorFilter = computed(
16
+ () => hasConversationCreatorFilterResults(conversationCreatorFilterCounts.value)
17
+ );
18
+ function isConversationCreatorFilter(value) {
19
+ return value === "all" || value === "mine";
20
+ }
21
+ function setFilterButtonElement(element, index) {
22
+ if (element && "focus" in element) {
23
+ filterButtonElements.value[index] = element;
24
+ }
25
+ }
26
+ function focusFilterButton(value) {
27
+ const index = filterOptions.value.findIndex((option) => option.value === value);
28
+ filterButtonElements.value[index]?.focus();
29
+ }
30
+ async function handleFilterUpdate(value, options = {}) {
31
+ if (isUpdating.value || !isConversationCreatorFilter(value) || value === conversationCreatorFilter.value)
32
+ return;
33
+ conversationCreatorFilter.value = value;
34
+ if (options.focus) {
35
+ await nextTick();
36
+ focusFilterButton(value);
37
+ }
38
+ isUpdating.value = true;
39
+ try {
40
+ await fetchConversations({ force: true });
41
+ } finally {
42
+ isUpdating.value = false;
43
+ }
44
+ }
45
+ function handleFilterKeydown(event, value) {
46
+ const nextFilter = resolveConversationCreatorFilterKeyboardTarget(value, event.key);
47
+ if (!nextFilter || isUpdating.value)
48
+ return;
49
+ event.preventDefault();
50
+ void handleFilterUpdate(nextFilter, { focus: true });
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <div
56
+ v-if="showConversationCreatorFilter"
57
+ role="tablist"
58
+ aria-label="Filtrar conversas por criador"
59
+ flex items-center w-full h-34px p-3px rounded-12px bg="#F1F1EF"
60
+ :class="{ 'opacity-70': isUpdating }"
61
+ >
62
+ <button
63
+ v-for="(option, index) in filterOptions"
64
+ :key="option.value"
65
+ :ref="(element) => setFilterButtonElement(element, index)"
66
+ type="button"
67
+ role="tab"
68
+ class="creator-filter-option"
69
+ flex-1 min-w-0 h-28px rounded-9px px-8px
70
+ flex items-center justify-center gap-4px
71
+ :class="conversationCreatorFilter === option.value ? 'creator-filter-option-active bg-white cursor-default' : 'bg-transparent hover:bg-white/45 cursor-pointer'"
72
+ :aria-disabled="isUpdating"
73
+ :aria-selected="conversationCreatorFilter === option.value"
74
+ :tabindex="conversationCreatorFilter === option.value ? 0 : -1"
75
+ @click="handleFilterUpdate(option.value)"
76
+ @keydown="handleFilterKeydown($event, option.value)"
77
+ >
78
+ <span
79
+ body-12-semibold leading-16px truncate
80
+ :class="conversationCreatorFilter === option.value ? 'text-neutral-950' : 'text-neutral-500'"
81
+ >
82
+ {{ option.label }}
83
+ </span>
84
+ <span
85
+ body-12-semibold leading-16px shrink-0 text-neutral-400
86
+ >
87
+ {{ option.count }}
88
+ </span>
89
+ </button>
90
+ </div>
91
+ </template>
92
+
93
+ <style scoped>
94
+ .creator-filter-option{transition:background-color .16s ease,color .16s ease,box-shadow .16s ease,opacity .16s ease}.creator-filter-option-active{box-shadow:0 1px 2px rgba(16,24,40,.08),0 2px 8px rgba(16,24,40,.1)}.creator-filter-option[aria-disabled=true]{cursor:wait}
95
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  conversation: DeepReadonly<Conversation>;
5
5
  active: boolean;
6
6
  userImage?: string;
7
+ selectConversation?: (id: string) => void | Promise<void>;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
10
  delete: (id: string) => any;
@@ -1,19 +1,24 @@
1
1
  <script setup>
2
2
  import { useMagicKeys, useMutationObserver } from "@vueuse/core";
3
+ import { resolveConversationRenameTitle } from "../../utils/conversation-title";
3
4
  import { capitalizeFirst } from "../../utils/string";
4
5
  const props = defineProps({
5
6
  conversation: { type: null, required: true },
6
7
  active: { type: Boolean, required: true },
7
- userImage: { type: String, required: false }
8
+ userImage: { type: String, required: false },
9
+ selectConversation: { type: Function, required: false }
8
10
  });
9
11
  const emit = defineEmits(["delete", "rename", "duplicate", "export"]);
10
- const isEditing = ref(false);
11
- const editTitle = ref("");
12
- const inputRef = ref(null);
13
- const justStartedEditing = ref(false);
12
+ const isRenamingInline = ref(false);
13
+ const renameTitle = ref("");
14
+ const renameInputRef = ref(null);
15
+ const isIgnoringInitialRenameBlur = ref(false);
14
16
  const showDeleteModal = ref(false);
15
17
  const { escape } = useMagicKeys();
16
18
  watch(() => escape?.value, (pressed) => {
19
+ if (pressed && isRenamingInline.value) {
20
+ cancelRename();
21
+ }
17
22
  if (pressed && showDeleteModal.value) {
18
23
  showDeleteModal.value = false;
19
24
  }
@@ -31,48 +36,91 @@ useMutationObserver(menuButtonRef, (mutations) => {
31
36
  }
32
37
  }
33
38
  }, { attributes: true });
34
- function startEditing() {
35
- editTitle.value = props.conversation.title;
36
- isEditing.value = true;
37
- justStartedEditing.value = true;
38
- nextTick(() => {
39
- inputRef.value?.focus();
40
- inputRef.value?.select();
41
- setTimeout(() => {
42
- justStartedEditing.value = false;
43
- }, 100);
44
- });
39
+ const conversationPath = computed(() => `/${props.conversation.id}`);
40
+ const conversationNavigationDelayMs = 250;
41
+ let conversationNavigationTimer = null;
42
+ let renameBlurTimer = null;
43
+ function clearPendingConversationNavigation() {
44
+ if (conversationNavigationTimer === null)
45
+ return;
46
+ clearTimeout(conversationNavigationTimer);
47
+ conversationNavigationTimer = null;
45
48
  }
46
- function saveTitle() {
47
- if (justStartedEditing.value) {
49
+ function clearRenameBlurTimer() {
50
+ if (renameBlurTimer === null)
48
51
  return;
49
- }
50
- const trimmed = editTitle.value.trim();
51
- if (trimmed && trimmed !== props.conversation.title) {
52
- emit("rename", props.conversation.id, trimmed);
53
- }
54
- isEditing.value = false;
55
- justStartedEditing.value = false;
52
+ clearTimeout(renameBlurTimer);
53
+ renameBlurTimer = null;
54
+ }
55
+ function allowRenameBlurSoon() {
56
+ clearRenameBlurTimer();
57
+ renameBlurTimer = setTimeout(() => {
58
+ isIgnoringInitialRenameBlur.value = false;
59
+ renameBlurTimer = null;
60
+ }, 100);
61
+ }
62
+ function handleConversationClick(event) {
63
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
64
+ return;
65
+ event.preventDefault();
66
+ clearPendingConversationNavigation();
67
+ if (event.detail > 1)
68
+ return;
69
+ conversationNavigationTimer = setTimeout(() => {
70
+ if (props.selectConversation) {
71
+ void props.selectConversation(props.conversation.id);
72
+ } else {
73
+ navigateTo(conversationPath.value);
74
+ }
75
+ conversationNavigationTimer = null;
76
+ }, conversationNavigationDelayMs);
77
+ }
78
+ async function focusRenameInput() {
79
+ await nextTick();
80
+ renameInputRef.value?.focus();
81
+ renameInputRef.value?.select();
82
+ allowRenameBlurSoon();
83
+ }
84
+ function startRename() {
85
+ isMenuOpen.value = false;
86
+ clearPendingConversationNavigation();
87
+ clearRenameBlurTimer();
88
+ renameTitle.value = props.conversation.title;
89
+ isIgnoringInitialRenameBlur.value = true;
90
+ isRenamingInline.value = true;
91
+ void focusRenameInput();
56
92
  }
57
- function cancelEditing() {
58
- isEditing.value = false;
59
- editTitle.value = "";
60
- justStartedEditing.value = false;
93
+ function handleConversationDoubleClick() {
94
+ clearPendingConversationNavigation();
95
+ startRename();
61
96
  }
62
- function handleKeydown(event) {
63
- if (event.key === "Enter") {
64
- event.preventDefault();
65
- saveTitle();
66
- } else if (event.key === "Escape") {
67
- event.preventDefault();
68
- cancelEditing();
97
+ function cancelRename() {
98
+ isRenamingInline.value = false;
99
+ isIgnoringInitialRenameBlur.value = false;
100
+ renameTitle.value = "";
101
+ clearRenameBlurTimer();
102
+ }
103
+ function submitRename() {
104
+ if (!isRenamingInline.value)
105
+ return;
106
+ const title = resolveConversationRenameTitle(renameTitle.value, props.conversation.title);
107
+ if (!title) {
108
+ cancelRename();
109
+ return;
69
110
  }
111
+ emit("rename", props.conversation.id, title);
112
+ cancelRename();
113
+ }
114
+ function handleRenameBlur() {
115
+ if (isIgnoringInitialRenameBlur.value)
116
+ return;
117
+ submitRename();
70
118
  }
71
119
  const menuItems = [
72
120
  {
73
121
  label: "Renomear",
74
122
  icon: "i-ph-pencil-simple",
75
- click: () => startEditing()
123
+ click: () => startRename()
76
124
  },
77
125
  {
78
126
  label: "Duplicar",
@@ -91,11 +139,56 @@ const menuItems = [
91
139
  click: () => showDeleteModal.value = true
92
140
  }
93
141
  ];
142
+ onBeforeUnmount(() => {
143
+ clearPendingConversationNavigation();
144
+ clearRenameBlurTimer();
145
+ });
94
146
  </script>
95
147
 
96
148
  <template>
97
- <NuxtLink
98
- :to="`/${conversation.id}`"
149
+ <div
150
+ v-if="isRenamingInline"
151
+ px-12px
152
+ h-32px
153
+ rounded-8px
154
+ :class="active ? 'bg-neutral-200' : ''"
155
+ >
156
+ <div flex items-center justify-between gap-12px h-full>
157
+ <div v-if="userImage" w-16px h-16px rounded-full overflow-hidden flex-shrink-0>
158
+ <TelaAvatar
159
+ :image="userImage"
160
+ alt="Usuário"
161
+ size="xs"
162
+ />
163
+ </div>
164
+
165
+ <div
166
+ flex-1 min-w-0 flex items-center
167
+ text-neutral-700
168
+ pb-1px
169
+ b-b=".5px solid #A3A3A3"
170
+ >
171
+ <input
172
+ ref="renameInputRef"
173
+ v-model="renameTitle"
174
+ type="text"
175
+ autocomplete="off"
176
+ aria-label="Nome da conversa"
177
+ w-full min-w-0
178
+ bg-transparent border-none outline-none
179
+ text-14px font-medium text-neutral-700 leading-18px tracking="-.15px"
180
+ p-0
181
+ @blur="handleRenameBlur"
182
+ @keydown.enter.prevent="submitRename"
183
+ @keydown.escape.prevent.stop="cancelRename"
184
+ >
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <a
190
+ v-else
191
+ :href="conversationPath"
99
192
  block
100
193
  px-12px
101
194
  h-32px
@@ -106,6 +199,8 @@ const menuItems = [
106
199
  :class="[
107
200
  active ? 'bg-neutral-200' : 'hover:bg-neutral-200/50'
108
201
  ]"
202
+ @click="handleConversationClick"
203
+ @dblclick.prevent.stop="handleConversationDoubleClick"
109
204
  >
110
205
  <div flex items-center justify-between gap-12px h-full>
111
206
  <!-- User Avatar -->
@@ -118,29 +213,7 @@ const menuItems = [
118
213
  </div>
119
214
 
120
215
  <div flex-1 min-w-0 flex items-center>
121
- <input
122
- v-if="isEditing"
123
- ref="inputRef"
124
- v-model="editTitle"
125
- type="text"
126
- text-14px
127
- font-medium
128
- text-neutral-700
129
- w-full
130
- bg-white
131
- border
132
- border-neutral-300
133
- rounded-4px
134
- px-6px
135
- py-2px
136
- outline-none
137
- focus:border-neutral-400
138
- @keydown="handleKeydown"
139
- @blur="saveTitle"
140
- @click.prevent.stop
141
- >
142
216
  <span
143
- v-else
144
217
  text-14px
145
218
  font-medium
146
219
  text-neutral-700
@@ -151,7 +224,13 @@ const menuItems = [
151
224
  </span>
152
225
  </div>
153
226
 
154
- <div v-if="!isEditing" mt-2px flex items-center opacity-0 group-hover:opacity-100 transition-opacity :class="isMenuOpen && 'opacity-100'">
227
+ <div
228
+ mt-2px
229
+ flex
230
+ items-center
231
+ class="opacity-0 group-hover:opacity-100 transition-opacity"
232
+ :class="isMenuOpen && 'opacity-100'"
233
+ >
155
234
  <TelaDropdownMenu :items="menuItems">
156
235
  <button
157
236
  ref="menuButtonRef"
@@ -168,7 +247,7 @@ const menuItems = [
168
247
  </TelaDropdownMenu>
169
248
  </div>
170
249
  </div>
171
- </NuxtLink>
250
+ </a>
172
251
 
173
252
  <TelaConfirmationModal
174
253
  v-model:open="showDeleteModal"
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  conversation: DeepReadonly<Conversation>;
5
5
  active: boolean;
6
6
  userImage?: string;
7
+ selectConversation?: (id: string) => void | Promise<void>;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
10
  delete: (id: string) => any;
@@ -4,6 +4,8 @@ type __VLS_Props = {
4
4
  conversations: DeepReadonly<Conversation[]>;
5
5
  currentId?: string;
6
6
  getMemberImage?: (userId: string) => string | undefined;
7
+ selectConversation?: (id: string) => void | Promise<void>;
8
+ resetConversation?: () => void | Promise<void>;
7
9
  loading?: boolean;
8
10
  };
9
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { filterConversationsByTitle } from "../../utils/conversation-search";
2
3
  import { groupConversationsByDate } from "../../utils/format-time";
3
4
  import { exportConversationToMarkdown } from "../../utils/export-conversation";
4
5
  import { conversationListSkeletonRows } from "../../utils/conversation-list-skeleton";
@@ -6,21 +7,36 @@ const props = defineProps({
6
7
  conversations: { type: null, required: true },
7
8
  currentId: { type: String, required: false },
8
9
  getMemberImage: { type: Function, required: false },
10
+ selectConversation: { type: Function, required: false },
11
+ resetConversation: { type: Function, required: false },
9
12
  loading: { type: Boolean, required: false }
10
13
  });
11
14
  const chatApi = useChatApi();
12
15
  const { deleteConversation, renameConversation, duplicateConversation } = useConversations();
13
16
  const { setConversation } = useChat();
14
17
  const router = useRouter();
18
+ const conversationSearch = ref("");
19
+ const filteredConversations = computed(
20
+ () => filterConversationsByTitle(props.conversations, conversationSearch.value, {
21
+ fallbackTitle: "Nova conversa",
22
+ shouldInclude: (conversation) => !conversation.title && conversation.id === props.currentId
23
+ })
24
+ );
15
25
  const groupedConversations = computed(() => {
16
- return groupConversationsByDate(props.conversations);
26
+ return groupConversationsByDate(filteredConversations.value);
17
27
  });
18
- function handleDelete(id) {
28
+ const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
29
+ async function handleDelete(id) {
19
30
  const route = useRoute();
20
- if (route.params.id === id) {
21
- router.push("/");
31
+ const shouldResetConversation = props.currentId === id || route.params.id === id;
32
+ const deleted = await deleteConversation(id);
33
+ if (deleted && shouldResetConversation) {
34
+ if (props.resetConversation) {
35
+ void props.resetConversation();
36
+ } else {
37
+ router.push("/");
38
+ }
22
39
  }
23
- deleteConversation(id);
24
40
  }
25
41
  async function handleRename(id, title) {
26
42
  await renameConversation(id, title);
@@ -29,7 +45,11 @@ async function handleDuplicate(id) {
29
45
  const duplicated = await duplicateConversation(id);
30
46
  if (duplicated) {
31
47
  setConversation(duplicated);
32
- router.push(`/${duplicated.id}`);
48
+ if (props.selectConversation) {
49
+ void props.selectConversation(duplicated.id);
50
+ } else {
51
+ router.push(`/${duplicated.id}`);
52
+ }
33
53
  }
34
54
  }
35
55
  async function handleExport(id) {
@@ -46,7 +66,12 @@ async function handleExport(id) {
46
66
  </script>
47
67
 
48
68
  <template>
49
- <div flex="~ col" gap-16px>
69
+ <div flex="~ col" gap-12px>
70
+ <div px-8px flex="~ col" gap-8px>
71
+ <ChatConversationSearch v-model="conversationSearch" />
72
+ <ChatConversationCreatorFilter />
73
+ </div>
74
+
50
75
  <div v-if="loading && conversations.length === 0" flex="~ col" gap-4px>
51
76
  <div h-20px flex items-center pl-12px pr-4px mb-4px>
52
77
  <TelaSkeleton rounded-4px bg-neutral-200 style="width: 68px; height: 12px;" />
@@ -63,9 +88,9 @@ async function handleExport(id) {
63
88
  </div>
64
89
  </div>
65
90
 
66
- <div v-else-if="conversations.length === 0" p-16px text-center>
91
+ <div v-else-if="filteredConversations.length === 0" p-16px text-center>
67
92
  <p text-14px text-neutral-400>
68
- Nenhuma conversa ainda
93
+ {{ hasConversationSearchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda" }}
69
94
  </p>
70
95
  </div>
71
96
 
@@ -86,6 +111,7 @@ async function handleExport(id) {
86
111
  :conversation="conversation"
87
112
  :active="conversation.id === currentId"
88
113
  :user-image="getMemberImage?.(conversation.userId)"
114
+ :select-conversation="selectConversation"
89
115
  @delete="handleDelete"
90
116
  @rename="handleRename"
91
117
  @duplicate="handleDuplicate"
@@ -4,6 +4,8 @@ type __VLS_Props = {
4
4
  conversations: DeepReadonly<Conversation[]>;
5
5
  currentId?: string;
6
6
  getMemberImage?: (userId: string) => string | undefined;
7
+ selectConversation?: (id: string) => void | Promise<void>;
8
+ resetConversation?: () => void | Promise<void>;
7
9
  loading?: boolean;
8
10
  };
9
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,16 @@
1
+ type __VLS_Props = {
2
+ placeholder?: string;
3
+ };
4
+ type __VLS_ModelProps = {
5
+ modelValue: string;
6
+ };
7
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:modelValue": (value: string) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
12
+ }>, {
13
+ placeholder: string;
14
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,35 @@
1
+ <script setup>
2
+ defineProps({
3
+ placeholder: { type: String, required: false, default: "Buscar conversas" }
4
+ });
5
+ const searchQuery = defineModel({ type: String, ...{ required: true } });
6
+ function clearSearch() {
7
+ searchQuery.value = "";
8
+ }
9
+ </script>
10
+
11
+ <template>
12
+ <TelaInput
13
+ v-model="searchQuery"
14
+ icon="i-ph-magnifying-glass-bold"
15
+ :placeholder="placeholder"
16
+ autocomplete="off"
17
+ >
18
+ <template v-if="searchQuery" #trailing>
19
+ <TelaTooltip content="Limpar busca">
20
+ <button
21
+ type="button"
22
+ aria-label="Limpar busca"
23
+ hover:bg="#EBEFF199"
24
+ transition-colors
25
+ p-4px rounded-full
26
+ class="group"
27
+ tabindex="-1"
28
+ @click.stop="clearSearch"
29
+ >
30
+ <TelaIcon name="i-ph-x-circle-light" text="#031E22" op-40 group-hover:op-100 transition-opacity />
31
+ </button>
32
+ </TelaTooltip>
33
+ </template>
34
+ </TelaInput>
35
+ </template>
@@ -0,0 +1,16 @@
1
+ type __VLS_Props = {
2
+ placeholder?: string;
3
+ };
4
+ type __VLS_ModelProps = {
5
+ modelValue: string;
6
+ };
7
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:modelValue": (value: string) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
12
+ }>, {
13
+ placeholder: string;
14
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;