This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/nextjs'
import { useEffect } from 'react'
import type { ComponentProps } from 'react'
import AudioBtn from '.'
import { ensureMockAudioManager } from '../../../../.storybook/utils/audio-player-manager.mock'
ensureMockAudioManager()
const StoryWrapper = (props: ComponentProps<typeof AudioBtn>) => {
useEffect(() => {
ensureMockAudioManager()
}, [])
return (
<div className="flex items-center justify-center space-x-3">
<AudioBtn {...props} />
<span className="text-xs text-gray-500">Audio toggle using ActionButton styling</span>
</div>
)
}
const meta = {
title: 'Base/General/NewAudioButton',
component: AudioBtn,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Updated audio playback trigger styled with `ActionButton`. Behaves like the legacy audio button but adopts the new button design system.',
},
},
nextjs: {
appDirectory: true,
navigation: {
pathname: '/apps/demo-app/text-to-audio',
params: { appId: 'demo-app' },
},
},
},
argTypes: {
id: {
control: 'text',
description: 'Message identifier used by the audio request.',
},
value: {
control: 'text',
description: 'Prompt or response text that will be converted to speech.',
},
voice: {
control: 'text',
description: 'Voice profile for the generated speech.',
},
},
} satisfies Meta<typeof AudioBtn>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: args => <StoryWrapper {...args} />,
args: {
id: 'message-1',
value: 'Listen to the latest assistant message.',
voice: 'alloy',
},
}

View File

@@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
import { useParams, usePathname } from 'next/navigation'
import {
RiVolumeUpLine,
} from '@remixicon/react'
import { t } from 'i18next'
import Tooltip from '@/app/components/base/tooltip'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
type AudioBtnProps = {
id?: string
voice?: string
value?: string
}
type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
const AudioBtn = ({
id,
voice,
value,
}: AudioBtnProps) => {
const [audioState, setAudioState] = useState<AudioState>('initial')
const params = useParams()
const pathname = usePathname()
const audio_finished_call = (event: string): any => {
switch (event) {
case 'ended':
setAudioState('ended')
break
case 'paused':
setAudioState('ended')
break
case 'loaded':
setAudioState('loading')
break
case 'play':
setAudioState('playing')
break
case 'error':
setAudioState('ended')
break
}
}
let url = ''
let isPublic = false
if (params.token) {
url = '/text-to-audio'
isPublic = true
}
else if (params.appId) {
if (pathname.search('explore/installed') > -1)
url = `/installed-apps/${params.appId}/text-to-audio`
else
url = `/apps/${params.appId}/text-to-audio`
}
const handleToggle = async () => {
if (audioState === 'playing' || audioState === 'loading') {
setTimeout(() => setAudioState('paused'), 1)
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).pauseAudio()
}
else {
setTimeout(() => setAudioState('loading'), 1)
AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).playAudio()
}
}
const tooltipContent = {
initial: t('appApi.play'),
ended: t('appApi.play'),
paused: t('appApi.pause'),
playing: t('appApi.playing'),
loading: t('appApi.loading'),
}[audioState]
return (
<Tooltip
popupContent={tooltipContent}
>
<ActionButton
state={
audioState === 'loading' || audioState === 'playing'
? ActionButtonState.Active
: ActionButtonState.Default
}
onClick={handleToggle}
disabled={audioState === 'loading'}
>
<RiVolumeUpLine className='h-4 w-4' />
</ActionButton>
</Tooltip>
)
}
export default AudioBtn