@meistrari/chat-nuxt 4.2.0-rc.5 → 4.2.0-rc.6
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 +1 -1
- package/dist/runtime/conversations/components/chat-conversation-list.vue +174 -99
- package/dist/runtime/embed/components/chat-configuration-modal.d.vue.ts +2 -2
- package/dist/runtime/embed/components/chat-configuration-modal.vue.d.ts +2 -2
- package/dist/runtime/embed/components/chat-embed-inner.vue +1 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { useVirtualList } from "@vueuse/core";
|
|
2
3
|
import { useConversationList } from "../composables/conversation-list";
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
conversations: { type: null, required: true },
|
|
@@ -48,11 +49,122 @@ const {
|
|
|
48
49
|
handleConversationDrop,
|
|
49
50
|
startConversationInGroup
|
|
50
51
|
} = useConversationList(props);
|
|
52
|
+
const rows = computed(() => {
|
|
53
|
+
const result = [];
|
|
54
|
+
if (groupItems.value.length > 0)
|
|
55
|
+
result.push({ type: "folders", key: "folders", height: 18, groupId: null });
|
|
56
|
+
for (const groupBucket of groupItems.value) {
|
|
57
|
+
const groupId = groupBucket.group.id;
|
|
58
|
+
result.push({ type: "group", key: `group:${groupId}`, height: 34, groupId, groupBucket });
|
|
59
|
+
if (groupBucket.isRenaming && renameGroupError.value)
|
|
60
|
+
result.push({ type: "error", key: `error:${groupId}`, height: 20, groupId, groupBucket });
|
|
61
|
+
if (!groupBucket.open)
|
|
62
|
+
continue;
|
|
63
|
+
for (const conversation of groupBucket.items)
|
|
64
|
+
result.push({ type: "conversation", key: conversation.id, height: 33, groupId, conversation });
|
|
65
|
+
if (groupBucket.showNewConversationAction)
|
|
66
|
+
result.push({ type: "new", key: `new:${groupId}`, height: 33, groupId, groupBucket });
|
|
67
|
+
else if (groupBucket.showSearchEmpty)
|
|
68
|
+
result.push({ type: "empty", key: `empty:${groupId}`, height: 33, groupId, groupBucket });
|
|
69
|
+
}
|
|
70
|
+
result.push({ type: "ungrouped", key: "ungrouped", height: 24, groupId: null });
|
|
71
|
+
for (const conversation of ungroupedConversations.value)
|
|
72
|
+
result.push({ type: "conversation", key: conversation.id, height: 33, groupId: null, conversation });
|
|
73
|
+
let offset = 0;
|
|
74
|
+
return result.map((row) => {
|
|
75
|
+
const positionedRow = { ...row, offset };
|
|
76
|
+
offset += row.height;
|
|
77
|
+
return positionedRow;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const { list: visibleRows, containerProps, scrollTo } = useVirtualList(rows, {
|
|
81
|
+
itemHeight: (index) => rows.value[index].height,
|
|
82
|
+
overscan: 8
|
|
83
|
+
});
|
|
84
|
+
const retainedRowKey = ref(null);
|
|
85
|
+
const draggedRowKey = ref(null);
|
|
86
|
+
const renderedRows = computed(() => {
|
|
87
|
+
const indexes = new Set(visibleRows.value.map((row) => row.index));
|
|
88
|
+
for (const key of [retainedRowKey.value, draggedRowKey.value]) {
|
|
89
|
+
const index = rows.value.findIndex((row) => row.key === key);
|
|
90
|
+
if (index >= 0)
|
|
91
|
+
indexes.add(index);
|
|
92
|
+
}
|
|
93
|
+
return [...indexes].sort((a, b) => a - b).map((index) => ({ ...rows.value[index], index }));
|
|
94
|
+
});
|
|
95
|
+
const totalHeight = computed(() => {
|
|
96
|
+
const last = rows.value.at(-1);
|
|
97
|
+
return last ? last.offset + last.height : 0;
|
|
98
|
+
});
|
|
99
|
+
function revealRow(index) {
|
|
100
|
+
const container = containerProps.ref.value;
|
|
101
|
+
const row = rows.value[index];
|
|
102
|
+
if (!row || !container)
|
|
103
|
+
return;
|
|
104
|
+
if (row.offset < container.scrollTop || row.offset + row.height > container.scrollTop + container.clientHeight)
|
|
105
|
+
scrollTo(index);
|
|
106
|
+
}
|
|
107
|
+
const activeRowIndex = computed(() => rows.value.findIndex((row) => row.type === "conversation" && row.conversation.id === props.currentId));
|
|
108
|
+
watch([activeRowIndex, containerProps.ref], ([index]) => revealRow(index), { flush: "post" });
|
|
109
|
+
const renamingGroupRowIndex = computed(() => rows.value.findIndex((row) => row.type === "group" && row.groupBucket.isRenaming));
|
|
110
|
+
watch([renamingGroupRowIndex, containerProps.ref], ([index]) => revealRow(index), { flush: "post" });
|
|
111
|
+
async function handleRowKeydown(event, index) {
|
|
112
|
+
if (event.key !== "Tab" || event.defaultPrevented)
|
|
113
|
+
return;
|
|
114
|
+
const selector = 'a[href], button:not([disabled]), input:not([disabled]), [tabindex="0"]';
|
|
115
|
+
const controls = [...event.currentTarget.querySelectorAll(selector)];
|
|
116
|
+
if (event.target !== (event.shiftKey ? controls[0] : controls.at(-1)))
|
|
117
|
+
return;
|
|
118
|
+
const direction = event.shiftKey ? -1 : 1;
|
|
119
|
+
let nextIndex = index + direction;
|
|
120
|
+
while (rows.value[nextIndex] && ["folders", "ungrouped", "empty", "error"].includes(rows.value[nextIndex].type))
|
|
121
|
+
nextIndex += direction;
|
|
122
|
+
const nextRow = rows.value[nextIndex];
|
|
123
|
+
if (!nextRow)
|
|
124
|
+
return;
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
retainedRowKey.value = nextRow.key;
|
|
127
|
+
revealRow(nextIndex);
|
|
128
|
+
await nextTick();
|
|
129
|
+
const nextControls = containerProps.ref.value?.querySelector(`[data-row-index="${nextIndex}"]`)?.querySelectorAll(selector);
|
|
130
|
+
const target = event.shiftKey ? nextControls?.[nextControls.length - 1] : nextControls?.[0];
|
|
131
|
+
target?.focus();
|
|
132
|
+
}
|
|
133
|
+
watch(conversationSearch, () => scrollTo(0), { flush: "post" });
|
|
134
|
+
function acceptsDrops(row) {
|
|
135
|
+
return row.type !== "folders";
|
|
136
|
+
}
|
|
137
|
+
function dropListeners(row) {
|
|
138
|
+
if (!acceptsDrops(row))
|
|
139
|
+
return {};
|
|
140
|
+
return {
|
|
141
|
+
dragenter: (event) => {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
handleDragEnter(row.groupId);
|
|
144
|
+
},
|
|
145
|
+
dragover: (event) => {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
handleDragOver(event, row.groupId);
|
|
148
|
+
},
|
|
149
|
+
dragleave: (event) => handleDragLeave(event, row.groupId),
|
|
150
|
+
drop: (event) => {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
void handleConversationDrop(event, row.groupId);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function dropTargetClass(row) {
|
|
157
|
+
if (!acceptsDrops(row))
|
|
158
|
+
return "";
|
|
159
|
+
if (row.groupId)
|
|
160
|
+
return groupItems.value.find((group) => group.group.id === row.groupId)?.dropTarget ? "bg-neutral-200" : "";
|
|
161
|
+
return ungroupedDropTarget.value ? "bg-lowered" : "";
|
|
162
|
+
}
|
|
51
163
|
</script>
|
|
52
164
|
|
|
53
165
|
<template>
|
|
54
|
-
<div flex="~ col" gap-12px>
|
|
55
|
-
<div px-8px flex="~ col" gap-8px>
|
|
166
|
+
<div flex="~ col" gap-12px h-full min-h-0 overflow-hidden>
|
|
167
|
+
<div px-8px flex="~ col" gap-8px shrink-0>
|
|
56
168
|
<ChatConversationSearch v-model="conversationSearch" />
|
|
57
169
|
<ChatConversationCreatorFilter />
|
|
58
170
|
</div>
|
|
@@ -79,33 +191,39 @@ const {
|
|
|
79
191
|
</p>
|
|
80
192
|
</div>
|
|
81
193
|
|
|
82
|
-
<div v-else
|
|
83
|
-
<div
|
|
84
|
-
<span body-12-medium text-secondary pl-10px>
|
|
85
|
-
Pastas · {{ groupItems.length }}
|
|
86
|
-
</span>
|
|
87
|
-
|
|
194
|
+
<div v-else v-bind="containerProps" flex-1 min-h-0 mt-4px>
|
|
195
|
+
<div relative :style="{ height: `${totalHeight}px` }">
|
|
88
196
|
<div
|
|
89
|
-
v-for="
|
|
90
|
-
:key="
|
|
91
|
-
|
|
197
|
+
v-for="row in renderedRows"
|
|
198
|
+
:key="row.key"
|
|
199
|
+
:style="{ height: `${row.height}px`, top: `${row.offset}px` }"
|
|
200
|
+
absolute inset-x-0
|
|
201
|
+
:data-row-index="row.index"
|
|
92
202
|
rounded-8px
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@
|
|
96
|
-
@
|
|
97
|
-
@
|
|
98
|
-
@
|
|
203
|
+
:class="dropTargetClass(row)"
|
|
204
|
+
@keydown="handleRowKeydown($event, row.index)"
|
|
205
|
+
@focusin="retainedRowKey = row.key"
|
|
206
|
+
@pointerdown="retainedRowKey = row.key"
|
|
207
|
+
@dragstart.capture="draggedRowKey = row.key"
|
|
208
|
+
@dragend.capture="draggedRowKey = null"
|
|
209
|
+
v-on="dropListeners(row)"
|
|
99
210
|
>
|
|
211
|
+
<span v-if="row.type === 'folders'" body-12-medium text-secondary pl-10px>
|
|
212
|
+
Pastas · {{ groupItems.length }}
|
|
213
|
+
</span>
|
|
214
|
+
<span v-else-if="row.type === 'ungrouped'" block body-12-medium text-secondary pl-10px pt-4px>
|
|
215
|
+
{{ ungroupedConversationLabel }}
|
|
216
|
+
</span>
|
|
100
217
|
<div
|
|
218
|
+
v-if="row.type === 'group'"
|
|
101
219
|
rounded-10px
|
|
102
220
|
class="group"
|
|
103
|
-
:class="groupBucket.dropTarget ? '' : groupBucket.open ? 'bg-muted' : 'hover:bg-subtle'"
|
|
221
|
+
:class="row.groupBucket.dropTarget ? '' : row.groupBucket.open ? 'bg-muted' : 'hover:bg-subtle'"
|
|
104
222
|
>
|
|
105
223
|
<div h-32px w-full pl-10px pr-4px flex items-center gap-8px text-left>
|
|
106
|
-
<template v-if="groupBucket.isRenaming">
|
|
224
|
+
<template v-if="row.groupBucket.isRenaming">
|
|
107
225
|
<TelaIcon
|
|
108
|
-
:name="groupBucket.folderIcon"
|
|
226
|
+
:name="row.groupBucket.folderIcon"
|
|
109
227
|
size="16px"
|
|
110
228
|
color="icon-secondary"
|
|
111
229
|
shrink-0
|
|
@@ -116,7 +234,7 @@ const {
|
|
|
116
234
|
mr-4px
|
|
117
235
|
>
|
|
118
236
|
<input
|
|
119
|
-
:ref="(element) => setRenameGroupInputRef(element, groupBucket.group.id)"
|
|
237
|
+
:ref="(element) => setRenameGroupInputRef(element, row.groupBucket.group.id)"
|
|
120
238
|
v-model="renameGroupName"
|
|
121
239
|
type="text"
|
|
122
240
|
autocomplete="off"
|
|
@@ -148,19 +266,20 @@ const {
|
|
|
148
266
|
flex items-center gap-8px
|
|
149
267
|
text-left
|
|
150
268
|
cursor-pointer
|
|
151
|
-
|
|
269
|
+
:aria-expanded="row.groupBucket.open"
|
|
270
|
+
@click="toggleGroup(row.groupBucket.group.id)"
|
|
152
271
|
>
|
|
153
272
|
<TelaIcon
|
|
154
|
-
:name="groupBucket.folderIcon"
|
|
273
|
+
:name="row.groupBucket.folderIcon"
|
|
155
274
|
size="16px"
|
|
156
275
|
color="icon-secondary"
|
|
157
276
|
shrink-0
|
|
158
277
|
/>
|
|
159
278
|
<div flex items-center gap-6px>
|
|
160
|
-
<span min-w-0 truncate body-14-medium :class="groupBucket.open ? 'text-primary' : 'text-secondary group-hover:text-primary'">
|
|
161
|
-
{{ groupBucket.group.name }}
|
|
279
|
+
<span min-w-0 truncate body-14-medium :class="row.groupBucket.open ? 'text-primary' : 'text-secondary group-hover:text-primary'">
|
|
280
|
+
{{ row.groupBucket.group.name }}
|
|
162
281
|
</span>
|
|
163
|
-
<TelaAnimatedNumber v-if="groupBucket.conversationCountLabel" :value="groupBucket.conversationCountLabel" body-12-medium text-tertiary />
|
|
282
|
+
<TelaAnimatedNumber v-if="row.groupBucket.conversationCountLabel" :value="row.groupBucket.conversationCountLabel" body-12-medium text-tertiary />
|
|
164
283
|
</div>
|
|
165
284
|
</button>
|
|
166
285
|
|
|
@@ -169,7 +288,7 @@ const {
|
|
|
169
288
|
class="opacity-0 group-hover:opacity-100 focus-within:opacity-100 [&:has([data-state=open])]:opacity-100"
|
|
170
289
|
>
|
|
171
290
|
<TelaDropdownMenu
|
|
172
|
-
:items="groupBucket.menuItems"
|
|
291
|
+
:items="row.groupBucket.menuItems"
|
|
173
292
|
content-class="chat-sidebar-floating"
|
|
174
293
|
>
|
|
175
294
|
<button
|
|
@@ -194,82 +313,15 @@ const {
|
|
|
194
313
|
</div>
|
|
195
314
|
</div>
|
|
196
315
|
|
|
197
|
-
<div
|
|
198
|
-
|
|
199
|
-
ml-44px mr-8px mb-4px
|
|
200
|
-
>
|
|
201
|
-
<span text-12px text-red-600 leading-16px aria-live="polite">
|
|
316
|
+
<div v-if="row.type === 'error'" ml-44px mr-8px>
|
|
317
|
+
<span block truncate text-12px text-red-600 leading-16px aria-live="polite" :title="renameGroupError ?? void 0">
|
|
202
318
|
{{ renameGroupError }}
|
|
203
319
|
</span>
|
|
204
320
|
</div>
|
|
205
|
-
|
|
206
|
-
<TelaCollapsible :open="groupBucket.open" @update:open="toggleGroup(groupBucket.group.id)">
|
|
207
|
-
<TelaCollapsibleContent>
|
|
208
|
-
<div flex="~ col" relative>
|
|
209
|
-
<div flex="~ col" gap-1px mt-1px>
|
|
210
|
-
<ChatConversationItem
|
|
211
|
-
v-for="conversation in groupBucket.items"
|
|
212
|
-
:key="conversation.id"
|
|
213
|
-
v-bind="getConversationItemProps(conversation)"
|
|
214
|
-
item-class="ml-24px"
|
|
215
|
-
@delete="handleDelete"
|
|
216
|
-
@rename="handleRename"
|
|
217
|
-
@duplicate="handleDuplicate"
|
|
218
|
-
@export="handleExport"
|
|
219
|
-
@drag-start="handleConversationDragStart"
|
|
220
|
-
@drag-end="clearDragState"
|
|
221
|
-
/>
|
|
222
|
-
<button
|
|
223
|
-
v-if="groupBucket.showNewConversationAction"
|
|
224
|
-
type="button"
|
|
225
|
-
h-32px px-12px
|
|
226
|
-
flex items-center gap-9px
|
|
227
|
-
rounded-10px
|
|
228
|
-
text-left
|
|
229
|
-
hover:bg-subtle
|
|
230
|
-
cursor-pointer
|
|
231
|
-
@click="startConversationInGroup(groupBucket.group.id)"
|
|
232
|
-
>
|
|
233
|
-
<span body-14-medium text-secondary leading-18px ml-24px>
|
|
234
|
-
Nova conversa
|
|
235
|
-
</span>
|
|
236
|
-
<TelaIcon name="i-ph-plus" size="14px" color="icon-secondary" shrink-0 />
|
|
237
|
-
</button>
|
|
238
|
-
<div
|
|
239
|
-
v-else-if="groupBucket.showSearchEmpty"
|
|
240
|
-
h-32px flex items-center ml-36px
|
|
241
|
-
>
|
|
242
|
-
<span body-14-medium text-tertiary>
|
|
243
|
-
Sem resultados
|
|
244
|
-
</span>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
|
|
248
|
-
<div aria-hidden="true" absolute left-17px h-full w-0.5px bg-border pointer-events-none />
|
|
249
|
-
</div>
|
|
250
|
-
</TelaCollapsibleContent>
|
|
251
|
-
</TelaCollapsible>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
<div
|
|
256
|
-
flex="~ col" gap-4px
|
|
257
|
-
rounded-10px
|
|
258
|
-
:class="ungroupedDropTarget ? 'bg-lowered' : ''"
|
|
259
|
-
@dragenter.prevent="handleDragEnter(null)"
|
|
260
|
-
@dragover.prevent="handleDragOver($event, null)"
|
|
261
|
-
@dragleave="handleDragLeave($event, null)"
|
|
262
|
-
@drop.prevent="handleConversationDrop($event, null)"
|
|
263
|
-
>
|
|
264
|
-
<span body-12-medium text-secondary pl-10px pt-4px>
|
|
265
|
-
{{ ungroupedConversationLabel }}
|
|
266
|
-
</span>
|
|
267
|
-
|
|
268
|
-
<div v-if="ungroupedConversations.length > 0" flex="~ col" gap-1px>
|
|
269
321
|
<ChatConversationItem
|
|
270
|
-
v-
|
|
271
|
-
|
|
272
|
-
|
|
322
|
+
v-if="row.type === 'conversation'"
|
|
323
|
+
v-bind="getConversationItemProps(row.conversation)"
|
|
324
|
+
:item-class="row.groupId ? 'ml-24px' : void 0"
|
|
273
325
|
@delete="handleDelete"
|
|
274
326
|
@rename="handleRename"
|
|
275
327
|
@duplicate="handleDuplicate"
|
|
@@ -277,6 +329,29 @@ const {
|
|
|
277
329
|
@drag-start="handleConversationDragStart"
|
|
278
330
|
@drag-end="clearDragState"
|
|
279
331
|
/>
|
|
332
|
+
<button
|
|
333
|
+
v-if="row.type === 'new'"
|
|
334
|
+
type="button"
|
|
335
|
+
h-32px px-12px w-full
|
|
336
|
+
flex items-center gap-9px
|
|
337
|
+
rounded-10px
|
|
338
|
+
text-left
|
|
339
|
+
hover:bg-subtle
|
|
340
|
+
cursor-pointer
|
|
341
|
+
@click="startConversationInGroup(row.groupBucket.group.id)"
|
|
342
|
+
>
|
|
343
|
+
<span body-14-medium text-secondary leading-18px ml-24px>
|
|
344
|
+
Nova conversa
|
|
345
|
+
</span>
|
|
346
|
+
<TelaIcon name="i-ph-plus" size="14px" color="icon-secondary" shrink-0 />
|
|
347
|
+
</button>
|
|
348
|
+
<div v-if="row.type === 'empty'" h-32px flex items-center ml-36px>
|
|
349
|
+
<span body-14-medium text-tertiary>Sem resultados</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div
|
|
352
|
+
v-if="row.groupId && (row.type === 'conversation' || row.type === 'new' || row.type === 'empty')"
|
|
353
|
+
aria-hidden="true" absolute top-0 left-17px h-full w-0.5px bg-border pointer-events-none
|
|
354
|
+
/>
|
|
280
355
|
</div>
|
|
281
356
|
</div>
|
|
282
357
|
</div>
|
|
@@ -9,11 +9,11 @@ type __VLS_Props = {
|
|
|
9
9
|
showCredentialsTab?: boolean;
|
|
10
10
|
};
|
|
11
11
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
12
|
-
"update:open": (value: boolean) => any;
|
|
13
12
|
save: (payload: ChatConfigurationSavePayload) => any;
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
14
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
-
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
15
|
onSave?: ((payload: ChatConfigurationSavePayload) => any) | undefined;
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
17
|
}>, {
|
|
18
18
|
initialTab: ChatConfigurationTab;
|
|
19
19
|
saving: boolean;
|
|
@@ -9,11 +9,11 @@ type __VLS_Props = {
|
|
|
9
9
|
showCredentialsTab?: boolean;
|
|
10
10
|
};
|
|
11
11
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
12
|
-
"update:open": (value: boolean) => any;
|
|
13
12
|
save: (payload: ChatConfigurationSavePayload) => any;
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
14
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
-
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
15
|
onSave?: ((payload: ChatConfigurationSavePayload) => any) | undefined;
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
17
|
}>, {
|
|
18
18
|
initialTab: ChatConfigurationTab;
|
|
19
19
|
saving: boolean;
|