feat(stage-web): added more settings

This commit is contained in:
Neko Ayaka
2025-03-11 11:05:29 +08:00
parent a65d16a154
commit 8a4886ad97
22 changed files with 1387 additions and 411 deletions
+1
View File
@@ -50,6 +50,7 @@
"@vueuse/shared": "^12.8.2",
"@xsai-ext/providers-cloud": "catalog:",
"@xsai-ext/providers-local": "catalog:",
"@xsai-ext/shared-providers": "catalog:",
"@xsai/generate-speech": "catalog:",
"@xsai/generate-text": "catalog:",
"@xsai/model": "catalog:",
@@ -1,8 +1,10 @@
<script setup lang="ts">
import type { ChatProvider } from '@xsai-ext/shared-providers'
import { BasicTextarea, TransitionVertical } from '@proj-airi/stage-ui/components'
import { useMicVAD, useWhisper } from '@proj-airi/stage-ui/composables'
import WhisperWorker from '@proj-airi/stage-ui/libs/workers/worker?worker&url'
import { useAudioContext, useChatStore, useSettings } from '@proj-airi/stage-ui/stores'
import { useAudioContext, useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
import { useDevicesList } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
@@ -16,6 +18,9 @@ const listening = ref(false)
const tab = ref<'chat' | 'custom' | 'clothes'>('chat')
const showMicrophoneSelect = ref(false)
const providersStore = useProvidersStore()
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
const { send, onAfterSend } = useChatStore()
@@ -29,7 +34,7 @@ const { transcribe: generate, load: loadWhisper, status: whisperStatus, terminat
return
}
await send(res)
await send(res, { chatProvider: providersStore.getProviderInstance(activeProvider.value) as ChatProvider, model: activeModel.value })
},
})
@@ -39,7 +44,7 @@ async function handleSend() {
}
try {
await send(messageInput.value)
await send(messageInput.value, { chatProvider: providersStore.getProviderInstance(activeProvider.value) as ChatProvider, model: activeModel.value })
}
catch (error) {
messages.value.pop()
@@ -1,26 +1,22 @@
<script setup lang="ts">
import { RadioCardDetail, RadioCardSimple } from '@proj-airi/stage-ui/components'
import { RadioCardDetailManySelect, RadioCardSimple } from '@proj-airi/stage-ui/components'
import { useConsciousnessStore, useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { onMounted } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
const isModelListExpanded = ref(false)
const providersStore = useProvidersStore()
const consciousnessStore = useConsciousnessStore()
const { availableProviders } = storeToRefs(providersStore)
const { availableProviders, availableProvidersMetadata } = storeToRefs(providersStore)
const {
activeProvider,
activeModel,
customModelName,
modelSearchQuery,
availableProvidersMetadata,
supportsModelListing,
providerModels,
isLoadingActiveProviderModels,
activeProviderModelError,
filteredModels,
} = storeToRefs(consciousnessStore)
const router = useRouter()
@@ -28,6 +24,10 @@ const router = useRouter()
onMounted(async () => {
await consciousnessStore.loadModelsForProvider(activeProvider.value)
})
function updateCustomModelName(value: string) {
customModelName.value = value
}
</script>
<template>
@@ -44,11 +44,11 @@ onMounted(async () => {
</div>
</h1>
</div>
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
<div>
<div flex="~ col gap-4">
<div>
<h2 class="text-lg md:text-2xl">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-500">
Provider
</h2>
<div text="neutral-400 dark:neutral-400">
@@ -62,19 +62,30 @@ onMounted(async () => {
See also: https://stackoverflow.com/a/33737340
-->
<fieldset
v-if="availableProviders.length > 0" flex="~ row gap-4" :style="{ 'scrollbar-width': 'none' }"
min-w-0 of-x-scroll scroll-smooth role="radiogroup"
v-if="availableProviders.length > 0"
flex="~ row gap-4"
:style="{ 'scrollbar-width': 'none' }"
min-w-0 of-x-scroll scroll-smooth
role="radiogroup"
>
<RadioCardSimple
v-for="metadata in availableProvidersMetadata" :id="metadata.id" :key="metadata.id"
v-model="activeProvider" name="provider" :value="metadata.id"
:title="metadata.localizedName" :description="metadata.localizedDescription"
v-for="metadata in availableProvidersMetadata"
:id="metadata.id"
:key="metadata.id"
v-model="activeProvider"
name="provider"
:value="metadata.id"
:title="metadata.localizedName"
:description="metadata.localizedDescription"
/>
</fieldset>
<div v-else>
<RouterLink
class="flex items-center gap-3 rounded-lg p-4" border="2 dashed neutral-200 dark:neutral-800"
bg="neutral-50 dark:neutral-800" transition="colors duration-200 ease-in-out" to="/settings/providers"
class="flex items-center gap-3 rounded-lg p-4"
border="2 dashed neutral-200 dark:neutral-800"
bg="neutral-50 dark:neutral-800"
transition="colors duration-200 ease-in-out"
to="/settings/providers"
>
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
@@ -135,114 +146,22 @@ onMounted(async () => {
</div>
</div>
<!-- Search bar (only show if we have models) -->
<!-- Using the new RadioCardDetailManySelect component -->
<template v-else-if="providerModels.length > 0">
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
</div>
<input
v-model="modelSearchQuery" type="search" class="w-full rounded-lg p-2.5 pl-10 text-sm outline-none"
border="focus:primary-500 dark:focus:primary-400 ~ neutral-300 dark:neutral-700 2"
transition="all duration-200 ease-in-out"
ring="focus:primary-500 dark:focus:primary-400 0 focus:2 focus:offset-0 focus:opacity-50"
bg="white dark:neutral-900"
:placeholder="$t('settings.modules.consciousness.provider-model-selection.search_placeholder')"
>
<button
v-if="modelSearchQuery" type="button" class="absolute inset-y-0 right-0 flex items-center pr-3"
@click="modelSearchQuery = ''"
>
<div
i-solar:close-circle-line-duotone
class="text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
/>
</button>
</div>
<!-- Models list with search results info -->
<div class="space-y-2">
<!-- Search results info -->
<div v-if="modelSearchQuery" class="text-sm text-neutral-500 dark:text-neutral-400">
{{ $t('settings.modules.consciousness.provider-model-selection.search_results', {
count: filteredModels.length,
total: providerModels.length,
}) }}
</div>
<!-- No search results -->
<div
v-if="modelSearchQuery && filteredModels.length === 0"
class="flex items-center gap-3 border border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">{{
$t('settings.modules.consciousness.provider-model-selection.no_search_results') }}</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
{{ $t('settings.modules.consciousness.provider-model-selection.no_search_results_description', {
query: modelSearchQuery }) }}
</span>
</div>
</div>
<!-- Models grid -->
<div class="relative">
<!-- Horizontally scrollable container -->
<div
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col gap-4 overflow-x-auto pb-4"
:class="[
isModelListExpanded ? 'md:grid-cols-2 md:grid-flow-row md:auto-cols-auto' : '',
]"
transition="all duration-200 ease-in-out"
style="scroll-snap-type: x mandatory;"
>
<RadioCardDetail
v-for="model in filteredModels"
:id="model.id"
:key="model.id"
v-model="activeModel"
:value="model.id"
:title="model.name"
:description="model.description"
:deprecated="model.deprecated"
:show-expand-collapse="true"
:expand-collapse-threshold="100"
:show-custom-input="model.id === 'custom'"
:custom-input-value="customModelName"
:custom-input-placeholder="$t('settings.modules.consciousness.provider-model-selection.custom_model_placeholder')"
name="model"
class="scroll-snap-align-start"
@update:custom-input-value="customModelName = $event"
/>
</div>
<!-- Expand/collapse handle -->
<div
bg="neutral-100 dark:[rgba(0,0,0,0.3)]"
rounded-xl
:class="[
isModelListExpanded ? 'fixed bottom-4 left-1/2 translate-x--1/2 z-10 w-[calc(100%-16px-40px-16px)]' : 'mt-0 w-full rounded-lg',
]"
>
<button
w-full
flex items-center justify-center gap-2 rounded-lg py-2 transition="all duration-200 ease-in-out"
:class="[
isModelListExpanded ? 'bg-primary-500 hover:bg-primary-600 text-white' : 'bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700',
]"
@click="isModelListExpanded = !isModelListExpanded"
>
<span>{{ isModelListExpanded ? $t('settings.modules.consciousness.provider-model-selection.collapse') : $t('settings.modules.consciousness.provider-model-selection.expand') }}</span>
<div
:class="isModelListExpanded ? 'rotate-180' : ''"
i-solar:alt-arrow-down-bold-duotone transition="transform duration-200 ease-in-out"
class="text-lg"
/>
</button>
</div>
</div>
</div>
<RadioCardDetailManySelect
v-model="activeModel"
v-model:search-query="modelSearchQuery"
:items="providerModels"
:searchable="true"
:search-placeholder="$t('settings.modules.consciousness.provider-model-selection.search_placeholder')"
:search-no-results-title="$t('settings.modules.consciousness.provider-model-selection.no_search_results')"
:search-no-results-description="$t('settings.modules.consciousness.provider-model-selection.no_search_results_description', { query: modelSearchQuery })"
:search-results-text="$t('settings.modules.consciousness.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
:custom-input-placeholder="$t('settings.modules.consciousness.provider-model-selection.custom_model_placeholder')"
:expand-button-text="$t('settings.modules.consciousness.provider-model-selection.expand')"
:collapse-button-text="$t('settings.modules.consciousness.provider-model-selection.collapse')"
@update:custom-value="updateCustomModelName"
/>
</template>
</div>
</div>
@@ -251,10 +170,10 @@ onMounted(async () => {
<div v-else-if="activeProvider && !supportsModelListing">
<div flex="~ col gap-4">
<div>
<h2 class="text-lg md:text-2xl">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ $t('settings.modules.consciousness.provider-model-selection.title') }}
</h2>
<div text="neutral-400 dark:neutral-400">
<div text="neutral-400 dark:neutral-500">
<span>{{ $t('settings.modules.consciousness.provider-model-selection.subtitle') }}</span>
</div>
</div>
@@ -2,7 +2,6 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import FactorioIcon from '../../../assets/icons/modules/games/factorio.png'
import IconStatusItem from '../../../components/Menu/IconStatusItem.vue'
const router = useRouter()
@@ -28,70 +27,70 @@ const modulesList = computed<Module[]>(() => [
to: '/settings/modules/consciousness',
configured: false,
},
{
id: 'hearing',
name: 'Hearing',
description: 'Hearing, speech recognition, etc.',
icon: 'i-lucide:ear',
to: '',
configured: false,
},
{
id: 'messaging-discord',
name: 'Discord',
description: 'Messaging, notifications, etc.',
icon: 'i-simple-icons:discord',
to: '',
configured: false,
},
// {
// id: 'hearing',
// name: 'Hearing',
// description: 'Hearing, speech recognition, etc.',
// icon: 'i-lucide:ear',
// to: '',
// configured: false,
// },
// {
// id: 'messaging-discord',
// name: 'Discord',
// description: 'Messaging, notifications, etc.',
// icon: 'i-simple-icons:discord',
// to: '',
// configured: false,
// },
{
id: 'speech',
name: 'Speech',
description: 'Speech synthesis, etc.',
icon: 'i-lucide:mic',
to: '',
configured: false,
},
{
id: 'memory-short-term',
name: 'Short-Term Memory',
description: 'Short-term memory, etc.',
icon: 'i-lucide:book',
to: '',
configured: false,
},
{
id: 'memory-long-term',
name: 'Long-Term Memory',
description: 'Long-term memory, etc.',
icon: 'i-lucide:book-copy',
to: '',
configured: false,
},
{
id: 'vision',
name: 'Vision',
description: 'Vision, etc.',
icon: 'i-lucide:eye',
to: '',
configured: false,
},
{
id: 'game-minecraft',
name: 'Minecraft',
description: 'Playing Minecraft with you, etc.',
iconColor: 'i-vscode-icons:file-type-minecraft',
to: '',
configured: false,
},
{
id: 'game-factorio',
name: 'Factorio',
description: 'Playing Factorio with you, etc.',
iconImage: FactorioIcon,
to: '',
to: '/settings/modules/speech',
configured: false,
},
// {
// id: 'memory-short-term',
// name: 'Short-Term Memory',
// description: 'Short-term memory, etc.',
// icon: 'i-lucide:book',
// to: '',
// configured: false,
// },
// {
// id: 'memory-long-term',
// name: 'Long-Term Memory',
// description: 'Long-term memory, etc.',
// icon: 'i-lucide:book-copy',
// to: '',
// configured: false,
// },
// {
// id: 'vision',
// name: 'Vision',
// description: 'Vision, etc.',
// icon: 'i-lucide:eye',
// to: '',
// configured: false,
// },
// {
// id: 'game-minecraft',
// name: 'Minecraft',
// description: 'Playing Minecraft with you, etc.',
// iconColor: 'i-vscode-icons:file-type-minecraft',
// to: '',
// configured: false,
// },
// {
// id: 'game-factorio',
// name: 'Factorio',
// description: 'Playing Factorio with you, etc.',
// iconImage: FactorioIcon,
// to: '',
// configured: false,
// },
])
</script>
@@ -0,0 +1,273 @@
<script setup lang="ts">
import { RadioCardSimple } from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { availableProviders, availableProvidersMetadata } = storeToRefs(providersStore)
const {
activeSpeechProvider,
activeSpeechModel,
voiceName,
pitch,
rate,
isLoadingSpeechProviderVoices,
speechProviderError,
supportsSSML,
ssmlEnabled,
} = storeToRefs(speechStore)
const router = useRouter()
const ssmlExample = ref(`<speak>
Hello, my name is <voice name="${voiceName.value || 'Default'}">
<prosody pitch="+${pitch.value || 0}%" rate="${rate.value || 1}">
AI Assistant
</prosody>
</voice>
</speak>`)
onMounted(async () => {
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
})
function updateVoiceName(value: string) {
voiceName.value = value
updateSSMLExample()
}
function updatePitch(value: number) {
pitch.value = value
updateSSMLExample()
}
function updateRate(value: number) {
rate.value = value
updateSSMLExample()
}
function updateSSMLExample() {
ssmlExample.value = `<speak>
Hello, my name is <voice name="${voiceName.value || 'Default'}">
<prosody pitch="+${pitch.value || 0}%" rate="${rate.value || 1}">
AI Assistant
</prosody>
</voice>
</speak>`
}
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Modules</span>
</div>
<div text-3xl font-semibold>
Speech
</div>
</h1>
</div>
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
<div>
<div flex="~ col gap-4">
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Provider
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Select the suitable speech provider</span>
</div>
</div>
<div max-w-full>
<fieldset
v-if="availableProviders.length > 0"
flex="~ row gap-4"
:style="{ 'scrollbar-width': 'none' }"
min-w-0 of-x-scroll scroll-smooth
role="radiogroup"
>
<RadioCardSimple
v-for="metadata in availableProvidersMetadata"
:id="metadata.id"
:key="metadata.id"
v-model="activeSpeechProvider"
name="speech-provider"
:value="metadata.id"
:title="metadata.localizedName"
:description="metadata.localizedDescription"
/>
</fieldset>
<div v-else>
<RouterLink
class="flex items-center gap-3 rounded-lg p-4"
border="2 dashed neutral-200 dark:neutral-800"
bg="neutral-50 dark:neutral-800"
transition="colors duration-200 ease-in-out" to="/settings/providers"
>
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">No Speech Providers Configured</span>
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your speech
providers</span>
</div>
<div i-solar:arrow-right-line-duotone class="ml-auto text-xl text-neutral-400 dark:text-neutral-500" />
</RouterLink>
</div>
</div>
</div>
</div>
<!-- Voice Configuration Section -->
<div v-if="activeSpeechProvider">
<div flex="~ col gap-4">
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Configuration
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Customize how your AI assistant speaks</span>
</div>
</div>
<!-- Loading state -->
<div v-if="isLoadingSpeechProviderVoices" class="flex items-center justify-center py-4">
<div class="mr-2 animate-spin">
<div i-solar:spinner-line-duotone text-xl />
</div>
<span>Loading available voices...</span>
</div>
<!-- Error state -->
<div
v-else-if="speechProviderError"
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
>
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
<div class="flex flex-col">
<span class="font-medium">Error loading voices</span>
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
</div>
</div>
<!-- Voice configuration form -->
<div v-else class="space-y-6">
<!-- Voice selection -->
<div>
<label class="mb-1 block text-sm font-medium">
Voice Name
</label>
<input
v-model="voiceName" type="text"
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
placeholder="Enter voice name (e.g., 'Rachel', 'Josh')"
@input="(event) => updateVoiceName((event.target as HTMLInputElement).value)"
>
<p class="mt-1 text-xs text-neutral-500">
For ElevenLabs, enter the exact voice name from your account
</p>
</div>
<!-- Model selection for ElevenLabs -->
<div v-if="activeSpeechProvider === 'elevenlabs'">
<label class="mb-1 block text-sm font-medium">
Model
</label>
<select
v-model="activeSpeechModel"
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
>
<option value="eleven_monolingual_v1">
Monolingual v1
</option>
<option value="eleven_multilingual_v1">
Multilingual v1
</option>
<option value="eleven_multilingual_v2">
Multilingual v2
</option>
</select>
</div>
<!-- Voice parameters -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="mb-1 block text-sm font-medium">
Pitch Adjustment (%)
</label>
<div class="flex items-center gap-3">
<input
v-model="pitch" type="range" min="-50" max="50" step="5"
class="w-full"
@input="(event) => updatePitch(parseInt((event.target as HTMLInputElement).value))"
>
<span class="w-12 text-center">{{ pitch }}%</span>
</div>
</div>
<div>
<label class="mb-1 block text-sm font-medium">
Speech Rate
</label>
<div class="flex items-center gap-3">
<input
v-model="rate" type="range" min="0.5" max="2" step="0.1"
class="w-full"
@input="(event) => updateRate(parseFloat((event.target as HTMLInputElement).value))"
>
<span class="w-12 text-center">{{ rate }}x</span>
</div>
</div>
</div>
<!-- SSML Support -->
<div v-if="supportsSSML" class="border border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
<div class="mb-3 flex items-center justify-between">
<label class="font-medium">SSML Support</label>
<div class="relative mr-2 inline-block w-10 select-none align-middle">
<input
id="ssml-toggle"
v-model="ssmlEnabled"
type="checkbox"
class="sr-only"
>
<label
for="ssml-toggle"
class="block h-6 cursor-pointer overflow-hidden rounded-full bg-neutral-300 dark:bg-neutral-700"
>
<span
:class="{ 'translate-x-4': ssmlEnabled, 'translate-x-0': !ssmlEnabled }"
class="block h-6 w-6 transform rounded-full bg-white shadow transition-transform duration-200 ease-in-out"
/>
</label>
</div>
</div>
<p class="mb-3 text-sm text-neutral-500">
Enable Speech Synthesis Markup Language for more control over speech output
</p>
<div v-if="ssmlEnabled" class="mt-3">
<label class="mb-1 block text-sm font-medium">
SSML Example
</label>
<pre class="overflow-auto rounded bg-neutral-50 p-3 text-xs dark:bg-neutral-800">{{ ssmlExample }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div fixed bottom-0 right-0 z--1 class="text-neutral-100/80 dark:text-neutral-500/20">
<div text="40" i-lucide:volume-2 translate-x-10 translate-y-10 />
</div>
</template>
<route lang="yaml">
meta:
stageTransition:
name: slide
</route>
@@ -0,0 +1,272 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { providers } = storeToRefs(providersStore)
// Get provider metadata
const providerId = 'elevenlabs'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const apiKey = ref(providers.value[providerId]?.apiKey || '')
const baseUrl = ref(providers.value[providerId]?.baseUrl || '')
// Speech settings
const selectedLanguage = ref(speechStore.selectedLanguage)
const selectedVoice = ref(speechStore.voiceName)
const availableVoices = computed(() => speechStore.availableVoicesForLanguage)
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.baseUrlDefault || ''
// Load voices if provider is configured
if (providersStore.configuredProviders[providerId]) {
speechStore.loadVoicesForProvider(providerId)
}
})
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
apiKey: apiKey.value,
baseUrl: baseUrl.value || providerMetadata.value?.baseUrlDefault || '',
}
})
watch(selectedLanguage, (newLanguage) => {
speechStore.setLanguage(newLanguage)
})
watch(selectedVoice, (newVoice) => {
speechStore.setVoiceName(newVoice)
})
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName }}
</div>
</h1>
</div>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<div>
<div flex="~ col gap-6">
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<div max-w-full>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="..."
>
</label>
</div>
</div>
</div>
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Settings
</h2>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Language
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Select voice language
</div>
</div>
<select
v-model="selectedLanguage"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Voice
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Select preferred voice
</div>
</div>
<select
v-model="selectedVoice"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Pitch
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Adjust voice pitch
</div>
</div>
<div flex="~ row" items-center gap-2>
<input
v-model="speechStore.pitch"
type="range"
min="-100"
max="100"
step="1"
w-full
>
<span class="text-xs">{{ speechStore.pitch }}</span>
</div>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Rate
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Adjust speaking rate
</div>
</div>
<div flex="~ row" items-center gap-2>
<input
v-model="speechStore.rate"
type="range"
min="0.5"
max="2"
step="0.1"
w-full
>
<span class="text-xs">{{ speechStore.rate.toFixed(1) }}</span>
</div>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
SSML
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Enable SSML support
</div>
</div>
<div>
<input
v-model="speechStore.ssmlEnabled"
type="checkbox"
class="mr-2"
:disabled="!speechStore.supportsSSML"
>
<span class="text-sm">{{ speechStore.ssmlEnabled ? 'Enabled' : 'Disabled' }}</span>
</div>
</label>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
:placeholder="providerMetadata?.baseUrlDefault"
>
</label>
<div mt-4>
<button
border="zinc-300 dark:zinc-800 solid 1"
transition="border duration-250 ease-in-out"
rounded
px-4 py-2 text-sm @click="speechStore.resetVoiceSettings"
>
Reset Voice Settings
</button>
</div>
</div>
</Collapsable>
</div>
</div>
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
<div text="40" :class="providerMetadata?.icon" translate-x-10 translate-y-10 />
</div>
</template>
@@ -46,69 +46,78 @@ watch([apiKey, baseUrl], () => {
</div>
</h1>
</div>
<form flex="~ col gap-4" @submit.prevent="() => {}">
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
<div>
<h2 text-2xl>
<span>Basic</span>
</h2>
</div>
<div>
<label grid="~ cols-2 gap-4">
<div flex="~ col gap-6">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for OpenAI
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="sk-..."
>
</label>
</div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 text-2xl>
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
<div max-w-full>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for OpenAI
</div>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="https://api.openai.com/v1/"
>
</label>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="sk-..."
>
</label>
</div>
</div>
</Collapsable>
</form>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="https://api.openai.com/v1/"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lobe-icons:openai translate-x-10 translate-y-10 />
</div>
@@ -50,69 +50,78 @@ watch([apiKey, baseUrl], () => {
</div>
</h1>
</div>
<form flex="~ col gap-4" @submit.prevent="() => {}">
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
<div>
<h2 text-2xl>
<span>Basic</span>
</h2>
</div>
<div>
<label grid="~ cols-2 gap-4">
<div flex="~ col gap-6">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="sk-or-..."
>
</label>
</div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 text-2xl>
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
<div max-w-full>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
:placeholder="providerMetadata?.baseUrlDefault"
>
</label>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="sk-or-..."
>
</label>
</div>
</div>
</Collapsable>
</form>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
:placeholder="providerMetadata?.baseUrlDefault"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
<div text="40" :class="providerMetadata?.icon" translate-x-10 translate-y-10 />
</div>
+2
View File
@@ -81,6 +81,7 @@ words:
- lobehub
- logg
- lucide
- magnifer
- Maru
- matchall
- micvad
@@ -160,5 +161,6 @@ words:
- xsai
- xsschema
- zhipu
- ssml
ignoreWords: []
import: []
@@ -48,11 +48,12 @@ function updateCustomInput(event: Event) {
<template>
<label
:key="id"
class="relative flex cursor-pointer items-start rounded-lg p-3 pr-[20px] transition-all duration-200 ease-in-out"
class="relative flex cursor-pointer items-start rounded-xl p-3 pr-[20px]"
transition="all duration-200 ease-in-out"
border="2 solid"
:class="[
modelValue === value
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400'
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-100 dark:border-primary-900'
: 'bg-white dark:bg-neutral-900/20 border-neutral-200 dark:border-neutral-700 hover:border-primary-500/30 dark:hover:border-primary-400/30',
deprecated ? 'opacity-60' : '',
]"
@@ -17,14 +17,15 @@ defineEmits<{
<label
:key="id"
border="2px solid"
class="relative transition-all duration-200 ease-in-out"
class="relative"
transition="all duration-200 ease-in-out"
:class="[
modelValue === value
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400'
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-100 dark:border-primary-900'
: 'bg-white dark:bg-neutral-900/20 border-neutral-200 dark:border-neutral-700 hover:border-primary-500/30 dark:hover:border-primary-400/30',
]"
flex="~ col"
block min-w-50 w-fit cursor-pointer items-start rounded-lg p-4 text-left
block min-w-50 w-fit cursor-pointer items-start rounded-xl p-4 text-left
>
<input
:checked="modelValue === value"
@@ -0,0 +1,178 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import RadioCardDetail from './Radio/RadioCardDetail.vue'
interface Item {
id: string
name: string
description?: string
deprecated?: boolean
customizable?: boolean
}
interface Props {
items: Item[]
modelValue: string
searchable?: boolean
searchPlaceholder?: string
searchNoResultsTitle?: string
searchNoResultsDescription?: string
searchResultsText?: string
customInputPlaceholder?: string
expandButtonText?: string
collapseButtonText?: string
}
const props = withDefaults(defineProps<Props>(), {
searchable: true,
searchPlaceholder: 'Search...',
searchNoResultsTitle: 'No results found',
searchNoResultsDescription: 'Try a different search term',
searchResultsText: '{count} of {total} results',
customInputPlaceholder: 'Enter custom value',
expandButtonText: 'Show more',
collapseButtonText: 'Show less',
})
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:customValue': [value: string]
}>()
const searchQuery = ref('')
const isListExpanded = ref(false)
const customValue = ref('')
const filteredItems = computed(() => {
if (!searchQuery.value)
return props.items
const query = searchQuery.value.toLowerCase()
return props.items.filter(item =>
item.name.toLowerCase().includes(query)
|| (item.description && item.description.toLowerCase().includes(query)),
)
})
function clearSearch() {
searchQuery.value = ''
}
function updateCustomValue(value: string) {
customValue.value = value
emit('update:customValue', value)
}
</script>
<template>
<div class="radio-card-detail-many-select">
<!-- Search bar -->
<div v-if="searchable" class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
</div>
<input
v-model="searchQuery"
type="search"
class="w-full rounded-xl p-2.5 pl-10 text-sm outline-none"
border="focus:primary-100 dark:focus:primary-900 ~ neutral-200 dark:neutral-800 2"
transition="all duration-200 ease-in-out"
ring="focus:primary-500 dark:focus:primary-400 0 focus:2 focus:offset-0 focus:opacity-50"
bg="white dark:neutral-900"
:placeholder="searchPlaceholder"
>
<button
v-if="searchQuery"
type="button"
class="absolute inset-y-0 right-0 flex items-center pr-3"
@click="clearSearch"
>
<div
i-solar:close-circle-line-duotone
class="text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
/>
</button>
</div>
<!-- Items list with search results info -->
<div class="mt-4 space-y-2">
<!-- Search results info -->
<div v-if="searchQuery" class="text-sm text-neutral-500 dark:text-neutral-400">
{{ searchResultsText.replace('{count}', filteredItems.length.toString()).replace('{total}', items.length.toString()) }}
</div>
<!-- No search results -->
<div
v-if="searchQuery && filteredItems.length === 0"
class="flex items-center gap-3 border border-amber-200 rounded-xl bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">{{ searchNoResultsTitle }}</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
{{ searchNoResultsDescription.replace('{query}', searchQuery) }}
</span>
</div>
</div>
<!-- Items grid -->
<div class="relative">
<!-- Horizontally scrollable container -->
<div
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col gap-4 overflow-x-auto pb-4"
:class="[
isListExpanded ? 'md:grid-cols-2 md:grid-flow-row md:auto-cols-auto' : '',
]"
transition="all duration-200 ease-in-out"
style="scroll-snap-type: x mandatory;"
>
<RadioCardDetail
v-for="item in filteredItems"
:id="item.id"
:key="item.id"
:value="item.id"
:title="item.name"
:description="item.description"
:deprecated="item.deprecated"
:show-expand-collapse="true"
:expand-collapse-threshold="100"
:show-custom-input="item.customizable"
:custom-input-value="customValue"
:custom-input-placeholder="customInputPlaceholder"
:model-value="modelValue"
name="radio-card-detail-many-select"
class="scroll-snap-align-start"
@update:model-value="emit('update:modelValue', $event)"
@update:custom-input-value="updateCustomValue($event)"
/>
</div>
<!-- Expand/collapse handle -->
<div
bg="neutral-100 dark:[rgba(0,0,0,0.3)]"
rounded-xl
:class="[
isListExpanded ? 'fixed bottom-4 left-1/2 translate-x--1/2 z-10 w-[calc(100%-16px-40px-16px)]' : 'mt-0 w-full rounded-lg',
]"
>
<button
w-full
flex items-center justify-center gap-2 rounded-lg py-2 transition="all duration-200 ease-in-out"
:class="[
isListExpanded ? 'bg-primary-500 hover:bg-primary-600 text-white' : 'bg-white dark:bg-neutral-900 hover:bg-neutral-100 dark:hover:bg-neutral-800',
]"
@click="isListExpanded = !isListExpanded"
>
<span>{{ isListExpanded ? collapseButtonText : expandButtonText }}</span>
<div
:class="isListExpanded ? 'rotate-180' : ''"
i-solar:alt-arrow-down-bold-duotone transition="transform duration-200 ease-in-out"
class="text-lg"
/>
</button>
</div>
</div>
</div>
</div>
</template>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { ElectronAPI } from '@electron-toolkit/preload'
import type { DuckDBWasmDrizzleDatabase } from '@proj-airi/drizzle-duckdb-wasm'
import type { SpeechProvider } from '@xsai-ext/shared-providers'
import type { Emotion } from '../../constants/emotions'
import { drizzle } from '@proj-airi/drizzle-duckdb-wasm'
import { createUnElevenLabs } from '@xsai-ext/providers-local'
// import { createTransformers } from '@proj-airi/provider-transformers'
// import embedWorkerURL from '@proj-airi/provider-transformers/worker?worker&url'
// import { embed } from '@xsai/embed'
@@ -12,20 +12,19 @@ import { generateSpeech } from '@xsai/generate-speech'
import { sql } from 'drizzle-orm'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMarkdown } from '../../composables/markdown'
import { useQueue } from '../../composables/queue'
import { useDelayMessageQueue, useEmotionsMessageQueue, useMessageContentQueue } from '../../composables/queues'
import { llmInferenceEndToken } from '../../constants'
import { Voice, voiceMap } from '../../constants/elevenlabs'
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionAngryMotionName, EmotionHappyMotionName, EmotionThinkMotionName } from '../../constants/emotions'
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
import { useChatStore } from '../../stores/chat'
import { useSpeechStore } from '../../stores/modules/speech'
import { useProvidersStore } from '../../stores/providers'
import { useSettings } from '../../stores/settings'
import Live2DScene from '../Scenes/Live2D.vue'
import VRMScene from '../Scenes/VRM.vue'
import '../../utils/live2d-zip-loader'
withDefaults(defineProps<{ paused?: boolean }>(), { paused: false })
@@ -35,12 +34,12 @@ const db = ref<DuckDBWasmDrizzleDatabase>()
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
const { stageView, elevenLabsApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(useSettings())
const { stageView } = storeToRefs(useSettings())
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
const { audioContext, calculateVolume } = useAudioContext()
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd, streamingMessage, onAssistantResponseEnd } = useChatStore()
const { process } = useMarkdown()
const { locale } = useI18n()
const providersStore = useProvidersStore()
const audioAnalyser = ref<AnalyserNode>()
const nowSpeaking = ref(false)
@@ -71,37 +70,47 @@ const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
],
})
const speechStore = useSpeechStore()
async function handleSpeechGeneration(ctx: { data: string }) {
try {
if (!speechStore.activeSpeechProvider) {
console.warn('No active speech provider configured')
return
}
const provider = providersStore.getProviderInstance(speechStore.activeSpeechProvider) as SpeechProvider
if (!provider) {
console.error('Failed to initialize speech provider')
return
}
const res = await generateSpeech({
...provider.speech(speechStore.activeSpeechModel),
input: ctx.data,
voice: speechStore.voiceId,
// Optional: Add SSML wrapping if enabled
...(speechStore.ssmlEnabled && {
input: speechStore.generateSSML(ctx.data),
}),
voiceSettings: {
stability: 0.4,
similarityBoost: 0.5,
},
})
// Decode the ArrayBuffer into an AudioBuffer
const audioBuffer = await audioContext.decodeAudioData(res)
await audioQueue.add({ audioBuffer, text: ctx.data })
}
catch (error) {
console.error('Speech generation failed:', error)
}
}
const ttsQueue = useQueue<string>({
handlers: [
async (ctx) => {
let voice = Voice.Camilla_KM
if (locale.value === 'jp' || locale.value === 'jp-JP')
voice = elevenlabsVoiceJapanese.value
else
voice = elevenlabsVoiceEnglish.value
const now = Date.now()
const elevenlabs = createUnElevenLabs(elevenLabsApiKey.value, 'https://unspeech.hyp3r.link/v1/')
const res = await generateSpeech({
...elevenlabs.speech('eleven_multilingual_v2', {
voiceSettings: {
stability: 0.4,
similarityBoost: 0.5,
},
}),
input: ctx.data,
voice: voiceMap[voice],
})
const elapsed = Date.now() - now
// eslint-disable-next-line no-console
console.debug('TTS took', elapsed, 'ms')
// Decode the ArrayBuffer into an AudioBuffer
const audioBuffer = await audioContext.decodeAudioData(res)
await audioQueue.add({ audioBuffer, text: ctx.data })
},
handleSpeechGeneration,
],
})
@@ -2,6 +2,7 @@ export { default as BasicTextarea } from './BasicTextarea.vue'
export { default as Collapsable } from './Collapsable.vue'
export { default as RadioCardDetail } from './Form/Radio/RadioCardDetail.vue'
export { default as RadioCardSimple } from './Form/Radio/RadioCardSimple.vue'
export { default as RadioCardDetailManySelect } from './Form/RadioCardDetailManySelect.vue'
export { default as Live2DCanvas } from './Live2D/Canvas.vue'
export { default as Live2DModel } from './Live2D/Model.vue'
export { default as SceneLive2D } from './Scenes/Live2D.vue'
+4 -15
View File
@@ -1,13 +1,13 @@
import type { ChatProvider } from '@xsai-ext/shared-providers'
import type { AssistantMessage, Message } from '@xsai/shared-chat'
import { defineStore, storeToRefs } from 'pinia'
import { defineStore } from 'pinia'
import { ref, toRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLlmmarkerParser } from '../composables/llmmarkerParser'
import SystemPromptV2 from '../constants/prompts/system-v2'
import { useLLM } from '../stores/llm'
import { useProvidersStore } from '../stores/providers'
import { asyncIteratorFromReadableStream } from '../utils/iterator'
export interface ErrorMessage {
@@ -18,7 +18,6 @@ export interface ErrorMessage {
export const useChatStore = defineStore('chat', () => {
const { stream } = useLLM()
const { t } = useI18n()
const { providers: providerValues } = storeToRefs(useProvidersStore())
const sending = ref(false)
@@ -72,11 +71,7 @@ export const useChatStore = defineStore('chat', () => {
const streamingMessage = ref<AssistantMessage>({ role: 'assistant', content: '' })
async function send(sendingMessage: string, options?: {
baseUrl?: string
apiKey?: string
model?: { id: string }
}) {
async function send(sendingMessage: string, options: { model: string, chatProvider: ChatProvider }) {
try {
sending.value = true
@@ -87,12 +82,6 @@ export const useChatStore = defineStore('chat', () => {
await hook(sendingMessage)
}
const {
baseUrl = providerValues.value['openrouter-ai']?.baseUrl as string | undefined || '',
apiKey = providerValues.value['openrouter-ai']?.apiKey as string | undefined || '',
model = providerValues.value['openrouter-ai']?.model as { id: string } | undefined || { id: 'openai/gpt-4o-mini' },
} = options ?? { }
streamingMessage.value = { role: 'assistant', content: '' }
messages.value.push({ role: 'user', content: sendingMessage })
messages.value.push(streamingMessage.value)
@@ -106,7 +95,7 @@ export const useChatStore = defineStore('chat', () => {
await hook(sendingMessage)
}
const res = await stream(baseUrl, apiKey, model.id, newMessages as Message[])
const res = await stream(options.model, options.chatProvider, newMessages as Message[])
for (const hook of onAfterSendHooks.value) {
await hook(sendingMessage)
+3 -4
View File
@@ -1,3 +1,4 @@
import type { ChatProvider } from '@xsai-ext/shared-providers'
import type { Message } from '@xsai/shared-chat'
import { listModels } from '@xsai/model'
@@ -5,11 +6,9 @@ import { streamText } from '@xsai/stream-text'
import { defineStore } from 'pinia'
export const useLLM = defineStore('llm', () => {
async function stream(apiUrl: string, apiKey: string, model: string, messages: Message[]) {
async function stream(model: string, chatProvider: ChatProvider, messages: Message[]) {
return await streamText({
baseURL: (apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`) as `${string}/`,
apiKey,
model,
...chatProvider.chat(model),
messages,
streamOptions: {
usage: true,
@@ -15,10 +15,6 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
const modelSearchQuery = ref('')
// Computed properties
const availableProvidersMetadata = computed(() => {
return providersStore.availableProviders.map(id => providersStore.getProviderMetadata(id))
})
const supportsModelListing = computed(() => {
return providersStore.supportsModelListing(activeProvider.value)
})
@@ -94,7 +90,6 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
modelSearchQuery,
// Computed
availableProvidersMetadata,
supportsModelListing,
providerModels,
isLoadingActiveProviderModels,
@@ -1 +1,2 @@
export * from './consciousness'
export * from './speech'
@@ -0,0 +1,300 @@
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { Voice, voiceList, voiceMap } from '../../constants/elevenlabs'
import { useProvidersStore } from '../providers'
export const useSpeechStore = defineStore('speech', () => {
const providersStore = useProvidersStore()
// State
const activeSpeechProvider = useLocalStorage('settings/speech/active-provider', '')
const activeSpeechModel = useLocalStorage('settings/speech/active-model', 'eleven_multilingual_v2')
const voiceName = useLocalStorage('settings/speech/voice-name', '')
const voiceId = useLocalStorage('settings/speech/voice-id', '')
const pitch = useLocalStorage('settings/speech/pitch', 0)
const rate = useLocalStorage('settings/speech/rate', 1)
const ssmlEnabled = useLocalStorage('settings/speech/ssml-enabled', false)
const isLoadingSpeechProviderVoices = ref(false)
const speechProviderError = ref<string | null>(null)
const availableVoices = ref<Record<string, VoiceInfo[]>>({})
const selectedLanguage = useLocalStorage('settings/speech/language', 'en-US')
// Voice info interface
interface VoiceInfo {
id: string
name: string
provider: string
description?: string
gender?: string
previewUrl?: string
language?: string
}
// Computed properties
const availableSpeechProvidersMetadata = computed(() => {
// Filter only speech providers from all available providers
return providersStore.availableProviders
.filter(id => isSpeechProvider(id))
.map(id => providersStore.getProviderMetadata(id))
})
const supportsSSML = computed(() => {
// Currently only ElevenLabs and some other providers support SSML
return ['elevenlabs', 'microsoft', 'google'].includes(activeSpeechProvider.value)
})
const availableLanguages = computed(() => {
return Object.keys(voiceList)
})
const availableVoicesForLanguage = computed(() => {
const language = selectedLanguage.value
if (!language || !voiceList[language]) {
return []
}
return voiceList[language].map(voiceEnum => ({
id: voiceMap[voiceEnum],
name: voiceEnum,
provider: 'elevenlabs',
language,
}))
})
// Helper function to determine if a provider is a speech provider
function isSpeechProvider(providerId: string): boolean {
// This is a simplified check - in a real implementation, you might have a more robust way
// to determine if a provider supports speech synthesis
return ['elevenlabs', 'microsoft', 'google', 'amazon'].includes(providerId)
}
// Actions
function setActiveSpeechProvider(provider: string) {
activeSpeechProvider.value = provider
}
function setActiveSpeechModel(model: string) {
activeSpeechModel.value = model
}
function setVoiceName(name: string) {
voiceName.value = name
// If this is a preset voice, also set the voiceId
for (const voiceEnum in voiceMap) {
if (voiceEnum === name) {
voiceId.value = voiceMap[voiceEnum as Voice]
break
}
}
}
function setVoiceId(id: string) {
voiceId.value = id
// Try to find the corresponding voice name
for (const voiceEnum in voiceMap) {
if (voiceMap[voiceEnum as Voice] === id) {
voiceName.value = voiceEnum
break
}
}
}
function setPitch(value: number) {
pitch.value = value
}
function setRate(value: number) {
rate.value = value
}
function setSSMLEnabled(enabled: boolean) {
ssmlEnabled.value = enabled
}
function setLanguage(language: string) {
selectedLanguage.value = language
}
function resetVoiceSettings() {
voiceName.value = ''
voiceId.value = ''
pitch.value = 0
rate.value = 1
ssmlEnabled.value = false
}
async function loadVoicesForProvider(provider: string) {
if (!provider || !isSpeechProvider(provider)) {
return []
}
isLoadingSpeechProviderVoices.value = true
speechProviderError.value = null
try {
// In a real implementation, you would fetch voices from the provider
// For now, we'll use our predefined voices for ElevenLabs
await new Promise(resolve => setTimeout(resolve, 500))
if (provider === 'elevenlabs') {
// Use the predefined voices from elevenlabs.ts
const allVoices: VoiceInfo[] = []
for (const language in voiceList) {
const voices = voiceList[language]
for (const voice of voices) {
allVoices.push({
id: voiceMap[voice],
name: voice,
provider: 'elevenlabs',
language,
description: getVoiceDescription(voice),
gender: getVoiceGender(voice),
})
}
}
availableVoices.value[provider] = allVoices
}
else if (provider === 'microsoft') {
availableVoices.value[provider] = [
{
id: 'en-US-AriaNeural',
name: 'Aria',
provider: 'microsoft',
description: 'Microsoft neural voice (female)',
gender: 'female',
language: 'en-US',
},
{
id: 'en-US-GuyNeural',
name: 'Guy',
provider: 'microsoft',
description: 'Microsoft neural voice (male)',
gender: 'male',
language: 'en-US',
},
]
}
else {
// For other providers, return an empty array for now
availableVoices.value[provider] = []
}
return availableVoices.value[provider]
}
catch (error) {
console.error(`Error fetching voices for ${provider}:`, error)
speechProviderError.value = error instanceof Error ? error.message : 'Unknown error'
return []
}
finally {
isLoadingSpeechProviderVoices.value = false
}
}
// Helper function to get voice descriptions
function getVoiceDescription(voice: string): string {
const descriptions: Record<string, string> = {
[Voice.Myriam]: 'Professional female voice with clear articulation',
[Voice.Beatrice]: 'Mature and sophisticated female voice',
[Voice.Camilla_KM]: 'Friendly and approachable female voice',
[Voice.SallySunshine]: 'Cheerful and upbeat female voice',
[Voice.Annie]: 'Young and energetic female voice',
[Voice.KawaiiAerisita]: 'Cute and playful female voice',
[Voice.Morioki]: 'Deep and authoritative Japanese male voice',
}
return descriptions[voice as Voice] || 'ElevenLabs voice'
}
// Helper function to get voice gender
function getVoiceGender(voice: string): string {
const maleVoices = [Voice.Morioki]
return maleVoices.includes(voice as Voice) ? 'male' : 'female'
}
// Get voices for a specific provider
function getVoicesForProvider(provider: string) {
return availableVoices.value[provider] || []
}
// Watch for provider changes and load voices
watch(activeSpeechProvider, async (newProvider) => {
if (newProvider) {
await loadVoicesForProvider(newProvider)
// Don't reset voice settings when changing providers to allow for persistence
}
})
// Generate SSML for the current configuration
function generateSSML(text: string): string {
if (!ssmlEnabled.value) {
return text
}
let ssml = '<speak>'
if (voiceName.value) {
ssml += `<voice name="${voiceName.value}">`
}
if (pitch.value !== 0 || rate.value !== 1) {
ssml += `<prosody pitch="+${pitch.value}%" rate="${rate.value}">`
}
ssml += text
if (pitch.value !== 0 || rate.value !== 1) {
ssml += '</prosody>'
}
if (voiceName.value) {
ssml += '</voice>'
}
ssml += '</speak>'
return ssml
}
return {
// State
activeSpeechProvider,
activeSpeechModel,
voiceName,
voiceId,
pitch,
rate,
ssmlEnabled,
selectedLanguage,
isLoadingSpeechProviderVoices,
speechProviderError,
availableVoices,
// Computed
availableSpeechProvidersMetadata,
supportsSSML,
availableLanguages,
availableVoicesForLanguage,
// Actions
setActiveSpeechProvider,
setActiveSpeechModel,
setVoiceName,
setVoiceId,
setPitch,
setRate,
setSSMLEnabled,
setLanguage,
resetVoiceSettings,
loadVoicesForProvider,
getVoicesForProvider,
generateSSML,
isSpeechProvider,
}
})
+37 -17
View File
@@ -1,4 +1,6 @@
/* eslint-disable no-case-declarations */
import type { ChatProvider, EmbedProvider, SpeechProvider, TranscriptionProvider } from '@xsai-ext/shared-providers'
import { useLocalStorage } from '@vueuse/core'
import {
createDeepSeek,
@@ -8,6 +10,7 @@ import {
createNovita,
createOpenAI,
createOpenRouter,
createPerplexity,
createTogetherAI,
createWorkersAI,
createXAI,
@@ -28,7 +31,7 @@ export interface ProviderMetadata {
iconColor?: string
iconImage?: string
baseUrlDefault?: string
createProvider?: (config: Record<string, unknown>) => any
createProvider: (config: Record<string, unknown>) => ChatProvider | EmbedProvider | SpeechProvider | TranscriptionProvider
modelSelectionType: 'dynamic' | 'manual' | 'hardcoded'
fetchModelsManually?: (config: Record<string, unknown>) => Promise<ModelInfo[]>
hardcodedModels?: ModelInfo[]
@@ -45,10 +48,7 @@ export interface ModelInfo {
}
export const useProvidersStore = defineStore('providers', () => {
const providers = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
const coreControllerProvider = useLocalStorage<string>('settings/credentials/coreControllerProvider', 'openai')
const audioSynthesisProvider = useLocalStorage<string>('settings/credentials/audioSynthesisProvider', 'elevenlabs')
const providerCredentials = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
// Helper function to fetch OpenRouter models manually
async function fetchOpenRouterModels(config: Record<string, unknown>): Promise<ModelInfo[]> {
@@ -178,10 +178,7 @@ export const useProvidersStore = defineStore('providers', () => {
description: 'perplexity.ai',
icon: 'i-lobe-icons:perplexity',
baseUrlDefault: 'https://api.perplexity.ai',
createProvider: config => ({
apiKey: config.apiKey as string,
baseUrl: config.baseUrl as string,
}),
createProvider: config => createPerplexity(config.apiKey as string, config.baseUrl as string),
modelSelectionType: 'hardcoded',
hardcodedModels: [
{
@@ -325,7 +322,7 @@ export const useProvidersStore = defineStore('providers', () => {
// Configuration validation functions
function validateProvider(providerId: string): boolean {
const config = providers.value[providerId]
const config = providerCredentials.value[providerId]
if (!config)
return false
@@ -366,9 +363,9 @@ export const useProvidersStore = defineStore('providers', () => {
// Initialize provider configurations
function initializeProvider(providerId: string) {
if (!providers.value[providerId]) {
if (!providerCredentials.value[providerId]) {
const metadata = providerMetadata[providerId]
providers.value[providerId] = {
providerCredentials.value[providerId] = {
baseUrl: metadata.baseUrlDefault || '',
}
}
@@ -386,7 +383,7 @@ export const useProvidersStore = defineStore('providers', () => {
// Call initially and watch for changes
updateConfigurationStatus()
watch(providers, updateConfigurationStatus, { deep: true })
watch(providerCredentials, updateConfigurationStatus, { deep: true })
// Available providers (only those that are properly configured)
const availableProviders = computed(() => {
@@ -413,7 +410,7 @@ export const useProvidersStore = defineStore('providers', () => {
// Function to fetch models for a specific provider
async function fetchModelsForProvider(providerId: string) {
const config = providers.value[providerId]
const config = providerCredentials.value[providerId]
if (!config)
return []
@@ -531,10 +528,31 @@ export const useProvidersStore = defineStore('providers', () => {
}))
})
// Function to get provider object by provider id
function getProviderInstance(providerId: string) {
const config = providerCredentials.value[providerId]
if (!config)
throw new Error(`Provider credentials for ${providerId} not found`)
const metadata = providerMetadata[providerId]
if (!metadata)
throw new Error(`Provider metadata for ${providerId} not found`)
try {
return metadata.createProvider(config)
}
catch (error) {
console.error(`Error creating provider instance for ${providerId}:`, error)
throw error
}
}
const availableProvidersMetadata = computed(() => {
return availableProviders.value.map(id => getProviderMetadata(id))
})
return {
providers,
coreControllerProvider,
audioSynthesisProvider,
providers: providerCredentials,
availableProviders,
configuredProviders,
providerMetadata,
@@ -550,5 +568,7 @@ export const useProvidersStore = defineStore('providers', () => {
allAvailableModels,
loadModelsForConfiguredProviders,
supportsModelListing,
getProviderInstance,
availableProvidersMetadata,
}
})
-10
View File
@@ -12,12 +12,6 @@ export const useSettings = defineStore('settings', () => {
const language = useLocalStorage('settings/language', 'en-US')
const stageView = useLocalStorage('settings/stage/view/model-renderer', '2d')
const openAiApiKey = useLocalStorage('settings/credentials/openai-api-key', '')
const openAiApiBaseURL = useLocalStorage('settings/credentials/openai-api-base-url', '')
const elevenLabsApiKey = useLocalStorage('settings/credentials/elevenlabs-api-key', '')
const openAiModel = useLocalStorage<{ id: string, name?: string }>('settings/llm/openai/model', { id: 'openai/gpt-3.5-turbo', name: 'OpenAI GPT3.5 Turbo' })
const isAudioInputOn = useLocalStorage('settings/audio/input', 'true')
const selectedAudioDeviceId = computed(() => selectedAudioDevice.value?.deviceId)
const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
@@ -62,10 +56,6 @@ export const useSettings = defineStore('settings', () => {
})
return {
openAiApiKey,
openAiApiBaseURL,
openAiModel,
elevenLabsApiKey,
live2dModelFile,
live2dModelUrl,
live2dLoadSource,
+23 -20
View File
@@ -348,7 +348,7 @@ importers:
version: 2.3.0
'@intlify/unplugin-vue-i18n':
specifier: ^6.0.3
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
'@proj-airi/elevenlabs':
specifier: workspace:^
version: link:../../packages/elevenlabs
@@ -375,10 +375,10 @@ importers:
version: 5.2.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
'@vue-macros/volar':
specifier: ^3.0.0-beta.3
version: 3.0.0-beta.3(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
version: 3.0.0-beta.3(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
'@vueuse/motion':
specifier: ^2.2.6
version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2))
electron:
specifier: ^34.3.1
version: 34.3.1
@@ -396,13 +396,13 @@ importers:
version: 3.2.0(unocss@66.1.0-beta.3(postcss@8.5.3)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)))
unplugin-auto-import:
specifier: ^19.1.1
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.8.2(typescript@5.8.2))
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.8.2(typescript@5.8.2))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2))
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2))
unplugin-vue-macros:
specifier: ^2.14.5
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
unplugin-vue-markdown:
specifier: ^28.3.1
version: 28.3.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
@@ -411,13 +411,13 @@ importers:
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rollup@2.79.1)
version: 1.2.1(rollup@4.34.9)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vite-plugin-vue-devtools:
specifier: ^7.7.2
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
@@ -526,6 +526,9 @@ importers:
'@xsai-ext/providers-local':
specifier: 'catalog:'
version: 0.1.2
'@xsai-ext/shared-providers':
specifier: 'catalog:'
version: 0.1.2
'@xsai/generate-speech':
specifier: 'catalog:'
version: 0.1.2
@@ -661,7 +664,7 @@ importers:
version: 2.3.0
'@intlify/unplugin-vue-i18n':
specifier: ^6.0.3
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
'@proj-airi/drizzle-duckdb-wasm':
specifier: workspace:^
version: link:../../packages/drizzle-duckdb-wasm
@@ -703,10 +706,10 @@ importers:
version: 5.2.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
'@vue-macros/volar':
specifier: ^3.0.0-beta.3
version: 3.0.0-beta.3(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
version: 3.0.0-beta.3(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
'@vueuse/motion':
specifier: ^2.2.6
version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2))
version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
hfup:
specifier: workspace:^
version: link:../../packages/hfup
@@ -718,13 +721,13 @@ importers:
version: 4.0.1
unplugin-auto-import:
specifier: ^19.1.1
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.8.2(typescript@5.8.2))
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.8.2(typescript@5.8.2))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2))
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2))
unplugin-vue-macros:
specifier: ^2.14.5
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
unplugin-vue-markdown:
specifier: ^28.3.1
version: 28.3.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
@@ -733,13 +736,13 @@ importers:
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rollup@4.34.9)
version: 1.2.1(rollup@2.79.1)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vite-plugin-vue-devtools:
specifier: ^7.7.2
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
@@ -23176,9 +23179,9 @@ snapshots:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.34.9)
'@vueuse/core': 12.8.2(typescript@5.8.2)
unplugin-combine@1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
unplugin-combine@1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
optionalDependencies:
esbuild: 0.25.0
esbuild: 0.19.12
rollup: 2.79.1
unplugin: 1.16.1
vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
@@ -23237,7 +23240,7 @@ snapshots:
transitivePeerDependencies:
- vue
unplugin-vue-macros@2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
unplugin-vue-macros@2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
dependencies:
'@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2))
'@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2))
@@ -23269,7 +23272,7 @@ snapshots:
'@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2))
'@vue-macros/volar': 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
unplugin: 1.16.1
unplugin-combine: 1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
unplugin-combine: 1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2))
vue: 3.5.13(typescript@5.8.2)
transitivePeerDependencies: