@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.
- package/README.md +4 -4
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -22
- package/dist/runtime/conversations/components/chat-conversation-item.vue +1 -1
- package/dist/runtime/messages/components/chat-message-bubble.vue +11 -15
- package/dist/runtime/messages/components/chat-message-list.d.vue.ts +2 -2
- package/dist/runtime/messages/components/chat-message-list.vue +31 -10
- package/dist/runtime/messages/components/chat-message-list.vue.d.ts +2 -2
- package/dist/runtime/rendering/components/app-markdown-render.d.vue.ts +2 -0
- package/dist/runtime/rendering/components/app-markdown-render.vue +7 -21
- package/dist/runtime/rendering/components/app-markdown-render.vue.d.ts +2 -0
- package/dist/runtime/rendering/markdown/custom-components.d.ts +2 -0
- package/dist/runtime/rendering/markdown/custom-components.js +8 -2
- package/package.json +3 -4
- package/dist/runtime/messages/composables/chat-scroll-stickiness.d.ts +0 -28
- package/dist/runtime/messages/composables/chat-scroll-stickiness.js +0 -181
- package/dist/runtime/rendering/components/renderers/mermaid-diagram.d.vue.ts +0 -7
- package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue +0 -168
- package/dist/runtime/rendering/components/renderers/mermaid-diagram.vue.d.ts +0 -7
- package/dist/runtime/rendering/composables/diagram-export.d.ts +0 -11
- package/dist/runtime/rendering/composables/diagram-export.js +0 -187
- package/dist/runtime/rendering/composables/mermaid-renderer.d.ts +0 -5
- package/dist/runtime/rendering/composables/mermaid-renderer.js +0 -121
- package/dist/runtime/rendering/composables/zoom-pan.d.ts +0 -18
- package/dist/runtime/rendering/composables/zoom-pan.js +0 -130
- package/dist/runtime/rendering/markdown/nodes.d.ts +0 -23
- 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
|
-
| `
|
|
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` | `
|
|
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
package/dist/module.mjs
CHANGED
|
@@ -145,8 +145,12 @@ function patchStreamMarkdownParserSource(source) {
|
|
|
145
145
|
}
|
|
146
146
|
function resolvePatchedStreamMarkdownParser(from) {
|
|
147
147
|
try {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
|
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
|
-
:
|
|
249
|
-
:
|
|
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
|
-
:
|
|
272
|
-
:
|
|
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:
|
|
17
|
-
scrollToBottom:
|
|
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 {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
17
|
-
scrollToBottom:
|
|
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, {
|
|
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
|
|
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
|
-
:
|
|
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
|
|
13
|
+
if (RESERVED_CUSTOM_MARKDOWN_PROPS.has(propName))
|
|
13
14
|
continue;
|
|
14
15
|
attrProps[propName] = value;
|
|
15
16
|
}
|
|
16
|
-
return {
|
|
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.
|
|
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": "
|
|
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;
|