feat(chat): Enhanced Chat Experience with Multimodality and Optimized Streaming (#445)

* feat(chat): Refactor streaming message handling and add math support
* feat(chat): Basic multimodalities
* fix: language regex and python as default
* fix: refactor llm stream event as one onStreamEvent
This commit is contained in:
Ilya Bogdanov
2025-08-26 16:53:46 +08:00
committed by GitHub
parent 1d26a2a98c
commit 5ec5b24f07
9 changed files with 486 additions and 335 deletions
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { UserMessagePart } from '@xsai/shared-chat'
import { MarkdownRenderer } from '@proj-airi/stage-ui/components'
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
import { storeToRefs } from 'pinia'
@@ -8,7 +10,7 @@ import { useI18n } from 'vue-i18n'
const chatHistoryRef = ref<HTMLDivElement>()
const { t } = useI18n()
const { messages, sending } = storeToRefs(useChatStore())
const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
@@ -34,75 +36,107 @@ onTokenLiteral(async () => {
</script>
<template>
<div relative px="<sm:2" py="<sm:2" flex="~ col" rounded="lg" overflow-hidden>
<div flex-1 /> <!-- spacer -->
<div ref="chatHistoryRef" v-auto-animate h-full w-full flex="~ col" overflow-scroll>
<div flex-1 /> <!-- spacer -->
<div v-for="(message, index) in messages" :key="index" mb-2>
<div v-if="message.role === 'error'" flex mr="12">
<div
flex="~ col" border="2 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
>
<div flex="~ row" gap-2>
<div flex-1>
<span text-xs text="violet-400/90 dark:violet-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.core-system') }}</span>
</div>
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
<div ref="chatHistoryRef" v-auto-animate flex="~ col" relative h-full w-full overflow-y-auto rounded-lg px="<sm:2" py="<sm:2">
<div v-for="(message, index) in messages" :key="index" mb-2>
<div v-if="message.role === 'error'" flex mr="12">
<div
flex="~ col" border="2 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
>
<div flex="~ row" gap-2>
<div flex-1>
<span text-xs text="violet-400/90 dark:violet-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.core-system') }}</span>
</div>
<div v-if="sending && index === messages.length - 1" i-eos-icons:three-dots-loading />
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
</div>
<div v-if="sending && index === messages.length - 1" i-eos-icons:three-dots-loading />
<MarkdownRenderer
v-else
:content="message.content as string"
class="break-words text-violet-500"
text="base <sm:xs"
/>
</div>
</div>
<div v-if="message.role === 'assistant' && message.content !== ''" flex mr="12">
<div
flex="~ col" border="2 solid primary-200/50 dark:primary-500/50" shadow="md primary-200/50 dark:none" min-w-20
rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:primary-500/25"
>
<div>
<span text-xs text="primary-400/90 dark:primary-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.airi') }}</span>
</div>
<div class="break-words" text="xs primary-400">
<div v-for="(slice, sliceIndex) in message.slices" :key="sliceIndex">
<div v-if="slice.type === 'tool-call'">
<div
p="1" border="1 solid primary-200" rounded-lg m="y-1" bg="primary-100"
>
Called: <code>{{ slice.toolCall.toolName }}</code>
</div>
</div>
<div v-else-if="slice.type === 'tool-call-result'" /> <!-- this line should be unreachable -->
<MarkdownRenderer
v-else
:content="slice.text"
/>
</div>
</div>
</div>
</div>
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
<div
flex="~ col" border="2 solid cyan-200/50 dark:cyan-500/50" shadow="md cyan-200/50 dark:none" px="2"
h="unset <sm:fit" min-w-20 rounded-lg px-2 py-1 bg="<md:cyan-500/25"
>
<div>
<span text-xs text="cyan-400/90 dark:cyan-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.you') }}</span>
</div>
<div v-if="Array.isArray(message.content)" class="flex flex-col gap-2">
<template v-for="(part, partIndex) in (message.content as UserMessagePart[])" :key="partIndex">
<MarkdownRenderer
v-if="part.type === 'text' && part.text"
:content="part.text"
class="break-words"
text="base <sm:xs"
/>
<img v-if="part.type === 'image_url'" :src="part.image_url.url" class="max-w-full rounded-lg">
</template>
</div>
<MarkdownRenderer
v-else-if="typeof message.content === 'string'"
:content="message.content"
class="break-words"
text="base <sm:xs"
/>
</div>
</div>
</div>
<div v-if="sending" flex mr="12">
<div
flex="~ col" border="2 solid primary-200/50 dark:primary-500/50" shadow="md primary-200/50 dark:none" min-w-20
rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:primary-500/25"
>
<div>
<span text-xs text="primary-400/90 dark:primary-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.airi') }}</span>
</div>
<div v-if="streamingMessage.content" class="break-words" text="xs primary-400">
<div v-for="(slice, sliceIndex) in streamingMessage.slices" :key="sliceIndex">
<div v-if="slice.type === 'tool-call'">
<div
p="1" border="1 solid primary-200" rounded-lg m="y-1" bg="primary-100"
>
Called: <code>{{ slice.toolCall.toolName }}</code>
</div>
</div>
<div v-else-if="slice.type === 'tool-call-result'" /> <!-- this line should be unreachable -->
<MarkdownRenderer
v-else
:content="message.content as string"
class="break-words text-violet-500"
text="base <sm:xs"
:content="slice.text"
/>
</div>
</div>
<div v-if="message.role === 'assistant'" flex mr="12">
<div
flex="~ col" border="2 solid primary-200/50 dark:primary-500/50" shadow="md primary-200/50 dark:none" min-w-20
rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:primary-500/25"
>
<div>
<span text-xs text="primary-400/90 dark:primary-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.airi') }}</span>
</div>
<div v-if="message.content && index === messages.length - 1" class="break-words" text="xs primary-400">
<div v-for="(slice, sliceIndex) in message.slices" :key="sliceIndex">
<div v-if="slice.type === 'tool-call'">
<div
p="1" border="1 solid primary-200" rounded-lg m="y-1" bg="primary-100"
>
Called: <code>{{ slice.toolCall.toolName }}</code>
</div>
</div>
<div v-else-if="slice.type === 'tool-call-result'" /> <!-- this line should be unreachable -->
<MarkdownRenderer
v-else
:content="slice.text"
/>
</div>
</div>
<div v-else-if="index === messages.length - 1 && !message.content" i-eos-icons:three-dots-loading />
</div>
</div>
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
<div
flex="~ col" border="2 solid cyan-200/50 dark:cyan-500/50" shadow="md cyan-200/50 dark:none" px="2"
h="unset <sm:fit" min-w-20 rounded-lg px-2 py-1 bg="<md:cyan-500/25"
>
<div>
<span text-xs text="cyan-400/90 dark:cyan-600/90" font-normal class="inline <sm:hidden">{{ t('stage.chat.message.character-name.you') }}</span>
</div>
<MarkdownRenderer
v-if="message.content"
:content="message.content as string"
class="break-words"
text="base <sm:xs"
/>
<div v-else />
</div>
</div>
<div v-else i-eos-icons:three-dots-loading />
</div>
</div>
</div>
@@ -15,6 +15,7 @@ import TamagotchiChatHistory from './ChatHistory.vue'
const messageInput = ref('')
const listening = ref(false)
const attachments = ref<{ type: 'image', data: string, mimeType: string, url: string }[]>([])
// const { askPermission } = useSettingsAudioDevice()
const { enabled, selectedAudioInput } = storeToRefs(useSettingsAudioDevice())
@@ -25,16 +26,18 @@ const providersStore = useProvidersStore()
const { activeModel, activeProvider } = storeToRefs(useConsciousnessStore())
async function handleSend() {
if (!messageInput.value.trim()) {
if (!messageInput.value.trim() && !attachments.value.length) {
return
}
try {
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
const attachmentsToSend = attachments.value.map(({ data, mimeType, type }) => ({ data, mimeType, type }))
await send(messageInput.value, {
model: activeModel.value,
chatProvider: await providersStore.getProviderInstance<ChatProvider>(activeProvider.value),
providerConfig,
attachments: attachmentsToSend,
})
}
catch (error) {
@@ -46,6 +49,34 @@ async function handleSend() {
}
}
async function handleFilePaste(files: File[]) {
for (const file of files) {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
const base64Data = (e.target?.result as string)?.split(',')[1]
if (base64Data) {
attachments.value.push({
type: 'image' as const,
data: base64Data,
mimeType: file.type,
url: URL.createObjectURL(file),
})
}
}
reader.readAsDataURL(file)
}
}
}
function removeAttachment(index: number) {
const attachment = attachments.value[index]
if (attachment) {
URL.revokeObjectURL(attachment.url)
attachments.value.splice(index, 1)
}
}
const { destroy, start } = useMicVAD(selectedAudioInput, {
onSpeechStart: () => {
// TODO: interrupt the playback
@@ -102,6 +133,8 @@ watch([activeProvider, activeModel], async () => {
onAfterMessageComposed(async () => {
messageInput.value = ''
attachments.value.forEach(att => URL.revokeObjectURL(att.url))
attachments.value = []
})
onMounted(() => {
@@ -110,21 +143,28 @@ onMounted(() => {
</script>
<template>
<div>
<div h-full w-full flex="~ col gap-1">
<div w-full flex-1>
<TamagotchiChatHistory />
</div>
<BasicTextarea
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100"
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:primary-100" max-h="[10lh]" min-h="[1lh]"
w-full shrink-0 resize-none overflow-y-scroll rounded-xl p-2 font-medium outline-none
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
@submit="handleSend"
/>
<div h-full w-full flex="~ col gap-1">
<div w-full flex-1 overflow-hidden>
<TamagotchiChatHistory />
</div>
<div v-if="attachments.length > 0" class="flex flex-wrap gap-2 border-t border-primary-100 p-2">
<div v-for="(attachment, index) in attachments" :key="index" class="relative">
<img :src="attachment.url" class="h-20 w-20 rounded-md object-cover">
<button class="absolute right-1 top-1 h-5 w-5 flex items-center justify-center rounded-full bg-red-500 text-xs text-white" @click="removeAttachment(index)">
&times;
</button>
</div>
</div>
<BasicTextarea
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100"
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:primary-100" max-h="[10lh]" min-h="[1lh]"
w-full shrink-0 resize-none overflow-y-scroll rounded-xl p-2 font-medium outline-none
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
@submit="handleSend"
@paste-file="handleFilePaste"
/>
</div>
</template>
+2
View File
@@ -100,8 +100,10 @@
"pixi-filters": "4",
"pixi-live2d-display": "^0.4.0",
"postprocessing": "^6.37.7",
"rehype-katex": "^7.0.1",
"rehype-stringify": "^10.0.1",
"reka-ui": "^2.4.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shiki": "^3.9.1",
@@ -1,85 +1,68 @@
enum States {
Literal = 'literal',
Special = 'special',
}
function peek(array: string, index: number, offset: number): string | undefined {
if (index + offset < 0 || index + offset >= (array.length - 1))
return ''
return array[index + offset]
}
/**
* A streaming parser for LLM responses that contain special markers (e.g., for tool calls).
* This composable is designed to be efficient and robust, using a regular expression
* to handle special tags enclosed in `<|...|>`.
*
* @example
* const parser = useLlmmarkerParser({
* onLiteral: (text) => console.log('Literal:', text),
* onSpecial: (tagContent) => console.log('Special:', tagContent),
* });
*
* await parser.consume('This is some text <|tool_code|> and some more |> text.');
* await parser.end();
*/
export function useLlmmarkerParser(options: {
onLiteral?: (literal: string) => void | Promise<void>
onSpecial?: (special: string) => void | Promise<void>
}) {
let state = States.Literal
let buffer = ''
const tagRegex = /(<\|.*?\|>)/
return {
/**
* Consumes a chunk of text from the stream.
* It processes the internal buffer to find and emit complete literal and special parts.
* Incomplete parts are kept in the buffer to be processed with the next chunk.
* @param textPart The chunk of text to consume.
*/
async consume(textPart: string) {
for (let i = 0; i < textPart.length; i++) {
let current = textPart[i]
let newState: States = state
buffer += textPart
// read
if (current === '<' && peek(textPart, i, 1) === '|') {
current += peek(textPart, i, 1)
newState = States.Special
i++
}
else if (current === '|' && peek(textPart, i, 1) === '>') {
current += peek(textPart, i, 1)
newState = States.Literal
i++
}
else if (current === '<') {
newState = States.Special
}
else if (current === '>') {
newState = States.Literal
}
// The regex splits the buffer by tags, keeping the tags in the result array.
const parts = buffer.split(tagRegex)
// handle
if (state === States.Literal && newState === States.Special) {
if (buffer !== '') {
await options.onLiteral?.(buffer)
buffer = ''
}
}
else if (state === States.Special && newState === States.Literal) {
if (buffer !== '') {
buffer += current
await options.onSpecial?.(buffer)
buffer = '' // Clear buffer when exiting Special state
}
}
// The last element of the array is the remainder of the string after the last
// complete tag. It could be a partial literal or a partial tag. We keep it
// in the buffer for the next consume call.
const processableParts = parts.slice(0, -1)
buffer = parts[parts.length - 1] || ''
if (state === States.Literal && newState === States.Literal) {
await options.onLiteral?.(current)
buffer = ''
}
else if (state === States.Special && newState === States.Literal) {
buffer = ''
for (const part of processableParts) {
if (!part)
continue // Skip empty strings that can result from the split.
// Check if the part is a tag or a literal.
if (tagRegex.test(part)) {
// Extract the content from inside the tag and pass it to the callback.
const specialContent = part.slice(2, -2)
await options.onSpecial?.(specialContent)
}
else {
buffer += current
await options.onLiteral?.(part)
}
state = newState
}
},
/**
* Finalizes the parsing process.
* Any remaining content in the buffer is flushed as a final literal part.
* This should be called after the stream has ended.
*/
async end() {
if (buffer !== '') {
if (state === States.Literal) {
await options.onLiteral?.(buffer)
}
else {
if (buffer.endsWith('|>')) {
await options.onSpecial?.(buffer)
}
}
if (buffer) {
await options.onLiteral?.(buffer)
buffer = ''
}
},
}
+66 -9
View File
@@ -1,38 +1,95 @@
import type { RehypeShikiOptions } from '@shikijs/rehype'
import type { BundledLanguage } from 'shiki'
import type { Processor } from 'unified'
import rehypeShiki from '@shikijs/rehype'
import rehypeKatex from 'rehype-katex'
import RehypeStringify from 'rehype-stringify'
import remarkMath from 'remark-math'
import RemarkParse from 'remark-parse'
import RemarkRehype from 'remark-rehype'
import { unified } from 'unified'
async function createProcessor() {
// Define a specific, compatible type for our processor to ensure type safety.
type MarkdownProcessor = Processor<any, any, any, any, string>
const processorCache = new Map<string, Promise<MarkdownProcessor>>()
const langRegex = /```(.{2,})\s/g
function extractLangs(markdown: string): BundledLanguage[] {
const matches = markdown.matchAll(langRegex)
const langs = new Set<BundledLanguage>()
langs.add('python')
for (const match of matches) {
if (match[1])
langs.add(match[1] as BundledLanguage)
}
return [...langs]
}
async function createProcessor(langs: BundledLanguage[]): Promise<MarkdownProcessor> {
const options: RehypeShikiOptions = {
themes: {
light: 'github-light',
dark: 'github-dark',
},
langs,
defaultLanguage: langs[0] || 'python',
}
return unified()
.use(RemarkParse)
.use(remarkMath)
.use(RemarkRehype)
.use(rehypeShiki, {
themes: {
light: 'github-light',
dark: 'github-dark',
},
})
.use(rehypeKatex)
.use(rehypeShiki, options)
.use(RehypeStringify)
}
function getProcessor(langs: BundledLanguage[]): Promise<MarkdownProcessor> {
// The cache key should be consistent, so we sort the languages.
const cacheKey = [...langs].sort().join(',')
if (!processorCache.has(cacheKey)) {
const processorPromise = createProcessor(langs)
processorCache.set(cacheKey, processorPromise)
}
return processorCache.get(cacheKey)!
}
export function useMarkdown() {
const fallbackProcessor = unified()
.use(RemarkParse)
.use(remarkMath)
.use(RemarkRehype)
.use(rehypeKatex)
.use(RehypeStringify)
return {
process: async (markdown: string): Promise<string> => {
try {
const processor = await createProcessor()
// A quick check for code fences. If none, use the fast fallback.
if (!/`{3,}/.test(markdown))
return fallbackProcessor.processSync(markdown).toString()
const langs = extractLangs(markdown)
// Always ensure 'python' is loaded as it's our default.
const langSet = new Set(langs)
langSet.add('python')
const languagesToLoad = Array.from(langSet)
const processor = await getProcessor(languagesToLoad)
const result = await processor.process(markdown)
return result.toString()
}
catch (error) {
console.warn('Failed to process markdown with syntax highlighting, falling back to basic processing:', error)
console.warn(
'Failed to process markdown with syntax highlighting, falling back to basic processing:',
error,
)
// Fallback to basic processor without highlighting
return fallbackProcessor.processSync(markdown).toString()
}
+87 -58
View File
@@ -1,9 +1,9 @@
import type { ChatProvider } from '@xsai-ext/shared-providers'
import type { Message, SystemMessage } from '@xsai/shared-chat'
import type { Message, SystemMessage, UserMessagePart } from '@xsai/shared-chat'
import type { StreamEvent } from '../stores/llm'
import type { ChatAssistantMessage, ChatMessage, ChatSlices } from '../types/chat'
import { readableStreamToAsyncIterator } from '@moeru/std'
import { defineStore, storeToRefs } from 'pinia'
import { ref, toRaw } from 'vue'
@@ -65,26 +65,57 @@ export const useChatStore = defineStore('chat', () => {
onAssistantResponseEndHooks.value.push(cb)
}
// I know this nu uh, better than loading all language on rehypeShiki
const codeBlockSystemPrompt = '- For any programming code block, always specify the programming language that supported on @shikijs/rehype on the rendered markdown, eg. ```python ... ```\n'
const mathSyntaxSystemPrompt = '- For any math equation, use LaTeX format, eg: $ x^3 $, always escape dollar sign outside math equation\n'
const messages = ref<Array<ChatMessage | ErrorMessage>>([
{
role: 'system',
content: systemPrompt.value, // TODO: compose, replace {{ user }} tag, etc
content: codeBlockSystemPrompt + mathSyntaxSystemPrompt + systemPrompt.value, // TODO: compose, replace {{ user }} tag, etc
} satisfies SystemMessage,
])
const streamingMessage = ref<ChatAssistantMessage>({ role: 'assistant', content: '', slices: [], tool_results: [] })
async function send(sendingMessage: string, options: { model: string, chatProvider: ChatProvider, providerConfig?: Record<string, unknown> }) {
async function send(
sendingMessage: string,
options: {
model: string
chatProvider: ChatProvider
providerConfig?: Record<string, unknown>
attachments?: { type: 'image', data: string, mimeType: string }[]
},
) {
try {
sending.value = true
if (!sendingMessage)
if (!sendingMessage && !options.attachments?.length)
return
for (const hook of onBeforeMessageComposedHooks.value) {
await hook(sendingMessage)
}
const contentParts: UserMessagePart[] = [{ type: 'text', text: sendingMessage }]
if (options.attachments) {
for (const attachment of options.attachments) {
if (attachment.type === 'image') {
contentParts.push({
type: 'image_url',
image_url: {
url: `data:${attachment.mimeType};base64,${attachment.data}`,
},
})
}
}
}
const finalContent = contentParts.length > 1 ? contentParts : sendingMessage
messages.value.push({ role: 'user', content: finalContent })
const parser = useLlmmarkerParser({
onLiteral: async (literal) => {
for (const hook of onTokenLiteralHooks.value) {
@@ -112,14 +143,9 @@ export const useChatStore = defineStore('chat', () => {
},
})
const slicesQueue = useQueue<ChatSlices>({
const toolCallQueue = useQueue<ChatSlices>({
handlers: [
async (ctx) => { // FIXME: it still looks dirty
if (ctx.data.type === 'text') {
await parser.consume(ctx.data.text)
return
}
async (ctx) => {
if (ctx.data.type === 'tool-call') {
streamingMessage.value.slices.push(ctx.data)
return
@@ -133,9 +159,7 @@ export const useChatStore = defineStore('chat', () => {
})
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] }
messages.value.push({ role: 'user', content: sendingMessage })
messages.value.push(streamingMessage.value)
const newMessages = messages.value.slice(0, messages.value.length - 1).map((msg) => {
const newMessages = messages.value.map((msg) => {
if (msg.role === 'assistant') {
const { slices: _, ...rest } = msg // exclude slices
rest.tool_results = toRaw(rest.tool_results)
@@ -152,57 +176,62 @@ export const useChatStore = defineStore('chat', () => {
await hook(sendingMessage)
}
let fullText = ''
const headers = (options.providerConfig?.headers || {}) as Record<string, string>
const res = await stream(options.model, options.chatProvider, newMessages as Message[], {
await stream(options.model, options.chatProvider, newMessages as Message[], {
headers,
onToolCall(toolCall) {
slicesQueue.add({
type: 'tool-call',
toolCall,
})
},
onToolCallResult(toolCallResult) {
slicesQueue.add({
type: 'tool-call-result',
id: toolCallResult.id,
result: toolCallResult.result,
})
async onStreamEvent(event: StreamEvent) {
if (event.type === 'tool-call') {
toolCallQueue.add({
type: 'tool-call',
toolCall: event,
})
}
else if (event.type === 'tool-result') {
toolCallQueue.add({
type: 'tool-call-result',
id: event.toolCallId,
result: event.result,
})
}
else if (event.type === 'text-delta') {
fullText += event.text
await parser.consume(event.text)
}
else if (event.type === 'finish') {
// Finalize the parsing of the actual message content
await parser.end()
// Add the completed message to the history only if it has content
if (streamingMessage.value.slices.length > 0)
messages.value.push(toRaw(streamingMessage.value))
// Reset the streaming message for the next turn
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] }
// Instruct the TTS pipeline to flush by calling hooks directly
const flushSignal = `${TTS_FLUSH_INSTRUCTION}${TTS_FLUSH_INSTRUCTION}`
for (const hook of onTokenLiteralHooks.value)
await hook(flushSignal)
// Call the end-of-stream hooks
for (const hook of onStreamEndHooks.value)
await hook()
// Call the end-of-response hooks with the full text
for (const hook of onAssistantResponseEndHooks.value)
await hook(fullText)
// eslint-disable-next-line no-console
console.debug('LLM output:', fullText)
}
},
})
for (const hook of onAfterSendHooks.value) {
await hook(sendingMessage)
}
let fullText = ''
for await (const textPart of readableStreamToAsyncIterator(res.textStream)) {
slicesQueue.add({
type: 'text',
text: textPart,
})
fullText += textPart
}
// Instruct the TTS pipeline to flush
// 2x TTS_FLUSH_INSTRUCTION tokens are used:
// - One should be left in the grapheme cluster reader
// - The other should be left in the TTS chunking reader
// Because both readers will never be closed in the lifecycle of the app
slicesQueue.add({ type: 'text', text: `${TTS_FLUSH_INSTRUCTION}${TTS_FLUSH_INSTRUCTION}` })
await parser.end()
for (const hook of onStreamEndHooks.value) {
await hook()
}
for (const hook of onAssistantResponseEndHooks.value) {
await hook(fullText)
}
// eslint-disable-next-line no-console
console.debug('LLM output:', fullText)
}
catch (error) {
console.error('Error sending message:', error)
+9 -11
View File
@@ -10,13 +10,16 @@ import { ref } from 'vue'
import { debug, mcp } from '../tools'
export type StreamEvent
= | { type: 'text-delta', text: string }
| ({ type: 'finish' } & any)
| ({ type: 'tool-call' } & CompletionToolCall)
| { type: 'tool-result', toolCallId: string, result?: string | ToolMessagePart[] }
| { type: 'error', error: any }
export interface StreamOptions {
headers?: Record<string, string>
onToolCall?: (toolCall: CompletionToolCall) => void
onToolCallResult?: (toolCallResult: {
id: string
result?: string | ToolMessagePart[]
}) => void
onStreamEvent?: (event: StreamEvent) => void | Promise<void>
toolsCompatibility?: Map<string, boolean>
supportsTools?: boolean
}
@@ -42,12 +45,7 @@ async function streamFrom(model: string, chatProvider: ChatProvider, messages: M
]
: undefined,
onEvent(event) {
if (event.type === 'tool-call') {
options?.onToolCall?.(event)
}
else if (event.type === 'tool-result') {
options?.onToolCallResult?.({ id: event.toolCallId, result: event.result })
}
options?.onStreamEvent?.(event as StreamEvent)
},
})
}
@@ -7,6 +7,7 @@ const props = defineProps<{
const events = defineEmits<{
(event: 'submit', message: string): void
(event: 'pasteFile', files: File[]): void
}>()
const input = defineModel<string>({
@@ -23,6 +24,17 @@ function onKeyDown(e: KeyboardEvent) {
}
}
function onPaste(e: ClipboardEvent) {
if (!e.clipboardData)
return
const { files } = e.clipboardData
if (files.length > 0) {
e.preventDefault()
events('pasteFile', Array.from(files))
}
}
// javascript - Creating a textarea with auto-resize - Stack Overflow
// https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize
watch(input, () => {
@@ -46,5 +58,6 @@ watch(input, () => {
v-model="input"
:style="{ height: textareaHeight }"
@keydown="onKeyDown"
@paste="onPaste"
/>
</template>
+107 -112
View File
@@ -1131,7 +1131,7 @@ importers:
version: 0.1.3
unplugin-yaml:
specifier: ^3.0.2
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
vitepress:
specifier: ^2.0.0-alpha.9
version: 2.0.0-alpha.9(@types/node@24.1.0)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
@@ -1174,7 +1174,7 @@ importers:
devDependencies:
unplugin-yaml:
specifier: ^3.0.2
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
packages/memory-pgvector:
dependencies:
@@ -1416,12 +1416,18 @@ importers:
postprocessing:
specifier: ^6.37.7
version: 6.37.7(three@0.179.0)
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
reka-ui:
specifier: ^2.4.1
version: 2.4.1(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))
remark-math:
specifier: ^6.0.0
version: 6.0.0
remark-parse:
specifier: ^11.0.0
version: 11.0.0
@@ -1563,7 +1569,7 @@ importers:
version: 1.2.4(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(esbuild@0.25.8)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
unplugin-yaml:
specifier: ^3.0.2
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
vite:
specifier: ^6.3.5
version: 6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
@@ -5920,6 +5926,9 @@ packages:
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
@@ -7523,6 +7532,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
@@ -9027,6 +9040,12 @@ packages:
hast-util-embedded@3.0.0:
resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==}
hast-util-from-dom@5.0.1:
resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
hast-util-from-html-isomorphic@2.0.0:
resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
@@ -9610,6 +9629,10 @@ packages:
jws@4.0.0:
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
katex@0.16.22:
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
hasBin: true
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -9987,6 +10010,9 @@ packages:
mdast-util-gfm@3.1.0:
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
mdast-util-math@3.0.0:
resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
@@ -10064,6 +10090,9 @@ packages:
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
micromark-extension-math@3.1.0:
resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
@@ -11352,6 +11381,9 @@ packages:
resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
hasBin: true
rehype-katex@7.0.1:
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
rehype-minify-whitespace@6.0.2:
resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==}
@@ -11381,6 +11413,9 @@ packages:
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-math@6.0.0:
resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
@@ -13775,7 +13810,7 @@ snapshots:
remark-parse: 11.0.0
remark-rehype: 11.1.2
remark-smartypants: 3.0.2
shiki: 3.9.1
shiki: 3.11.0
smol-toml: 1.4.1
unified: 11.0.5
unist-util-remove-position: 5.0.0
@@ -17836,6 +17871,8 @@ snapshots:
dependencies:
'@types/node': 24.1.0
'@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {}
'@types/long@4.0.2': {}
@@ -19563,7 +19600,7 @@ snapshots:
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.2
shiki: 3.9.1
shiki: 3.11.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tsconfck: 3.1.6(typescript@5.9.2)
@@ -19664,108 +19701,7 @@ snapshots:
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.2
shiki: 3.9.1
tinyexec: 0.3.2
tinyglobby: 0.2.14
tsconfck: 3.1.6(typescript@5.9.2)
ultrahtml: 1.6.0
unifont: 0.5.2
unist-util-visit: 5.0.0
unstorage: 1.16.0
vfile: 6.0.3
vite: 6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
vitefu: 1.0.7(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.2)(zod@3.25.76)
optionalDependencies:
sharp: 0.33.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/kv'
- aws4fetch
- db0
- encoding
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
optional: true
astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0):
dependencies:
'@astrojs/compiler': 2.12.2
'@astrojs/internal-helpers': 0.6.1
'@astrojs/markdown-remark': 6.3.2
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 2.4.0(encoding@0.1.13)
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
acorn: 8.15.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.3.0
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.0.2
cssesc: 3.0.0
debug: 4.4.1
deterministic-object-hash: 2.0.2
devalue: 5.1.1
diff: 5.2.0
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.25.8
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.3.0
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.1.0
js-yaml: 4.1.0
kleur: 4.1.5
magic-string: 0.30.17
magicast: 0.3.5
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.0
package-manager-detector: 1.3.0
picomatch: 4.0.3
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.2
shiki: 3.9.1
shiki: 3.11.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tsconfck: 3.1.6(typescript@5.9.2)
@@ -20293,6 +20229,8 @@ snapshots:
commander@4.1.1: {}
commander@8.3.0: {}
commander@9.5.0: {}
comment-parser@1.4.1: {}
@@ -22021,6 +21959,19 @@ snapshots:
'@types/hast': 3.0.4
hast-util-is-element: 3.0.0
hast-util-from-dom@5.0.1:
dependencies:
'@types/hast': 3.0.4
hastscript: 9.0.0
web-namespaces: 2.0.1
hast-util-from-html-isomorphic@2.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-from-dom: 5.0.1
hast-util-from-html: 2.0.3
unist-util-remove-position: 5.0.0
hast-util-from-html@2.0.3:
dependencies:
'@types/hast': 3.0.4
@@ -22715,6 +22666,10 @@ snapshots:
jwa: 2.0.0
safe-buffer: '@nolyfill/safe-buffer@1.0.44'
katex@0.16.22:
dependencies:
commander: 8.3.0
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -23171,6 +23126,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
mdast-util-math@3.0.0:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
devlop: 1.1.0
longest-streak: 3.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
unist-util-remove-position: 5.0.0
transitivePeerDependencies:
- supports-color
mdast-util-phrasing@4.1.0:
dependencies:
'@types/mdast': 4.0.4
@@ -23310,6 +23277,16 @@ snapshots:
micromark-util-combine-extensions: 2.0.1
micromark-util-types: 2.0.1
micromark-extension-math@3.1.0:
dependencies:
'@types/katex': 0.16.7
devlop: 1.1.0
katex: 0.16.22
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.1
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -24948,6 +24925,16 @@ snapshots:
dependencies:
jsesc: 3.0.2
rehype-katex@7.0.1:
dependencies:
'@types/hast': 3.0.4
'@types/katex': 0.16.7
hast-util-from-html-isomorphic: 2.0.0
hast-util-to-text: 4.0.2
katex: 0.16.22
unist-util-visit-parents: 6.0.1
vfile: 6.0.3
rehype-minify-whitespace@6.0.2:
dependencies:
'@types/hast': 3.0.4
@@ -25023,6 +25010,15 @@ snapshots:
- supports-color
optional: true
remark-math@6.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-math: 3.0.0
micromark-extension-math: 3.1.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -26311,7 +26307,6 @@ snapshots:
dependencies:
'@types/unist': 3.0.0
unist-util-visit: 5.0.0
optional: true
unist-util-remove@4.0.0:
dependencies:
@@ -26698,7 +26693,7 @@ snapshots:
rollup: 4.46.2
vite: rolldown-vite@7.0.12(@types/node@24.1.0)(esbuild@0.25.8)(jiti@2.5.1)(less@4.4.0)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
unplugin: 2.3.5
@@ -26706,13 +26701,13 @@ snapshots:
optionalDependencies:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
astro: 5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
astro: 5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
esbuild: 0.25.8
rolldown: 1.0.0-beta.30
rollup: 4.46.2
vite: 6.3.5(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
unplugin: 2.3.5
@@ -26720,7 +26715,7 @@ snapshots:
optionalDependencies:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
astro: 5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
astro: 5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
esbuild: 0.25.8
rolldown: 1.0.0-beta.30
rollup: 4.46.2