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

View File

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

View File

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

View File

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

View File

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

View File

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