wip enhance state management transcript

This commit is contained in:
lebaudantoine
2025-04-03 19:18:58 +02:00
parent 95f2d5de52
commit 4ea0231744
15 changed files with 261 additions and 51 deletions
@@ -88,6 +88,24 @@ export const MainNotificationToast = () => {
if (notification.data?.emoji)
handleEmoji(notification.data.emoji, participant)
break
case NotificationType.TranscriptionStarted:
toastQueue.add(
{
participant,
type: NotificationType.TranscriptionStarted,
},
{ timeout: NotificationDuration.ALERT }
)
break
case NotificationType.TranscriptionStopped:
toastQueue.add(
{
participant,
type: NotificationType.TranscriptionStopped,
},
{ timeout: NotificationDuration.ALERT }
)
break
default:
return
}
@@ -6,4 +6,6 @@ export enum NotificationType {
LowerHand = 'lowerHand',
ReactionReceived = 'reactionReceived',
ParticipantWaiting = 'participantWaiting',
TranscriptionStarted = 'transcriptionStarted',
TranscriptionStopped = 'transcriptionStopped',
}
@@ -9,6 +9,7 @@ import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastTranscription } from '@/features/notifications/components/ToastTranscription.tsx'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -36,6 +37,12 @@ const renderToast = (
case NotificationType.LowerHand:
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
case NotificationType.TranscriptionStarted:
return <ToastTranscription key={toast.key} toast={toast} state={state} />
case NotificationType.TranscriptionStopped:
return <ToastTranscription key={toast.key} toast={toast} state={state} />
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -0,0 +1,35 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { NotificationType } from '../NotificationType'
export function ToastTranscription({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
{t(
props.toast.content.type == NotificationType.TranscriptionStarted
? 'recordingStarted'
: 'recordingStopped',
{
name: participant.name,
}
)}
</HStack>
</StyledToastContainer>
)
}
@@ -2,8 +2,6 @@ import { A, Button, Div, Text } from '@/primitives'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { css } from '@/styled-system/css'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
@@ -11,9 +9,17 @@ import {
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload.ts'
import { NotificationType } from '@/features/notifications/NotificationType.ts'
import { useSnapshot } from 'valtio/index'
import {
TranscriptionStatus,
transcriptionStore,
} from '@/stores/transcription.ts'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess.ts'
const CRISP_HELP_ARTICLE =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x'
@@ -28,6 +34,8 @@ export const Transcript = () => {
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const transcriptionSnap = useSnapshot(transcriptionStore)
const room = useRoomContext()
useEffect(() => {
@@ -40,6 +48,17 @@ export const Transcript = () => {
}
}, [room])
const notifyParticipant = async (status) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
type: status,
}
const data = encoder.encode(JSON.stringify(payload))
await room.localParticipant.publishData(data, {
reliable: true,
})
}
const handleTranscript = async () => {
if (!roomId) {
console.warn('No room ID found')
@@ -49,8 +68,12 @@ export const Transcript = () => {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
await notifyParticipant(NotificationType.TranscriptionStopped)
transcriptionStore.status = TranscriptionStatus.STOPPING
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
await notifyParticipant(NotificationType.TranscriptionStarted)
transcriptionStore.status = TranscriptionStatus.STARTING
}
} catch (error) {
console.error('Failed to handle transcript:', error)
@@ -58,7 +81,13 @@ export const Transcript = () => {
}
}
if (!hasTranscriptAccess) return
const isDisabled = useMemo(
() =>
isLoading ||
transcriptionSnap.status === TranscriptionStatus.STARTING ||
transcriptionSnap.status === TranscriptionStatus.STOPPING,
[isLoading, transcriptionSnap]
)
return (
<Div
@@ -77,9 +106,9 @@ export const Transcript = () => {
marginBottom: '1rem',
})}
/>
{room.isRecording ? (
{!hasTranscriptAccess ? (
<>
<Text>{t('stop.heading')}</Text>
<Text>Join the beta</Text>
<Text
variant="note"
wrap={'pretty'}
@@ -92,44 +121,67 @@ export const Transcript = () => {
>
{t('stop.body')}
</Text>
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
<Button size="sm" variant="tertiary">
Join the beta
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
{room.isRecording ? (
<>
<Text>{t('stop.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('stop.body')}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
</>
)}
</Div>
@@ -3,12 +3,76 @@ import { Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner'
import { useEffect, useMemo, useState } from 'react'
import { RemoteParticipant, RoomEvent } from 'livekit-client'
import { decodeNotificationDataReceived } from '@/features/notifications/utils.ts'
import { NotificationType } from '@/features/notifications/NotificationType.ts'
import { useSnapshot } from 'valtio/index'
import {
TranscriptionStatus,
transcriptionStore,
} from '@/stores/transcription.ts'
export const TranscriptStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording' })
const room = useRoomContext()
if (!room?.isRecording) return
const transcriptionSnap = useSnapshot(transcriptionStore)
useEffect(() => {
if (room.isRecording) {
transcriptionStore.status = TranscriptionStatus.STARTED
}
}, [])
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) => {
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 message = useMemo(() => {
switch (transcriptionSnap.status) {
case TranscriptionStatus.STOPPING:
return t('transcriptStopping')
case TranscriptionStatus.STARTING:
return t('transcriptStarting')
default:
return t('transcriptStarted')
}
}, [transcriptionSnap])
if (transcriptionSnap.status == TranscriptionStatus.STOPPED) return
return (
<div
@@ -34,7 +98,7 @@ export const TranscriptStateToast = () => {
fontWeight: '500 !important',
})}
>
{t('transcript')}
{message}
</Text>
</div>
)
@@ -21,5 +21,7 @@
"several": "",
"open": "",
"accept": ""
}
},
"recordingStarted": "",
"recordingStopped": ""
}
+3 -1
View File
@@ -288,7 +288,9 @@
}
},
"recording": {
"transcript": ""
"transcriptStarted": "",
"transcriptStarting": "",
"transcriptStopping": ""
},
"participantTileFocus": {
"pin": {
@@ -21,5 +21,7 @@
"several": "Several people want to join this call.",
"open": "Open",
"accept": "Accept"
}
},
"recordingStarted": "{{name}} started the meeting transcription.",
"recordingStopped": "{{name}} stopped the meeting transcription."
}
+3 -1
View File
@@ -287,7 +287,9 @@
}
},
"recording": {
"transcript": "Transcribing"
"transcriptStarted": "Transcribing",
"transcriptStarting": "Transcription starting",
"transcriptStopping": "Transcription stopping"
},
"participantTileFocus": {
"pin": {
@@ -21,5 +21,7 @@
"several": "Plusieurs personnes souhaitent participer à cet appel.",
"open": "Afficher",
"accept": "Accepter"
}
},
"recordingStarted": "{{name}} a démarré la transcription de la réunion.",
"recordingStopped": "{{name}} a arrêté la transcription de la réunion."
}
+3 -1
View File
@@ -287,7 +287,9 @@
}
},
"recording": {
"transcript": "Transcription en cours"
"transcriptStarted": "Transcription en cours",
"transcriptStarting": "Démarrage de la transcription",
"transcriptStopping": "Arrêt de la transcription"
},
"participantTileFocus": {
"pin": {
@@ -21,5 +21,7 @@
"several": "Meerdere mensen willen deelnemen aan dit gesprek.",
"open": "Openen",
"accept": "Accepteren"
}
},
"recordingStarted": "{{name}} is de transcriptie van de vergadering gestart.",
"recordingStopped": "{{name}} heeft de transcriptie van de vergadering gestopt."
}
+3 -1
View File
@@ -287,7 +287,9 @@
}
},
"recording": {
"transcript": "Transcriptie bezig"
"transcriptStarted": "Transcriptie bezig",
"transcriptStarting": "Transcriptie begint",
"transcriptStopping": "Transcriptie stopt"
},
"participantTileFocus": {
"pin": {
+16
View File
@@ -0,0 +1,16 @@
import { proxy } from 'valtio'
export enum TranscriptionStatus {
STARTING,
STARTED,
STOPPING,
STOPPED,
}
type State = {
status: TranscriptionStatus
}
export const transcriptionStore = proxy<State>({
status: TranscriptionStatus.STOPPED,
})