Compare commits

...

34 Commits

Author SHA1 Message Date
lebaudantoine 4232c0a303 🔖(patch) bump release to 0.1.12
Release a fix while calling Docs backend API.
2024-12-18 22:54:57 +01:00
lebaudantoine a5454e48b7 🔥(frontend) remove redundant yarn.lock file
We decided to stick to npm for dependencies management.
Having both package-lock.json and yarn.lock isn't clear for
developers, simplify the stack.
2024-12-17 18:53:55 +01:00
dependabot[bot] 591706f363 🔒️(frontend) bump nanoid from 3.3.7 to 3.3.8 in /src/frontend
3.3.8 fixed a way to break Nano ID by passing non-integer size.
Raised as a moderate severity issue by Dependabot.
2024-12-17 18:53:55 +01:00
dependabot[bot] 1ea84b6e6c 🔒️(mail) bump cross-spawn to fix a high severity issue
Bump cross-spawn from 7.0.3 to 7.0.6 in /src/mail
Alert triggered by DependaBot.
2024-12-17 16:43:54 +01:00
renovate[bot] c0d101a326 ⬆️(dependencies) update PyJWT to v2.10.1 [SECURITY] 2024-12-17 16:23:05 +01:00
lebaudantoine 64cfcb6c0f 💩(summary) fix temporarily the summary microservice
The new endpoint requires title and content, breaking
the original implementation. This hotfix ensures staging works
immediately while I plan an LLM-based solution
for title generation.
2024-12-17 11:34:46 +01:00
lebaudantoine cdd69b741a 🚑️(mail) bump package version
Oopsie, I forgot to bump mail package version.
Fix it
2024-12-17 11:34:46 +01:00
Jacques ROUSSEL f5a87cc210 🔐(helm) bump secret in staging
We add a typo in secret so we bump secrets to fix it.
2024-12-17 10:19:40 +01:00
lebaudantoine 4d4ddb9ee8 🔖(patch) bump release to 0.1.11
Weekly release, responsiveness is still under construction.
2024-12-17 00:26:29 +01:00
lebaudantoine 70dbf94f7b 🩹(frontend) avoid duplicating Grist form's URL
It led to an error, with two occurrences of the form's URL being
desynchronized. Fix this minor issue, and refactor the constant
in a constant file to be shared across the app.
2024-12-17 00:08:53 +01:00
Jacques ROUSSEL 0ad37ee6de 🔨(tilt) improve local stack
Improve the local tilt file in order to be abble to start all thing
without any dependencies to DINUM environment
2024-12-16 23:41:09 +01:00
Jacques ROUSSEL ed4f7dcf6c 🔐(helm) add production secrets for recording and transcription
Added all missing secrets for the recording feature
and transcription microservice.
2024-12-16 23:08:04 +01:00
lebaudantoine c54773008c 🚨(backend) fix backend linter
I wrongly merged a backend pr, and broke the linter.
2024-12-16 23:08:04 +01:00
lebaudantoine 6848321bcc 🔧(helm) configure transcription microservice in production
Added one replica for the summary and Celery deployments.
Secret will be added in the upcoming commits.
2024-12-16 23:08:04 +01:00
lebaudantoine f161a5cf6a 🔧(helm) configure recording in production
Enabled recording feature in production. MinIO needs to trigger a webhook
when a new recording is saved. Secret will be updated in the upcoming commits.
2024-12-16 23:08:04 +01:00
lebaudantoine 51270571bc 🔧(helm) update transcription microservice in staging
Updated the webhook URL to the definitive version in docs.

Jacques also updated the webhook secret for authentication
against Impress API. Not tested locally.
2024-12-16 23:08:04 +01:00
Nathan Vasse c6fdeaf1e9 📱(front) add responsive menu on mobile
We want to have a specific responsive menu on mobile browsers.
It also implied to refactor a bit the way the settings modals is opened
because it could be opened from this responsive menu, so in order to achive
that a specific context has been created in order to allow its opening
from any sub component of the control bar.
2024-12-13 15:41:54 +01:00
Nathan Vasse f1959cbb3a ♻️(front) add more customization and event handler to toggles
We want to be able to customize the variant which those toggle uses as
well as being able to trigger an event when the toggle is pressed. This
is going to be useful to close the responsive menu after each clic as
react-aria prevent click event propgation.
2024-12-13 15:41:54 +01:00
Nathan Vasse 84cea2f658 (front) add description to Buttons
We want to display full description below buttons for mobile specific
displays.
2024-12-13 15:41:54 +01:00
Nathan Vasse db1fdb9871 (front) add useIsMobile hook
This hook tells whether the current browser is a mobile one or not.
2024-12-13 15:41:54 +01:00
lebaudantoine a8618239d1 🔧(frontend) enable dynacast optimizations
According to the documentation:

Dynacast dynamically pauses video layers that are
not being consumed by any subscribers, significantly
reducing publishing CPU and bandwidth usage.

Dynacast will be enabled if SVC codecs (VP9/AV1) are used.
Multi-codec simulcast requires dynacast

My goal is to reduce CPU and bandwidth usage for clients.

Dynacast is enabled both in OpenTalk and LiveKit demo app!
2024-12-12 11:25:06 +01:00
lebaudantoine 0eb283b75a 🔧(frontend) enable adaptive stream optimizations
Adaptive stream is a key optimization in large room.

Enabled adaptive stream to automatically manage
the quality of subscribed video tracks, optimizing
for bandwidth and CPU usage.

When video elements are visible, it adjusts
the resolution based on the size of the largest visible
element. If no video elements are visible, it
temporarily pauses the track until they are visible again.

Additionally, introduced support for custom pixel density,
which defaults to `2` for high-density screens
(devicePixelRatio ≥ 3) or `1` for standard screens.
Setting it to `screen` allows the pixel density to match
the actual screen's devicePixelRatio, optimizing video
clarity on ultra-high-definition displays.

This ensures a balance between streaming quality
and resource consumption.

This might also significantly increase the bandwidth
consumed by people streaming on high definition screens.
It needs to be battle tested.

OpenTalk uses a adaptiveStream equals true, while LiveKit
demo app uses 'screen' value. I followed OpenTalk choice,
I was scared 'screen' value creates performance issues
for user with high resolution screen in poor network conditions.
2024-12-12 11:25:06 +01:00
lebaudantoine 0104cabc5e 🔧(frontend) default to vp9 for video codec
As a mandatory codec in WebRTC specifications, VP8 serves as
the baseline for compatibility, making it the default choice
in LiveKit client configuration.

There is room for optimization.

Newer codecs like VP9 offer significant efficiency gains
compared to VP8, with a 23-33% improvement in compression
efficiency. This translates to better video quality at
the same bitrate or reduced bandwidth usage for the same quality.

VP9 is supported in Safari starting from version 15.0+,
and Firefox offers partial support. However, Firefox lacks
support for VP9's Scalable Video Coding (SVC).

With SVC, participants can send a single VP9 stream
with multiple spatial or temporal layers. This allows receivers
to dynamically adjust video quality by using lower layers when
resolution or bandwidth needs to be reduced, improving
adaptability in heterogeneous network conditions.

Simulcast, by contrast, sends multiple separate streams
at different resolutions. While widely supported in VP8 and VP9,
it consumes more bandwidth compared to SVC.

