@meistrari/chat-nuxt 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "1.8.0",
4
+ "version": "1.9.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,5 +1,6 @@
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 },
@@ -7,13 +8,15 @@ const props = defineProps({
7
8
  userImage: { type: String, required: false }
8
9
  });
9
10
  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);
11
+ const isRenamingInline = ref(false);
12
+ const renameTitle = ref("");
13
+ const renameInputRef = ref(null);
14
14
  const showDeleteModal = ref(false);
15
15
  const { escape } = useMagicKeys();
16
16
  watch(() => escape?.value, (pressed) => {
17
+ if (pressed && isRenamingInline.value) {
18
+ cancelRename();
19
+ }
17
20
  if (pressed && showDeleteModal.value) {
18
21
  showDeleteModal.value = false;
19
22
  }
@@ -31,48 +34,63 @@ useMutationObserver(menuButtonRef, (mutations) => {
31
34
  }
32
35
  }
33
36
  }, { 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
- });
37
+ const conversationPath = computed(() => `/${props.conversation.id}`);
38
+ const conversationNavigationDelayMs = 250;
39
+ let conversationNavigationTimer = null;
40
+ function clearPendingConversationNavigation() {
41
+ if (conversationNavigationTimer === null)
42
+ return;
43
+ clearTimeout(conversationNavigationTimer);
44
+ conversationNavigationTimer = null;
45
45
  }
46
- function saveTitle() {
47
- if (justStartedEditing.value) {
46
+ function handleConversationClick(event) {
47
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
48
48
  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;
49
+ event.preventDefault();
50
+ clearPendingConversationNavigation();
51
+ if (event.detail > 1)
52
+ return;
53
+ conversationNavigationTimer = setTimeout(() => {
54
+ navigateTo(conversationPath.value);
55
+ conversationNavigationTimer = null;
56
+ }, conversationNavigationDelayMs);
56
57
  }
57
- function cancelEditing() {
58
- isEditing.value = false;
59
- editTitle.value = "";
60
- justStartedEditing.value = false;
58
+ async function focusRenameInput() {
59
+ await nextTick();
60
+ renameInputRef.value?.focus();
61
+ renameInputRef.value?.select();
61
62
  }
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();
63
+ function startRename() {
64
+ isMenuOpen.value = false;
65
+ clearPendingConversationNavigation();
66
+ renameTitle.value = props.conversation.title;
67
+ isRenamingInline.value = true;
68
+ void focusRenameInput();
69
+ }
70
+ function handleConversationDoubleClick() {
71
+ clearPendingConversationNavigation();
72
+ startRename();
73
+ }
74
+ function cancelRename() {
75
+ isRenamingInline.value = false;
76
+ renameTitle.value = "";
77
+ }
78
+ function submitRename() {
79
+ if (!isRenamingInline.value)
80
+ return;
81
+ const title = resolveConversationRenameTitle(renameTitle.value, props.conversation.title);
82
+ if (!title) {
83
+ cancelRename();
84
+ return;
69
85
  }
86
+ emit("rename", props.conversation.id, title);
87
+ cancelRename();
70
88
  }
71
89
  const menuItems = [
72
90
  {
73
91
  label: "Renomear",
74
92
  icon: "i-ph-pencil-simple",
75
- click: () => startEditing()
93
+ click: () => startRename()
76
94
  },
77
95
  {
78
96
  label: "Duplicar",
@@ -91,11 +109,53 @@ const menuItems = [
91
109
  click: () => showDeleteModal.value = true
92
110
  }
93
111
  ];
112
+ onBeforeUnmount(clearPendingConversationNavigation);
94
113
  </script>
95
114
 
96
115
  <template>
97
- <NuxtLink
98
- :to="`/${conversation.id}`"
116
+ <div
117
+ v-if="isRenamingInline"
118
+ px-12px
119
+ h-32px
120
+ rounded-8px
121
+ :class="active ? 'bg-neutral-200' : ''"
122
+ >
123
+ <div flex items-center justify-between gap-12px h-full>
124
+ <div v-if="userImage" w-16px h-16px rounded-full overflow-hidden flex-shrink-0>
125
+ <TelaAvatar
126
+ :image="userImage"
127
+ alt="Usuário"
128
+ size="xs"
129
+ />
130
+ </div>
131
+
132
+ <div
133
+ flex-1 min-w-0 flex items-center
134
+ text-neutral-700
135
+ pb-1px
136
+ b-b=".5px solid #A3A3A3"
137
+ >
138
+ <input
139
+ ref="renameInputRef"
140
+ v-model="renameTitle"
141
+ type="text"
142
+ autocomplete="off"
143
+ aria-label="Nome da conversa"
144
+ w-full min-w-0
145
+ bg-transparent border-none outline-none
146
+ text-14px font-medium text-neutral-700 leading-18px tracking="-.15px"
147
+ p-0
148
+ @blur="submitRename"
149
+ @keydown.enter.prevent="submitRename"
150
+ @keydown.escape.prevent.stop="cancelRename"
151
+ >
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <a
157
+ v-else
158
+ :href="conversationPath"
99
159
  block
100
160
  px-12px
101
161
  h-32px
@@ -106,6 +166,8 @@ const menuItems = [
106
166
  :class="[
107
167
  active ? 'bg-neutral-200' : 'hover:bg-neutral-200/50'
108
168
  ]"
169
+ @click="handleConversationClick"
170
+ @dblclick.prevent.stop="handleConversationDoubleClick"
109
171
  >
110
172
  <div flex items-center justify-between gap-12px h-full>
111
173
  <!-- User Avatar -->
@@ -118,29 +180,7 @@ const menuItems = [
118
180
  </div>
119
181
 
120
182
  <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
183
  <span
143
- v-else
144
184
  text-14px
145
185
  font-medium
146
186
  text-neutral-700
@@ -151,7 +191,7 @@ const menuItems = [
151
191
  </span>
152
192
  </div>
153
193
 
154
- <div v-if="!isEditing" mt-2px flex items-center opacity-0 group-hover:opacity-100 transition-opacity :class="isMenuOpen && 'opacity-100'">
194
+ <div mt-2px flex items-center opacity-0 group-hover:opacity-100 transition-opacity :class="isMenuOpen && 'opacity-100'">
155
195
  <TelaDropdownMenu :items="menuItems">
156
196
  <button
157
197
  ref="menuButtonRef"
@@ -168,7 +208,7 @@ const menuItems = [
168
208
  </TelaDropdownMenu>
169
209
  </div>
170
210
  </div>
171
- </NuxtLink>
211
+ </a>
172
212
 
173
213
  <TelaConfirmationModal
174
214
  v-model:open="showDeleteModal"
@@ -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";
@@ -12,9 +13,17 @@ const chatApi = useChatApi();
12
13
  const { deleteConversation, renameConversation, duplicateConversation } = useConversations();
13
14
  const { setConversation } = useChat();
14
15
  const router = useRouter();
16
+ const conversationSearch = ref("");
17
+ const filteredConversations = computed(
18
+ () => filterConversationsByTitle(props.conversations, conversationSearch.value, {
19
+ fallbackTitle: "Nova conversa",
20
+ shouldInclude: (conversation) => !conversation.title && conversation.id === props.currentId
21
+ })
22
+ );
15
23
  const groupedConversations = computed(() => {
16
- return groupConversationsByDate(props.conversations);
24
+ return groupConversationsByDate(filteredConversations.value);
17
25
  });
26
+ const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
18
27
  function handleDelete(id) {
19
28
  const route = useRoute();
20
29
  if (route.params.id === id) {
@@ -46,7 +55,11 @@ async function handleExport(id) {
46
55
  </script>
47
56
 
48
57
  <template>
49
- <div flex="~ col" gap-16px>
58
+ <div flex="~ col" gap-12px>
59
+ <div px-8px>
60
+ <ChatConversationSearch v-model="conversationSearch" />
61
+ </div>
62
+
50
63
  <div v-if="loading && conversations.length === 0" flex="~ col" gap-4px>
51
64
  <div h-20px flex items-center pl-12px pr-4px mb-4px>
52
65
  <TelaSkeleton rounded-4px bg-neutral-200 style="width: 68px; height: 12px;" />
@@ -63,9 +76,9 @@ async function handleExport(id) {
63
76
  </div>
64
77
  </div>
65
78
 
66
- <div v-else-if="conversations.length === 0" p-16px text-center>
79
+ <div v-else-if="filteredConversations.length === 0" p-16px text-center>
67
80
  <p text-14px text-neutral-400>
68
- Nenhuma conversa ainda
81
+ {{ hasConversationSearchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda" }}
69
82
  </p>
70
83
  </div>
71
84
 
@@ -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,16 @@
1
+ <script setup>
2
+ defineProps({
3
+ placeholder: { type: String, required: false, default: "Buscar conversas" }
4
+ });
5
+ const searchQuery = defineModel({ type: String, ...{ required: true } });
6
+ </script>
7
+
8
+ <template>
9
+ <TelaInput
10
+ v-model="searchQuery"
11
+ icon="i-ph-magnifying-glass-bold"
12
+ :placeholder="placeholder"
13
+ show-clear-button
14
+ autocomplete="off"
15
+ />
16
+ </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;
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { formatRelativeTime } from "../../../../utils/format-time";
3
3
  import { conversationListSkeletonRows } from "../../../../utils/conversation-list-skeleton";
4
+ import { filterConversationsByTitle } from "../../../../utils/conversation-search";
4
5
  const props = defineProps({
5
6
  open: { type: Boolean, required: true },
6
7
  conversations: { type: null, required: true },
@@ -15,6 +16,14 @@ const initial = computed(() => {
15
16
  const source = props.userName ?? props.userEmail ?? "U";
16
17
  return source.charAt(0).toUpperCase();
17
18
  });
19
+ const conversationSearch = ref("");
20
+ const filteredConversations = computed(
21
+ () => filterConversationsByTitle(props.conversations, conversationSearch.value, {
22
+ fallbackTitle: "Nova conversa",
23
+ shouldInclude: (conversation) => !conversation.title && conversation.id === props.activeId
24
+ })
25
+ );
26
+ const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
18
27
  function handleSelect(id) {
19
28
  emit("select", id);
20
29
  emit("close");
@@ -74,7 +83,7 @@ function handleNew() {
74
83
  </button>
75
84
  </div>
76
85
 
77
- <div p-12px>
86
+ <div p-12px flex="~ col" gap-8px>
78
87
  <button
79
88
  flex items-center justify-center gap-6px
80
89
  w-full px-12px py-10px rounded-10px
@@ -88,6 +97,7 @@ function handleNew() {
88
97
  Nova conversa
89
98
  </span>
90
99
  </button>
100
+ <ChatConversationSearch v-model="conversationSearch" />
91
101
  </div>
92
102
 
93
103
  <div px-14px pt-4px pb-4px>
@@ -95,7 +105,7 @@ function handleNew() {
95
105
  text-12px font-medium uppercase text-neutral-400
96
106
  style="letter-spacing: 0.5px"
97
107
  >
98
- Recentes
108
+ {{ hasConversationSearchQuery ? "Resultados" : "Recentes" }}
99
109
  </span>
100
110
  </div>
101
111
 
@@ -109,14 +119,14 @@ function handleNew() {
109
119
  <TelaSkeleton rounded-6px bg-neutral-200 :style="{ width: row.width, height: '13px' }" />
110
120
  </div>
111
121
  </div>
112
- <div v-else-if="conversations.length === 0" p-16px text-center>
122
+ <div v-else-if="filteredConversations.length === 0" p-16px text-center>
113
123
  <p text-14px text-neutral-400>
114
- Nenhuma conversa ainda
124
+ {{ hasConversationSearchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda" }}
115
125
  </p>
116
126
  </div>
117
127
  <div v-else flex="~ col" gap-1px>
118
128
  <button
119
- v-for="conv in conversations"
129
+ v-for="conv in filteredConversations"
120
130
  :key="conv.id"
121
131
  flex="~ col" gap-2px
122
132
  w-full px-12px py-10px rounded-10px text-left
@@ -53,7 +53,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
53
53
  rename: () => any;
54
54
  duplicate: () => any;
55
55
  export: () => any;
56
- saveTitle: () => any;
57
56
  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
57
  type: "file";
59
58
  name: string;
@@ -68,6 +67,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
68
67
  "update:model": (value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any;
69
68
  settings: () => any;
70
69
  "update:editTitle": (value: string) => any;
70
+ saveTitle: () => any;
71
71
  cancelEditing: () => any;
72
72
  copyLink: () => any;
73
73
  debug: () => any;
@@ -85,7 +85,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
85
85
  onRename?: (() => any) | undefined;
86
86
  onDuplicate?: (() => any) | undefined;
87
87
  onExport?: (() => any) | undefined;
88
- onSaveTitle?: (() => any) | undefined;
89
88
  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
89
  type: "file";
91
90
  name: string;
@@ -100,6 +99,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
100
99
  "onUpdate:model"?: ((value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any) | undefined;
101
100
  onSettings?: (() => any) | undefined;
102
101
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
102
+ onSaveTitle?: (() => any) | undefined;
103
103
  onCancelEditing?: (() => any) | undefined;
104
104
  onCopyLink?: (() => any) | undefined;
105
105
  onDebug?: (() => any) | undefined;
@@ -53,7 +53,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
53
53
  rename: () => any;
54
54
  duplicate: () => any;
55
55
  export: () => any;
56
- saveTitle: () => any;
57
56
  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
57
  type: "file";
59
58
  name: string;
@@ -68,6 +67,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
68
67
  "update:model": (value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any;
69
68
  settings: () => any;
70
69
  "update:editTitle": (value: string) => any;
70
+ saveTitle: () => any;
71
71
  cancelEditing: () => any;
72
72
  copyLink: () => any;
73
73
  debug: () => any;
@@ -85,7 +85,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
85
85
  onRename?: (() => any) | undefined;
86
86
  onDuplicate?: (() => any) | undefined;
87
87
  onExport?: (() => any) | undefined;
88
- onSaveTitle?: (() => any) | undefined;
89
88
  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
89
  type: "file";
91
90
  name: string;
@@ -100,6 +99,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
100
99
  "onUpdate:model"?: ((value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any) | undefined;
101
100
  onSettings?: (() => any) | undefined;
102
101
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
102
+ onSaveTitle?: (() => any) | undefined;
103
103
  onCancelEditing?: (() => any) | undefined;
104
104
  onCopyLink?: (() => any) | undefined;
105
105
  onDebug?: (() => any) | undefined;
@@ -19,9 +19,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
19
  rename: () => any;
20
20
  duplicate: () => any;
21
21
  export: () => any;
22
- saveTitle: () => any;
23
22
  "update:isEditing": (value: boolean) => any;
24
23
  "update:editTitle": (value: string) => any;
24
+ saveTitle: () => any;
25
25
  cancelEditing: () => any;
26
26
  copyLink: () => any;
27
27
  debug: () => any;
@@ -31,9 +31,9 @@ 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
- onSaveTitle?: (() => any) | undefined;
35
34
  "onUpdate:isEditing"?: ((value: boolean) => any) | undefined;
36
35
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
36
+ onSaveTitle?: (() => any) | undefined;
37
37
  onCancelEditing?: (() => any) | undefined;
38
38
  onCopyLink?: (() => any) | undefined;
39
39
  onDebug?: (() => any) | undefined;
@@ -19,6 +19,7 @@ const props = defineProps({
19
19
  const emit = defineEmits(["update:isEditing", "update:editTitle", "saveTitle", "cancelEditing", "rename", "duplicate", "export", "copyLink", "debug", "delete", "tabChange"]);
20
20
  const titleInputRef = ref(null);
21
21
  const currentTab = computed(() => props.activeTab ?? "chat");
22
+ const titleInputWidth = computed(() => `${Math.max(props.editTitle.length + 2, 8)}ch`);
22
23
  const headerMenuItems = computed(() => {
23
24
  const items = [
24
25
  {
@@ -87,19 +88,36 @@ watch(() => props.isEditing, (editing) => {
87
88
  :style="{ left: 'var(--chat-panel-offset)' }"
88
89
  >
89
90
  <div flex="~ col 1" gap-4px min-w-0>
90
- <input
91
+ <div
91
92
  v-if="isEditing"
92
- ref="titleInputRef"
93
- :value="editTitle"
94
- type="text"
95
- heading-h4-semibold text-primary
96
- bg-white border border-gray-200 rounded-6px px-8px py-2px
97
- outline-none focus:border-gray-400
98
- @input="emit('update:editTitle', $event.target.value)"
99
- @keydown="handleTitleKeydown"
100
- @blur="emit('saveTitle')"
93
+ flex items-center
94
+ text-primary
95
+ w-fit max-w-full
96
+ pb-1px
97
+ b-b=".5px solid #A3A3A3"
98
+ :style="{ width: titleInputWidth }"
99
+ >
100
+ <input
101
+ ref="titleInputRef"
102
+ :value="editTitle"
103
+ type="text"
104
+ autocomplete="off"
105
+ aria-label="Nome da conversa"
106
+ w-full min-w-0 max-w-full
107
+ bg-transparent border-none outline-none
108
+ text-16px font-580 text-primary leading-20px tracking="-.2px"
109
+ p-0
110
+ @input="emit('update:editTitle', $event.target.value)"
111
+ @keydown="handleTitleKeydown"
112
+ @blur="emit('saveTitle')"
113
+ >
114
+ </div>
115
+ <p
116
+ v-else
117
+ text-16px font-580 text-primary leading-20px tracking="-.2px"
118
+ truncate cursor-text
119
+ @dblclick.prevent.stop="emit('rename')"
101
120
  >
102
- <p v-else text-16px font-580 text-primary leading-20px tracking="-.2px" truncate>
103
121
  {{ capitalizeFirst(title) }}<span v-if="generatingTitle" class="typing-cursor">|</span>
104
122
  </p>
105
123
 
@@ -19,9 +19,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
19
  rename: () => any;
20
20
  duplicate: () => any;
21
21
  export: () => any;
22
- saveTitle: () => any;
23
22
  "update:isEditing": (value: boolean) => any;
24
23
  "update:editTitle": (value: string) => any;
24
+ saveTitle: () => any;
25
25
  cancelEditing: () => any;
26
26
  copyLink: () => any;
27
27
  debug: () => any;
@@ -31,9 +31,9 @@ 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
- onSaveTitle?: (() => any) | undefined;
35
34
  "onUpdate:isEditing"?: ((value: boolean) => any) | undefined;
36
35
  "onUpdate:editTitle"?: ((value: string) => any) | undefined;
36
+ onSaveTitle?: (() => any) | undefined;
37
37
  onCancelEditing?: (() => any) | undefined;
38
38
  onCopyLink?: (() => any) | undefined;
39
39
  onDebug?: (() => any) | undefined;
@@ -5,8 +5,8 @@ async function loadPdfjs() {
5
5
  if (!import.meta.client)
6
6
  return null;
7
7
  if (!pdfjsLibPromise) {
8
- pdfjsLibPromise = import("pdfjs-dist").then((lib) => {
9
- lib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/build/pdf.worker.min.mjs`;
8
+ pdfjsLibPromise = import("pdfjs-dist/legacy/build/pdf.mjs").then((lib) => {
9
+ lib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/legacy/build/pdf.worker.min.mjs`;
10
10
  return lib;
11
11
  });
12
12
  }
@@ -3,6 +3,7 @@ import { useChatAuth } from "#chat-auth";
3
3
  import { useBreakpoints } from "@vueuse/core";
4
4
  import { exportConversationToMarkdown } from "../../utils/export-conversation";
5
5
  import { CHAT_MOBILE_BREAKPOINT } from "../../utils/breakpoints";
6
+ import { filterConversationsByTitle } from "../../utils/conversation-search";
6
7
  import ChatMobileFilePreview from "../../components/chat/mobile/files/file-preview.vue";
7
8
  import ChatMobileShell from "../../components/chat/mobile/shell/shell.vue";
8
9
  import { conversationListSkeletonRows } from "../../utils/conversation-list-skeleton";
@@ -51,6 +52,7 @@ const resolvedFeatures = computed(() => resolveChatFeatures(props.features));
51
52
  const dropZoneRef = ref(null);
52
53
  const activeTab = ref("chat");
53
54
  const messageInputRef = ref(null);
55
+ const conversationSearch = ref("");
54
56
  const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
55
57
  const isMobile = breakpoints.smaller("mobile");
56
58
  const drawerOpen = ref(false);
@@ -106,6 +108,13 @@ const visibleTelaAgentInputSchemaLoading = computed(() => !hasMessages.value &&
106
108
  const visibleConversations = computed(
107
109
  () => conversations.value.filter((c) => c.title || c.id === activeConversationId.value)
108
110
  );
111
+ const searchFilteredConversations = computed(
112
+ () => filterConversationsByTitle(visibleConversations.value, conversationSearch.value, {
113
+ fallbackTitle: "Nova conversa",
114
+ shouldInclude: (conversation) => !conversation.title && conversation.id === activeConversationId.value
115
+ })
116
+ );
117
+ const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
109
118
  function syncConversationId(nextConversationId) {
110
119
  activeConversationId.value = normalizeConversationId(nextConversationId);
111
120
  }
@@ -408,7 +417,7 @@ async function handleDelete() {
408
417
  w-240px h-full flex="~ col" flex-shrink-0
409
418
  bg-neutral-50 b-r=".5px neutral-200"
410
419
  >
411
- <div px-12px pt-16px pb-8px>
420
+ <div px-12px pt-16px pb-8px flex="~ col" gap-8px>
412
421
  <button
413
422
  flex items-center justify-center gap-6px
414
423
  h-32px px-12px w-full
@@ -421,6 +430,7 @@ async function handleDelete() {
421
430
  Nova conversa
422
431
  </span>
423
432
  </button>
433
+ <ChatConversationSearch v-model="conversationSearch" />
424
434
  </div>
425
435
 
426
436
  <div flex-1 overflow-y-auto px-4px>
@@ -433,14 +443,14 @@ async function handleDelete() {
433
443
  <TelaSkeleton rounded-6px bg-neutral-200 :style="{ width: row.width, height: '13px' }" />
434
444
  </div>
435
445
  </div>
436
- <div v-else-if="visibleConversations.length === 0" p-16px text-center>
446
+ <div v-else-if="searchFilteredConversations.length === 0" p-16px text-center>
437
447
  <p text-14px text-neutral-400>
438
- Nenhuma conversa ainda
448
+ {{ hasConversationSearchQuery ? "Nenhuma conversa encontrada" : "Nenhuma conversa ainda" }}
439
449
  </p>
440
450
  </div>
441
451
  <div v-else flex="~ col" gap-1px px-8px py-8px>
442
452
  <button
443
- v-for="conv in visibleConversations"
453
+ v-for="conv in searchFilteredConversations"
444
454
  :key="conv.id"
445
455
  flex items-center gap-8px h-32px px-8px rounded-8px
446
456
  w-full text-left truncate transition-colors cursor-pointer
@@ -161,6 +161,10 @@ export async function resolveDefaultAgentEnvironmentVariables(event, context, op
161
161
  if (resolveChatNuxtFeatures(useRuntimeConfig()).credentials) {
162
162
  try {
163
163
  credentialVariables = await resolveWorkspaceCredentials(event, context.workspaceId);
164
+ logger.info({
165
+ ...logContext,
166
+ credentialKeys: Object.keys(credentialVariables)
167
+ }, "Workspace credentials loaded for agent API");
164
168
  } catch (error) {
165
169
  logger.warn({ ...logContext, error }, options.credentialsFailureMessage);
166
170
  }
@@ -0,0 +1,8 @@
1
+ export interface ConversationTitleSearchItem {
2
+ title?: string | null;
3
+ }
4
+ export interface ConversationTitleSearchOptions<T extends ConversationTitleSearchItem> {
5
+ fallbackTitle?: string;
6
+ shouldInclude?: (conversation: T) => boolean;
7
+ }
8
+ export declare function filterConversationsByTitle<T extends ConversationTitleSearchItem>(conversations: readonly T[], searchQuery: string, options?: ConversationTitleSearchOptions<T>): T[];
@@ -0,0 +1,11 @@
1
+ export function filterConversationsByTitle(conversations, searchQuery, options = {}) {
2
+ const query = searchQuery.toLowerCase().trim();
3
+ if (!query)
4
+ return [...conversations];
5
+ return conversations.filter((conversation) => {
6
+ if (options.shouldInclude?.(conversation))
7
+ return true;
8
+ const title = conversation.title ?? options.fallbackTitle;
9
+ return title?.toLowerCase().includes(query) ?? false;
10
+ });
11
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveConversationRenameTitle(nextTitle: string, currentTitle: string): string | null;
@@ -0,0 +1,6 @@
1
+ export function resolveConversationRenameTitle(nextTitle, currentTitle) {
2
+ const trimmedTitle = nextTitle.trim();
3
+ if (!trimmedTitle || trimmedTitle === currentTitle)
4
+ return null;
5
+ return trimmedTitle;
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {