fix(stage): cover manual TTS stop across layouts

Add the stop-speaking control to the mobile shared input layout and the Electron Tamagotchi chat input so Web/shared, mobile, and Electron all expose the same manual TTS interruption path.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex
This commit is contained in:
RainbowBird
2026-06-06 00:53:34 +08:00
parent 7549d4e9fb
commit bc9baa4e3e
2 changed files with 35 additions and 0 deletions
@@ -3,6 +3,7 @@ import type { ChatToolCallRendererRegistry } from '@proj-airi/stage-ui/component
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
import { errorMessageFrom } from '@moeru/std'
import { useStopSpeakingButton } from '@proj-airi/stage-layouts/composables/useStopSpeakingButton'
import { ChatHistory, JournalPreviewModal } from '@proj-airi/stage-ui/components'
import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
@@ -56,6 +57,7 @@ const sendModeLabels = computed<Record<SendMode, string>>(() => ({
'ctrl-enter': t('stage.send-mode.ctrl-enter'),
'double-enter': t('stage.send-mode.double-enter'),
}))
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton()
const latestImageEntries = computed(() => {
if (!activeCardId.value)
@@ -322,6 +324,24 @@ async function handleRetryMessage(index: number) {
</DropdownMenuPortal>
</DropdownMenuRoot>
<button
v-if="showStopSpeakingButton"
data-testid="stop-speaking-button"
:class="[
'max-h-[10lh] min-h-[1lh]',
]"
bg="neutral-100 dark:neutral-800"
text="lg neutral-500 dark:neutral-400"
hover:text="primary-500 dark:primary-400"
flex items-center justify-center rounded-md p-2 outline-none
transition-colors transition-transform active:scale-95
title="Stop speaking"
aria-label="Stop speaking"
@click="stopSpeakingFromChat"
>
<div class="i-solar:stop-circle-bold-duotone" />
</button>
<button
:class="[
'max-h-[10lh] min-h-[1lh]',
@@ -28,6 +28,7 @@ import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
import ActionAbout from './InteractiveArea/Actions/About.vue'
import { useTranscriptions } from '../../composables/use-transcriptions'
import { useStopSpeakingButton } from '../../composables/useStopSpeakingButton'
import { BackgroundDialogPicker } from '../Backgrounds'
const { isDark, toggleDark } = useTheme()
@@ -76,6 +77,7 @@ const { isListening, startStreamingTranscription, stopStreamingTranscription } =
isStageTamagotchi,
},
)
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton()
const toggleTranscription = () => isListening.value ? stopStreamingTranscription() : startStreamingTranscription()
async function handleSubmit() {
@@ -253,6 +255,19 @@ onMounted(() => {
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
/>
<button
v-if="showStopSpeakingButton"
data-testid="stop-speaking-button"
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
text="neutral-500 hover:primary-600 dark:neutral-900 dark:hover:primary-700"
bg="primary-50/80 dark:neutral-100/80 hover:neutral-50"
transition="all duration-250 ease-in-out"
title="Stop speaking"
aria-label="Stop speaking"
@click="stopSpeakingFromChat"
>
<div i-solar:stop-circle-bold-duotone />
</button>
<button
v-if="messageInput.trim() || isComposing"
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md