feat(stage-web): added more settings
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+23
-20
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user