Compare commits

..

2 Commits

Author SHA1 Message Date
leo 543ac20d66 harmonize dev prod 2026-02-24 16:36:49 +01:00
leo 1d42dd26ed harmonize docker commands 2026-02-24 16:16:56 +01:00
17 changed files with 129 additions and 157 deletions
+4 -4
View File
@@ -152,7 +152,7 @@ jobs:
uses: numerique-gouv/action-trivy-cache@main uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true continue-on-error: true
with: with:
docker-build-args: '-f src/summary/Dockerfile --target production' docker-build-args: '-f src/summary/Dockerfile --target summary-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}' docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
docker-context: './src/summary' docker-context: './src/summary'
- -
@@ -161,7 +161,7 @@ jobs:
with: with:
context: ./src/summary context: ./src/summary
file: ./src/summary/Dockerfile file: ./src/summary/Dockerfile
target: production target: summary-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
@@ -191,7 +191,7 @@ jobs:
uses: numerique-gouv/action-trivy-cache@main uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true continue-on-error: true
with: with:
docker-build-args: '-f src/agents/Dockerfile --target production' docker-build-args: '-f src/agents/Dockerfile --target agents-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}' docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
docker-context: './src/agents' docker-context: './src/agents'
- -
@@ -200,7 +200,7 @@ jobs:
with: with:
context: ./src/agents context: ./src/agents
file: ./src/agents/Dockerfile file: ./src/agents/Dockerfile
target: production target: agents-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
-4
View File
@@ -8,10 +8,6 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Changed
- ♻️(frontend) Use React Aria FocusScope for side panel accessibility
## [1.7.0] - 2026-02-19 ## [1.7.0] - 2026-02-19
### Added ### Added
+14 -14
View File
@@ -40,9 +40,9 @@ DOCKER_GID = $(shell id -g)
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID) DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE_EXEC = $(COMPOSE) exec COMPOSE_EXEC = $(COMPOSE) exec
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-development
COMPOSE_RUN = $(COMPOSE) run --rm COMPOSE_RUN = $(COMPOSE) run --rm
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev COMPOSE_RUN_APP = $(COMPOSE_RUN) app-development
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
@@ -94,8 +94,8 @@ build: ## build the project containers
@$(MAKE) build-frontend @$(MAKE) build-frontend
.PHONY: build .PHONY: build
build-backend: ## build the app-dev container build-backend: ## build the app-development container
@$(COMPOSE) build app-dev @$(COMPOSE) build app-development
.PHONY: build-backend .PHONY: build-backend
@@ -107,12 +107,12 @@ down: ## stop and remove containers, networks, images, and volumes
@$(COMPOSE) down @$(COMPOSE) down
.PHONY: down .PHONY: down
logs: ## display app-dev logs (follow mode) logs: ## display app-development logs (follow mode)
@$(COMPOSE) logs -f app-dev @$(COMPOSE) logs -f app-development
.PHONY: logs .PHONY: logs
run-backend: ## start only the backend application and all needed services run-backend: ## start only the backend application and all needed services
@$(COMPOSE) up --force-recreate -d celery-dev @$(COMPOSE) up --force-recreate -d celery-development
@echo "Wait for postgresql to be up..." @echo "Wait for postgresql to be up..."
@$(WAIT_DB) @$(WAIT_DB)
.PHONY: run-backend .PHONY: run-backend
@@ -227,14 +227,14 @@ back-i18n-generate: ## create the .pot files used for i18n
@$(MANAGE) makemessages -a --keep-pot @$(MANAGE) makemessages -a --keep-pot
.PHONY: back-i18n-generate .PHONY: back-i18n-generate
shell: ## connect to database shell shell: ## open a Django Python shell
@$(MANAGE) shell #_plus @$(MANAGE) shell #_plus
.PHONY: dbshell .PHONY: shell
# -- Database # -- Database
dbshell: ## connect to database shell dbshell: ## connect to database shell
docker compose exec app-dev python manage.py dbshell docker compose exec app-development python manage.py dbshell
.PHONY: dbshell .PHONY: dbshell
resetdb: FLUSH_ARGS ?= resetdb: FLUSH_ARGS ?=
@@ -348,16 +348,16 @@ build-k8s-cluster: ## build the kubernetes cluster using kind
install-external-secrets: ## install the kubernetes secrets from Vaultwarden install-external-secrets: ## install the kubernetes secrets from Vaultwarden
./bin/install-external-secrets.sh ./bin/install-external-secrets.sh
.PHONY: build-k8s-cluster .PHONY: install-external-secrets
start-tilt: ## start the kubernetes cluster using kind start-tilt: ## start the kubernetes cluster using kind
tilt up -f ./bin/Tiltfile tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster .PHONY: start-tilt
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak 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 DEV_ENV=dev-keycloak tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster .PHONY: start-tilt-keycloak
start-tilt-dinum: ## start the kubernetes cluster using kind, without Pro Connect for authentication, but with DINUM styles start-tilt-dinum: ## start the kubernetes cluster using kind, without Pro Connect for authentication, but with DINUM styles
DEV_ENV=dev-dinum tilt up -f ./bin/Tiltfile DEV_ENV=dev-dinum tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster .PHONY: start-tilt-dinum
+2 -2
View File
@@ -59,7 +59,7 @@ docker_build(
context='../src/summary', context='../src/summary',
dockerfile='../src/summary/Dockerfile', dockerfile='../src/summary/Dockerfile',
only=['.'], only=['.'],
target = 'production', target = 'summary-production',
live_update=[ live_update=[
sync('../src/summary', '/app'), sync('../src/summary', '/app'),
] ]
@@ -71,7 +71,7 @@ docker_build(
context='../src/agents', context='../src/agents',
dockerfile='../src/agents/Dockerfile', dockerfile='../src/agents/Dockerfile',
only=['.'], only=['.'],
target = 'production', target = 'agents-production',
live_update=[ live_update=[
sync('../src/agents', '/app'), sync('../src/agents', '/app'),
] ]
+1 -1
View File
@@ -89,5 +89,5 @@ function _dc_exec() {
# #
# ARGS : django's manage.py command arguments # ARGS : django's manage.py command arguments
function _django_manage() { function _django_manage() {
_dc_run "app-dev" python manage.py "$@" _dc_run "app-development" python manage.py "$@"
} }
+1 -1
View File
@@ -4,5 +4,5 @@ source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
_dc_run \ _dc_run \
-e DJANGO_CONFIGURATION=Test \ -e DJANGO_CONFIGURATION=Test \
app-dev \ app-development \
pytest "$@" pytest "$@"
+1 -1
View File
@@ -4,7 +4,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
_dc_run \ _dc_run \
-e DJANGO_CONFIGURATION=Test \ -e DJANGO_CONFIGURATION=Test \
app-dev \ app-development \
python manage.py spectacular \ python manage.py spectacular \
--api-version 'v1.0' \ --api-version 'v1.0' \
--urlconf 'meet.api_urls' \ --urlconf 'meet.api_urls' \
+14 -14
View File
@@ -55,13 +55,13 @@ services:
entrypoint: > entrypoint: >
sh -c " sh -c "
/usr/bin/mc alias set meet http://minio:9000 meet password && /usr/bin/mc alias set meet http://minio:9000 meet password &&
/usr/bin/mc admin config set meet notify_webhook:meet-webhook endpoint='http://app-dev:8000/api/v1.0/recordings/storage-hook/' auth_token='Bearer password' && /usr/bin/mc admin config set meet notify_webhook:meet-webhook endpoint='http://app-development:8000/api/v1.0/recordings/storage-hook/' auth_token='Bearer password' &&
/usr/bin/mc admin service restart meet --wait --json && /usr/bin/mc admin service restart meet --wait --json &&
sleep 15 && sleep 15 &&
/usr/bin/mc event add meet/meet-media-storage arn:minio:sqs::meet-webhook:webhook --event put && /usr/bin/mc event add meet/meet-media-storage arn:minio:sqs::meet-webhook:webhook --event put &&
exit 0;" exit 0;"
app-dev: app-development:
build: build:
context: . context: .
target: backend-development target: backend-development
@@ -94,7 +94,7 @@ services:
- resource-server - resource-server
- default - default
celery-dev: celery-development:
user: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000}
image: meet:backend-development image: meet:backend-development
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "DEBUG"] command: ["celery", "-A", "meet.celery_app", "worker", "-l", "DEBUG"]
@@ -107,9 +107,9 @@ services:
- ./src/backend:/app - ./src/backend:/app
- ./data/static:/data/static - ./data/static:/data/static
depends_on: depends_on:
- app-dev - app-development
app: app-production:
build: build:
context: . context: .
target: backend-production target: backend-production
@@ -128,7 +128,7 @@ services:
- livekit - livekit
- minio - minio
celery: celery-production:
user: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000}
image: meet:backend-production image: meet:backend-production
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "INFO"] command: ["celery", "-A", "meet.celery_app", "worker", "-l", "INFO"]
@@ -138,7 +138,7 @@ services:
- env.d/development/common - env.d/development/common
- env.d/development/postgresql - env.d/development/postgresql
depends_on: depends_on:
- app - app-production
nginx: nginx:
image: nginx:1.25 image: nginx:1.25
@@ -161,7 +161,7 @@ services:
args: args:
VITE_API_BASE_URL: "http://localhost:8071" VITE_API_BASE_URL: "http://localhost:8071"
VITE_APP_TITLE: "LaSuite Meet" VITE_APP_TITLE: "LaSuite Meet"
image: meet:frontend-development image: meet:frontend-production
ports: ports:
- "3000:8080" - "3000:8080"
@@ -249,10 +249,10 @@ services:
ports: ports:
- "6379:6379" - "6379:6379"
app-summary-dev: app-summary-development:
build: build:
context: src/summary context: src/summary
target: development target: summary-development
args: args:
DOCKER_USER: ${DOCKER_USER:-1000} DOCKER_USER: ${DOCKER_USER:-1000}
user: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000}
@@ -270,7 +270,7 @@ services:
build: build:
context: ./src/summary context: ./src/summary
dockerfile: Dockerfile dockerfile: Dockerfile
target: production target: summary-production
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q transcribe-queue command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q transcribe-queue
env_file: env_file:
- env.d/development/summary - env.d/development/summary
@@ -278,7 +278,7 @@ services:
- ./src/summary:/app - ./src/summary:/app
depends_on: depends_on:
- redis-summary - redis-summary
- app-summary-dev - app-summary-development
- minio - minio
develop: develop:
watch: watch:
@@ -290,7 +290,7 @@ services:
build: build:
context: ./src/summary context: ./src/summary
dockerfile: Dockerfile dockerfile: Dockerfile
target: production target: summary-production
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q summarize-queue command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q summarize-queue
env_file: env_file:
- env.d/development/summary - env.d/development/summary
@@ -298,7 +298,7 @@ services:
- ./src/summary:/app - ./src/summary:/app
depends_on: depends_on:
- redis-summary - redis-summary
- app-summary-dev - app-summary-development
- minio - minio
develop: develop:
watch: watch:
+1 -1
View File
@@ -7,4 +7,4 @@ keys:
webhook: webhook:
api_key: devkey api_key: devkey
urls: urls:
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/ - http://app-development:8000/api/v1.0/rooms/webhooks-livekit/
+1 -1
View File
@@ -61,7 +61,7 @@ ALLOW_UNREGISTERED_ROOMS=False
RECORDING_ENABLE=True RECORDING_ENABLE=True
RECORDING_STORAGE_EVENT_ENABLE=True RECORDING_STORAGE_EVENT_ENABLE=True
RECORDING_STORAGE_EVENT_TOKEN=password RECORDING_STORAGE_EVENT_TOKEN=password
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v1/tasks/ SUMMARY_SERVICE_ENDPOINT=http://app-summary-development:8000/api/v1/tasks/
SUMMARY_SERVICE_API_TOKEN=password SUMMARY_SERVICE_API_TOKEN=password
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME="meet-app-summary-dev" APP_NAME="meet-app-summary-development"
APP_API_TOKEN="password" APP_API_TOKEN="password"
AWS_STORAGE_BUCKET_NAME="http://meet-media-storage" AWS_STORAGE_BUCKET_NAME="http://meet-media-storage"
+1 -1
View File
@@ -15,7 +15,7 @@ COPY pyproject.toml .
RUN mkdir /install && \ RUN mkdir /install && \
pip install --prefix=/install . pip install --prefix=/install .
FROM base AS production FROM base AS agents-production
WORKDIR /app WORKDIR /app
@@ -7,7 +7,7 @@ import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList' import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode, useEffect, useRef } from 'react' import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat' import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects' import { Effects } from './effects/Effects'
import { Admin } from './Admin' import { Admin } from './Admin'
@@ -15,11 +15,9 @@ import { Tools } from './Tools'
import { Info } from './Info' import { Info } from './Info'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
const SIDE_PANEL_HEADING_ID = 'side-panel-heading'
const SIDE_PANEL_CLOSE_ID = 'side-panel-close'
type StyledSidePanelProps = { type StyledSidePanelProps = {
title: string title: string
ariaLabel: string
children: ReactNode children: ReactNode
onClose: () => void onClose: () => void
isClosed: boolean isClosed: boolean
@@ -31,6 +29,7 @@ type StyledSidePanelProps = {
const StyledSidePanel = ({ const StyledSidePanel = ({
title, title,
ariaLabel,
children, children,
onClose, onClose,
isClosed, isClosed,
@@ -39,14 +38,7 @@ const StyledSidePanel = ({
onBack, onBack,
backButtonLabel, backButtonLabel,
}: StyledSidePanelProps) => ( }: StyledSidePanelProps) => (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- role="dialog" makes this interactive
<aside <aside
role="dialog"
aria-labelledby={!isClosed ? SIDE_PANEL_HEADING_ID : undefined}
aria-hidden={isClosed || undefined}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose()
}}
className={css({ className={css({
borderWidth: '1px', borderWidth: '1px',
borderStyle: 'solid', borderStyle: 'solid',
@@ -71,57 +63,57 @@ const StyledSidePanel = ({
style={{ style={{
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none', transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
}} }}
aria-hidden={isClosed}
aria-label={ariaLabel}
> >
<HStack alignItems="center"> <HStack alignItems="center">
{isSubmenu && ( {isSubmenu && (
<Button <Button
variant="secondaryText" variant="secondaryText"
size="sm" size="sm"
square square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })} className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel} aria-label={backButtonLabel}
onPress={onBack} onPress={onBack}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
</Button>
)}
<Heading
id={SIDE_PANEL_HEADING_ID}
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
> >
{title} <RiArrowLeftLine size={20} aria-hidden="true" />
</Heading> </Button>
</HStack> )}
<Div <Heading
position="absolute" slot="title"
top="5" level={1}
right="5" className={text({ variant: 'h2' })}
style={{ style={{
display: isClosed ? 'none' : undefined, paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}} }}
> >
<Button {title}
id={SIDE_PANEL_CLOSE_ID} </Heading>
invisible </HStack>
variant="tertiaryText" <Div
size="xs" position="absolute"
onPress={onClose} top="5"
aria-label={closeButtonTooltip} right="5"
tooltip={closeButtonTooltip} style={{
> display: isClosed ? 'none' : undefined,
<RiCloseLine /> }}
</Button> >
</Div> <Button
{children} invisible
variant="tertiaryText"
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
{children}
</aside> </aside>
) )
@@ -143,7 +135,6 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null} {keepAlive || isOpen ? children : null}
</div> </div>
) )
export const SidePanel = () => { export const SidePanel = () => {
const { const {
activePanelId, activePanelId,
@@ -159,51 +150,14 @@ export const SidePanel = () => {
} = useSidePanel() } = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' }) const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const triggerRef = useRef<HTMLElement | null>(null)
// The aside stays mounted (CSS slide + keepAlive), so we manually handle
// auto-focus on open and restore focus on close (via handleClose).
useEffect(() => {
if (!isSidePanelOpen) return
const active = document.activeElement as HTMLElement
// Menu items render as DIVs that unmount when the menu closes — resolve to the menu trigger
triggerRef.current =
active?.tagName === 'DIV'
? (document.querySelector<HTMLElement>('#room-options-trigger') ??
active)
: active
requestAnimationFrame(() => {
const closeBtn = document.getElementById(SIDE_PANEL_CLOSE_ID)
// Skip if a child panel already moved focus inside (e.g. Chat input)
if (closeBtn?.closest('aside')?.contains(document.activeElement)) return
closeBtn?.focus({ preventScroll: true })
})
}, [isSidePanelOpen])
const handleClose = () => {
const trigger = triggerRef.current
triggerRef.current = null
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
// Double RAF: first lets React re-render, second lets FocusScope release containment
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (trigger?.isConnected) {
trigger.focus({ preventScroll: true })
} else {
document
.querySelector<HTMLElement>('#room-options-trigger')
?.focus({ preventScroll: true })
}
})
})
}
return ( return (
<StyledSidePanel <StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)} title={t(`heading.${activeSubPanelId || activePanelId}`)}
onClose={handleClose} ariaLabel={t('ariaLabel')}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
closeButtonTooltip={t('closeButton', { closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`), content: t(`content.${activeSubPanelId || activePanelId}`),
})} })}
@@ -4,6 +4,7 @@ import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel' import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
import { import {
useIsRecordingModeEnabled, useIsRecordingModeEnabled,
RecordingMode, RecordingMode,
@@ -94,10 +95,26 @@ const ToolButton = ({
export const Tools = () => { export const Tools = () => {
const { data } = useConfig() const { data } = useConfig()
const { openTranscript, openScreenRecording, activeSubPanelId } = const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } =
useSidePanel() useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
// Restore focus to the element that opened the Tools panel
// following the same pattern as Chat.
useRestoreFocus(isToolsOpen, {
// If the active element is a MenuItem (DIV) that will be unmounted when the menu closes,
// find the "more options" button ("Plus d'options") that opened the menu
resolveTrigger: (activeEl) => {
if (activeEl?.tagName === 'DIV') {
return document.querySelector<HTMLElement>('#room-options-trigger')
}
// For direct button clicks (e.g. "Plus d'outils"), use the active element as is
return activeEl
},
restoreFocusRaf: true,
preventScroll: true,
})
const isTranscriptEnabled = useIsRecordingModeEnabled( const isTranscriptEnabled = useIsRecordingModeEnabled(
RecordingMode.Transcript RecordingMode.Transcript
) )
@@ -80,12 +80,10 @@ export const ChatInput = ({
<TextArea <TextArea
ref={inputRef} ref={inputRef}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== 'Escape') e.stopPropagation() e.stopPropagation()
submitOnEnter(e) submitOnEnter(e)
}} }}
onKeyUp={(e) => { onKeyUp={(e) => e.stopPropagation()}
if (e.key !== 'Escape') e.stopPropagation()
}}
placeholder={t('textArea.placeholder')} placeholder={t('textArea.placeholder')}
value={text} value={text}
onChange={(e) => { onChange={(e) => {
@@ -15,6 +15,7 @@ import { ChatEntry } from '../components/chat/Entry'
import { useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import { LocalParticipant, RemoteParticipant, RoomEvent } from 'livekit-client' import { LocalParticipant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
export interface ChatProps export interface ChatProps
extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {} extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {}
@@ -35,12 +36,18 @@ export function Chat({ ...props }: ChatProps) {
const { isChatOpen } = useSidePanel() const { isChatOpen } = useSidePanel()
const chatSnap = useSnapshot(chatStore) const chatSnap = useSnapshot(chatStore)
React.useEffect(() => { // Keep track of the element that opened the chat so we can restore focus
if (!isChatOpen) return // when the chat panel is closed.
requestAnimationFrame(() => { useRestoreFocus(isChatOpen, {
inputRef.current?.focus({ preventScroll: true }) // Avoid layout "jump" during the side panel slide-in animation.
}) // Focusing can trigger scroll into view; preventScroll keeps the animation smooth.
}, [isChatOpen]) onOpened: () => {
requestAnimationFrame(() => {
inputRef.current?.focus({ preventScroll: true })
})
},
preventScroll: true,
})
// Use useParticipants hook to trigger a re-render when the participant list changes. // Use useParticipants hook to trigger a re-render when the participant list changes.
const participants = useParticipants() const participants = useParticipants()
+2 -2
View File
@@ -16,7 +16,7 @@ COPY pyproject.toml .
RUN pip3 install --no-cache-dir . RUN pip3 install --no-cache-dir .
FROM base AS development FROM base AS summary-development
WORKDIR /app WORKDIR /app
COPY . . COPY . .
@@ -24,7 +24,7 @@ RUN pip3 install --no-cache-dir -e ".[dev]" || pip3 install --no-cache-dir -e .
CMD ["uvicorn", "summary.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] CMD ["uvicorn", "summary.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
FROM base AS production FROM base AS summary-production
WORKDIR /app WORKDIR /app