Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine cffd658825 🩹(sdk) notify iframe parent when room is gathered through callback
On the first room creation, when the authentication redirection takes
place, the iframe wasn't properly messaging the parent.
Fixed it. Send the room data in any of the two methods to acquire it.

Actually, the code when gathering data through the callback endpoint is
a bit dirty.
2025-04-04 13:18:17 +02:00
40 changed files with 193 additions and 689 deletions
+2 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.18"
version = "0.1.17"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -53,6 +53,7 @@ dependencies = [
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.24.1",
"url-normalize==1.4.3",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
"livekit-api==0.8.2",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "0.1.18",
"version": "0.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.18",
"version": "0.1.17",
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.18",
"version": "0.1.17",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -2,7 +2,7 @@ import { css } from '@/styled-system/css'
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Text, A } from '@/primitives'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { GRIST_FORM } from '@/utils/constants'
export const FeedbackBanner = () => {
const { t } = useTranslation()
@@ -35,7 +35,7 @@ export const FeedbackBanner = () => {
gap: 0.25,
})}
>
<A href={GRIST_FEEDBACKS_FORM} target="_blank" size="sm">
<A href={GRIST_FORM} target="_blank" size="sm">
{t('feedback.cta')}
</A>
<RiExternalLinkLine size={16} aria-hidden="true" />
@@ -8,7 +8,10 @@ import { Button, LinkButton } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BETA_USERS_FORM_URL } from '@/utils/constants'
// todo - extract in a proper env variable
const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'
const Heading = styled('h2', {
base: {
@@ -204,7 +207,6 @@ export const IntroSlider = () => {
{slide.isAvailableInBeta && (
<LinkButton
href={BETA_USERS_FORM_URL}
target="_blank"
tooltip={t('beta.tooltip')}
variant={'primary'}
size={'sm'}
@@ -88,16 +88,6 @@ export const MainNotificationToast = () => {
if (notification.data?.emoji)
handleEmoji(notification.data.emoji, participant)
break
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
toastQueue.add(
{
participant,
type: notification.type,
},
{ timeout: NotificationDuration.ALERT }
)
break
default:
return
}
@@ -6,6 +6,4 @@ export enum NotificationType {
LowerHand = 'lowerHand',
ReactionReceived = 'reactionReceived',
ParticipantWaiting = 'participantWaiting',
TranscriptionStarted = 'transcriptionStarted',
TranscriptionStopped = 'transcriptionStopped',
}
@@ -9,7 +9,6 @@ import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastTranscript } from './ToastTranscript'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -37,10 +36,6 @@ const renderToast = (
case NotificationType.LowerHand:
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
return <ToastTranscript key={toast.key} toast={toast} state={state} />
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -1,33 +0,0 @@
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 ToastTranscript({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant
const type = props.toast.content.type
const key = `recording${type == NotificationType.TranscriptionStarted ? 'Started' : 'Stopped'}`
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
{t(key, {
name: participant.name,
})}
</HStack>
</StyledToastContainer>
)
}
@@ -0,0 +1,43 @@
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>
)
}
@@ -3,15 +3,15 @@ import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat'
import { Transcript } from './Transcript'
import { Effects } from './effects/Effects'
import { Admin } from './Admin'
import { Tools } from './Tools'
type StyledSidePanelProps = {
title: string
@@ -19,8 +19,6 @@ type StyledSidePanelProps = {
onClose: () => void
isClosed: boolean
closeButtonTooltip: string
isSubmenu: boolean
onBack: () => void
}
const StyledSidePanel = ({
@@ -29,8 +27,6 @@ const StyledSidePanel = ({
onClose,
isClosed,
closeButtonTooltip,
isSubmenu = false,
onBack,
}: StyledSidePanelProps) => (
<div
className={css({
@@ -65,22 +61,9 @@ const StyledSidePanel = ({
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
display: isClosed ? 'none' : undefined,
}}
>
{isSubmenu && (
<Button
variant="secondaryText"
size={'sm'}
square
className={css({ marginRight: '0.5rem' })}
onPress={onBack}
>
<RiArrowLeftLine size={20} />
</Button>
)}
{title}
</Heading>
<Div
@@ -131,26 +114,19 @@ export const SidePanel = () => {
isEffectsOpen,
isChatOpen,
isSidePanelOpen,
isToolsOpen,
isTranscriptOpen,
isAdminOpen,
isSubPanelOpen,
activeSubPanelId,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
title={t(`heading.${activePanelId}`)}
onClose={() => (layoutStore.activePanelId = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`),
content: t(`content.${activePanelId}`),
})}
isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen}
onBack={() => (layoutStore.activeSubPanelId = null)}
>
<Panel isOpen={isParticipantsOpen}>
<ParticipantsList />
@@ -161,8 +137,8 @@ export const SidePanel = () => {
<Panel isOpen={isChatOpen}>
<Chat />
</Panel>
<Panel isOpen={isToolsOpen}>
<Tools />
<Panel isOpen={isTranscriptOpen}>
<Transcript />
</Panel>
<Panel isOpen={isAdminOpen}>
<Admin />
@@ -1,113 +0,0 @@
import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { CRISP_HELP_ARTICLE_MORE_TOOLS } from '@/utils/constants'
import { ReactNode } from 'react'
import { Transcript } from './Transcript'
import { RiFileTextFill } from '@remixicon/react'
import { useIsTranscriptEnabled } from '../hooks/useIsTranscriptEnabled'
import { useSidePanel } from '../hooks/useSidePanel'
export interface ToolsButtonProps {
icon: ReactNode
title: string
description: string
onPress: () => void
}
const ToolButton = ({
icon,
title,
description,
onPress,
}: ToolsButtonProps) => {
return (
<RACButton
className={css({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start',
paddingY: '0.5rem',
paddingX: '0.75rem 1.5rem',
borderRadius: '5px',
gap: '1.25rem',
width: 'full',
textAlign: 'start',
'&[data-hovered]': {
backgroundColor: 'primary.50',
cursor: 'pointer',
},
})}
onPress={onPress}
>
<div
className={css({
height: '50px',
minWidth: '50px',
borderRadius: '25px',
backgroundColor: 'primary.800',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
{icon}
</div>
<div>
<Text margin={false} as="h3">
{title}
</Text>
<Text as="p" variant="smNote" wrap="pretty">
{description}
</Text>
</div>
</RACButton>
)
}
export const Tools = () => {
const { openTranscript, isTranscriptOpen } = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
const isTranscriptEnabled = useIsTranscriptEnabled()
if (isTranscriptOpen && isTranscriptEnabled) {
return <Transcript />
}
return (
<Div
display="flex"
overflowY="scroll"
padding="0 0.75rem"
flexGrow={1}
flexDirection="column"
alignItems="start"
>
<Text
variant="note"
wrap="balance"
className={css({
textStyle: 'sm',
paddingX: '0.75rem',
})}
margin="md"
>
{t('body')}{' '}
<A href={CRISP_HELP_ARTICLE_MORE_TOOLS} target="_blank">
{t('moreLink')}
</A>
.
</Text>
{isTranscriptEnabled && (
<ToolButton
icon={<RiFileTextFill size={24} color="white" />}
title={t('tools.transcript.title')}
description={t('tools.transcript.body')}
onPress={() => openTranscript()}
/>
)}
</Div>
)
}
@@ -1,7 +1,10 @@
import { A, Button, Div, LinkButton, Text } from '@/primitives'
import { Button, Div, H, Text } from '@/primitives'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { css } from '@/styled-system/css'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import { RiRecordCircleLine, RiStopCircleLine } from '@remixicon/react'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
@@ -9,34 +12,22 @@ import {
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
import { NotificationType } from '@/features/notifications/NotificationType'
import { useSnapshot } from 'valtio/index'
import {
TranscriptionStatus,
transcriptionStore,
} from '@/stores/transcription.ts'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import {
BETA_USERS_FORM_URL,
CRISP_HELP_ARTICLE_TRANSCRIPT,
} from '@/utils/constants'
export const Transcript = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
const hasTranscriptAccess = useHasTranscriptAccess()
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const transcriptionSnap = useSnapshot(transcriptionStore)
const room = useRoomContext()
useEffect(() => {
@@ -49,17 +40,6 @@ export const Transcript = () => {
}
}, [room])
const notifyParticipant = async (status: NotificationType) => {
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')
@@ -69,12 +49,8 @@ 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)
@@ -82,13 +58,7 @@ export const Transcript = () => {
}
}
const isDisabled = useMemo(
() =>
isLoading ||
transcriptionSnap.status === TranscriptionStatus.STARTING ||
transcriptionSnap.status === TranscriptionStatus.STOPPING,
[isLoading, transcriptionSnap]
)
if (!hasTranscriptAccess) return
return (
<Div
@@ -99,98 +69,38 @@ export const Transcript = () => {
flexDirection="column"
alignItems="center"
>
<img
src={thirdSlide}
alt={''}
className={css({
minHeight: '309px',
marginBottom: '1rem',
})}
/>
{!hasTranscriptAccess ? (
<img src={thirdSlide} alt={'wip'} />
{room.isRecording ? (
<>
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
<H lvl={2}>{t('stop.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('stop.body')}
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
>
{t('beta.button')}
</LinkButton>
<RiStopCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('stop.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_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
<H lvl={2}>{t('start.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('start.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="start-transcript"
>
<RiRecordCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('start.button')}
</Button>
</>
)}
</Div>
@@ -1,103 +0,0 @@
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>
)
}
@@ -2,14 +2,14 @@ import { RiMegaphoneLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { GRIST_FORM } from '@/utils/constants'
export const FeedbackMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
return (
<MenuItem
href={GRIST_FEEDBACKS_FORM}
href={GRIST_FORM}
target="_blank"
className={menuRecipe({ icon: true, variant: 'dark' }).item}
>
@@ -1,19 +1,24 @@
import { ToggleButton } from '@/primitives'
import { RiShapesLine } from '@remixicon/react'
import { RiBardLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useSidePanel } from '../../hooks/useSidePanel'
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
import { css } from '@/styled-system/css'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const ToolsToggle = ({
export const TranscriptToggle = ({
variant = 'primaryTextDark',
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.tools' })
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
const { isToolsOpen, toggleTools } = useSidePanel()
const tooltipLabel = isToolsOpen ? 'open' : 'closed'
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
const tooltipLabel = isTranscriptOpen ? 'open' : 'closed'
const hasTranscriptAccess = useHasTranscriptAccess()
if (!hasTranscriptAccess) return
return (
<div
@@ -27,15 +32,15 @@ export const ToolsToggle = ({
variant={variant}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isToolsOpen}
isSelected={isTranscriptOpen}
onPress={(e) => {
toggleTools()
toggleTranscript()
onPress?.(e)
}}
{...props}
data-attr="toggle-tools"
data-attr="toggle-transcript"
>
<RiShapesLine />
<RiBardLine />
</ToggleButton>
</div>
)
@@ -5,74 +5,53 @@ export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TOOLS = 'tools',
ADMIN = 'admin',
}
export enum SubPanelId {
TRANSCRIPT = 'transcript',
ADMIN = 'admin',
}
export const useSidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const activePanelId = layoutSnap.activePanelId
const activeSubPanelId = layoutSnap.activeSubPanelId
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId == PanelId.EFFECTS
const isChatOpen = activePanelId == PanelId.CHAT
const isToolsOpen = activePanelId == PanelId.TOOLS
const isTranscriptOpen = activePanelId == PanelId.TRANSCRIPT
const isAdminOpen = activePanelId == PanelId.ADMIN
const isTranscriptOpen = activeSubPanelId == SubPanelId.TRANSCRIPT
const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId
const toggleAdmin = () => {
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleParticipants = () => {
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleChat = () => {
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleEffects = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleTools = () => {
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const openTranscript = () => {
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
layoutStore.activePanelId = PanelId.TOOLS
const toggleTranscript = () => {
layoutStore.activePanelId = isTranscriptOpen ? null : PanelId.TRANSCRIPT
}
return {
activePanelId,
activeSubPanelId,
toggleParticipants,
toggleChat,
toggleEffects,
toggleTools,
toggleTranscript,
toggleAdmin,
openTranscript,
isSubPanelOpen,
isChatOpen,
isParticipantsOpen,
isEffectsOpen,
isSidePanelOpen,
isToolsOpen,
isAdminOpen,
isTranscriptOpen,
isAdminOpen,
}
}
@@ -14,7 +14,6 @@ import {
RiMore2Line,
RiSettings3Line,
} from '@remixicon/react'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
@@ -22,7 +21,7 @@ import { useSidePanel } from '../../hooks/useSidePanel'
import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
import { ResponsiveMenu } from './ResponsiveMenu'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
export function MobileControlBar({
@@ -134,7 +133,7 @@ export function MobileControlBar({
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<ToolsToggle
<TranscriptToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
@@ -151,7 +150,7 @@ export function MobileControlBar({
<RiAccountBoxLine size={20} />
</Button>
<LinkButton
href={GRIST_FEEDBACKS_FORM}
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
variant="primaryTextDark"
tooltip={t('options.items.feedback')}
aria-label={t('options.items.feedback')}
@@ -1,7 +1,7 @@
import { css } from '@/styled-system/css'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { AdminToggle } from '../../components/AdminToggle'
import { useSize } from '../../hooks/useResizeObserver'
import { useState, RefObject } from 'react'
@@ -20,7 +20,7 @@ const NavigationControls = ({
<>
<ChatToggle onPress={onPress} tooltipType={tooltipType} />
<ParticipantsToggle onPress={onPress} tooltipType={tooltipType} />
<ToolsToggle onPress={onPress} tooltipType={tooltipType} />
<TranscriptToggle onPress={onPress} tooltipType={tooltipType} />
<AdminToggle onPress={onPress} tooltipType={tooltipType} />
</>
)
@@ -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 { TranscriptStateToast } from '../components/TranscriptStateToast'
import { RecordingStateToast } from '../components/RecordingStateToast'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
const LayoutWrapper = styled(
@@ -231,7 +231,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)}
<RoomAudioRenderer />
<ConnectionStateToast />
<TranscriptStateToast />
<RecordingStateToast />
</div>
)
}
@@ -78,7 +78,7 @@ export const CreateMeetingButton = () => {
if (isPending) {
return (
<div>
<Spinner size={34} />
<Spinner />
</div>
)
}
@@ -21,7 +21,5 @@
"several": "",
"open": "",
"accept": ""
},
"recordingStarted": "",
"recordingStopped": ""
}
}
+5 -27
View File
@@ -104,7 +104,7 @@
"open": "",
"closed": ""
},
"tools": {
"transcript": {
"open": "",
"closed": ""
},
@@ -169,45 +169,27 @@
"effects": "",
"chat": "",
"transcript": "",
"admin": "",
"tools": ""
"admin": ""
},
"content": {
"participants": "",
"effects": "",
"chat": "",
"transcript": "",
"admin": "",
"tools": ""
"admin": ""
},
"closeButton": ""
},
"chat": {
"disclaimer": ""
},
"moreTools": {
"body": "",
"moreLink": "",
"tools": {
"transcript": {
"title": "",
"body": ""
}
}
},
"transcript": {
"start": {
"heading": "",
"body": "",
"button": "",
"linkMore": ""
},
"stop": {
"heading": "",
"body": "",
"button": ""
},
"beta": {
"stop": {
"heading": "",
"body": "",
"button": ""
@@ -293,11 +275,7 @@
}
},
"recording": {
"transcript": {
"started": "",
"starting": "",
"stopping": ""
}
"label": ""
},
"participantTileFocus": {
"pin": {
@@ -21,7 +21,5 @@
"several": "Several people want to join this call.",
"open": "Open",
"accept": "Accept"
},
"recordingStarted": "{{name}} started the meeting transcription.",
"recordingStopped": "{{name}} stopped the meeting transcription."
}
}
+14 -36
View File
@@ -103,9 +103,9 @@
"open": "Hide everyone",
"closed": "See everyone"
},
"tools": {
"open": "Hide more tools",
"closed": "Show more tools"
"transcript": {
"open": "Hide AI assistant",
"closed": "Show AI assistant"
},
"admin": {
"open": "Hide admin",
@@ -167,49 +167,31 @@
"participants": "Participants",
"effects": "Effects",
"chat": "Messages in the chat",
"transcript": "Transcription",
"admin": "Admin settings",
"tools": "More tools"
"transcript": "AI Assistant",
"admin": "Admin settings"
},
"content": {
"participants": "participants",
"effects": "effects",
"chat": "messages",
"transcript": "Transcription",
"admin": "admin settings",
"tools": "more tools"
"transcript": "AI assistant",
"admin": "Admin settings"
},
"closeButton": "Hide {{content}}"
},
"chat": {
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"moreTools": {
"body": "Access more tools in Visio to enhance your meetings,",
"moreLink": "learn more",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcribe your meeting for later"
}
}
},
"transcript": {
"start": {
"heading": "Transcribe this call",
"body": "Automatically transcribe this call and receive the summary in Docs.",
"button": "Start transcription",
"linkMore": "Learn more"
"heading": "Start the Assistant!",
"body": "The assistant automatically starts recording your meeting audio (limited to 1 hour). At the end, you'll receive a clear and concise summary of the discussion directly via email.",
"button": "Start"
},
"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"
},
"beta": {
"heading": "Become a beta tester",
"body": "Record your meeting for later. You will receive a summary by email once the meeting is finished.",
"button": "Sign up"
"heading": "Recording in Progress...",
"body": "Your meeting is currently being recorded. You will receive a summary via email once the meeting ends.",
"button": "Stop Recording"
}
},
"admin": {
@@ -292,11 +274,7 @@
}
},
"recording": {
"transcript": {
"started": "Transcribing",
"starting": "Transcription starting",
"stopping": "Transcription stopping"
}
"label": "Recording"
},
"participantTileFocus": {
"pin": {
@@ -21,7 +21,5 @@
"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."
}
}
+14 -36
View File
@@ -103,9 +103,9 @@
"open": "Masquer les participants",
"closed": "Afficher les participants"
},
"tools": {
"open": "Masquer plus d'outils",
"closed": "Afficher plus d'outils"
"transcript": {
"open": "Masquer l'assistant IA",
"closed": "Afficher l'assistant IA"
},
"admin": {
"open": "Masquer l'admin",
@@ -167,49 +167,31 @@
"participants": "Participants",
"effects": "Effets",
"chat": "Messages dans l'appel",
"transcript": "Transcription",
"admin": "Commandes de l'organisateur",
"tools": "Plus d'outils"
"transcript": "Assistant IA",
"admin": "Commandes de l'organisateur"
},
"content": {
"participants": "les participants",
"effects": "les effets",
"chat": "les messages",
"transcript": "transcription",
"admin": "commandes de l'organisateur",
"tools": "plus d'outils"
"transcript": "l'assistant IA",
"admin": "Commandes de l'organisateur"
},
"closeButton": "Masquer {{content}}"
},
"chat": {
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"moreTools": {
"body": "Accèder à d'avantage d'outils dans Visio pour améliorer vos réunions,",
"moreLink": "en savoir plus",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcrivez votre réunion pour plus tard"
}
}
},
"transcript": {
"start": {
"heading": "Transcrire cet appel",
"body": "Transcrivez cet appel automatiquement et recevez le compte rendu dans Docs.",
"button": "Démarrer la transcription",
"linkMore": "En savoir plus"
"heading": "Démarrer l'assistant !",
"body": "L'assistant démarre automatiquement l'enregistrement sonore de votre réunion (limité à 1h). À la fin, vous recevrez un résumé clair et concis des échanges directement par e-mail.",
"button": "Démarrer"
},
"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"
},
"beta": {
"heading": "Devenez beta testeur",
"body": "Enregistrer votre réunion pour plus tard. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Inscrivez-vous"
"heading": "Enregistrement en cours …",
"body": "L'enregistrement de votre réunion est en cours. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Arrêter l'enregistrement"
}
},
"admin": {
@@ -292,11 +274,7 @@
}
},
"recording": {
"transcript": {
"started": "Transcription en cours",
"starting": "Démarrage de la transcription",
"stopping": "Arrêt de la transcription"
}
"label": "Enregistrement"
},
"participantTileFocus": {
"pin": {
@@ -21,7 +21,5 @@
"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."
}
}
+14 -36
View File
@@ -103,9 +103,9 @@
"open": "Verberg iedereen",
"closed": "Toon iedereen"
},
"tools": {
"open": "Meer tools verbergen",
"closed": "Meer tools weergeven"
"transcript": {
"open": "Verberg AI-assistent",
"closed": "Toon AI-assistant"
},
"admin": {
"open": "Verberg beheerder",
@@ -167,49 +167,31 @@
"participants": "Deelnemers",
"effects": "Effecten",
"chat": "Berichten in de chat",
"transcript": "Transcriptie",
"admin": "Beheerdersbediening",
"tools": "Meer tools"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"content": {
"participants": "deelnemers",
"effects": "effecten",
"chat": "berichten",
"transcript": "transcriptie",
"admin": "beheerdersbediening",
"tools": "meer tools"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"closeButton": "Verberg {{content}}"
},
"chat": {
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek."
},
"moreTools": {
"body": "Toegang tot meer tools in Visio om je vergaderingen te verbeteren,",
"moreLink": "lees meer",
"tools": {
"transcript": {
"title": "Transcriptie",
"body": "Transcribeer je vergadering voor later"
}
}
},
"transcript": {
"start": {
"heading": "Transcribeer dit gesprek",
"body": "Transcribeer dit gesprek automatisch en ontvang het verslag in Docs.",
"button": "Transcriptie starten",
"linkMore": "Meer informatie"
"heading": "Start de assistent!",
"body": "De assistent begint automatisch de audio van uw vergadering op te nemen (beperkt tot 1 uur). Na afloop krijgt u direct een heldere en beknopte samenvatting van de discussies in uw e-mail.",
"button": "Start"
},
"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"
},
"beta": {
"heading": "Word betatester",
"body": "Neem uw vergadering op voor later. U ontvangt een samenvatting per e-mail zodra de vergadering is afgelopen.",
"button": "Aanmelden"
"heading": "Opname loopt ...",
"body": "Uw vergadering wordt momenteel opgenomen. U ontvangt een samenvatting via e-mail, zo gauw de vergarding gesloten wordt.",
"button": "Stop met opname"
}
},
"admin": {
@@ -292,11 +274,7 @@
}
},
"recording": {
"transcript": {
"started": "Transcriptie bezig",
"starting": "Transcriptie begint",
"stopping": "Transcriptie stopt"
}
"label": "Opnemen"
},
"participantTileFocus": {
"pin": {
@@ -9,7 +9,6 @@ type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
target?: string
}
export const LinkButton = ({
+5 -11
View File
@@ -1,13 +1,7 @@
import { ProgressBar } from 'react-aria-components'
import { css } from '@/styled-system/css'
export const Spinner = ({
size = 56,
variant = 'light',
}: {
size?: number
variant?: 'light' | 'dark'
}) => {
export const Spinner = () => {
const center = 14
const strokeWidth = 3
const r = 14 - strokeWidth
@@ -17,8 +11,8 @@ export const Spinner = ({
{({ percentage }) => (
<>
<svg
width={size}
height={size}
width={56}
height={56}
viewBox="0 0 28 28"
fill="none"
strokeWidth={strokeWidth}
@@ -31,7 +25,7 @@ export const Spinner = ({
strokeDashoffset={0}
strokeLinecap="round"
className={css({
stroke: variant == 'light' ? 'primary.100' : 'primaryDark.100',
stroke: 'primary.100',
})}
style={{}}
/>
@@ -43,7 +37,7 @@ export const Spinner = ({
strokeDashoffset={percentage && c - (percentage / 100) * c}
strokeLinecap="round"
className={css({
stroke: variant == 'light' ? 'primary.800' : 'white',
stroke: 'primary.800',
})}
style={{
animation: `rotate 1s ease-in-out infinite`,
@@ -42,7 +42,6 @@ export const buttonRecipe = cva({
primary: {
backgroundColor: 'primary.800',
color: 'white',
fontWeight: 'medium !important',
'&[data-hovered]': {
backgroundColor: 'primary.action',
},
@@ -57,7 +56,6 @@ export const buttonRecipe = cva({
secondary: {
backgroundColor: 'white',
color: 'primary.800',
fontWeight: 'medium !important',
borderColor: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
@@ -115,7 +113,6 @@ export const buttonRecipe = cva({
},
tertiary: {
backgroundColor: 'primary.100',
fontWeight: 'medium !important',
color: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -123,14 +120,9 @@ export const buttonRecipe = cva({
'&[data-pressed]': {
backgroundColor: 'primary.300',
},
'&[data-disabled]': {
backgroundColor: 'transparent',
color: 'primary.400',
},
},
tertiaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'primary.900',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -141,7 +133,6 @@ export const buttonRecipe = cva({
},
primaryDark: {
backgroundColor: 'primaryDark.100',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.900',
@@ -158,7 +149,6 @@ export const buttonRecipe = cva({
},
secondaryDark: {
backgroundColor: 'primaryDark.50',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.200',
@@ -174,7 +164,6 @@ export const buttonRecipe = cva({
},
primaryTextDark: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'white',
'&[data-hovered]': {
backgroundColor: 'primaryDark.100',
@@ -190,7 +179,6 @@ export const buttonRecipe = cva({
},
quaternaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'greyscale.600',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
+1 -6
View File
@@ -1,19 +1,14 @@
import { proxy } from 'valtio'
import {
PanelId,
SubPanelId,
} from '@/features/rooms/livekit/hooks/useSidePanel'
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
type State = {
showHeader: boolean
showFooter: boolean
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
}
export const layoutStore = proxy<State>({
showHeader: false,
showFooter: false,
activePanelId: null,
activeSubPanelId: null,
})
-16
View File
@@ -1,16 +0,0 @@
import { proxy } from 'valtio'
export enum TranscriptionStatus {
STARTING,
STARTED,
STOPPING,
STOPPED,
}
type State = {
status: TranscriptionStatus
}
export const transcriptionStore = proxy<State>({
status: TranscriptionStatus.STOPPED,
})
+2 -11
View File
@@ -1,11 +1,2 @@
export const GRIST_FEEDBACKS_FORM =
'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26' as const
export const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17' as const
export const CRISP_HELP_ARTICLE_MORE_TOOLS =
'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23' as const
export const CRISP_HELP_ARTICLE_TRANSCRIPT =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x' as const
export const GRIST_FORM =
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4' as const
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.18",
"version": "0.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.18",
"version": "0.1.17",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.18",
"version": "0.1.17",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.18",
"version": "0.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.18",
"version": "0.1.17",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.18",
"version": "0.1.17",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.18"
version = "0.1.17"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",