wip enhance state management transcript
This commit is contained in:
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -287,7 +287,9 @@
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"transcript": "Transcriptie bezig"
|
||||
"transcriptStarted": "Transcriptie bezig",
|
||||
"transcriptStarting": "Transcriptie begint",
|
||||
"transcriptStopping": "Transcriptie stopt"
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"pin": {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user