fix(stage-layouts): replace Tooltip with Popover for microphone controls (#1169)

* refactor(ChatArea): replace Tooltip with Popover for microphone controls

The Tooltip component was replaced with Popover to provide better user experience and more consistent behavior with other UI elements. The functionality remains the same but uses the more appropriate Popover component from the UI library.

* refactor(ChatArea): rename hearingTooltipOpen to hearingPopoverOpen for clarity

The variable name was changed to better reflect its actual usage with PopoverRoot component. This improves code readability and maintainability by using more accurate terminology.

---------

Co-authored-by: Rin <shinohara-rin@users.noreply.github.com>
This commit is contained in:
𝓒𝓱𝓮𝓻𝓻𝔂🍒
2026-03-07 17:20:22 +08:00
committed by GitHub
co-authored by Rin
parent 675df7c0ac
commit f8fb6e2282
@@ -13,14 +13,14 @@ import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/
import { BasicTextarea, FieldSelect } from '@proj-airi/ui'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
import { PopoverContent, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IndicatorMicVolume from './IndicatorMicVolume.vue'
const messageInput = ref('')
const hearingTooltipOpen = ref(false)
const hearingPopoverOpen = ref(false)
const isComposing = ref(false)
const isListening = ref(false) // Transcription listening state (separate from microphone enabled)
@@ -128,7 +128,7 @@ async function handleSend() {
}
}
watch(hearingTooltipOpen, async (value) => {
watch(hearingPopoverOpen, async (value) => {
if (value) {
await askPermission()
}
@@ -158,7 +158,7 @@ function teardownAnalyzer() {
async function setupAnalyzer() {
teardownAnalyzer()
if (!hearingTooltipOpen.value || !enabled.value || !stream.value)
if (!hearingPopoverOpen.value || !enabled.value || !stream.value)
return
if (audioContext.state === 'suspended')
await audioContext.resume()
@@ -169,7 +169,7 @@ async function setupAnalyzer() {
analyzerSource.connect(analyser)
}
watch([hearingTooltipOpen, enabled, stream], () => {
watch([hearingPopoverOpen, enabled, stream], () => {
setupAnalyzer()
}, { immediate: true })
@@ -427,74 +427,70 @@ watch(autoSendEnabled, (enabled) => {
absolute bottom-2 left-2 z-10 flex items-center gap-2
>
<!-- Microphone icon button -->
<TooltipProvider :delay-duration="0" :skip-delay-duration="0">
<TooltipRoot v-model:open="hearingTooltipOpen">
<TooltipTrigger as-child>
<button
class="h-8 w-8 flex items-center justify-center rounded-md outline-none transition-all duration-200 active:scale-95"
text="lg neutral-500 dark:neutral-400"
:title="t('settings.hearing.title')"
>
<Transition name="fade" mode="out-in">
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" />
<div v-else class="i-ph:microphone-slash h-5 w-5" />
</Transition>
</button>
</TooltipTrigger>
<Transition name="fade">
<TooltipContent
side="top"
:side-offset="8"
:class="[
'w-72 max-w-[18rem] rounded-xl border border-neutral-200/60 bg-neutral-50/90 p-4',
'shadow-lg backdrop-blur-md dark:border-neutral-800/30 dark:bg-neutral-900/80',
'flex flex-col gap-3',
]"
>
<div class="flex flex-col items-center justify-center">
<div class="relative h-28 w-28 select-none">
<div
class="absolute left-1/2 top-1/2 h-20 w-20 rounded-full transition-all duration-150 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1 + normalizedVolume * 0.35})`, opacity: String(0.25 + normalizedVolume * 0.25) }"
:class="enabled ? 'bg-primary-500/15 dark:bg-primary-600/20' : 'bg-neutral-300/20 dark:bg-neutral-700/20'"
/>
<div
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + normalizedVolume * 0.55})`, opacity: String(0.15 + normalizedVolume * 0.2) }"
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
/>
<div
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + normalizedVolume * 0.8})`, opacity: String(0.08 + normalizedVolume * 0.15) }"
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
/>
<button
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:class="enabled
? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95'
: 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200'"
@click="enabled = !enabled"
>
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
</button>
</div>
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
{{ enabled ? 'Microphone enabled' : 'Microphone disabled' }}
</p>
</div>
<FieldSelect
v-model="selectedAudioInput"
label="Input device"
description="Select the microphone you want to use."
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
layout="vertical"
placeholder="Select microphone"
<PopoverRoot v-model:open="hearingPopoverOpen">
<PopoverTrigger as-child>
<button
class="h-8 w-8 flex items-center justify-center rounded-md outline-none transition-all duration-200 active:scale-95"
text="lg neutral-500 dark:neutral-400"
:title="t('settings.hearing.title')"
>
<Transition name="fade" mode="out-in">
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" />
<div v-else class="i-ph:microphone-slash h-5 w-5" />
</Transition>
</button>
</PopoverTrigger>
<PopoverContent
side="top"
:side-offset="8"
:class="[
'w-72 max-w-[18rem] rounded-xl border border-neutral-200/60 bg-neutral-50/90 p-4',
'shadow-lg backdrop-blur-md dark:border-neutral-800/30 dark:bg-neutral-900/80',
'flex flex-col gap-3',
]"
>
<div class="flex flex-col items-center justify-center">
<div class="relative h-28 w-28 select-none">
<div
class="absolute left-1/2 top-1/2 h-20 w-20 rounded-full transition-all duration-150 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1 + normalizedVolume * 0.35})`, opacity: String(0.25 + normalizedVolume * 0.25) }"
:class="enabled ? 'bg-primary-500/15 dark:bg-primary-600/20' : 'bg-neutral-300/20 dark:bg-neutral-700/20'"
/>
</TooltipContent>
</Transition>
</TooltipRoot>
</TooltipProvider>
<div
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + normalizedVolume * 0.55})`, opacity: String(0.15 + normalizedVolume * 0.2) }"
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
/>
<div
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + normalizedVolume * 0.8})`, opacity: String(0.08 + normalizedVolume * 0.15) }"
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
/>
<button
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:class="enabled
? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95'
: 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200'"
@click="enabled = !enabled"
>
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
</button>
</div>
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
{{ enabled ? 'Microphone enabled' : 'Microphone disabled' }}
</p>
</div>
<FieldSelect
v-model="selectedAudioInput"
label="Input device"
description="Select the microphone you want to use."
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
layout="vertical"
placeholder="Select microphone"
/>
</PopoverContent>
</PopoverRoot>
</div>
</div>
</div>