feat(stage-ui): process for reasoning models (#859)
--------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Rin <shinohara-rin@users.noreply.github.com> Co-authored-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
co-authored by
gemini-code-assist[bot]
Rin
Neko Ayaka
parent
4c0d81e4e7
commit
1f58faafa2
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Core System
|
||||
you: You
|
||||
reasoning: Reasoning
|
||||
message: Say something...
|
||||
operations:
|
||||
load-models: Load Models
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Sistema Central
|
||||
you: Tu
|
||||
reasoning: Razonamiento
|
||||
message: Dile algo...
|
||||
operations:
|
||||
load-models: Cargar Modelos
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Système central
|
||||
you: Vous
|
||||
reasoning: Raisonnement
|
||||
message: Dites quelque chose...
|
||||
operations:
|
||||
load-models: Charger les modèles
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: コアシステム
|
||||
you: あなた
|
||||
reasoning: 推論
|
||||
message: 何か話しかけてみましょう…
|
||||
operations:
|
||||
load-models: モデルの読み込み
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Core System
|
||||
you: 당신
|
||||
reasoning: 추론
|
||||
message: 듣고 있어요...
|
||||
operations:
|
||||
load-models: 모델 불러오기
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Core система
|
||||
you: Ты
|
||||
reasoning: Рассуждение
|
||||
message: Спроси что-нибудь
|
||||
operations:
|
||||
load-models: Загрузить модели
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: Hệ thống lõi
|
||||
you: Bạn
|
||||
reasoning: Lý luận
|
||||
message: Nói gì đó...
|
||||
operations:
|
||||
load-models: Tải mô hình
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: 核心系统
|
||||
you: 你
|
||||
reasoning: 推理
|
||||
message: 说点什么...
|
||||
operations:
|
||||
load-models: 加载模型
|
||||
|
||||
@@ -4,6 +4,7 @@ chat:
|
||||
airi: AIRI
|
||||
core-system: 核心系統
|
||||
you: 你
|
||||
reasoning: 推理
|
||||
message: 說點什麼...
|
||||
operations:
|
||||
load-models: 載入模型
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
"posthog-js": "catalog:",
|
||||
"postprocessing": "^6.38.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"reka-ui": "^2.7.0",
|
||||
"remark-math": "^6.0.0",
|
||||
@@ -132,6 +133,7 @@
|
||||
"three": "^0.182.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-builder": "^4.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unspeech": "catalog:xsai",
|
||||
"uuid": "^13.0.0",
|
||||
"valibot": "catalog:",
|
||||
@@ -170,8 +172,10 @@
|
||||
"@proj-airi/vite-plugin-warpdrive": "workspace:*",
|
||||
"@types/audioworklet": "catalog:",
|
||||
"@types/culori": "^4.0.1",
|
||||
"@types/hast": "catalog:",
|
||||
"@types/splitpanes": "catalog:",
|
||||
"@types/three": "^0.182.0",
|
||||
"@types/unist": "catalog:",
|
||||
"@unocss/reset": "^66.5.11",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"clustr": "^1.0.2",
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ChatAssistantMessage, ChatSlices, ChatSlicesText } from '../../../
|
||||
import { computed } from 'vue'
|
||||
|
||||
import MarkdownRenderer from '../../markdown/MarkdownRenderer.vue'
|
||||
import ChatResponsePart from './ChatResponsePart.vue'
|
||||
import ChatToolCallBlock from './ChatToolCallBlock.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -66,6 +67,12 @@ const boxClasses = computed(() => [
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="showLoader" i-eos-icons:three-dots-loading />
|
||||
|
||||
<ChatResponsePart
|
||||
v-if="message.categorization"
|
||||
:message="message"
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatAssistantMessage } from '../../../types/chat'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MarkdownRenderer from '../../markdown/MarkdownRenderer.vue'
|
||||
import Collapsable from '../../misc/Collapsable.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
message: ChatAssistantMessage
|
||||
variant?: 'desktop' | 'mobile'
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const hasReasoning = computed(() => !!props.message.categorization?.reasoning?.trim())
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
'mt-2',
|
||||
props.variant === 'mobile' ? 'text-xs' : 'text-sm',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasReasoning" :class="containerClasses" flex="~ col" gap-1>
|
||||
<Collapsable :default="false">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
class="w-full flex items-center justify-between rounded-lg bg-neutral-100/50 px-2 py-1 text-xs text-neutral-600 outline-none transition-all duration-200 dark:bg-neutral-800/50 hover:bg-neutral-200/50 dark:text-neutral-400 dark:hover:bg-neutral-700/50"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ items-center" gap-1.5>
|
||||
<div i-solar:lightbulb-bolt-bold-duotone size-3.5 text-amber-500 dark:text-amber-400 />
|
||||
<span font-medium>{{ t('stage.chat.reasoning') }}</span>
|
||||
</div>
|
||||
<div
|
||||
i-solar:alt-arrow-down-linear
|
||||
size-3
|
||||
transition="transform duration-200"
|
||||
:class="{ 'rotate-180': slotProps.visible }"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<div
|
||||
class="mt-1 border border-neutral-200 rounded-md bg-neutral-50/80 px-2 py-1.5 dark:border-neutral-700 dark:bg-neutral-900/80"
|
||||
>
|
||||
<MarkdownRenderer
|
||||
:content="message.categorization?.reasoning ?? ''"
|
||||
class="break-words"
|
||||
text="xs neutral-700 dark:neutral-300"
|
||||
/>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,5 +6,6 @@ export * from './micvad'
|
||||
export * from './queues'
|
||||
export * from './use-analytics'
|
||||
export * from './use-build-info'
|
||||
export * from './use-chat-session/summary'
|
||||
export * from './use-versioned-local-storage'
|
||||
export * from './whisper'
|
||||
|
||||
@@ -142,4 +142,42 @@ describe('useLlmmarkerParser', async () => {
|
||||
expect(collectedSpecials).toEqual(expectedSpecials)
|
||||
}
|
||||
})
|
||||
|
||||
it('should call onEnd with full text', async () => {
|
||||
const fullText = 'Hello, world!'
|
||||
let endText = ''
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onEnd(text) {
|
||||
endText = text
|
||||
},
|
||||
})
|
||||
|
||||
for (const char of fullText) {
|
||||
await parser.consume(char)
|
||||
}
|
||||
|
||||
await parser.end()
|
||||
|
||||
expect(endText).toBe(fullText)
|
||||
})
|
||||
|
||||
it('should call onEnd with full text including specials', async () => {
|
||||
const fullText = 'Hello <|special|> world!'
|
||||
let endText = ''
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onEnd(text) {
|
||||
endText = text
|
||||
},
|
||||
})
|
||||
|
||||
for (const char of fullText) {
|
||||
await parser.consume(char)
|
||||
}
|
||||
|
||||
await parser.end()
|
||||
|
||||
expect(endText).toBe(fullText)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,6 +18,11 @@ const TAG_CLOSE = '|>'
|
||||
export function useLlmmarkerParser(options: {
|
||||
onLiteral?: (literal: string) => void | Promise<void>
|
||||
onSpecial?: (special: string) => void | Promise<void>
|
||||
/**
|
||||
* Called when parsing ends with the full accumulated text.
|
||||
* Useful for final processing like categorization or filtering.
|
||||
*/
|
||||
onEnd?: (fullText: string) => void | Promise<void>
|
||||
/**
|
||||
* The minimum length of text required to emit a literal part.
|
||||
* Useful for avoiding emitting literal parts too fast.
|
||||
@@ -27,6 +32,7 @@ export function useLlmmarkerParser(options: {
|
||||
const minLiteralEmitLength = Math.max(1, options.minLiteralEmitLength ?? 1)
|
||||
let buffer = ''
|
||||
let inTag = false
|
||||
let fullText = ''
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -36,6 +42,7 @@ export function useLlmmarkerParser(options: {
|
||||
* @param textPart The chunk of text to consume.
|
||||
*/
|
||||
async consume(textPart: string) {
|
||||
fullText += textPart
|
||||
buffer += textPart
|
||||
|
||||
while (buffer.length > 0) {
|
||||
@@ -82,6 +89,7 @@ export function useLlmmarkerParser(options: {
|
||||
await options.onLiteral?.(buffer)
|
||||
buffer = ''
|
||||
}
|
||||
await options.onEnd?.(fullText)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createStreamingCategorizer } from './response-categoriser'
|
||||
|
||||
describe('createStreamingCategorizer', () => {
|
||||
it('should handle pure speech without tags', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello, world! This is a test.'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe(text)
|
||||
expect(result.reasoning).toBe('')
|
||||
expect(result.segments).toEqual([])
|
||||
})
|
||||
|
||||
it('should filter out reasoning tags from speech', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello <reasoning>thinking about this</reasoning> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking about this')
|
||||
expect(result.segments).toHaveLength(1)
|
||||
expect(result.segments[0].tagName).toBe('reasoning')
|
||||
expect(result.segments[0].content).toBe('thinking about this')
|
||||
})
|
||||
|
||||
it('should handle multiple reasoning tags', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Start <think>first thought</think> middle <reasoning>second thought</reasoning> end'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Start middle end')
|
||||
expect(result.reasoning).toBe('first thought\n\nsecond thought')
|
||||
expect(result.segments).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should filter incomplete tags from speech during streaming', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
|
||||
// Stream incomplete tag
|
||||
categorizer.consume('Hello <reasoning>thinking')
|
||||
const filtered1 = categorizer.filterToSpeech('Hello <reasoning>thinking', 0)
|
||||
|
||||
// Before tag closes, everything should be filtered (tag is incomplete)
|
||||
expect(filtered1).toBe('')
|
||||
|
||||
// Complete the tag - the important thing is the final result is correct
|
||||
categorizer.consume(' about this</reasoning> world!')
|
||||
|
||||
const result = categorizer.end()
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking about this')
|
||||
})
|
||||
|
||||
it('should handle tags split across chunks', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const chunks = [
|
||||
'Hello <',
|
||||
'reasoning',
|
||||
'>thinking',
|
||||
' about this',
|
||||
'</reasoning',
|
||||
'> world!',
|
||||
]
|
||||
|
||||
for (const chunk of chunks) {
|
||||
categorizer.consume(chunk)
|
||||
}
|
||||
|
||||
const result = categorizer.end()
|
||||
|
||||
// Final result should correctly categorize the complete text
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking about this')
|
||||
})
|
||||
|
||||
it('should handle nested tags', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Start <outer>outer <inner>inner</inner> more outer</outer> end'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
// Both tags should be extracted
|
||||
expect(result.segments.length).toBeGreaterThan(0)
|
||||
// Speech should exclude tag content
|
||||
// Note: rehype handles nested tags by extracting both, but the speech extraction
|
||||
// may include text between nested tags or closing tag text
|
||||
// The important thing is that the main tag content (inner, outer text) is in reasoning
|
||||
expect(result.speech).toContain('Start')
|
||||
expect(result.speech).toContain('end')
|
||||
// Tag content should be in reasoning, not speech
|
||||
expect(result.reasoning).toContain('inner')
|
||||
// The exact speech format may vary with nested tags, but should not contain the main tag content
|
||||
expect(result.speech).not.toContain('inner')
|
||||
})
|
||||
|
||||
it('should correctly identify speech positions with isSpeechAt', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello <reasoning>thinking</reasoning> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
|
||||
// Position 0-5: "Hello " - should be speech
|
||||
expect(categorizer.isSpeechAt(0)).toBe(true)
|
||||
expect(categorizer.isSpeechAt(5)).toBe(true)
|
||||
|
||||
// Position 6-36: inside <reasoning> tag (including the tag itself) - should not be speech
|
||||
// "Hello " = 6 chars, "<reasoning>" starts at 6, "</reasoning>" ends at 36
|
||||
expect(categorizer.isSpeechAt(6)).toBe(false)
|
||||
expect(categorizer.isSpeechAt(20)).toBe(false)
|
||||
expect(categorizer.isSpeechAt(36)).toBe(false)
|
||||
|
||||
// Position 37+: " world!" - should be speech (after closing tag)
|
||||
expect(categorizer.isSpeechAt(37)).toBe(true)
|
||||
expect(categorizer.isSpeechAt(43)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle tags with special tokens like emotes inside reasoning', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
// Special tokens like <|EMOTE_HAPPY|> and <|DELAY:1|> should be included in reasoning
|
||||
const text = 'Hello <reasoning>thinking <|EMOTE_HAPPY|> about this <|DELAY:1|></reasoning> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking <|EMOTE_HAPPY|> about this <|DELAY:1|>')
|
||||
})
|
||||
|
||||
it('should handle <think> tag with special tokens in between', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
// Special tokens like <|EMOTE_CURIOUS|> should be included within reasoning tags
|
||||
const text = 'Hello <think>thinking <|EMOTE_CURIOUS|> about this <|DELAY:2|> and that</think> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
// Log what was recognized
|
||||
console.info('📋 Test: should handle <think> tag with special tokens')
|
||||
console.info(' Input text:', text)
|
||||
console.info(' Segments found:', result.segments.length)
|
||||
console.info(' Tag name:', result.segments[0]?.tagName)
|
||||
console.info(' Segment content:', result.segments[0]?.content)
|
||||
console.info(' Reasoning:', result.reasoning)
|
||||
console.info(' Speech:', result.speech)
|
||||
|
||||
// Verify tag is recognized
|
||||
expect(result.segments).toHaveLength(1)
|
||||
expect(result.segments[0].tagName).toBe('think')
|
||||
|
||||
// Verify special tokens are preserved in segment content
|
||||
expect(result.segments[0].content).toContain('<|EMOTE_CURIOUS|>')
|
||||
expect(result.segments[0].content).toContain('<|DELAY:2|>')
|
||||
|
||||
// Verify special tokens are in reasoning output
|
||||
expect(result.reasoning).toContain('<|EMOTE_CURIOUS|>')
|
||||
expect(result.reasoning).toContain('<|DELAY:2|>')
|
||||
|
||||
// Verify speech excludes the reasoning content
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.speech).not.toContain('<|EMOTE_CURIOUS|>')
|
||||
expect(result.speech).not.toContain('<|DELAY:2|>')
|
||||
expect(result.reasoning).toBe('thinking <|EMOTE_CURIOUS|> about this <|DELAY:2|> and that')
|
||||
})
|
||||
|
||||
it('should handle <think> with special tokens like <|EMOTE_HAPPY|> in between', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
// Testing the exact scenario: <think>...<|Special in between|>...</think>
|
||||
const text = 'Hello <think>thinking <|EMOTE_HAPPY|> about this <|DELAY:1|> and that</think> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
// Log what was recognized
|
||||
console.info('📋 Test: should handle <think> with special tokens like <|EMOTE_HAPPY|>')
|
||||
console.info(' Input text:', text)
|
||||
console.info(' Segments found:', result.segments.length)
|
||||
console.info(' Tag name:', result.segments[0]?.tagName)
|
||||
console.info(' Segment content:', result.segments[0]?.content)
|
||||
console.info(' Has <|EMOTE_HAPPY|>:', result.segments[0]?.content.includes('<|EMOTE_HAPPY|>'))
|
||||
console.info(' Has <|DELAY:1|>:', result.segments[0]?.content.includes('<|DELAY:1|>'))
|
||||
console.info(' Reasoning:', result.reasoning)
|
||||
console.info(' Speech:', result.speech)
|
||||
|
||||
// Verify tag is recognized
|
||||
expect(result.segments).toHaveLength(1)
|
||||
expect(result.segments[0].tagName).toBe('think')
|
||||
|
||||
// Verify special tokens are preserved in segment content
|
||||
expect(result.segments[0].content).toContain('<|EMOTE_HAPPY|>')
|
||||
expect(result.segments[0].content).toContain('<|DELAY:1|>')
|
||||
|
||||
// Verify special tokens are in reasoning output
|
||||
expect(result.reasoning).toContain('<|EMOTE_HAPPY|>')
|
||||
expect(result.reasoning).toContain('<|DELAY:1|>')
|
||||
|
||||
// Verify speech excludes the reasoning content
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.speech).not.toContain('<|EMOTE_HAPPY|>')
|
||||
expect(result.speech).not.toContain('<|DELAY:1|>')
|
||||
expect(result.reasoning).toBe('thinking <|EMOTE_HAPPY|> about this <|DELAY:1|> and that')
|
||||
})
|
||||
|
||||
it('should handle any tag name as reasoning', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello <think>secret thoughts</think> world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('secret thoughts')
|
||||
expect(result.segments[0].tagName).toBe('think')
|
||||
})
|
||||
|
||||
it('should handle multiple tags with speech in between', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'First <think>thought1</think> Second <reasoning>thought2</reasoning> Third'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('First Second Third')
|
||||
expect(result.reasoning).toBe('thought1\n\nthought2')
|
||||
expect(result.segments).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should return empty speech when only tags present', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = '<reasoning>only reasoning</reasoning>'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('')
|
||||
expect(result.reasoning).toBe('only reasoning')
|
||||
})
|
||||
|
||||
it('should handle streaming with incremental categorization', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
|
||||
// Stream in small chunks
|
||||
categorizer.consume('Hello ')
|
||||
expect(categorizer.getCurrent()?.speech).toBe('Hello ')
|
||||
|
||||
categorizer.consume('<reasoning>')
|
||||
categorizer.consume('thinking')
|
||||
// Tag not closed yet, but should still categorize
|
||||
const midResult = categorizer.getCurrent()
|
||||
expect(midResult).not.toBeNull()
|
||||
|
||||
categorizer.consume('</reasoning>')
|
||||
categorizer.consume(' world!')
|
||||
|
||||
const result = categorizer.end()
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking')
|
||||
})
|
||||
|
||||
it('should call onSegment callback when segments are detected', () => {
|
||||
const segments: Array<{ tagName: string, content: string }> = []
|
||||
const categorizer = createStreamingCategorizer(undefined, (segment) => {
|
||||
segments.push({ tagName: segment.tagName, content: segment.content })
|
||||
})
|
||||
|
||||
const text = 'Hello <reasoning>thought1</reasoning> <think>thought2</think> world!'
|
||||
|
||||
// Stream the text - segments are emitted when they complete
|
||||
categorizer.consume(text)
|
||||
categorizer.end()
|
||||
|
||||
// Check final result has both segments
|
||||
const result = categorizer.end()
|
||||
expect(result.segments.length).toBeGreaterThanOrEqual(2)
|
||||
expect(result.segments.some(s => s.tagName === 'reasoning')).toBe(true)
|
||||
expect(result.segments.some(s => s.tagName === 'think')).toBe(true)
|
||||
|
||||
// onSegment is called when segments complete during streaming
|
||||
// At minimum, we should have segments detected in the final result
|
||||
expect(result.segments.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('should handle getCurrentPosition correctly', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
|
||||
expect(categorizer.getCurrentPosition()).toBe(0)
|
||||
|
||||
categorizer.consume('Hello')
|
||||
expect(categorizer.getCurrentPosition()).toBe(5)
|
||||
|
||||
categorizer.consume(' world!')
|
||||
expect(categorizer.getCurrentPosition()).toBe(12)
|
||||
})
|
||||
|
||||
it('should handle edge case: tag at start', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = '<reasoning>thinking</reasoning>Hello world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking')
|
||||
})
|
||||
|
||||
it('should handle edge case: tag at end', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello world!<reasoning>thinking</reasoning>'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
expect(result.reasoning).toBe('thinking')
|
||||
})
|
||||
|
||||
it('should handle malformed tags gracefully', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
const text = 'Hello <unclosed>thinking world!'
|
||||
|
||||
categorizer.consume(text)
|
||||
const result = categorizer.end()
|
||||
|
||||
// Unclosed tag should be treated as speech
|
||||
expect(result.speech).toContain('Hello')
|
||||
expect(result.segments.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should filter speech correctly when tag closes mid-chunk', () => {
|
||||
const categorizer = createStreamingCategorizer()
|
||||
|
||||
// Stream incomplete tag
|
||||
categorizer.consume('Hello <reasoning>thinking')
|
||||
let filtered = categorizer.filterToSpeech('Hello <reasoning>thinking', 0)
|
||||
expect(filtered).toBe('') // Tag not closed, filter everything
|
||||
|
||||
// Stream closing tag
|
||||
categorizer.consume('</reasoning>')
|
||||
filtered = categorizer.filterToSpeech('</reasoning>', 25)
|
||||
expect(filtered).toBe('') // This is the closing tag itself
|
||||
|
||||
// Stream speech after
|
||||
categorizer.consume(' world!')
|
||||
filtered = categorizer.filterToSpeech(' world!', 38)
|
||||
expect(filtered).toBe(' world!')
|
||||
|
||||
const result = categorizer.end()
|
||||
expect(result.speech).toBe('Hello world!')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,466 @@
|
||||
import type { Element, Root } from 'hast'
|
||||
import type { Position } from 'unist'
|
||||
|
||||
import rehypeParse from 'rehype-parse'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
|
||||
import { unified } from 'unified'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
export type ResponseCategory = 'speech' | 'reasoning' | 'unknown'
|
||||
|
||||
export interface CategorizedSegment {
|
||||
category: ResponseCategory
|
||||
content: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
raw: string // Original tagged content including tags
|
||||
tagName: string // The actual tag name found (e.g., "think", "thought", "reasoning")
|
||||
}
|
||||
|
||||
export interface CategorizedResponse {
|
||||
segments: CategorizedSegment[]
|
||||
speech: string // Combined speech content (everything outside tags)
|
||||
reasoning: string // Combined reasoning/thought content
|
||||
raw: string // Original full response
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps tag names to categories
|
||||
* All tags are treated as reasoning (filtered from TTS)
|
||||
*/
|
||||
function mapTagNameToCategory(_tagName: string): ResponseCategory {
|
||||
// All tags are reasoning - no need to distinguish tag names
|
||||
return 'reasoning'
|
||||
}
|
||||
|
||||
interface ExtractedTag {
|
||||
tagName: string
|
||||
content: string
|
||||
fullMatch: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all XML-like tags from a response using rehype pipeline
|
||||
* Works with any tag format: <tag>content</tag>
|
||||
* Only extracts tags that are actually complete (have closing tags in source)
|
||||
*/
|
||||
function extractAllTags(response: string): ExtractedTag[] {
|
||||
const tags: ExtractedTag[] = []
|
||||
|
||||
try {
|
||||
const tree = unified().use(rehypeParse, { fragment: true }).parse(response) as Root
|
||||
|
||||
visit(tree, 'element', (node: Element) => {
|
||||
const position = node.position
|
||||
if (!position?.start || !position?.end)
|
||||
return
|
||||
|
||||
const startIndex = getOffsetFromPosition(response, position.start)
|
||||
const endIndex = getOffsetFromPosition(response, position.end)
|
||||
|
||||
if (startIndex === -1 || endIndex === -1)
|
||||
return
|
||||
|
||||
// Extract the actual tag content from source
|
||||
const fullMatch = response.slice(startIndex, endIndex)
|
||||
|
||||
// Only include tags that have a closing tag in the source (not auto-closed by rehype)
|
||||
// Check if the source actually contains the closing tag
|
||||
const expectedClosingTag = `</${node.tagName}>`
|
||||
if (!fullMatch.includes(expectedClosingTag)) {
|
||||
// This tag was auto-closed by rehype, so it's incomplete - skip it
|
||||
return
|
||||
}
|
||||
|
||||
tags.push({
|
||||
tagName: node.tagName,
|
||||
content: extractTextContent(node),
|
||||
fullMatch,
|
||||
startIndex,
|
||||
endIndex,
|
||||
})
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to parse response for tag extraction:', error)
|
||||
// If parsing fails, return empty array (no tags found)
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a position (line/column) to a character offset in the string
|
||||
*/
|
||||
function getOffsetFromPosition(text: string, position: Position['start']): number {
|
||||
if (!position || typeof position.line !== 'number' || typeof position.column !== 'number')
|
||||
return -1
|
||||
|
||||
const lines = text.split('\n')
|
||||
let offset = 0
|
||||
|
||||
// Sum up lengths of all lines before the target line
|
||||
for (let i = 0; i < position.line - 1 && i < lines.length; i++) {
|
||||
offset += lines[i].length + 1 // +1 for the newline character
|
||||
}
|
||||
|
||||
// Add the column offset (subtract 1 because columns are 1-indexed)
|
||||
offset += position.column - 1
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text content from an element node
|
||||
*/
|
||||
function extractTextContent(node: Element): string {
|
||||
const textParts: string[] = []
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'text') {
|
||||
textParts.push(child.value)
|
||||
}
|
||||
else if (child.type === 'element') {
|
||||
textParts.push(extractTextContent(child))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textParts.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorizes a model response by dynamically extracting any XML-like tags
|
||||
* Works with any tag format the model uses
|
||||
*/
|
||||
export function categorizeResponse(
|
||||
response: string,
|
||||
_providerId?: string,
|
||||
): CategorizedResponse {
|
||||
// Extract all tags dynamically
|
||||
const extractedTags = extractAllTags(response)
|
||||
|
||||
if (extractedTags.length === 0) {
|
||||
// No tags found, treat everything as speech
|
||||
return {
|
||||
segments: [],
|
||||
speech: response,
|
||||
reasoning: '',
|
||||
raw: response,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert extracted tags to categorized segments
|
||||
const segments: CategorizedSegment[] = extractedTags.map(tag => ({
|
||||
category: mapTagNameToCategory(tag.tagName),
|
||||
content: tag.content.trim(),
|
||||
startIndex: tag.startIndex,
|
||||
endIndex: tag.endIndex,
|
||||
raw: tag.fullMatch,
|
||||
tagName: tag.tagName,
|
||||
}))
|
||||
|
||||
// Sort segments by position
|
||||
segments.sort((a, b) => a.startIndex - b.startIndex)
|
||||
|
||||
// Extract speech content (everything outside tags)
|
||||
const speechParts: string[] = []
|
||||
let lastEnd = 0
|
||||
|
||||
for (const segment of segments) {
|
||||
// Add text before this segment
|
||||
if (segment.startIndex > lastEnd) {
|
||||
const text = response.slice(lastEnd, segment.startIndex).trim()
|
||||
if (text) {
|
||||
speechParts.push(text)
|
||||
}
|
||||
}
|
||||
lastEnd = segment.endIndex
|
||||
}
|
||||
|
||||
// Add remaining text after last segment
|
||||
if (lastEnd < response.length) {
|
||||
const text = response.slice(lastEnd).trim()
|
||||
if (text) {
|
||||
speechParts.push(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Combine segments by category
|
||||
const reasoning = segments
|
||||
.filter(s => s.category === 'reasoning')
|
||||
.map(s => s.content)
|
||||
.join('\n\n')
|
||||
|
||||
// Speech is everything outside tags
|
||||
const speech = speechParts.join(' ').trim()
|
||||
|
||||
return {
|
||||
segments,
|
||||
speech: speech || '',
|
||||
reasoning,
|
||||
raw: response,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This receives literal text from useLlmmarkerParser (special tokens <|...|> are already extracted).
|
||||
* Only XML/HTML tags like <think>, <reasoning> need to be parsed here.
|
||||
*/
|
||||
export function createStreamingCategorizer(
|
||||
providerId?: string,
|
||||
onSegment?: (segment: CategorizedSegment) => void,
|
||||
) {
|
||||
let buffer = ''
|
||||
let categorized: CategorizedResponse | null = null
|
||||
let lastEmittedSegmentIndex = -1
|
||||
let lastParsedLength = 0
|
||||
|
||||
// Lightweight state machine to detect tag closures without parsing entire buffer
|
||||
type TagState = 'outside' | 'in-opening-tag' | 'in-content' | 'in-closing-tag'
|
||||
let tagState: TagState = 'outside'
|
||||
let tagStackDepth = 0
|
||||
|
||||
// Fallback for filterToSpeech - uses rehype for robust incomplete tag detection
|
||||
function checkIncompleteTag(): boolean {
|
||||
try {
|
||||
const tree = unified().use(rehypeParse, { fragment: true }).parse(buffer) as Root
|
||||
const stringified = unified().use(rehypeStringify).stringify(tree).toString()
|
||||
|
||||
if (stringified !== buffer) {
|
||||
const bufferEnd = buffer.trim().slice(-30)
|
||||
const stringifiedEnd = stringified.trim().slice(-30)
|
||||
return bufferEnd !== stringifiedEnd
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
catch {
|
||||
// If parsing fails, assume incomplete
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks tag state incrementally (O(chunk.length)) to detect when tags close
|
||||
// Returns true when the outermost tag just closed
|
||||
function processChunkIncrementally(chunk: string): boolean {
|
||||
let tagJustClosed = false
|
||||
|
||||
for (let i = 0; i < chunk.length; i++) {
|
||||
const char = chunk[i]
|
||||
|
||||
switch (tagState) {
|
||||
case 'outside': {
|
||||
if (char === '<') {
|
||||
if (i + 1 < chunk.length && chunk[i + 1] === '/') {
|
||||
tagState = 'in-closing-tag'
|
||||
i++
|
||||
}
|
||||
else {
|
||||
tagState = 'in-opening-tag'
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'in-opening-tag': {
|
||||
if (char === '>') {
|
||||
tagState = 'in-content'
|
||||
tagStackDepth++
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'in-content': {
|
||||
if (char === '<') {
|
||||
if (i + 1 < chunk.length && chunk[i + 1] === '/') {
|
||||
tagState = 'in-closing-tag'
|
||||
i++
|
||||
}
|
||||
else {
|
||||
tagState = 'in-opening-tag'
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'in-closing-tag': {
|
||||
if (char === '>') {
|
||||
tagStackDepth--
|
||||
if (tagStackDepth === 0) {
|
||||
tagState = 'outside'
|
||||
tagJustClosed = true
|
||||
}
|
||||
else {
|
||||
tagState = 'in-content'
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagJustClosed
|
||||
}
|
||||
|
||||
return {
|
||||
consume(chunk: string) {
|
||||
// Process before adding to buffer to detect tag closure in this chunk
|
||||
const tagJustClosed = processChunkIncrementally(chunk)
|
||||
buffer += chunk
|
||||
|
||||
// Re-categorize on first chunk, tag closure, or every 1KB (periodic fallback)
|
||||
const shouldRecategorize = !categorized
|
||||
|| tagJustClosed
|
||||
|| buffer.length - lastParsedLength > 1000
|
||||
|
||||
if (shouldRecategorize) {
|
||||
categorized = categorizeResponse(buffer, providerId)
|
||||
lastParsedLength = buffer.length
|
||||
}
|
||||
|
||||
// Type guard for TypeScript (shouldRecategorize handles !categorized, but TS doesn't know)
|
||||
if (!categorized) {
|
||||
categorized = categorizeResponse(buffer, providerId)
|
||||
lastParsedLength = buffer.length
|
||||
}
|
||||
|
||||
if (onSegment && categorized.segments.length > 0) {
|
||||
for (let i = lastEmittedSegmentIndex + 1; i < categorized.segments.length; i++) {
|
||||
const segment = categorized.segments[i]
|
||||
if (buffer.length >= segment.endIndex) {
|
||||
onSegment(segment)
|
||||
lastEmittedSegmentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Checks if the current position in the stream is part of speech content
|
||||
* Returns true if the text should be sent to TTS
|
||||
*/
|
||||
isSpeechAt(position: number): boolean {
|
||||
if (!categorized || categorized.segments.length === 0) {
|
||||
// No categorization yet, assume it's speech
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if position falls within any non-speech segment
|
||||
for (const segment of categorized.segments) {
|
||||
if (position >= segment.startIndex && position < segment.endIndex) {
|
||||
// Position is within a tagged segment (thought/reasoning)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Position is not in any tagged segment, so it's speech
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* Filters text to only include speech parts
|
||||
* Removes content that falls within thought/reasoning segments
|
||||
*/
|
||||
filterToSpeech(text: string, startPosition: number): string {
|
||||
// Check if we're currently inside an incomplete tag
|
||||
if (checkIncompleteTag()) {
|
||||
// Try to find where the tag closes in the combined buffer + text
|
||||
const fullText = buffer + text
|
||||
try {
|
||||
const tree = unified().use(rehypeParse, { fragment: true }).parse(fullText) as Root
|
||||
let closingOffset = -1
|
||||
|
||||
visit(tree, 'element', (node: Element) => {
|
||||
const position = node.position
|
||||
if (position?.end && closingOffset === -1) {
|
||||
const endOffset = getOffsetFromPosition(fullText, position.end)
|
||||
// Check if this element actually has a closing tag in the source
|
||||
const elementSource = fullText.slice(
|
||||
getOffsetFromPosition(fullText, position.start),
|
||||
endOffset,
|
||||
)
|
||||
const expectedClosingTag = `</${node.tagName}>`
|
||||
|
||||
// Only consider it complete if the closing tag exists in source
|
||||
if (elementSource.includes(expectedClosingTag)) {
|
||||
// If this element closes within the new text chunk
|
||||
if (endOffset >= buffer.length && endOffset <= fullText.length) {
|
||||
closingOffset = endOffset - buffer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (closingOffset === -1)
|
||||
return '' // Still incomplete, filter everything
|
||||
|
||||
// Return only content after the closing tag
|
||||
// The buffer already includes text up to closingOffset (from consume())
|
||||
text = text.slice(closingOffset)
|
||||
startPosition += closingOffset
|
||||
// Re-categorize with the complete tag now in buffer
|
||||
categorized = categorizeResponse(buffer, providerId)
|
||||
}
|
||||
catch {
|
||||
return '' // Parsing failed, filter everything
|
||||
}
|
||||
}
|
||||
|
||||
if (!categorized || categorized.segments.length === 0) {
|
||||
// No segments detected, all text is speech
|
||||
return text
|
||||
}
|
||||
|
||||
let filtered = ''
|
||||
const endPosition = startPosition + text.length
|
||||
|
||||
// Find all non-speech segments that overlap with this text
|
||||
// Note: segments are already filtered to be complete by extractAllTags
|
||||
const overlappingSegments = categorized.segments.filter(
|
||||
segment => segment.endIndex > startPosition && segment.startIndex < endPosition,
|
||||
)
|
||||
|
||||
if (overlappingSegments.length === 0) {
|
||||
// No overlapping segments, all text is speech
|
||||
return text
|
||||
}
|
||||
|
||||
// Build filtered text by excluding non-speech segments
|
||||
let currentPos = startPosition
|
||||
for (const segment of overlappingSegments) {
|
||||
const segmentStart = Math.max(segment.startIndex, startPosition)
|
||||
const segmentEnd = Math.min(segment.endIndex, endPosition)
|
||||
|
||||
// Add text before this segment
|
||||
if (segmentStart > currentPos) {
|
||||
const beforeStart = currentPos - startPosition
|
||||
const beforeEnd = segmentStart - startPosition
|
||||
filtered += text.slice(beforeStart, beforeEnd)
|
||||
}
|
||||
|
||||
// Skip the segment content (don't add to filtered)
|
||||
currentPos = segmentEnd
|
||||
}
|
||||
|
||||
// Add remaining text after last segment
|
||||
if (currentPos < endPosition) {
|
||||
const afterStart = currentPos - startPosition
|
||||
filtered += text.slice(afterStart)
|
||||
}
|
||||
|
||||
return filtered
|
||||
},
|
||||
getCurrentPosition(): number {
|
||||
return buffer.length
|
||||
},
|
||||
end(): CategorizedResponse {
|
||||
return categorizeResponse(buffer, providerId)
|
||||
},
|
||||
getCurrent(): CategorizedResponse | null {
|
||||
return categorized
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ChatAssistantMessage, ChatHistoryItem } from '../../types/chat'
|
||||
|
||||
/**
|
||||
* Extract all reasoning from a session's messages
|
||||
*/
|
||||
export function getAllReasoning(messages: ChatHistoryItem[]): string[] {
|
||||
return messages
|
||||
.filter((msg): msg is ChatAssistantMessage =>
|
||||
msg.role === 'assistant' && 'categorization' in msg,
|
||||
)
|
||||
.map(msg => msg.categorization?.reasoning)
|
||||
.filter((reasoning): reasoning is string => !!reasoning?.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get combined reasoning as a single string
|
||||
*/
|
||||
export function getCombinedReasoning(messages: ChatHistoryItem[]): string {
|
||||
return getAllReasoning(messages).join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session summary with reasoning, speech, and metadata
|
||||
*/
|
||||
export function getSessionSummary(
|
||||
sessionId: string,
|
||||
messages: ChatHistoryItem[],
|
||||
): {
|
||||
sessionId: string
|
||||
messageCount: number
|
||||
reasoningCount: number
|
||||
allReasoning: string[]
|
||||
allSpeech: string[]
|
||||
combinedReasoning: string
|
||||
combinedSpeech: string
|
||||
createdAt?: number
|
||||
lastMessageAt?: number
|
||||
} {
|
||||
const allReasoning: string[] = []
|
||||
const allSpeech: string[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'assistant' && 'categorization' in msg) {
|
||||
const reasoning = msg.categorization?.reasoning
|
||||
if (reasoning?.trim()) {
|
||||
allReasoning.push(reasoning)
|
||||
}
|
||||
const speech = msg.categorization?.speech
|
||||
if (speech?.trim()) {
|
||||
allSpeech.push(speech)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
messageCount: messages.length,
|
||||
reasoningCount: allReasoning.length,
|
||||
allReasoning,
|
||||
allSpeech,
|
||||
combinedReasoning: allReasoning.join('\n\n'),
|
||||
combinedSpeech: allSpeech.join('\n\n'),
|
||||
createdAt: messages[0]?.createdAt,
|
||||
lastMessageAt: messages[messages.length - 1]?.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reasoning from all sessions
|
||||
*/
|
||||
export function getAllReasoningFromAllSessions(
|
||||
allSessions: Record<string, ChatHistoryItem[]>,
|
||||
): string[] {
|
||||
return Object.values(allSessions)
|
||||
.flatMap(messages => getAllReasoning(messages))
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import { computed, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import { useAnalytics } from '../composables'
|
||||
import { useLlmmarkerParser } from '../composables/llmmarkerParser'
|
||||
import { categorizeResponse, createStreamingCategorizer } from '../composables/response-categoriser'
|
||||
import { useLLM } from '../stores/llm'
|
||||
import { createQueue } from '../utils/queue'
|
||||
import { TTS_FLUSH_INSTRUCTION } from '../utils/tts'
|
||||
import { useCharacterStore } from './character'
|
||||
import { useConsciousnessStore } from './modules/consciousness'
|
||||
|
||||
const CHAT_STORAGE_KEY = 'chat/messages/v2'
|
||||
const ACTIVE_SESSION_STORAGE_KEY = 'chat/active-session'
|
||||
@@ -23,6 +25,8 @@ export const CHAT_STREAM_CHANNEL_NAME = 'airi-chat-stream'
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const { stream, discoverToolsCompatibility } = useLLM()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider } = storeToRefs(consciousnessStore)
|
||||
const { systemPrompt } = storeToRefs(useCharacterStore())
|
||||
const { trackFirstMessage } = useAnalytics()
|
||||
|
||||
@@ -391,26 +395,44 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const sessionMessagesForSend = getSessionMessagesById(sessionId)
|
||||
sessionMessagesForSend.push({ role: 'user', content: finalContent })
|
||||
|
||||
// Create categorizer for response categorization
|
||||
const categorizer = createStreamingCategorizer(activeProvider.value)
|
||||
let streamPosition = 0 // Track position in stream for TTS filtering
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onLiteral: async (literal) => {
|
||||
if (shouldAbort())
|
||||
return
|
||||
|
||||
await emitTokenLiteralHooks(literal, streamingMessageContext)
|
||||
// Feed to categorizer first
|
||||
categorizer.consume(literal)
|
||||
|
||||
streamingMessage.value.content += literal
|
||||
// Filter to only include speech parts (exclude reasoning)
|
||||
// The categorizer handles incomplete tags and filters based on detected tags during streaming
|
||||
const speechOnly = categorizer.filterToSpeech(literal, streamPosition)
|
||||
streamPosition += literal.length
|
||||
|
||||
// merge text slices for markdown
|
||||
const lastSlice = streamingMessage.value.slices.at(-1)
|
||||
if (lastSlice?.type === 'text') {
|
||||
lastSlice.text += literal
|
||||
return
|
||||
// Only process non-empty speech content (filter empty/whitespace-only chunks)
|
||||
// Preserve spacing in chunks with content for proper word boundaries
|
||||
if (speechOnly.trim()) {
|
||||
streamingMessage.value.content += speechOnly
|
||||
|
||||
// Emit TTS only for speech parts, not reasoning (clean data, no empty chunks)
|
||||
await emitTokenLiteralHooks(speechOnly, streamingMessageContext)
|
||||
|
||||
// Add speech content to slices for rendering
|
||||
// merge text slices for markdown
|
||||
const lastSlice = streamingMessage.value.slices.at(-1)
|
||||
if (lastSlice?.type === 'text') {
|
||||
lastSlice.text += speechOnly
|
||||
return
|
||||
}
|
||||
|
||||
streamingMessage.value.slices.push({
|
||||
type: 'text',
|
||||
text: speechOnly,
|
||||
})
|
||||
}
|
||||
|
||||
streamingMessage.value.slices.push({
|
||||
type: 'text',
|
||||
text: literal,
|
||||
})
|
||||
},
|
||||
onSpecial: async (special) => {
|
||||
if (shouldAbort())
|
||||
@@ -418,6 +440,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
await emitTokenSpecialHooks(special, streamingMessageContext)
|
||||
},
|
||||
onEnd: async (fullText) => {
|
||||
if (isStaleGeneration())
|
||||
return
|
||||
|
||||
// Categorize the full text stream
|
||||
const finalCategorization = categorizeResponse(fullText, activeProvider.value)
|
||||
|
||||
// Always store categorization (even if empty) for consistency and memory features
|
||||
streamingMessage.value.categorization = {
|
||||
speech: finalCategorization.speech,
|
||||
reasoning: finalCategorization.reasoning,
|
||||
}
|
||||
},
|
||||
minLiteralEmitLength: 24, // Avoid emitting literals too fast. This is a magic number and can be changed later.
|
||||
})
|
||||
|
||||
@@ -443,7 +478,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const rawMessage = toRaw(withoutContext)
|
||||
|
||||
if (rawMessage.role === 'assistant') {
|
||||
const { slices: _, tool_results, ...rest } = rawMessage as ChatAssistantMessage
|
||||
const { slices: _, tool_results, categorization: __categorization, ...rest } = rawMessage as ChatAssistantMessage
|
||||
return {
|
||||
...toRaw(rest),
|
||||
tool_results: toRaw(tool_results),
|
||||
@@ -520,6 +555,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
|
||||
// Finalize the parsing of the actual message content
|
||||
// Categorization and filtering happens in the onEnd callback
|
||||
await parser.end()
|
||||
|
||||
// Add the completed message to the history only if it has content
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface ChatAssistantMessage extends AssistantMessage {
|
||||
id: string
|
||||
result?: string | CommonContentPart[]
|
||||
}[]
|
||||
categorization?: {
|
||||
speech: string
|
||||
reasoning: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ChatMessage = ChatAssistantMessage | SystemMessage | ToolMessage | UserMessage
|
||||
|
||||
Generated
+50
-193
@@ -45,9 +45,15 @@ catalogs:
|
||||
'@types/audioworklet':
|
||||
specifier: ^0.0.92
|
||||
version: 0.0.92
|
||||
'@types/hast':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
'@types/splitpanes':
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6
|
||||
'@types/unist':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@xsai-ext/providers':
|
||||
specifier: ^0.4.0-beta.13
|
||||
version: 0.4.0
|
||||
@@ -685,7 +691,7 @@ importers:
|
||||
version: 1.0.18
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 0.2.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: ^0.1.6
|
||||
version: 0.1.6(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
@@ -718,7 +724,7 @@ importers:
|
||||
version: 66.5.11
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
version: 6.0.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))
|
||||
@@ -736,34 +742,34 @@ importers:
|
||||
version: 4.5.1
|
||||
unplugin-info:
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@2.79.2)
|
||||
version: 1.2.4(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@2.79.2)
|
||||
unplugin-vue-router:
|
||||
specifier: ^0.19.0
|
||||
version: 0.19.2(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||
unplugin-yaml:
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)
|
||||
version: 3.0.7(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)
|
||||
vite:
|
||||
specifier: catalog:rolldown-vite
|
||||
version: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rolldown@1.0.0-beta.53)(rollup@2.79.2)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 1.17.9(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.17.9(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-pwa:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
version: 1.2.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^8.0.5
|
||||
version: 8.0.5(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
version: 8.0.5(@nuxt/kit@3.20.2(magicast@0.5.1))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||
version: 0.11.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))
|
||||
version: 3.1.1(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.1.8
|
||||
version: 3.2.1(typescript@5.9.3)
|
||||
@@ -2310,6 +2316,9 @@ importers:
|
||||
rehype-katex:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
rehype-parse:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
rehype-stringify:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
@@ -2340,6 +2349,9 @@ importers:
|
||||
unist-builder:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
unspeech:
|
||||
specifier: catalog:xsai
|
||||
version: 0.1.11
|
||||
@@ -2449,12 +2461,18 @@ importers:
|
||||
'@types/culori':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@types/hast':
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
'@types/splitpanes':
|
||||
specifier: 'catalog:'
|
||||
version: 2.2.6
|
||||
'@types/three':
|
||||
specifier: ^0.182.0
|
||||
version: 0.182.0
|
||||
'@types/unist':
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.3
|
||||
'@unocss/reset':
|
||||
specifier: ^66.5.11
|
||||
version: 66.5.11
|
||||
@@ -2796,7 +2814,7 @@ importers:
|
||||
version: 66.5.11
|
||||
'@wxt-dev/module-vue':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vue-tsc:
|
||||
specifier: ^3.1.8
|
||||
version: 3.2.1(typescript@5.9.3)
|
||||
@@ -20724,11 +20742,6 @@ snapshots:
|
||||
|
||||
'@proj-airi/unocss-preset-chromatic@1.0.2': {}
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
@@ -21935,12 +21948,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@vibrant/types': 4.0.0
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
@@ -21959,19 +21966,11 @@ snapshots:
|
||||
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
'@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
|
||||
'@vitest/mocker': 4.0.16(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
playwright: 1.57.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@25.0.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
optional: true
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
'@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
@@ -21986,24 +21985,6 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser@4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
'@vitest/mocker': 4.0.16(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/utils': 4.0.16
|
||||
magic-string: 0.30.21
|
||||
pixelmatch: 7.1.0
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@25.0.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/browser@4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
'@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -22060,15 +22041,6 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.16
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.16
|
||||
@@ -22249,15 +22221,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@vue-macros/devtools@3.1.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vue-macros/devtools@3.1.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
@@ -22528,18 +22491,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
|
||||
'@vue/devtools-core@8.0.5(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.6
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 2.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
'@vue/devtools-core@8.0.5(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
@@ -22766,9 +22717,9 @@ snapshots:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -29076,25 +29027,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- oxc-resolver
|
||||
|
||||
rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.101.0
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
lightningcss: 1.30.2
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rolldown: 1.0.0-beta.53
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
esbuild: 0.25.12
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
less: 4.5.1
|
||||
terser: 5.44.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.101.0
|
||||
@@ -30451,13 +30383,13 @@ snapshots:
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
unplugin-combine@2.1.3(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(unplugin@2.3.11):
|
||||
unplugin-combine@2.1.3(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(unplugin@2.3.11):
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.12
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-beta.53
|
||||
rollup: 2.79.2
|
||||
unplugin: 2.3.11
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
unplugin-combine@2.1.3(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@4.54.0)(unplugin@2.3.11):
|
||||
optionalDependencies:
|
||||
@@ -30495,16 +30427,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.2.4(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@2.79.2):
|
||||
unplugin-info@1.2.4(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@2.79.2):
|
||||
dependencies:
|
||||
ci-info: 4.3.1
|
||||
git-url-parse: 16.1.0
|
||||
simple-git: 3.30.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.12
|
||||
esbuild: 0.27.2
|
||||
rollup: 2.79.2
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -30629,16 +30561,16 @@ snapshots:
|
||||
rollup: 4.54.0
|
||||
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
unplugin-yaml@3.0.7(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2):
|
||||
unplugin-yaml@3.0.7(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||
unplugin: 2.3.10
|
||||
yaml: 2.8.1
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.12
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-beta.53
|
||||
rollup: 2.79.2
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
unplugin-yaml@3.0.7(esbuild@0.27.2)(rolldown@1.0.0-beta.53)(rollup@4.54.0)(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
@@ -30826,12 +30758,6 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-dev-rpc@1.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
vite-dev-rpc@1.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
@@ -30844,10 +30770,6 @@ snapshots:
|
||||
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
vite-hot-client@2.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vite-hot-client@2.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
@@ -30912,21 +30834,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
error-stack-parser-es: 1.0.5
|
||||
ohash: 2.0.11
|
||||
open: 10.2.0
|
||||
perfect-debounce: 2.0.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
@@ -30942,23 +30849,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-mkcert@1.17.9(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-plugin-mkcert@1.17.9(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
axios: feaxios@0.0.23
|
||||
debug: 4.4.3
|
||||
picocolors: 1.1.1
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-pwa@1.2.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
pretty-bytes: 6.1.1
|
||||
tinyglobby: 0.2.15
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
workbox-build: 7.4.0
|
||||
workbox-window: 7.4.0
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -30987,35 +30883,6 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.0.5(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.0.5(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
sirv: 3.0.2
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-vue-inspector: 5.3.2(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5)
|
||||
'@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
|
||||
'@vue/compiler-dom': 3.5.26
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -31031,16 +30898,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
fast-glob: 3.3.3
|
||||
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
vue-router: 4.6.4(vue@3.5.25(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -31195,7 +31052,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 24.10.4
|
||||
'@vitest/browser-playwright': 4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
|
||||
'@vitest/browser-playwright': 4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
|
||||
jsdom: 25.0.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
@@ -31242,7 +31099,7 @@ snapshots:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
vue-macros@3.1.1(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3)):
|
||||
vue-macros@3.1.1(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/boolean-prop': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
@@ -31257,7 +31114,7 @@ snapshots:
|
||||
'@vue-macros/define-render': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/define-slots': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/define-stylex': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/devtools': 3.1.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3)
|
||||
'@vue-macros/devtools': 3.1.1(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3)
|
||||
'@vue-macros/export-expose': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/export-props': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/export-render': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
@@ -31274,7 +31131,7 @@ snapshots:
|
||||
'@vue-macros/short-vmodel': 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
'@vue-macros/volar': 3.1.1(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))
|
||||
unplugin: 2.3.11
|
||||
unplugin-combine: 2.1.3(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(unplugin@2.3.11)
|
||||
unplugin-combine: 2.1.3(esbuild@0.27.2)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(rolldown@1.0.0-beta.53)(rollup@2.79.2)(unplugin@2.3.11)
|
||||
unplugin-vue-define-options: 3.1.1(vue@3.5.25(typescript@5.9.3))
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -21,7 +21,9 @@ catalog:
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@proj-airi/drizzle-duckdb-wasm': ^0.4.29
|
||||
'@types/audioworklet': ^0.0.92
|
||||
'@types/hast': ^3.0.4
|
||||
'@types/splitpanes': ^2.2.6
|
||||
'@types/unist': ^3.0.3
|
||||
'@xsai-ext/providers': ^0.4.0-beta.13
|
||||
'@xsai/embed': ^0.4.0-beta.13
|
||||
'@xsai/generate-speech': 0.4.0-beta.13
|
||||
|
||||
Reference in New Issue
Block a user