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:
@@ -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)">
|
||||
×
|
||||
</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>
|
||||
|
||||
@@ -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 = ''
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+107
-112
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user