@meistrari/chat-nuxt 3.16.0-rc.1 → 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 (27) 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 +11 -15
  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/rendering/components/app-markdown-render.d.vue.ts +2 -0
  10. package/dist/runtime/rendering/components/app-markdown-render.vue +7 -21
  11. package/dist/runtime/rendering/components/app-markdown-render.vue.d.ts +2 -0
  12. package/dist/runtime/rendering/markdown/custom-components.d.ts +2 -0
  13. package/dist/runtime/rendering/markdown/custom-components.js +8 -2
  14. package/package.json +3 -4
  15. package/dist/runtime/messages/composables/chat-scroll-stickiness.d.ts +0 -28
  16. package/dist/runtime/messages/composables/chat-scroll-stickiness.js +0 -181
  17. package/dist/runtime/rendering/components/renderers/mermaid-diagram.d.vue.ts +0 -7
  18. package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue +0 -168
  19. package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue.d.ts +0 -7
  20. package/dist/runtime/rendering/composables/diagram-export.d.ts +0 -11
  21. package/dist/runtime/rendering/composables/diagram-export.js +0 -187
  22. package/dist/runtime/rendering/composables/mermaid-renderer.d.ts +0 -5
  23. package/dist/runtime/rendering/composables/mermaid-renderer.js +0 -121
  24. package/dist/runtime/rendering/composables/zoom-pan.d.ts +0 -18
  25. package/dist/runtime/rendering/composables/zoom-pan.js +0 -130
  26. package/dist/runtime/rendering/markdown/nodes.d.ts +0 -23
  27. 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.1",
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")
@@ -197,7 +187,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
197
187
  v-if="message.content && (!isPending || streamActive)"
198
188
  max-w-prose
199
189
  :class="[
200
- 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'
201
191
  ]"
202
192
  >
203
193
  <template v-if="isFailed">
@@ -245,8 +235,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
245
235
  :content="segment.content"
246
236
  :custom-components="mergedCustomComponents"
247
237
  :custom-html-tags="mergedCustomHtmlTags"
248
- :render-code-blocks-as-pre="true"
249
- :typewriter="isPending || shouldAnimate"
238
+ :fade="true"
239
+ :final="!isPending"
240
+ :smooth-streaming="isPending ? 'auto' : false"
241
+ :typewriter="isPending"
242
+ mode="chat"
250
243
  />
251
244
  </ClientOnly>
252
245
  <ChatFilePreview
@@ -268,8 +261,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
268
261
  :content="displayContent"
269
262
  :custom-components="mergedCustomComponents"
270
263
  :custom-html-tags="mergedCustomHtmlTags"
271
- :render-code-blocks-as-pre="true"
272
- :typewriter="isPending || shouldAnimate"
264
+ :fade="true"
265
+ :final="!isPending"
266
+ :smooth-streaming="isPending ? 'auto' : false"
267
+ :typewriter="isPending"
268
+ mode="chat"
273
269
  text-black-900
274
270
  />
