@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 CHANGED
@@ -35,11 +35,13 @@ Runtime internals under `src/runtime/**`, generated Nuxt aliases, and workspace-
35
35
 
36
36
  ## Conversation forks
37
37
 
38
- Hover a completed message (or focus its actions with the keyboard) and choose **Iniciar nova
39
- conversa a partir daqui** from the three-dot menu inside the top-right of either message bubble. Touch devices show the action without requiring
40
- hover. The embed selects the new conversation and emits its existing `update:conversationId` event.
41
- A user-message cutoff starts a response automatically; an assistant-message cutoff waits for input.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.2.0-rc.4",
4
+ "version": "4.2.0-rc.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -15,13 +15,13 @@ type __VLS_Props = {
15
15
  user?: EmbedUser | null;
16
16
  features?: Partial<ChatFeatureConfig>;
17
17
  };
18
- declare var __VLS_11: {}, __VLS_95: any, __VLS_202: any;
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 __VLS_95) => any;
22
+ 'failed-message-actions'?: (props: typeof __VLS_96) => any;
23
23
  } & {
24
- 'failed-message-actions'?: (props: typeof __VLS_202) => any;
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="conversationEvents"
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="conversationEvents"
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: {}, __VLS_95: any, __VLS_202: any;
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 __VLS_95) => any;
22
+ 'failed-message-actions'?: (props: typeof __VLS_96) => any;
23
23
  } & {
24
- 'failed-message-actions'?: (props: typeof __VLS_202) => any;
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, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
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-tertiary text-center>
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, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
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', forkEnabled && 'pr-44px!']"
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
- return [
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "4.2.0-rc.4",
3
+ "version": "4.2.0-rc.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {