🚸(frontend) share more information about transcription state

Previous toast state was too naive. Enhance message deliver to
participants.
This commit is contained in:
lebaudantoine
2025-04-04 17:32:48 +02:00
committed by aleb_the_flash
parent ac9ba0df62
commit d202a025e7
8 changed files with 134 additions and 52 deletions

View File

@@ -1,43 +0,0 @@
import { css } from '@/styled-system/css'
import { RiRecordCircleLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useRoomContext } from '@livekit/components-react'
export const RecordingStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording' })
const room = useRoomContext()
if (!room?.isRecording) return
return (
<div
className={css({
display: 'flex',
position: 'fixed',
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.25rem 0.35rem',
backgroundColor: 'primaryDark.200',
borderColor: 'primaryDark.400',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<RiRecordCircleLine
size={20}
className={css({
color: 'white',
backgroundColor: 'danger.700',
padding: '3px',
borderRadius: '3px',
})}
/>
<Text variant={'sm'}>{t('label')}</Text>
</div>
)
}

View File

@@ -0,0 +1,103 @@
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio/index'
import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner'
import { useEffect, useMemo } from 'react'
import { Text } from '@/primitives'
import { RemoteParticipant, RoomEvent } from 'livekit-client'
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
import { NotificationType } from '@/features/notifications/NotificationType'
import { TranscriptionStatus, transcriptionStore } from '@/stores/transcription'
export const TranscriptStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording.transcript' })
const room = useRoomContext()
const transcriptionSnap = useSnapshot(transcriptionStore)
useEffect(() => {
if (room.isRecording) {
transcriptionStore.status = TranscriptionStatus.STARTED
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
participant?: RemoteParticipant
) => {
const notification = decodeNotificationDataReceived(payload)
if (!participant || !notification) return
switch (notification.type) {
case NotificationType.TranscriptionStarted:
transcriptionStore.status = TranscriptionStatus.STARTING
break
case NotificationType.TranscriptionStopped:
transcriptionStore.status = TranscriptionStatus.STOPPING
break
default:
return
}
}
const handleRecordingStatusChanged = (status: boolean) => {
transcriptionStore.status = status
? TranscriptionStatus.STARTED
: TranscriptionStatus.STOPPED
}
room.on(RoomEvent.DataReceived, handleDataReceived)
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived)
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room])
const key = useMemo(() => {
switch (transcriptionSnap.status) {
case TranscriptionStatus.STOPPING:
return 'stopping'
case TranscriptionStatus.STARTING:
return 'starting'
default:
return 'started'
}
}, [transcriptionSnap])
if (transcriptionSnap.status == TranscriptionStatus.STOPPED) return
return (
<div
className={css({
display: 'flex',
position: 'fixed',
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.75rem 0.75rem',
backgroundColor: 'primaryDark.100',
borderColor: 'white',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<Spinner size={20} variant="dark" />
<Text
variant={'sm'}
className={css({
fontWeight: '500 !important',
})}
>
{t(key)}
</Text>
</div>
)
}

View File

@@ -28,7 +28,7 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile' import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel' import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '../components/RecordingStateToast' import { TranscriptStateToast } from '../components/TranscriptStateToast'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
const LayoutWrapper = styled( const LayoutWrapper = styled(
@@ -231,7 +231,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)} )}
<RoomAudioRenderer /> <RoomAudioRenderer />
<ConnectionStateToast /> <ConnectionStateToast />
<RecordingStateToast /> <TranscriptStateToast />
</div> </div>
) )
} }

View File

@@ -282,7 +282,11 @@
} }
}, },
"recording": { "recording": {
"label": "" "transcript": {
"started": "",
"starting": "",
"stopping": ""
}
}, },
"participantTileFocus": { "participantTileFocus": {
"pin": { "pin": {

View File

@@ -281,7 +281,11 @@
} }
}, },
"recording": { "recording": {
"label": "Recording" "transcript": {
"started": "Transcribing",
"starting": "Transcription starting",
"stopping": "Transcription stopping"
}
}, },
"participantTileFocus": { "participantTileFocus": {
"pin": { "pin": {

View File

@@ -281,7 +281,11 @@
} }
}, },
"recording": { "recording": {
"label": "Enregistrement" "transcript": {
"started": "Transcription en cours",
"starting": "Démarrage de la transcription",
"stopping": "Arrêt de la transcription"
}
}, },
"participantTileFocus": { "participantTileFocus": {
"pin": { "pin": {

View File

@@ -281,7 +281,11 @@
} }
}, },
"recording": { "recording": {
"label": "Opnemen" "transcript": {
"started": "Transcriptie bezig",
"starting": "Transcriptie begint",
"stopping": "Transcriptie stopt"
}
}, },
"participantTileFocus": { "participantTileFocus": {
"pin": { "pin": {

View File

@@ -1,7 +1,13 @@
import { ProgressBar } from 'react-aria-components' import { ProgressBar } from 'react-aria-components'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
export const Spinner = ({ size = 56 }: { size?: number }) => { export const Spinner = ({
size = 56,
variant = 'light',
}: {
size?: number
variant?: 'light' | 'dark'
}) => {
const center = 14 const center = 14
const strokeWidth = 3 const strokeWidth = 3
const r = 14 - strokeWidth const r = 14 - strokeWidth
@@ -25,7 +31,7 @@ export const Spinner = ({ size = 56 }: { size?: number }) => {
strokeDashoffset={0} strokeDashoffset={0}
strokeLinecap="round" strokeLinecap="round"
className={css({ className={css({
stroke: 'primary.100', stroke: variant == 'light' ? 'primary.100' : 'primaryDark.100',
})} })}
style={{}} style={{}}
/> />
@@ -37,7 +43,7 @@ export const Spinner = ({ size = 56 }: { size?: number }) => {
strokeDashoffset={percentage && c - (percentage / 100) * c} strokeDashoffset={percentage && c - (percentage / 100) * c}
strokeLinecap="round" strokeLinecap="round"
className={css({ className={css({
stroke: 'primary.800', stroke: variant == 'light' ? 'primary.800' : 'white',
})} })}
style={{ style={{
animation: `rotate 1s ease-in-out infinite`, animation: `rotate 1s ease-in-out infinite`,