feat(stage-tamagotchi): stage-web like ControlsIsland

This commit is contained in:
Neko Ayaka
2025-10-08 17:15:35 +08:00
parent 766e7da18c
commit 82a089605f
4 changed files with 62 additions and 19 deletions
@@ -8,7 +8,7 @@ import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { BasicTextarea } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import TamagotchiChatHistory from './ChatHistory.vue'
@@ -17,7 +17,7 @@ const messageInput = ref('')
const listening = ref(false)
const attachments = ref<{ type: 'image', data: string, mimeType: string, url: string }[]>([])
// const { askPermission } = useSettingsAudioDevice()
const { askPermission } = useSettingsAudioDevice()
const { enabled, selectedAudioInput } = storeToRefs(useSettingsAudioDevice())
const { send, onAfterMessageComposed, discoverToolsCompatibility } = useChatStore()
const { messages } = storeToRefs(useChatStore())
@@ -116,21 +116,16 @@ function handleTranscription(_buffer: Float32Array) {
alert('Transcription is not implemented yet')
}
// async function handleAudioInputChange(event: Event) {
// const target = event.target as HTMLSelectElement
// const found = audioInputs.value.find(d => d.deviceId === target.value)
// if (!found) {
// selectedAudioDevice.value = undefined
// return
// }
// selectedAudioDevice.value = found
// }
watch(enabled, async (value) => {
if (value === false) {
destroy()
}
else {
await askPermission()
start()
}
}, {
immediate: true,
})
watch([activeProvider, activeModel], async () => {
@@ -144,10 +139,6 @@ onAfterMessageComposed(async () => {
attachments.value.forEach(att => URL.revokeObjectURL(att.url))
attachments.value = []
})
onMounted(() => {
start()
})
</script>
<template>
@@ -0,0 +1,13 @@
<template>
<button
:class="[
'border-2 border-solid border-neutral-200/60 dark:border-neutral-800/10',
'bg-neutral-50/80 dark:bg-neutral-800/70',
'w-fit flex items-center self-end justify-center p-2',
'rounded-xl backdrop-blur-md',
'transition-all hover:transition-none transition-duration-300 transition-ease-out',
]"
>
<slot />
</button>
</template>
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { HearingConfigDialog } from '@proj-airi/stage-ui/components'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { useDark, useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import ControlButton from './ControlButton.vue'
const isDark = useDark({ disableTransition: false })
const toggleDark = useToggle(isDark)
const settingsAudioDeviceStore = useSettingsAudioDevice()
const { enabled } = storeToRefs(settingsAudioDeviceStore)
</script>
<template>
<div fixed bottom-2 right-2>
<div flex flex-col gap-1>
<HearingConfigDialog>
<ControlButton>
<Transition name="fade" mode="out-in">
<div v-if="enabled" i-ph:microphone size-5 text="neutral-800 dark:neutral-300" />
<div v-else i-ph:microphone-slash size-5 text="neutral-800 dark:neutral-300" />
</Transition>
</ControlButton>
</HearingConfigDialog>
<ControlButton :class="['cursor-move', 'drag-region']">
<div i-ph:arrows-out-cardinal size-5 text="neutral-800 dark:neutral-300" />
</ControlButton>
<ControlButton @click="() => toggleDark()">
<Transition name="fade" mode="out-in">
<div v-if="isDark" i-solar:moon-outline size-5 text="neutral-800 dark:neutral-300" />
<div v-else i-solar:sun-2-outline size-5 text="neutral-800 dark:neutral-300" />
</Transition>
</ControlButton>
</div>
</div>
</template>
@@ -4,6 +4,7 @@ import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
import { storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import ControlsIsland from '../components/Widgets/ControlsIsland/index.vue'
import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue'
import { useWindowStore } from '../stores/window'
@@ -55,9 +56,9 @@ const modeIndicatorClass = computed(() => {
flex="~ col"
relative z-2 h-full overflow-hidden rounded-xl
transition="opacity duration-500 ease-in-out"
class="drag-region"
>
<div v-show="!isLoading" relative h-full w-full items-end gap-2 class="view">
<ResourceStatusIsland ref="resourceStatusIslandRef" />
<WidgetStage
ref="widgetStageRef"
v-model:state="componentStateStage"
@@ -69,7 +70,7 @@ const modeIndicatorClass = computed(() => {
:y-offset="positionInPercentageString.y"
mb="<md:18"
/>
<ResourceStatusIsland ref="resourceStatusIslandRef" />
<ControlsIsland />
</div>
<div v-show="isLoading" h-full w-full>
<div class="absolute left-0 top-0 z-99 h-full w-full flex cursor-grab items-center justify-center overflow-hidden">