@meistrari/chat-nuxt 4.1.0-rc.0 → 4.1.0-rc.2

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
@@ -72,7 +72,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
72
72
  | `labels` | `Partial<ChatLabels>` | Portuguese defaults | Override conversation-search text; see [UI labels](docs/guides/embedding.md#ui-labels) for supported keys and surfaces |
73
73
  | `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name. Components receive `node` plus the parser lifecycle props `loading` and `autoClosed` |
74
74
  | `customHtmlTags` | `readonly string[]` | `undefined` | Extra HTML tags allowed by the markdown renderer |
75
- | `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant messages (see [Citations](docs/guides/citations.md)) |
75
+ | `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant and user messages (see [Citations](docs/guides/citations.md)) |
76
76
  | `feedbackConfig` | `MessageFeedbackConfig \| null` | `undefined` | Enables 👍/👎 feedback on assistant messages; providing the prop (even `{}`) shows the controls |
77
77
  | `messageFeedback` | `Record<string, MessageFeedbackRating> \| null` | `undefined` | Host-persisted votes keyed by message id; when provided, it is the single source of truth for the selected thumbs |
78
78
 
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.0",
4
+ "version": "4.1.0-rc.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -6,7 +6,8 @@ import { useWorkspaceMembers } from "../../workspace/composables/workspace-membe
6
6
  import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
7
7
  import { linkifyBareCitations } from "../../citations/citation-links";
8
8
  import { parseContentWithFiles } from "../../files/content-detection";
9
- import { hasUserMentionTags, renderEscapedUserMentions } from "../user-mentions";
9
+ import { renderEscapedUserMentions } from "../user-mentions";
10
+ import { parseUserCitations } from "../user-citations";
10
11
  import ChatVaultLink from "../../files/components/chat-vault-link.vue";
11
12
  const props = defineProps({
12
13
  message: { type: null, required: true },
@@ -73,15 +74,14 @@ const displayContent = computed(() => {
73
74
  return content;
74
75
  return linkifyBareCitations(content);
75
76
  });
76
- const userContentHtml = computed(() => {
77
- if (props.message.role !== "user")
78
- return "";
79
- const escaped = displayContent.value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
77
+ const userContentSegments = computed(() => parseUserCitations(
78
+ props.message.role === "user" ? props.message.content ?? "" : "",
79
+ Boolean(embedConfig?.citations.value)
80
+ ));
81
+ function renderUserText(content) {
82
+ const escaped = content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
80
83
  return renderEscapedUserMentions(escaped);
81
- });
82
- const hasUserMentions = computed(
83
- () => props.message.role === "user" && hasUserMentionTags(displayContent.value)
84
- );
84
+ }
85
85
  const formattedTtft = computed(() => {
86
86
  if (props.message.role !== "assistant" || !props.message.ttft)
87
87
  return null;
@@ -154,9 +154,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
154
154
  <span truncate max-w-200px>{{ file.name }}</span>
155
155
  </a>
156
156
  </div>
157
- <div v-if="hasUserMentions" body-16-regular whitespace-pre-wrap v-html="userContentHtml" />
158
- <div v-else body-16-regular whitespace-pre-wrap>
159
- {{ message.content }}
157
+ <div body-16-regular whitespace-pre-wrap>
158
+ <template v-for="(segment, index) in userContentSegments" :key="`${index}-${segment.type === 'citation' ? segment.citation.raw : 'text'}`">
159
+ <ChatCitation v-if="segment.type === 'citation'" :citation="segment.citation" :label="segment.label" />
160
+ <span v-else v-html="renderUserText(segment.content)" />
161
+ </template>
160
162
  </div>
161
163
  </TelaChatMessageContent>
162
164
 
@@ -182,7 +184,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
182
184
  :reasoning-data="message.reasoningData ?? []"
183
185
  :completed="!isPending"
184
186
  :preparing-steps="preparingSteps"
185
- :created-at="message.createdAt"
187
+ :started-at="message.generationStartedAt ?? message.createdAt"
186
188
  :updated-at="message.updatedAt"
187
189
  />
188
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,
@@ -1476,7 +1477,15 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
1476
1477
  message: message(baseTime, 0, {
1477
1478
  id: "citations-user-message",
1478
1479
  role: "user",
1479
- content: "A empresa pode monitorar os acessos dos empregados \xE0 internet?"
1480
+ content: [
1481
+ "A empresa pode monitorar os acessos dos empregados \xE0 internet?",
1482
+ "",
1483
+ "Documentos da base selecionados:",
1484
+ "- [S\xFAmula 331 do TST](cite://tst_sumulas/331)",
1485
+ "- [Parecer do BNDES](cite://bndes_pareceres/7154)",
1486
+ "",
1487
+ "Veja tamb\xE9m cite://stf_jurisprudencia/sjur336558."
1488
+ ].join("\n")
1480
1489
  })
1481
1490
  }),
1482
1491
  event({
@@ -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
  };
@@ -0,0 +1,11 @@
1
+ import { type CitationRef } from '../citations/citation-links.js';
2
+ export type UserCitationSegment = {
3
+ type: 'text';
4
+ content: string;
5
+ } | {
6
+ type: 'citation';
7
+ citation: CitationRef;
8
+ label?: string;
9
+ };
10
+ /** Only citations become components; all other user content keeps its plain-text semantics. */
11
+ export declare function parseUserCitations(content: string, enabled: boolean): UserCitationSegment[];
@@ -0,0 +1,31 @@
1
+ import { isCitationUrl, parseCitationUrl } from "../citations/citation-links.js";
2
+ const TOKENS = /<[a-z][a-z0-9-]*_mention\s[^>]*>[\s\S]*?<\/[a-z][a-z0-9-]*_mention>|(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\1|$)|(`+)[^`]*?\2|!?\[((?:[^\[\]\n]|\[[^\]\n]*\])*)\]\(([^\s)]+)(?:\s+["'][^\n]*?["'])?\)|\[[ \t]*(cite:\/\/[^\s"'<>`\[\]()*~]+)[ \t]*\]|cite:\/\/[^\s"'<>`\[\]()*~]+/gi;
3
+ export function parseUserCitations(content, enabled) {
4
+ if (!enabled)
5
+ return [{ type: "text", content }];
6
+ const segments = [];
7
+ let end = 0;
8
+ for (const match of content.matchAll(TOKENS)) {
9
+ const [raw, , , label, href, bracketed] = match;
10
+ if (raw.startsWith("!"))
11
+ continue;
12
+ const bare = !href && !bracketed && isCitationUrl(raw);
13
+ const url = href ?? bracketed ?? (bare ? raw.replace(/[.,:;!?]+$/, "") : "");
14
+ const citation = parseCitationUrl(url);
15
+ if (!citation)
16
+ continue;
17
+ const start = match.index;
18
+ if (start > end)
19
+ segments.push({ type: "text", content: content.slice(end, start) });
20
+ const visibleLabel = label?.trim();
21
+ segments.push({
22
+ type: "citation",
23
+ citation,
24
+ ...visibleLabel && visibleLabel !== citation.id && !isCitationUrl(visibleLabel) ? { label: visibleLabel } : {}
25
+ });
26
+ end = start + (bare ? url.length : raw.length);
27
+ }
28
+ if (end < content.length || segments.length === 0)
29
+ segments.push({ type: "text", content: content.slice(end) });
30
+ return segments;
31
+ }
@@ -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.0",
3
+ "version": "4.1.0-rc.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {