@meistrari/chat-nuxt 3.16.0-rc.0 → 3.16.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.
Files changed (31) hide show
  1. package/README.md +4 -4
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +6 -22
  4. package/dist/runtime/conversations/components/chat-conversation-item.vue +1 -1
  5. package/dist/runtime/messages/components/chat-message-bubble.vue +15 -20
  6. package/dist/runtime/messages/components/chat-message-list.d.vue.ts +2 -2
  7. package/dist/runtime/messages/components/chat-message-list.vue +31 -10
  8. package/dist/runtime/messages/components/chat-message-list.vue.d.ts +2 -2
  9. package/dist/runtime/messages/composables/chat.js +3 -8
  10. package/dist/runtime/messages/composables/use-direct-stream.js +7 -7
  11. package/dist/runtime/messages/message-reasoning.d.ts +9 -2
  12. package/dist/runtime/messages/message-reasoning.js +30 -13
  13. package/dist/runtime/rendering/components/app-markdown-render.d.vue.ts +2 -0
  14. package/dist/runtime/rendering/components/app-markdown-render.vue +7 -21
  15. package/dist/runtime/rendering/components/app-markdown-render.vue.d.ts +2 -0
  16. package/dist/runtime/rendering/markdown/custom-components.d.ts +2 -0
  17. package/dist/runtime/rendering/markdown/custom-components.js +8 -2
  18. package/package.json +3 -4
  19. package/dist/runtime/messages/composables/chat-scroll-stickiness.d.ts +0 -28
  20. package/dist/runtime/messages/composables/chat-scroll-stickiness.js +0 -181
  21. package/dist/runtime/rendering/components/renderers/mermaid-diagram.d.vue.ts +0 -7
  22. package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue +0 -168
  23. package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue.d.ts +0 -7
  24. package/dist/runtime/rendering/composables/diagram-export.d.ts +0 -11
  25. package/dist/runtime/rendering/composables/diagram-export.js +0 -187
  26. package/dist/runtime/rendering/composables/mermaid-renderer.d.ts +0 -5
  27. package/dist/runtime/rendering/composables/mermaid-renderer.js +0 -121
  28. package/dist/runtime/rendering/composables/zoom-pan.d.ts +0 -18
  29. package/dist/runtime/rendering/composables/zoom-pan.js +0 -130
  30. package/dist/runtime/rendering/markdown/nodes.d.ts +0 -23
  31. package/dist/runtime/rendering/markdown/nodes.js +0 -46
package/README.md CHANGED
@@ -65,7 +65,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
65
65
  | `features` | `Partial<ChatFeatureConfig>` | `{}` | Toggle `showUsageTab`, `showDebugOption`, `showCancelButton`, `showTtft`, and `chatOutboundQueue`; all released features default to `true` |
66
66
  | `loadingMessages` | `readonly string[] \| null` | Default chat messages | Override pending-response messages |
67
67
  | `loadingMessagesMode` | `'ordered' \| 'random' \| null` | `'ordered'` | Rotate custom messages in order or at random; only applies when `loadingMessages` has a non-empty message |
68
- | `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name |
68
+ | `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` |
69
69
  | `customHtmlTags` | `readonly string[]` | `undefined` | Extra HTML tags allowed by the markdown renderer |
70
70
  | `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant messages (see [Citations](docs/guides/citations.md)) |
71
71
  | `feedbackConfig` | `MessageFeedbackConfig \| null` | `undefined` | Enables 👍/👎 feedback on assistant messages; providing the prop (even `{}`) shows the controls |
@@ -111,14 +111,14 @@ Installing `@meistrari/chat-nuxt` installs the runtime packages used by the laye
111
111
  | `@iconify/vue` + `@iconify-json/ph` | `^5.0.0` / `^1.2.2` | Icon rendering support (Phosphor set) |
112
112
  | `@vueuse/core` | `^12.8.0` | Runtime composables used by chat UI |
113
113
  | `@vueuse/components` | `^12.8.0` | VueUse component peer expected by the shared runtime |
114
- | `beautiful-mermaid` + `mermaid` | `^1.1.3` / `^11.13.0` | Mermaid diagram rendering in markdown |
115
- | `dompurify` | `^3.3.2` | HTML sanitization for rendered markdown |
114
+ | `mermaid` | `^11.13.0` | Native markstream Mermaid diagram rendering |
116
115
  | `h3` | `^1.15.0` | Server route utilities provided by Nuxt/Nitro |
