refactor(stage-*): move shared pages to stage-pages package
This commit is contained in:
@@ -24,15 +24,45 @@ export default defineConfig({
|
||||
renderer: {
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
// Internal Packages
|
||||
'@proj-airi/stage-ui/*',
|
||||
'@proj-airi/drizzle-duckdb-wasm',
|
||||
'@proj-airi/drizzle-duckdb-wasm/*',
|
||||
|
||||
// Static Assets: Models, Images, etc.
|
||||
'src/renderer/public/assets/*',
|
||||
|
||||
// Live2D SDK
|
||||
'@framework/live2dcubismframework',
|
||||
'@framework/math/cubismmatrix44',
|
||||
'@framework/type/csmvector',
|
||||
'@framework/math/cubismviewmatrix',
|
||||
'@framework/cubismdefaultparameterid',
|
||||
'@framework/cubismmodelsettingjson',
|
||||
'@framework/effect/cubismbreath',
|
||||
'@framework/effect/cubismeyeblink',
|
||||
'@framework/model/cubismusermodel',
|
||||
'@framework/motion/acubismmotion',
|
||||
'@framework/motion/cubismmotionqueuemanager',
|
||||
'@framework/type/csmmap',
|
||||
'@framework/utils/cubismdebug',
|
||||
'@framework/model/cubismmoc',
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')),
|
||||
'@proj-airi/server-sdk': resolve(join(import.meta.dirname, '..', '..', 'packages', 'server-sdk', 'src')),
|
||||
'@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')),
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')),
|
||||
'@proj-airi/stage-pages': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src')),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
warmup: {
|
||||
clientFiles: [
|
||||
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src'))}/*.vue`,
|
||||
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src'))}/*.vue`,
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -51,7 +81,10 @@ export default defineConfig({
|
||||
|
||||
VueRouter({
|
||||
dts: resolve(import.meta.dirname, 'src/renderer/typed-router.d.ts'),
|
||||
routesFolder: 'src/renderer/pages',
|
||||
routesFolder: [
|
||||
resolve(import.meta.dirname, 'src', 'renderer', 'pages'),
|
||||
resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'),
|
||||
],
|
||||
}),
|
||||
|
||||
VitePluginVueDevTools(),
|
||||
@@ -74,10 +107,5 @@ export default defineConfig({
|
||||
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'assets/vrm/models/AvatarSample-A'),
|
||||
Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'assets/vrm/models/AvatarSample-B'),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import { Alert } from '@proj-airi/stage-ui/components'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { InputFile } from '@proj-airi/ui'
|
||||
import { Select } from '@proj-airi/ui/components/form'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CardCreate from './components/CardCreate.vue'
|
||||
import CardCreationDialog from './components/CardCreationDialog.vue'
|
||||
import CardDetailDialog from './components/CardDetailDialog.vue'
|
||||
import CardListItem from './components/CardListItem.vue'
|
||||
import DeleteCardDialog from './components/DeleteCardDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
const { addCard, removeCard } = cardStore
|
||||
const { cards, activeCardId } = storeToRefs(cardStore)
|
||||
|
||||
// Currently selected card ID (different from active card ID)
|
||||
const selectedCardId = ref<string>('')
|
||||
// Dialog state
|
||||
const isCardDialogOpen = ref(false)
|
||||
const isCardCreationDialogOpen = ref(false)
|
||||
|
||||
// Search query
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Sort option
|
||||
const sortOption = ref('nameAsc')
|
||||
|
||||
const inputFiles = ref<File[]>([])
|
||||
|
||||
// Card list data structure
|
||||
interface CardItem {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
deprecated?: boolean
|
||||
customizable?: boolean
|
||||
}
|
||||
|
||||
watch(inputFiles, async (newFiles) => {
|
||||
const file = newFiles[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
const cardJSON = JSON.parse(content) as ccv3.CharacterCardV3
|
||||
|
||||
// Add card and select it
|
||||
selectedCardId.value = addCard(cardJSON)
|
||||
isCardDialogOpen.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing card file:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// Transform cards Map to array for display
|
||||
const cardsArray = computed<CardItem[]>(() =>
|
||||
Array.from(cards.value.entries()).map(([id, card]) => ({
|
||||
id,
|
||||
name: card.name,
|
||||
description: card.description,
|
||||
})),
|
||||
)
|
||||
|
||||
// Filtered cards based on search query
|
||||
const filteredCards = computed<CardItem[]>(() => {
|
||||
if (!searchQuery.value)
|
||||
return cardsArray.value
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return cardsArray.value.filter(item =>
|
||||
item.name.toLowerCase().includes(query)
|
||||
|| (item.description && item.description.toLowerCase().includes(query)),
|
||||
)
|
||||
})
|
||||
|
||||
// Sorted filtered cards based on sort option
|
||||
const sortedFilteredCards = computed<CardItem[]>(() => {
|
||||
// Create a new array to avoid mutating the source
|
||||
const sorted = [...filteredCards.value]
|
||||
|
||||
if (sortOption.value === 'nameAsc')
|
||||
return sorted.sort((a, b) => a.name.localeCompare(b.name))
|
||||
else if (sortOption.value === 'nameDesc')
|
||||
return sorted.sort((a, b) => b.name.localeCompare(a.name))
|
||||
else if (sortOption.value === 'recent')
|
||||
return sorted.sort((a, b) => b.id.localeCompare(a.id))
|
||||
else
|
||||
return sorted
|
||||
})
|
||||
|
||||
// Delete confirmation
|
||||
const showDeleteConfirm = ref(false)
|
||||
const cardToDelete = ref<string | null>(null)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
if (cardToDelete.value) {
|
||||
removeCard(cardToDelete.value)
|
||||
cardToDelete.value = null
|
||||
showDeleteConfirm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Card deletion confirmation
|
||||
function confirmDelete(id: string) {
|
||||
cardToDelete.value = id
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
function handleSelectCard(cardId: string) {
|
||||
selectedCardId.value = cardId
|
||||
isCardDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleCardCreationDialog() {
|
||||
isCardCreationDialogOpen.value = true
|
||||
}
|
||||
|
||||
// Card activation
|
||||
function activateCard(id: string) {
|
||||
activeCardId.value = id
|
||||
}
|
||||
|
||||
// Card version number
|
||||
function getVersionNumber(id: string) {
|
||||
const card = cards.value.get(id)
|
||||
return card?.version || '1.0.0'
|
||||
}
|
||||
|
||||
// Card module short name
|
||||
function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
|
||||
const card = cards.value.get(id)
|
||||
if (!card || !card.extensions?.airi?.modules)
|
||||
return 'default'
|
||||
|
||||
const airiExt = card.extensions.airi.modules
|
||||
|
||||
if (module === 'consciousness') {
|
||||
return airiExt.consciousness?.model ? airiExt.consciousness.model.split('-').pop() || 'default' : 'default'
|
||||
}
|
||||
else if (module === 'voice') {
|
||||
return airiExt.speech?.voice_id || 'default'
|
||||
}
|
||||
|
||||
return 'default'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div rounded-xl p-4 flex="~ col gap-4">
|
||||
<!-- Toolbar with search and filters -->
|
||||
<div flex="~ row" flex-wrap items-center justify-between gap-4>
|
||||
<!-- Search bar -->
|
||||
<div class="relative min-w-[200px] flex-1" inline-flex="~" w-full items-center>
|
||||
<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-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out"
|
||||
bg="white dark:neutral-900"
|
||||
:placeholder="t('settings.pages.card.search')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Sort options -->
|
||||
<div class="relative flex flex-row justify-start gap-2 lg:flex-col">
|
||||
<div class="top-[-32px] whitespace-nowrap text-sm text-neutral-500 leading-10 lg:absolute dark:text-neutral-400">
|
||||
{{ t('settings.pages.card.sort_by') }}:
|
||||
</div>
|
||||
<Select
|
||||
v-model="sortOption"
|
||||
:options="[
|
||||
{ value: 'nameAsc', label: t('settings.pages.card.name_asc') },
|
||||
{ value: 'nameDesc', label: t('settings.pages.card.name_desc') },
|
||||
{ value: 'recent', label: t('settings.pages.card.recent') },
|
||||
]"
|
||||
placeholder="Select sort option"
|
||||
class="min-w-[150px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Masonry card layout -->
|
||||
<div
|
||||
class="mt-4"
|
||||
:class="{ 'grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 grid-auto-rows-[minmax(min-content,max-content)] grid-auto-flow-dense sm:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] sm:gap-5 md:grid-cols-[repeat(auto-fill,minmax(220px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(250px,1fr))]': cards.size > 0 }"
|
||||
>
|
||||
<!-- Upload card -->
|
||||
<InputFile v-model="inputFiles" accept="*.json">
|
||||
<template #default="{ isDragging }">
|
||||
<template v-if="!isDragging">
|
||||
<div flex flex-col items-center>
|
||||
<div i-solar:upload-square-line-duotone mb-4 text-5xl text="neutral-400 dark:neutral-500" />
|
||||
<p font-medium text="neutral-600 dark:neutral-300">
|
||||
{{ t('settings.pages.card.upload') }}
|
||||
</p>
|
||||
<p text="neutral-500 dark:neutral-400" mt-2 text-sm>
|
||||
{{ t('settings.pages.card.upload_desc') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div flex flex-col items-center>
|
||||
<div i-solar:upload-minimalistic-bold class="mb-2 text-5xl text-primary-500 dark:text-primary-400" />
|
||||
<p font-medium text="primary-600 dark:primary-300">
|
||||
{{ t('settings.pages.card.drop_here') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</InputFile>
|
||||
|
||||
<!-- Create card -->
|
||||
<CardCreate @click="handleCardCreationDialog" />
|
||||
|
||||
<!-- Card Items -->
|
||||
<template v-if="cards.size > 0">
|
||||
<CardListItem
|
||||
v-for="item in sortedFilteredCards"
|
||||
:id="item.id"
|
||||
:key="item.id"
|
||||
:name="item.name"
|
||||
:description="item.description"
|
||||
:is-active="item.id === activeCardId"
|
||||
:is-selected="item.id === selectedCardId && isCardDialogOpen"
|
||||
:version="getVersionNumber(item.id)"
|
||||
:consciousness-model="getModuleShortName(item.id, 'consciousness')"
|
||||
:voice-model="getModuleShortName(item.id, 'voice')"
|
||||
@select="handleSelectCard(item.id)"
|
||||
@activate="activateCard(item.id)"
|
||||
@delete="confirmDelete(item.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- No cards message -->
|
||||
<div
|
||||
v-if="cards.size === 0"
|
||||
class="col-span-full rounded-xl p-8 text-center"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
bg="neutral-50/50 dark:neutral-900/50"
|
||||
>
|
||||
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
||||
<p>{{ t('settings.pages.card.no_cards') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- No search results -->
|
||||
<Alert
|
||||
v-if="searchQuery && sortedFilteredCards.length === 0"
|
||||
type="warning"
|
||||
class="col-span-full"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.card.no_results') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.card.try_different_search') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<DeleteCardDialog
|
||||
v-model="showDeleteConfirm"
|
||||
:card-name="cardToDelete ? cardStore.getCard(cardToDelete)?.name : ''"
|
||||
@confirm="handleDeleteConfirm"
|
||||
@cancel="cardToDelete = null"
|
||||
/>
|
||||
|
||||
<!-- Card detail dialog -->
|
||||
<CardDetailDialog
|
||||
v-model="isCardDialogOpen"
|
||||
:card-id="selectedCardId"
|
||||
/>
|
||||
|
||||
<!-- Card detail dialog -->
|
||||
<CardCreationDialog
|
||||
v-model="isCardCreationDialogOpen"
|
||||
/>
|
||||
|
||||
<!-- Background decoration -->
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:emoji-funny-square-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,51 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../components/IconAnimation.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const iconAnimationStarted = ref(false)
|
||||
const iconAnimation = ref<InstanceType<typeof IconAnimation>>()
|
||||
const resolveAnimation = ref<() => void>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const animationIcon = ref('')
|
||||
const animationPosition = ref('')
|
||||
const showAnimationComponent = ref(false)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
function handleAnimationEnded() {
|
||||
resolveAnimation.value?.()
|
||||
}
|
||||
|
||||
async function handleIconItemClick(event: MouseEvent, setting: typeof settings.value[0]) {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const iconElement = target.querySelector('.menu-icon-item-icon') as HTMLElement
|
||||
if (!iconElement)
|
||||
return
|
||||
|
||||
// get the position of the icon element
|
||||
const rect = iconElement.getBoundingClientRect()
|
||||
const position = `${rect.left}px, ${rect.top}px`
|
||||
|
||||
// set the icon and position
|
||||
animationIcon.value = setting.icon
|
||||
animationPosition.value = position
|
||||
|
||||
// show the animation component
|
||||
showAnimationComponent.value = true
|
||||
|
||||
// wait for the DOM to update
|
||||
await nextTick()
|
||||
|
||||
// start the animation
|
||||
iconAnimationStarted.value = true
|
||||
}
|
||||
|
||||
const removeBeforeEach = router.beforeEach(async (_, __, next) => {
|
||||
if (!settingsStore.usePageSpecificTransitions || settingsStore.disableTransitions) {
|
||||
next()
|
||||
@@ -123,22 +87,9 @@ const settings = computed(() => [
|
||||
:description="setting.description"
|
||||
:icon="setting.icon"
|
||||
:to="setting.to"
|
||||
@click="(e: MouseEvent) => handleIconItemClick(e, setting)"
|
||||
/>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showAnimationComponent && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions"
|
||||
ref="iconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="6 * 1.2"
|
||||
:position="animationPosition"
|
||||
:duration="1000"
|
||||
text-color="text-neutral-400/50 dark:text-neutral-600/20"
|
||||
:started="iconAnimationStarted"
|
||||
@animation-ended.once="handleAnimationEnded"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-12rem)]" bottom-0 right--10 z--1
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Live2DCanvas } from '@proj-airi/stage-ui/components/scenes'
|
||||
|
||||
import { ModelSettings } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings'
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
|
||||
async function extractColorsFromModel() {
|
||||
if (!live2dCanvasRef.value)
|
||||
return
|
||||
|
||||
const frame = await live2dCanvasRef.value.captureFrame()
|
||||
if (!frame) {
|
||||
console.error('No frame captured')
|
||||
return
|
||||
}
|
||||
|
||||
const frameUrl = URL.createObjectURL(frame)
|
||||
try {
|
||||
const vibrant = new Vibrant(frameUrl)
|
||||
|
||||
const paletteFromVibrant = await vibrant.getPalette()
|
||||
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
||||
}
|
||||
finally {
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:people-nearby-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex class="relative h-full flex-col-reverse md:flex-row">
|
||||
<ModelSettings
|
||||
settings-class="w-100% md:w-40% lg:w-40% xl:w-25% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
|
||||
live-2d-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
vrm-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
:palette="palette" @extract-colors-from-model="extractColorsFromModel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 15 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:people-nearby-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,216 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Alert, ErrorContainer, RadioCardManySelect, RadioCardSimple } from '@proj-airi/stage-ui/components'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { configuredChatProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeProvider,
|
||||
activeModel,
|
||||
customModelName,
|
||||
modelSearchQuery,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
} = storeToRefs(consciousnessStore)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
async function loadModelsForProvider() {
|
||||
if (activeProvider.value) {
|
||||
await consciousnessStore.loadModelsForProvider(activeProvider.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadModelsForProvider)
|
||||
|
||||
watch(activeProvider, loadModelsForProvider)
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
customModelName.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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 text-neutral-500 md:text-2xl dark:text-neutral-500">
|
||||
{{ t('settings.pages.providers.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<!--
|
||||
fieldset has min-width set to --webkit-min-container, in order to use over flow scroll,
|
||||
we need to set the min-width to 0.
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset
|
||||
v-if="configuredChatProvidersMetadata.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 configuredChatProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeProvider"
|
||||
name="provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName || 'Unknown'"
|
||||
: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 Providers Configured</span>
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your LLM
|
||||
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>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<ErrorContainer
|
||||
v-else-if="activeProviderModelError"
|
||||
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
|
||||
:error="activeProviderModelError"
|
||||
/>
|
||||
|
||||
<!-- No models available -->
|
||||
<Alert
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
type="warning"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Using the new RadioCardManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardManySelect
|
||||
v-model="activeModel"
|
||||
v-model:search-query="modelSearchQuery"
|
||||
:items="providerModels.sort((a, b) => a.id === activeModel ? -1 : b.id === activeModel ? 1 : 0)"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: modelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider doesn't support model listing -->
|
||||
<div v-else-if="activeProvider && !supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 border border-primary-200 rounded-lg bg-primary-50 p-4 dark:border-primary-800 dark:bg-primary-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-primary-500 dark:text-primary-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.not_supported')
|
||||
}}</span>
|
||||
<span class="text-sm text-primary-600 dark:text-primary-400">{{
|
||||
t('settings.pages.modules.consciousness.sections.section.provider-model-selection.not_supported_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual model input for providers without model listing -->
|
||||
<div class="mt-2">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_name') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="activeModel" 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="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:ghost-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,650 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { Alert, Button, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
// import { useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
// import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// import workletUrl from '../../../tauri/vad/process.worklet?worker&url'
|
||||
|
||||
// import { createVAD, createVADStates } from '../../../tauri/vad'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
const {
|
||||
activeTranscriptionProvider,
|
||||
activeTranscriptionModel,
|
||||
providerModels,
|
||||
activeProviderModelError,
|
||||
isLoadingActiveProviderModels,
|
||||
supportsModelListing,
|
||||
transcriptionModelSearchQuery,
|
||||
activeCustomModelName,
|
||||
} = storeToRefs(hearingStore)
|
||||
const providersStore = useProvidersStore()
|
||||
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const { stopStream, startStream } = useSettingsAudioDevice()
|
||||
const { audioInputs, selectedAudioInput, stream } = storeToRefs(useSettingsAudioDevice())
|
||||
// const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
|
||||
const { startAnalyzer, stopAnalyzer, onAnalyzerUpdate, volumeLevel } = useAudioAnalyzer()
|
||||
const { audioContext } = storeToRefs(useAudioContext())
|
||||
|
||||
const error = ref<string>('')
|
||||
const vadModelError = ref('')
|
||||
|
||||
const isMonitoring = ref(false)
|
||||
const enablePlayback = ref(false)
|
||||
|
||||
// Audio processing state
|
||||
const gainNode = ref<GainNode>()
|
||||
const animationFrame = ref<number>()
|
||||
|
||||
// Audio levels and indicators
|
||||
const isSpeaking = ref(false)
|
||||
const speakingThreshold = ref(25) // 0-100 (for volume-based fallback)
|
||||
const monitorVolume = ref(50) // 0-100
|
||||
|
||||
// VAD integration
|
||||
// const vadManager = ref<ReturnType<typeof createVADStates>>()
|
||||
const isVADModelLoaded = ref(false)
|
||||
const isLoadingVADModel = ref(false)
|
||||
const useVADModel = ref(true) // Toggle between VAD and volume-based detection
|
||||
const vadProbability = ref(0) // Raw VAD probability
|
||||
const vadThreshold = ref(0.5) // VAD probability threshold for speech detection
|
||||
|
||||
// VAD visualization
|
||||
const vadHistory = ref<number[]>([]) // History for chart visualization
|
||||
// const maxVadHistory = 50 // Keep 50 samples (~1.6 seconds at 32ms intervals)
|
||||
|
||||
const audios = ref<Blob[]>([])
|
||||
const audioCleanups = ref<(() => void)[]>([])
|
||||
const audioURLs = computed(() => {
|
||||
return audios.value.map((blob) => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
audioCleanups.value.push(() => URL.revokeObjectURL(url))
|
||||
return url
|
||||
})
|
||||
})
|
||||
const transcriptions = ref<string[]>([])
|
||||
|
||||
// // VAD functions
|
||||
// async function loadVADModel() {
|
||||
// if (isVADModelLoaded.value || isLoadingVADModel.value)
|
||||
// return
|
||||
|
||||
// isLoadingVADModel.value = true
|
||||
// vadModelError.value = ''
|
||||
|
||||
// try {
|
||||
// // // Create and initialize the VAD
|
||||
// // const vad = await createVAD({
|
||||
// // sampleRate: 16000,
|
||||
// // speechThreshold: vadThreshold.value,
|
||||
// // exitThreshold: vadThreshold.value * 0.3,
|
||||
// // minSilenceDurationMs: 400,
|
||||
// // })
|
||||
|
||||
// // // Set up event handlers
|
||||
// // vad.on('speech-start', () => {
|
||||
// // isSpeaking.value = true
|
||||
// // startRecord() // Start recording when speech is detected
|
||||
// // })
|
||||
|
||||
// // vad.on('speech-end', () => {
|
||||
// // isSpeaking.value = false
|
||||
// // stopRecord() // Stop recording when speech ends
|
||||
// // })
|
||||
|
||||
// // vad.on('debug', ({ data }) => {
|
||||
// // if (data?.probability !== undefined) {
|
||||
// // vadProbability.value = data.probability
|
||||
|
||||
// // // Update VAD history for visualization
|
||||
// // vadHistory.value.push(data.probability)
|
||||
// // if (vadHistory.value.length > maxVadHistory) {
|
||||
// // vadHistory.value.shift()
|
||||
// // }
|
||||
// // }
|
||||
// // })
|
||||
|
||||
// // vad.on('status', ({ type, message }) => {
|
||||
// // if (type === 'error') {
|
||||
// // vadModelError.value = message
|
||||
// // }
|
||||
// // })
|
||||
|
||||
// // // Create and initialize audio manager
|
||||
// // const manager = createVADStates(vad, workletUrl, {
|
||||
// // minChunkSize: 512,
|
||||
// // // NOTICE: VAD will have it's own audio context since
|
||||
// // // it needs special sample rate and latency settings
|
||||
// // audioContextOptions: {
|
||||
// // sampleRate: 16000,
|
||||
// // latencyHint: 'interactive',
|
||||
// // },
|
||||
// // })
|
||||
|
||||
// // await manager.initialize()
|
||||
// // vadManager.value = manager
|
||||
// // isVADModelLoaded.value = true
|
||||
// }
|
||||
// catch (error) {
|
||||
// vadModelError.value = error instanceof Error ? error.message : String(error)
|
||||
// console.error('Failed to load VAD model:', error)
|
||||
// }
|
||||
// finally {
|
||||
// isLoadingVADModel.value = false
|
||||
// }
|
||||
// }
|
||||
|
||||
// onStopRecord(async (recording) => {
|
||||
// if (!recording)
|
||||
// return
|
||||
|
||||
// try {
|
||||
// if (recording && recording.size > 0) {
|
||||
// audios.value.push(recording)
|
||||
|
||||
// const provider = await providersStore.getProviderInstance<TranscriptionProvider<string>>(activeTranscriptionProvider.value)
|
||||
// if (!provider) {
|
||||
// throw new Error('Failed to initialize speech provider')
|
||||
// }
|
||||
|
||||
// // Get model from configuration or use default
|
||||
// const model = activeTranscriptionModel.value
|
||||
// const res = await hearingStore.transcription(provider, model, new File([recording], 'recording.wav'))
|
||||
|
||||
// transcriptions.value.push(res.text)
|
||||
// }
|
||||
// }
|
||||
// catch (err) {
|
||||
// error.value = err instanceof Error ? err.message : String(err)
|
||||
// console.error('Error generating transcription:', error.value)
|
||||
// }
|
||||
// })
|
||||
|
||||
// Audio monitoring
|
||||
async function setupAudioMonitoring() {
|
||||
try {
|
||||
if (!selectedAudioInput.value) {
|
||||
console.warn('No audio input device selected')
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up existing connections
|
||||
await stopAudioMonitoring()
|
||||
|
||||
await startStream()
|
||||
if (!stream.value) {
|
||||
console.warn('No audio stream available')
|
||||
return
|
||||
}
|
||||
|
||||
const source = audioContext.value.createMediaStreamSource(stream.value)
|
||||
const analyzer = startAnalyzer(audioContext.value)
|
||||
|
||||
onAnalyzerUpdate((volumeLevel) => {
|
||||
// Fallback speaking detection (when VAD model is not used)
|
||||
if (!useVADModel.value || !isVADModelLoaded.value) {
|
||||
isSpeaking.value = volumeLevel > speakingThreshold.value
|
||||
}
|
||||
})
|
||||
|
||||
// Create gain node for playback volume control
|
||||
gainNode.value = audioContext.value.createGain()
|
||||
gainNode.value.gain.value = enablePlayback.value ? (monitorVolume.value / 100) : 0
|
||||
|
||||
// Connect audio graph
|
||||
if (analyzer)
|
||||
source.connect(analyzer)
|
||||
|
||||
if (enablePlayback.value) {
|
||||
source.connect(gainNode.value)
|
||||
gainNode.value.connect(audioContext.value.destination)
|
||||
}
|
||||
|
||||
// // Load VAD model and start VAD processing if enabled
|
||||
// if (useVADModel.value) {
|
||||
// await loadVADModel()
|
||||
// if (vadManager.value) {
|
||||
// await vadManager.value.start(stream.value)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error setting up audio monitoring:', error)
|
||||
vadModelError.value = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function stopAudioMonitoring() {
|
||||
if (animationFrame.value) { // Stop animation frame
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
animationFrame.value = undefined
|
||||
}
|
||||
// if (vadManager.value) { // Stop VAD manager
|
||||
// await vadManager.value.stop()
|
||||
// }
|
||||
if (stream.value) { // Stop media stream
|
||||
stopStream()
|
||||
}
|
||||
|
||||
stopAnalyzer()
|
||||
|
||||
gainNode.value = undefined
|
||||
isSpeaking.value = false
|
||||
vadProbability.value = 0
|
||||
vadHistory.value = []
|
||||
}
|
||||
|
||||
// Update playback routing when playback setting changes
|
||||
async function updatePlayback() {
|
||||
if (!audioContext.value || !gainNode.value)
|
||||
return
|
||||
|
||||
if (enablePlayback.value) {
|
||||
gainNode.value.gain.value = monitorVolume.value / 100
|
||||
gainNode.value.connect(audioContext.value.destination)
|
||||
}
|
||||
else {
|
||||
gainNode.value.gain.value = 0
|
||||
gainNode.value.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(selectedAudioInput, async () => {
|
||||
if (isMonitoring.value) {
|
||||
await setupAudioMonitoring()
|
||||
}
|
||||
})
|
||||
|
||||
watch(enablePlayback, updatePlayback)
|
||||
watch(monitorVolume, () => {
|
||||
if (gainNode.value && enablePlayback.value) {
|
||||
gainNode.value.gain.value = monitorVolume.value / 100
|
||||
}
|
||||
})
|
||||
|
||||
// watch(vadThreshold, () => {
|
||||
// // Update VAD threshold if model is loaded
|
||||
// if (vadManager.value && isVADModelLoaded.value) {
|
||||
// // TODO: We would need to add an updateConfig method to VADAudioManager
|
||||
// }
|
||||
// })
|
||||
|
||||
// Monitoring toggle
|
||||
async function toggleMonitoring() {
|
||||
if (!isMonitoring.value) {
|
||||
await setupAudioMonitoring()
|
||||
isMonitoring.value = true
|
||||
}
|
||||
else {
|
||||
await stopAudioMonitoring()
|
||||
isMonitoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Speaking indicator with enhanced VAD visualization
|
||||
const speakingIndicatorClass = computed(() => {
|
||||
if (!useVADModel.value || !isVADModelLoaded.value) {
|
||||
// Volume-based: simple green/white
|
||||
return isSpeaking.value
|
||||
? 'bg-green-500 shadow-lg shadow-green-500/50'
|
||||
: 'bg-white dark:bg-neutral-900 border-2 border-neutral-300 dark:border-neutral-600'
|
||||
}
|
||||
|
||||
// VAD-based: color intensity based on probability
|
||||
const prob = vadProbability.value
|
||||
const threshold = vadThreshold.value
|
||||
|
||||
if (prob > threshold) {
|
||||
// Speaking: green (could add intensity in future)
|
||||
return `bg-green-500 shadow-lg shadow-green-500/50`
|
||||
}
|
||||
else if (prob > threshold * 0.5) {
|
||||
// Close to threshold: yellow
|
||||
return 'bg-yellow-500 shadow-lg shadow-yellow-500/30'
|
||||
}
|
||||
else {
|
||||
// Low probability: neutral
|
||||
return 'bg-white dark:bg-neutral-900 border-2 border-neutral-300 dark:border-neutral-600'
|
||||
}
|
||||
})
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
activeCustomModelName.value = value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await hearingStore.loadModelsForProvider(activeTranscriptionProvider.value)
|
||||
})
|
||||
|
||||
// onUnmounted(() => {
|
||||
// stopAudioMonitoring()
|
||||
// if (vadManager.value) {
|
||||
// vadManager.value.dispose()
|
||||
// }
|
||||
|
||||
// audioCleanups.value.forEach(cleanup => cleanup())
|
||||
// })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col md:row gap-6">
|
||||
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4" class="h-fit w-full md:w-[40%]">
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Audio Input Selection -->
|
||||
<div>
|
||||
<FieldSelect
|
||||
v-model="selectedAudioInput"
|
||||
label="Audio Input Device"
|
||||
description="Select the audio input device for your hearing module."
|
||||
:options="audioInputs.map(input => ({
|
||||
label: input.label || input.deviceId,
|
||||
value: input.deviceId,
|
||||
}))"
|
||||
placeholder="Select an audio input device"
|
||||
layout="vertical"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-500">
|
||||
{{ t('settings.pages.providers.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.hearing.sections.section.provider-model-selection.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<!--
|
||||
fieldset has min-width set to --webkit-min-container, in order to use over flow scroll,
|
||||
we need to set the min-width to 0.
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset
|
||||
v-if="configuredTranscriptionProvidersMetadata.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 configuredTranscriptionProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeTranscriptionProvider"
|
||||
name="provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName || 'Unknown'"
|
||||
: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 Providers Configured</span>
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your Transcription 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>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeTranscriptionProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<ErrorContainer
|
||||
v-else-if="activeProviderModelError"
|
||||
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
|
||||
:error="activeProviderModelError"
|
||||
/>
|
||||
|
||||
<!-- No models available -->
|
||||
<Alert
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
type="warning"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Using the new RadioCardManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardManySelect
|
||||
v-model="activeTranscriptionModel"
|
||||
v-model:search-query="transcriptionModelSearchQuery"
|
||||
:items="providerModels.sort((a, b) => a.id === activeTranscriptionModel ? -1 : b.id === activeTranscriptionModel ? 1 : 0)"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: transcriptionModelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-6" class="w-full md:w-[60%]">
|
||||
<div w-full rounded-xl>
|
||||
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
|
||||
<div class="inline-flex items-center gap-4">
|
||||
<TestDummyMarker />
|
||||
<div>
|
||||
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<ErrorContainer v-if="error" title="Error occurred" :error="error" mb-4 />
|
||||
|
||||
<Button class="mb-4" w-full @click="toggleMonitoring">
|
||||
{{ isMonitoring ? 'Stop Monitoring' : 'Start Monitoring' }}
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<div v-for="(audio, index) in audioURLs" :key="index" class="mb-2">
|
||||
<audio :src="audio" controls class="w-full" />
|
||||
<div v-if="transcriptions[index]" class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ transcriptions[index] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<div class="space-y-4">
|
||||
<!-- Audio Level Visualization -->
|
||||
<div class="space-y-3">
|
||||
<!-- Volume Meter -->
|
||||
<LevelMeter :level="volumeLevel" label="Input Level" />
|
||||
|
||||
<!-- VAD Probability Meter (when VAD model is active) -->
|
||||
<ThresholdMeter
|
||||
v-if="useVADModel && isVADModelLoaded"
|
||||
:value="vadProbability"
|
||||
:threshold="vadThreshold"
|
||||
label="Probability of Speech"
|
||||
below-label="Silence"
|
||||
above-label="Speech"
|
||||
threshold-label="Detection threshold"
|
||||
/>
|
||||
|
||||
<!-- Threshold Controls -->
|
||||
<div v-if="useVADModel && isVADModelLoaded" class="space-y-3">
|
||||
<FieldRange
|
||||
v-model="vadThreshold"
|
||||
label="Sensitivity"
|
||||
description="Adjust the threshold for speech detection"
|
||||
:min="0.1"
|
||||
:max="0.9"
|
||||
:step="0.05"
|
||||
:format-value="value => `${(value * 100).toFixed(0)}%`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<FieldRange
|
||||
v-model="speakingThreshold"
|
||||
label="Sensitivity"
|
||||
description="Adjust the threshold for speech detection"
|
||||
:min="1"
|
||||
:max="80"
|
||||
:step="1"
|
||||
:format-value="value => `${value}%`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Speaking Indicator -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-4 w-4 rounded-full transition-all duration-200"
|
||||
:class="speakingIndicatorClass"
|
||||
/>
|
||||
<span class="text-sm font-medium">
|
||||
{{ isSpeaking ? 'Speaking Detected' : 'Silence' }}
|
||||
</span>
|
||||
<span class="ml-auto text-xs text-neutral-500">
|
||||
{{ useVADModel && isVADModelLoaded ? 'Model Based' : 'Volume Based' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- VAD Method Selection -->
|
||||
<div class="border-t border-neutral-200 pt-3 dark:border-neutral-700">
|
||||
<FieldCheckbox
|
||||
v-model="useVADModel"
|
||||
label="Model Based"
|
||||
description="Use AI models for more accurate speech detection"
|
||||
/>
|
||||
|
||||
<!-- VAD Model Status -->
|
||||
<div v-if="useVADModel" class="mt-3 space-y-2">
|
||||
<div v-if="isLoadingVADModel" class="flex items-center gap-2 text-primary-600 dark:text-primary-400">
|
||||
<div class="animate-spin text-sm" i-solar:spinner-line-duotone />
|
||||
<span class="text-sm">Loading...</span>
|
||||
</div>
|
||||
|
||||
<ErrorContainer
|
||||
v-else-if="vadModelError"
|
||||
title="Inference error"
|
||||
:error="vadModelError"
|
||||
/>
|
||||
|
||||
<div v-else-if="isVADModelLoaded" class="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<div class="text-sm" i-solar:check-circle-bold-duotone />
|
||||
<span class="text-sm">Activated</span>
|
||||
<span class="ml-auto text-xs text-neutral-500">
|
||||
Probability: {{ (vadProbability * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice Activity Visualization (when VAD model is active) -->
|
||||
<TimeSeriesChart
|
||||
v-if="useVADModel && isVADModelLoaded"
|
||||
:history="vadHistory"
|
||||
:current-value="vadProbability"
|
||||
:threshold="vadThreshold"
|
||||
:is-active="isSpeaking"
|
||||
title="Voice Activity"
|
||||
subtitle="Last 2 seconds"
|
||||
active-label="Speaking"
|
||||
active-legend-label="Voice detected"
|
||||
inactive-legend-label="Silence"
|
||||
threshold-label="Speech threshold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Audio Playback (Monitor) -->
|
||||
<div v-if="isMonitoring" class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
|
||||
<FieldCheckbox
|
||||
v-model="enablePlayback"
|
||||
label="Monitor Audio (Listen)"
|
||||
description="Enable audio playback monitoring (like OBS). Be careful of feedback!"
|
||||
/>
|
||||
|
||||
<div v-if="enablePlayback" class="mt-3">
|
||||
<FieldRange
|
||||
v-model="monitorVolume"
|
||||
label="Monitor Volume"
|
||||
description="Control the volume of audio monitoring playback"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="5"
|
||||
:format-value="value => `${value}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning for playback -->
|
||||
<div v-if="enablePlayback" class="border border-amber-200 rounded-lg bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
|
||||
<div class="flex items-center gap-2 text-amber-700 dark:text-amber-300">
|
||||
<div class="text-sm" i-solar:warning-circle-bold-duotone />
|
||||
<span class="text-sm font-medium">Audio feedback warning</span>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
Use headphones to prevent audio feedback. Lower the monitor volume if you hear echoing.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useModulesList } from '@proj-airi/stage-ui/composables/use-modules-list'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const { modulesList } = useModulesList()
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:layers-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div grid="~ cols-1 sm:cols-2 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(module, index) of modulesList"
|
||||
:key="module.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="index * 50"
|
||||
:title="module.name"
|
||||
:description="module.description"
|
||||
:icon="module.icon"
|
||||
:icon-color="module.iconColor"
|
||||
:icon-image="module.iconImage"
|
||||
:to="module.to"
|
||||
:configured="module.configured"
|
||||
/>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:z-index="-1"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:layers-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,152 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import type { Tool } from '@proj-airi/tauri-plugin-mcp'
|
||||
|
||||
import { useMcpStore } from '@proj-airi/stage-ui/stores/mcp'
|
||||
// import { connectServer, disconnectServer, listTools } from '@proj-airi/tauri-plugin-mcp'
|
||||
import {
|
||||
FieldInput,
|
||||
} from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mcpStore = useMcpStore()
|
||||
const connecting = ref(false)
|
||||
|
||||
const {
|
||||
serverCmd,
|
||||
serverArgs,
|
||||
connected,
|
||||
} = storeToRefs(mcpStore)
|
||||
|
||||
interface Tool {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const tools = ref<Tool[]>([])
|
||||
|
||||
async function connect() {
|
||||
connecting.value = true
|
||||
try {
|
||||
// await connectServer(serverCmd.value, serverArgs.value.split(' '))
|
||||
connected.value = true
|
||||
}
|
||||
catch (e) {
|
||||
const error = e as string
|
||||
console.error(error)
|
||||
if (error.includes('already connected')) {
|
||||
connected.value = true
|
||||
}
|
||||
}
|
||||
finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
// await disconnectServer()
|
||||
connected.value = false
|
||||
tools.value = []
|
||||
}
|
||||
|
||||
async function getTools() {
|
||||
// tools.value = await listTools()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col md:row gap-6">
|
||||
<div flex="~ col gap-6" class="w-full md:w-[60%]">
|
||||
<div w-full rounded-xl>
|
||||
<div flex="~ col gap-4">
|
||||
<FieldInput
|
||||
v-model="serverCmd"
|
||||
type="text"
|
||||
label="Server Command"
|
||||
description="Enter the server command to run"
|
||||
placeholder="docker"
|
||||
:disabled="connecting || connected"
|
||||
/>
|
||||
|
||||
<FieldInput
|
||||
v-model="serverArgs"
|
||||
type="text"
|
||||
label="Server Arguments"
|
||||
description="Enter the server command arguments"
|
||||
placeholder="run -i --rm -e ADB_HOST=host.docker.internal ghcr.io/lemonnekogh/airi-android:v0.1.0"
|
||||
:disabled="connecting || connected"
|
||||
/>
|
||||
|
||||
<div flex="~ row" gap-4>
|
||||
<button
|
||||
v-if="!connected"
|
||||
border="neutral-800 dark:neutral-200 solid 2" transition="border duration-250 ease-in-out"
|
||||
rounded-lg px-4 text="neutral-100 dark:neutral-900" py-2 text-sm
|
||||
:disabled="connecting || !serverCmd || !serverArgs"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': connecting || !serverCmd || !serverArgs }"
|
||||
bg="neutral-700 dark:neutral-300" @click="connect"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:play-circle-bold-duotone />
|
||||
<span>Connect</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-else border="primary-300 dark:primary-800 solid 2"
|
||||
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="disconnect"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:stop-circle-bold-duotone />
|
||||
<span>Disconnect</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="connected"
|
||||
border="neutral-800 dark:neutral-200 solid 2" transition="border duration-250 ease-in-out"
|
||||
rounded-lg px-4 text="neutral-100 dark:neutral-900" py-2 text-sm
|
||||
:disabled="connecting"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': connecting }"
|
||||
bg="neutral-700 dark:neutral-300" @click="getTools"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div solar:list-arrow-down-line-duotone />
|
||||
<span>List Tools</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="tools.length > 0" flex="~ col gap-4">
|
||||
<div v-for="tool in tools" :key="tool.name" border="neutral-200 dark:neutral-800 solid 2" rounded-lg p-4>
|
||||
<div text="neutral-900 dark:neutral-100" text-sm>
|
||||
{{ tool.name }}
|
||||
</div>
|
||||
<div text="neutral-500 dark:neutral-400" text-xs>
|
||||
{{ tool.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:user-speak-rounded-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import { useServerStore } from '@proj-airi/stage-ui/stores/server'
|
||||
|
||||
// const serverStore = useServerStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,502 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ErrorContainer,
|
||||
RadioCardManySelect,
|
||||
RadioCardSimple,
|
||||
Skeleton,
|
||||
TestDummyMarker,
|
||||
VoiceCardManySelect,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import {
|
||||
FieldCheckbox,
|
||||
FieldInput,
|
||||
FieldRange,
|
||||
Textarea,
|
||||
} from '@proj-airi/ui'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const { configuredSpeechProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
activeSpeechVoice,
|
||||
activeSpeechVoiceId,
|
||||
pitch,
|
||||
isLoadingSpeechProviderVoices,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
modelSearchQuery,
|
||||
speechProviderError,
|
||||
ssmlEnabled,
|
||||
availableVoices,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const voiceSearchQuery = ref('')
|
||||
const useSSML = ref(false)
|
||||
const testText = ref('Hello, my name is AI Assistant')
|
||||
const ssmlText = ref('')
|
||||
const isGenerating = ref(false)
|
||||
const audioUrl = ref('')
|
||||
const audioPlayer = ref<HTMLAudioElement | null>(null)
|
||||
const errorMessage = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await providersStore.loadModelsForConfiguredProviders()
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
})
|
||||
|
||||
watchDebounced(activeSpeechProvider, async () => {
|
||||
await providersStore.loadModelsForConfiguredProviders()
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
}, { debounce: 100 })
|
||||
|
||||
// Function to generate speech
|
||||
async function generateTestSpeech() {
|
||||
if (!testText.value.trim() && !useSSML.value)
|
||||
return
|
||||
|
||||
if (useSSML.value && !ssmlText.value.trim())
|
||||
return
|
||||
|
||||
if (!activeSpeechModel.value) {
|
||||
console.error('No model selected')
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeSpeechVoice.value) {
|
||||
console.error('No voice selected')
|
||||
return
|
||||
}
|
||||
|
||||
const provider = await providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions<string, any>
|
||||
if (!provider) {
|
||||
console.error('Failed to initialize speech provider')
|
||||
return
|
||||
}
|
||||
|
||||
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
|
||||
|
||||
isGenerating.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// Stop any currently playing audio
|
||||
if (audioUrl.value) {
|
||||
stopTestAudio()
|
||||
}
|
||||
|
||||
const input = useSSML.value
|
||||
? ssmlText.value
|
||||
: speechStore.supportsSSML ? speechStore.generateSSML(testText.value, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value }) : testText.value
|
||||
|
||||
const response = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
input,
|
||||
voice: activeSpeechVoice.value.id,
|
||||
})
|
||||
|
||||
// Convert the response to a blob and create an object URL
|
||||
audioUrl.value = URL.createObjectURL(new Blob([response]))
|
||||
|
||||
// Play the audio
|
||||
setTimeout(() => {
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.play()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error generating speech:', error)
|
||||
errorMessage.value = error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
}
|
||||
finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Function to stop audio playback
|
||||
function stopTestAudio() {
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.pause()
|
||||
audioPlayer.value.currentTime = 0
|
||||
}
|
||||
|
||||
// Clean up the object URL to prevent memory leaks
|
||||
if (audioUrl.value) {
|
||||
URL.revokeObjectURL(audioUrl.value)
|
||||
audioUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up when component is unmounted
|
||||
onUnmounted(() => {
|
||||
if (audioUrl.value) {
|
||||
URL.revokeObjectURL(audioUrl.value)
|
||||
}
|
||||
})
|
||||
|
||||
function updateCustomVoiceName(value: string | undefined) {
|
||||
if (!value) {
|
||||
activeSpeechVoice.value = undefined
|
||||
return
|
||||
}
|
||||
activeSpeechVoice.value = {
|
||||
id: value,
|
||||
name: value,
|
||||
description: value,
|
||||
previewURL: value,
|
||||
languages: [{ code: 'en', title: 'English' }],
|
||||
provider: activeSpeechProvider.value,
|
||||
gender: 'male',
|
||||
}
|
||||
}
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
activeSpeechModel.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col md:row gap-6">
|
||||
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4" class="h-fit w-full md:w-[40%]">
|
||||
<div>
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
|
||||
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-500">
|
||||
<span>{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<fieldset
|
||||
v-if="configuredSpeechProvidersMetadata.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 configuredSpeechProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeSpeechProvider"
|
||||
name="speech-provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName || 'Unknown'"
|
||||
: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>
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeSpeechProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<ErrorContainer
|
||||
v-else-if="activeProviderModelError"
|
||||
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
|
||||
:error="activeProviderModelError"
|
||||
/>
|
||||
|
||||
<!-- No models available -->
|
||||
<Alert
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
type="warning"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Using the new RadioCardManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardManySelect
|
||||
v-model="activeSpeechModel"
|
||||
v-model:search-query="modelSearchQuery"
|
||||
:items="providerModels"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: modelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</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">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Skeleton class="w-full rounded-lg p-2.5 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<div flex="~ row gap-4">
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
<Skeleton class="w-full rounded-lg p-4 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
</div>
|
||||
<Skeleton class="w-full rounded-lg p-3 text-sm">
|
||||
<div class="h-1lh" />
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<!-- Voice selection with RadioCardManySelect -->
|
||||
<div
|
||||
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
|
||||
class="space-y-6"
|
||||
>
|
||||
<VoiceCardManySelect
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
v-model:voice-id="activeSpeechVoiceId"
|
||||
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.id,
|
||||
name: voice.name,
|
||||
description: voice.description,
|
||||
previewURL: voice.previewURL,
|
||||
customizable: false,
|
||||
}))"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
|
||||
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
|
||||
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
|
||||
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
|
||||
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
|
||||
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
|
||||
@update:custom-value="updateCustomVoiceName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorContainer
|
||||
v-else-if="speechProviderError"
|
||||
title="Error loading voices"
|
||||
:error="speechProviderError"
|
||||
/>
|
||||
|
||||
<!-- No voices available -->
|
||||
<Alert v-else type="warning">
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description') }}.
|
||||
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_hint') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Voice parameters -->
|
||||
<div flex="~ col gap-4">
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
label="Pitch"
|
||||
description="Tune the pitch of the voice"
|
||||
:min="-100" :max="100" :step="1"
|
||||
:format-value="value => `${value}%`"
|
||||
/>
|
||||
<!-- SSML Support -->
|
||||
<FieldCheckbox
|
||||
v-model="ssmlEnabled"
|
||||
label="Enable SSML"
|
||||
description="Enable Speech Synthesis Markup Language for more control over speech output"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual voice input when no voices are available -->
|
||||
<div
|
||||
v-if="!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0"
|
||||
class="mt-2 space-y-6"
|
||||
>
|
||||
<FieldInput
|
||||
type="text"
|
||||
label="Voice Name"
|
||||
description="Enter the voice name for your custom voice"
|
||||
placeholder="Enter voice name (e.g., 'Rachel', 'Josh')"
|
||||
@update:model-value="updateCustomVoiceName"
|
||||
/>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-6" class="w-full md:w-[60%]">
|
||||
<div w-full rounded-xl>
|
||||
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
|
||||
<div class="inline-flex items-center gap-4">
|
||||
<TestDummyMarker />
|
||||
<div>
|
||||
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div flex="~ col gap-4">
|
||||
<FieldCheckbox
|
||||
v-model="useSSML"
|
||||
label="Use Custom SSML"
|
||||
description="Enable to input raw SSML instead of plain text"
|
||||
/>
|
||||
|
||||
<template v-if="!useSSML">
|
||||
<Textarea
|
||||
v-model="testText" h-24
|
||||
w-full
|
||||
:placeholder="t('settings.pages.providers.provider.elevenlabs.playground.fields.field.input.placeholder')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<textarea
|
||||
v-model="ssmlText"
|
||||
placeholder="Enter SSML text..."
|
||||
border="neutral-100 dark:neutral-800 solid 2 focus:neutral-200 dark:focus:neutral-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
h-48 w-full rounded-lg px-3 py-2 text-sm font-mono outline-none
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div flex="~ row" gap-4>
|
||||
<button
|
||||
border="neutral-800 dark:neutral-200 solid 2" transition="border duration-250 ease-in-out"
|
||||
rounded-lg px-4 text="neutral-100 dark:neutral-900" py-2 text-sm
|
||||
:disabled="isGenerating || (!testText.trim() && !useSSML) || (useSSML && !ssmlText.trim()) || !activeSpeechVoice"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || (!testText.trim() && !useSSML) || (useSSML && !ssmlText.trim()) || !activeSpeechVoice }"
|
||||
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:play-circle-bold-duotone />
|
||||
<span>{{ isGenerating ? t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.generating') : t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.label') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
|
||||
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
|
||||
>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<div i-solar:stop-circle-bold-duotone />
|
||||
<span>Stop</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:user-speak-rounded-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,163 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const {
|
||||
allChatProvidersMetadata,
|
||||
allAudioSpeechProvidersMetadata,
|
||||
allAudioTranscriptionProvidersMetadata,
|
||||
} = storeToRefs(providersStore)
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:box-minimalistic-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div mb-6 flex flex-col gap-5>
|
||||
<div bg="primary-500/10 dark:primary-800/25" rounded-lg p-4>
|
||||
<div mb-2 text-xl font-normal text="primary-800 dark:primary-100">
|
||||
First time here?
|
||||
</div>
|
||||
<div text="primary-700 dark:primary-300">
|
||||
AIRI requires at least one <div bg="primary-500/10 dark:primary-800/25" inline-flex items-center gap-1 rounded-lg px-2 py-0.5 translate-y="[0.25lh]">
|
||||
<div i-solar:chat-square-like-bold-duotone /><strong font-normal>Chat</strong>
|
||||
</div> provider to be configured to think, and behave properly. You could think of
|
||||
it as the brain of the characters living in AIRI system.
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row items-center gap-2">
|
||||
<div i-solar:chat-square-like-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
||||
<div>
|
||||
<div>
|
||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.</span>
|
||||
</div>
|
||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
||||
<div>
|
||||
Chat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(provider, index) of allChatProvidersMetadata"
|
||||
:key="provider.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="index * 50"
|
||||
:title="provider.localizedName || 'Unknown'"
|
||||
:description="provider.localizedDescription"
|
||||
:icon="provider.icon"
|
||||
:icon-color="provider.iconColor"
|
||||
:icon-image="provider.iconImage"
|
||||
:to="`/settings/providers/${provider.id}`"
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
<div flex="~ row items-center gap-2" my-5>
|
||||
<div i-solar:user-speak-rounded-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
||||
<div>
|
||||
<div>
|
||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Speech (text-to-speech) model providers. e.g. ElevenLabs, Azure Speech.</span>
|
||||
</div>
|
||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
||||
<div>
|
||||
Speech
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(provider, index) of allAudioSpeechProvidersMetadata"
|
||||
:key="provider.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="(allChatProvidersMetadata.length + index) * 50"
|
||||
:title="provider.localizedName || 'Unknown'"
|
||||
:description="provider.localizedDescription"
|
||||
:icon="provider.icon"
|
||||
:icon-color="provider.iconColor"
|
||||
:icon-image="provider.iconImage"
|
||||
:to="`/settings/providers/${provider.id}`"
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
<div flex="~ row items-center gap-2" my-5>
|
||||
<div i-solar:microphone-3-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
||||
<div>
|
||||
<div>
|
||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Transcription (speech-to-text) model providers. e.g. Whisper.cpp, OpenAI, Azure Speech</span>
|
||||
</div>
|
||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
||||
<div>
|
||||
Transcription
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
||||
<IconStatusItem
|
||||
v-for="(provider, index) of allAudioTranscriptionProvidersMetadata"
|
||||
:key="provider.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + index * 10"
|
||||
:delay="(allChatProvidersMetadata.length + allAudioSpeechProvidersMetadata.length + index) * 50"
|
||||
:title="provider.localizedName || 'Unknown'"
|
||||
:description="provider.localizedDescription"
|
||||
:icon="provider.icon"
|
||||
:icon-color="provider.iconColor"
|
||||
:icon-image="provider.iconImage"
|
||||
:to="`/settings/providers/${provider.id}`"
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-500/5 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:box-minimalistic-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:armchair-2-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Callout
|
||||
label="In development, needs your help!"
|
||||
theme="orange"
|
||||
>
|
||||
<div>
|
||||
This functionality is still under development. If you have any suggestions or would like to contribute, please reach out to us on our <a underline decoration-dotted href="https://github.com/moeru-ai/airi/issues">GitHub issues page</a>.
|
||||
The source code of this page is located at <a underline decoration-dotted href="https://github.com/moeru-ai/airi/tree/main/apps/stage-tamagotchi/src/pages/settings/scene/index.vue">here</a>.
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:armchair-2-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,80 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { all } from '@proj-airi/i18n'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { FieldCheckbox, FieldSelect } from '@proj-airi/ui'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settings = useSettings()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { allowVisibleOnAllWorkspaces } = storeToRefs(settings)
|
||||
|
||||
const dark = useDark()
|
||||
|
||||
const languages = computed(() => {
|
||||
return Object.entries(all).map(([value, label]) => ({ label, value }))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800 flex="~ col gap-4">
|
||||
<FieldCheckbox
|
||||
v-model="dark"
|
||||
v-motion
|
||||
mb-2
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (2 * 10)"
|
||||
:delay="2 * 50"
|
||||
:label="t('settings.theme.title')"
|
||||
:description="t('settings.theme.description')"
|
||||
/>
|
||||
<FieldCheckbox
|
||||
v-model="allowVisibleOnAllWorkspaces"
|
||||
v-motion
|
||||
mb-2
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (2 * 10)"
|
||||
:delay="2 * 50"
|
||||
:label="t('tamagotchi.settings.allow-visible-on-all-workspaces.title')"
|
||||
:description="t('tamagotchi.settings.allow-visible-on-all-workspaces.description')"
|
||||
/>
|
||||
|
||||
<!-- Language Setting -->
|
||||
<FieldSelect
|
||||
v-model="settings.language"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (3 * 10)"
|
||||
:delay="3 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
:label="t('settings.language.title')"
|
||||
:description="t('settings.language.description')"
|
||||
:options="languages"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:emoji-funny-square-bold-duotone />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -109,7 +109,7 @@ const { t } = useI18n()
|
||||
|
||||
<Section
|
||||
v-motion
|
||||
mb-2 :title="t('settings.pages.themes.sections.section.theme-presets.title')"
|
||||
mb-2 :title="t('settings.pages.system.sections.section.theme-presets.title')"
|
||||
icon="i-solar:magic-stick-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
|
||||
@@ -86,8 +86,8 @@ useEventListener('click', (e) => {
|
||||
|
||||
const pressKeysMessage = computed(() => {
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return t('tamagotchi.settings.pages.themes.window-shortcuts.press-keys')
|
||||
return `${t('tamagotchi.settings.pages.themes.window-shortcuts.press-keys')}: ${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
return t('tamagotchi.settings.pages.system.window-shortcuts.press-keys')
|
||||
return `${t('tamagotchi.settings.pages.system.window-shortcuts.press-keys')}: ${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
})
|
||||
function isConflict(shortcut: typeof shortcuts.value[0]) {
|
||||
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
|
||||
|
||||
const shortcuts = ref([
|
||||
{
|
||||
name: 'tamagotchi.settings.pages.themes.window-shortcuts.toggle-move.label',
|
||||
name: 'tamagotchi.settings.pages.system.window-shortcuts.toggle-move.label',
|
||||
shortcut: useVersionedLocalStorage('shortcuts/window/move', 'Shift+Alt+N', { defaultVersion: '1.0.2', satisfiesVersionBy: v => v === '1.0.2', onVersionMismatch: () => ({ action: 'reset' }) }), // Shift + Alt + N
|
||||
group: 'window',
|
||||
type: 'move',
|
||||
@@ -80,7 +80,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tamagotchi.settings.pages.themes.window-shortcuts.toggle-resize.label',
|
||||
name: 'tamagotchi.settings.pages.system.window-shortcuts.toggle-resize.label',
|
||||
shortcut: useVersionedLocalStorage('shortcuts/window/resize', 'Shift+Alt+A', { defaultVersion: '1.0.2', satisfiesVersionBy: v => v === '1.0.2', onVersionMismatch: () => ({ action: 'reset' }) }), // Shift + Alt + A
|
||||
group: 'window',
|
||||
type: 'resize',
|
||||
@@ -89,7 +89,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tamagotchi.settings.pages.themes.window-shortcuts.toggle-ignore-mouse-event.label',
|
||||
name: 'tamagotchi.settings.pages.system.window-shortcuts.toggle-ignore-mouse-event.label',
|
||||
shortcut: useVersionedLocalStorage('shortcuts/window/debug', 'Shift+Alt+I', { defaultVersion: '1.0.2', satisfiesVersionBy: v => v === '1.0.2', onVersionMismatch: () => ({ action: 'reset' }) }), // Shift + Alt + I
|
||||
group: 'window',
|
||||
type: 'ignore-mouse-event',
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"@date-fns/utc": "^2.1.1",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@huggingface/transformers": "^3.7.3",
|
||||
"@llama-flow/core": "^0.4.4",
|
||||
"@moeru/std": "catalog:",
|
||||
"@nekopaw/tempora": "0.3.1-alpha.1",
|
||||
"@proj-airi/audio": "workspace:^",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
if (!e)
|
||||
return
|
||||
|
||||
const file = fileInputRef.value?.files?.[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
const audioElem = document.createElement('audio')
|
||||
containerRef.value?.appendChild(audioElem)
|
||||
|
||||
audioElem.src = URL.createObjectURL(file)
|
||||
audioElem.controls = true
|
||||
audioElem.load()
|
||||
audioElem.play()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div ref="containerRef" />
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,184 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AssistantMessage, Message } from '@xsai/shared-chat'
|
||||
|
||||
import { createWorkflow, workflowEvent } from '@llama-flow/core'
|
||||
import { withValidation } from '@llama-flow/core/middleware/validation'
|
||||
import { runWorkflow } from '@llama-flow/core/stream/run'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { streamText } from '@xsai/stream-text'
|
||||
import { ref, toRaw } from 'vue'
|
||||
|
||||
const baseUrl = useLocalStorage('settings/llm/baseUrl', 'https://openrouter.ai/api/v1/')
|
||||
const apiKey = useLocalStorage('settings/llm/apiKey', '')
|
||||
const model = useLocalStorage('settings/llm/model', 'openai/gpt-4o-mini')
|
||||
const sendingMessage = ref('')
|
||||
const messages = ref<Message[]>([])
|
||||
const streamingMessage = ref<AssistantMessage>({ role: 'assistant', content: '' })
|
||||
const loading = ref(false)
|
||||
|
||||
const sendingEvent = workflowEvent<void, 'sending'>()
|
||||
const tokenEvent = workflowEvent<string, 'token'>()
|
||||
const textEvent = workflowEvent<string, 'text'>()
|
||||
const sentenceEvent = workflowEvent<string, 'sentence'>()
|
||||
const doneEvent = workflowEvent<void, 'done'>()
|
||||
|
||||
async function handleChatSendMessage() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const streamWorkflow = withValidation(createWorkflow(), [
|
||||
[[sendingEvent], [tokenEvent, doneEvent]],
|
||||
[[tokenEvent], [textEvent]],
|
||||
[[textEvent], [sentenceEvent]],
|
||||
])
|
||||
|
||||
streamWorkflow.handle([sendingEvent], async () => {
|
||||
const { sendEvent } = streamWorkflow.createContext()
|
||||
|
||||
streamingMessage.value = { role: 'assistant', content: '' }
|
||||
messages.value.push({ role: 'user', content: sendingMessage.value })
|
||||
messages.value.push(streamingMessage.value)
|
||||
|
||||
const response = await streamText({
|
||||
baseURL: baseUrl.value,
|
||||
apiKey: apiKey.value,
|
||||
model: model.value,
|
||||
messages: messages.value.slice(0, messages.value.length - 1).map(msg => toRaw(msg)),
|
||||
})
|
||||
|
||||
for await (const chunk of response.fullStream) {
|
||||
if (chunk.type === 'text-delta')
|
||||
sendEvent(tokenEvent.with(chunk.text || ''))
|
||||
}
|
||||
|
||||
return doneEvent.with()
|
||||
})
|
||||
|
||||
streamWorkflow.handle([tokenEvent], async (token) => {
|
||||
if (!streamingMessage.value.content)
|
||||
streamingMessage.value.content = token.data
|
||||
else
|
||||
streamingMessage.value.content += token.data
|
||||
})
|
||||
|
||||
await runWorkflow(streamWorkflow, sendingEvent.with(), doneEvent)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// function useMessageTerminationWorkflow(parentWorkflow: WithValidationWorkflow<[[[typeof textEvent], [typeof sentenceEvent]]]>) {
|
||||
// let processed = ''
|
||||
|
||||
// parentWorkflow.handle([textEvent], async (sendEvent, text) => {
|
||||
// const endMarker = /[.?!]/
|
||||
// processed += text.data
|
||||
|
||||
// while (processed) {
|
||||
// const endMarkerExecArray = endMarker.exec(processed)
|
||||
// if (!endMarkerExecArray || typeof endMarkerExecArray.index === 'undefined')
|
||||
// break
|
||||
|
||||
// const before = processed.slice(0, endMarkerExecArray.index + 1)
|
||||
// const after = processed.slice(endMarkerExecArray.index + 1)
|
||||
|
||||
// sendEvent(sentenceEvent.with(before))
|
||||
// processed = after
|
||||
// }
|
||||
// })
|
||||
|
||||
// parentWorkflow.handle([doneEvent], async () => {
|
||||
// const { sendEvent } = getContext()
|
||||
|
||||
// const content = processed.trim()
|
||||
// if (content)
|
||||
// sendEvent(sentenceEvent.with(content))
|
||||
|
||||
// processed = ''
|
||||
// })
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2>
|
||||
<!-- <h2 text-xl>
|
||||
Storage
|
||||
</h2> -->
|
||||
<div flex="~ col" gap-2>
|
||||
<div flex flex-col gap-2>
|
||||
<div>
|
||||
<span text-neutral-500 dark:text-neutral-400>LLM</span>
|
||||
</div>
|
||||
<div grid grid-cols-2 gap-2>
|
||||
<label flex items-center gap-2>
|
||||
<span text-nowrap>
|
||||
Base URL
|
||||
</span>
|
||||
<input
|
||||
v-model="baseUrl"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out" text="disabled:neutral-400 dark:disabled:neutral-600"
|
||||
cursor="disabled:not-allowed" w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none shadow="sm"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
>
|
||||
</label>
|
||||
<label flex items-center gap-2>
|
||||
<span text-nowrap>
|
||||
API Key
|
||||
</span>
|
||||
<input
|
||||
v-model="apiKey"
|
||||
type="password"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out" text="disabled:neutral-400 dark:disabled:neutral-600"
|
||||
cursor="disabled:not-allowed" w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none shadow="sm"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
>
|
||||
</label>
|
||||
<label flex items-center gap-2>
|
||||
<span text-nowrap>
|
||||
Model
|
||||
</span>
|
||||
<input
|
||||
v-model="model"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out" text="disabled:neutral-400 dark:disabled:neutral-600"
|
||||
cursor="disabled:not-allowed" w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none shadow="sm"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
v-model="sendingMessage"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out" text="disabled:neutral-400 dark:disabled:neutral-600"
|
||||
cursor="disabled:not-allowed" w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none shadow="sm"
|
||||
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
|
||||
/>
|
||||
</div>
|
||||
<button rounded-lg bg="blue-100 dark:blue-900" px-4 py-2 @click="handleChatSendMessage">
|
||||
Send
|
||||
</button>
|
||||
<div>
|
||||
<div v-for="(message, index) of messages" :key="index">
|
||||
<div v-if="message.role === 'user'">
|
||||
<span>
|
||||
{{ message.content }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="message.role === 'assistant'">
|
||||
<span>
|
||||
{{ message.content }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,149 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { sleep } from '@moeru/std'
|
||||
import { createQueue } from '@proj-airi/stage-ui/utils/queue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const temp = ref<string>('')
|
||||
|
||||
const audioQueue = createQueue<string>({
|
||||
handlers: [
|
||||
async (text) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ready to play speech audio for', text)
|
||||
},
|
||||
],
|
||||
})
|
||||
const ttsQueue = createQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ready to stream speech audio for', ctx)
|
||||
audioQueue.enqueue(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
const textQueue = createQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
const endMarker = ['.', '?', '!']
|
||||
|
||||
let newEndPartDiscovered = false
|
||||
|
||||
for (const marker of endMarker) {
|
||||
if (!ctx.data.includes(marker))
|
||||
continue
|
||||
|
||||
// find the end of the sentence and push it to the queue with temp
|
||||
const periodIndex = ctx.data.indexOf(marker)
|
||||
// split
|
||||
const beforePeriod = ctx.data.slice(0, periodIndex + 1)
|
||||
const afterPeriod = ctx.data.slice(periodIndex + 1)
|
||||
|
||||
temp.value += beforePeriod
|
||||
ttsQueue.enqueue(temp.value.trim())
|
||||
temp.value = afterPeriod
|
||||
|
||||
newEndPartDiscovered = true
|
||||
}
|
||||
|
||||
if (!newEndPartDiscovered)
|
||||
temp.value += ctx.data
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const textParts = [
|
||||
'Hello',
|
||||
' N',
|
||||
'eko',
|
||||
'! I',
|
||||
' am',
|
||||
' an',
|
||||
' AI',
|
||||
' assistant',
|
||||
' trained',
|
||||
' to',
|
||||
' help',
|
||||
' with',
|
||||
' a',
|
||||
' variety',
|
||||
' of',
|
||||
' tasks',
|
||||
' such',
|
||||
' as',
|
||||
' answering',
|
||||
' questions',
|
||||
',',
|
||||
' providing',
|
||||
' information',
|
||||
',',
|
||||
' giving',
|
||||
' recommendations',
|
||||
',',
|
||||
' and',
|
||||
' more',
|
||||
'. How',
|
||||
' can',
|
||||
' I',
|
||||
' assist',
|
||||
' you',
|
||||
' today',
|
||||
'?',
|
||||
'Hello',
|
||||
' N',
|
||||
'eko',
|
||||
',',
|
||||
' I',
|
||||
' am',
|
||||
' an',
|
||||
' AI',
|
||||
' assistant',
|
||||
'.',
|
||||
' I',
|
||||
' can',
|
||||
' help',
|
||||
' answer',
|
||||
' questions',
|
||||
',',
|
||||
' provide',
|
||||
' information',
|
||||
',',
|
||||
' assist',
|
||||
' with',
|
||||
' tasks',
|
||||
',',
|
||||
' and',
|
||||
' engage',
|
||||
' in',
|
||||
' conversation',
|
||||
'.',
|
||||
' How',
|
||||
' can',
|
||||
' I',
|
||||
' assist',
|
||||
' you',
|
||||
' today',
|
||||
'?',
|
||||
]
|
||||
|
||||
async function mockTextPartsStreamHandler() {
|
||||
for (const part of textParts) {
|
||||
await sleep(100)
|
||||
textQueue.enqueue(part)
|
||||
}
|
||||
}
|
||||
|
||||
async function handler() {
|
||||
mockTextPartsStreamHandler()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handler()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Style from ui/InputFile component, may be centralized later -->
|
||||
<div
|
||||
relative
|
||||
class="min-h-[120px] flex flex-col cursor-pointer items-center justify-center border-neutral-200 rounded-xl bg-white/60 p-6 dark:border-neutral-700 hover:border-primary-300 dark:bg-black/30 hover:bg-white/80 dark:hover:border-primary-700 dark:hover:bg-black/40"
|
||||
border="solid 2"
|
||||
transition="all duration-300"
|
||||
cursor-pointer opacity-95
|
||||
hover="scale-100 opacity-100 shadow-md dark:shadow-lg"
|
||||
>
|
||||
<div i-solar:add-square-line-duotone mb-4 text-5xl text="neutral-400 dark:neutral-500" />
|
||||
<p font-medium text="neutral-600 dark:neutral-300">
|
||||
{{ t('settings.pages.card.create_card') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,281 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Card } from '@proj-airi/ccc'
|
||||
|
||||
import kebabcase from '@stdlib/string-base-kebabcase'
|
||||
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { FieldInput, FieldValues } from '@proj-airi/ui'
|
||||
import {
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from 'reka-ui'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
|
||||
// Tab type definition
|
||||
interface Tab {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// Active tab ID state
|
||||
const activeTabId = ref('')
|
||||
|
||||
// Tabs for card details
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'identity', label: t('settings.pages.card.creation.identity'), icon: 'i-solar:emoji-funny-square-bold-duotone' },
|
||||
{ id: 'behavior', label: t('settings.pages.card.creation.behavior'), icon: 'i-solar:chat-round-line-bold-duotone' },
|
||||
{ id: 'settings', label: t('settings.pages.card.creation.settings'), icon: 'i-solar:settings-bold-duotone' },
|
||||
]
|
||||
|
||||
// Active tab state - set to first available tab by default
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
// If current active tab is not in available tabs, reset to first tab
|
||||
if (!tabs.find(tab => tab.id === activeTabId.value))
|
||||
return tabs[0]?.id || ''
|
||||
return activeTabId.value
|
||||
},
|
||||
set: (value: string) => {
|
||||
activeTabId.value = value
|
||||
},
|
||||
})
|
||||
|
||||
// Check for errors, and save built Cards :
|
||||
|
||||
const showError = ref<boolean>(false)
|
||||
const errorMessage = ref<string>('')
|
||||
|
||||
function saveCard(card: Card): boolean {
|
||||
// Before saving, let's validate what the user entered :
|
||||
const rawCard: Card = toRaw(card)
|
||||
|
||||
if (!(rawCard.name!.length > 0)) { // ! is used, since a default value is provided, and computed values passed to v-model should never be undefined
|
||||
// No name
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.name')
|
||||
return false
|
||||
}
|
||||
else if (!/^(?:\d+\.)+\d+$/.test(rawCard.version)) {
|
||||
// Invalid version
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.version')
|
||||
return false
|
||||
}
|
||||
else if (!(rawCard.description!.length > 0)) {
|
||||
// No description
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.description')
|
||||
return false
|
||||
}
|
||||
else if (!(rawCard.personality!.length > 0)) {
|
||||
// No personality
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.personality')
|
||||
return false
|
||||
}
|
||||
else if (!(rawCard.scenario!.length > 0)) {
|
||||
// No Scenario
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.scenario')
|
||||
return false
|
||||
}
|
||||
else if (!(rawCard.systemPrompt!.length > 0)) {
|
||||
// No sys prompt
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.systemprompt')
|
||||
return false
|
||||
}
|
||||
else if (!(rawCard.postHistoryInstructions!.length > 0)) {
|
||||
// No post history prompt
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.posthistoryinstructions')
|
||||
return false
|
||||
}
|
||||
showError.value = false
|
||||
|
||||
cardStore.addCard(rawCard)
|
||||
modelValue.value = false // Close this
|
||||
return true
|
||||
}
|
||||
|
||||
// Cards data holders :
|
||||
|
||||
const card = ref<Card>({
|
||||
name: t('settings.pages.card.creation.defaults.name'),
|
||||
nickname: undefined,
|
||||
version: '1.0',
|
||||
description: '',
|
||||
notes: undefined,
|
||||
personality: t('settings.pages.card.creation.defaults.personality'),
|
||||
scenario: t('settings.pages.card.creation.defaults.scenario'),
|
||||
systemPrompt: t('settings.pages.card.creation.defaults.systemprompt'),
|
||||
postHistoryInstructions: t('settings.pages.card.creation.defaults.posthistoryinstructions'),
|
||||
greetings: [],
|
||||
messageExample: [],
|
||||
})
|
||||
|
||||
function makeComputed<T extends keyof Card>(
|
||||
/*
|
||||
Function used to generate Computed values, with an optional sanitize function
|
||||
*/
|
||||
key: T,
|
||||
transform?: (input: string) => string,
|
||||
) {
|
||||
return computed({
|
||||
get: () => {
|
||||
return card.value[key] ?? ''
|
||||
},
|
||||
set: (val: string) => { // Set,
|
||||
const input = val.trim() // We first trim the value
|
||||
card.value[key] = (input.length > 0
|
||||
? (transform ? transform(input) : input) // then potentially transform it
|
||||
: '') as Card[T]// or default to empty string value if nothing was given
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const cardName = makeComputed('name', input => kebabcase(input))
|
||||
const cardNickname = makeComputed('nickname')
|
||||
const cardDescription = makeComputed('description')
|
||||
const cardNotes = makeComputed('notes')
|
||||
|
||||
const cardPersonality = makeComputed('personality')
|
||||
const cardScenario = makeComputed('scenario')
|
||||
const cardGreetings = computed({
|
||||
get: () => card.value.greetings ?? [],
|
||||
set: (val: string[]) => {
|
||||
card.value.greetings = val || []
|
||||
},
|
||||
})
|
||||
|
||||
const cardVersion = makeComputed('version')
|
||||
const cardSystemPrompt = makeComputed('systemPrompt')
|
||||
const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="modelValue" @update:open="emit('update:modelValue', $event)">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 z-100 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
|
||||
<DialogContent class="fixed left-1/2 top-1/2 z-100 m-0 max-h-[90vh] max-w-6xl w-[92vw] flex flex-col overflow-auto border border-neutral-200 rounded-xl bg-white p-5 shadow-xl 2xl:w-[60vw] lg:w-[80vw] md:w-[85vw] xl:w-[70vw] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:border-neutral-700 dark:bg-neutral-800 sm:p-6">
|
||||
<div class="w-full flex flex-col gap-5">
|
||||
<DialogTitle text-2xl font-normal class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
|
||||
{{ t("settings.pages.card.create_card") }}
|
||||
</DialogTitle>
|
||||
|
||||
<!-- Dialog tabs -->
|
||||
<div class="mt-4">
|
||||
<div class="border-b border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex justify-center -mb-px sm:justify-start space-x-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="px-4 py-2 text-sm font-medium"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500 dark:border-primary-400'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300',
|
||||
]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div :class="tab.icon" />
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error div -->
|
||||
<div v-if="showError" class="w-full rounded-xl bg-red900">
|
||||
<p class="w-full p-4">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actual content -->
|
||||
<!-- Identity details -->
|
||||
<div v-if="activeTab === 'identity'" class="tab-content ml-auto mr-auto w-95%">
|
||||
<p class="mb-3">
|
||||
{{ t('settings.pages.card.creation.fields_info.subtitle') }}
|
||||
</p>
|
||||
|
||||
<div class="input-list ml-auto mr-auto w-90% flex flex-row flex-wrap justify-center gap-8">
|
||||
<FieldInput v-model="cardName" :label="t('settings.pages.card.creation.name')" :description="t('settings.pages.card.creation.fields_info.name')" :required="true" />
|
||||
<FieldInput v-model="cardNickname" :label="t('settings.pages.card.creation.nickname')" :description="t('settings.pages.card.creation.fields_info.nickname')" />
|
||||
<FieldInput v-model="cardDescription" :label="t('settings.pages.card.creation.description')" :single-line="false" :required="true" :description="t('settings.pages.card.creation.fields_info.description')" />
|
||||
<FieldInput v-model="cardNotes" :label="t('settings.pages.card.creator_notes')" :single-line="false" :description="t('settings.pages.card.creation.fields_info.notes')" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Behavior -->
|
||||
<div v-else-if="activeTab === 'behavior'" class="tab-content ml-auto mr-auto w-95%">
|
||||
<div class="input-list ml-auto mr-auto w-90% flex flex-row flex-wrap justify-center gap-8">
|
||||
<FieldInput v-model="cardPersonality" :label="t('settings.pages.card.personality')" :single-line="false" :required="true" :description="t('settings.pages.card.creation.fields_info.personality')" />
|
||||
<FieldInput v-model="cardScenario" :label="t('settings.pages.card.scenario')" :single-line="false" :required="true" :description="t('settings.pages.card.creation.fields_info.scenario')" />
|
||||
<FieldValues v-model="cardGreetings" :label="t('settings.pages.card.creation.greetings')" :description="t('settings.pages.card.creation.fields_info.greetings')" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Settings -->
|
||||
<div v-else-if="activeTab === 'settings'" class="tab-content ml-auto mr-auto w-95%">
|
||||
<div class="input-list ml-auto mr-auto w-90% flex flex-row flex-wrap justify-center gap-8">
|
||||
<FieldInput v-model="cardSystemPrompt" :label="t('settings.pages.card.systemprompt')" :single-line="false" :required="true" :description="t('settings.pages.card.creation.fields_info.systemprompt')" />
|
||||
<FieldInput v-model="cardPostHistoryInstructions" :label="t('settings.pages.card.posthistoryinstructions')" :single-line="false" :required="true" :description="t('settings.pages.card.creation.fields_info.posthistoryinstructions')" />
|
||||
<FieldInput v-model="cardVersion" :label="t('settings.pages.card.creation.version')" :required="true" :description="t('settings.pages.card.creation.fields_info.version')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto mr-1 flex flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="i-solar:undo-left-bold-duotone"
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
:disabled="false"
|
||||
@click="modelValue = false"
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon="i-solar:check-circle-bold-duotone"
|
||||
:label="t('settings.pages.card.creation.create')"
|
||||
:disabled="false"
|
||||
@click="saveCard(card)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-list > * {
|
||||
min-width: 45%;
|
||||
}
|
||||
|
||||
@media (max-width: 641px) {
|
||||
.input-list * {
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,354 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiriCard } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DeleteCardDialog from './DeleteCardDialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
cardId: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
const { removeCard } = cardStore
|
||||
const { activeCardId } = storeToRefs(cardStore)
|
||||
|
||||
// Get selected card data
|
||||
const selectedCard = computed<AiriCard | undefined>(() => {
|
||||
if (!props.cardId)
|
||||
return undefined
|
||||
return cardStore.getCard(props.cardId)
|
||||
})
|
||||
|
||||
// Get module settings
|
||||
const moduleSettings = computed(() => {
|
||||
if (!selectedCard.value || !selectedCard.value.extensions?.airi?.modules) {
|
||||
return {
|
||||
consciousness: '',
|
||||
speech: '',
|
||||
voice: '',
|
||||
}
|
||||
}
|
||||
|
||||
const airiExt = selectedCard.value.extensions.airi.modules
|
||||
return {
|
||||
consciousness: airiExt.consciousness?.model || '',
|
||||
speech: airiExt.speech?.model || '',
|
||||
voice: airiExt.speech?.voice_id || '',
|
||||
}
|
||||
})
|
||||
|
||||
// Get character settings
|
||||
const characterSettings = computed(() => {
|
||||
if (!selectedCard.value)
|
||||
return {}
|
||||
|
||||
return {
|
||||
personality: selectedCard.value.personality,
|
||||
scenario: selectedCard.value.scenario,
|
||||
systemPrompt: selectedCard.value.systemPrompt,
|
||||
postHistoryInstructions: selectedCard.value.postHistoryInstructions,
|
||||
}
|
||||
})
|
||||
|
||||
// Check if card is active
|
||||
const isActive = computed(() => props.cardId === activeCardId.value)
|
||||
|
||||
// Animation control for card activation
|
||||
const isActivating = ref(false)
|
||||
|
||||
function handleActivate() {
|
||||
isActivating.value = true
|
||||
setTimeout(() => {
|
||||
activeCardId.value = props.cardId
|
||||
isActivating.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function highlightTagToHtml(text: string) {
|
||||
return DOMPurify.sanitize(text?.replace(/\{\{(.*?)\}\}/g, '<span class="bg-primary-500/20 inline-block">{{ $1 }}</span>').trim())
|
||||
}
|
||||
|
||||
// Delete confirmation
|
||||
const showDeleteConfirm = ref(false)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
if (selectedCard.value) {
|
||||
removeCard(props.cardId)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
showDeleteConfirm.value = false
|
||||
}
|
||||
|
||||
// Tab type definition
|
||||
interface Tab {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// Active tab ID state
|
||||
const activeTabId = ref('')
|
||||
|
||||
// Tabs for card details
|
||||
const tabs = computed<Tab[]>(() => {
|
||||
const availableTabs: Tab[] = []
|
||||
|
||||
// Description tab - always show if there's description
|
||||
if (selectedCard.value?.description) {
|
||||
availableTabs.push({
|
||||
id: 'description',
|
||||
label: t('settings.pages.card.description_label'),
|
||||
icon: 'i-solar:document-text-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Notes tab - only show if there are creator notes
|
||||
if (selectedCard.value?.notes) {
|
||||
availableTabs.push({
|
||||
id: 'notes',
|
||||
label: t('settings.pages.card.creator_notes'),
|
||||
icon: 'i-solar:notes-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Character tab - only show if there are character settings
|
||||
if (Object.values(characterSettings.value).some(value => !!value)) {
|
||||
availableTabs.push({
|
||||
id: 'character',
|
||||
label: t('settings.pages.card.character'),
|
||||
icon: 'i-solar:user-rounded-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Modules tab - always show
|
||||
availableTabs.push({
|
||||
id: 'modules',
|
||||
label: t('settings.pages.card.modules'),
|
||||
icon: 'i-solar:tuning-square-linear',
|
||||
})
|
||||
|
||||
return availableTabs
|
||||
})
|
||||
|
||||
// Active tab state - set to first available tab by default
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
// If current active tab is not in available tabs, reset to first tab
|
||||
if (!tabs.value.find(tab => tab.id === activeTabId.value))
|
||||
return tabs.value[0]?.id || ''
|
||||
return activeTabId.value
|
||||
},
|
||||
set: (value: string) => {
|
||||
activeTabId.value = value
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="modelValue" @update:open="emit('update:modelValue', $event)">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 z-100 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
|
||||
<DialogContent class="fixed left-1/2 top-1/2 z-100 m-0 max-h-[90vh] max-w-6xl w-[92vw] flex flex-col overflow-auto border border-neutral-200 rounded-xl bg-white p-5 shadow-xl 2xl:w-[60vw] lg:w-[80vw] md:w-[85vw] xl:w-[70vw] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:border-neutral-700 dark:bg-neutral-800 sm:p-6">
|
||||
<div v-if="selectedCard" class="w-full flex flex-col gap-5">
|
||||
<!-- Header with status indicator -->
|
||||
<div flex="~ col" gap-3>
|
||||
<div flex="~ row" items-center justify-between>
|
||||
<div>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<DialogTitle text-2xl font-normal class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
|
||||
{{ selectedCard.name }}
|
||||
</DialogTitle>
|
||||
<div v-if="isActive" class="flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-xs text-primary-600 font-medium dark:bg-primary-900/40 dark:text-primary-400">
|
||||
<div i-solar:check-circle-bold-duotone text-xs />
|
||||
{{ t('settings.pages.card.active_badge') }}
|
||||
</div>
|
||||
</div>
|
||||
<div mt-1 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
v{{ selectedCard.version }}
|
||||
<template v-if="selectedCard.creator">
|
||||
· {{ t('settings.pages.card.created_by') }} <span font-medium>{{ selectedCard.creator }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div flex="~ row" gap-2>
|
||||
<!-- Activation button -->
|
||||
<Button
|
||||
variant="primary"
|
||||
:icon="isActive ? 'i-solar:check-circle-bold-duotone' : 'i-solar:play-circle-broken'"
|
||||
:label="isActive ? t('settings.pages.card.active') : t('settings.pages.card.activate')"
|
||||
:disabled="isActive"
|
||||
:class="{ 'animate-pulse': isActivating }"
|
||||
@click="handleActivate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card content tabs -->
|
||||
<div class="mt-4">
|
||||
<div class="border-b border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex justify-center -mb-px sm:justify-start space-x-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="px-4 py-2 text-sm font-medium"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500 dark:border-primary-400'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300',
|
||||
]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div :class="tab.icon" />
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator notes -->
|
||||
<div v-if="activeTab === 'notes' && selectedCard.notes">
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
max-h-60 overflow-auto whitespace-pre-line rounded-lg p-4 text-neutral-700 sm:max-h-80 dark:text-neutral-300 transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
v-html="highlightTagToHtml(selectedCard.notes)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description section -->
|
||||
<div v-if="activeTab === 'description' && selectedCard.description">
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
max-h-60 overflow-auto whitespace-pre-line rounded-lg p-4 sm:max-h-80
|
||||
text="neutral-600 dark:neutral-300"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
v-html="highlightTagToHtml(selectedCard.description)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Character -->
|
||||
<div v-if="activeTab === 'character' && Object.values(characterSettings).some(value => !!value)">
|
||||
<div flex="~ col" max-h-60 gap-4 overflow-auto pr-1 sm:max-h-80>
|
||||
<template v-for="(value, key) in characterSettings" :key="key">
|
||||
<div v-if="value" flex="~ col" gap-2>
|
||||
<h2 text-lg text-neutral-500 font-medium dark:text-neutral-400>
|
||||
{{ t(`settings.pages.card.${key.toLowerCase()}`) }}
|
||||
</h2>
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
max-h-none overflow-auto whitespace-pre-line rounded-lg p-3 text-neutral-700 dark:text-neutral-300
|
||||
v-html="highlightTagToHtml(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modules -->
|
||||
<div v-if="activeTab === 'modules'">
|
||||
<div grid="~ cols-1 sm:cols-3" gap-4>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-1 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:ghost />
|
||||
{{ t('settings.pages.card.consciousness.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.consciousness ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-2 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:mic />
|
||||
{{ t('settings.pages.card.speech.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.speech ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-2 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:music />
|
||||
{{ t('settings.pages.card.speech.voice') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.voice ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
bg="neutral-50/50 dark:neutral-900/50"
|
||||
rounded-xl p-8 text-center
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
shadow="sm"
|
||||
>
|
||||
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
||||
{{ t('settings.pages.card.card_not_found') }}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<DeleteCardDialog
|
||||
v-model="showDeleteConfirm"
|
||||
:card-name="selectedCard?.name"
|
||||
@confirm="handleDeleteConfirm"
|
||||
@cancel="showDeleteConfirm = false"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CursorFloating } from '@proj-airi/stage-ui/components'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
isActive: boolean
|
||||
isSelected: boolean
|
||||
version: string
|
||||
consciousnessModel: string
|
||||
voiceModel: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'select'): void
|
||||
(e: 'activate'): void
|
||||
(e: 'delete'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CursorFloating
|
||||
relative min-h-120px flex="~ col" cursor-pointer overflow-hidden rounded-xl
|
||||
:class="[
|
||||
isSelected
|
||||
? 'border-2 border-primary-400 dark:border-primary-600'
|
||||
: 'border-2 border-neutral-100 dark:border-neutral-800/25',
|
||||
]"
|
||||
bg="neutral-200/50 dark:neutral-800/50"
|
||||
drop-shadow="none hover:[0px_4px_4px_rgba(220,220,220,0.4)] active:[0px_0px_0px_rgba(220,220,220,0.25)] dark:hover:none"
|
||||
transition="all ease-in-out duration-400"
|
||||
before="content-empty absolute inset-0 z-0 w-25% h-full transition-all duration-400 ease-in-out bg-gradient-to-r from-primary-500/0 to-primary-500/0 dark:from-primary-400/0 dark:to-primary-400/0 mask-image-[linear-gradient(120deg,white_100%)] opacity-0"
|
||||
hover="before:(opacity-100 bg-gradient-to-r from-primary-500/20 via-primary-500/10 to-transparent dark:from-primary-400/20 dark:via-primary-400/10 dark:to-transparent)"
|
||||
@click="emit('select')"
|
||||
>
|
||||
<!-- Card content -->
|
||||
<div
|
||||
relative flex="~ col 1" justify-between gap-3 overflow-hidden rounded-lg bg="white dark:neutral-900" p-5
|
||||
transition="all ease-in-out duration-400"
|
||||
after="content-empty absolute inset-0 z--2 w-full h-full bg-dotted-[neutral-200/80] bg-size-10px mask-image-[linear-gradient(165deg,white_30%,transparent_50%)] transition-all duration-400 ease-in-out"
|
||||
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20] text-primary-600/80 dark:text-primary-300/80"
|
||||
>
|
||||
<!-- Card header (name and badge) -->
|
||||
<div z-1 flex items-start justify-between gap-2>
|
||||
<h3 flex-1 truncate text-lg font-normal>
|
||||
{{ name }}
|
||||
</h3>
|
||||
<div v-if="isActive" shrink-0 rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
|
||||
<div i-solar:check-circle-bold-duotone text-sm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card description -->
|
||||
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- Card stats -->
|
||||
<div z-1 flex items-center justify-between text-xs text="neutral-500 dark:neutral-400">
|
||||
<div>v{{ version }}</div>
|
||||
<div flex items-center gap-1.5>
|
||||
<div flex items-center gap-0.5>
|
||||
<div i-lucide:ghost text-xs />
|
||||
<span>{{ consciousnessModel }}</span>
|
||||
</div>
|
||||
<div flex items-center gap-0.5>
|
||||
<div i-lucide:mic text-xs />
|
||||
<span>{{ voiceModel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card actions -->
|
||||
<div flex items-center justify-end px-2 py-1.5>
|
||||
<button
|
||||
rounded-lg p-1.5 transition-colors hover="bg-neutral-200 dark:bg-neutral-700/50"
|
||||
:disabled="isActive"
|
||||
@click.stop="emit('activate')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
isActive
|
||||
? 'i-solar:check-circle-bold-duotone text-primary-500 dark:text-primary-400'
|
||||
: 'i-solar:play-circle-broken text-neutral-500 dark:text-neutral-400',
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="id !== 'default'"
|
||||
rounded-lg p-1.5 transition-colors hover="bg-neutral-200 dark:bg-neutral-700/50"
|
||||
@click.stop="emit('delete')"
|
||||
>
|
||||
<div i-solar:trash-bin-trash-linear text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</CursorFloating>
|
||||
</template>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
} from 'reka-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
cardName?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:modelValue', false)
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('update:modelValue', false)
|
||||
emit('confirm')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot :open="modelValue" @update:open="emit('update:modelValue', $event)">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-100 bg-black/50 data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
|
||||
<AlertDialogContent
|
||||
class="fixed left-1/2 top-1/2 z-100 max-w-md w-full border border-neutral-200 rounded-xl bg-white p-6 shadow-xl -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<AlertDialogTitle class="mb-4 text-xl font-normal">
|
||||
{{ t('settings.pages.card.delete_card') }}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mb-6">
|
||||
{{ t('settings.pages.card.delete_confirmation') }} <b>"{{ cardName || '' }}"</b>
|
||||
</AlertDialogDescription>
|
||||
|
||||
<div class="flex flex-row justify-end gap-3">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button
|
||||
variant="secondary"
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button
|
||||
variant="danger"
|
||||
:label="t('settings.pages.card.delete')"
|
||||
@click="handleConfirm"
|
||||
/>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { GamingFactorio } from '@proj-airi/stage-ui/components'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GamingFactorio />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { GamingMinecraft } from '@proj-airi/stage-ui/components'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GamingMinecraft />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import { useServerStore } from '@proj-airi/stage-ui/stores/server'
|
||||
|
||||
// const serverStore = useServerStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { MessagingDiscord } from '@proj-airi/stage-ui/components'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MessagingDiscord />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from '@proj-airi/stage-ui/components'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<X />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = '302-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.302.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,162 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
import type { UnElevenLabsOptions } from 'unspeech'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const providerId = 'alibaba-cloud-model-studio'
|
||||
const defaultModel = 'cosyvoice-v1'
|
||||
|
||||
// Default voice settings specific to ElevenLabs
|
||||
const defaultVoiceSettings = {
|
||||
speed: 1.0,
|
||||
}
|
||||
|
||||
const pitch = ref<number>(0)
|
||||
const speed = ref<number>(1.0)
|
||||
const volume = ref<number>(0)
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for ElevenLabs
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
...defaultVoiceSettings,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
})
|
||||
|
||||
watch(pitch, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.pitch = pitch.value
|
||||
})
|
||||
|
||||
watch(speed, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.speed = speed.value
|
||||
})
|
||||
|
||||
watch(volume, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.volume = volume.value
|
||||
})
|
||||
|
||||
watch(providers, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #voice-settings>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Pitch control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.pitch.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.pitch.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Volume control - available in some providers -->
|
||||
<FieldRange
|
||||
v-model="volume"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.volume.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.volume.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'anthropic'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-ant-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.anthropic.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,139 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAccountIdInput,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'azure-ai-foundry'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const resourceName = computed({
|
||||
get: () => providers.value[providerId]?.resourceName || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].resourceName = value
|
||||
},
|
||||
})
|
||||
|
||||
const apiVersion = computed({
|
||||
get: () => providers.value[providerId]?.apiVersion || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiVersion = value
|
||||
},
|
||||
})
|
||||
|
||||
const modelId = computed({
|
||||
get: () => providers.value[providerId]?.modelId || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].modelId = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="..."
|
||||
required
|
||||
/>
|
||||
<ProviderAccountIdInput
|
||||
v-model="resourceName"
|
||||
label="Resouce name"
|
||||
placeholder="..."
|
||||
description="Prefix used in https://<prefix>.services.ai.azure.com"
|
||||
required
|
||||
/>
|
||||
<ProviderAccountIdInput
|
||||
v-model="modelId"
|
||||
label="Model id"
|
||||
placeholder="..."
|
||||
description="Model ID on Azure AI Foundry"
|
||||
required
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderAccountIdInput
|
||||
v-model="apiVersion"
|
||||
label="API version"
|
||||
placeholder="e.g. 2025-04-01-preview"
|
||||
description="API version for snapshot of the models"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAccountIdInput,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'cloudflare-workers-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const accountId = computed({
|
||||
get: () => providers.value[providerId]?.accountId || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].accountId = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:placeholder="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.api-key.placeholder')"
|
||||
/>
|
||||
|
||||
<ProviderAccountIdInput
|
||||
v-model="accountId"
|
||||
:label="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.label')"
|
||||
:description="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.description')"
|
||||
:placeholder="t('settings.pages.providers.provider.cloudflare-workers-ai.fields.field.account-id.placeholder')"
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'deepseek'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="ds-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.deepseek.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,223 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
import type { UnElevenLabsOptions } from 'unspeech'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldCheckbox, FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const providerId = 'elevenlabs'
|
||||
const defaultModel = 'eleven_multilingual_v2'
|
||||
|
||||
// Default voice settings specific to ElevenLabs
|
||||
const defaultVoiceSettings = {
|
||||
similarityBoost: 0.75,
|
||||
stability: 0.5,
|
||||
speed: 1.0,
|
||||
style: 0,
|
||||
useSpeakerBoost: true,
|
||||
}
|
||||
|
||||
const pitch = ref<number>(0)
|
||||
const speed = ref<number>(1.0)
|
||||
const volume = ref<number>(0)
|
||||
const style = ref<number>(0)
|
||||
const stability = ref<number>(0.5)
|
||||
const similarityBoost = ref<number>(0.75)
|
||||
const useSpeakerBoost = ref<boolean>(false)
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for ElevenLabs
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
...defaultVoiceSettings,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
})
|
||||
|
||||
watch(pitch, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.pitch = pitch.value
|
||||
})
|
||||
|
||||
watch(speed, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.speed = speed.value
|
||||
})
|
||||
|
||||
watch(volume, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.volume = volume.value
|
||||
})
|
||||
|
||||
watch(style, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.style = style.value
|
||||
})
|
||||
|
||||
watch(stability, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.stability = stability.value
|
||||
})
|
||||
|
||||
watch(similarityBoost, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.similarityBoost = similarityBoost.value
|
||||
})
|
||||
|
||||
watch(useSpeakerBoost, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.useSpeakerBoost = useSpeakerBoost.value
|
||||
})
|
||||
|
||||
watch(providers, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #voice-settings>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Pitch control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.pitch.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.pitch.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Volume control - available in some providers -->
|
||||
<FieldRange
|
||||
v-model="volume"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.volume.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.volume.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
|
||||
<!-- Style control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-model="style"
|
||||
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.style.label')"
|
||||
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.style.description')"
|
||||
:min="0"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Stability control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-model="stability"
|
||||
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.label')"
|
||||
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.description')"
|
||||
:min="0"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Similarity Boost control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-model="similarityBoost"
|
||||
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.label')"
|
||||
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.description')"
|
||||
:min="0"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Speaker Boost checkbox - specific to ElevenLabs -->
|
||||
<FieldCheckbox
|
||||
v-model="useSpeakerBoost"
|
||||
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.label')"
|
||||
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.description')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'featherless-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.featherless.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'fireworks-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="fw-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.fireworks.ai/inference/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'google-generative-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="AIza..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://generativelanguage.googleapis.com/v1beta/openai/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
// import { useI18n } from 'vue-i18n'
|
||||
|
||||
// const { t } = useI18n()
|
||||
|
||||
const providerId = 'index-tts-vllm'
|
||||
const defaultModel = 'IndexTTS-1.5'
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
// const { providers } = storeToRefs(providersStore)
|
||||
|
||||
// Check if API key is configured
|
||||
// const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
const apiKeyConfigured = true // Assuming API key is always configured as its not required
|
||||
|
||||
// Get available voices for Index TTS provider
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
watch([apiKeyConfigured], async () => {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
async function handleGenerateSpeech(input: string, voiceId: string) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProvider
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
const options = {
|
||||
...providerConfig,
|
||||
}
|
||||
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
options,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings :provider-id="providerId" :default-model="defaultModel">
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices" :generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured" :use-ssml="false"
|
||||
default-text="Hello! This is a test of the Index TTS Speech synthesis?."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'lm-studio'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="lm-studio"
|
||||
:is-required="false"
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="http://localhost:1234/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,190 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
import type { UnMicrosoftOptions } from 'unspeech'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldInput, FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const providerId = 'microsoft-speech'
|
||||
const defaultModel = 'v1'
|
||||
|
||||
// Default voice settings specific to Microsoft Speech
|
||||
const defaultVoiceSettings = {
|
||||
pitch: 0,
|
||||
speed: 1.0,
|
||||
volume: 0,
|
||||
}
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const pitch = ref(0)
|
||||
const speed = ref(1.0)
|
||||
const volume = ref(0)
|
||||
|
||||
// Additional settings specific to Microsoft Speech (region)
|
||||
const region = computed({
|
||||
get: () => providers.value[providerId]?.region as string | undefined || 'eastasia',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = { region: 'eastasia' }
|
||||
|
||||
providers.value[providerId].region = value
|
||||
},
|
||||
})
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for Microsoft Speech
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!region.value) {
|
||||
region.value = 'eastasia' // Default region
|
||||
}
|
||||
if (!providers.value[providerId]?.region) {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = { region: region.value }
|
||||
else
|
||||
providers.value[providerId].region = region.value
|
||||
}
|
||||
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
watch([apiKeyConfigured, region], async () => {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
// Generate speech with Microsoft-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// For Microsoft Speech, we need to ensure we're using the right region
|
||||
const options = {
|
||||
...providerConfig,
|
||||
region: region.value,
|
||||
disableSsml: !useSSML, // If useSSML is true, we don't disable SSML
|
||||
}
|
||||
|
||||
// If not using SSML and we have a voice, generate SSML
|
||||
if (!useSSML && voiceId) {
|
||||
const voice = availableVoices.value.find(v => v.id === voiceId)
|
||||
if (voice) {
|
||||
const ssml = speechStore.generateSSML(
|
||||
input,
|
||||
voice,
|
||||
{ ...providerConfig, pitch: pitch.value },
|
||||
)
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
ssml,
|
||||
voiceId,
|
||||
options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Either using direct SSML or no voice found
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
options,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Basic settings specific to Microsoft Speech -->
|
||||
<template #basic-settings>
|
||||
<FieldInput
|
||||
v-model="region"
|
||||
:label="t('settings.pages.providers.provider.microsoft-speech.fields.field.region.label')"
|
||||
:description="t('settings.pages.providers.provider.microsoft-speech.fields.field.region.description')"
|
||||
placeholder="eastasia"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #voice-settings>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Pitch control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="pitch"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.pitch.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.pitch.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Volume control - available in some providers -->
|
||||
<FieldRange
|
||||
v-model="volume"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.volume.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.volume.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the Microsoft Speech synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'mistral-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.mistral.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,125 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'modelscope'
|
||||
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
|
||||
// Use computed properties for settings
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize provider if it doesn't exist
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="ms-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api-inference.modelscope.cn/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'moonshot-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.moonshot.cn/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'novita-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.novita.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,176 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldKeyValues } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const providerId = 'ollama'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || 'http://localhost:11434/v1/',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
|
||||
const headers = ref<{ key: string, value: string }[]>(Object.entries(providers.value[providerId]?.headers || {}).map(([key, value]) => ({ key, value } as { key: string, value: string })) || [{ key: '', value: '' }])
|
||||
|
||||
function addKeyValue(headers: { key: string, value: string }[], key: string, value: string) {
|
||||
if (!headers)
|
||||
return
|
||||
|
||||
headers.push({ key, value })
|
||||
}
|
||||
|
||||
function removeKeyValue(index: number, headers: { key: string, value: string }[]) {
|
||||
if (!headers)
|
||||
return
|
||||
|
||||
if (headers.length === 1) {
|
||||
headers[0].key = ''
|
||||
headers[0].value = ''
|
||||
}
|
||||
else {
|
||||
headers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
watch(headers, (headers) => {
|
||||
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
|
||||
headers.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
providers.value[providerId].headers = headers.filter(header => header.key !== '').reduce((acc, header) => {
|
||||
acc[header.key] = header.value
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
async function refetch() {
|
||||
try {
|
||||
const validationResult = await providerMetadata.value.validators.validateProviderConfig({
|
||||
baseUrl: baseUrl.value,
|
||||
headers: headers.value.filter(header => header.key !== '').reduce((acc, header) => {
|
||||
acc[header.key] = header.value
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
})
|
||||
|
||||
if (!validationResult.valid) {
|
||||
validationMessage.value = t('settings.dialogs.onboarding.validationError', {
|
||||
error: validationResult.reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
validationMessage.value = t('settings.dialogs.onboarding.validationError', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch([baseUrl, headers], refetch, { immediate: true })
|
||||
watch(headers, refetch, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.().baseUrl || ''
|
||||
|
||||
// Initialize headers if not already set
|
||||
if (!providers.value[providerId]?.headers) {
|
||||
providers.value[providerId].headers = {}
|
||||
}
|
||||
if (headers.value.length === 0) {
|
||||
headers.value = [{ key: '', value: '' }]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="http://localhost:11434/v1/"
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<FieldKeyValues
|
||||
v-model="headers"
|
||||
:label="t('settings.pages.providers.common.section.advanced.fields.field.headers.label')"
|
||||
:description="t('settings.pages.providers.common.section.advanced.fields.field.headers.description')"
|
||||
:key-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.key.placeholder')"
|
||||
:value-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.value.placeholder')"
|
||||
@add="(key: string, value: string) => addKeyValue(headers, key, value)"
|
||||
@remove="(index: number) => removeKeyValue(index, headers)"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
const defaultVoiceSettings = {
|
||||
speed: 1.0,
|
||||
}
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'openai-audio-speech'
|
||||
const defaultModel = 'gpt-4o-mini-tts'
|
||||
|
||||
const speed = ref<number>(1.0)
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance<SpeechProvider<string>>(providerId)
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
...defaultVoiceSettings,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
watch(speed, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.speed = speed.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
:additional-settings="defaultVoiceSettings"
|
||||
>
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #voice-settings>
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the OpenAI Speech."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
TranscriptionPlayground,
|
||||
TranscriptionProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'openai-audio-transcription'
|
||||
const defaultModel = 'whisper-1'
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateTranscription(file: File) {
|
||||
const provider = await providersStore.getProviderInstance<TranscriptionProvider<string>>(providerId)
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await hearingStore.transcription(
|
||||
provider,
|
||||
model,
|
||||
file,
|
||||
'json',
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TranscriptionProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
>
|
||||
<template #playground>
|
||||
<TranscriptionPlayground
|
||||
:generate-transcription="handleGenerateTranscription"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
/>
|
||||
</template>
|
||||
</TranscriptionProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,171 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
import type { SpeechProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
SpeechPlaygroundOpenAICompatible,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
const defaultVoiceSettings = {
|
||||
speed: 1.0,
|
||||
}
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'openai-compatible-audio-speech'
|
||||
|
||||
// Settings refs
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (providers.value[providerId])
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (providers.value[providerId])
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
const model = computed({
|
||||
get: () => providers.value[providerId]?.model || 'tts-1',
|
||||
set: (value) => {
|
||||
if (providers.value[providerId])
|
||||
providers.value[providerId].model = value
|
||||
},
|
||||
})
|
||||
|
||||
const voice = computed({
|
||||
get: () => providers.value[providerId]?.voice || 'alloy',
|
||||
set: (value) => {
|
||||
if (providers.value[providerId])
|
||||
providers.value[providerId].voice = value
|
||||
},
|
||||
})
|
||||
|
||||
const speed = ref<number>(1.0)
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Generate speech with specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean, modelId?: string) {
|
||||
const provider = await providersStore.getProviderInstance<SpeechProvider<string>>(providerId)
|
||||
if (!provider)
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
modelId || model.value,
|
||||
input,
|
||||
voiceId || voice.value,
|
||||
{
|
||||
...providerConfig,
|
||||
...defaultVoiceSettings,
|
||||
speed: speed.value,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:required="false"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
/>
|
||||
<FieldRange
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
|
||||
<SpeechPlaygroundOpenAICompatible
|
||||
v-model:model-value="model"
|
||||
v-model:voice="voice"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the OpenAI Compatible Speech."
|
||||
/>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
TranscriptionPlayground,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldInput } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'openai-compatible-audio-transcription'
|
||||
const hearingStore = useHearingStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
const model = computed({
|
||||
get: () => providers.value[providerId]?.model || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].model = value
|
||||
},
|
||||
})
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Generate transcription
|
||||
async function handleGenerateTranscription(file: File) {
|
||||
const provider = await providersStore.getProviderInstance<TranscriptionProvider<string>>(providerId)
|
||||
if (!provider)
|
||||
throw new Error('Failed to initialize transcription provider')
|
||||
|
||||
return await hearingStore.transcription(
|
||||
provider,
|
||||
model.value,
|
||||
file,
|
||||
'json',
|
||||
)
|
||||
}
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<FieldInput
|
||||
v-model="model"
|
||||
:label="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_name')"
|
||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
|
||||
<TranscriptionPlayground
|
||||
:generate-transcription="handleGenerateTranscription"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
/>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'openai-compatible'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'openai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const providerId = 'openrouter-ai'
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Define computed properties for credentials
|
||||
const apiKey = computed({
|
||||
get: () => providers.value[providerId]?.apiKey || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].apiKey = value
|
||||
},
|
||||
})
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
|
||||
// Use the composable to get validation logic and state
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-or-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://openrouter.ai/api/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,133 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
import type { UnElevenLabsOptions } from 'unspeech'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldRange } from '@proj-airi/ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const providerId = 'player2-speech'
|
||||
const defaultModel = 'v1'
|
||||
const speedRatio = ref<number>(1.0)
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { t } = useI18n()
|
||||
// Get available voices for Player2
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
// Generate speech with Player2-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
// Player2 doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
},
|
||||
)
|
||||
}
|
||||
const hasPlayer2 = ref(true)
|
||||
onMounted(async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
try {
|
||||
const baseUrl = (providerConfig.baseUrl as string | undefined) ?? ''
|
||||
const res = await fetch(`${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/health`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'player2-game-key': 'airi',
|
||||
},
|
||||
})
|
||||
hasPlayer2.value = res.status === 200
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
hasPlayer2.value = false
|
||||
}
|
||||
})
|
||||
watch(speedRatio, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
providerConfig.speed = speedRatio.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
>
|
||||
<template #voice-settings>
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speedRatio"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="5.0" :step="0.01"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="true"
|
||||
default-text="Hello! This is a test of the Player 2 voice synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
<Alert v-if="!hasPlayer2" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
<div>
|
||||
Please download and run the Player2 App:
|
||||
<a href="https://player2.game" target="_blank" rel="noopener noreferrer">
|
||||
https://player2.game
|
||||
</a>
|
||||
|
||||
<div>
|
||||
After downloading, if you still are having trouble, please reach out to us on Discord:
|
||||
<a href="https://player2.game/discord" target="_blank" rel="noopener noreferrer">
|
||||
https://player2.game/discord
|
||||
</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { RemovableRef } from '@vueuse/shared'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
|
||||
|
||||
// Get provider metadata
|
||||
const providerId = 'player2'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
const baseUrl = computed({
|
||||
get: () => providers.value[providerId]?.baseUrl || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].baseUrl = value
|
||||
},
|
||||
})
|
||||
const hasPlayer2 = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
providersStore.initializeProvider(providerId)
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || ''
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl.value}health`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'player2-game-key': 'airi',
|
||||
},
|
||||
})
|
||||
hasPlayer2.value = res.status === 200
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
hasPlayer2.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Watch settings and update the provider configuration
|
||||
watch([baseUrl], () => {
|
||||
providers.value[providerId] = {
|
||||
...providers.value[providerId],
|
||||
baseUrl: baseUrl.value || '',
|
||||
}
|
||||
})
|
||||
|
||||
function handleResetSettings() {
|
||||
providers.value[providerId] = {
|
||||
...(providerMetadata.value?.defaultOptions as any),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon="providerMetadata?.icon"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderBaseUrlInput v-model="baseUrl" placeholder="http://localhost:4315/v1/" />
|
||||
</ProviderBasicSettings>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
<Alert v-if="!hasPlayer2" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
<div>
|
||||
Please download and run the Player2 App:
|
||||
<a href="https://player2.game" target="_blank" rel="noopener noreferrer">
|
||||
https://player2.game
|
||||
</a>
|
||||
|
||||
<div>
|
||||
After downloading, if you still are having trouble, please reach out to us on Discord:
|
||||
<a href="https://player2.game/discord" target="_blank" rel="noopener noreferrer">
|
||||
https://player2.game/discord
|
||||
</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
|
||||
const providerId = 'together-ai'
|
||||
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.together.xyz/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
|
||||
const providerId = 'vllm'
|
||||
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="token-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="http://localhost:8000/v1"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,151 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
import type { UnElevenLabsOptions } from 'unspeech'
|
||||
|
||||
import {
|
||||
SpeechPlayground,
|
||||
SpeechProviderSettings,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { FieldInput, FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const providerId = 'volcengine'
|
||||
const defaultModel = 'v1'
|
||||
|
||||
const speedRatio = ref<number>(1.0)
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
// Additional settings specific to Volcengine (appId)
|
||||
const appId = computed({
|
||||
get: () => (providers.value[providerId]?.app as any)?.appId as string | undefined || '',
|
||||
set: (value) => {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = {}
|
||||
|
||||
providers.value[providerId].app = {
|
||||
appId: value,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
|
||||
|
||||
// Get available voices for ElevenLabs
|
||||
const availableVoices = computed(() => {
|
||||
return speechStore.availableVoices[providerId] || []
|
||||
})
|
||||
|
||||
// Generate speech with ElevenLabs-specific parameters
|
||||
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
|
||||
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
|
||||
if (!provider) {
|
||||
throw new Error('Failed to initialize speech provider')
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = providerConfig.model as string | undefined || defaultModel
|
||||
|
||||
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
model,
|
||||
input,
|
||||
voiceId,
|
||||
{
|
||||
...providerConfig,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
})
|
||||
|
||||
watch(speedRatio, async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
if (!providerConfig.audio) {
|
||||
providerConfig.audio = {}
|
||||
}
|
||||
|
||||
(providerConfig.audio as any).speedRatio = speedRatio.value
|
||||
})
|
||||
|
||||
watch([providers, appId], async () => {
|
||||
const providerConfig = providersStore.getProviderConfig(providerId)
|
||||
const providerMetadata = providersStore.getProviderMetadata(providerId)
|
||||
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
}
|
||||
else {
|
||||
console.error('Failed to validate provider config', providerConfig)
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SpeechProviderSettings
|
||||
:provider-id="providerId"
|
||||
:default-model="defaultModel"
|
||||
>
|
||||
<!-- Voice settings specific to ElevenLabs -->
|
||||
<template #basic-settings>
|
||||
<div flex="~ col gap-4">
|
||||
<FieldInput
|
||||
v-model="appId"
|
||||
:label="t('settings.pages.providers.provider.volcengine.fields.field.appId.label')"
|
||||
:description="t('settings.pages.providers.provider.volcengine.fields.field.appId.description')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #voice-settings>
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-model="speedRatio"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Replace the default playground with our standalone component -->
|
||||
<template #playground>
|
||||
<SpeechPlayground
|
||||
:available-voices="availableVoices"
|
||||
:generate-speech="handleGenerateSpeech"
|
||||
:api-key-configured="apiKeyConfigured"
|
||||
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
|
||||
/>
|
||||
</template>
|
||||
</SpeechProviderSettings>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
|
||||
const providerId = 'xai'
|
||||
|
||||
const {
|
||||
t,
|
||||
router,
|
||||
providerMetadata,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
isValidating,
|
||||
isValid,
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProviderSettingsLayout
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
:provider-icon-color="providerMetadata?.iconColor"
|
||||
:on-back="() => router.back()"
|
||||
>
|
||||
<ProviderSettingsContainer>
|
||||
<ProviderBasicSettings
|
||||
:title="t('settings.pages.providers.common.section.basic.title')"
|
||||
:description="t('settings.pages.providers.common.section.basic.description')"
|
||||
:on-reset="handleResetSettings"
|
||||
>
|
||||
<ProviderApiKeyInput
|
||||
v-model="apiKey"
|
||||
:provider-name="providerMetadata?.localizedName"
|
||||
placeholder="xai-..."
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
|
||||
<ProviderBaseUrlInput
|
||||
v-model="baseUrl"
|
||||
placeholder="https://api.x.ai/v1/"
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Callout } from '@proj-airi/stage-ui/components'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:armchair-2-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Callout
|
||||
label="In development, needs your help!"
|
||||
theme="orange"
|
||||
>
|
||||
<div>
|
||||
This functionality is still under development. If you have any suggestions or would like to contribute, please reach out to us on our <a underline decoration-dotted href="https://github.com/moeru-ai/airi/issues">GitHub issues page</a>.
|
||||
The source code of this page is located at <a underline decoration-dotted href="https://github.com/moeru-ai/airi/tree/main/apps/stage-web/src/pages/settings/scene/index.vue">here</a>.
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:armchair-2-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,8 +0,0 @@
|
||||
[
|
||||
[],
|
||||
["#A5978B", "#D8CAAF", "#B8B4A7", "#C4BCB1", "#E5DED8", "#9A8F7D", "#BEB5A7", "#C9C0B6"],
|
||||
["#7A9EAF", "#B8C7CC", "#D4B79C", "#8B9D77", "#C7D5CB", "#E6D0B1", "#94A7B1", "#B4C8C3"],
|
||||
["#D9B48F", "#B5917A", "#8C7A6B", "#A17F5F", "#B98C46", "#C7A252", "#DAB300", "#D19826"],
|
||||
["#9BA7B0", "#C1CBD4", "#A5ADB6", "#8B959E", "#D4DCE4", "#7F8A94", "#B3BCC6", "#98A4AE"],
|
||||
["#E4C6D0", "#A61B29", "#5D513C", "#789262", "#1C0D1A", "#F7C242", "#62A9DD", "#8C4B3C"]
|
||||
]
|
||||
@@ -1,187 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ColorPalette, Section } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { ColorHueRange } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import COLOR_PRESETS from './color-presets.json'
|
||||
|
||||
const settings = useSettings()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Section
|
||||
v-motion
|
||||
mb-2
|
||||
:title="t('settings.pages.themes.sections.section.custom-color.title')"
|
||||
icon="i-solar:pallete-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (4 * 10)"
|
||||
:delay="4 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-motion flex items-center
|
||||
justify-between
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (5 * 10)"
|
||||
:delay="5 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span text-lg font-normal>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
|
||||
<label relative flex cursor-pointer items-center gap-2>
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 peer-checked:bg-primary-500 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}
|
||||
</label>
|
||||
</div>
|
||||
<ColorHueRange
|
||||
v-model="settings.themeColorsHue"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (6 * 10)"
|
||||
:delay="6 * 50"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
/>
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar text-[10px] md:text-base sm:text-xs"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (7 * 10)"
|
||||
:delay="7 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-50>50</span>
|
||||
<span bg-primary-100>100</span>
|
||||
<span bg-primary-200>200</span>
|
||||
<span bg-primary-300>300</span>
|
||||
<span bg-primary-400>400</span>
|
||||
<span bg-primary-500>500</span>
|
||||
<div
|
||||
v-motion
|
||||
text-white
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (8 * 10)"
|
||||
:delay="8 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-600>600</span>
|
||||
<span bg-primary-700>700</span>
|
||||
<span bg-primary-800>800</span>
|
||||
<span bg-primary-900>900</span>
|
||||
<span bg-primary-950>950</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar transparency-grid text-[10px] md:text-base sm:text-xs"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (9 * 10)"
|
||||
:delay="9 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg="primary-500/5">500/5</span>
|
||||
<span bg="primary-500/10">500/10</span>
|
||||
<span bg="primary-500/20">500/20</span>
|
||||
<span bg="primary-500/30">500/30</span>
|
||||
<span bg="primary-500/40">500/40</span>
|
||||
<span bg="primary-500/50">500/50</span>
|
||||
<span bg="primary-500/60">500/60</span>
|
||||
<span bg="primary-500/70">500/70</span>
|
||||
<span bg="primary-500/80">500/80</span>
|
||||
<span bg="primary-500/90">500/90</span>
|
||||
<span bg="primary-500">500</span>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
v-motion
|
||||
mb-2 :title="t('settings.pages.themes.sections.section.theme-presets.title')"
|
||||
icon="i-solar:magic-stick-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (10 * 10)"
|
||||
:delay="10 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
||||
v-motion
|
||||
class="w-full flex flex-col items-start justify-between gap-2 rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out md:flex-row md:items-center md:gap-0"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (11 * 10) + (i * 10)"
|
||||
:delay="11 * 50 + (i * 50)"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div>
|
||||
<span font-medium>{{ $rt(title) }}</span>
|
||||
<div text="sm neutral-500">
|
||||
{{ $rt(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<ColorPalette :colors="(colors as any[]).map((name, j) => ({ hex: COLOR_PRESETS[i][j], name: $rt(name) }))" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:pallete-2-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-bar {
|
||||
--at-apply: flex of-hidden rounded-lg lh-10 text-center text-black;
|
||||
|
||||
* {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { breakpointsTailwind, useBreakpoints, useDark, useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import Cross from '../../components/Backgrounds/Cross.vue'
|
||||
import Header from '../../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../../components/Layouts/MobileInteractiveArea.vue'
|
||||
import AnimatedWave from '../../components/Widgets/AnimatedWave.vue'
|
||||
|
||||
import { themeColorFromPropertyOf, useThemeColor } from '../../composables/theme-color'
|
||||
|
||||
const dark = useDark()
|
||||
const paused = ref(false)
|
||||
|
||||
function handleSettingsOpen(open: boolean) {
|
||||
paused.value = open
|
||||
}
|
||||
|
||||
const positionCursor = useMouse()
|
||||
const { scale, position, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const { updateThemeColor } = useThemeColor(themeColorFromPropertyOf('.widgets.top-widgets .colored-area', 'background-color'))
|
||||
watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Cross>
|
||||
<AnimatedWave
|
||||
class="widgets top-widgets"
|
||||
:fill-color="dark
|
||||
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
|
||||
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
|
||||
>
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
<WidgetStage
|
||||
flex-1 min-w="1/2"
|
||||
:paused="paused"
|
||||
:focus-at="{
|
||||
x: positionCursor.x.value,
|
||||
y: positionCursor.y.value,
|
||||
}"
|
||||
:x-offset="`${isMobile ? position.x : position.x - 10}%`"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedWave>
|
||||
</Cross>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
name: StageScenePage
|
||||
meta:
|
||||
layout: stage
|
||||
stageTransition:
|
||||
name: bubble-wave-out
|
||||
</route>
|
||||
@@ -1,77 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { sleep } from '@moeru/std'
|
||||
import { Textarea } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const processing = ref<boolean>(false)
|
||||
const streamingMessage = ref({ content: '' })
|
||||
|
||||
async function onSendMessage() {
|
||||
processing.value = true
|
||||
|
||||
const tokens = messageInput.value.split('')
|
||||
|
||||
enum States {
|
||||
Literal = 'literal',
|
||||
Special = 'special',
|
||||
}
|
||||
|
||||
let state = States.Literal
|
||||
let buffer = ''
|
||||
|
||||
for (const textPart of tokens) {
|
||||
await sleep(50)
|
||||
let newState: States = state
|
||||
|
||||
if (textPart === '<')
|
||||
newState = States.Special
|
||||
else if (textPart === '>')
|
||||
newState = States.Literal
|
||||
|
||||
if (state === States.Literal && newState === States.Special) {
|
||||
streamingMessage.value.content += buffer
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
if (state === States.Special && newState === States.Literal)
|
||||
buffer = '' // Clear buffer when exiting Special state
|
||||
|
||||
if (state === States.Literal && newState === States.Literal) {
|
||||
streamingMessage.value.content += textPart
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
state = newState
|
||||
}
|
||||
|
||||
if (buffer)
|
||||
streamingMessage.value.content += buffer
|
||||
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<Textarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="neutral-100 dark:neutral-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="neutral-100 dark:neutral-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
Streaming Message
|
||||
</h3>
|
||||
<div>{{ streamingMessage.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useDelayMessageQueue } from '@proj-airi/stage-ui/composables'
|
||||
import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants'
|
||||
import { Textarea } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const emotionMessageContentProcessed = ref<string[]>([])
|
||||
const delaysProcessed = ref<number[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
// const emotionMessageContentQueue = useQueue<string>({
|
||||
// handlers: [
|
||||
// async (ctx) => {
|
||||
// emotionMessageContentProcessed.value.push(ctx.data)
|
||||
// },
|
||||
// ],
|
||||
// })
|
||||
|
||||
const delaysQueue = useDelayMessageQueue()
|
||||
delaysQueue.onHandlerEvent('delay', (delay) => {
|
||||
delaysProcessed.value.push(delay)
|
||||
})
|
||||
|
||||
function onSendMessage() {
|
||||
processing.value = true
|
||||
const tokens = messageInput.value.split('')
|
||||
for (const token of tokens)
|
||||
delaysQueue.enqueue(token)
|
||||
|
||||
delaysQueue.enqueue(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<Textarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="neutral-100 dark:neutral-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="neutral-100 dark:neutral-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
Emotion Message
|
||||
</h3>
|
||||
<div v-for="message in emotionMessageContentProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
Delays
|
||||
</h3>
|
||||
<div v-for="message in delaysProcessed" :key="message">
|
||||
<div>{{ message }}s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Emotion } from '@proj-airi/stage-ui/constants/emotions'
|
||||
|
||||
import { useEmotionsMessageQueue } from '@proj-airi/stage-ui/composables/queues'
|
||||
import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants'
|
||||
import { createQueue } from '@proj-airi/stage-ui/utils/queue'
|
||||
import { Textarea } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const messagesProcessed = ref<string[]>([])
|
||||
const emotionsProcessed = ref<string[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
const emotionsQueue = createQueue<Emotion>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
emotionsProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const emotionMessageContentQueue = useEmotionsMessageQueue(emotionsQueue)
|
||||
|
||||
function onSendMessage() {
|
||||
processing.value = true
|
||||
const tokens = messageInput.value.split('')
|
||||
for (const token of tokens)
|
||||
emotionMessageContentQueue.enqueue(token)
|
||||
|
||||
emotionMessageContentQueue.enqueue(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<Textarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="neutral-100 dark:neutral-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="neutral-100 dark:neutral-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
Messages
|
||||
</h3>
|
||||
<div v-for="message in messagesProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
Emotions
|
||||
</h3>
|
||||
<div v-for="message in emotionsProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { usePipelineWorkflowTextSegmentationStore } from '@proj-airi/stage-ui/composables/queues'
|
||||
import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants'
|
||||
import { createQueue } from '@proj-airi/stage-ui/utils/queue'
|
||||
import { Textarea } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const messageInput = ref<string>('')
|
||||
const ttsProcessed = ref<string[]>([])
|
||||
const processing = ref<boolean>(false)
|
||||
|
||||
const textSegmentationStore = usePipelineWorkflowTextSegmentationStore()
|
||||
const { onTextSegmented } = textSegmentationStore
|
||||
const { textSegmentationQueue } = storeToRefs(textSegmentationStore)
|
||||
|
||||
const ttsQueue = createQueue<string>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
ttsProcessed.value.push(ctx.data)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
onTextSegmented((text) => {
|
||||
ttsQueue.enqueue(text)
|
||||
})
|
||||
|
||||
async function onSendMessage() {
|
||||
processing.value = true
|
||||
// const tokens = messageInput.value.split('')
|
||||
// for (const token of tokens) {
|
||||
// await sleep(100)
|
||||
// messageContentQueue.add(token)
|
||||
// }
|
||||
textSegmentationQueue.value.enqueue(messageInput.value)
|
||||
textSegmentationQueue.value.enqueue(llmInferenceEndToken)
|
||||
messageInput.value = ''
|
||||
processing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-2 p-2>
|
||||
<div flex flex-row gap-2>
|
||||
<Textarea
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="neutral-100 dark:neutral-700"
|
||||
w-full rounded-lg outline-none
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button rounded-lg bg="neutral-100 dark:neutral-700" p-4>
|
||||
{{ processing ? 'Processing...' : 'Send' }}
|
||||
</button>
|
||||
</div>
|
||||
<div w-full flex flex-row gap-4>
|
||||
<div w-full rounded-lg bg="neutral-100 dark:neutral-700" p-2>
|
||||
<h3 font-normal>
|
||||
TTS Message
|
||||
</h3>
|
||||
<div v-for="message in ttsProcessed" :key="message">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { CommonRequestOptions } from '@xsai/shared'
|
||||
import type { Message } from '@xsai/shared-chat'
|
||||
import type { Infer, Schema } from 'xsschema'
|
||||
|
||||
import { generateText } from '@xsai/generate-text'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
import { toJsonSchema, validate } from 'xsschema'
|
||||
|
||||
type SchemaOrString<S extends Schema | undefined | unknown> = S extends unknown ? string : S extends Schema ? Infer<S> : never
|
||||
|
||||
async function parseJSONFormat<S extends Schema, R extends SchemaOrString<S>>(content: string, options: { messages: Message[], apiKey?: string, baseURL: string, model: string } & Partial<CommonRequestOptions>, schema?: S, erroredValue?: string, errorMessage?: string): Promise<R> {
|
||||
if (!schema)
|
||||
return content as unknown as R
|
||||
|
||||
try {
|
||||
let parsedContent: Infer<S>
|
||||
let correctionPrompt = ''
|
||||
|
||||
if (erroredValue && errorMessage) {
|
||||
correctionPrompt = `Previous response "${JSON.stringify(erroredValue)}" was invalid due to: ${JSON.stringify(errorMessage)}\n\n`
|
||||
}
|
||||
|
||||
try {
|
||||
parsedContent = JSON.parse(content)
|
||||
}
|
||||
catch (parseError) {
|
||||
console.error('Error parsing JSON:', parseError, content)
|
||||
|
||||
options.messages.push(message.user(`
|
||||
${correctionPrompt}The response was not valid JSON:
|
||||
${JSON.stringify(content)}
|
||||
|
||||
Error: ${String(parseError)}
|
||||
|
||||
Please provide a corrected JSON response that matches the schema:
|
||||
${JSON.stringify(await toJsonSchema(schema))}`))
|
||||
|
||||
const response = await call(options, schema)
|
||||
return parseJSONFormat(response, options, schema, content, String(parseError))
|
||||
}
|
||||
|
||||
// TODO: print validation issues
|
||||
return await validate(schema, parsedContent) as R
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing response:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes user input and generates LLM response along with thought nodes.
|
||||
*/
|
||||
async function call<S extends Schema, R extends SchemaOrString<S>>(options: { messages: Message[], apiKey?: string, baseURL: string, model: string } & Partial<CommonRequestOptions>, schema?: S): Promise<R> {
|
||||
if (schema != null) {
|
||||
options.messages.push(message.user(`Your response must follow the following schema:
|
||||
${JSON.stringify(await toJsonSchema(schema))}
|
||||
|
||||
Without any extra markups such as \`\`\` in markdown, or descriptions.`))
|
||||
}
|
||||
|
||||
const response = await generateText({
|
||||
baseURL: options.baseURL,
|
||||
apiKey: options.apiKey,
|
||||
model: options.model,
|
||||
messages: options.messages,
|
||||
})
|
||||
|
||||
return await parseJSONFormat<S, R>(response.text || '', options, schema)
|
||||
}
|
||||
|
||||
export async function generateObject<S extends Schema, R extends SchemaOrString<S>>(options: { messages: Message[], model: string, apiKey?: string, baseURL: string } & Partial<CommonRequestOptions>, schema?: S): Promise<R> {
|
||||
return await call(options, schema)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function isPlatformTamagotchi() {
|
||||
return import.meta.env.MODE === 'tamagotchi'
|
||||
}
|
||||
@@ -46,15 +46,22 @@ export default defineConfig({
|
||||
'@framework/model/cubismmoc',
|
||||
],
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@proj-airi/server-sdk': resolve(join(import.meta.dirname, '..', '..', 'packages', 'server-sdk', 'src')),
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')),
|
||||
'@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')),
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')),
|
||||
'@proj-airi/stage-pages': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src')),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
warmup: {
|
||||
clientFiles: [
|
||||
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src'))}/*.vue`,
|
||||
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src'))}/*.vue`,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
Info(),
|
||||
|
||||
@@ -76,6 +83,10 @@ export default defineConfig({
|
||||
extensions: ['.vue', '.md'],
|
||||
dts: resolve(import.meta.dirname, 'src/typed-router.d.ts'),
|
||||
importMode: 'async',
|
||||
routesFolder: [
|
||||
resolve(import.meta.dirname, 'src', 'pages'),
|
||||
resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'),
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-pages",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "Shared Pages",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moeru-ai/airi.git",
|
||||
"directory": "packages/stage-pages"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nekopaw/tempora": "0.3.1-alpha.1",
|
||||
"@proj-airi/ccc": "workspace:*",
|
||||
"@proj-airi/i18n": "workspace:*",
|
||||
"@proj-airi/stage-ui": "workspace:*",
|
||||
"@proj-airi/ui": "workspace:*",
|
||||
"@stdlib/string-base-kebabcase": "^0.2.2",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/shared": "^13.9.0",
|
||||
"@xsai-ext/shared-providers": "catalog:",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"animejs": "^4.2.0",
|
||||
"dompurify": "^3.2.7",
|
||||
"nanoid": "^5.1.6",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"pinia": "^3.0.3",
|
||||
"reka-ui": "^2.5.1",
|
||||
"unspeech": "^0.1.7",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.180.0",
|
||||
"vue-tsc": "^3.0.8"
|
||||
}
|
||||
}
|
||||
+1
-49
@@ -1,51 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../components/IconAnimation.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const iconAnimationStarted = ref(false)
|
||||
const iconAnimation = ref<InstanceType<typeof IconAnimation>>()
|
||||
const resolveAnimation = ref<() => void>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const animationIcon = ref('')
|
||||
const animationPosition = ref('')
|
||||
const showAnimationComponent = ref(false)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
function handleAnimationEnded() {
|
||||
resolveAnimation.value?.()
|
||||
}
|
||||
|
||||
async function handleIconItemClick(event: MouseEvent, setting: typeof settings.value[0]) {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const iconElement = target.querySelector('.menu-icon-item-icon') as HTMLElement
|
||||
if (!iconElement)
|
||||
return
|
||||
|
||||
// get the position of the icon element
|
||||
const rect = iconElement.getBoundingClientRect()
|
||||
const position = `${rect.left}px, ${rect.top}px`
|
||||
|
||||
// set the icon and position
|
||||
animationIcon.value = setting.icon
|
||||
animationPosition.value = position
|
||||
|
||||
// show the animation component
|
||||
showAnimationComponent.value = true
|
||||
|
||||
// wait for the DOM to update
|
||||
await nextTick()
|
||||
|
||||
// start the animation
|
||||
iconAnimationStarted.value = true
|
||||
}
|
||||
|
||||
const removeBeforeEach = router.beforeEach(async (_, __, next) => {
|
||||
if (!settingsStore.usePageSpecificTransitions || settingsStore.disableTransitions) {
|
||||
next()
|
||||
@@ -123,22 +88,9 @@ const settings = computed(() => [
|
||||
:description="setting.description"
|
||||
:icon="setting.icon"
|
||||
:to="setting.to"
|
||||
@click="(e: MouseEvent) => handleIconItemClick(e, setting)"
|
||||
/>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showAnimationComponent && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions"
|
||||
ref="iconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="6 * 1.2"
|
||||
:position="animationPosition"
|
||||
:duration="1000"
|
||||
text-color="text-neutral-400/50 dark:text-neutral-600/20"
|
||||
:started="iconAnimationStarted"
|
||||
@animation-ended.once="handleAnimationEnded"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-12rem)]" bottom-0 right--10 z--1
|
||||
-23
@@ -5,10 +5,6 @@ import { ModelSettings } from '@proj-airi/stage-ui/components/scenarios/settings
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
@@ -34,12 +30,6 @@ async function extractColorsFromModel() {
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:people-nearby-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -52,20 +42,7 @@ const {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
+3
-4
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import workletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url'
|
||||
|
||||
import { Alert, Button, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { createVAD, createVADStates } from '@proj-airi/stage-ui/workers/vad'
|
||||
import { FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import workletUrl from '../../../workers/vad/process.worklet?worker&url'
|
||||
|
||||
import { createVAD, createVADStates } from '../../../workers/vad'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
-22
@@ -2,17 +2,7 @@
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useModulesList } from '@proj-airi/stage-ui/composables/use-modules-list'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/icon-animation'
|
||||
|
||||
const { modulesList } = useModulesList()
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-solar:layers-bold-duotone')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,19 +24,7 @@ const {
|
||||
:configured="module.configured"
|
||||
/>
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:z-index="-1"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-15rem)]" bottom-0 right--5 z--1
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user