@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.
- package/README.md +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/MeistrariChatEmbed.vue +3 -0
- package/dist/runtime/components/chat/conversation-creator-filter.d.vue.ts +3 -0
- package/dist/runtime/components/chat/conversation-creator-filter.vue +95 -0
- package/dist/runtime/components/chat/conversation-creator-filter.vue.d.ts +3 -0
- package/dist/runtime/components/chat/conversation-item.d.vue.ts +1 -0
- package/dist/runtime/components/chat/conversation-item.vue +142 -63
- package/dist/runtime/components/chat/conversation-item.vue.d.ts +1 -0
- package/dist/runtime/components/chat/conversation-list.d.vue.ts +2 -0
- package/dist/runtime/components/chat/conversation-list.vue +35 -9
- package/dist/runtime/components/chat/conversation-list.vue.d.ts +2 -0
- package/dist/runtime/components/chat/conversation-search.d.vue.ts +16 -0
- package/dist/runtime/components/chat/conversation-search.vue +35 -0
- package/dist/runtime/components/chat/conversation-search.vue.d.ts +16 -0
- package/dist/runtime/components/chat/mobile/shell/drawer.vue +16 -5
- package/dist/runtime/components/chat/mobile/shell/header.d.vue.ts +4 -4
- package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +4 -4
- package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +14 -14
- package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +14 -14
- package/dist/runtime/components/chat/topbar.d.vue.ts +8 -8
- package/dist/runtime/components/chat/topbar.vue +29 -11
- package/dist/runtime/components/chat/topbar.vue.d.ts +8 -8
- package/dist/runtime/composables/useConversations.d.ts +2 -0
- package/dist/runtime/composables/useConversations.js +81 -36
- package/dist/runtime/composables/useEmbedConfig.d.ts +3 -0
- package/dist/runtime/composables/useEmbedConfig.js +4 -0
- package/dist/runtime/embed/components/ChatEmbed.vue +2 -0
- package/dist/runtime/embed/components/ChatEmbedInner.vue +11 -30
- package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.d.vue.ts +4 -4
- package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.vue.d.ts +4 -4
- package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +2 -2
- package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +2 -2
- package/dist/runtime/server/api/conversations/index.get.js +5 -4
- package/dist/runtime/server/utils/conversation-list.d.ts +4 -0
- package/dist/runtime/server/utils/conversation-list.js +28 -0
- package/dist/runtime/types/chat.d.ts +8 -0
- package/dist/runtime/types/chat.js +4 -0
- package/dist/runtime/types/embed.d.ts +3 -0
- package/dist/runtime/utils/conversation-creator-filter.d.ts +5 -0
- package/dist/runtime/utils/conversation-creator-filter.js +24 -0
- package/dist/runtime/utils/conversation-search.d.ts +8 -0
- package/dist/runtime/utils/conversation-search.js +11 -0
- package/dist/runtime/utils/conversation-title.d.ts +1 -0
- package/dist/runtime/utils/conversation-title.js +6 -0
- 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
|
@@ -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
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
47
|
-
if (
|
|
49
|
+
function clearRenameBlurTimer() {
|
|
50
|
+
if (renameBlurTimer === null)
|
|
48
51
|
return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
justStartedEditing.value = false;
|
|
93
|
+
function handleConversationDoubleClick() {
|
|
94
|
+
clearPendingConversationNavigation();
|
|
95
|
+
startRename();
|
|
61
96
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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: () =>
|
|
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
|
-
<
|
|
98
|
-
|
|
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
|
|
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
|
-
</
|
|
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(
|
|
26
|
+
return groupConversationsByDate(filteredConversations.value);
|
|
17
27
|
});
|
|
18
|
-
|
|
28
|
+
const hasConversationSearchQuery = computed(() => conversationSearch.value.trim().length > 0);
|
|
29
|
+
async function handleDelete(id) {
|
|
19
30
|
const route = useRoute();
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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-
|
|
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="
|
|
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;
|