🚸(frontend) rework the transcription side panel

Inspired by proprietary solutions, add clearer details on how transcription
works and what users can expect from the feature. This new presentation is much
simpler to read, parse, and understand than the previous large block of text
that users were not reading at all.

Using icons helps users quickly understand where the transcription is sent, how
they are notified, and which meeting language is used.

Some information is currently hardcoded and will be parameterized in upcoming
commits. This work is ongoing.
This commit is contained in:
lebaudantoine
2025-12-13 00:00:16 +01:00
committed by aleb_the_flash
parent d3e6af6f82
commit b19ac7f82b
5 changed files with 236 additions and 201 deletions

View File

@@ -6,7 +6,6 @@ import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useHasRecordingAccess,
useIsRecordingTransitioning,
useStartRecording,
useStopRecording,
useHasFeatureWithoutAdminRights,
@@ -29,6 +28,8 @@ import humanizeDuration from 'humanize-duration'
import i18n from 'i18next'
import { useUser } from '@/features/auth'
import { LoginButton } from '@/components/LoginButton'
import { HStack, VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx'
export const TranscriptSidePanel = () => {
const { data } = useConfig()
@@ -76,8 +77,6 @@ export const TranscriptSidePanel = () => {
}
}, [recordingSnap])
const isRecordingTransitioning = useIsRecordingTransitioning()
const room = useRoomContext()
const isRoomConnected = room.state == ConnectionState.Connected
@@ -122,15 +121,6 @@ export const TranscriptSidePanel = () => {
}
}
const isDisabled = useMemo(
() =>
isLoading ||
isRecordingTransitioning ||
statuses.isAnotherModeStarted ||
!isRoomConnected,
[isLoading, isRecordingTransitioning, statuses, isRoomConnected]
)
if (hasFeatureWithoutAdminRights) {
return (
<Div
@@ -320,142 +310,195 @@ export const TranscriptSidePanel = () => {
>
<img
src="/assets/intro-slider/3.png"
alt={''}
alt=""
className={css({
minHeight: '309px',
height: '309px',
minHeight: '250px',
height: '250px',
marginBottom: '1rem',
'@media (max-height: 700px)': {
marginTop: '-16px',
'@media (max-height: 900px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '45%',
marginBottom: '0.3rem',
maxHeight: '25%',
marginBottom: '0.75rem',
},
'@media (max-height: 530px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '40%',
marginBottom: '0.1rem',
'@media (max-height: 770px)': {
display: 'none',
},
})}
/>
<>
{statuses.isStarted ? (
<>
<H lvl={3} margin={false}>
{t('stop.heading')}
</H>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('stop.body')}
<VStack gap={0} marginBottom={30}>
<H lvl={1} margin={'sm'}>
{t('heading')}
</H>
<Text variant="body" fullWidth>
{data?.recording?.max_duration
? t('body', {
max_duration: humanizeDuration(data?.recording?.max_duration, {
language: i18n.language,
}),
})
: t('bodyWithoutMaxDuration')}{' '}
{data?.support?.help_article_transcript && (
<A href={data.support.help_article_transcript} target="_blank">
{t('linkMore')}
</A>
)}
</Text>
</VStack>
<VStack gap={0} marginBottom={40}>
<div
className={css({
width: '100%',
// border: '1px solid black',
background: 'gray.100',
borderRadius: '4px 4px 0 0',
paddingLeft: '4px',
padding: '8px',
display: 'flex',
})}
>
<div
className={css({
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
<span className="material-icons">article</span>
</div>
<div
className={css({
flex: 5,
})}
>
<Text variant="sm">
{t('details.destination')}{' '}
<A
href="https://docs.numerique.gouv.fr"
target="_blank"
rel="noopener noreferrer"
>
docs.numerique.gouv.fr
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
</div>
</div>
<div
className={css({
width: '100%',
// border: '1px solid black',
background: 'gray.100',
paddingLeft: '4px',
padding: '8px',
display: 'flex',
marginTop: '4px',
})}
>
<div
className={css({
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
<span className="material-icons">mail</span>
</div>
<div
className={css({
flex: 5,
})}
>
<Text variant="sm">{t('details.receiver')}</Text>
</div>
</div>
<div
className={css({
width: '100%',
background: 'gray.100',
borderRadius: '0 0 4px 4px',
paddingLeft: '4px',
padding: '8px',
display: 'flex',
marginTop: '4px',
})}
>
<div
className={css({
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
<span className="material-icons">language</span>
</div>
<div
className={css({
flex: 5,
})}
>
<Text variant="sm">{t('details.language')}</Text>
</div>
</div>
<div className={css({ height: '15px' })} />
<div
className={css({
width: '100%',
marginLeft: '20px',
})}
>
<Checkbox
size="sm"
isDisabled={
statuses.isStarting || statuses.isStarted || isPendingToStart
}
>
<Text variant="sm">{t('details.recording')}</Text>
</Checkbox>
</div>
</VStack>
<div
className={css({
marginBottom: '80px',
width: '100%',
})}
>
{statuses.isStopping || isPendingToStop ? (
<HStack width={'100%'} height={'46px'} justify="center">
<Spinner size={30} />
<Text variant="body">{t('button.saving')}</Text>
</HStack>
) : (
<>
{statuses.isStopping || isPendingToStop ? (
<>
<H lvl={3} margin={false}>
{t('stopping.heading')}
</H>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('stopping.body')}
</Text>
<Spinner />
</>
{statuses.isStarted || statuses.isStarting || room.isRecording ? (
<Button
variant="tertiary"
fullWidth
onPress={() => handleTranscript()}
isDisabled={statuses.isStopping || isPendingToStop || isLoading}
data-attr="stop-transcript"
>
{t('button.stop')}
</Button>
) : (
<>
<H lvl={3} margin={false}>
{t('start.heading')}
</H>
<Text
variant="note"
wrap="balance"
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('start.body', {
duration_message: data?.recording?.max_duration
? t('durationMessage', {
max_duration: humanizeDuration(
data?.recording?.max_duration,
{
language: i18n.language,
}
),
})
: '',
})}{' '}
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('start.linkMore')}
</A>
)}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{statuses.isStarting || isPendingToStart ? (
<>
<Spinner size={20} />
{t('start.loading')}
</>
) : (
t('start.button')
)}
</Button>
</>
<Button
variant="tertiary"
fullWidth
onPress={() => handleTranscript()}
isDisabled={isPendingToStart || !isRoomConnected || isLoading}
data-attr="start-transcript"
>
{t('button.start')}
</Button>
)}
</>
)}
</>
</div>
<Dialog
isOpen={!!isErrorDialogOpen}
role="alertdialog"

View File

@@ -310,13 +310,21 @@
}
},
"transcript": {
"start": {
"heading": "Dieses Gespräch transkribieren",
"body": "Dieses Gespräch automatisch transkribieren {{duration_message}} und die Zusammenfassung in Docs erhalten.",
"button": "Transkription starten",
"loading": "Transkription wird gestartet",
"linkMore": "Mehr erfahren"
"heading": "Transkribieren Sie Ihr Meeting mit dem KI-Assistenten",
"body": "Transkribieren Sie bis zu {{max_duration}} Meetingdauer.",
"bodyWithoutMaxDuration": "Transkribieren Sie Ihr Meeting ohne Begrenzung.",
"details": {
"receiver": "Das Transkript wird an den Organisator und die Mitorganisatoren gesendet.",
"destination": "Ein neues Dokument wird erstellt auf",
"language": "Meeting-Sprachen: Französisch (fr)",
"recording": "Auch eine Aufzeichnung starten"
},
"button": {
"start": "Meeting transkribieren starten",
"stop": "Meeting transkribieren beenden",
"saving": "Speichern…"
},
"linkMore": "Mehr erfahren",
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Transkription (Beta) starten.",
@@ -326,15 +334,6 @@
"body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
}
},
"stop": {
"heading": "Transkription läuft...",
"body": "Die Transkription deines Meetings läuft. Du erhältst das Ergebnis per E-Mail, sobald das Meeting beendet ist.",
"button": "Transkription stoppen"
},
"stopping": {
"heading": "Daten werden gespeichert…",
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
},
"premium": {
"heading": "Premium-Funktion",
"body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn Ihre E-Mail-Adresse nicht autorisiert ist, kontaktieren Sie bitte den Support, um Zugriff zu erhalten.",
@@ -350,8 +349,7 @@
"start": "Die Transkription konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
},
"button": "OK"
},
"durationMessage": "(begrenzt auf {{max_duration}}) "
}
},
"screenRecording": {
"start": {

View File

@@ -310,13 +310,21 @@
}
},
"transcript": {
"start": {
"heading": "Transcribe this call",
"body": "Automatically transcribe this call {{duration_message}} and receive the summary in Docs.",
"button": "Start transcription",
"loading": "Transcription starting",
"linkMore": "Learn more"
"heading": "Transcribe your meeting with the AI Assistant",
"body": "Transcribe up to {{max_duration}} of meeting time.",
"bodyWithoutMaxDuration": "Transcribe your meeting without limits.",
"details": {
"receiver": "The transcript will be sent to the host and co-hosts.",
"destination": "A new document will be created on",
"language": "Meeting language: French (fr)",
"recording": "Also start a recording"
},
"button": {
"start": "Start transcribing the meeting",
"stop": "Stop transcribing the meeting",
"saving": "Saving…"
},
"linkMore": "Learn more",
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a transcription (beta).",
@@ -326,15 +334,6 @@
"body": "Only the meeting creator or an admin can start a transcription. Log in to verify your permissions."
}
},
"stop": {
"heading": "Transcription in progress...",
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
"button": "Stop transcription"
},
"stopping": {
"heading": "Saving your data…",
"body": "You can leave the meeting if you wish; the recording will finish automatically."
},
"premium": {
"heading": "Premium feature",
"body": "This feature is reserved for public agents. If your email address is not authorized, please contact support to get access.",
@@ -350,8 +349,7 @@
"start": "We were unable to start the transcription. Please try again in a moment."
},
"button": "OK"
},
"durationMessage": "(limited to {{max_duration}}) "
}
},
"screenRecording": {
"start": {

View File

@@ -310,13 +310,21 @@
}
},
"transcript": {
"start": {
"heading": "Transcrire cet appel",
"body": "Transcrivez cet appel automatiquement {{duration_message}} et recevez le compte rendu dans Docs.",
"button": "Démarrer la transcription",
"loading": "Démarrage de la transcription",
"linkMore": "En savoir plus"
"heading": "Transcrivez votre réunion avec l'Assistant IA",
"body": "Transcrivez jusqu'à {{max_duration}} de réunion.",
"bodyWithoutMaxDuration": "Transcrivez votre réunion sans limite.",
"details": {
"receiver": "La transcription sera envoyée à l'organisateur et aux coorganisateurs.",
"destination": "Un nouveau document sera créé sur",
"language": "Langues de la réunion : Français (fr)",
"recording": "Démarrer aussi un enregistrement"
},
"button": {
"start": "Commencer à transcrire la réunion",
"stop": "Arrêter de transcrire la réunion",
"saving": "Sauvegarde…"
},
"linkMore": "En savoir plus",
"notAdminOrOwner": {
"heading": "Accès restreint",
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer une transcription (beta).",
@@ -326,15 +334,6 @@
"body": "Seul le créateur de la réunion ou un administrateur peut démarrer la transcription. Connectez-vous pour vérifier vos autorisations."
}
},
"stop": {
"heading": "Transcription en cours …",
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
"button": "Arrêter la transcription"
},
"stopping": {
"heading": "Sauvegarde de vos données…",
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
},
"premium": {
"heading": "Fonctionnalité premium",
"body": "Cette fonctionnalité est réservée aux agents publics. Si votre adresse email nest pas autorisée, contactez le support pour obtenir l'accès.",
@@ -350,8 +349,7 @@
"start": "Nous n'avons pas pu démarrer la transcription. Veuillez réessayer dans quelques instants."
},
"button": "OK"
},
"durationMessage": "(limité à {{max_duration}}) "
}
},
"screenRecording": {
"start": {

View File

@@ -310,13 +310,21 @@
}
},
"transcript": {
"start": {
"heading": "Transcribeer dit gesprek",
"body": "Transcribeer dit gesprek automatisch {{duration_message}} en ontvang het verslag in Docs.",
"button": "Transcriptie starten",
"loading": "Transcriptie begint",
"linkMore": "Meer informatie"
"heading": "Transcribeer uw vergadering met de AI-assistent",
"body": "Transcribeer tot {{max_duration}} aan vergadertijd.",
"bodyWithoutMaxDuration": "Transcribeer uw vergadering zonder limiet.",
"details": {
"receiver": "Het transcript wordt verzonden naar de organisator en medeorganisatoren.",
"destination": "Er wordt een nieuw document aangemaakt op",
"language": "Vergadertalen: Frans (fr)",
"recording": "Start ook een opname"
},
"button": {
"start": "Begin met het transcriberen van de vergadering",
"stop": "Stop met het transcriberen van de vergadering",
"saving": "Opslaan…"
},
"linkMore": "Meer informatie",
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een transcriptie starten (beta).",
@@ -326,15 +334,6 @@
"body": "Alleen de maker van de vergadering of een beheerder kan de transcriptie starten. Log in om uw machtigingen te controleren."
}
},
"stop": {
"heading": "Transcriptie bezig...",
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
"button": "Transcriptie stoppen"
},
"stopping": {
"heading": "Uw gegevens worden opgeslagen…",
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
},
"premium": {
"heading": "Premiumfunctie",
"body": "Deze functie is voorbehouden aan openbare medewerkers. Als uw e-mailadres niet is toegestaan, neem dan contact op met de support om toegang te krijgen.",
@@ -350,8 +349,7 @@
"start": "We konden de transcriptie niet starten. Probeer het over enkele ogenblikken opnieuw."
},
"button": "Opnieuw proberen"
},
"durationMessage": "(beperkt tot {{max_duration}}) "
}
},
"screenRecording": {
"start": {