117
- | `markstream-vue` | `0.0.3-beta.6` | Markdown streaming renderer |
116
+ | `markstream-vue` | `2.0.7` | Markdown streaming renderer |
118
117
  | `motion` | `^11.13.0` | Animation component runtime |
119
118
  | `nitropack` | `^2.10.0` | Runtime config and task utilities provided by Nuxt/Nitro |
120
119
  | `pdfjs-dist` | `^5.4.530` | PDF file previews |
121
120
  | `posthog-js` | `^1.364.2` | Optional PostHog feature-flag helper |
121
+ | `stream-diffs` | `0.0.2` | Native markstream rich code and diff rendering |
122
122
  | `virtua` | `^0.42.0` | Virtualized list runtime expected by shared UI |
123
123
  | `vue-i18n` | `^11.0.0` | Internationalization runtime expected by shared UI |
124
124
  | `xlsx` | `^0.18.5` | Spreadsheet file previews |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.16.0-rc.0",
4
+ "version": "3.16.0-rc.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -145,8 +145,12 @@ function patchStreamMarkdownParserSource(source) {
145
145
  }
146
146
  function resolvePatchedStreamMarkdownParser(from) {
147
147
  try {
148
- const markstreamEntry = createRequire(from).resolve("markstream-vue");
149
- const entry = createRequire(markstreamEntry).resolve("stream-markdown-parser");
148
+ const markstreamStyles = createRequire(from).resolve("markstream-vue/index.css");
149
+ const markstreamRoot = dirname(dirname(markstreamStyles));
150
+ const parserRequireEntry = createRequire(join(markstreamRoot, "package.json")).resolve("stream-markdown-parser");
151
+ const parserRoot = dirname(dirname(parserRequireEntry));
152
+ const parserPackage = JSON.parse(readFileSync(join(parserRoot, "package.json"), "utf8"));
153
+ const entry = parserPackage.module ? join(parserRoot, parserPackage.module) : parserRequireEntry;
150
154
  const { source, applied } = patchStreamMarkdownParserSource(readFileSync(entry, "utf8"));
151
155
  return { entry, source, applied };
152
156
  } catch {
@@ -508,7 +512,6 @@ const module$1 = defineNuxtModule({
508
512
  if (isServer) {
509
513
  return;
510
514
  }
511
- config.plugins = config.plugins || [];
512
515
  if (Array.isArray(config.resolve.alias)) {
513
516
  config.resolve.alias.push(...Object.entries(runtimeAliases).map(([find, replacement]) => ({ find, replacement })));
514
517
  } else {
@@ -521,25 +524,6 @@ const module$1 = defineNuxtModule({
521
524
  ...config.resolve.dedupe || [],
522
525
  ...runtimeAliasDedupe
523
526
  ]));
524
- config.plugins.push({
525
- name: "@meistrari/chat-nuxt:elkjs-umd-interop",
526
- enforce: "pre",
527
- apply: "serve",
528
- load(id) {
529
- const cleanId = id.split("?")[0] ?? id;
530
- if (!cleanId.endsWith("/elkjs/lib/elk.bundled.js")) {
531
- return null;
532
- }
533
- try {
534
- const source = readFileSync(cleanId, "utf8");
535
- return `${source}
536
- export default globalThis.ELK;
537
- `;
538
- } catch {
539
- return null;
540
- }
541
- }
542
- });
543
527
  });
544
528
  const serverDir = join(runtimeDir, "server");
545
529
  const publicAssetsDir = join(runtimeDir, "public/assets");
@@ -4,7 +4,7 @@ const props = defineProps({
4
4
  conversation: { type: null, required: true },
5
5
  active: { type: Boolean, required: true },
6
6
  draggable: { type: Boolean, required: false },
7
- itemClass: { type: null, required: false },
7
+ itemClass: { type: [Boolean, null, String, Object, Array], required: false, skipCheck: true },
8
8
  userImage: { type: String, required: false },
9
9
  selectConversation: { type: Function, required: false }
10
10
  });
@@ -56,16 +56,6 @@ function formatTime(date) {
56
56
  const d = date instanceof Date ? date : new Date(date);
57
57
  return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
58
58
  }
59
- const wasEverPending = ref(false);
60
- const shouldAnimate = ref(false);
61
- watch(() => props.message.status, (status, oldStatus) => {
62
- if (status === "pending") {
63
- wasEverPending.value = true;
64
- }
65
- if (oldStatus === "pending" && status === "completed" && wasEverPending.value) {
66
- shouldAnimate.value = true;
67
- }
68
- }, { immediate: true });
69
59
  const displayContent = computed(() => props.message.content ?? "");
70
60
  const userContentHtml = computed(() => {
71
61
  if (props.message.role !== "user")
@@ -180,13 +170,12 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
180
170
 
181
171
  <template v-else>
182
172
  <!--
183
- A live stream normally owns the pending state, so this block stays
184
- out of its way. Stand-in steps are the exception: until the run
185
- emits anything the stream has nothing to show, and that silence is
186
- exactly what they cover.
173
+ Mounted for the whole pending turn so activity remains visible
174
+ while the current streamed chunk is empty between assistant
175
+ messages or tool phases.
187
176
  -->
188
177
  <ChatReasoningThought
189
- v-if="hasReasoning || preparingSteps?.length && !message.content || isPending && !streamActive"
178
+ v-if="hasReasoning || isPending"
190
179
  :reasoning-data="message.reasoningData ?? []"
191
180
  :completed="!isPending"
192
181
  :preparing-steps="preparingSteps"
@@ -198,7 +187,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
198
187
  v-if="message.content && (!isPending || streamActive)"
199
188
  max-w-prose
200
189
  :class="[
201
- isFailed ? 'flex gap-12px items-start bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px code-theme'
190
+ isFailed ? 'flex gap-12px items-start bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px'
202
191
  ]"
203
192
  >
204
193
  <template v-if="isFailed">
@@ -246,8 +235,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
246
235
  :content="segment.content"
247
236
  :custom-components="mergedCustomComponents"
248
237
  :custom-html-tags="mergedCustomHtmlTags"
249
- :render-code-blocks-as-pre="true"
250
- :typewriter="isPending || shouldAnimate"
238
+ :fade="true"
239
+ :final="!isPending"
240
+ :smooth-streaming="isPending ? 'auto' : false"
241
+ :typewriter="isPending"
242
+ mode="chat"
251
243
  />
252
244
  </ClientOnly>
253
245
  <ChatFilePreview
@@ -269,8 +261,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
269
261
  :content="displayContent"
270
262
  :custom-components="mergedCustomComponents"
271
263
  :custom-html-tags="mergedCustomHtmlTags"
272
- :render-code-blocks-as-pre="true"
273
- :typewriter="isPending || shouldAnimate"
264
+ :fade="true"
265
+ :final="!isPending"
266
+ :smooth-streaming="isPending ? 'auto' : false"
267
+ :typewriter="isPending"
268
+ mode="chat"
274
269
  text-black-900
275
270
  />
276
271
  </ClientOnly>
@@ -13,8 +13,8 @@ type __VLS_Props = {
13
13
  showTtft?: boolean;
14
14
  };
15
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
- isPinned: import("vue").Ref<boolean, boolean>;
17
- scrollToBottom: () => void;
16
+ isPinned: any;
17
+ scrollToBottom: any;
18
18
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
19
19
  retry: (messageId: string) => any;
20
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { useChatScrollStickiness } from "../composables/chat-scroll-stickiness";
2
+ import { useStickToBottom } from "markstream-vue/utils";
3
3
  const props = defineProps({
4
4
  messages: { type: Array, required: true },
5
5
  loading: { type: Boolean, required: false },
@@ -10,22 +10,42 @@ const props = defineProps({
10
10
  });
11
11
  const emit = defineEmits(["retry"]);
12
12
  const conversationRef = ref(null);
13
- const { isPinned, scrollToBottom } = useChatScrollStickiness(conversationRef);
13
+ const conversationElements = computed(() => {
14
+ const root = conversationRef.value?.$el;
15
+ if (!(root instanceof HTMLElement))
16
+ return null;
17
+ const content = root.matches("[data-chat-conversation]") ? root : root.querySelector("[data-chat-conversation]");
18
+ if (!content)
19
+ return null;
20
+ const scroll = root.matches("[data-reka-scroll-area-viewport]") ? root : root.querySelector("[data-reka-scroll-area-viewport]") ?? content;
21
+ return {
22
+ content: content.firstElementChild ?? content,
23
+ scroll
24
+ };
25
+ });
26
+ const scrollRoot = computed(() => conversationElements.value?.scroll ?? null);
27
+ const contentRoot = computed(() => conversationElements.value?.content ?? null);
28
+ const {
29
+ bottomPinned: isPinned,
30
+ scheduleScrollToBottom,
31
+ scrollToBottom
32
+ } = useStickToBottom(scrollRoot, contentRoot);
14
33
  const skeletonMessages = [
15
34
  { id: "user-1", align: "end", width: "58%", height: "44px" },
16
35
  { id: "assistant-1", align: "start", width: "92%", height: "116px" },
17
36
  { id: "user-2", align: "end", width: "46%", height: "44px" },
18
37
  { id: "assistant-2", align: "start", width: "84%", height: "92px" }
19
38
  ];
20
- let knownMessageIds = new Set(props.messages.map((message) => message.id));
21
- watch(() => props.messages.map((message) => `${message.id}:${message.role}`).join("|"), () => {
22
- const hasNewUserMessage = props.messages.some(
23
- (message) => message.role === "user" && !knownMessageIds.has(message.id)
24
- );
25
- knownMessageIds = new Set(props.messages.map((message) => message.id));
26
- if (hasNewUserMessage)
27
- scrollToBottom();
39
+ const latestOutputSignal = computed(() => {
40
+ const latestMessage = props.messages.at(-1);
41
+ if (!latestMessage)
42
+ return "empty";
43
+ return `${props.messages.length}:${latestMessage.id}:${latestMessage.status}:${latestMessage.content?.length ?? 0}`;
28
44
  });
45
+ watch(latestOutputSignal, async () => {
46
+ await nextTick();
47
+ scheduleScrollToBottom();
48
+ }, { flush: "post" });
29
49
  defineExpose({
30
50
  isPinned,
31
51
  scrollToBottom
@@ -43,6 +63,7 @@ defineExpose({
43
63
  >
44
64
  <TelaChatConversation
45
65
  ref="conversationRef"
66
+ :scroll-area="variant === 'widget'"
46
67
  :class="{
47
68
  'pt-72px!': (variant ?? 'page') === 'page',
48
69
  'pt-72px pb-24px px-16px': variant === 'mobile'
@@ -13,8 +13,8 @@ type __VLS_Props = {
13
13
  showTtft?: boolean;
14
14
  };
15
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
- isPinned: import("vue").Ref<boolean, boolean>;
17
- scrollToBottom: () => void;
16
+ isPinned: any;
17
+ scrollToBottom: any;
18
18
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
19
19
  retry: (messageId: string) => any;
20
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
@@ -207,12 +207,10 @@ export function useChat(options = {}) {
207
207
  );
208
208
  const reasoningCache = createReasoningCache();
209
209
  const processedMessages = computed(() => {
210
- const resolved = resolveReasoningDisplayMessages(messages.value, reasoningCache);
211
210
  const messageId = streamingMessageId.value;
212
211
  const content = directStream.streamingText.value;
213
- if (!messageId || !content)
214
- return resolved;
215
- return resolved.map((message) => message.id === messageId && (message.status === "pending" || pendingStreamReconciliationIds.value.has(message.id)) ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
212
+ const overlaid = !messageId || content === null ? messages.value : messages.value.map((message) => message.id === messageId && (message.status === "pending" || pendingStreamReconciliationIds.value.has(message.id)) ? { ...message, content } : message);
213
+ return resolveReasoningDisplayMessages(overlaid, reasoningCache);
216
214
  });
217
215
  const stopCondition = computed(
218
216
  () => !hasPendingMessage.value && pendingStreamReconciliationIds.value.size === 0
@@ -529,9 +527,9 @@ export function useChat(options = {}) {
529
527
  agentSessionExecutable.value = false;
530
528
  agentSessionRewakeable.value = false;
531
529
  }
530
+ startPolling();
532
531
  };
533
532
  let response;
534
- let acceptedByStream = false;
535
533
  try {
536
534
  response = await directStream.connectMessage(
537
535
  chatApi.path(`/conversations/${currentConversation.value.id}/messages/stream`),
@@ -542,7 +540,6 @@ export function useChat(options = {}) {
542
540
  }),
543
541
  acceptMessagePair
544
542
  );
545
- acceptedByStream = true;
546
543
  } catch (streamError) {
547
544
  const statusCode = getChatApiErrorStatus(streamError);
548
545
  if (statusCode !== null && statusCode >= 400 && statusCode < 500)
@@ -565,8 +562,6 @@ export function useChat(options = {}) {
565
562
  "user"
566
563
  );
567
564
  }
568
- if (!acceptedByStream)
569
- startPolling();
570
565
  return response.userMessage;
571
566
  } catch (err) {
572
567
  const code = getChatApiErrorCode(err);
@@ -115,7 +115,7 @@ function reviveMessagePair(input) {
115
115
  }
116
116
  export function useDirectStream(options = {}) {
117
117
  const activity = ref("idle");
118
- const streamingText = ref("");
118
+ const streamingText = ref(null);
119
119
  const streamingThinking = ref("");
120
120
  const activeRunId = ref(null);
121
121
  let accumulator = createMessagePartsAccumulator();
@@ -123,7 +123,7 @@ export function useDirectStream(options = {}) {
123
123
  function resetAccumulator(runId = null) {
124
124
  accumulator = createMessagePartsAccumulator(runId);
125
125
  activeRunId.value = runId;
126
- streamingText.value = "";
126
+ streamingText.value = null;
127
127
  streamingThinking.value = "";
128
128
  }
129
129
  function beginMessage() {
@@ -199,7 +199,7 @@ export function useDirectStream(options = {}) {
199
199
  if (status === "completed" || status === "waiting_messages") {
200
200
  const result = {
201
201
  status,
202
- text: streamingText.value
202
+ text: streamingText.value ?? ""
203
203
  };
204
204
  if (!optimisticResultReported) {
205
205
  optimisticResultReported = true;
@@ -287,11 +287,11 @@ export function useDirectStream(options = {}) {
287
287
  if (event.kind !== "message-parts" || typeof event.runId !== "string" || typeof event.deltaSeq !== "number" || !Number.isInteger(event.deltaSeq) || event.deltaSeq < 0 || !isRecord(event.event)) {
288
288
  continue;
289
289
  }
290
- const accumulated = applyMessagePartsEvent(accumulator, event);
290
+ const current = applyMessagePartsEvent(accumulator, event);
291
291
  activeRunId.value = accumulator.runId;
292
- if (accumulated.changed) {
293
- streamingText.value = accumulated.text;
294
- streamingThinking.value = accumulated.thinking;
292
+ if (current.changed) {
293
+ streamingText.value = current.text;
294
+ streamingThinking.value = current.thinking;
295
295
  }
296
296
  }
297
297
  }
@@ -1,11 +1,18 @@
1
1
  import type { Message } from '../types/schemas/chat/conversations.js';
2
2
  import type { ReasoningStep } from './reasoning.js';
3
+ type ParsedReasoning = {
4
+ steps: ReasoningStep[];
5
+ thinkingTexts: Set<string>;
6
+ currentAnswerParts: string[];
7
+ currentAnswerStepTexts: Set<string>;
8
+ };
3
9
  export type ReasoningCache = {
4
- bySignature: Map<string, ReasoningStep[]>;
10
+ bySignature: Map<string, ParsedReasoning>;
5
11
  byReference: WeakMap<unknown[], {
6
12
  messageId: string;
7
- parsedSteps: ReasoningStep[];
13
+ parsed: ParsedReasoning;
8
14
  }>;
9
15
  };
10
16
  export declare function createReasoningCache(): ReasoningCache;
11
17
  export declare function resolveReasoningDisplayMessages(messages: Message[], cache?: ReasoningCache): Message[];
18
+ export {};
@@ -46,27 +46,30 @@ function groupReasoningSteps(parsedSteps) {
46
46
  function getReasoningCacheKey(messageId, reasoningData) {
47
47
  return `${messageId}:${JSON.stringify(reasoningData)}`;
48
48
  }
49
- function getParsedReasoningSteps(cache, messageId, reasoningData) {
49
+ function getParsedReasoning(cache, messageId, reasoningData) {
50
50
  const referenceHit = cache.byReference.get(reasoningData);
51
51
  if (referenceHit?.messageId === messageId)
52
- return referenceHit.parsedSteps;
52
+ return referenceHit.parsed;
53
53
  const cacheKey = getReasoningCacheKey(messageId, reasoningData);
54
- let parsedSteps = cache.bySignature.get(cacheKey);
55
- if (!parsedSteps) {
56
- parsedSteps = parseReasoningFromPolling(reasoningData);
54
+ let parsed = cache.bySignature.get(cacheKey);
55
+ if (!parsed) {
56
+ parsed = parseReasoningFromPolling(reasoningData);
57
57
  for (const staleKey of cache.bySignature.keys()) {
58
58
  if (staleKey.startsWith(`${messageId}:`))
59
59
  cache.bySignature.delete(staleKey);
60
60
  }
61
- cache.bySignature.set(cacheKey, parsedSteps);
61
+ cache.bySignature.set(cacheKey, parsed);
62
62
  }
63
- cache.byReference.set(reasoningData, { messageId, parsedSteps });
64
- return parsedSteps;
63
+ cache.byReference.set(reasoningData, { messageId, parsed });
64
+ return parsed;
65
65
  }
66
66
  function parseReasoningFromPolling(reasoning) {
67
67
  const allSteps = [];
68
68
  const seenContentIds = /* @__PURE__ */ new Set();
69
69
  const seenTexts = /* @__PURE__ */ new Set();
70
+ const thinkingTexts = /* @__PURE__ */ new Set();
71
+ let currentAnswerParts = [];
72
+ let currentAnswerStepTexts = /* @__PURE__ */ new Set();
70
73
  for (const item of reasoning) {
71
74
  if (!item || typeof item !== "object")
72
75
  continue;
@@ -98,15 +101,23 @@ function parseReasoningFromPolling(reasoning) {
98
101
  const message = itemObj.message;
99
102
  if (!Array.isArray(message.content))
100
103
  continue;
104
+ const answerParts = [];
105
+ const answerStepTexts = /* @__PURE__ */ new Set();
101
106
  for (const contentPart of message.content) {
102
107
  if (!contentPart || typeof contentPart !== "object")
103
108
  continue;
104
109
  const part = contentPart;
105
- if (part.type === "text" && typeof part.text === "string") {
110
+ if ((part.type === "text" || part.type === "thinking") && typeof part.text === "string") {
111
+ if (part.type === "text")
112
+ answerParts.push(part.text);
113
+ if (part.type === "thinking")
114
+ thinkingTexts.add(part.text);
106
115
  if (seenTexts.has(part.text))
107
116
  continue;
108
117
  seenTexts.add(part.text);
109
118
  allSteps.push(part.text);
119
+ if (part.type === "text")
120
+ answerStepTexts.add(part.text);
110
121
  } else if (part.type === "tool_use" && part.id && part.name) {
111
122
  const toolId = part.id;
112
123
  if (seenContentIds.has(`tool_use:${toolId}`))
@@ -130,6 +141,10 @@ function parseReasoningFromPolling(reasoning) {
130
141
  });
131
142
  }
132
143
  }
144
+ if (!itemObj.parent_tool_use_id && answerParts.length > 0) {
145
+ currentAnswerParts = answerParts;
146
+ currentAnswerStepTexts = answerStepTexts;
147
+ }
133
148
  } else if (itemObj.type === "text" && typeof itemObj.text === "string") {
134
149
  if (seenTexts.has(itemObj.text))
135
150
  continue;
@@ -158,21 +173,23 @@ function parseReasoningFromPolling(reasoning) {
158
173
  });
159
174
  }
160
175
  }
161
- return groupReasoningSteps(allSteps);
176
+ return { steps: groupReasoningSteps(allSteps), thinkingTexts, currentAnswerParts, currentAnswerStepTexts };
162
177
  }
163
178
  export function resolveReasoningDisplayMessages(messages, cache = createReasoningCache()) {
164
179
  const renderedSteps = /* @__PURE__ */ new Set();
165
180
  return messages.map((message) => {
166
181
  if (message.role !== "assistant" || !message.reasoningData || !Array.isArray(message.reasoningData))
167
182
  return message;
168
- const parsedSteps = getParsedReasoningSteps(cache, message.id, message.reasoningData);
183
+ const parsed = getParsedReasoning(cache, message.id, message.reasoningData);
169
184
  const displaySteps = [];
170
- for (const step of parsedSteps) {
185
+ const currentAnswerMatches = message.content === parsed.currentAnswerParts.join("");
186
+ for (const step of parsed.steps) {
171
187
  const key = getReasoningStepKey(step);
172
188
  if (renderedSteps.has(key))
173
189
  continue;
174
- if (typeof step === "string" && message.content && step === message.content)
190
+ if (typeof step === "string" && currentAnswerMatches && parsed.currentAnswerStepTexts.has(step) && !parsed.thinkingTexts.has(step)) {
175
191
  continue;
192
+ }
176
193
  renderedSteps.add(key);
177
194
  displaySteps.push(step);
178
195
  }
@@ -1,6 +1,7 @@
1
1
  import type { Component } from 'vue';
2
2
  type __VLS_Props = {
3
3
  content?: string | null;
4
+ final?: boolean;
4
5
  renderCodeBlocksAsPre?: boolean;
5
6
  typewriter?: boolean;
6
7
  customComponents?: Record<string, Component>;
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
11
  content: string | null;
11
12
  customComponents: Record<string, Component>;
12
13
  customHtmlTags: readonly string[];
14
+ final: boolean;
13
15
  renderCodeBlocksAsPre: boolean;
14
16
  typewriter: boolean;
15
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,10 +1,9 @@
1
1
  <script setup>
2
- import MarkdownRender, { getMarkdown, parseMarkdownToStructure, removeCustomComponents, setCustomComponents } from "markstream-vue";
3
- import MermaidDiagram from "./renderers/mermaid-diagram.vue";
4
- import { transformMarkdownNodes } from "../markdown/nodes";
2
+ import MarkdownRender, { removeCustomComponents, setCustomComponents } from "markstream-vue";
5
3
  import { wrapCustomMarkdownComponents } from "../markdown/custom-components";
6
4
  const props = defineProps({
7
5
  content: { type: [String, null], required: false, default: "" },
6
+ final: { type: Boolean, required: false, default: true },
8
7
  renderCodeBlocksAsPre: { type: Boolean, required: false, default: false },
9
8
  typewriter: { type: Boolean, required: false, default: true },
10
9
  customComponents: { type: Object, required: false, default: () => ({}) },
@@ -13,7 +12,6 @@ const props = defineProps({
13
12
  const attrs = useAttrs();
14
13
  const instance = getCurrentInstance();
15
14
  const scopeId = `app-markdown-render-${instance?.uid ?? Math.random().toString(36).slice(2)}`;
16
- const markdown = computed(() => getMarkdown(scopeId, { customHtmlTags: props.customHtmlTags }));
17
15
  function escapeSingleDollarSigns(text) {
18
16
  return text.replace(/(```[\s\S]*?```|``+[^`]*``+|`[^`]*`|!?\[(?:[^\[\]]|\[[^\[\]]*\])*\]\([^)]*\))|(?<![$\\])\$(?!\$)/g, (_match, codeSpan) => {
19
17
  if (codeSpan)
@@ -21,24 +19,10 @@ function escapeSingleDollarSigns(text) {
21
19
  return "\\$";
22
20
  });
23
21
  }
24
- const parseOptions = computed(() => {
25
- const tags = props.customHtmlTags;
26
- return tags && tags.length > 0 ? { customHtmlTags: [...tags] } : void 0;
27
- });
28
- const nodes = computed(() => {
29
- const parsedNodes = parseMarkdownToStructure(
30
- escapeSingleDollarSigns(props.content ?? ""),
31
- markdown.value,
32
- parseOptions.value
33
- );
34
- return transformMarkdownNodes(parsedNodes);
35
- });
22
+ const escapedContent = computed(() => escapeSingleDollarSigns(props.content ?? ""));
36
23
  const customMarkdownComponents = computed(() => wrapCustomMarkdownComponents(props.customComponents));
37
24
  watchEffect(() => {
38
- setCustomComponents(scopeId, {
39
- mermaid: MermaidDiagram,
40
- ...customMarkdownComponents.value
41
- });
25
+ setCustomComponents(scopeId, customMarkdownComponents.value);
42
26
  });
43
27
  onBeforeUnmount(() => {
44
28
  removeCustomComponents(scopeId);
@@ -48,8 +32,10 @@ onBeforeUnmount(() => {
48
32
  <template>
49
33
  <MarkdownRender
50
34
  v-bind="attrs"
51
- :nodes="nodes"
35
+ :content="escapedContent"
52
36
  :custom-id="scopeId"
37
+ :custom-html-tags="customHtmlTags"
38
+ :final="final"
53
39
  :render-code-blocks-as-pre="renderCodeBlocksAsPre"
54
40
  :typewriter="typewriter"
55
41
  />
@@ -1,6 +1,7 @@
1
1
  import type { Component } from 'vue';
2
2
  type __VLS_Props = {
3
3
  content?: string | null;
4
+ final?: boolean;
4
5
  renderCodeBlocksAsPre?: boolean;
5
6
  typewriter?: boolean;
6
7
  customComponents?: Record<string, Component>;
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
11
  content: string | null;
11
12
  customComponents: Record<string, Component>;
12
13
  customHtmlTags: readonly string[];
14
+ final: boolean;
13
15
  renderCodeBlocksAsPre: boolean;
14
16
  typewriter: boolean;
15
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -2,7 +2,9 @@ import { type Component } from 'vue';
2
2
  import type { BaseNode } from 'markstream-vue';
3
3
  export type CustomMarkdownNode = BaseNode & {
4
4
  attrs?: [string, unknown][];
5
+ autoClosed?: boolean;
5
6
  content?: string;
7
+ loading?: boolean;
6
8
  };
7
9
  export declare function customMarkdownNodeProps(node: CustomMarkdownNode): Record<string, unknown>;
8
10
  export declare function wrapCustomMarkdownComponent(component: Component, tagName?: string): Component;
@@ -1,4 +1,5 @@
1
1
  import { defineComponent, h } from "vue";
2
+ const RESERVED_CUSTOM_MARKDOWN_PROPS = /* @__PURE__ */ new Set(["node", "loading", "autoClosed"]);
2
3
  function camelizeAttributeName(name) {
3
4
  return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
4
5
  }
@@ -9,11 +10,16 @@ export function customMarkdownNodeProps(node) {
9
10
  if (!name)
10
11
  continue;
11
12
  const propName = camelizeAttributeName(name);
12
- if (propName === "node")
13
+ if (RESERVED_CUSTOM_MARKDOWN_PROPS.has(propName))
13
14
  continue;
14
15
  attrProps[propName] = value;
15
16
  }
16
- return { node, ...attrProps };
17
+ return {
18
+ node,
19
+ ...typeof node.loading === "boolean" ? { loading: node.loading } : {},
20
+ ...typeof node.autoClosed === "boolean" ? { autoClosed: node.autoClosed } : {},
21
+ ...attrProps
22
+ };
17
23
  }
18
24
  function customMarkdownComponentAdapterName(tagName, component) {
19
25
  if (typeof component === "function" && component.name)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "3.16.0-rc.0",
3
+ "version": "3.16.0-rc.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -50,15 +50,14 @@
50
50
  "@sentry/nuxt": "^10.0.0",
51
51
  "@vueuse/components": "^12.8.0",
52
52
  "@vueuse/core": "^12.8.0",
53
- "beautiful-mermaid": "^1.1.3",
54
- "dompurify": "^3.3.2",
55
53
  "h3": "^1.15.0",
56
- "markstream-vue": "0.0.3-beta.6",
54
+ "markstream-vue": "2.0.7",
57
55
  "mermaid": "^11.13.0",
58
56
  "motion": "^11.13.0",
59
57
  "nitropack": "^2.10.0",
60
58
  "pdfjs-dist": "^5.4.530",
61
59
  "posthog-js": "^1.364.2",
60
+ "stream-diffs": "0.0.2",
62
61
  "virtua": "^0.42.0",
63
62
  "vue-i18n": "^11.0.0",
64
63
  "xlsx": "^0.18.5",