From 70403ad0d8fff4c2db65c2193bd83b3dcfdc6bdb Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 2 Jan 2026 12:41:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20handle=20another=20record?= =?UTF-8?q?ing=20mode=20is=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor literals in the recording status hook and introduce a new status. Align the login prompt style with the newly introduced warning message, and guide users by clearly indicating that the two modes are mutually exclusive. Users are prompted to stop the other mode before starting a new one. This situation should happen less often now that checkboxes allow users to start transcription and recording together. Hopefully, the UX is clear enough. The growing number of props passed to the controls buttons may become an issue and will likely require refactoring later. --- .../recording/components/ControlsButton.tsx | 59 ++++++++++++++++++- .../recording/components/LoginPrompt.tsx | 2 + .../components/ScreenRecordingSidePanel.tsx | 3 + .../components/TranscriptSidePanel.tsx | 3 + .../recording/hooks/useRecordingStatuses.ts | 30 ++++++++-- src/frontend/src/locales/de/rooms.json | 6 +- src/frontend/src/locales/en/rooms.json | 6 +- src/frontend/src/locales/fr/rooms.json | 6 +- src/frontend/src/locales/nl/rooms.json | 6 +- src/frontend/src/utils/parseLineBreaks.tsx | 12 ++++ 10 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 src/frontend/src/utils/parseLineBreaks.tsx diff --git a/src/frontend/src/features/recording/components/ControlsButton.tsx b/src/frontend/src/features/recording/components/ControlsButton.tsx index 6b31c593..f719d5bb 100644 --- a/src/frontend/src/features/recording/components/ControlsButton.tsx +++ b/src/frontend/src/features/recording/components/ControlsButton.tsx @@ -1,4 +1,4 @@ -import { css } from '@/styled-system/css' +import { css, cx } from '@/styled-system/css' import { HStack } from '@/styled-system/jsx' import { Spinner } from '@/primitives/Spinner' import { Button, Text } from '@/primitives' @@ -7,6 +7,8 @@ import { RecordingStatuses } from '../hooks/useRecordingStatuses' import { ReactNode, useEffect, useRef, useState } from 'react' import { useRoomContext } from '@livekit/components-react' import { ConnectionState } from 'livekit-client' +import { Button as RACButton } from 'react-aria-components' +import { parseLineBreaks } from '@/utils/parseLineBreaks' const Layout = ({ children }: { children: ReactNode }) => (
void isPendingToStart: boolean isPendingToStop: boolean + openSidePanel: () => void } const MIN_SPINNER_DISPLAY_TIME = 2000 @@ -35,6 +38,7 @@ export const ControlsButton = ({ handle, isPendingToStart, isPendingToStop, + openSidePanel, }: ControlsButtonProps) => { const { t } = useTranslation('rooms', { keyPrefix: i18nKeyPrefix }) @@ -45,7 +49,7 @@ export const ControlsButton = ({ const timeoutRef = useRef() const isSaving = statuses.isSaving || isPendingToStop - const isDisabled = !isRoomConnected + const isDisabled = !isRoomConnected || statuses.isAnotherModeStarted useEffect(() => { if (isSaving) { @@ -103,8 +107,57 @@ export const ControlsButton = ({ // Inactive state (Start button) return ( + {statuses.isAnotherModeStarted && ( + openSidePanel()} + > + + info + + + {parseLineBreaks(t('button.anotherModeStarted'))} + + + chevron_right + + + )}
) diff --git a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx index 15bdbaf8..82661635 100644 --- a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx +++ b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx @@ -32,6 +32,7 @@ import { NoAccessView } from './NoAccessView' import { ControlsButton } from './ControlsButton' import { RowWrapper } from './RowWrapper' import { useMutateRecording } from '../hooks/useMutateRecording' +import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' export const TranscriptSidePanel = () => { const { data } = useConfig() @@ -66,6 +67,7 @@ export const TranscriptSidePanel = () => { const statuses = useRecordingStatuses(RecordingMode.Transcript) const room = useRoomContext() + const { openScreenRecording } = useSidePanel() const handleTranscript = async () => { if (!roomId) { @@ -241,6 +243,7 @@ export const TranscriptSidePanel = () => { statuses={statuses} isPendingToStart={isPendingToStart} isPendingToStop={isPendingToStop} + openSidePanel={openScreenRecording} /> ) diff --git a/src/frontend/src/features/recording/hooks/useRecordingStatuses.ts b/src/frontend/src/features/recording/hooks/useRecordingStatuses.ts index 78ea4df6..38ab40fa 100644 --- a/src/frontend/src/features/recording/hooks/useRecordingStatuses.ts +++ b/src/frontend/src/features/recording/hooks/useRecordingStatuses.ts @@ -2,7 +2,20 @@ import { RecordingMode } from '@/features/recording' import { useRoomMetadata } from './useRoomMetadata' import { useMemo } from 'react' +export enum RecordingStatus { + Starting = 'starting', + Started = 'started', + Saving = 'saving', +} + +const ACTIVE_STATUSES = [ + RecordingStatus.Starting, + RecordingStatus.Started, + RecordingStatus.Saving, +] as const + export interface RecordingStatuses { + isAnotherModeStarted: boolean isStarting: boolean isStarted: boolean isSaving: boolean @@ -17,16 +30,23 @@ export const useRecordingStatuses = ( return useMemo(() => { if (metadata && metadata?.recording_mode === mode) { return { - isStarting: metadata.recording_status === 'starting', - isStarted: metadata.recording_status === 'started', - isSaving: metadata.recording_status === 'saving', - isActive: ['starting', 'started', 'saving'].includes( - metadata.recording_status + isAnotherModeStarted: false, + isStarting: metadata.recording_status === RecordingStatus.Starting, + isStarted: metadata.recording_status === RecordingStatus.Started, + isSaving: metadata.recording_status === RecordingStatus.Saving, + isActive: ACTIVE_STATUSES.includes( + metadata.recording_status as RecordingStatus ), } } + const isAnotherModeStarted = + !!metadata?.recording_mode && + metadata?.recording_mode !== mode && + ACTIVE_STATUSES.includes(metadata.recording_status as RecordingStatus) + return { + isAnotherModeStarted, isStarting: false, isStarted: false, isSaving: false, diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index c9492f10..8b2a5c48 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -324,7 +324,8 @@ "start": "Meeting transkribieren starten", "stop": "Meeting transkribieren beenden", "saving": "Speichern…", - "starting": "Wird gestartet…" + "starting": "Wird gestartet…", + "anotherModeStarted": "Eine Aufnahme läuft.
Wechseln Sie das Menü und stoppen Sie sie." }, "linkMore": "Mehr erfahren", "notAdminOrOwner": { @@ -362,7 +363,8 @@ "start": "Meeting-Aufzeichnung starten", "stop": "Meeting-Aufzeichnung beenden", "saving": "Wird gespeichert…", - "starting": "Wird gestartet…" + "starting": "Wird gestartet…", + "anotherModeStarted": "Eine Transkription läuft.
Wechseln Sie das Menü und stoppen Sie sie." }, "notAdminOrOwner": { "heading": "Zugriff eingeschränkt", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index d996104e..101a9717 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -324,7 +324,8 @@ "start": "Start transcribing the meeting", "stop": "Stop transcribing the meeting", "saving": "Saving…", - "starting": "Starting…" + "starting": "Starting…", + "anotherModeStarted": "Screen recording is running. Switch panels and stop it." }, "linkMore": "Learn more", "notAdminOrOwner": { @@ -362,7 +363,8 @@ "start": "Start recording the meeting", "stop": "Stop recording the meeting", "saving": "Saving…", - "starting": "Starting…" + "starting": "Starting…", + "anotherModeStarted": "Transcription is running.
Switch panels and stop it." }, "notAdminOrOwner": { "heading": "Restricted Access", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 30c5dbc5..31d2e6d3 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -324,7 +324,8 @@ "start": "Commencer à transcrire la réunion", "stop": "Arrêter de transcrire la réunion", "saving": "Sauvegarde…", - "starting": "Démarrage…" + "starting": "Démarrage…", + "anotherModeStarted": "Un enregistrement est en cours.
Changez de menu et arrêtez le." }, "linkMore": "En savoir plus", "notAdminOrOwner": { @@ -362,7 +363,8 @@ "start": "Commencer à enregistrer la réunion", "stop": "Arrêter d'enregistrer la réunion", "saving": "Sauvegarde…", - "starting": "Démarrage…" + "starting": "Démarrage…", + "anotherModeStarted": "Une transcription est en cours.
Changez de menu et arrêtez la." }, "notAdminOrOwner": { "heading": "Accès restreint", diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index dc433fc8..26851cd6 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -324,7 +324,8 @@ "start": "Begin met het transcriberen van de vergadering", "stop": "Stop met het transcriberen van de vergadering", "saving": "Opslaan…", - "starting": "Wordt gestart…" + "starting": "Wordt gestart…", + "anotherModeStarted": "Er loopt een opname.
Ga naar het menu en stop deze." }, "linkMore": "Meer informatie", "notAdminOrOwner": { @@ -362,7 +363,8 @@ "start": "Start met opnemen van de vergadering", "stop": "Stop met opnemen van de vergadering", "saving": "Bezig met opslaan…", - "starting": "Wordt gestart…" + "starting": "Wordt gestart…", + "anotherModeStarted": "Er loopt een transcriptie.
Ga naar het menu en stop deze." }, "notAdminOrOwner": { "heading": "Toegang beperkt", diff --git a/src/frontend/src/utils/parseLineBreaks.tsx b/src/frontend/src/utils/parseLineBreaks.tsx new file mode 100644 index 00000000..335df8c3 --- /dev/null +++ b/src/frontend/src/utils/parseLineBreaks.tsx @@ -0,0 +1,12 @@ +import { Fragment, ReactNode } from 'react' + +export const parseLineBreaks = (text: string): ReactNode[] => { + const parts = text.split(/()/gi) + + return parts.map((part, index) => { + if (part.match(/^$/gi)) { + return
+ } + return {part} + }) +}