The configuration added here is based on the LiveKit demo app,
which defaults to VP9 when supported. OpenTalk’s configuration
also recommends VP9.

If a browser does not support VP9, LiveKit falls back to VP8 or
other codecs as needed. Notably, LiveKit disables VP9 encoding for
Firefox due to longstanding issues, but it can still decode VP9
streams and encode VP8 for outgoing streams. This ensures
compatibility with other participants, even in mixed environments
where some browsers use VP9 and others fallback to VP8.

In theory, participants do not all need to switch to a single codec,
as both LiveKit and browsers intelligently handle codec negotiation
on a per-participant basis. This dynamic adaptation ensures seamless
communication without manual intervention.

A similar challenge with codec compatibility was raised
in Jitsi two years ago, check issue #10657.

Before any release, this needs to be battle tested
with Firefox 115 browsers.
2024-12-12 11:25:06 +01:00
lebaudantoine dbfba564c5 🔧(backend) fix sentry deprecated scope
`sentry_sdk.configure_scope` is deprecated and will
be removed in the next major version.

(commit taken from people by @qbey)
2024-12-10 12:00:41 +01:00
renovate[bot] 4830206bb2 ⬆️(dependencies) update python dependencies 2024-12-09 18:20:12 +01:00
lebaudantoine e4f30f926c 🩹(summary) fix configuration in staging
@rouja forgot to add the right environment variable for the celery
workers. Fixed it.
2024-12-09 18:15:47 +01:00
lebaudantoine fc61f58596 🔧(summary) update LLM and whisper endpoints
@rouja fixed the issue from OutScale load balancer.
He found the right parameter in OutScale documentation,
that was causing a request timeout.
2024-12-09 18:15:47 +01:00
lebaudantoine e0021dbb80 (frontend) introduce a notification settings
I extended Room settings with a new tab, allowing users to toggle
the sound notification they want.

Their choices will be persisted.
2024-12-09 18:14:46 +01:00
lebaudantoine 83914f8307 (frontend) add notification store for managing user preferences
Introduced a global state to handle user preferences related to notifications.
The first use case is sound notifications, allowing users to disable them
based on feedback.

Additionally, the sound volume is now stored globally, making it easy to
configure in the future if needed. I've lowered the volume of
the notifications to make them more discreet.

Preferences are persisted in local storage, ensuring they are retained
between meetings.
2024-12-09 18:14:46 +01:00
lebaudantoine be54709598 (frontend) create STORAGE_KEYS constant
Add STORAGE_KEYS object to centralize localStorage keys,
ensuring no key overlaps by maintaining a single source
of truth for key declarations across the app.

Might be premature, as only the notification store will be persisted.
2024-12-09 18:14:46 +01:00
lebaudantoine 5b76ea492b (frontend) create utils function for valtio
Valtio allows state persistence in local storage, which is
necessary for the notification store. In this case, I'll need
to persist a `proxyMap`—a utility provided by Valtio to create
an observable map.

However, since `proxyMap` isn't natively serializable,
I'll need to implement two custom functions: one for serialization
and another for deserialization (revival).

Regarding the file structure, I've named the file `utils/valtio`,
but this can be discussed further. The purpose of this file is
to centralize common utility functions related to Valtio
for better organization and reuse.
2024-12-09 18:14:46 +01:00
lebaudantoine 46934a84d1 🚚(frontend) rename NotificationType items
I found the item names unclear, so I updated them for better clarity.

I also removed the unnecessary 'lowered' item and added
a TODO comment about handling the message received notification,
which is not yet implemented in the code.
2024-12-09 18:14:46 +01:00
lebaudantoine deb9ab1a1d (frontend) create a new primitive Switch
Inspired by Robin's design, I've styled a React Aria
Switch component using our DSFR theme.

