mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 04:09:26 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddb8387c03 |
@@ -24,7 +24,7 @@ RUN npm run build
|
||||
# Inject PostHog sourcemap metadata into the built assets
|
||||
# This metadata is essential for correctly mapping errors to source maps in production
|
||||
RUN set -e && \
|
||||
npx @posthog/cli@0.4.8 sourcemap inject --directory ./dist/assets
|
||||
npx @posthog/cli sourcemap inject --directory ./dist/assets
|
||||
|
||||
COPY ./docker/dinum-frontend/dinum-styles.css \
|
||||
./dist/assets/
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ info:
|
||||
|
||||
#### Authentication Flow
|
||||
|
||||
1. Exchange application credentials for a JWT token via `/external-api/v1.0/application/token`.
|
||||
1. Exchange application credentials for a JWT token via `/external-api/v1.0/applications/token`.
|
||||
2. Use the JWT token in the `Authorization: Bearer <token>` header for all subsequent requests.
|
||||
3. Tokens are scoped and allow applications to act on behalf of specific users.
|
||||
|
||||
@@ -40,7 +40,7 @@ tags:
|
||||
description: Room management operations
|
||||
|
||||
paths:
|
||||
/application/token:
|
||||
/applications/token:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
@@ -283,7 +283,7 @@ components:
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT token obtained from the `/application/token` endpoint.
|
||||
JWT token obtained from the `/applications/token` endpoint.
|
||||
Include in requests as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "0.1.41"
|
||||
version = "0.1.40"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.2.6",
|
||||
|
||||
@@ -30,7 +30,7 @@ class LiveKitTokenAuthentication(authentication.BaseAuthentication):
|
||||
raise exceptions.AuthenticationFailed("Token missing user identity")
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(sub=user_id)
|
||||
user = UserModel.objects.get(id=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
user = AnonymousUser()
|
||||
|
||||
|
||||
@@ -326,11 +326,6 @@ class Base(Configuration):
|
||||
"is_silent_login_enabled": values.BooleanValue(
|
||||
True, environ_name="FRONTEND_IS_SILENT_LOGIN_ENABLED", environ_prefix=None
|
||||
),
|
||||
"idle_disconnect_warning_delay": values.PositiveIntegerValue(
|
||||
None,
|
||||
environ_name="FRONTEND_IDLE_DISCONNECT_WARNING_DELAY",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"feedback": values.DictValue(
|
||||
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
|
||||
),
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.41"
|
||||
version = "0.1.40"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.9.13",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface ApiConfig {
|
||||
custom_css_url?: string
|
||||
use_french_gov_footer?: boolean
|
||||
use_proconnect_button?: boolean
|
||||
idle_disconnect_warning_delay?: number
|
||||
recording?: {
|
||||
is_enabled?: boolean
|
||||
available_modes?: RecordingMode[]
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Button, Dialog, H, P } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import i18n from 'i18next'
|
||||
|
||||
const IDLE_DISCONNECT_TIMEOUT_MS = 120000 // 2 minutes
|
||||
|
||||
export const IsIdleDisconnectModal = () => {
|
||||
const connectionObserverSnap = useSnapshot(connectionObserverStore)
|
||||
const [timeRemaining, setTimeRemaining] = useState(IDLE_DISCONNECT_TIMEOUT_MS)
|
||||
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'isIdleDisconnectModal' })
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionObserverSnap.isIdleDisconnectModalOpen) {
|
||||
setTimeRemaining(IDLE_DISCONNECT_TIMEOUT_MS)
|
||||
const interval = setInterval(() => {
|
||||
setTimeRemaining((prev) => {
|
||||
if (prev <= 1000) {
|
||||
clearInterval(interval)
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
navigateTo('feedback', { duplicateIdentity: false })
|
||||
return 0
|
||||
}
|
||||
return prev - 1000
|
||||
})
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [connectionObserverSnap.isIdleDisconnectModalOpen])
|
||||
|
||||
const minutes = Math.floor(timeRemaining / 1000 / 60)
|
||||
const seconds = (timeRemaining / 1000) % 60
|
||||
const formattedTime = `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={connectionObserverSnap.isIdleDisconnectModalOpen}
|
||||
role="alertdialog"
|
||||
type="alert"
|
||||
aria-label={t('title')}
|
||||
onClose={() => {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
}}
|
||||
>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={css({
|
||||
height: '50px',
|
||||
width: '50px',
|
||||
backgroundColor: 'blue.100',
|
||||
borderRadius: '25px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontWeight: '500',
|
||||
color: 'blue.800',
|
||||
margin: 'auto',
|
||||
})}
|
||||
>
|
||||
{formattedTime}
|
||||
</div>
|
||||
<H lvl={2} centered>
|
||||
{t('title')}
|
||||
</H>
|
||||
<P>
|
||||
{t('body', {
|
||||
duration: humanizeDuration(IDLE_DISCONNECT_TIMEOUT_MS, {
|
||||
language: i18n.language,
|
||||
}),
|
||||
})}
|
||||
</P>
|
||||
<P>{t('settings')}</P>
|
||||
<HStack marginTop="2rem">
|
||||
<Button
|
||||
onPress={() => {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
navigateTo('feedback', { duplicateIdentity: false })
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('leaveButton')}
|
||||
</Button>
|
||||
<Button onPress={close} size="sm" variant="primary">
|
||||
{t('stayButton')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +1,15 @@
|
||||
import {
|
||||
useRemoteParticipants,
|
||||
useRoomContext,
|
||||
} from '@livekit/components-react'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { DisconnectReason, RoomEvent } from 'livekit-client'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
import posthog from 'posthog-js'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export const useConnectionObserver = () => {
|
||||
const room = useRoomContext()
|
||||
const connectionStartTimeRef = useRef<number | null>(null)
|
||||
|
||||
const { data } = useConfig()
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
const idleDisconnectModalTimeoutRef = useRef<ReturnType<
|
||||
typeof setTimeout
|
||||
> | null>(null)
|
||||
|
||||
const remoteParticipants = useRemoteParticipants({
|
||||
updateOnlyOn: [
|
||||
RoomEvent.ParticipantConnected,
|
||||
RoomEvent.ParticipantDisconnected,
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Always clear existing timer on dependency change
|
||||
if (idleDisconnectModalTimeoutRef.current) {
|
||||
clearTimeout(idleDisconnectModalTimeoutRef.current)
|
||||
idleDisconnectModalTimeoutRef.current = null
|
||||
}
|
||||
|
||||
const isEnabled = userPreferencesSnap.is_idle_disconnect_modal_enabled
|
||||
const delay = data?.idle_disconnect_warning_delay
|
||||
|
||||
// Disabled or invalid delay: ensure modal is closed
|
||||
if (!isEnabled || !delay) {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
return
|
||||
}
|
||||
|
||||
if (remoteParticipants.length === 0) {
|
||||
idleDisconnectModalTimeoutRef.current = setTimeout(() => {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = true
|
||||
}, delay)
|
||||
} else {
|
||||
connectionObserverStore.isIdleDisconnectModalOpen = false
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (idleDisconnectModalTimeoutRef.current) {
|
||||
clearTimeout(idleDisconnectModalTimeoutRef.current)
|
||||
idleDisconnectModalTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [
|
||||
remoteParticipants.length,
|
||||
data?.idle_disconnect_warning_delay,
|
||||
userPreferencesSnap.is_idle_disconnect_modal_enabled,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
|
||||
import { Subtitles } from '@/features/subtitle/component/Subtitles'
|
||||
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
||||
import { GridLayout } from '../components/layout/GridLayout'
|
||||
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
||||
|
||||
const LayoutWrapper = styled(
|
||||
'div',
|
||||
@@ -192,7 +191,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
isOpen={isShareErrorVisible}
|
||||
onClose={() => setIsShareErrorVisible(false)}
|
||||
/>
|
||||
<IsIdleDisconnectModal />
|
||||
<div
|
||||
// todo - extract these magic values into constant
|
||||
style={{
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Field, H } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
|
||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export type GeneralTabProps = Pick<TabPanelProps, 'id'>
|
||||
|
||||
@@ -11,8 +9,6 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
|
||||
const { t, i18n } = useTranslation('settings')
|
||||
const { languagesList, currentLanguage } = useLanguageLabels()
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
return (
|
||||
<TabPanel padding={'md'} flex id={id}>
|
||||
<H lvl={2}>{t('language.heading')}</H>
|
||||
@@ -25,20 +21,6 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
|
||||
i18n.changeLanguage(lang as string)
|
||||
}}
|
||||
/>
|
||||
<H lvl={2}>{t('preferences.title')}</H>
|
||||
<Field
|
||||
type="switch"
|
||||
label={t('preferences.idleDisconnectModal.label')}
|
||||
description={t('preferences.idleDisconnectModal.description')}
|
||||
isSelected={userPreferencesSnap.is_idle_disconnect_modal_enabled}
|
||||
onChange={(value) =>
|
||||
(userPreferencesStore.is_idle_disconnect_modal_enabled = value)
|
||||
}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||
import { Field, H } from '@/primitives'
|
||||
import { H, Switch } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
@@ -23,19 +23,15 @@ export const NotificationsTab = ({ id }: NotificationsTabProps) => {
|
||||
{Array.from(notificationsSnap.soundNotifications).map(
|
||||
([key, value]) => (
|
||||
<li key={key}>
|
||||
<Field
|
||||
type="switch"
|
||||
aria-label={`${t(`actions.${value ? 'disable' : 'enable'}`)} ${t('label')} "${t(`items.${key}.label`)}"`}
|
||||
label={t(`items.${key}.label`)}
|
||||
<Switch
|
||||
aria-label={`${t(`actions.${value ? 'disable' : 'enable'}`)} ${t('label')} "${t(`items.${key}`)}"`}
|
||||
isSelected={value}
|
||||
onChange={(v) => {
|
||||
notificationsStore.soundNotifications.set(key, v)
|
||||
}}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{t(`items.${key}`)}
|
||||
</Switch>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -149,13 +149,6 @@
|
||||
"newTab": "Neues Fenster"
|
||||
}
|
||||
},
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Bist du noch da?",
|
||||
"body": "Du bist der einzige Teilnehmer. Dieses Gespräch endet in {{duration}}. Möchtest du das Gespräch fortsetzen?",
|
||||
"settings": "Um diese Nachricht nicht mehr zu sehen, gehe zu Einstellungen > Allgemein.",
|
||||
"stayButton": "Gespräch fortsetzen",
|
||||
"leaveButton": "Jetzt verlassen"
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Mikrofon",
|
||||
"camera": "Kamera",
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"authentication": "Authentifizierung",
|
||||
"nameError": "Ihr Name darf nicht leer sein"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Einstellungen",
|
||||
"idleDisconnectModal": {
|
||||
"label": "Anrufe ohne Teilnehmer verlassen",
|
||||
"description": "Verlässt automatisch einen Anruf nach einigen Minuten, wenn kein anderer Teilnehmer beitritt"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "Mikrofon",
|
||||
@@ -76,15 +69,9 @@
|
||||
"enable": "Aktivieren"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": {
|
||||
"label": "Teilnehmer beigetreten"
|
||||
},
|
||||
"handRaised": {
|
||||
"label": "Hand gehoben"
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Nachricht erhalten"
|
||||
}
|
||||
"participantJoined": "Teilnehmer beigetreten",
|
||||
"handRaised": "Hand gehoben",
|
||||
"messageReceived": "Nachricht erhalten"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@@ -149,13 +149,6 @@
|
||||
"newTab": "New window"
|
||||
}
|
||||
},
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Are you still there?",
|
||||
"body": "You are the only participant. This call will end in {{duration}}. Would you like to continue the call?",
|
||||
"settings": "To stop seeing this message, go to Settings > General.",
|
||||
"stayButton": "Continue the call",
|
||||
"leaveButton": "Leave now"
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microphone",
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"authentication": "Authentication",
|
||||
"nameError": "Your name cannot be empty"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferences",
|
||||
"idleDisconnectModal": {
|
||||
"label": "Leave calls with no participants",
|
||||
"description": "Automatically leaves a call after a few minutes if no other participant joins"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "Microphone",
|
||||
@@ -76,15 +69,9 @@
|
||||
"enable": "Enable"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": {
|
||||
"label": "Participant joined"
|
||||
},
|
||||
"handRaised": {
|
||||
"label": "Hand raised"
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Message received"
|
||||
}
|
||||
"participantJoined": "Participant joined",
|
||||
"handRaised": "Hand raised",
|
||||
"messageReceived": "Message received"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@@ -149,13 +149,6 @@
|
||||
"newTab": "Nouvelle fenêtre"
|
||||
}
|
||||
},
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Êtes-vous toujours là ?",
|
||||
"body": "Vous êtes le seul participant. Cet appel va donc se terminer dans {{duration}}. Voulez-vous poursuivre l'appel ?",
|
||||
"settings": "Pour ne plus voir ce message, accèder à Paramètres > Général.",
|
||||
"stayButton": "Poursuivre l'appel",
|
||||
"leaveButton": "Quitter maintenant"
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microphone",
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"authentication": "Authentification",
|
||||
"nameError": "Votre Nom ne peut pas être vide"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Préférences",
|
||||
"idleDisconnectModal": {
|
||||
"label": "Quitter les appels sans autre participant",
|
||||
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "Micro",
|
||||
@@ -76,15 +69,9 @@
|
||||
"enable": "Activer"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": {
|
||||
"label": "Un nouveau participant"
|
||||
},
|
||||
"handRaised": {
|
||||
"label": "Une main levée"
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Un message reçu"
|
||||
}
|
||||
"participantJoined": "Un nouveau participant",
|
||||
"handRaised": "Une main levée",
|
||||
"messageReceived": "Un message reçu"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@@ -149,13 +149,6 @@
|
||||
"newTab": "Nieuw venster"
|
||||
}
|
||||
},
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Ben je er nog?",
|
||||
"body": "Je bent de enige deelnemer. Dit gesprek wordt over {{duration}} beëindigd. Wil je het gesprek voortzetten?",
|
||||
"settings": "Om dit bericht niet meer te zien, ga naar Instellingen > Algemeen.",
|
||||
"stayButton": "Gesprek voortzetten",
|
||||
"leaveButton": "Nu verlaten"
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Microfoon",
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
"authentication": "Authenticatie",
|
||||
"nameError": "Uw naam mag niet leeg zijn"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Voorkeuren",
|
||||
"idleDisconnectModal": {
|
||||
"label": "Verlaat oproepen zonder deelnemers",
|
||||
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "Microfoon",
|
||||
@@ -76,15 +69,9 @@
|
||||
"enable": "Inschakelen"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": {
|
||||
"label": "Deelnemer is toegevoegd"
|
||||
},
|
||||
"handRaised": {
|
||||
"label": "Hand opgestoken"
|
||||
},
|
||||
"messageReceived": {
|
||||
"label": "Bericht ontvangen"
|
||||
}
|
||||
"participantJoined": "Deelnemer is toegevoegd",
|
||||
"handRaised": "Hand opgestoken",
|
||||
"messageReceived": "Bericht ontvangen"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@@ -28,10 +28,6 @@ const box = cva({
|
||||
width: '30rem',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
alert: {
|
||||
width: '24rem',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
light: {
|
||||
|
||||
@@ -80,7 +80,7 @@ export type DialogProps = RACDialogProps & {
|
||||
* after user interaction
|
||||
*/
|
||||
onOpenChange?: (isOpen: boolean) => void
|
||||
type?: 'flex' | 'alert'
|
||||
type?: 'flex'
|
||||
innerRef?: MutableRefObject<HTMLDivElement | null>
|
||||
size?: 'full' | 'large'
|
||||
}
|
||||
@@ -96,12 +96,7 @@ export const Dialog = ({
|
||||
...dialogProps
|
||||
}: DialogProps) => {
|
||||
const isAlert = dialogProps['role'] === 'alertdialog'
|
||||
const boxType =
|
||||
dialogProps['type'] === 'alert'
|
||||
? 'alert'
|
||||
: dialogProps['type'] !== 'flex'
|
||||
? 'dialog'
|
||||
: undefined
|
||||
const boxType = dialogProps['type'] !== 'flex' ? 'dialog' : undefined
|
||||
return (
|
||||
<StyledModalOverlay
|
||||
isKeyboardDismissDisabled={isAlert}
|
||||
|
||||
@@ -259,7 +259,6 @@ export const Field = <T extends object>({
|
||||
}
|
||||
|
||||
if (type === 'switch') {
|
||||
const SWITCH_COMPONENT_WIDTH = '41px'
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<div
|
||||
@@ -274,11 +273,10 @@ export const Field = <T extends object>({
|
||||
{description && (
|
||||
<Text
|
||||
variant="note"
|
||||
wrap={'balance'}
|
||||
wrap={'pretty'}
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
marginBottom: '0.5rem',
|
||||
marginRight: SWITCH_COMPONENT_WIDTH,
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
isIdleDisconnectModalOpen: boolean
|
||||
}
|
||||
|
||||
export const connectionObserverStore = proxy<State>({
|
||||
isIdleDisconnectModalOpen: false,
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { proxy } from 'valtio'
|
||||
import { subscribe } from 'valtio/index'
|
||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||
|
||||
type State = {
|
||||
is_idle_disconnect_modal_enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
is_idle_disconnect_modal_enabled: true,
|
||||
}
|
||||
|
||||
function getUserPreferencesState(): State {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.USER_PREFERENCES)
|
||||
if (!stored) return DEFAULT_STATE
|
||||
const parsed = JSON.parse(stored)
|
||||
return {
|
||||
...DEFAULT_STATE,
|
||||
...parsed,
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
'[UserPreferencesStore] Failed to parse stored settings:',
|
||||
error
|
||||
)
|
||||
return DEFAULT_STATE
|
||||
}
|
||||
}
|
||||
|
||||
export const userPreferencesStore = proxy<State>(getUserPreferencesState())
|
||||
|
||||
subscribe(userPreferencesStore, () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.USER_PREFERENCES,
|
||||
JSON.stringify(userPreferencesStore)
|
||||
)
|
||||
})
|
||||
@@ -3,5 +3,4 @@
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
NOTIFICATIONS: 'app_notification_settings',
|
||||
USER_PREFERENCES: 'app_user_preferences',
|
||||
} as const
|
||||
|
||||
@@ -57,7 +57,6 @@ backend:
|
||||
FRONTEND_TRANSCRIPT: "{'form_beta_users': 'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'}"
|
||||
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
|
||||
FRONTEND_MANIFEST_LINK: "https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/"
|
||||
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
|
||||
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: meet
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: meet
|
||||
version: 0.0.14
|
||||
version: 0.0.13
|
||||
|
||||
@@ -428,13 +428,6 @@ posthog:
|
||||
## @section summary
|
||||
|
||||
summary:
|
||||
## @param summary.image.repository Repository to use to pull meet's summary container image
|
||||
## @param summary.image.tag meet's summary container tag
|
||||
## @param summary.image.pullPolicy summary container image pull policy
|
||||
image:
|
||||
repository: lasuite/meet-summary
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param summary.dpAnnotations Annotations to add to the summary Deployment
|
||||
dpAnnotations: {}
|
||||
@@ -534,27 +527,11 @@ summary:
|
||||
## @section celeryTranscribe
|
||||
|
||||
celeryTranscribe:
|
||||
## @param celeryTranscribe.image.repository Repository to use to pull meet's celeryTranscribe container image
|
||||
## @param celeryTranscribe.image.tag meet's celeryTranscribe container tag
|
||||
## @param celeryTranscribe.image.pullPolicy celeryTranscribe container image pull policy
|
||||
image:
|
||||
repository: lasuite/meet-summary
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param celeryTranscribe.dpAnnotations Annotations to add to the celeryTranscribe Deployment
|
||||
dpAnnotations: {}
|
||||
|
||||
## @param celeryTranscribe.command Override the celeryTranscribe container command
|
||||
command:
|
||||
- "celery"
|
||||
- "-A"
|
||||
- "summary.core.celery_worker"
|
||||
- "worker"
|
||||
- "--pool=solo"
|
||||
- "--loglevel=info"
|
||||
- "-Q"
|
||||
- "transcribe-queue"
|
||||
command: []
|
||||
|
||||
## @param celeryTranscribe.args Override the celeryTranscribe container args
|
||||
args: []
|
||||
@@ -643,27 +620,11 @@ celeryTranscribe:
|
||||
## @section celerySummarize
|
||||
|
||||
celerySummarize:
|
||||
## @param celerySummarize.image.repository Repository to use to pull meet's celerySummarize container image
|
||||
## @param celerySummarize.image.tag meet's celerySummarize container tag
|
||||
## @param celerySummarize.image.pullPolicy celerySummarize container image pull policy
|
||||
image:
|
||||
repository: lasuite/meet-summary
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
## @param celerySummarize.dpAnnotations Annotations to add to the celerySummarize Deployment
|
||||
dpAnnotations: {}
|
||||
|
||||
## @param celerySummarize.command Override the celerySummarize container command
|
||||
command:
|
||||
- "celery"
|
||||
- "-A"
|
||||
- "summary.core.celery_worker"
|
||||
- "worker"
|
||||
- "--pool=solo"
|
||||
- "--loglevel=info"
|
||||
- "-Q"
|
||||
- "summarize-queue"
|
||||
command: []
|
||||
|
||||
## @param celerySummarize.args Override the celerySummarize container args
|
||||
args: []
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.41",
|
||||
"version": "0.1.40",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.41"
|
||||
version = "0.1.40"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
@@ -123,7 +123,7 @@ class MetadataManager:
|
||||
logger.error("Invalid number of arguments to enable metadata manager.")
|
||||
return
|
||||
|
||||
_, filename, email, _, received_at, *_ = task_args
|
||||
filename, email, _, received_at, *_ = task_args
|
||||
|
||||
initial_metadata = {
|
||||
**initial_metadata,
|
||||
|
||||
@@ -8,6 +8,7 @@ import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Optional
|
||||
import math
|
||||
|
||||
import openai
|
||||
import sentry_sdk
|
||||
@@ -103,6 +104,12 @@ def create_retry_session():
|
||||
session.mount("https://", HTTPAdapter(max_retries=retries))
|
||||
return session
|
||||
|
||||
def add_line_numbers(transcript: str):
|
||||
"""Add line numbers to a text file."""
|
||||
lines = transcript.splitlines()
|
||||
lines = [line for line in lines if line.strip()]
|
||||
numbered_lines = [f"{i + 1}: {line}" for i, line in enumerate(lines)]
|
||||
return "\n".join(numbered_lines)
|
||||
|
||||
class LLMException(Exception):
|
||||
"""LLM call failed."""
|
||||
@@ -386,28 +393,36 @@ def summarize_transcription(self, transcript: str, email: str, sub: str, title:
|
||||
|
||||
llm_service = LLMService()
|
||||
|
||||
tldr = llm_service.call(PROMPT_SYSTEM_TLDR, transcript)
|
||||
token_estimate = len(transcript) // 3.6
|
||||
t = round(0.5 + 2.3 * math.log1p((token_estimate / 3800) ** 2))
|
||||
nb_parts = max(1, t)
|
||||
|
||||
logger.info("TLDR generated")
|
||||
|
||||
transcript = add_line_numbers(transcript)
|
||||
|
||||
logger.info("Transcription with line numbers: \n%s", transcript)
|
||||
parts = llm_service.call(
|
||||
PROMPT_SYSTEM_PLAN, transcript, response_format=FORMAT_PLAN
|
||||
PROMPT_SYSTEM_PLAN.format(nb_parts_min=nb_parts-2, nb_parts_max=nb_parts+2), "Transcript : \n" + transcript, response_format=FORMAT_PLAN
|
||||
)
|
||||
logger.info("Plan generated")
|
||||
|
||||
res = json.loads(parts)
|
||||
parts = res.get("titles", [])
|
||||
parts = res.get("parts", [])
|
||||
|
||||
logger.info("Parts to summarize: %s", parts)
|
||||
parts_summarized = []
|
||||
|
||||
parts_titles = ""
|
||||
for part in parts:
|
||||
prompt_user_part = PROMPT_USER_PART.format(part=part, transcript=transcript)
|
||||
logger.info("Summarizing part: %s", part)
|
||||
parts_summarized.append(llm_service.call(PROMPT_SYSTEM_PART, prompt_user_part))
|
||||
parts_titles += f"- {part['title']}, lignes {part['plages_lignes']}\n"
|
||||
|
||||
raw_summary = llm_service.call(
|
||||
PROMPT_SYSTEM_PART,
|
||||
f"Plan:\n{parts_titles}\n\nTranscript:\n{transcript}",
|
||||
)
|
||||
|
||||
logger.info("Parts summarized")
|
||||
|
||||
raw_summary = "\n\n".join(parts_summarized)
|
||||
|
||||
next_steps = llm_service.call(
|
||||
PROMPT_SYSTEM_NEXT_STEP, transcript, response_format=FORMAT_NEXT_STEPS
|
||||
)
|
||||
@@ -419,7 +434,12 @@ def summarize_transcription(self, transcript: str, email: str, sub: str, title:
|
||||
cleaned_summary = llm_service.call(PROMPT_SYSTEM_CLEANING, raw_summary)
|
||||
logger.info("Summary cleaned")
|
||||
|
||||
summary = tldr + "\n\n" + cleaned_summary + "\n\n" + next_steps
|
||||
tldr = llm_service.call(
|
||||
PROMPT_SYSTEM_TLDR,
|
||||
"transcript :\n" + transcript + "\n\nsummary :\n" + cleaned_summary,
|
||||
)
|
||||
|
||||
summary = tldr + "\n\n" + raw_summary + "\n\n" + next_steps
|
||||
|
||||
data = {
|
||||
"title": settings.summary_title_template.format(
|
||||
|
||||
@@ -4,14 +4,50 @@ PROMPT_SYSTEM_TLDR = """Tu es un agent dont le rôle est de créer un TL;DR (ré
|
||||
### Résumé TL;DR
|
||||
[Résumé concis et structuré]"""
|
||||
|
||||
PROMPT_SYSTEM_PLAN = """Ta tâche est de diviser le contenu du transcript en sujets concrets correspondant aux grands axes discutés durant la réunion. Ne crée pas de catégories génériques. Les titres doivent être courts, précis et représentatifs des échanges. Veille à ce que chaque sujet soit distinct et qu’aucun thème ne soit répété. Tu te limiteras à 5 ou 6 sujets maximum.
|
||||
L'introduction, ordre du jour, conclusion, etc. seront rajoutés a posteriori. Si il n'y a pas de sujets clairs, réponds "Général".
|
||||
"""
|
||||
PROMPT_SYSTEM_PLAN = """Ta tâche consiste à identifier les sujets abordés dans un transcript, c’est-à-dire les thèmes précis et concrets correspondant aux grands axes réellement discutés durant la réunion.
|
||||
- N’inclus pas de catégories génériques.
|
||||
- Les titres doivent être courts, précis et représentatifs des échanges.
|
||||
- Chaque sujet doit être distinct, sans répétition de thèmes.
|
||||
- Tu fera entre {nb_parts_min} et {nb_parts_max} sujets.
|
||||
Pour chaque sujet identifié :
|
||||
- Donne le titre du sujet
|
||||
- Indique la ou les plages de lignes (numérotées) du transcript correspondant à ce sujet (une ou plusieurs par sujet, si nécessaire : [[début, fin], ...]).
|
||||
L’introduction, l’ordre du jour, la conclusion, etc., seront ajoutés ultérieurement.
|
||||
Si aucun sujet clair n’est repéré, réponds exactement : "Général"."""
|
||||
|
||||
PROMPT_SYSTEM_PART = """Tu es un agent dont le rôle est de créer une partie du résumé d'un compte rendu de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript, et le titre du sujet correspondant. Ta tâche est de rédiger un résumé concis de cette partie et uniquement cette partie, en te concentrant uniquement sur les informations essentielles et pertinentes. Le résumé de chaque partie doit tenir en 4 à 6 phrases maximum, sans entrer dans les détails mineurs. Tu répondras dans le format suivant :
|
||||
### Titre du sujet [Traduire ce titre selon la langue du transcript]
|
||||
[Résumé concis et structuré de la partie du transcript]
|
||||
"""
|
||||
|
||||
PROMPT_SYSTEM_PART = """
|
||||
Tu es un agent dont le rôle est de rédiger le résumé complet d’un compte rendu de réunion.
|
||||
Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect.
|
||||
|
||||
En entrée, tu recevras :
|
||||
- L’intégralité du transcript de la réunion,
|
||||
- Un plan indiquant pour chaque sujet : son titre et la plage de lignes correspondante.
|
||||
- Les notes prises par un utilisateur durant la réunion contenant des points important à aborder dans le transcript.
|
||||
|
||||
Consignes :
|
||||
- Générer un résumé pour chacune des parties définies dans le plan.
|
||||
- Pour chaque partie :
|
||||
- Résumer les lignes indiquées et en lien avec le sujet.,
|
||||
- Utiliser exclusivement des bullet points clairs et concis,
|
||||
- Ne pas dépasser 4 ou 5 bullet points par partie,
|
||||
- Se limiter aux informations essentielles et pertinentes,
|
||||
- Éviter tout détail secondaire ou interprétation.
|
||||
- mettre en avant les points écrits dans les notes de l'utilisateur.
|
||||
- mettre en gras les noms propres (personnes, entreprises, produits, etc.)
|
||||
|
||||
Format de sortie attendu : un enchaînement de résumés, un par sujet, par exemple :
|
||||
|
||||
### [Titre du sujet 1]
|
||||
- [Point essentiel 1]
|
||||
- [Point essentiel 2]
|
||||
- [Point essentiel 3]
|
||||
|
||||
### [Titre du sujet 2]
|
||||
- [Point essentiel 1]
|
||||
- [Point essentiel 2]
|
||||
(...)
|
||||
"""
|
||||
|
||||
PROMPT_USER_PART = """Titre de la partie à résumer : {part}
|
||||
Transcript complet :
|
||||
@@ -57,15 +93,37 @@ FORMAT_NEXT_STEPS = {
|
||||
}
|
||||
|
||||
FORMAT_PLAN = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "Titles",
|
||||
"schema": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "PlanWithLineRanges",
|
||||
"strict": True,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["parts"],
|
||||
"properties": {
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {"titles": {"type": "array", "items": {"type": "string"}}},
|
||||
"required": ["titles"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"strict": True,
|
||||
},
|
||||
"required": ["title", "plages_lignes"],
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"plages_lignes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer", "minimum": 1 },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user