@meistrari/chat-nuxt 4.1.0-rc.1 → 4.1.0-rc.3

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
@@ -45,6 +45,8 @@ bunx --bun typedoc --entryPoints typedoc.ts --out /tmp/chat-nuxt-api --skipError
45
45
 
46
46
  The table below is a quick index for reading in-repo. **When you change a prop, update its JSDoc and this row together.**
47
47
 
48
+ The embed automatically sets the browser tab title to the active conversation title and synchronizes its ID to `?conversationId=` using router replacement, preserving other query parameters and the URL fragment. Starting a new conversation removes the conversation parameter. Opening or reloading that URL restores the conversation; existing `?conversation=` links are also accepted and converted to `?conversationId=`. Explicit `conversationId` and `initialConversationId` props take precedence over the URL on mount. Without a conversation title, the host application controls the tab title.
49
+
48
50
  ### `<MeistrariChatEmbed>` props
49
51
 
50
52
  Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload types are importable from the package root or `@meistrari/chat-nuxt/types/embed`.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.1.0-rc.1",
4
+ "version": "4.1.0-rc.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -13,7 +13,8 @@ import ChatMessageInput from "../../messages/components/chat-message-input.vue";
13
13
  import { normalizeConversationId } from "../../conversations/scope";
14
14
  import { resolveChatFeatures } from "../feature-config";
15
15
  import { resolveConversationCreator } from "../../conversations/creator";
16
- import { buildConversationShareUrl } from "../conversation-share-url";
16
+ import { buildConversationShareUrl, getSharedConversationId } from "../conversation-share-url";
17
+ import { useConversationBrowserState } from "../composables/conversation-browser-state";
17
18
  import {
18
19
  resolveTelaAgentInputs,
19
20
  useChat
@@ -133,7 +134,7 @@ const {
133
134
  closePreview
134
135
  } = useFilePreviewPanel();
135
136
  const { isOpen: isCitationPanelOpen } = useCitationPanel();
136
- const sharedConversationId = typeof route.query.conversation === "string" ? route.query.conversation : null;
137
+ const sharedConversationId = getSharedConversationId(route.query);
137
138
  const activeConversationId = ref(
138
139
  normalizeConversationId(props.conversationId) ?? normalizeConversationId(props.initialConversationId) ?? normalizeConversationId(sharedConversationId)
139
140
  );
@@ -492,6 +493,11 @@ function dismissOutdatedBanner() {
492
493
  }
493
494
  }
494
495
  const optimisticTitle = ref(null);
496
+ useConversationBrowserState(activeConversationId, computed(() => {
497
+ if (currentConversation.value?.id !== activeConversationId.value)
498
+ return null;
499
+ return optimisticTitle.value ?? currentConversation.value?.title ?? null;
500
+ }));
495
501
  const displayTitle = computed(() => {
496
502
  if (optimisticTitle.value !== null)
497
503
  return optimisticTitle.value;
@@ -0,0 +1,2 @@
1
+ import type { Ref } from 'vue';
2
+ export declare function useConversationBrowserState(conversationId: Ref<string | null>, title: Ref<string | null>): void;
@@ -0,0 +1,18 @@
1
+ export function useConversationBrowserState(conversationId, title) {
2
+ const route = useRoute();
3
+ const router = useRouter();
4
+ useHead(computed(() => title.value ? { title: title.value } : {}));
5
+ onMounted(() => {
6
+ watch(conversationId, async (id) => {
7
+ if (route.query.conversationId === (id ?? void 0) && route.query.conversation === void 0)
8
+ return;
9
+ const query = { ...route.query };
10
+ delete query.conversation;
11
+ if (id)
12
+ query.conversationId = id;
13
+ else
14
+ delete query.conversationId;
15
+ await router.replace({ path: route.path, query, hash: route.hash });
16
+ }, { immediate: true });
17
+ });
18
+ }
@@ -1 +1,2 @@
1
+ export declare function getSharedConversationId(query: Record<string, unknown>): string | null;
1
2
  export declare function buildConversationShareUrl(currentHref: string, conversationId: string): string;
@@ -1,5 +1,10 @@
1
+ import { normalizeConversationId } from "../conversations/scope.js";
2
+ export function getSharedConversationId(query) {
3
+ return normalizeConversationId(typeof query.conversationId === "string" ? query.conversationId : null) ?? normalizeConversationId(typeof query.conversation === "string" ? query.conversation : null);
4
+ }
1
5
  export function buildConversationShareUrl(currentHref, conversationId) {
2
6
  const shareUrl = new URL(currentHref);
3
- shareUrl.searchParams.set("conversation", conversationId);
7
+ shareUrl.searchParams.delete("conversation");
8
+ shareUrl.searchParams.set("conversationId", conversationId);
4
9
  return shareUrl.href;
5
10
  }
@@ -184,7 +184,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
184
184
  :reasoning-data="message.reasoningData ?? []"
185
185
  :completed="!isPending"
186
186
  :preparing-steps="preparingSteps"
187
- :created-at="message.createdAt"
187
+ :started-at="message.generationStartedAt ?? message.createdAt"
188
188
  :updated-at="message.updatedAt"
189
189
  />
190
190
 
@@ -2,7 +2,7 @@ import { type ReasoningStep } from '../../reasoning.js';
2
2
  type __VLS_Props = {
3
3
  reasoningData: ReasoningStep[];
4
4
  completed: boolean;
5
- createdAt: Date | string;
5
+ startedAt: Date | string;
6
6
  updatedAt?: Date | string | null;
7
7
  preparingSteps?: readonly string[] | null;
8
8
  };
@@ -1,20 +1,23 @@
1
1
  <script setup>
2
2
  import { resolveDisplayedReasoning } from "../../reasoning";
3
+ import { useReasoningTimer } from "../../composables/reasoning-timer";
3
4
  import { resolveToolTitle } from "../../tool-registry";
4
5
  import ChatReasoningCollapsibleContent from "./chat-reasoning-collapsible-content.vue";
5
6
  import ChatReasoningSteps from "./chat-reasoning-steps.vue";
6
7
  const props = defineProps({
7
8
  reasoningData: { type: Array, required: true },
8
9
  completed: { type: Boolean, required: true },
9
- createdAt: { type: [Date, String], required: true },
10
+ startedAt: { type: [Date, String], required: true },
10
11
  updatedAt: { type: [Date, String, null], required: false },
11
12
  preparingSteps: { type: [Array, null], required: false }
12
13
  });
13
14
  const TEXT_TRUNCATE_LENGTH = 40;
14
15
  const isExpanded = ref(false);
15
- const elapsedTime = ref(0);
16
- let timerInterval;
17
- const storageKey = computed(() => `reasoning-timer-${new Date(props.createdAt).getTime()}`);
16
+ const { elapsedTime, thinkingDuration } = useReasoningTimer({
17
+ startedAt: computed(() => props.startedAt),
18
+ updatedAt: computed(() => props.updatedAt),
19
+ completed: computed(() => props.completed)
20
+ });
18
21
  const displayedReasoning = computed(
19
22
  () => resolveDisplayedReasoning(props.reasoningData, props.preparingSteps)
20
23
  );
@@ -37,14 +40,6 @@ const currentStepName = computed(() => {
37
40
  }
38
41
  return "Trabalhando";
39
42
  });
40
- const thinkingDuration = computed(() => {
41
- if (!props.updatedAt)
42
- return null;
43
- const start = new Date(props.createdAt).getTime();
44
- const end = new Date(props.updatedAt).getTime();
45
- const duration = end - start;
46
- return Number.isFinite(duration) && duration >= 0 ? duration : null;
47
- });
48
43
  const formattedDuration = computed(() => {
49
44
  if (!thinkingDuration.value)
50
45
  return null;
@@ -61,37 +56,6 @@ const liveFormattedDuration = computed(() => {
61
56
  const seconds = Math.round(elapsedTime.value / 1e3);
62
57
  return seconds > 0 ? `${seconds}s` : "";
63
58
  });
64
- watch(() => props.completed, (isCompleted) => {
65
- if (!isCompleted && !timerInterval) {
66
- const startTime = new Date(props.createdAt).getTime();
67
- if (typeof window !== "undefined") {
68
- localStorage.setItem(storageKey.value, startTime.toString());
69
- }
70
- elapsedTime.value = Date.now() - startTime;
71
- timerInterval = setInterval(() => {
72
- elapsedTime.value = Date.now() - startTime;
73
- }, 1e3);
74
- } else if (isCompleted && timerInterval) {
75
- clearInterval(timerInterval);
76
- timerInterval = void 0;
77
- if (typeof window !== "undefined") {
78
- localStorage.removeItem(storageKey.value);
79
- }
80
- }
81
- }, { immediate: true });
82
- onMounted(() => {
83
- if (!props.completed && typeof window !== "undefined") {
84
- const storedStartTime = localStorage.getItem(storageKey.value);
85
- if (storedStartTime) {
86
- const startTime = Number.parseInt(storedStartTime, 10);
87
- elapsedTime.value = Date.now() - startTime;
88
- }
89
- }
90
- });
91
- onUnmounted(() => {
92
- if (timerInterval)
93
- clearInterval(timerInterval);
94
- });
95
59
  </script>
96
60
 
97
61
  <template>
@@ -2,7 +2,7 @@ import { type ReasoningStep } from '../../reasoning.js';
2
2
  type __VLS_Props = {
3
3
  reasoningData: ReasoningStep[];
4
4
  completed: boolean;
5
- createdAt: Date | string;
5
+ startedAt: Date | string;
6
6
  updatedAt?: Date | string | null;
7
7
  preparingSteps?: readonly string[] | null;
8
8
  };
@@ -495,6 +495,7 @@ export function useChat(options = {}) {
495
495
  ttft: null,
496
496
  createdBy: authUser.value?.email ?? null,
497
497
  createdAt: /* @__PURE__ */ new Date(),
498
+ generationStartedAt: null,
498
499
  updatedAt: null
499
500
  };
500
501
  const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? {
@@ -509,6 +510,7 @@ export function useChat(options = {}) {
509
510
  ttft: null,
510
511
  createdBy: null,
511
512
  createdAt: /* @__PURE__ */ new Date(),
513
+ generationStartedAt: /* @__PURE__ */ new Date(),
512
514
  updatedAt: null
513
515
  };
514
516
  streamingMessageId.value = optimisticAssistantMessage.id;
@@ -873,10 +875,13 @@ export function useChat(options = {}) {
873
875
  messages.value = retry.messages;
874
876
  streamingMessageId.value = messageId;
875
877
  directStream.beginMessage();
876
- await $fetch(
878
+ const retriedMessage = await $fetch(
877
879
  chatApi.path(`/conversations/${conversationId}/messages/${messageId}/retry`),
878
880
  chatApi.withChatHeaders({ method: "POST" })
879
881
  );
882
+ if (currentConversation.value?.id !== conversationId)
883
+ return true;
884
+ messages.value = messages.value.map((message) => message.id === messageId ? { ...message, ...retriedMessage } : message);
880
885
  startPolling();
881
886
  return true;
882
887
  } catch (err) {
@@ -0,0 +1,9 @@
1
+ import { type Ref } from 'vue';
2
+ export declare function useReasoningTimer(options: {
3
+ startedAt: Readonly<Ref<Date | string>>;
4
+ updatedAt: Readonly<Ref<Date | string | null | undefined>>;
5
+ completed: Readonly<Ref<boolean>>;
6
+ }): {
7
+ elapsedTime: Ref<number, number>;
8
+ thinkingDuration: import("vue").ComputedRef<number | null>;
9
+ };
@@ -0,0 +1,22 @@
1
+ import { computed, ref, watch } from "vue";
2
+ export function useReasoningTimer(options) {
3
+ const elapsedTime = ref(0);
4
+ const startTime = computed(() => new Date(options.startedAt.value).getTime());
5
+ const thinkingDuration = computed(() => {
6
+ if (!options.updatedAt.value)
7
+ return null;
8
+ const duration = new Date(options.updatedAt.value).getTime() - startTime.value;
9
+ return Number.isFinite(duration) && duration >= 0 ? duration : null;
10
+ });
11
+ watch([startTime, options.completed], ([start, completed], _previous, onCleanup) => {
12
+ if (completed)
13
+ return;
14
+ const update = () => {
15
+ elapsedTime.value = Number.isFinite(start) ? Math.max(0, Date.now() - start) : 0;
16
+ };
17
+ update();
18
+ const interval = setInterval(update, 1e3);
19
+ onCleanup(() => clearInterval(interval));
20
+ }, { immediate: true });
21
+ return { elapsedTime, thinkingDuration };
22
+ }
@@ -88,6 +88,7 @@ function message(baseTime, offsetMs, input) {
88
88
  files: null,
89
89
  externalUuid: null,
90
90
  ttft: null,
91
+ generationStartedAt: null,
91
92
  createdBy: input.role === "user" ? "debugger@tela.com" : null,
92
93
  createdAt,
93
94
  updatedAt: input.updatedAt ?? createdAt,
@@ -31,6 +31,7 @@ function message(baseTime, offsetMs, input) {
31
31
  files: null,
32
32
  externalUuid: null,
33
33
  ttft: null,
34
+ generationStartedAt: null,
34
35
  createdBy: input.role === "user" ? "debugger@tela.com" : null,
35
36
  createdAt,
36
37
  updatedAt: createdAt,
@@ -37,7 +37,9 @@ export function beginAssistantMessageRetry(messages, messageId) {
37
37
  content: "",
38
38
  status: "pending",
39
39
  failReason: null,
40
- reasoningData: null
40
+ reasoningData: null,
41
+ generationStartedAt: /* @__PURE__ */ new Date(),
42
+ updatedAt: null
41
43
  };
42
44
  })
43
45
  };
@@ -114,6 +114,7 @@ export declare const sendMessageV2ResponseSchema: z.ZodObject<{
114
114
  createdBy: z.ZodNullable<z.ZodString>;
115
115
  createdAt: z.ZodDate;
116
116
  updatedAt: z.ZodNullable<z.ZodDate>;
117
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
117
118
  }, z.core.$strip>;
118
119
  assistantMessage: z.ZodObject<{
119
120
  id: z.ZodString;
@@ -146,6 +147,7 @@ export declare const sendMessageV2ResponseSchema: z.ZodObject<{
146
147
  createdBy: z.ZodNullable<z.ZodString>;
147
148
  createdAt: z.ZodDate;
148
149
  updatedAt: z.ZodNullable<z.ZodDate>;
150
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
149
151
  }, z.core.$strip>;
150
152
  turn: z.ZodNullable<z.ZodObject<{
151
153
  id: z.ZodString;
@@ -110,6 +110,7 @@ export declare const messageSchema: z.ZodObject<{
110
110
  createdBy: z.ZodNullable<z.ZodString>;
111
111
  createdAt: z.ZodDate;
112
112
  updatedAt: z.ZodNullable<z.ZodDate>;
113
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
113
114
  }, z.core.$strip>;
114
115
  export type Message = z.infer<typeof messageSchema>;
115
116
  export declare const conversationCreatorFilterCountsSchema: z.ZodRecord<z.ZodEnum<{
@@ -222,6 +223,7 @@ export declare const conversationWithMessagesSchema: z.ZodObject<{
222
223
  createdBy: z.ZodNullable<z.ZodString>;
223
224
  createdAt: z.ZodDate;
224
225
  updatedAt: z.ZodNullable<z.ZodDate>;
226
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
225
227
  }, z.core.$strip>>;
226
228
  }, z.core.$strip>;
227
229
  export type ConversationWithMessages = z.infer<typeof conversationWithMessagesSchema>;
@@ -49,6 +49,7 @@ export const messageSchema = z.object({
49
49
  createdBy: z.string().nullable(),
50
50
  createdAt: z.date(),
51
51
  updatedAt: z.date().nullable(),
52
+ generationStartedAt: z.date().nullable(),
52
53
  });
53
54
  export const conversationCreatorFilterCountsSchema = z.record(conversationCreatorFilterSchema, z.number());
54
55
  export const conversationListMetadataSchema = z.object({
@@ -119,6 +119,7 @@ export declare const sendMessageResponseSchema: z.ZodObject<{
119
119
  createdBy: z.ZodNullable<z.ZodString>;
120
120
  createdAt: z.ZodDate;
121
121
  updatedAt: z.ZodNullable<z.ZodDate>;
122
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
122
123
  }, z.core.$strip>;
123
124
  assistantMessage: z.ZodObject<{
124
125
  id: z.ZodString;
@@ -151,6 +152,7 @@ export declare const sendMessageResponseSchema: z.ZodObject<{
151
152
  createdBy: z.ZodNullable<z.ZodString>;
152
153
  createdAt: z.ZodDate;
153
154
  updatedAt: z.ZodNullable<z.ZodDate>;
155
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
154
156
  }, z.core.$strip>;
155
157
  }, z.core.$strip>;
156
158
  export type SendMessageResponse = z.infer<typeof sendMessageResponseSchema>;
@@ -186,6 +188,7 @@ export declare const messagePollResponseSchema: z.ZodObject<{
186
188
  createdBy: z.ZodNullable<z.ZodString>;
187
189
  createdAt: z.ZodDate;
188
190
  updatedAt: z.ZodNullable<z.ZodDate>;
191
+ generationStartedAt: z.ZodNullable<z.ZodDate>;
189
192
  }, z.core.$strip>>;
190
193
  newFiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
191
194
  url: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "4.1.0-rc.1",
3
+ "version": "4.1.0-rc.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {