@meistrari/chat-nuxt 4.2.0-rc.4 → 4.2.0-rc.5
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 +6 -4
- package/dist/module.json +1 -1
- package/dist/runtime/embed/components/chat-embed-inner.d.vue.ts +3 -3
- package/dist/runtime/embed/components/chat-embed-inner.vue +30 -2
- package/dist/runtime/embed/components/chat-embed-inner.vue.d.ts +3 -3
- package/dist/runtime/messages/components/chat-conversation-event.d.vue.ts +5 -1
- package/dist/runtime/messages/components/chat-conversation-event.vue +25 -4
- package/dist/runtime/messages/components/chat-conversation-event.vue.d.ts +5 -1
- package/dist/runtime/messages/components/chat-message-bubble.vue +7 -18
- package/dist/runtime/messages/components/chat-message-list.d.vue.ts +2 -0
- package/dist/runtime/messages/components/chat-message-list.vue +2 -1
- package/dist/runtime/messages/components/chat-message-list.vue.d.ts +2 -0
- package/dist/runtime/messages/conversation-timeline.d.ts +5 -0
- package/dist/runtime/messages/conversation-timeline.js +23 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,11 +35,13 @@ Runtime internals under `src/runtime/**`, generated Nuxt aliases, and workspace-
|
|
|
35
35
|
|
|
36
36
|
## Conversation forks
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
Completed assistant responses offer **Iniciar nova conversa a partir daqui** in the three-dot
|
|
39
|
+
menu below the response, beside the feedback controls. User messages cannot be forked.
|
|
40
|
+
The embed selects the new conversation and emits its existing `update:conversationId` event.
|
|
41
|
+
The new conversation stays idle until the user sends a message.
|
|
42
42
|
Fork titles use `<original title> - Cópia <number>` and remain intact when the first response completes.
|
|
43
|
+
New forks show a separator after the copied response with a link to the immediate source conversation.
|
|
44
|
+
Existing forks do not gain this separator.
|
|
43
45
|
|
|
44
46
|
This feature requires the matching Chat API fork endpoint described in
|
|
45
47
|
[Conversation forks](../chat-api/README.md#conversation-forks). History, tool data and file references
|
package/dist/module.json
CHANGED
|
@@ -15,13 +15,13 @@ type __VLS_Props = {
|
|
|
15
15
|
user?: EmbedUser | null;
|
|
16
16
|
features?: Partial<ChatFeatureConfig>;
|
|
17
17
|
};
|
|
18
|
-
declare var __VLS_11: {},
|
|
18
|
+
declare var __VLS_11: {}, __VLS_96: any, __VLS_204: any;
|
|
19
19
|
type __VLS_Slots = {} & {
|
|
20
20
|
'sidebar-bottom'?: (props: typeof __VLS_11) => any;
|
|
21
21
|
} & {
|
|
22
|
-
'failed-message-actions'?: (props: typeof
|
|
22
|
+
'failed-message-actions'?: (props: typeof __VLS_96) => any;
|
|
23
23
|
} & {
|
|
24
|
-
'failed-message-actions'?: (props: typeof
|
|
24
|
+
'failed-message-actions'?: (props: typeof __VLS_204) => any;
|
|
25
25
|
};
|
|
26
26
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
27
|
"update:conversationId": (value: string | null) => any;
|
|
@@ -783,6 +783,32 @@ async function handleShare() {
|
|
|
783
783
|
statusToast.update({ text: "Erro ao copiar link", icon: "i-ph-warning" });
|
|
784
784
|
}
|
|
785
785
|
}
|
|
786
|
+
const forkEvents = shallowRef([]);
|
|
787
|
+
const displayConversationEvents = computed(() => [...new Map([
|
|
788
|
+
...forkEvents.value,
|
|
789
|
+
...props.conversationEvents ?? []
|
|
790
|
+
].filter((event) => event.conversationId === currentConversation.value?.id).map((event) => [event.id, event])).values()]);
|
|
791
|
+
watch([
|
|
792
|
+
() => currentConversation.value?.id,
|
|
793
|
+
() => JSON.stringify(chatApi.withChatHeaders().headers)
|
|
794
|
+
], async ([conversationId], _previous, onCleanup) => {
|
|
795
|
+
forkEvents.value = [];
|
|
796
|
+
if (!conversationId || !chatApi.conversationV2Enabled)
|
|
797
|
+
return;
|
|
798
|
+
const controller = new AbortController();
|
|
799
|
+
onCleanup(() => controller.abort());
|
|
800
|
+
try {
|
|
801
|
+
const { events } = await $fetch(
|
|
802
|
+
chatApi.path(`/conversations/${conversationId}/events`),
|
|
803
|
+
chatApi.withChatHeaders({ signal: controller.signal })
|
|
804
|
+
);
|
|
805
|
+
if (!controller.signal.aborted)
|
|
806
|
+
forkEvents.value = events.filter((event) => event.type === "conversation_forked");
|
|
807
|
+
} catch {
|
|
808
|
+
if (!controller.signal.aborted)
|
|
809
|
+
statusToast.update({ text: "Erro ao carregar a origem da conversa", icon: "i-ph-warning" });
|
|
810
|
+
}
|
|
811
|
+
}, { immediate: true });
|
|
786
812
|
const forking = ref(false);
|
|
787
813
|
async function handleFork(messageId) {
|
|
788
814
|
const sourceId = currentConversation.value?.id;
|
|
@@ -1075,7 +1101,7 @@ async function handleDelete() {
|
|
|
1075
1101
|
|
|
1076
1102
|
<ChatMessageList
|
|
1077
1103
|
:messages="messages"
|
|
1078
|
-
:events="
|
|
1104
|
+
:events="displayConversationEvents"
|
|
1079
1105
|
:streaming-active-message-id="streamingActiveMessageId"
|
|
1080
1106
|
:preparing-steps="preparingSteps"
|
|
1081
1107
|
:show-ttft="resolvedFeatures.showTtft"
|
|
@@ -1083,6 +1109,7 @@ async function handleDelete() {
|
|
|
1083
1109
|
:fork-enabled="chatApi.conversationV2Enabled"
|
|
1084
1110
|
:fork-disabled="forking"
|
|
1085
1111
|
@fork="handleFork"
|
|
1112
|
+
@open-conversation="openConversation"
|
|
1086
1113
|
@retry="handleRetry"
|
|
1087
1114
|
>
|
|
1088
1115
|
<template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
|
|
@@ -1288,7 +1315,7 @@ async function handleDelete() {
|
|
|
1288
1315
|
>
|
|
1289
1316
|
<ChatMessageList
|
|
1290
1317
|
:messages="messages"
|
|
1291
|
-
:events="
|
|
1318
|
+
:events="displayConversationEvents"
|
|
1292
1319
|
:streaming-active-message-id="streamingActiveMessageId"
|
|
1293
1320
|
:preparing-steps="preparingSteps"
|
|
1294
1321
|
:show-ttft="resolvedFeatures.showTtft"
|
|
@@ -1296,6 +1323,7 @@ async function handleDelete() {
|
|
|
1296
1323
|
:fork-enabled="chatApi.conversationV2Enabled"
|
|
1297
1324
|
:fork-disabled="forking"
|
|
1298
1325
|
@fork="handleFork"
|
|
1326
|
+
@open-conversation="openConversation"
|
|
1299
1327
|
@retry="handleRetry"
|
|
1300
1328
|
>
|
|
1301
1329
|
<template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
|
|
@@ -15,13 +15,13 @@ type __VLS_Props = {
|
|
|
15
15
|
user?: EmbedUser | null;
|
|
16
16
|
features?: Partial<ChatFeatureConfig>;
|
|
17
17
|
};
|
|
18
|
-
declare var __VLS_11: {},
|
|
18
|
+
declare var __VLS_11: {}, __VLS_96: any, __VLS_204: any;
|
|
19
19
|
type __VLS_Slots = {} & {
|
|
20
20
|
'sidebar-bottom'?: (props: typeof __VLS_11) => any;
|
|
21
21
|
} & {
|
|
22
|
-
'failed-message-actions'?: (props: typeof
|
|
22
|
+
'failed-message-actions'?: (props: typeof __VLS_96) => any;
|
|
23
23
|
} & {
|
|
24
|
-
'failed-message-actions'?: (props: typeof
|
|
24
|
+
'failed-message-actions'?: (props: typeof __VLS_204) => any;
|
|
25
25
|
};
|
|
26
26
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
27
|
"update:conversationId": (value: string | null) => any;
|
|
@@ -2,6 +2,10 @@ import type { ConversationEvent } from '../../types/schemas/chat/conversations.j
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
event: ConversationEvent;
|
|
4
4
|
};
|
|
5
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
|
+
openConversation: (conversationId: string) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onOpenConversation?: ((conversationId: string) => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
10
|
declare const _default: typeof __VLS_export;
|
|
7
11
|
export default _default;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { getConversationForkSource } from "../conversation-timeline";
|
|
2
3
|
const props = defineProps({
|
|
3
4
|
event: { type: null, required: true }
|
|
4
5
|
});
|
|
6
|
+
const emit = defineEmits(["openConversation"]);
|
|
7
|
+
const route = useRoute();
|
|
8
|
+
const forkSource = computed(() => getConversationForkSource(props.event));
|
|
9
|
+
const sourceLocation = computed(() => ({
|
|
10
|
+
path: route.path,
|
|
11
|
+
query: { ...route.query, conversation: void 0, conversationId: forkSource.value?.sourceConversationId },
|
|
12
|
+
hash: route.hash
|
|
13
|
+
}));
|
|
5
14
|
const eventDate = computed(() => new Date(props.event.createdAt));
|
|
6
15
|
const eventTime = computed(() => new Intl.DateTimeFormat("pt-BR", {
|
|
7
16
|
hour: "2-digit",
|
|
@@ -16,16 +25,28 @@ const eventTime = computed(() => new Intl.DateTimeFormat("pt-BR", {
|
|
|
16
25
|
items-center
|
|
17
26
|
gap-12px
|
|
18
27
|
py-6px
|
|
19
|
-
:aria-label="`${event.actorLabel ? `${event.actorLabel} ` : ''}${event.description}, ${eventTime}`"
|
|
28
|
+
:aria-label="forkSource ? `Conversa iniciada a partir de ${forkSource.sourceTitle}` : `${event.actorLabel ? `${event.actorLabel} ` : ''}${event.description}, ${eventTime}`"
|
|
20
29
|
>
|
|
21
|
-
<span h-0 flex-1 border-t-0.5px border />
|
|
22
|
-
<p body-12-regular text-
|
|
30
|
+
<span h-0 flex-1 border-t-0.5px border :class="forkSource && 'min-w-24px'" />
|
|
31
|
+
<p v-if="forkSource" body-12-regular text-secondary text-center min-w-0>
|
|
32
|
+
Conversa iniciada a partir de
|
|
33
|
+
<NuxtLink
|
|
34
|
+
:to="sourceLocation"
|
|
35
|
+
body-12-medium
|
|
36
|
+
underline
|
|
37
|
+
break-words
|
|
38
|
+
@click.exact.prevent="emit('openConversation', forkSource.sourceConversationId)"
|
|
39
|
+
>
|
|
40
|
+
{{ forkSource.sourceTitle }}
|
|
41
|
+
</NuxtLink>
|
|
42
|
+
</p>
|
|
43
|
+
<p v-else body-12-regular text-tertiary text-center>
|
|
23
44
|
<span v-if="event.actorLabel" body-12-medium text-secondary>{{ event.actorLabel }}</span>
|
|
24
45
|
{{ event.description }}
|
|
25
46
|
<time :datetime="eventDate.toISOString()" :title="eventDate.toLocaleString('pt-BR')">
|
|
26
47
|
· {{ eventTime }}
|
|
27
48
|
</time>
|
|
28
49
|
</p>
|
|
29
|
-
<span h-0 flex-1 border-t-0.5px border />
|
|
50
|
+
<span h-0 flex-1 border-t-0.5px border :class="forkSource && 'min-w-24px'" />
|
|
30
51
|
</div>
|
|
31
52
|
</template>
|
|
@@ -2,6 +2,10 @@ import type { ConversationEvent } from '../../types/schemas/chat/conversations.j
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
event: ConversationEvent;
|
|
4
4
|
};
|
|
5
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
|
+
openConversation: (conversationId: string) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onOpenConversation?: ((conversationId: string) => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
10
|
declare const _default: typeof __VLS_export;
|
|
7
11
|
export default _default;
|
|
@@ -137,14 +137,8 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
137
137
|
variant="contained"
|
|
138
138
|
align="end"
|
|
139
139
|
class="chat-message-bubble__content relative"
|
|
140
|
-
:class="[isPending && 'op-80'
|
|
140
|
+
:class="[isPending && 'op-80']"
|
|
141
141
|
>
|
|
142
|
-
<ChatMessageActions
|
|
143
|
-
v-if="forkEnabled && message.status === 'completed' && !streamActive"
|
|
144
|
-
class="chat-message-bubble__actions absolute right-8px top-8px"
|
|
145
|
-
:disabled="forkDisabled"
|
|
146
|
-
@fork="emit('fork', message.id)"
|
|
147
|
-
/>
|
|
148
142
|
<div v-if="hasFiles" flex="~ col" gap-4px mb-8px>
|
|
149
143
|
<a
|
|
150
144
|
v-for="file in message.files"
|
|
@@ -204,16 +198,9 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
204
198
|
max-w-prose
|
|
205
199
|
class="chat-message-bubble__content relative"
|
|
206
200
|
:class="[
|
|
207
|
-
forkEnabled && 'pr-36px! min-h-24px',
|
|
208
201
|
isFailed ? 'flex gap-12px items-start px-12px py-10px bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px'
|
|
209
202
|
]"
|
|
210
203
|
>
|
|
211
|
-
<ChatMessageActions
|
|
212
|
-
v-if="forkEnabled && message.status === 'completed' && !streamActive"
|
|
213
|
-
class="chat-message-bubble__actions absolute right-0 top-0"
|
|
214
|
-
:disabled="forkDisabled"
|
|
215
|
-
@fork="emit('fork', message.id)"
|
|
216
|
-
/>
|
|
217
204
|
<template v-if="isFailed">
|
|
218
205
|
<div i-ph-warning-circle-fill text-16px text-red-600 shrink-0 mt-2px />
|
|
219
206
|
<div flex flex-col gap-4px grow>
|
|
@@ -301,6 +288,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
301
288
|
</ClientOnly>
|
|
302
289
|
<div
|
|
303
290
|
v-if="!isPending"
|
|
291
|
+
data-chat-message-footer
|
|
304
292
|
mt-8px
|
|
305
293
|
flex
|
|
306
294
|
items-center
|
|
@@ -315,6 +303,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
315
303
|
{{ formattedTtft }}
|
|
316
304
|
</span>
|
|
317
305
|
<ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
|
|
306
|
+
<ChatMessageActions
|
|
307
|
+
v-if="forkEnabled && message.role === 'assistant' && message.status === 'completed' && !streamActive"
|
|
308
|
+
:disabled="forkDisabled"
|
|
309
|
+
@fork="emit('fork', message.id)"
|
|
310
|
+
/>
|
|
318
311
|
</div>
|
|
319
312
|
</div>
|
|
320
313
|
</template>
|
|
@@ -322,7 +315,3 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
322
315
|
</template>
|
|
323
316
|
</TelaChatMessage>
|
|
324
317
|
</template>
|
|
325
|
-
|
|
326
|
-
<style scoped>
|
|
327
|
-
@media (hover:hover) and (pointer:fine){.chat-message-bubble__actions{opacity:0}.chat-message-bubble:focus-within .chat-message-bubble__actions,.chat-message-bubble:hover .chat-message-bubble__actions,.chat-message-bubble__actions:has([data-state=open]){opacity:1}}
|
|
328
|
-
</style>
|
|
@@ -25,9 +25,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
25
25
|
scrollToBottom: any;
|
|
26
26
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
27
|
retry: (messageId: string) => any;
|
|
28
|
+
openConversation: (conversationId: string) => any;
|
|
28
29
|
fork: (messageId: string) => any;
|
|
29
30
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
30
31
|
onRetry?: ((messageId: string) => any) | undefined;
|
|
32
|
+
onOpenConversation?: ((conversationId: string) => any) | undefined;
|
|
31
33
|
onFork?: ((messageId: string) => any) | undefined;
|
|
32
34
|
}>, {
|
|
33
35
|
showTtft: boolean;
|
|
@@ -12,7 +12,7 @@ const props = defineProps({
|
|
|
12
12
|
forkEnabled: { type: Boolean, required: false },
|
|
13
13
|
forkDisabled: { type: Boolean, required: false }
|
|
14
14
|
});
|
|
15
|
-
const emit = defineEmits(["retry", "fork"]);
|
|
15
|
+
const emit = defineEmits(["retry", "fork", "openConversation"]);
|
|
16
16
|
defineSlots();
|
|
17
17
|
const conversationRef = ref(null);
|
|
18
18
|
const conversationElements = computed(() => {
|
|
@@ -102,6 +102,7 @@ defineExpose({
|
|
|
102
102
|
<ChatConversationEvent
|
|
103
103
|
v-if="item.kind === 'event'"
|
|
104
104
|
:event="item.value"
|
|
105
|
+
@open-conversation="emit('openConversation', $event)"
|
|
105
106
|
/>
|
|
106
107
|
<ChatMessageBubble
|
|
107
108
|
v-else
|
|
@@ -25,9 +25,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
25
25
|
scrollToBottom: any;
|
|
26
26
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
27
27
|
retry: (messageId: string) => any;
|
|
28
|
+
openConversation: (conversationId: string) => any;
|
|
28
29
|
fork: (messageId: string) => any;
|
|
29
30
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
30
31
|
onRetry?: ((messageId: string) => any) | undefined;
|
|
32
|
+
onOpenConversation?: ((conversationId: string) => any) | undefined;
|
|
31
33
|
onFork?: ((messageId: string) => any) | undefined;
|
|
32
34
|
}>, {
|
|
33
35
|
showTtft: boolean;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ConversationEvent, Message } from '../types/schemas/chat/conversations.js';
|
|
2
|
+
export declare function getConversationForkSource(event: ConversationEvent): {
|
|
3
|
+
sourceConversationId: string;
|
|
4
|
+
sourceTitle: string;
|
|
5
|
+
copiedMessageId: string;
|
|
6
|
+
} | null;
|
|
2
7
|
export type ConversationTimelineItem = {
|
|
3
8
|
kind: 'event';
|
|
4
9
|
id: string;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const forkSourceSchema = z.object({
|
|
3
|
+
sourceConversationId: z.string().uuid(),
|
|
4
|
+
sourceTitle: z.string(),
|
|
5
|
+
copiedMessageId: z.string().uuid()
|
|
6
|
+
});
|
|
7
|
+
export function getConversationForkSource(event) {
|
|
8
|
+
if (event.type !== "conversation_forked")
|
|
9
|
+
return null;
|
|
10
|
+
const parsed = forkSourceSchema.safeParse(event.payload);
|
|
11
|
+
return parsed.success ? parsed.data : null;
|
|
12
|
+
}
|
|
1
13
|
export function mergeConversationTimeline(messages, events = []) {
|
|
2
|
-
|
|
3
|
-
...(events ?? []).map((event) => ({ kind: "event", id: event.id, createdAt: event.createdAt, value: event })),
|
|
14
|
+
const items = [
|
|
15
|
+
...(events ?? []).filter((event) => event.type !== "conversation_forked").map((event) => ({ kind: "event", id: event.id, createdAt: event.createdAt, value: event })),
|
|
4
16
|
...messages.map((message) => ({ kind: "message", id: message.id, createdAt: message.createdAt, value: message }))
|
|
5
17
|
].sort((left, right) => {
|
|
6
18
|
const difference = new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();
|
|
@@ -10,4 +22,13 @@ export function mergeConversationTimeline(messages, events = []) {
|
|
|
10
22
|
return left.kind === "event" ? -1 : 1;
|
|
11
23
|
return left.id.localeCompare(right.id);
|
|
12
24
|
});
|
|
25
|
+
for (const event of events ?? []) {
|
|
26
|
+
const source = getConversationForkSource(event);
|
|
27
|
+
if (!source)
|
|
28
|
+
continue;
|
|
29
|
+
const index = items.findIndex((item) => item.kind === "message" && item.id === source.copiedMessageId);
|
|
30
|
+
if (index !== -1)
|
|
31
|
+
items.splice(index + 1, 0, { kind: "event", id: event.id, createdAt: event.createdAt, value: event });
|
|
32
|
+
}
|
|
33
|
+
return items;
|
|
13
34
|
}
|