275
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<{
@@ -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.1",
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",
@@ -1,28 +0,0 @@
1
- import type { ComponentPublicInstance, Ref } from 'vue';
2
- export declare const CHAT_SCROLL_PIN_THRESHOLD_PX = 24;
3
- export declare const CHAT_SCROLL_DISCLOSURE_SETTLE_TIMEOUT_MS = 320;
4
- export type ChatScrollPinEvent = {
5
- type: 'content-resize';
6
- } | {
7
- type: 'scroll';
8
- distanceToBottom: number;
9
- } | {
10
- type: 'user-scroll-away';
11
- };
12
- export declare function resolveChatScrollPinnedState(current: boolean, event: ChatScrollPinEvent): boolean;
13
- type ChatConversationInstance = ComponentPublicInstance & {
14
- scrollToBottom: () => void;
15
- };
16
- export declare function resolveChatScrollResizeTarget(content: HTMLElement): HTMLElement;
17
- export declare function resolveChatScrollDisclosureMotionTarget(content: HTMLElement): HTMLElement;
18
- export declare function didChatScrollMoveUp(previousScrollTop: number, currentScrollTop: number): boolean;
19
- /**
20
- * Keeps a conversation attached to the newest content until the user expresses
21
- * an intent to read something above it. Content growth follows synchronously;
22
- * content growth itself never detaches the conversation.
23
- */
24
- export declare function useChatScrollStickiness(conversationRef: Ref<ChatConversationInstance | null>): {
25
- isPinned: Ref<boolean, boolean>;
26
- scrollToBottom: () => void;
27
- };
28
- export {};
@@ -1,181 +0,0 @@
1
- import { nextTick, onBeforeUnmount, onMounted, ref } from "vue";
2
- export const CHAT_SCROLL_PIN_THRESHOLD_PX = 24;
3
- export const CHAT_SCROLL_DISCLOSURE_SETTLE_TIMEOUT_MS = 320;
4
- export function resolveChatScrollPinnedState(current, event) {
5
- if (event.type === "user-scroll-away")
6
- return false;
7
- if (event.type === "scroll" && event.distanceToBottom <= CHAT_SCROLL_PIN_THRESHOLD_PX)
8
- return true;
9
- return current;
10
- }
11
- function distanceToBottom(element) {
12
- return Math.max(0, element.scrollHeight - element.clientHeight - element.scrollTop);
13
- }
14
- export function resolveChatScrollResizeTarget(content) {
15
- return content.firstElementChild ?? content;
16
- }
17
- export function resolveChatScrollDisclosureMotionTarget(content) {
18
- return content.closest("[data-chat-disclosure-motion]") ?? content;
19
- }
20
- export function didChatScrollMoveUp(previousScrollTop, currentScrollTop) {
21
- return currentScrollTop < previousScrollTop - 1;
22
- }
23
- function resolveConversationElements(instance) {
24
- const root = instance?.$el;
25
- if (!(root instanceof HTMLElement))
26
- return null;
27
- const content = root.matches("[data-chat-conversation]") ? root : root.querySelector("[data-chat-conversation]");
28
- if (!content)
29
- return null;
30
- const scroll = root.matches("[data-reka-scroll-area-viewport]") ? root : root.querySelector("[data-reka-scroll-area-viewport]") ?? content;
31
- const resizeTarget = resolveChatScrollResizeTarget(content);
32
- return { content, resizeTarget, scroll };
33
- }
34
- export function useChatScrollStickiness(conversationRef) {
35
- const isPinned = ref(true);
36
- let elements = null;
37
- let resizeObserver;
38
- let touchStartY = null;
39
- let lastScrollTop = 0;
40
- let interactionFrame = 0;
41
- let interactionTimer;
42
- let interactionContent = null;
43
- let interactionEndHandler;
44
- function setPinnedFrom(event) {
45
- isPinned.value = resolveChatScrollPinnedState(isPinned.value, event);
46
- }
47
- function writeBottom() {
48
- if (!isPinned.value || !elements)
49
- return;
50
- elements.scroll.scrollTop = elements.scroll.scrollHeight;
51
- lastScrollTop = elements.scroll.scrollTop;
52
- }
53
- function scrollToBottom() {
54
- isPinned.value = true;
55
- nextTick(writeBottom);
56
- }
57
- function handleScroll() {
58
- if (!elements)
59
- return;
60
- const currentScrollTop = elements.scroll.scrollTop;
61
- const distance = distanceToBottom(elements.scroll);
62
- if (didChatScrollMoveUp(lastScrollTop, currentScrollTop))
63
- setPinnedFrom({ type: "user-scroll-away" });
64
- setPinnedFrom({ type: "scroll", distanceToBottom: distance });
65
- lastScrollTop = currentScrollTop;
66
- }
67
- function handleWheel(event) {
68
- if (event.deltaY < 0 && elements && elements.scroll.scrollHeight > elements.scroll.clientHeight)
69
- setPinnedFrom({ type: "user-scroll-away" });
70
- }
71
- function handleTouchStart(event) {
72
- touchStartY = event.touches[0]?.clientY ?? null;
73
- }
74
- function handleTouchMove(event) {
75
- const currentY = event.touches[0]?.clientY;
76
- if (touchStartY != null && currentY != null && currentY > touchStartY + 2 && elements && elements.scroll.scrollHeight > elements.scroll.clientHeight) {
77
- setPinnedFrom({ type: "user-scroll-away" });
78
- }
79
- touchStartY = currentY ?? touchStartY;
80
- }
81
- function handleKeyDown(event) {
82
- if (["ArrowUp", "Home", "PageUp"].includes(event.key) && elements && elements.scroll.scrollHeight > elements.scroll.clientHeight) {
83
- setPinnedFrom({ type: "user-scroll-away" });
84
- }
85
- }
86
- function clearInteractionWait() {
87
- cancelAnimationFrame(interactionFrame);
88
- if (interactionTimer)
89
- clearTimeout(interactionTimer);
90
- interactionTimer = void 0;
91
- if (interactionContent && interactionEndHandler)
92
- interactionContent.removeEventListener("transitionend", interactionEndHandler);
93
- interactionContent = null;
94
- interactionEndHandler = void 0;
95
- }
96
- function queueDisclosureReevaluation() {
97
- cancelAnimationFrame(interactionFrame);
98
- interactionFrame = requestAnimationFrame(() => {
99
- interactionFrame = requestAnimationFrame(() => {
100
- if (!elements)
101
- return;
102
- setPinnedFrom({ type: "scroll", distanceToBottom: distanceToBottom(elements.scroll) });
103
- });
104
- });
105
- }
106
- function waitForDisclosureMotion(disclosureTrigger) {
107
- clearInteractionWait();
108
- const controlsId = disclosureTrigger.getAttribute("aria-controls");
109
- const controlledContent = controlsId ? disclosureTrigger.ownerDocument.getElementById(controlsId) : null;
110
- const reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
111
- if (!(controlledContent instanceof HTMLElement) || reducedMotion) {
112
- queueDisclosureReevaluation();
113
- return;
114
- }
115
- const motionContent = resolveChatScrollDisclosureMotionTarget(controlledContent);
116
- const finish = () => {
117
- clearInteractionWait();
118
- queueDisclosureReevaluation();
119
- };
120
- interactionContent = motionContent;
121
- interactionEndHandler = (transitionEvent) => {
122
- if (transitionEvent.target === motionContent && transitionEvent.propertyName === "grid-template-rows") {
123
- finish();
124
- }
125
- };
126
- motionContent.addEventListener("transitionend", interactionEndHandler);
127
- interactionTimer = setTimeout(finish, CHAT_SCROLL_DISCLOSURE_SETTLE_TIMEOUT_MS);
128
- }
129
- function handleContentClick(event) {
130
- const target = event.target;
131
- if (!(target instanceof Element))
132
- return;
133
- const disclosureTrigger = target.closest(
134
- "[data-chat-scroll-preserve-trigger], button[aria-controls][data-state]"
135
- );
136
- if (!disclosureTrigger?.closest("[data-chat-scroll-preserve]"))
137
- return;
138
- setPinnedFrom({ type: "user-scroll-away" });
139
- waitForDisclosureMotion(disclosureTrigger);
140
- }
141
- function bind() {
142
- elements = resolveConversationElements(conversationRef.value);
143
- if (!elements)
144
- return;
145
- const { content, resizeTarget, scroll } = elements;
146
- scroll.addEventListener("scroll", handleScroll, { passive: true });
147
- scroll.addEventListener("wheel", handleWheel, { passive: true });
148
- scroll.addEventListener("touchstart", handleTouchStart, { passive: true });
149
- scroll.addEventListener("touchmove", handleTouchMove, { passive: true });
150
- scroll.addEventListener("keydown", handleKeyDown);
151
- content.addEventListener("click", handleContentClick, true);
152
- lastScrollTop = scroll.scrollTop;
153
- resizeObserver = new ResizeObserver(() => {
154
- setPinnedFrom({ type: "content-resize" });
155
- writeBottom();
156
- });
157
- resizeObserver.observe(resizeTarget);
158
- scrollToBottom();
159
- }
160
- function unbind() {
161
- if (elements) {
162
- const { content, scroll } = elements;
163
- scroll.removeEventListener("scroll", handleScroll);
164
- scroll.removeEventListener("wheel", handleWheel);
165
- scroll.removeEventListener("touchstart", handleTouchStart);
166
- scroll.removeEventListener("touchmove", handleTouchMove);
167
- scroll.removeEventListener("keydown", handleKeyDown);
168
- content.removeEventListener("click", handleContentClick, true);
169
- }
170
- resizeObserver?.disconnect();
171
- resizeObserver = void 0;
172
- clearInteractionWait();
173
- elements = null;
174
- }
175
- onMounted(() => nextTick(bind));
176
- onBeforeUnmount(unbind);
177
- return {
178
- isPinned,
179
- scrollToBottom
180
- };
181
- }
@@ -1,7 +0,0 @@
1
- import { type MarkdownNode } from '../../markdown/nodes.js';
2
- type __VLS_Props = {
3
- node?: MarkdownNode;
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>;
6
- declare const _default: typeof __VLS_export;
7
- export default _default;