refactor(stage-*): improve transcription UX and DX (#1685)
This commit is contained in:
+1
-4
@@ -11,7 +11,7 @@ import { onMounted, onUnmounted, watch } from 'vue'
|
||||
const show = defineModel('show', { type: Boolean, default: false })
|
||||
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDeviceStore)
|
||||
const { enabled, stream } = storeToRefs(settingsAudioDeviceStore)
|
||||
|
||||
const getMediaAccessStatus = useElectronEventaInvoke(electron.systemPreferences.getMediaAccessStatus)
|
||||
const { state: mediaAccessStatus, execute: refreshMediaAccessStatus } = useAsyncState(() => getMediaAccessStatus(['microphone']), 'not-determined')
|
||||
@@ -58,10 +58,7 @@ onUnmounted(async () => {
|
||||
<template>
|
||||
<HearingConfigDialog
|
||||
v-model:show="show"
|
||||
v-model:enabled="enabled"
|
||||
v-model:selected-audio-input="selectedAudioInput"
|
||||
:granted="mediaAccessStatus !== 'denied' && mediaAccessStatus !== 'restricted'"
|
||||
:audio-inputs="audioInputs"
|
||||
:volume-level="volumeLevel"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -62,8 +62,11 @@
|
||||
"@proj-airi/stage-shared": "workspace:^",
|
||||
"@types/audioworklet": "catalog:",
|
||||
"@types/three": "^0.184.0",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"jsdom": "catalog:",
|
||||
"unplugin-info": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vue-tsc": "^3.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { useThreeViewControl } from '@proj-airi/stage-ui-three'
|
||||
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
|
||||
import { ChatSessionsDrawer } from '@proj-airi/stage-ui/components/scenarios/chat'
|
||||
@@ -26,6 +27,7 @@ import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
|
||||
import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
|
||||
import ActionAbout from './InteractiveArea/Actions/About.vue'
|
||||
|
||||
import { useTranscriptions } from '../../composables/use-transcriptions'
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
@@ -68,6 +70,14 @@ function isMobileDevice() {
|
||||
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
const { isListening } = useTranscriptions(
|
||||
{
|
||||
messageInputRef: messageInput,
|
||||
sendMessage: handleSend,
|
||||
isStageTamagotchi,
|
||||
},
|
||||
)
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isMobileDevice()) {
|
||||
await handleSend()
|
||||
@@ -123,7 +133,7 @@ async function setupAnalyzer() {
|
||||
analyzerSource.connect(analyser)
|
||||
}
|
||||
|
||||
watch([hearingDialogOpen, enabled, stream], () => {
|
||||
watch([enabled, stream], () => {
|
||||
setupAnalyzer()
|
||||
}, { immediate: true })
|
||||
|
||||
@@ -200,7 +210,7 @@ onMounted(() => {
|
||||
title="Hearing"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" size-5 color-class="text-neutral-500 dark:text-neutral-400" />
|
||||
<IndicatorMicVolume v-if="enabled" size-5 :color-class="isListening ? undefined : 'text-neutral-500 dark:text-neutral-400'" />
|
||||
<div v-else i-solar:microphone-3-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</Transition>
|
||||
</button>
|
||||
|
||||
@@ -4,28 +4,29 @@ import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { ChatSessionsDrawer } from '@proj-airi/stage-ui/components/scenarios/chat'
|
||||
import { HearingConfig } from '@proj-airi/stage-ui/components/scenarios/dialogs/audio-input/index'
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { BasicTextarea, FieldCombobox } from '@proj-airi/ui'
|
||||
import { until, useLocalStorage } from '@vueuse/core'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger, PopoverContent, PopoverRoot, PopoverTrigger } from 'reka-ui'
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IndicatorMicVolume from './IndicatorMicVolume.vue'
|
||||
|
||||
const messageInput = ref('')
|
||||
import { useTranscriptions } from '../../composables/use-transcriptions'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const hearingPopoverOpen = ref(false)
|
||||
const sessionsDrawerOpen = ref(false)
|
||||
const isComposing = ref(false)
|
||||
const isListening = ref(false) // Transcription listening state (separate from microphone enabled)
|
||||
const DOUBLE_ENTER_INTERVAL_MS = 300
|
||||
const TRAILING_NEWLINES_REGEX = /[\r\n]+$/
|
||||
const SEND_MODES = ['enter', 'ctrl-enter', 'double-enter'] as const
|
||||
@@ -37,8 +38,8 @@ const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { askPermission, startStream } = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(useSettingsAudioDevice())
|
||||
const { askPermission } = useSettingsAudioDevice()
|
||||
const { enabled, stream } = storeToRefs(useSettingsAudioDevice())
|
||||
const chatOrchestrator = useChatOrchestratorStore()
|
||||
const chatSession = useChatSessionStore()
|
||||
const { ingest, onAfterMessageComposed } = chatOrchestrator
|
||||
@@ -51,73 +52,13 @@ const sendModeLabels = computed<Record<SendMode, string>>(() => ({
|
||||
'double-enter': t('stage.send-mode.double-enter'),
|
||||
}))
|
||||
|
||||
// Transcription pipeline
|
||||
const hearingStore = useHearingStore()
|
||||
const hearingPipeline = useHearingSpeechInputPipeline()
|
||||
const { transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline
|
||||
const { supportsStreamInput } = storeToRefs(hearingPipeline)
|
||||
const { configured: hearingConfigured, autoSendEnabled, autoSendDelay } = storeToRefs(hearingStore)
|
||||
const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!stream.value)
|
||||
|
||||
// Auto-send logic
|
||||
let autoSendTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
const pendingAutoSendText = ref('')
|
||||
|
||||
function clearPendingAutoSend() {
|
||||
if (autoSendTimeout) {
|
||||
clearTimeout(autoSendTimeout)
|
||||
autoSendTimeout = undefined
|
||||
}
|
||||
pendingAutoSendText.value = ''
|
||||
}
|
||||
|
||||
async function debouncedAutoSend(text: string) {
|
||||
// Double-check auto-send is enabled before proceeding
|
||||
if (!autoSendEnabled.value) {
|
||||
clearPendingAutoSend()
|
||||
return
|
||||
}
|
||||
|
||||
// Add text to pending buffer
|
||||
pendingAutoSendText.value = pendingAutoSendText.value ? `${pendingAutoSendText.value} ${text}` : text
|
||||
|
||||
// Clear existing timeout
|
||||
if (autoSendTimeout) {
|
||||
clearTimeout(autoSendTimeout)
|
||||
}
|
||||
|
||||
// Set new timeout
|
||||
autoSendTimeout = setTimeout(async () => {
|
||||
// Final check before sending - auto-send might have been disabled while waiting
|
||||
if (!autoSendEnabled.value) {
|
||||
clearPendingAutoSend()
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = pendingAutoSendText.value.trim()
|
||||
if (textToSend && autoSendEnabled.value) {
|
||||
try {
|
||||
// `ingest()` resolves only after the full assistant turn finishes; clear UI/buffer now so
|
||||
// the next SentenceEnd during streaming does not append to the message we already committed.
|
||||
messageInput.value = ''
|
||||
pendingAutoSendText.value = ''
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
await ingest(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[ChatArea] Auto-send error:', err)
|
||||
// Preserve any transcription that arrived while ingest was in flight (see PR review).
|
||||
messageInput.value = [textToSend, messageInput.value.trim()].filter(Boolean).join(' ')
|
||||
pendingAutoSendText.value = [textToSend, pendingAutoSendText.value.trim()].filter(Boolean).join(' ')
|
||||
}
|
||||
}
|
||||
autoSendTimeout = undefined
|
||||
}, autoSendDelay.value)
|
||||
}
|
||||
const { isListening, startStreamingTranscription, stopStreamingTranscription, autoSendEnabled } = useTranscriptions(
|
||||
{
|
||||
messageInputRef: messageInput,
|
||||
sendMessage: handleSend,
|
||||
isStageTamagotchi,
|
||||
},
|
||||
)
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim() || isComposing.value) {
|
||||
@@ -137,7 +78,8 @@ async function handleSend() {
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messageInput.value = textToSend
|
||||
// preserve any user input when failed to send the message
|
||||
messageInput.value = [textToSend, messageInput.value.trim()].filter(Boolean).join(' ')
|
||||
chatSession.setSessionMessages(chatSession.activeSessionId, [
|
||||
...messages.value.slice(0, -1),
|
||||
{
|
||||
@@ -223,231 +165,12 @@ async function setupAnalyzer() {
|
||||
analyzerSource.connect(analyser)
|
||||
}
|
||||
|
||||
watch([hearingPopoverOpen, enabled, stream], () => {
|
||||
watch([enabled], () => {
|
||||
setupAnalyzer()
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
teardownAnalyzer()
|
||||
stopListening()
|
||||
|
||||
// Clear auto-send timeout on unmount
|
||||
if (autoSendTimeout) {
|
||||
clearTimeout(autoSendTimeout)
|
||||
autoSendTimeout = undefined
|
||||
}
|
||||
})
|
||||
|
||||
// Transcription listening functions
|
||||
async function startListening() {
|
||||
// Allow calling this even if already listening - transcribeForMediaStream will handle session reuse/restart
|
||||
try {
|
||||
console.info('[ChatArea] Starting listening...', {
|
||||
enabled: enabled.value,
|
||||
hasStream: !!stream.value,
|
||||
supportsStreamInput: supportsStreamInput.value,
|
||||
hearingConfigured: hearingConfigured.value,
|
||||
})
|
||||
|
||||
// Auto-configure Web Speech API as default if no provider is configured
|
||||
if (!hearingConfigured.value) {
|
||||
// Check if Web Speech API is available in the browser
|
||||
// Web Speech API is NOT available in Electron (stage-tamagotchi) - it requires Google's embedded API keys
|
||||
// which are not available in Electron, causing it to fail at runtime
|
||||
const isWebSpeechAvailable = typeof window !== 'undefined'
|
||||
&& !isStageTamagotchi() // Explicitly exclude Electron
|
||||
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
|
||||
|
||||
if (isWebSpeechAvailable) {
|
||||
console.info('[ChatArea] No transcription provider configured. Auto-configuring Web Speech API as default...')
|
||||
|
||||
// Initialize the provider in the providers store first
|
||||
try {
|
||||
providersStore.initializeProvider('browser-web-speech-api')
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[ChatArea] Error initializing Web Speech API provider:', err)
|
||||
}
|
||||
|
||||
// Set as active provider
|
||||
hearingStore.activeTranscriptionProvider = 'browser-web-speech-api'
|
||||
|
||||
// Wait for reactivity to update
|
||||
await nextTick()
|
||||
|
||||
// Verify the provider was set correctly
|
||||
if (hearingStore.activeTranscriptionProvider === 'browser-web-speech-api') {
|
||||
console.info('[ChatArea] Web Speech API configured as default provider')
|
||||
// Continue with transcription - Web Speech API is ready
|
||||
}
|
||||
else {
|
||||
console.error('[ChatArea] Failed to set Web Speech API as default provider')
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error('[ChatArea] Web Speech API not available. No transcription provider configured and Web Speech API is not available in this browser. Please go to Settings > Modules > Hearing to configure a transcription provider. Browser support:', {
|
||||
hasWindow: typeof window !== 'undefined',
|
||||
hasWebkitSpeechRecognition: typeof window !== 'undefined' && 'webkitSpeechRecognition' in window,
|
||||
hasSpeechRecognition: typeof window !== 'undefined' && 'SpeechRecognition' in window,
|
||||
})
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Request microphone permission if needed (microphone should already be enabled by the user)
|
||||
if (!stream.value) {
|
||||
console.info('[ChatArea] Requesting microphone permission...')
|
||||
await askPermission()
|
||||
|
||||
// If still no stream, try starting it manually
|
||||
if (!stream.value && enabled.value) {
|
||||
console.info('[ChatArea] Attempting to start stream manually...')
|
||||
startStream()
|
||||
// Wait for the stream to become available with a timeout.
|
||||
try {
|
||||
await until(stream).toBeTruthy({ timeout: 3000, throwOnTimeout: true })
|
||||
}
|
||||
catch {
|
||||
console.error('[ChatArea] Timed out waiting for audio stream.')
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream.value) {
|
||||
const errorMsg = 'Failed to get audio stream for transcription. Please check microphone permissions and ensure a device is selected.'
|
||||
console.error('[ChatArea]', errorMsg)
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Check if streaming input is supported
|
||||
if (!shouldUseStreamInput.value) {
|
||||
const errorMsg = 'Streaming input not supported by the selected transcription provider. Please select a provider that supports streaming (e.g., Web Speech API).'
|
||||
console.warn('[ChatArea]', errorMsg)
|
||||
// Clean up any existing sessions from other pages (e.g., test page) that might interfere
|
||||
await stopStreamingTranscription(true)
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
|
||||
console.info('[ChatArea] Starting streaming transcription with stream:', stream.value.id)
|
||||
|
||||
// Call transcribeForMediaStream - it's async so we await it
|
||||
// Set listening state AFTER successful call
|
||||
try {
|
||||
await transcribeForMediaStream(stream.value, {
|
||||
onSentenceEnd: (delta) => {
|
||||
if (delta && delta.trim()) {
|
||||
// Append transcribed text to message input
|
||||
const currentText = messageInput.value.trim()
|
||||
messageInput.value = currentText ? `${currentText} ${delta}` : delta
|
||||
console.info('[ChatArea] Received transcription delta:', delta)
|
||||
|
||||
// Auto-send if enabled - check the current value (not captured in closure)
|
||||
// This ensures we always respect the current setting, even if callbacks are reused
|
||||
if (autoSendEnabled.value) {
|
||||
debouncedAutoSend(delta)
|
||||
}
|
||||
else {
|
||||
// If auto-send is disabled, clear any pending auto-send text to prevent accidental sends
|
||||
clearPendingAutoSend()
|
||||
}
|
||||
}
|
||||
},
|
||||
// Omit onSpeechEnd to avoid re-adding user-deleted text; use sentence deltas only.
|
||||
})
|
||||
|
||||
// Only set listening to true if transcription started successfully
|
||||
// (transcribeForMediaStream might return early if session already exists)
|
||||
isListening.value = true
|
||||
console.info('[ChatArea] Streaming transcription initiated successfully')
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[ChatArea] Transcription error:', err)
|
||||
isListening.value = false
|
||||
throw err // Re-throw to be caught by outer catch
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[ChatArea] Failed to start transcription:', err)
|
||||
isListening.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stopListening() {
|
||||
if (!isListening.value)
|
||||
return
|
||||
|
||||
try {
|
||||
console.info('[ChatArea] Stopping transcription...')
|
||||
|
||||
// Clear auto-send timeout
|
||||
clearPendingAutoSend()
|
||||
|
||||
// Send any pending text immediately if auto-send is enabled
|
||||
if (autoSendEnabled.value && pendingAutoSendText.value.trim()) {
|
||||
const textToSend = pendingAutoSendText.value.trim()
|
||||
pendingAutoSendText.value = ''
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
await ingest(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
messageInput.value = ''
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[ChatArea] Auto-send error on stop:', err)
|
||||
}
|
||||
}
|
||||
|
||||
await stopStreamingTranscription(true)
|
||||
isListening.value = false
|
||||
console.info('[ChatArea] Transcription stopped')
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[ChatArea] Error stopping transcription:', err)
|
||||
isListening.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Start listening when microphone is enabled and stream is available
|
||||
watch(enabled, async (val) => {
|
||||
if (val && stream.value) {
|
||||
// Microphone was just enabled and we have a stream, start transcription
|
||||
await startListening()
|
||||
}
|
||||
else if (!val && isListening.value) {
|
||||
// Microphone was disabled, stop transcription
|
||||
await stopListening()
|
||||
}
|
||||
})
|
||||
|
||||
// Start listening when stream becomes available (if microphone is enabled)
|
||||
watch(stream, async (val) => {
|
||||
if (val && enabled.value && !isListening.value) {
|
||||
// Stream became available and microphone is enabled, start transcription
|
||||
await startListening()
|
||||
}
|
||||
else if (!val && isListening.value) {
|
||||
// Stream was lost, stop transcription
|
||||
await stopListening()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for auto-send setting changes and clear pending sends if disabled
|
||||
watch(autoSendEnabled, (enabled) => {
|
||||
if (!enabled) {
|
||||
// Auto-send was disabled - clear any pending auto-send
|
||||
clearPendingAutoSend()
|
||||
console.info('[ChatArea] Auto-send disabled, cleared pending text')
|
||||
}
|
||||
})
|
||||
|
||||
watch(sendMode, () => {
|
||||
@@ -553,7 +276,7 @@ watch(sendMode, () => {
|
||||
:title="t('settings.hearing.title')"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" />
|
||||
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" :color-class="isListening ? undefined : 'text-neutral-500 dark:text-neutral-400'" />
|
||||
<div v-else class="i-ph:microphone-slash h-5 w-5" />
|
||||
</Transition>
|
||||
</button>
|
||||
@@ -567,45 +290,12 @@ watch(sendMode, () => {
|
||||
'flex flex-col gap-3',
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="relative h-28 w-28 select-none">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-20 w-20 rounded-full transition-all duration-150 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1 + normalizedVolume * 0.35})`, opacity: String(0.25 + normalizedVolume * 0.25) }"
|
||||
:class="enabled ? 'bg-primary-500/15 dark:bg-primary-600/20' : 'bg-neutral-300/20 dark:bg-neutral-700/20'"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + normalizedVolume * 0.55})`, opacity: String(0.15 + normalizedVolume * 0.2) }"
|
||||
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + normalizedVolume * 0.8})`, opacity: String(0.08 + normalizedVolume * 0.15) }"
|
||||
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
|
||||
/>
|
||||
<button
|
||||
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:class="enabled
|
||||
? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95'
|
||||
: 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200'"
|
||||
@click="enabled = !enabled"
|
||||
>
|
||||
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ enabled ? 'Microphone enabled' : 'Microphone disabled' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldCombobox
|
||||
v-model="selectedAudioInput"
|
||||
label="Input device"
|
||||
description="Select the microphone you want to use."
|
||||
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
|
||||
layout="vertical"
|
||||
placeholder="Select microphone"
|
||||
<HearingConfig
|
||||
v-model:auto-send="autoSendEnabled"
|
||||
:transcription="isListening"
|
||||
:granted="true"
|
||||
:volume-level="normalizedVolume"
|
||||
@toggle-transcription="() => isListening ? stopStreamingTranscription() : startStreamingTranscription()"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { until } from '@vueuse/core'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useTranscriptions } from './use-transcriptions'
|
||||
|
||||
function createMockStore() {
|
||||
return {
|
||||
activeTranscriptionProvider: undefined,
|
||||
configured: ref(false),
|
||||
autoSendEnabled: ref(true),
|
||||
autoSendDelay: ref(2000),
|
||||
initializeProvider: vi.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
const mockTranscribedContent = 'test content'
|
||||
function createMockPipeline() {
|
||||
return {
|
||||
transcribeForMediaStream: vi.fn().mockImplementation((_stream, options: { onSentenceEnd: (delta: string) => void }) => {
|
||||
options.onSentenceEnd(mockTranscribedContent)
|
||||
}),
|
||||
stopStreamingTranscription: vi.fn().mockResolvedValue(undefined),
|
||||
supportsStreamInput: ref(true),
|
||||
}
|
||||
}
|
||||
|
||||
function createMockAudioDevice() {
|
||||
const instance = {
|
||||
enabled: ref(false),
|
||||
stream: ref(null),
|
||||
askPermission: vi.fn().mockResolvedValue(undefined),
|
||||
startStream: vi.fn(),
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
let mockHearingStore: ReturnType<typeof createMockStore>
|
||||
let mockHearingPipeline: ReturnType<typeof createMockPipeline>
|
||||
let mockAudioDevice: ReturnType<typeof createMockAudioDevice>
|
||||
let mockProvidersStore: ReturnType<typeof createMockStore>
|
||||
|
||||
// Mock the modules
|
||||
vi.mock('@proj-airi/stage-ui/stores/modules/hearing', () => ({
|
||||
useHearingStore: vi.fn().mockImplementation(() => mockHearingStore),
|
||||
useHearingSpeechInputPipeline: vi.fn().mockImplementation(() => mockHearingPipeline),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/stage-ui/stores/providers', () => ({
|
||||
useProvidersStore: vi.fn().mockImplementation(() => mockProvidersStore),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/stage-ui/stores/settings', () => ({
|
||||
useSettingsAudioDevice: vi.fn().mockImplementation(() => mockAudioDevice),
|
||||
}))
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: vi.fn().mockImplementation((val: any) => val),
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
until: vi.fn(),
|
||||
}))
|
||||
|
||||
// Global setup for jsdom environment
|
||||
beforeAll(() => {
|
||||
// Ensure window is available
|
||||
if (typeof window === 'undefined') {
|
||||
;(globalThis as any).window = {
|
||||
webkitSpeechRecognition: undefined,
|
||||
SpeechRecognition: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useTranscriptions', () => {
|
||||
// Setup mutable instances before each test
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers() // Use fake timers for auto-send tests
|
||||
mockHearingStore = createMockStore()
|
||||
mockHearingPipeline = createMockPipeline()
|
||||
mockAudioDevice = createMockAudioDevice()
|
||||
mockProvidersStore = createMockStore()
|
||||
|
||||
// Mock 'until' to resolve immediately for stream checks
|
||||
;(until as any).mockImplementation((_source: Ref) => ({
|
||||
toBeTruthy: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
// Mock SpeechRecognition for browser tests
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).SpeechRecognition = function () {
|
||||
this.start = vi.fn()
|
||||
this.stop = vi.fn()
|
||||
this.onresult = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const createOptions = (isTamagotchi = false) => ({
|
||||
messageInputRef: ref(''),
|
||||
sendMessage: vi.fn(),
|
||||
isStageTamagotchi: ref(isTamagotchi),
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with isListening false', () => {
|
||||
const { isListening } = useTranscriptions(createOptions())
|
||||
|
||||
expect(isListening.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should expose startListening and stopListening', () => {
|
||||
const { startStreamingTranscription, stopStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
expect(startStreamingTranscription).toBeInstanceOf(Function)
|
||||
expect(stopStreamingTranscription).toBeInstanceOf(Function)
|
||||
})
|
||||
})
|
||||
|
||||
describe('auto-Configuration (Web Speech API)', () => {
|
||||
it('should auto-configure Web Speech API if no provider is set', async () => {
|
||||
mockHearingStore.configured.value = false
|
||||
mockAudioDevice.enabled.value = true
|
||||
|
||||
const { startStreamingTranscription }
|
||||
= useTranscriptions(createOptions())
|
||||
await startStreamingTranscription()
|
||||
|
||||
expect(mockProvidersStore.initializeProvider).toHaveBeenCalledWith('browser-web-speech-api')
|
||||
expect(mockHearingStore.activeTranscriptionProvider).toBe('browser-web-speech-api')
|
||||
})
|
||||
|
||||
it('should fail gracefully if Web Speech API is not available', async () => {
|
||||
// Setup: Tamagotchi mode or no API
|
||||
if (typeof window !== 'undefined') {
|
||||
delete (window as any).SpeechRecognition
|
||||
delete (window as any).webkitSpeechRecognition
|
||||
}
|
||||
|
||||
mockHearingStore.configured.value = false
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
|
||||
const { isListening, startStreamingTranscription }
|
||||
= useTranscriptions(createOptions())
|
||||
await startStreamingTranscription()
|
||||
|
||||
expect(isListening.value).toBe(false)
|
||||
expect(mockHearingPipeline.transcribeForMediaStream).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle tamagotchi', async () => {
|
||||
mockHearingStore.configured.value = false
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
|
||||
const { isListening, stopStreamingTranscription }
|
||||
= useTranscriptions(createOptions(true))
|
||||
await stopStreamingTranscription()
|
||||
expect(isListening.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming Logic', () => {
|
||||
it('should start streaming if stream exists and provider supports it', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
await startStreamingTranscription()
|
||||
|
||||
await nextTick()
|
||||
expect(isListening.value).toBe(true)
|
||||
expect(mockHearingPipeline.transcribeForMediaStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'stream-1' }),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it('should request permission if stream is missing', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = null
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { startStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
expect(mockAudioDevice.askPermission).toHaveBeenCalled()
|
||||
expect(mockAudioDevice.startStream).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should stop streaming if stream is missing after permission', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = null
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true;
|
||||
|
||||
// simulate failure (stream never appears)
|
||||
(until as any).mockImplementation(() => ({
|
||||
toBeTruthy: vi.fn().mockRejectedValue(new Error('Timeout')),
|
||||
}))
|
||||
|
||||
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
expect(isListening.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transcription & Input', () => {
|
||||
it('should append transcribed text to messageInputRef', async () => {
|
||||
const mockInput = ref('')
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { startStreamingTranscription }
|
||||
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput })
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
expect(mockInput.value).toBe(mockTranscribedContent)
|
||||
})
|
||||
|
||||
it('should append transcribed text with a space when input contains value', async () => {
|
||||
const prependText = 'prepend text'
|
||||
const mockInput = ref(prependText)
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { startStreamingTranscription }
|
||||
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput })
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
expect(mockInput.value).toBe(`${prependText} ${mockTranscribedContent}`)
|
||||
})
|
||||
|
||||
it('should trigger auto-send after delay', async () => {
|
||||
const mockInput = ref('')
|
||||
const mockSendMessage = vi.fn()
|
||||
|
||||
mockHearingStore.autoSendDelay.value = 500
|
||||
mockHearingStore.configured.value = true
|
||||
mockHearingStore.autoSendEnabled.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { startStreamingTranscription }
|
||||
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput, sendMessage: mockSendMessage })
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
expect(mockSendMessage).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
expect(mockSendMessage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear pending auto-send if disabled', async () => {
|
||||
const mockInput = ref('')
|
||||
const mockSendMessage = vi.fn()
|
||||
|
||||
mockHearingStore.autoSendDelay.value = 500
|
||||
mockHearingStore.configured.value = true
|
||||
mockHearingStore.autoSendEnabled.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { startStreamingTranscription }
|
||||
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput, sendMessage: mockSendMessage })
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
|
||||
// Disable auto-send before timeout
|
||||
mockHearingStore.autoSendEnabled.value = false
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
expect(mockSendMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should stop streaming and clear timeout', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { isListening, startStreamingTranscription, stopStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
await startStreamingTranscription()
|
||||
await nextTick()
|
||||
expect(isListening.value).toBe(true)
|
||||
|
||||
await stopStreamingTranscription()
|
||||
await nextTick()
|
||||
expect(isListening.value).toBe(false)
|
||||
expect(mockHearingPipeline.stopStreamingTranscription).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should stop streaming on unmount', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const app = mount({
|
||||
setup() {
|
||||
const { startStreamingTranscription } = useTranscriptions(createOptions())
|
||||
startStreamingTranscription()
|
||||
},
|
||||
template: '<div></div>',
|
||||
})
|
||||
await nextTick()
|
||||
expect(mockHearingPipeline.transcribeForMediaStream).toHaveBeenCalled()
|
||||
|
||||
app.unmount()
|
||||
await nextTick()
|
||||
expect(mockHearingPipeline.stopStreamingTranscription).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactive watchers', () => {
|
||||
it('should stop listening if microphone is disabled', async () => {
|
||||
mockHearingStore.configured.value = true
|
||||
mockAudioDevice.stream.value = { id: 'stream-1' } as any
|
||||
mockAudioDevice.enabled.value = true
|
||||
mockHearingPipeline.supportsStreamInput.value = true
|
||||
|
||||
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
|
||||
|
||||
await startStreamingTranscription()
|
||||
|
||||
await nextTick()
|
||||
expect(isListening.value).toBe(true)
|
||||
|
||||
mockAudioDevice.enabled.value = false
|
||||
|
||||
await nextTick()
|
||||
expect(isListening.value).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,229 @@
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue'
|
||||
|
||||
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { until } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, onScopeDispose, ref, toValue, watch } from 'vue'
|
||||
|
||||
interface TranscriptionOptions {
|
||||
messageInputRef: Ref<string>
|
||||
sendMessage: () => void
|
||||
isStageTamagotchi: MaybeRefOrGetter<boolean>
|
||||
}
|
||||
|
||||
export function useTranscriptions(options: TranscriptionOptions) {
|
||||
const { messageInputRef: messageInput, sendMessage, isStageTamagotchi } = options
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
const hearingPipeline = useHearingSpeechInputPipeline()
|
||||
const { transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline
|
||||
const { supportsStreamInput } = storeToRefs(hearingPipeline)
|
||||
const { configured: hearingConfigured, autoSendEnabled, autoSendDelay } = storeToRefs(hearingStore)
|
||||
const { enabled: hearingEnabled, stream } = storeToRefs(useSettingsAudioDevice())
|
||||
const providersStore = useProvidersStore()
|
||||
const { askPermission, startStream } = useSettingsAudioDevice()
|
||||
|
||||
const isListening = ref(false)
|
||||
|
||||
// Auto-send logic
|
||||
let autoSendTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
function clearPendingAutoSend() {
|
||||
if (autoSendTimeout) {
|
||||
clearTimeout(autoSendTimeout)
|
||||
autoSendTimeout = undefined
|
||||
}
|
||||
}
|
||||
async function debouncedAutoSend() {
|
||||
// Double-check auto-send is enabled before proceeding
|
||||
if (!autoSendEnabled.value) {
|
||||
clearPendingAutoSend()
|
||||
return
|
||||
}
|
||||
if (autoSendTimeout) {
|
||||
clearTimeout(autoSendTimeout)
|
||||
}
|
||||
|
||||
autoSendTimeout = setTimeout(async () => {
|
||||
// Final check before sending - auto-send might have been disabled while waiting
|
||||
if (!autoSendEnabled.value) {
|
||||
clearPendingAutoSend()
|
||||
return
|
||||
}
|
||||
sendMessage()
|
||||
autoSendTimeout = undefined
|
||||
}, autoSendDelay.value)
|
||||
}
|
||||
|
||||
const stopStreaming = async () => {
|
||||
if (!isListening.value)
|
||||
return
|
||||
|
||||
try {
|
||||
console.info('Stopping transcription...', { source: 'useTranscriptions' })
|
||||
clearPendingAutoSend()
|
||||
await stopStreamingTranscription(true)
|
||||
isListening.value = false
|
||||
console.info('Transcription stopped', { source: 'useTranscriptions' })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Error stopping transcription:', err, { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startStreaming = async () => {
|
||||
console.info('Starting streaming transcription', {
|
||||
enabled: hearingEnabled.value,
|
||||
hasStream: !!stream.value,
|
||||
supportsStreamInput: supportsStreamInput.value,
|
||||
hearingConfigured: hearingConfigured.value,
|
||||
}, { source: 'useTranscriptions' })
|
||||
|
||||
// Auto-configure Web Speech API as default if no provider is configured
|
||||
if (!hearingConfigured.value) {
|
||||
console.info('No transcription provider configured. Auto-configuring Web Speech API as default', { source: 'useTranscriptions' })
|
||||
// Check if Web Speech API is available in the browser
|
||||
// Web Speech API is NOT available in Electron (stage-tamagotchi) - it requires Google's embedded API keys
|
||||
// which are not available in Electron, causing it to fail at runtime
|
||||
const isWebSpeechAvailable = typeof window !== 'undefined'
|
||||
&& !toValue(isStageTamagotchi) // Explicitly exclude Electron
|
||||
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
|
||||
|
||||
if (!isWebSpeechAvailable) {
|
||||
// TODO: also propagate to user
|
||||
const errorMsg = 'Web Speech API is not available and no transcription provider is configured. Please go to Settings > Modules > Hearing to configure a transcription provider. '
|
||||
console.error(errorMsg, 'Browser support:', {
|
||||
hasWindow: typeof window !== 'undefined',
|
||||
hasWebkitSpeechRecognition: typeof window !== 'undefined' && 'webkitSpeechRecognition' in window,
|
||||
hasSpeechRecognition: typeof window !== 'undefined' && 'SpeechRecognition' in window,
|
||||
}, { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the provider in the providers store first
|
||||
try {
|
||||
providersStore.initializeProvider('browser-web-speech-api')
|
||||
hearingStore.activeTranscriptionProvider = 'browser-web-speech-api'
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('Error initializing Web Speech API provider:', err, { source: 'useTranscriptions' })
|
||||
}
|
||||
// Wait for reactivity to update
|
||||
await nextTick()
|
||||
|
||||
// Verify the provider was set to Web Speech API
|
||||
if (hearingStore.activeTranscriptionProvider !== 'browser-web-speech-api') {
|
||||
console.error('Failed to set Web Speech API as default provider', { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
console.info('Web Speech API configured as default provider', { source: 'useTranscriptions' })
|
||||
}
|
||||
|
||||
// Check if streaming input is supported
|
||||
// TODO: implement non-streaming transcription
|
||||
if (!supportsStreamInput.value) {
|
||||
const errorMsg = 'Streaming input not supported by the selected transcription provider. Please select a provider that supports streaming (e.g., Web Speech API).'
|
||||
console.warn(errorMsg, { source: 'useTranscriptions' })
|
||||
// Clean up any existing sessions from other pages (e.g., test page) that might interfere
|
||||
await stopStreamingTranscription(true)
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Request microphone permission if needed (microphone should already be enabled by the user)
|
||||
if (!stream.value) {
|
||||
console.info('Requesting microphone permission', { source: 'useTranscriptions' })
|
||||
await askPermission()
|
||||
|
||||
// If still no stream, try starting it manually
|
||||
if (!stream.value && hearingEnabled.value) {
|
||||
console.info('Attempting to start stream manually', { source: 'useTranscriptions' })
|
||||
startStream()
|
||||
// Wait for the stream to become available with a timeout.
|
||||
try {
|
||||
await until(stream).toBeTruthy({ timeout: 3000, throwOnTimeout: true })
|
||||
}
|
||||
catch {
|
||||
console.error('Timed out waiting for audio stream. Stopping transcription.', { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to request microphone permission:', err, { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
}
|
||||
|
||||
if (!stream.value) {
|
||||
const errorMsg = 'Failed to get audio stream for transcription. Please check microphone permissions and ensure a device is selected.'
|
||||
console.error(errorMsg, { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
return
|
||||
}
|
||||
|
||||
console.info('Starting streaming transcription with stream:', stream.value.id, { source: 'useTranscriptions' })
|
||||
|
||||
// Allow calling this even if already listening - transcribeForMediaStream will handle session reuse/restart
|
||||
// Call transcribeForMediaStream - it's async so we await it
|
||||
// Set listening state AFTER successful call
|
||||
try {
|
||||
await transcribeForMediaStream(stream.value, {
|
||||
onSentenceEnd: (delta) => {
|
||||
if (delta && delta.trim()) {
|
||||
console.info('Received transcription delta:', delta, { source: 'useTranscriptions' })
|
||||
// Append transcribed text to message input
|
||||
const currentText = messageInput.value.trim()
|
||||
messageInput.value = currentText ? `${currentText} ${delta}` : delta
|
||||
debouncedAutoSend()
|
||||
}
|
||||
},
|
||||
// Omit onSpeechEnd to avoid re-adding user-deleted text; use sentence deltas only.
|
||||
})
|
||||
|
||||
// Only set listening to true if transcription started successfully
|
||||
// (transcribeForMediaStream might return early if session already exists)
|
||||
isListening.value = true
|
||||
console.info('Streaming transcription initiated successfully', { source: 'useTranscriptions' })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Transcription error:', err, { source: 'useTranscriptions' })
|
||||
isListening.value = false
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for auto-send setting changes and clear pending sends if disabled
|
||||
watch(autoSendEnabled, (enabled) => {
|
||||
if (!enabled) {
|
||||
clearPendingAutoSend()
|
||||
console.info('Auto-send disabled', { source: 'useTranscriptions' })
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for auto-send setting changes and clear pending sends if disabled
|
||||
watch(hearingEnabled, async (enabled) => {
|
||||
if (!enabled) {
|
||||
await stopStreaming()
|
||||
console.info('Stopping streaming transcription because hearing is disabled.', { source: 'useTranscriptions' })
|
||||
}
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
clearPendingAutoSend()
|
||||
stopStreaming()
|
||||
})
|
||||
|
||||
return {
|
||||
startStreamingTranscription: startStreaming,
|
||||
stopStreamingTranscription: stopStreaming,
|
||||
isListening,
|
||||
autoSendEnabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
-9
@@ -12,13 +12,10 @@ const props = defineProps<{
|
||||
overlayDim?: boolean
|
||||
overlayBlur?: boolean
|
||||
granted?: boolean
|
||||
audioInputs?: MediaDeviceInfo[]
|
||||
volumeLevel?: number
|
||||
}>()
|
||||
|
||||
const showDialog = defineModel('show', { type: Boolean, default: false, required: false })
|
||||
const selectedAudioInput = defineModel<string>('selectedAudioInput')
|
||||
const enabled = defineModel<boolean>('enabled', { default: false })
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
const screenSafeArea = useScreenSafeArea()
|
||||
@@ -45,9 +42,6 @@ onMounted(() => screenSafeArea.update())
|
||||
<DialogTitle>Hearing Input</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<HearingConfig
|
||||
v-model:enabled="enabled"
|
||||
v-model:selected-audio-input="selectedAudioInput"
|
||||
:audio-inputs="props.audioInputs"
|
||||
:granted="props.granted"
|
||||
:volume-level="props.volumeLevel"
|
||||
/>
|
||||
@@ -78,9 +72,6 @@ onMounted(() => screenSafeArea.update())
|
||||
]"
|
||||
/>
|
||||
<HearingConfig
|
||||
v-model:enabled="enabled"
|
||||
v-model:selected-audio-input="selectedAudioInput"
|
||||
:audio-inputs="props.audioInputs"
|
||||
:granted="props.granted"
|
||||
:volume-level="props.volumeLevel"
|
||||
/>
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { Callout, FieldCombobox } from '@proj-airi/ui'
|
||||
import { Button, Callout, FieldCombobox } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingsAudioDevice } from '../../../../stores'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
enabled?: boolean
|
||||
granted?: boolean
|
||||
audioInputs?: MediaDeviceInfo[]
|
||||
volumeLevel?: number
|
||||
transcription?: boolean
|
||||
}>(), {
|
||||
enabled: false,
|
||||
granted: false,
|
||||
audioInputs: () => [],
|
||||
volumeLevel: 0,
|
||||
})
|
||||
|
||||
const enabled = defineModel<boolean>('enabled')
|
||||
const selectedAudioInput = defineModel<string>('selectedAudioInput')
|
||||
const emit = defineEmits(['toggleTranscription'])
|
||||
|
||||
const { enabled, selectedAudioInput, audioInputs } = storeToRefs(useSettingsAudioDevice())
|
||||
|
||||
const autoSend = defineModel<boolean>('autoSend')
|
||||
const ringEnabledClass = computed(() => enabled.value
|
||||
? 'bg-primary-500/15 dark:bg-primary-600/20'
|
||||
: 'bg-neutral-300/20 dark:bg-neutral-700/20',
|
||||
)
|
||||
|
||||
function toggleHearingEnabled() {
|
||||
if (enabled.value)
|
||||
return enabled.value = false
|
||||
if (!enabled.value && selectedAudioInput.value !== '')
|
||||
enabled.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,21 +46,21 @@ const ringEnabledClass = computed(() => enabled.value
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + (props.volumeLevel / 100) * 0.55})`, opacity: String(0.15 + (props.volumeLevel / 100) * 0.2) }"
|
||||
:class="props.enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
|
||||
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + (props.volumeLevel / 100) * 0.8})`, opacity: String(0.08 + (props.volumeLevel / 100) * 0.15) }"
|
||||
:class="props.enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
|
||||
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
|
||||
/>
|
||||
|
||||
<!-- Mic icon button -->
|
||||
<button
|
||||
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:class="[
|
||||
props.enabled ? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95' : 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200',
|
||||
enabled ? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95' : 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200',
|
||||
]"
|
||||
@click="() => enabled = !enabled"
|
||||
@click="toggleHearingEnabled"
|
||||
>
|
||||
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
|
||||
</button>
|
||||
@@ -70,13 +79,30 @@ const ringEnabledClass = computed(() => enabled.value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
v-if="props.transcription !== undefined"
|
||||
label="Transcription"
|
||||
:variant="props.transcription ? 'primary' : 'secondary'"
|
||||
flex-1
|
||||
@click="() => emit('toggleTranscription')"
|
||||
/>
|
||||
<Button
|
||||
v-if="autoSend !== undefined"
|
||||
label="Auto send"
|
||||
:variant="autoSend ? 'primary' : 'secondary'"
|
||||
flex-1
|
||||
@click="autoSend = !autoSend"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Always-visible device selector -->
|
||||
<div class="mt-3 w-full">
|
||||
<FieldCombobox
|
||||
v-model="selectedAudioInput"
|
||||
label="Input device"
|
||||
description="Select the microphone you want to use."
|
||||
:options="props.audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
|
||||
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
|
||||
placeholder="Select microphone"
|
||||
layout="vertical"
|
||||
/>
|
||||
|
||||
Generated
+224
-46
@@ -138,6 +138,9 @@ catalogs:
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
'@vue/test-utils':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@vueuse/core':
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0
|
||||
@@ -255,6 +258,9 @@ catalogs:
|
||||
jose:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
jsdom:
|
||||
specifier: ^29.0.2
|
||||
version: 29.0.2
|
||||
knip:
|
||||
specifier: ^6.4.1
|
||||
version: 6.4.1
|
||||
@@ -339,6 +345,9 @@ catalogs:
|
||||
vite-plugin-mkcert:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
vitest:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4
|
||||
vitest-browser-vue:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
@@ -543,7 +552,7 @@ importers:
|
||||
version: 12.0.0-beta.1(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))
|
||||
vitest:
|
||||
specifier: catalog:vitest
|
||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest-browser-vue:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3))
|
||||
@@ -2491,7 +2500,7 @@ importers:
|
||||
version: 5.1.9
|
||||
vieval:
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
xsschema:
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
|
||||
@@ -2940,12 +2949,21 @@ importers:
|
||||
'@types/three':
|
||||
specifier: ^0.184.0
|
||||
version: 0.184.0
|
||||
'@vue/test-utils':
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.6
|
||||
'@webgpu/types':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.69
|
||||
jsdom:
|
||||
specifier: 'catalog:'
|
||||
version: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
|
||||
unplugin-info:
|
||||
specifier: 'catalog:'
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
@@ -3431,7 +3449,7 @@ importers:
|
||||
version: 5.2.10
|
||||
'@histoire/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.0-beta.1(histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 1.0.0-beta.1(histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
'@iconify-json/carbon':
|
||||
specifier: ^1.2.20
|
||||
version: 1.2.20
|
||||
@@ -3503,7 +3521,7 @@ importers:
|
||||
version: 3.2.3
|
||||
histoire:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
|
||||
version: 1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
|
||||
is-network-error:
|
||||
specifier: 'catalog:'
|
||||
version: 1.3.1
|
||||
@@ -3994,7 +4012,7 @@ importers:
|
||||
version: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vieval:
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
xsschema:
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
|
||||
@@ -4579,9 +4597,21 @@ packages:
|
||||
'@asamuzakjp/css-color@4.1.1':
|
||||
resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}
|
||||
|
||||
'@asamuzakjp/css-color@5.1.11':
|
||||
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/dom-selector@6.7.6':
|
||||
resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}
|
||||
|
||||
'@asamuzakjp/dom-selector@7.1.1':
|
||||
resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/generational-cache@1.0.1':
|
||||
resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9':
|
||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||
|
||||
@@ -5314,6 +5344,10 @@ packages:
|
||||
'@braidai/lang@1.1.2':
|
||||
resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||
hasBin: true
|
||||
|
||||
'@bufbuild/protobuf@2.10.2':
|
||||
resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==}
|
||||
|
||||
@@ -5405,6 +5439,10 @@ packages:
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@csstools/color-helpers@6.0.2':
|
||||
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@csstools/css-calc@2.1.4':
|
||||
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5412,6 +5450,13 @@ packages:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-calc@3.2.0':
|
||||
resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-color-parser@3.1.0':
|
||||
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5419,20 +5464,41 @@ packages:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-color-parser@4.1.0':
|
||||
resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5':
|
||||
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.25':
|
||||
resolution: {integrity: sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/css-parser-algorithms@4.0.0':
|
||||
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.3':
|
||||
resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==}
|
||||
peerDependencies:
|
||||
css-tree: ^3.2.1
|
||||
peerDependenciesMeta:
|
||||
css-tree:
|
||||
optional: true
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4':
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0':
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
@@ -6132,13 +6198,13 @@ packages:
|
||||
resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@exodus/bytes@1.8.0':
|
||||
resolution: {integrity: sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==}
|
||||
'@exodus/bytes@1.15.0':
|
||||
resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
'@exodus/crypto': ^1.0.0-rc.4
|
||||
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
'@exodus/crypto':
|
||||
'@noble/hashes':
|
||||
optional: true
|
||||
|
||||
'@ffmpeg-installer/darwin-arm64@4.1.5':
|
||||
@@ -11908,6 +11974,10 @@ packages:
|
||||
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
data-urls@7.0.0:
|
||||
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
@@ -14070,6 +14140,15 @@ packages:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsdom@29.0.2:
|
||||
resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -16899,8 +16978,8 @@ packages:
|
||||
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
tough-cookie@6.0.1:
|
||||
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
@@ -17132,6 +17211,10 @@ packages:
|
||||
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.25.0:
|
||||
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@8.1.0:
|
||||
resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
@@ -18022,6 +18105,10 @@ packages:
|
||||
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
whatwg-url@16.0.1:
|
||||
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -18500,6 +18587,14 @@ snapshots:
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
lru-cache: 11.3.5
|
||||
|
||||
'@asamuzakjp/css-color@5.1.11':
|
||||
dependencies:
|
||||
'@asamuzakjp/generational-cache': 1.0.1
|
||||
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@asamuzakjp/dom-selector@6.7.6':
|
||||
dependencies:
|
||||
'@asamuzakjp/nwsapi': 2.3.9
|
||||
@@ -18508,6 +18603,16 @@ snapshots:
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.3.5
|
||||
|
||||
'@asamuzakjp/dom-selector@7.1.1':
|
||||
dependencies:
|
||||
'@asamuzakjp/generational-cache': 1.0.1
|
||||
'@asamuzakjp/nwsapi': 2.3.9
|
||||
bidi-js: 1.0.3
|
||||
css-tree: 3.2.1
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
|
||||
'@asamuzakjp/generational-cache@1.0.1': {}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
'@ax-llm/ax@19.0.45(zod@4.3.6)':
|
||||
@@ -19471,6 +19576,10 @@ snapshots:
|
||||
|
||||
'@braidai/lang@1.1.2': {}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
dependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@bufbuild/protobuf@2.10.2': {}
|
||||
|
||||
'@capacitor/android@8.3.1(@capacitor/core@8.3.1)':
|
||||
@@ -19610,11 +19719,18 @@ snapshots:
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
'@csstools/color-helpers@6.0.2': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 5.1.0
|
||||
@@ -19622,14 +19738,29 @@ snapshots:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 6.0.2
|
||||
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.25': {}
|
||||
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)':
|
||||
optionalDependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@date-fns/utc@2.1.1': {}
|
||||
@@ -20225,7 +20356,9 @@ snapshots:
|
||||
'@eslint/core': 1.2.1
|
||||
levn: 0.4.1
|
||||
|
||||
'@exodus/bytes@1.8.0': {}
|
||||
'@exodus/bytes@1.15.0(@noble/hashes@2.0.1)':
|
||||
optionalDependencies:
|
||||
'@noble/hashes': 2.0.1
|
||||
|
||||
'@ffmpeg-installer/darwin-arm64@4.1.5':
|
||||
optional: true
|
||||
@@ -20361,14 +20494,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
'@histoire/plugin-vue@1.0.0-beta.1(histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
'@histoire/plugin-vue@1.0.0-beta.1(histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@histoire/controls': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@histoire/shared': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@histoire/vendors': 1.0.0-beta.1
|
||||
change-case: 5.4.4
|
||||
globby: 14.1.0
|
||||
histoire: 1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
|
||||
histoire: 1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
|
||||
launch-editor: 2.12.0
|
||||
pathe: 1.1.2
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
@@ -24330,7 +24463,7 @@ snapshots:
|
||||
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
playwright: 1.59.1
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
@@ -24343,7 +24476,7 @@ snapshots:
|
||||
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
playwright: 1.59.1
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
@@ -24360,7 +24493,7 @@ snapshots:
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -24377,7 +24510,7 @@ snapshots:
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -24398,7 +24531,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.1.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
|
||||
@@ -24410,7 +24543,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.51.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -25559,7 +25692,7 @@ snapshots:
|
||||
drizzle-orm: 0.41.0(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9)
|
||||
pg: 8.20.0
|
||||
react: 19.2.3
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
better-auth@1.6.5(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(better-sqlite3@12.5.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9))(pg@8.20.0)(react@19.2.3)(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3)):
|
||||
@@ -25588,7 +25721,7 @@ snapshots:
|
||||
drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9)
|
||||
pg: 8.20.0
|
||||
react: 19.2.3
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
@@ -26239,7 +26372,7 @@ snapshots:
|
||||
cssstyle@5.3.7:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 4.1.1
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.0.25
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
|
||||
css-tree: 3.2.1
|
||||
lru-cache: 11.3.5
|
||||
|
||||
@@ -26415,6 +26548,13 @@ snapshots:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 15.1.0
|
||||
|
||||
data-urls@7.0.0(@noble/hashes@2.0.1):
|
||||
dependencies:
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1(@noble/hashes@2.0.1)
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
@@ -28326,7 +28466,7 @@ snapshots:
|
||||
gray-matter: 4.0.3
|
||||
unplugin: 3.0.0
|
||||
|
||||
histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3):
|
||||
histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@akryum/tinypool': 0.3.1
|
||||
'@histoire/app': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -28344,7 +28484,7 @@ snapshots:
|
||||
globby: 14.1.0
|
||||
gray-matter: 4.0.3
|
||||
jiti: 2.6.1
|
||||
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
||||
jsdom: 27.4.0(@noble/hashes@2.0.1)(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
||||
markdown-it: 14.1.1
|
||||
markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1)
|
||||
markdown-it-attrs: 4.3.1(markdown-it@14.1.1)
|
||||
@@ -28359,7 +28499,7 @@ snapshots:
|
||||
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- '@exodus/crypto'
|
||||
- '@noble/hashes'
|
||||
- '@types/node'
|
||||
- bufferutil
|
||||
- canvas
|
||||
@@ -28393,11 +28533,11 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
html-encoding-sniffer@6.0.0:
|
||||
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.8.0
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
||||
transitivePeerDependencies:
|
||||
- '@exodus/crypto'
|
||||
- '@noble/hashes'
|
||||
|
||||
html-entities@2.6.0: {}
|
||||
|
||||
@@ -28812,22 +28952,22 @@ snapshots:
|
||||
|
||||
jsdoc-type-pratt-parser@7.2.0: {}
|
||||
|
||||
jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10):
|
||||
jsdom@27.4.0(@noble/hashes@2.0.1)(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10):
|
||||
dependencies:
|
||||
'@acemir/cssom': 0.9.31
|
||||
'@asamuzakjp/dom-selector': 6.7.6
|
||||
'@exodus/bytes': 1.8.0
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
||||
cssstyle: 5.3.7
|
||||
data-urls: 6.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 6.0.0
|
||||
html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1)
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
parse5: 8.0.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 6.0.0
|
||||
tough-cookie: 6.0.1
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
@@ -28837,11 +28977,39 @@ snapshots:
|
||||
optionalDependencies:
|
||||
canvas: 3.2.3
|
||||
transitivePeerDependencies:
|
||||
- '@exodus/crypto'
|
||||
- '@noble/hashes'
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3):
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 5.1.11
|
||||
'@asamuzakjp/dom-selector': 7.1.1
|
||||
'@bramus/specificity': 2.4.2
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
||||
css-tree: 3.2.1
|
||||
data-urls: 7.0.0(@noble/hashes@2.0.1)
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1)
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.3.5
|
||||
parse5: 8.0.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 6.0.1
|
||||
undici: 7.25.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1(@noble/hashes@2.0.1)
|
||||
xml-name-validator: 5.0.0
|
||||
optionalDependencies:
|
||||
canvas: 3.2.3
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
@@ -32460,7 +32628,7 @@ snapshots:
|
||||
psl: 1.15.0
|
||||
punycode: 2.3.1
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
tough-cookie@6.0.1:
|
||||
dependencies:
|
||||
tldts: 7.0.19
|
||||
|
||||
@@ -32675,6 +32843,8 @@ snapshots:
|
||||
|
||||
undici@6.24.1: {}
|
||||
|
||||
undici@7.25.0: {}
|
||||
|
||||
undici@8.1.0: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
@@ -33204,7 +33374,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vieval@0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
vieval@0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@moeru/std': 0.1.0-beta.17
|
||||
'@pnpm/find-workspace-dir': 1000.1.5
|
||||
@@ -33219,7 +33389,7 @@ snapshots:
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@edge-runtime/vm'
|
||||
- '@opentelemetry/api'
|
||||
@@ -33591,10 +33761,10 @@ snapshots:
|
||||
vitest-browser-vue@2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/test-utils': 2.4.6
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.4
|
||||
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -33621,11 +33791,11 @@ snapshots:
|
||||
'@types/node': 24.12.2
|
||||
'@vitest/browser-playwright': 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
'@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
|
||||
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
||||
jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.4
|
||||
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -33652,7 +33822,7 @@ snapshots:
|
||||
'@types/node': 25.6.0
|
||||
'@vitest/browser-playwright': 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
'@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
|
||||
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
|
||||
jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
@@ -33972,6 +34142,14 @@ snapshots:
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
|
||||
whatwg-url@16.0.1(@noble/hashes@2.0.1):
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
|
||||
@@ -73,6 +73,7 @@ catalog:
|
||||
'@types/unist': ^3.0.3
|
||||
'@types/whatwg-mimetype': ^5.0.0
|
||||
'@types/ws': ^8.18.1
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': 14.1.0
|
||||
'@webgpu/types': ^0.1.69
|
||||
'@xsai-ext/providers': 0.5.0-beta.2
|
||||
@@ -112,6 +113,7 @@ catalog:
|
||||
is-network-error: ^1.3.1
|
||||
isolated-vm: ^6.1.2
|
||||
jose: ^6.2.2
|
||||
jsdom: ^29.0.2
|
||||
knip: ^6.4.1
|
||||
meow: ^14.1.0
|
||||
mkcert: ^3.2.0
|
||||
@@ -140,6 +142,7 @@ catalog:
|
||||
vite: ^8.0.8
|
||||
vite-plugin-inspect: 12.0.0-beta.1
|
||||
vite-plugin-mkcert: ^2.0.0
|
||||
vitest: ^4.1.4
|
||||
vitest-browser-vue: ^2.1.0
|
||||
vue: ^3.5.32
|
||||
vue-router: ^5.0.4
|
||||
|
||||
Reference in New Issue
Block a user