This is an initial draft and isn't yet pixel-perfect compared
to Robin's design. It also hasn't been integrated into
the form inputs yet.
2024-12-09 18:14:46 +01:00
renovate[bot] d0fd16d7d2 ⬆️(dependencies) update django to v5.1.4 [SECURITY] 2024-12-08 22:21:18 +01:00
59 changed files with 1825 additions and 6757 deletions
+3
View File
@@ -305,3 +305,6 @@ start-tilt: ## start the kubernetes cluster using kind
tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
DEV_ENV=dev-keycloak tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
+2
View File
@@ -118,6 +118,8 @@ $ make build-k8s-cluster
Once the Kubernetes cluster is ready, start the application stack locally:
```shell
$ make start-tilt
or
$ make start-tilt-keycloak # start stack without Pro Connect, use keycloak
```
These commands set up and run your application environment using Tilt for local Kubernetes development.
+1 -1
View File
@@ -38,7 +38,7 @@ docker_build(
]
)
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e dev template .'))
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .'))
migration = '''
set -eu
+405 -405
View File
File diff suppressed because it is too large Load Diff
+1 -1
Submodule secrets updated: f5aea0c251...2ba12db71d
+4 -2
View File
@@ -503,8 +503,10 @@ class Base(Configuration):
release=get_release(),
integrations=[DjangoIntegration()],
)
with sentry_sdk.configure_scope() as scope:
scope.set_extra("application", "backend")
# Add the application name to the Sentry scope
scope = sentry_sdk.get_global_scope()
scope.set_tag("application", "backend")
class Build(Base):
+14 -14
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.10"
version = "0.1.12"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,20 +25,20 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.68",
"boto3==1.35.76",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
"django-cors-headers==4.6.0",
"django-countries==7.6.1",
"django-parler==2.3",
"redis==5.2.0",
"redis==5.2.1",
"django-redis==5.4.0",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.1.3",
"django==5.1.4",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"factory_boy==3.3.1",
@@ -48,16 +48,16 @@ dependencies = [
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.3",
"PyJWT==2.10.0",
"PyJWT==2.10.1",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.19.0",
"sentry-sdk==2.19.2",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.8.2",
"mozilla-django-oidc==4.0.1",
"livekit-api==0.8.0",
"aiohttp==3.11.7",
"aiohttp==3.11.10",
]
[project.urls]
@@ -69,20 +69,20 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.11.1",
"drf-spectacular-sidecar==2024.12.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==8.29.0",
"pyfakefs==5.7.1",
"ipython==8.30.0",
"pyfakefs==5.7.2",
"pylint-django==2.6.1",
"pylint==3.3.1",
"pylint==3.3.2",
"pytest-cov==6.0.0",
"pytest-django==4.9.0",
"pytest==8.3.3",
"pytest==8.3.4",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.8.0",
"ruff==0.8.2",
"types-requests==2.32.0.20241016",
]
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "0.1.10",
"version": "0.1.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.10",
"version": "0.1.12",
"dependencies": {
"@livekit/components-react": "2.6.9",
"@livekit/components-styles": "1.1.4",
@@ -7702,9 +7702,9 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.10",
"version": "0.1.12",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -2,9 +2,7 @@ import { css } from '@/styled-system/css'
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Text, A } from '@/primitives'
const GRIST_FORM =
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4'
import { GRIST_FORM } from '@/utils/constants'
export const FeedbackBanner = () => {
const { t } = useTranslation()
@@ -16,11 +16,11 @@ export const MainNotificationToast = () => {
if (isMobileBrowser()) {
return
}
triggerNotificationSound(NotificationType.Joined)
triggerNotificationSound(NotificationType.ParticipantJoined)
toastQueue.add(
{
participant,
type: NotificationType.Joined,
type: NotificationType.ParticipantJoined,
},
{
timeout: 5000,
@@ -67,7 +67,7 @@ export const MainNotificationToast = () => {
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Raised
toast.content.type === NotificationType.HandRaised
)
if (existingToast && prevMetadata.raised && !metadata.raised) {
@@ -76,11 +76,11 @@ export const MainNotificationToast = () => {
}
if (!existingToast && !prevMetadata.raised && metadata.raised) {
triggerNotificationSound(NotificationType.Raised)
triggerNotificationSound(NotificationType.HandRaised)
toastQueue.add(
{
participant,
type: NotificationType.Raised,
type: NotificationType.HandRaised,
},
{ timeout: 5000 }
)
@@ -1,6 +1,5 @@
export enum NotificationType {
Joined = 'joined',
Default = 'default',
Raised = 'raised',
Lowered = 'lowered',
ParticipantJoined = 'participantJoined',
HandRaised = 'handRaised',
// todo - implement message received notification
}
@@ -17,10 +17,10 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => {
if (toast.content?.type === NotificationType.Joined) {
if (toast.content?.type === NotificationType.ParticipantJoined) {
return <ToastJoined key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.Raised) {
if (toast.content?.type === NotificationType.HandRaised) {
return <ToastRaised key={toast.key} toast={toast} state={state} />
}
return <Toast key={toast.key} toast={toast} state={state} />
@@ -1,18 +1,24 @@
import useSound from 'use-sound'
import { useSnapshot } from 'valtio'
import { notificationsStore } from '@/stores/notifications'
import { NotificationType } from '@/features/notifications/NotificationType'
// fixme - handle dynamic audio output changes
export const useNotificationSound = () => {
const notificationsSnap = useSnapshot(notificationsStore)
const [play] = useSound('./sounds/notifications.mp3', {
sprite: {
joined: [0, 1150],
raised: [1400, 180],
message: [1580, 300],
participantJoined: [0, 1150],
handRaised: [1400, 180],
messageReceived: [1580, 300],
waiting: [2039, 710],
success: [2740, 1304],
},
volume: notificationsSnap.soundNotificationVolume,
})
const triggerNotificationSound = (type: string) => {
play({ id: type })
const triggerNotificationSound = (type: NotificationType) => {
const isSoundEnabled = notificationsSnap.soundNotifications.get(type)
if (isSoundEnabled) play({ id: type })
}
return { triggerNotificationSound }
}
@@ -65,6 +65,11 @@ export const Conference = ({
const roomOptions = useMemo((): RoomOptions => {
return {
adaptiveStream: true,
dynacast: true,
publishDefaults: {
videoCodec: 'vp9',
},
videoCaptureDefaults: {
deviceId: userConfig.videoDeviceId ?? undefined,
},
@@ -5,8 +5,12 @@ import { css } from '@/styled-system/css'
import { ToggleButton } from '@/primitives'
import { chatStore } from '@/stores/chat'
import { useSidePanel } from '../../hooks/useSidePanel'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const ChatToggle = () => {
export const ChatToggle = ({
onPress,
...props
}: Partial<ToggleButtonProps>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
const chatSnap = useSnapshot(chatStore)
@@ -27,8 +31,12 @@ export const ChatToggle = () => {
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isChatOpen}
onPress={() => toggleChat()}
onPress={(e) => {
toggleChat()
onPress?.(e)
}}
data-attr={`controls-chat-${tooltipLabel}`}
{...props}
>
<RiChat1Line />
</ToggleButton>
@@ -1,16 +1,11 @@
import { useTranslation } from 'react-i18next'
import { RiMore2Line } from '@remixicon/react'
import { Button, Menu } from '@/primitives'
import { useState } from 'react'
import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems'
import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended'
export type DialogState = 'username' | 'settings' | null
export const OptionsButton = () => {
const { t } = useTranslation('rooms')
const [dialogOpen, setDialogOpen] = useState<DialogState>(null)
return (
<>
<Menu>
@@ -22,12 +17,8 @@ export const OptionsButton = () => {
>
<RiMore2Line />
</Button>
<OptionsMenuItems onOpenDialog={setDialogOpen} />
<OptionsMenuItems />
</Menu>
<SettingsDialogExtended
isOpen={dialogOpen === 'settings'}
onOpenChange={(v) => !v && setDialogOpen(null)}
/>
</>
)
}
@@ -5,20 +5,17 @@ import {
} from '@remixicon/react'
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction } from 'react'
import { DialogState } from './OptionsButton'
import { Separator } from '@/primitives/Separator'
import { useSidePanel } from '../../../hooks/useSidePanel'
import { menuRecipe } from '@/primitives/menuRecipe.ts'
import { useSettingsDialog } from '../SettingsDialogContext'
import { GRIST_FORM } from '@/utils/constants'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
onOpenDialog,
}: {
onOpenDialog: Dispatch<SetStateAction<DialogState>>
}) => {
export const OptionsMenuItems = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useSidePanel()
const { setDialogOpen } = useSettingsDialog()
return (
<RACMenu
style={{
@@ -38,7 +35,7 @@ export const OptionsMenuItems = ({
<Separator />
<Section>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
href={GRIST_FORM}
target="_blank"
className={menuRecipe({ icon: true }).item}
>
@@ -47,7 +44,7 @@ export const OptionsMenuItems = ({
</MenuItem>
<MenuItem
className={menuRecipe({ icon: true }).item}
onAction={() => onOpenDialog('settings')}
onAction={() => setDialogOpen(true)}
>
<RiSettings3Line size={20} />
{t('settings')}
@@ -4,8 +4,12 @@ import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { useSidePanel } from '../../../hooks/useSidePanel'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const ParticipantsToggle = () => {
export const ParticipantsToggle = ({
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
/**
@@ -33,8 +37,12 @@ export const ParticipantsToggle = () => {
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isParticipantsOpen}
onPress={() => toggleParticipants()}
onPress={(e) => {
toggleParticipants()
onPress?.(e)
}}
data-attr={`controls-participants-${tooltipLabel}`}
{...props}
>
<RiGroupLine />
</ToggleButton>
@@ -4,13 +4,21 @@ import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Track } from 'livekit-client'
import React from 'react'
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const ScreenShareToggle = (
props: Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
'source' | 'captureOptions'
>
) => {
type Props = Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
'source' | 'captureOptions'
> &
Pick<NonNullable<ButtonRecipeProps>, 'variant'> &
ToggleButtonProps
export const ScreenShareToggle = ({
variant = 'primaryDark',
onPress,
...props
}: Props) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
const { buttonProps, enabled } = useTrackToggle({
...props,
@@ -26,18 +34,16 @@ export const ScreenShareToggle = (
<ToggleButton
isSelected={enabled}
square
variant="primaryDark"
variant={variant}
tooltip={t(tooltipLabel)}
onPress={(e) =>
onPress={(e) => {
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
)
}
style={{
maxWidth: '46px',
maxHeight: '46px',
onPress?.(e)
}}
data-attr={`controls-screenshare-${tooltipLabel}`}
{...props}
>
<Div position="relative">
<RiRectangleLine size={28} />
@@ -0,0 +1,36 @@
import { SettingsDialogExtended } from '@/features/settings/components/SettingsDialogExtended'
import React, { createContext, useContext, useState } from 'react'
const SettingsDialogContext = createContext<
| {
dialogOpen: boolean
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>
}
| undefined
>(undefined)
export const SettingsDialogProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
const [dialogOpen, setDialogOpen] = useState(false)
return (
<SettingsDialogContext.Provider value={{ dialogOpen, setDialogOpen }}>
{children}
<SettingsDialogExtended
isOpen={dialogOpen}
onOpenChange={(v) => !v && setDialogOpen(false)}
/>
</SettingsDialogContext.Provider>
)
}
// eslint-disable-next-line react-refresh/only-export-components
export const useSettingsDialog = () => {
const context = useContext(SettingsDialogContext)
if (!context) {
throw new Error(
'useSettingsDialog must be used within a SettingsDialogProvider'
)
}
return context
}
@@ -3,8 +3,9 @@ import { RiQuestionLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Crisp } from 'crisp-sdk-web'
import { useEffect, useState } from 'react'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const SupportToggle = () => {
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const [isOpened, setIsOpened] = useState($crisp.is('chat:opened'))
@@ -32,8 +33,16 @@ export const SupportToggle = () => {
tooltip={t('support')}
aria-label={t('support')}
isSelected={isOpened}
onPress={() => (isOpened ? Crisp.chat.close() : Crisp.chat.open())}
onPress={(e) => {
if (isOpened) {
Crisp.chat.close()
} else {
Crisp.chat.open()
}
onPress?.(e)
}}
data-attr="controls-support"
{...props}
>
<RiQuestionLine />
</ToggleButton>
@@ -4,8 +4,13 @@ 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 TranscriptToggle = () => {
export const TranscriptToggle = ({
variant = 'primaryDark',
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
@@ -24,11 +29,15 @@ export const TranscriptToggle = () => {
>
<ToggleButton
square
variant="primaryTextDark"
variant={variant}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isTranscriptOpen}
onPress={() => toggleTranscript()}
onPress={(e) => {
toggleTranscript()
onPress?.(e)
}}
{...props}
>
<RiBardLine />
</ToggleButton>
@@ -1,166 +0,0 @@
import { Track } from 'livekit-client'
import * as React from 'react'
import { supportsScreenSharing } from '@livekit/components-core'
import { usePersistentUserChoices } from '@livekit/components-react'
import { StartMediaButton } from '../components/controls/StartMediaButton'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '../components/controls/ChatToggle'
import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { SupportToggle } from '../components/controls/SupportToggle'
import { TranscriptToggle } from '../components/controls/TranscriptToggle'
import { css } from '@/styled-system/css'
/** @public */
export type ControlBarControls = {
microphone?: boolean
camera?: boolean
chat?: boolean
screenShare?: boolean
leave?: boolean
settings?: boolean
}
/** @public */
export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
onDeviceError?: (error: { source: Track.Source; error: Error }) => void
variation?: 'minimal' | 'verbose' | 'textOnly'
controls?: ControlBarControls
/**
* If `true`, the user's device choices will be persisted.
* This will enable the user to have the same device choices when they rejoin the room.
* @defaultValue true
* @alpha
*/
saveUserChoices?: boolean
}
/**
* The `ControlBar` prefab gives the user the basic user interface to control their
* media devices (camera, microphone and screen share), open the `Chat` and leave the room.
*
* @remarks
* This component is build with other LiveKit components like `TrackToggle`,
* `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
*
* @example
* ```tsx
* <LiveKitRoom>
* <ControlBar />
* </LiveKitRoom>
* ```
* @public
*/
export function ControlBar({
saveUserChoices = true,
onDeviceError,
}: ControlBarProps) {
const browserSupportsScreenSharing = supportsScreenSharing()
const {
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
const microphoneOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
isUserInitiated ? saveAudioInputEnabled(enabled) : null,
[saveAudioInputEnabled]
)
const cameraOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
isUserInitiated ? saveVideoInputEnabled(enabled) : null,
[saveVideoInputEnabled]
)
return (
<div
className={css({
width: '100vw',
display: 'flex',
position: 'absolute',
padding: '1.125rem',
bottom: 0,
left: 0,
right: 0,
})}
>
<div
className={css({
display: 'flex',
justifyContent: 'flex-start',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
marginLeft: '0.5rem',
})}
></div>
<div
className={css({
flex: '1 1 33%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '0.65rem',
})}
>
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
{browserSupportsScreenSharing && (
<ScreenShareToggle
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
/>
)}
<HandToggle />
<OptionsButton />
<LeaveButton />
<StartMediaButton />
</div>
<div
className={css({
display: 'flex',
justifyContent: 'flex-end',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
paddingRight: '0.25rem',
})}
>
<ChatToggle />
<ParticipantsToggle />
<TranscriptToggle />
<SupportToggle />
</div>
</div>
)
}
@@ -0,0 +1,103 @@
import { Track } from 'livekit-client'
import * as React from 'react'
import { usePersistentUserChoices } from '@livekit/components-react'
import { MobileControlBar } from './MobileControlBar'
import { DesktopControlBar } from './DesktopControlBar'
import { SettingsDialogProvider } from '../../components/controls/SettingsDialogContext'
import { useIsMobile } from '@/utils/useIsMobile'
/** @public */
export type ControlBarControls = {
microphone?: boolean
camera?: boolean
chat?: boolean
screenShare?: boolean
leave?: boolean
settings?: boolean
}
/** @public */
export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
onDeviceError?: (error: { source: Track.Source; error: Error }) => void
variation?: 'minimal' | 'verbose' | 'textOnly'
controls?: ControlBarControls
/**
* If `true`, the user's device choices will be persisted.
* This will enable the user to have the same device choices when they rejoin the room.
* @defaultValue true
* @alpha
*/
saveUserChoices?: boolean
}
/**
* The `ControlBar` prefab gives the user the basic user interface to control their
* media devices (camera, microphone and screen share), open the `Chat` and leave the room.
*
* @remarks
* This component is build with other LiveKit components like `TrackToggle`,
* `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
*
* @example
* ```tsx
* <LiveKitRoom>
* <ControlBar />
* </LiveKitRoom>
* ```
* @public
*/
export function ControlBar({
saveUserChoices = true,
onDeviceError,
}: ControlBarProps) {
const {
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
const microphoneOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
isUserInitiated ? saveAudioInputEnabled(enabled) : null,
[saveAudioInputEnabled]
)
const cameraOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
isUserInitiated ? saveVideoInputEnabled(enabled) : null,
[saveVideoInputEnabled]
)
const barProps = {
onDeviceError,
microphoneOnChange,
cameraOnChange,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
}
const isMobile = useIsMobile()
return (
<SettingsDialogProvider>
{isMobile ? (
<MobileControlBar {...barProps} />
) : (
<DesktopControlBar {...barProps} />
)}
</SettingsDialogProvider>
)
}
export interface ControlBarAuxProps {
onDeviceError: ControlBarProps['onDeviceError']
microphoneOnChange: (
enabled: boolean,
isUserInitiated: boolean
) => void | null
cameraOnChange: (enabled: boolean, isUserInitiated: boolean) => void | null
saveAudioInputDeviceId: (deviceId: string) => void
saveVideoInputDeviceId: (deviceId: string) => void
}
@@ -0,0 +1,106 @@
import { supportsScreenSharing } from '@livekit/components-core'
import { ControlBarAuxProps } from './ControlBar'
import { css } from '@/styled-system/css'
import { LeaveButton } from '../../components/controls/LeaveButton'
import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice'
import { Track } from 'livekit-client'
import { HandToggle } from '../../components/controls/HandToggle'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
import { StartMediaButton } from '../../components/controls/StartMediaButton'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { SupportToggle } from '../../components/controls/SupportToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
export function DesktopControlBar({
onDeviceError,
microphoneOnChange,
cameraOnChange,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
}: ControlBarAuxProps) {
const browserSupportsScreenSharing = supportsScreenSharing()
return (
<>
<div
className={css({
width: '100vw',
display: 'flex',
position: 'absolute',
padding: '1.125rem',
bottom: 0,
left: 0,
right: 0,
})}
>
<div
className={css({
display: 'flex',
justifyContent: 'flex-start',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
marginLeft: '0.5rem',
})}
></div>
<div
className={css({
flex: '1 1 33%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '0.65rem',
})}
>
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
{browserSupportsScreenSharing && (
<ScreenShareToggle
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
/>
)}
<HandToggle />
<OptionsButton />
<LeaveButton />
<StartMediaButton />
</div>
<div
className={css({
display: 'flex',
justifyContent: 'flex-end',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
paddingRight: '0.25rem',
})}
>
<ChatToggle />
<ParticipantsToggle />
<TranscriptToggle />
<SupportToggle />
</div>
</div>
</>
)
}
@@ -0,0 +1,182 @@
import { supportsScreenSharing } from '@livekit/components-core'
import { useTranslation } from 'react-i18next'
import { ControlBarAuxProps } from './ControlBar'
import React from 'react'
import { css } from '@/styled-system/css'
import { LeaveButton } from '../../components/controls/LeaveButton'
import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice'
import { Track } from 'livekit-client'
import { HandToggle } from '../../components/controls/HandToggle'
import { Button } from '@/primitives/Button'
import {
RiAccountBoxLine,
RiMegaphoneLine,
RiMore2Line,
RiSettings3Line,
} from '@remixicon/react'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { SupportToggle } from '../../components/controls/SupportToggle'
import { useSidePanel } from '../../hooks/useSidePanel'
import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
import { ResponsiveMenu } from './ResponsiveMenu'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
export function MobileControlBar({
onDeviceError,
microphoneOnChange,
cameraOnChange,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
}: ControlBarAuxProps) {
const { t } = useTranslation('rooms')
const [isMenuOpened, setIsMenuOpened] = React.useState(false)
const browserSupportsScreenSharing = supportsScreenSharing()
const { toggleEffects } = useSidePanel()
const { setDialogOpen } = useSettingsDialog()
return (
<>
<div
className={css({
width: '100vw',
display: 'flex',
position: 'absolute',
padding: '1.125rem',
justifyContent: 'center',
bottom: 0,
left: 0,
right: 0,
})}
>
<div
className={css({
display: 'flex',
justifyContent: 'space-between',
width: '422px',
})}
>
<LeaveButton />
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
<HandToggle />
<Button
square
variant="primaryDark"
aria-label={t('options.buttonLabel')}
tooltip={t('options.buttonLabel')}
onPress={() => setIsMenuOpened(true)}
>
<RiMore2Line />
</Button>
</div>
</div>
<ResponsiveMenu
isOpened={isMenuOpened}
onClosed={() => setIsMenuOpened(false)}
>
<div
className={css({
display: 'flex',
justifyContent: 'center',
})}
>
<div
className={css({
flexGrow: 1,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
gridGap: '1rem',
'& > *': {
alignSelf: 'center',
justifySelf: 'center',
},
})}
>
{browserSupportsScreenSharing && (
<ScreenShareToggle
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
variant="primaryTextDark"
description={true}
onPress={() => setIsMenuOpened(false)}
/>
)}
<ChatToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<ParticipantsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<TranscriptToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<SupportToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<Button
onPress={() => {
toggleEffects()
setIsMenuOpened(false)
}}
variant="primaryTextDark"
aria-label={t('options.items.effects')}
tooltip={t('options.items.effects')}
description={true}
>
<RiAccountBoxLine size={20} />
</Button>
<LinkButton
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
variant="primaryTextDark"
tooltip={t('options.items.feedbacks')}
aria-label={t('options.items.feedbacks')}
description={true}
target="_blank"
onPress={() => setIsMenuOpened(false)}
>
<RiMegaphoneLine size={20} />
</LinkButton>
<Button
onPress={() => {
setDialogOpen(true)
setIsMenuOpened(false)
}}
variant="primaryTextDark"
aria-label={t('options.items.settings')}
tooltip={t('options.items.settings')}
description={true}
>
<RiSettings3Line size={20} />
</Button>
</div>
</div>
</ResponsiveMenu>
</>
)
}
@@ -0,0 +1,56 @@
import { css } from '@/styled-system/css'
import { PropsWithChildren } from 'react'
import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
interface ResponsiveMenuProps extends PropsWithChildren {
isOpened: boolean
onClosed: () => void
}
export function ResponsiveMenu({
isOpened,
onClosed,
children,
}: ResponsiveMenuProps) {
return (
<ModalOverlay
isDismissable
isOpen={isOpened}
onOpenChange={(isOpened) => {
if (!isOpened) {
onClosed()
}
}}
className={css({
width: '100vw',
height: 'var(--visual-viewport-height)',
zIndex: 100,
justifyContent: 'center',
alignItems: 'flex-end',
display: 'flex',
position: 'fixed',
top: 0,
left: 0,
padding: '1.5rem 1.5rem 1rem 1.5rem',
boxSizing: 'border-box',
})}
>
<Modal
className={css({
backgroundColor: 'primaryDark.200',
borderRadius: '20px',
flexGrow: 1,
padding: '1.5rem',
'&[data-entering]': {
animation: 'slide-full 200ms',
},
'&[data-exiting]': {
animation: 'slide-full 200ms reverse',
},
})}
>
<Dialog>{children}</Dialog>
</Modal>
</ModalOverlay>
)
}
@@ -20,7 +20,7 @@ import {
useCreateLayoutContext,
} from '@livekit/components-react'
import { ControlBar } from './ControlBar'
import { ControlBar } from './ControlBar/ControlBar'
import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
@@ -6,12 +6,14 @@ import { Heading } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import {
RiAccountCircleLine,
RiNotification3Line,
RiSettings3Line,
RiSpeakerLine,
} from '@remixicon/react'
import { AccountTab } from './tabs/AccountTab'
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
import { NotificationsTab } from './tabs/NotificationsTab'
import { GeneralTab } from './tabs/GeneralTab'
import { AudioTab } from './tabs/AudioTab'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useRef } from 'react'
@@ -81,12 +83,17 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
<RiSettings3Line />
{isWideScreen && t('tabs.general')}
</Tab>
<Tab icon highlight id="4">
<RiNotification3Line />
{isWideScreen && t('tabs.notifications')}
</Tab>
</TabList>
</div>
<div className={tabPanelContainerStyle}>
<AccountTab id="1" onOpenChange={props.onOpenChange} />
<AudioTab id="2" />
<GeneralTab id="3" />
<NotificationsTab id="4" />
</div>
</Tabs>
</Dialog>
@@ -0,0 +1,41 @@
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { H, Switch } from '@/primitives'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { notificationsStore } from '@/stores/notifications'
export type NotificationsTabProps = Pick<TabPanelProps, 'id'>
export const NotificationsTab = ({ id }: NotificationsTabProps) => {
const { t } = useTranslation('settings', { keyPrefix: 'notifications' })
const notificationsSnap = useSnapshot(notificationsStore)
return (
<TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('heading')}</H>
<ul
className={css({
display: 'flex',
flexDirection: 'column',
gap: '1rem',
})}
>
{Array.from(notificationsSnap.soundNotifications).map(
([key, value]) => (
<li key={key}>
<Switch
aria-label={`${t(`actions.${value ? 'disable' : 'enable'}`)} ${t('label')} "${t(`items.${key}`)}"`}
isSelected={value}
onChange={(v) => {
notificationsStore.soundNotifications.set(key, v)
}}
>
{t(`items.${key}`)}
</Switch>
</li>
)
)}
</ul>
</TabPanel>
)
}
+15 -1
View File
@@ -19,6 +19,19 @@
},
"permissionsRequired": ""
},
"notifications": {
"heading": "",
"label": "",
"actions": {
"disable": "",
"enable": ""
},
"items": {
"participantJoined": "",
"handRaised": "",
"messageReceived": ""
}
},
"dialog": {
"heading": ""
},
@@ -30,6 +43,7 @@
"tabs": {
"account": "",
"audio": "",
"general": ""
"general": "",
"notifications": ""
}
}
+15 -1
View File
@@ -19,6 +19,19 @@
},
"permissionsRequired": "Permissions required"
},
"notifications": {
"heading": "Sound notifications",
"label": "sound notifications for",
"actions": {
"disable": "Disable",
"enable": "Enable"
},
"items": {
"participantJoined": "Participant joined",
"handRaised": "Hand raised",
"messageReceived": "Message received"
}
},
"dialog": {
"heading": "Settings"
},
@@ -30,6 +43,7 @@
"tabs": {
"account": "Profile",
"audio": "Audio",
"general": "General"
"general": "General",
"notifications": "Notifications"
}
}
+15 -1
View File
@@ -19,6 +19,19 @@
},
"permissionsRequired": "Autorisations nécessaires"
},
"notifications": {
"heading": "Notifications sonores",
"label": "la notification sonore pour",
"actions": {
"disable": "Désactiver",
"enable": "Activer"
},
"items": {
"participantJoined": "Un nouveau participant",
"handRaised": "Une main levée",
"messageReceived": "Un message reçu"
}
},
"dialog": {
"heading": "Paramètres"
},
@@ -30,6 +43,7 @@
"tabs": {
"account": "Profile",
"audio": "Audio",
"general": "Général"
"general": "Général",
"notifications": "Notifications"
}
}
+9 -2
View File
@@ -5,10 +5,14 @@ import {
import { type RecipeVariantProps } from '@/styled-system/css'
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
import { ReactNode } from 'react'
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
RACButtonsProps &
TooltipWrapperProps
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
}
export const Button = ({
tooltip,
@@ -22,7 +26,10 @@ export const Button = ({
<RACButton
className={buttonRecipe(variantProps)}
{...(componentProps as RACButtonsProps)}
/>
>
{componentProps.children as ReactNode}
{props.description && <span>{tooltip}</span>}
</RACButton>
</TooltipWrapper>
)
}
+11 -2
View File
@@ -2,10 +2,14 @@ import { Link, LinkProps } from 'react-aria-components'
import { type RecipeVariantProps } from '@/styled-system/css'
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
import { ReactNode } from 'react'
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
LinkProps &
TooltipWrapperProps
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
}
export const LinkButton = ({
tooltip,
@@ -16,7 +20,12 @@ export const LinkButton = ({
return (
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
<Link className={buttonRecipe(variantProps)} {...componentProps} />
<Link className={buttonRecipe(variantProps)} {...componentProps}>
<>
{componentProps.children as ReactNode}
{props.description && <span>{tooltip}</span>}
</>
</Link>
</TooltipWrapper>
)
}
+69
View File
@@ -0,0 +1,69 @@
import {
Switch as RACSwitch,
SwitchProps as RACSwitchProps,
} from 'react-aria-components'
import { styled } from '@/styled-system/jsx'
import { StyledVariantProps } from '@/styled-system/types'
import { ReactNode } from 'react'
export const StyledSwitch = styled(RACSwitch, {
base: {
display: 'flex',
alignItems: 'center',
gap: '0.571rem',
color: 'black',
forcedColorAdjust: 'none',
'& .indicator': {
width: '2.6rem',
height: '1.563rem',
border: '0.125rem solid',
borderColor: 'primary.800',
borderRadius: '1.143rem',
transition: 'all 200ms, outline 200ms',
_before: {
content: '""',
display: 'block',
margin: '0.125rem',
width: '1.063rem',
height: '1.063rem',
borderRadius: '1.063rem',
background: 'primary.800',
transition: 'all 200ms',
},
},
'&[data-selected] .indicator': {
borderColor: 'primary.800',
background: 'primary.800',
_before: {
background: 'white',
transform: 'translateX(100%)',
},
},
'&[data-disabled] .indicator': {
borderColor: 'primary.200',
background: 'transparent',
_before: {
background: 'primary.200',
},
},
'&[data-focus-visible] .indicator': {
outline: '2px solid!',
outlineColor: 'focusRing!',
outlineOffset: '2px!',
},
},
variants: {},
})
export type SwitchProps = StyledVariantProps<typeof StyledSwitch> &
RACSwitchProps & { children: ReactNode }
/**
* Styled RAC Switch.
*/
export const Switch = ({ children, ...props }: SwitchProps) => (
<StyledSwitch {...props}>
<div className="indicator" />
{children}
</StyledSwitch>
)
+17 -3
View File
@@ -1,9 +1,17 @@
import {
ToggleButton as RACToggleButton,
ToggleButtonProps,
ToggleButtonProps as RACToggleButtonProps,
} from 'react-aria-components'
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
import { ReactNode } from 'react'
export type ToggleButtonProps = RACToggleButtonProps &
ButtonRecipeProps &
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
}
/**
* React aria ToggleButton with our button styles, that can take a tooltip if needed
@@ -12,14 +20,20 @@ export const ToggleButton = ({
tooltip,
tooltipType,
...props
}: ToggleButtonProps & ButtonRecipeProps & TooltipWrapperProps) => {
}: ToggleButtonProps) => {
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
return (
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
<RACToggleButton
{...componentProps}
className={buttonRecipe(variantProps)}
/>
>
<>
{componentProps.children as ReactNode}
{props.description && <span>{tooltip}</span>}
</>
</RACToggleButton>
</TooltipWrapper>
)
}
+14 -4
View File
@@ -1,9 +1,5 @@
import { type RecipeVariantProps, cva } from '@/styled-system/css'
export type ButtonRecipe = typeof buttonRecipe
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
export const buttonRecipe = cva({
base: {
display: 'flex',
@@ -222,6 +218,16 @@ export const buttonRecipe = cva({
shySelected: {
true: {},
},
description: {
true: {
flexDirection: 'column',
gap: '0.5rem',
'& span': {
fontSize: '13px',
textAlign: 'center',
},
},
},
// if the button is next to other ones to make a "button group", tell where the button is to handle radius
groupPosition: {
left: {
@@ -255,3 +261,7 @@ export const buttonRecipe = cva({
variant: 'primary',
},
})
export type ButtonRecipe = typeof buttonRecipe
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
+1
View File
@@ -29,3 +29,4 @@ export { ToggleButton } from './ToggleButton'
export { Ul } from './Ul'
export { VerticallyOffCenter } from './VerticallyOffCenter'
export { TextArea } from './TextArea'
export { Switch } from './Switch'
+44
View File
@@ -0,0 +1,44 @@
import { proxy, subscribe } from 'valtio'
import { proxyMap } from 'valtio/utils'
import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys'
import { NotificationType } from '@/features/notifications/NotificationType'
type State = {
soundNotifications: Map<NotificationType, boolean>
soundNotificationVolume: number
}
const DEFAULT_STATE: State = {
soundNotifications: proxyMap(
new Map([
[NotificationType.ParticipantJoined, true],
[NotificationType.HandRaised, true],
])
),
soundNotificationVolume: 0.1,
}
function getNotificationsState(): State {
try {
const stored = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS)
if (!stored) return DEFAULT_STATE
const parsed = JSON.parse(stored, deserializeToProxyMap)
return parsed || DEFAULT_STATE
} catch (error: unknown) {
console.error(
'[NotificationsStore] Failed to parse stored settings:',
error
)
return DEFAULT_STATE
}
}
export const notificationsStore = proxy<State>(getNotificationsState())
subscribe(notificationsStore, () => {
localStorage.setItem(
STORAGE_KEYS.NOTIFICATIONS,
JSON.stringify(notificationsStore, serializeProxyMap)
)
})
+9
View File
@@ -29,3 +29,12 @@ body,
body:has(.lk-video-conference) #crisp-chatbox > div > a {
display: none !important;
}
@keyframes slide-full {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
+2
View File
@@ -0,0 +1,2 @@
export const GRIST_FORM =
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4' as const
+6
View File
@@ -0,0 +1,6 @@
/**
* Object containing all localStorage keys used across the app
*/
export const STORAGE_KEYS = {
NOTIFICATIONS: 'app_notification_settings',
} as const
+20
View File
@@ -0,0 +1,20 @@
import { isMobileBrowser } from '@livekit/components-core'
import { useEffect, useState } from 'react'
export const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(isMobileBrowser())
useEffect(() => {
const handleResize = () => {
setIsMobile(isMobileBrowser())
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return isMobile
}
+28
View File
@@ -0,0 +1,28 @@
import { proxyMap } from 'valtio/utils'
/**
* Serializes Map objects into a JSON-friendly format while preserving valtio proxyMap compatibility
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeProxyMap(_: string, value: any) {
if (value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()),
}
}
return value
}
/**
* Custom JSON reviver function for deserializing Map objects and wrapping them in valtio proxyMap
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function deserializeToProxyMap(_: string, value: any) {
if (typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return proxyMap(new Map(value.value))
}
}
return value
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
replicaCount: 1
terminationGracePeriodSeconds: 18000
egress:
log_level: debug
ws_url: ws://livekit-livekit-server:80
insecure: true
enable_chrome_sandbox: true
{{- with .Values.livekit.keys }}
{{- range $key, $value := . }}
api_key: {{ $key }}
api_secret: {{ $value }}
{{- end }}
{{- end }}
redis:
address: redis-master:6379
password: pass
s3:
access_key: meet
secret: password
region: local
bucket: meet-media-storage
endpoint: http://minio:9000
force_path_style: true
loadBalancer:
type: nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- hosts:
- livekit-egress.127.0.0.1.nip.io
secretName: livekit-egress-dinum-cert
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
nodeSelector: {}
resources: {}
@@ -0,0 +1,40 @@
replicaCount: 1
terminationGracePeriodSeconds: 18000
livekit:
log_level: debug
rtc:
use_external_ip: false
port_range_start: 50000
port_range_end: 60000
tcp_port: 7881
redis:
address: redis-master:6379
password: pass
keys:
turn:
enabled: true
udp_port: 443
domain: livekit.127.0.0.1.nip.io
loadBalancerAnnotations: {}
loadBalancer:
type: nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- hosts:
- livekit.127.0.0.1.nip.io
secretName: livekit-dinum-cert
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 60
nodeSelector: {}
resources: {}
@@ -0,0 +1,188 @@
image:
repository: localhost:5001/meet-backend
pullPolicy: Always
tag: "latest"
backend:
replicas: 1
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.127.0.0.1.nip.io,http://meet.127.0.0.1.nip.io
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet.127.0.0.1.nip.io
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
DJANGO_SETTINGS_MODULE: meet.settings
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
DJANGO_SUPERUSER_PASSWORD: admin
DJANGO_EMAIL_HOST: "mailcatcher"
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_USE_SSL: False
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/session/end
OIDC_RP_CLIENT_ID:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_ID
OIDC_RP_CLIENT_SECRET:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
OIDC_VERIFY_SSL: False
LOGIN_REDIRECT_URL: https://meet.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
DB_HOST: postgres-postgresql
DB_NAME: meet
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
{{- range $key, $value := . }}
LIVEKIT_API_SECRET: {{ $value }}
LIVEKIT_API_KEY: {{ $key }}
{{- end }}
{{- end }}
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_REGION_NAME: local
RECORDING_ENABLE: True
RECORDING_VERIFY_SSL: False
RECORDING_STORAGE_EVENT_ENABLE: True
RECORDING_STORAGE_EVENT_TOKEN: password
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
SUMMARY_SERVICE_API_TOKEN: password
migrate:
command:
- "/bin/sh"
- "-c"
- |
python manage.py migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
command:
- "gunicorn"
- "-c"
- "/usr/local/etc/gunicorn/meet.py"
- "meet.wsgi:application"
- "--reload"
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
frontend:
envVars:
VITE_PORT: 8080
VITE_HOST: 0.0.0.0
VITE_API_BASE_URL: https://meet.127.0.0.1.nip.io/
replicas: 1
image:
repository: localhost:5001/meet-frontend
pullPolicy: Always
tag: "latest"
ingress:
enabled: true
host: meet.127.0.0.1.nip.io
ingressAdmin:
enabled: true
host: meet.127.0.0.1.nip.io
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
summary:
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "uvicorn"
- "summary.main:app"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--reload"
celery:
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "celery"
- "-A"
- "summary.core.celery_worker"
- "worker"
- "--pool=solo"
- "--loglevel=info"
@@ -0,0 +1,10 @@
djangoSecretKey: u!vbjDW71aru&OZA%NZQi0x
livekit:
keys:
devkey: secret
livekitApi:
key: devkey
secret: secret
oidc:
clientId: meet
clientSecret: ThisIsAnExampleKeyForDevPurposeOnly
+2 -2
View File
@@ -125,7 +125,7 @@ summary:
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
@@ -158,7 +158,7 @@ celery:
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.10"
tag: "v0.1.12"
backend:
migrateJobAnnotations:
@@ -116,6 +116,17 @@ backend:
name: meet-media-storage.bucket.libre.sh
key: bucket
AWS_S3_REGION_NAME: local
RECORDING_ENABLE: True
RECORDING_STORAGE_EVENT_ENABLE: True
RECORDING_STORAGE_EVENT_TOKEN:
secretKeyRef:
name: backend
key: RECORDING_STORAGE_EVENT_TOKEN
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
SUMMARY_SERVICE_API_TOKEN:
secretKeyRef:
name: summary
key: APP_API_TOKEN
createsuperuser:
command:
@@ -129,7 +140,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.10"
tag: "v0.1.12"
ingress:
enabled: true
@@ -167,7 +178,113 @@ posthog:
nginx.ingress.kubernetes.io/backend-protocol: https
summary:
replicas: 0
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN:
secretKeyRef:
name: summary
key: APP_API_TOKEN
AWS_S3_ENDPOINT_URL:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: url
AWS_S3_ACCESS_KEY_ID:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: accessKey
AWS_S3_SECRET_ACCESS_KEY:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: secretKey
AWS_STORAGE_BUCKET_NAME:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: bucket
AWS_S3_REGION_NAME: local
OPENAI_API_KEY:
secretKeyRef:
name: summary
key: OPENAI_API_KEY
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN:
secretKeyRef:
name: summary
key: WEBHOOK_API_TOKEN
WEBHOOK_URL: https://docs.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
CELERY_BROKER_URL:
secretKeyRef:
name: redis-summary.redis.libre.sh
key: url
CELERY_RESULT_BACKEND:
secretKeyRef:
name: redis-summary.redis.libre.sh
key: url
image:
repository: lasuite/meet-summary
pullPolicy: Always
tag: "v0.1.12"
celery:
replicas: 0
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN:
secretKeyRef:
name: summary
key: APP_API_TOKEN
AWS_S3_ENDPOINT_URL:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: endpoint
AWS_S3_ACCESS_KEY_ID:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: accessKey
AWS_S3_SECRET_ACCESS_KEY:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: secretKey
AWS_STORAGE_BUCKET_NAME:
secretKeyRef:
name: meet-media-storage.bucket.libre.sh
key: bucket
AWS_S3_REGION_NAME: local
OPENAI_API_KEY:
secretKeyRef:
name: summary
key: OPENAI_API_KEY
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN:
secretKeyRef:
name: summary
key: WEBHOOK_API_TOKEN
WEBHOOK_URL: https://docs.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
CELERY_BROKER_URL:
secretKeyRef:
name: redis-summary.redis.libre.sh
key: url
CELERY_RESULT_BACKEND:
secretKeyRef:
name: redis-summary.redis.libre.sh
key: url
SENTRY_IS_ENABLED: True
SENTRY_DSN: https://5aead03f03505da5130af6d642c42faf@sentry.incubateur.net/202
image:
repository: lasuite/meet-summary
pullPolicy: Always
tag: "v0.1.12"
command:
- "celery"
- "-A"
- "summary.core.celery_worker"
- "worker"
- "--pool=solo"
- "--loglevel=info"
@@ -216,14 +216,14 @@ summary:
secretKeyRef:
name: summary
key: OPENAI_API_KEY
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr:32222/v1
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN:
secretKeyRef:
name: summary
key: WEBHOOK_API_TOKEN
WEBHOOK_URL: https://www.mock-impress.com/webhook/
WEBHOOK_URL: https://impress-staging.beta.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
CELERY_BROKER_URL:
secretKeyRef:
name: redis-summary.redis.libre.sh
@@ -267,11 +267,14 @@ celery:
secretKeyRef:
name: summary
key: OPENAI_API_KEY
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN:
secretKeyRef:
name: summary
key: WEBHOOK_API_TOKEN
WEBHOOK_URL: https://www.mock-impress.com/webhook/
WEBHOOK_URL: https://impress-staging.beta.numerique.gouv.fr/api/v1.0/documents/create-for-owner/
CELERY_BROKER_URL:
secretKeyRef:
name: redis-summary.redis.libre.sh
+61 -7
View File
@@ -1,4 +1,8 @@
environments:
dev-keycloak:
values:
- version: 0.0.1
- env.d/{{ .Environment.Name }}/values.secrets.yaml
dev:
values:
- version: 0.0.1
@@ -32,7 +36,8 @@ repositories:
releases:
- name: postgres
installed: {{ eq .Environment.Name "dev" | toYaml }}
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: bitnami/postgresql
version: 13.1.5
@@ -45,9 +50,50 @@ releases:
enabled: true
autoGenerated: true
- name: minio
installed: {{ eq .Environment.Name "dev" | toYaml }}
- name: keycloak
installed: {{ eq .Environment.Name "dev-keycloak" | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: bitnami/keycloak
version: 17.3.6
values:
- postgresql:
auth:
username: keycloak
password: keycloak
database: keycloak
- extraEnvVars:
- name: KEYCLOAK_EXTRA_ARGS
value: "--import-realm"
- name: KC_HOSTNAME_URL
value: https://keycloak.127.0.0.1.nip.io
- extraVolumes:
- name: import
configMap:
name: meet-keycloak
- extraVolumeMounts:
- name: import
mountPath: /opt/bitnami/keycloak/data/import/
- auth:
adminUser: su
adminPassword: su
- proxy: edge
- ingress:
enabled: true
hostname: keycloak.127.0.0.1.nip.io
- extraDeploy:
- apiVersion: v1
kind: ConfigMap
metadata:
name: meet-keycloak
data:
meet.json: |
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 14 }}
- name: minio
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
namespace: {{ .Namespace }}
missingFileHandler: Warn
chart: bitnami/minio
version: 12.10.10
values:
@@ -75,7 +121,8 @@ releases:
name: mkcert
- name: redis
installed: {{ eq .Environment.Name "dev" | toYaml }}
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: bitnami/redis
version: 18.19.2
@@ -85,7 +132,8 @@ releases:
architecture: standalone
- name: extra
installed: {{ ne .Environment.Name "dev" | toYaml }}
installed: {{ not (regexMatch "^dev.*" .Environment.Name) | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: ./extra
secrets:
@@ -100,26 +148,32 @@ releases:
- name: meet
version: {{ .Values.version }}
namespace: {{ .Namespace }}
missingFileHandler: Warn
chart: ./meet
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit
installed: {{ eq .Environment.Name "dev" | toYaml }}
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/livekit-server
values:
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
- name: livekit-egress
installed: {{ eq .Environment.Name "dev" | toYaml }}
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
missingFileHandler: Warn
namespace: {{ .Namespace }}
chart: livekit/egress
values:
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
- env.d/{{ .Environment.Name }}/values.secrets.yaml
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
+5 -4
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.10",
"version": "0.1.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.10",
"version": "0.1.12",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
@@ -344,8 +344,9 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"license": "MIT",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.10",
"version": "0.1.12",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+4 -4
View File
@@ -8,16 +8,16 @@ dependencies = [
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"celery==5.4.0",
"redis==4.5.4",
"minio==7.2.9",
"openai==1.55.3",
"redis==5.2.1",
"minio==7.2.12",
"openai==1.57.1",
"requests==2.32.3",
"sentry-sdk[fastapi, celery]==2.19.0",
]
[project.optional-dependencies]
dev = [
"ruff==0.7.4",
"ruff==0.8.2",
]
[build-system]
+3 -1
View File
@@ -125,8 +125,10 @@ def process_audio_transcribe_summarize(filename: str, email: str, sub: str):
summary = summary_response.choices[0].message.content
logger.debug("Summary: \n %s", summary)
# fixme - generate a title using LLM
data = {
"summary": summary,
"title": "Votre résumé",
"content": summary,
"email": email,
"sub": sub,
}