Compare commits

...

28 Commits

Author SHA1 Message Date
lebaudantoine d537a4449a 🔖(minor) bump release to 0.1.18
Refactoring of transcription ui + SDK
2025-04-08 23:04:24 +02:00
lebaudantoine 50c9304afe (back) remove url-normalize dependency
We have the dependency url-normalize installed but we don't use it in
our codebase.

(from @lunika on docs)
2025-04-07 21:51:54 +02:00
lebaudantoine 37fe23c0f7 🚸(frontend) enhance transcript copywritting
Simplify the instructions. Also offer a way to user to discover
the feature and register as beta users.
2025-04-04 19:11:33 +02:00
lebaudantoine 255da4bf60 ♻️(frontend) refactor the transcription side panel into a sub menu
Add a new level of nesting while handling side panel.
Code quality is quite poor, we need a better interface.
2025-04-04 19:11:33 +02:00
lebaudantoine 6dccb507d2 🎨(frontend) remove duplicated forms literals
Use a proper constant, which already exists.
2025-04-04 19:11:33 +02:00
lebaudantoine d202a025e7 🚸(frontend) share more information about transcription state
Previous toast state was too naive. Enhance message deliver to
participants.
2025-04-04 19:11:33 +02:00
lebaudantoine ac9ba0df62 💩(frontend) introduce transcription store
Introduce a dedicated store for transcription to better manage its status
independently of meeting recording status. This lays the groundwork for
future improvements, especially as we plan to support additional recording
options beyond the current setup.

This isn't perfect and still coupled with room recording status
2025-04-04 19:11:33 +02:00
lebaudantoine 91562d049c ♻️(frontend) introduce a reusable tools sidepanel
tools will be added in the future, let's generalize the sidepanel
previously reserved to transcription.
2025-04-04 19:11:33 +02:00
lebaudantoine 60321296e5 (frontend) introduce a notification for transcription
Main goal: notify participant when the meeting is being recorded.
It helps understand the whole system behavior with clear
notification states.
2025-04-04 19:11:33 +02:00
lebaudantoine ae06873ff5 👔(frontend) update feedback form's URL
Gregoire reworked our framework on user feedbacks.
Update the Grist form's URL.
2025-04-04 15:57:45 +02:00
lebaudantoine 70ffb758c7 ♻️(frontend) refactor Grist forms urls in a single constants file
Consolidate all Grist forms into a single file to improve code organization
and enhance maintainability.
2025-04-04 15:57:45 +02:00
lebaudantoine 33a94da636 🩹(frontend) add a disabled style on tertiary button
Was lacking, necessary in my next jobs working on
transcript side pannel v2.
2025-04-04 15:57:45 +02:00
lebaudantoine aa6757601d 💄(frontend) force having medium font weight buttons
Improve the readability of the buttons by increasing their thickness.
2025-04-04 15:57:45 +02:00
lebaudantoine c26b1b711c (frontend) add external link support to LinkButton component
Enhanced LinkButton with target props for external link usage.
2025-04-04 15:57:45 +02:00
lebaudantoine 3ee33fc2ec ♻️(frontend) parametrize spinner size
Allow passing custom spinner size.
Needed to align the spinner height with an inline button.
2025-04-04 14:31:31 +02:00
lebaudantoine d43b2857c1 🩹(sdk) notify iframe parent when room is gathered through callback
On the first room creation, when the authentication redirection takes
place, the iframe wasn't properly messaging the parent.
Fixed it. Send the room data in any of the two methods to acquire it.

Actually, the code when gathering data through the callback endpoint is
a bit dirty.
2025-04-04 13:22:12 +02:00
lebaudantoine 3b1b644ef9 ⬆️(frontend) upgrade vite dependency
Necessary for security reasons, CVE-2025-30208.
2025-04-04 11:35:38 +02:00
lebaudantoine ab7a0e1ec2 ⬆️(sdk) upgrade vite dependency in consumer and library
Necessary for security reasons.
2025-04-04 11:35:38 +02:00
renovate[bot] 86a4d68daa ⬆️(dependencies) update django to v5.1.8 [SECURITY] 2025-04-04 10:44:07 +02:00
lebaudantoine de3df889ba (frontend) move support options to menu for a clearer UI
Relocated support options to the menu to keep the interface concise
and reduce visual clutter.
2025-04-04 10:43:51 +02:00
lebaudantoine 7c9fc15359 🚸(frontend) delay tooltip appearance in lateral menu
Improve UX by adding a delay before showing tooltips in the lateral menu.
Users often hover from bottom to top, causing tooltips to overlap other
options too quickly.
2025-04-04 10:43:51 +02:00
lebaudantoine a6a67f2752 🍱(readme) update readme to rename Visio to La Suite Meet
Visio is a MS product. In the official documentation call
our product La Suite Meet.
2025-04-03 23:34:49 +02:00
lebaudantoine 00eb52cd3f 🩹(summary) fix bold markdown syntax
Wrong syntaxt to handle bold, instead was italic.
2025-04-03 23:34:36 +02:00
lebaudantoine 6324be9fd3 🩹(frontend) fix missing notification when state is clear
Notify iframe's parent while integrating Visio when state
is cleared. Requested by Eleonore.
2025-04-03 23:33:23 +02:00
lebaudantoine 66f307b7e8 🐛(frontend) rework the calendar integration approach
Use a totally different strategy, which hopefully works in prod env.
Actually broadcast channel cannot be shared between two different
browsing context, even on a same domain, an iFrame and a pop up
cannot communicate.

Moreover, in an iFrame session cookie are unavailable.
Rely on a newly created backend endpoint to pass room data.
2025-04-02 12:31:04 +02:00
lebaudantoine 506b3978e1 🧑‍💻(frontend) move dev tool to the bottom left corner
Nothing happens in the bottom left corner.
Avoid hiding features while developing.
2025-04-02 12:31:04 +02:00
lebaudantoine db65aef56e 💄(frontend) introduc smNote text variant
Necessary for room information in calendar iframe.
2025-04-02 12:31:04 +02:00
lebaudantoine 4e1a4be650 (backend) introduce a creation callback endpoint
Necessary in cross browser context situation, where we need to
pass data of a room newly created between two different windows.

This happens in Calendar integration.
2025-04-02 12:31:04 +02:00
81 changed files with 1740 additions and 666 deletions
+13 -13
View File
@@ -1,5 +1,5 @@
<p align="center">
<img alt="posthoglogo" src="./docs/assets/visio-logo.png" maxWidth="100%">
<img alt="meet logo" src="./docs/assets/banner-meet-fr.png" maxWidth="100%">
</p>
@@ -21,13 +21,13 @@
<p align="center">
<a href="https://visio.numerique.gouv.fr/">
<img src="https://github.com/user-attachments/assets/09c1faa1-de88-4848-af3a-6fbe793999bf" alt="Visio Demonstration">
<img src="https://github.com/user-attachments/assets/09c1faa1-de88-4848-af3a-6fbe793999bf" alt="La Suite Meet Demonstration">
</a>
</p>
## Visio: Simple Video Conferencing
## La Suite Meet: Simple Video Conferencing
Powered by [LiveKit](https://livekit.io/), Visio offers Zoom-level performance with high-quality video and audio. No installation required—simply join calls directly from your browser. Check out LiveKit's impressive optimizations in their [blog post](https://blog.livekit.io/livekit-one-dot-zero/).
Powered by [LiveKit](https://livekit.io/), La Suite Meet offers Zoom-level performance with high-quality video and audio. No installation required—simply join calls directly from your browser. Check out LiveKit's impressive optimizations in their [blog post](https://blog.livekit.io/livekit-one-dot-zero/).
### Features
- Optimized for stability in large meetings (+100 p.)
- Support for multiple screen sharing streams
@@ -45,7 +45,7 @@ Powered by [LiveKit](https://livekit.io/), Visio offers Zoom-level performance w
- SVC codecs (VP9, AV1)
Visio is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's simple to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
La Suite Meet is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's simple to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
Were continuously adding new features to enhance your experience, with the latest updates coming soon!
@@ -61,31 +61,31 @@ Were continuously adding new features to enhance your experience, with the la
## Get started
### Visio Cloud (Recommended)
Sign up for Visio Cloud, designed for European public servants. Hosted on SecNumCloud-compliant providers and accessible via government SSO, [ProConnect](https://www.proconnect.gouv.fr/). The easiest way to try our product. Reach out if your entity isn't connected yet to our sso.
### La Suite Meet Cloud (Recommended)
Sign up for La Suite Meet Cloud, designed for french public servants. Hosted on SecNumCloud-compliant providers and accessible via government SSO, [ProConnect](https://www.proconnect.gouv.fr/). The easiest way to try our product. Reach out if your entity isn't connected yet to our sso.
### Open-source deployment (Advanced)
Deploy Visio on your own infrastructure using [our self-hosting guide](https://github.com/suitenumerique/meet/blob/main/docs/installation.md). Our open-source deployment is optimized for Kubernetes, and we're working on supporting additional deployment options. Keycloak integration and any SSO are supported. We offer customer support for open-source setups—just reach out for assistance.
Deploy La Suite Meet on your own infrastructure using [our self-hosting guide](https://github.com/suitenumerique/meet/blob/main/docs/installation.md). Our open-source deployment is optimized for Kubernetes, and we're working on supporting additional deployment options. Keycloak integration and any SSO are supported. We offer customer support for open-source setups—just reach out for assistance.
## Docs
We're currently working on both technical and user documentation for Visio. In the meantime, many of the essential aspects are already well covered by the [LiveKit documentation](https://docs.livekit.io/home/) and their [self-hosting guide](https://docs.livekit.io/home/self-hosting/deployment/). Stay tuned for more updates!
We're currently working on both technical and user documentation for La Suite Meet. In the meantime, many of the essential aspects are already well covered by the [LiveKit documentation](https://docs.livekit.io/home/) and their [self-hosting guide](https://docs.livekit.io/home/self-hosting/deployment/). Stay tuned for more updates!
## Contributing
We <3 contributions of any kind, big and small:
- Vote on features or get early access to beta functionality in our [roadmap](https://github.com/orgs/suitenumerique/projects/3/views/2)
- Open a PR (see our instructions on [developing Visio locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md))
- Open a PR (see our instructions on [developing La Suite Meet locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md))
- Submit a [feature request](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=enhancement&template=Feature_request.md) or [bug report](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md)
## Philosophy
Were relentlessly focused on building the best open-source video conferencing product—Visio. Growth comes from creating something people truly need, not just from chasing metrics.
Were relentlessly focused on building the best open-source video conferencing product—La Suite Meet. Growth comes from creating something people truly need, not just from chasing metrics.
Our users come first. Were committed to making Visio as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
Our users come first. Were committed to making La Suite Meet as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
Most of the heavy engineering is handled by the incredible LiveKit team, allowing us to focus on delivering a top-tier product. We follow extreme programming practices, favoring pair programming and quick, iterative releases. Challenge our tech and architecture—simplicity is always our top priority.
@@ -99,7 +99,7 @@ To learn more, don't hesitate to [reach out](mailto:visio@numerique.gouv.fr).
### Help us!
Come help us make Visio even better. We're growing fast and [would love some help](mailto:visio@numerique.gouv.fr).
Come help us make La Suite Meet even better. We're growing fast and [would love some help](mailto:visio@numerique.gouv.fr).
## Contributors 🧞
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

+14
View File
@@ -209,3 +209,17 @@ class ParticipantEntrySerializer(serializers.Serializer):
def update(self, instance, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
class CreationCallbackSerializer(serializers.Serializer):
"""Validate room creation callback data."""
callback_id = serializers.CharField(required=True)
def create(self, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
def update(self, instance, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("StartRecordingSerializer is validation-only")
+35
View File
@@ -47,6 +47,7 @@ from core.services.lobby import (
LobbyParticipantNotFound,
LobbyService,
)
from core.services.room_creation import RoomCreation
from . import permissions, serializers
@@ -186,6 +187,12 @@ class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
scope = "request_entry"
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
"""Throttle Anonymous user requesting room generation callback"""
scope = "creation_callback"
class RoomViewSet(
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
@@ -268,6 +275,9 @@ class RoomViewSet(
role=models.RoleChoices.OWNER,
)
if callback_id := self.request.data.get("callback_id"):
RoomCreation().persist_callback_state(callback_id, room)
@decorators.action(
detail=True,
methods=["post"],
@@ -460,6 +470,31 @@ class RoomViewSet(
{"status": "error", "message": str(e)}, status=status_code
)
@decorators.action(
detail=False,
methods=["POST"],
url_path="creation-callback",
permission_classes=[],
throttle_classes=[CreationCallbackAnonRateThrottle],
)
def creation_callback(self, request):
"""Retrieve cached room data via an unauthenticated request with a unique ID.
Designed for interoperability across iframes, popups, and other contexts,
even on the same domain, bypassing browser security restrictions on direct communication.
"""
serializer = serializers.CreationCallbackSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
room = RoomCreation().get_callback_state(
callback_id=serializer.validated_data.get("callback_id")
)
return drf_response.Response(
{"status": "success", "room": room}, status=drf_status.HTTP_200_OK
)
class ResourceAccessListModelMixin:
"""List mixin for resource access API."""
@@ -0,0 +1,37 @@
"""Room creation service."""
from django.conf import settings
from django.core.cache import cache
class RoomCreation:
"""Room creation related methods"""
@staticmethod
def _get_cache_key(callback_id):
"""Generate a standardized cache key for room creation callbacks."""
return f"room-creation-callback_{callback_id}"
def persist_callback_state(self, callback_id: str, room) -> None:
"""Store room data in cache using the callback ID as an identifier."""
data = {
"slug": room.slug,
}
cache.set(
self._get_cache_key(callback_id),
data,
timeout=settings.ROOM_CREATION_CALLBACK_CACHE_TIMEOUT,
)
def get_callback_state(self, callback_id: str) -> dict:
"""Retrieve and clear cached room data for the given callback ID."""
cache_key = self._get_cache_key(callback_id)
data = cache.get(cache_key)
if not data:
return {}
cache.delete(cache_key)
return data
@@ -2,6 +2,9 @@
Test rooms API endpoints in the Meet core app: create.
"""
# pylint: disable=W0621,W0613
from django.core.cache import cache
import pytest
from rest_framework.test import APIClient
@@ -11,6 +14,15 @@ from ...models import Room
pytestmark = pytest.mark.django_db
@pytest.fixture
def reset_cache():
"""Provide cache cleanup after each test to maintain test isolation."""
yield
keys = cache.keys("room-creation-callback_*")
if keys:
cache.delete(*keys)
def test_api_rooms_create_anonymous():
"""Anonymous users should not be allowed to create rooms."""
client = APIClient()
@@ -26,7 +38,7 @@ def test_api_rooms_create_anonymous():
assert Room.objects.exists() is False
def test_api_rooms_create_authenticated():
def test_api_rooms_create_authenticated(reset_cache):
"""
Authenticated users should be able to create rooms and should automatically be declared
as owner of the newly created room.
@@ -49,6 +61,33 @@ def test_api_rooms_create_authenticated():
assert room.slug == "my-room"
assert room.accesses.filter(role="owner", user=user).exists() is True
rooms_data = cache.keys("room-creation-callback_*")
assert not rooms_data
def test_api_rooms_create_generation_cache(reset_cache):
"""
Authenticated users creating a room with a callback ID should have room data stored in cache.
"""
user = UserFactory()
client = APIClient()
client.force_login(user)
response = client.post(
"/api/v1.0/rooms/",
{"name": "my room", "callback_id": "1234"},
)
assert response.status_code == 201
room = Room.objects.get()
assert room.name == "my room"
assert room.slug == "my-room"
assert room.accesses.filter(role="owner", user=user).exists() is True
room_data = cache.get("room-creation-callback_1234")
assert room_data.get("slug") == "my-room"
def test_api_rooms_create_authenticated_existing_slug():
"""
@@ -0,0 +1,98 @@
"""
Test rooms API endpoints in the Meet core app: creation callback functionality.
"""
# pylint: disable=W0621,W0613
from django.core.cache import cache
import pytest
from rest_framework.test import APIClient
from ...factories import UserFactory
pytestmark = pytest.mark.django_db
# Tests for creation_callback endpoint
@pytest.fixture
def reset_cache():
"""Provide cache cleanup after each test to maintain test isolation."""
yield
keys = cache.keys("room-creation-callback_*")
if keys:
cache.delete(*keys)
def test_api_rooms_create_anonymous(reset_cache):
"""Anonymous user can retrieve room data once using a valid callback ID."""
client = APIClient()
cache.set("room-creation-callback_123", {"slug": "my room"})
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
# Data should be cleared after retrieval
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {}}
def test_api_rooms_create_empty_cache():
"""Valid callback ID return empty room data when nothing is stored in the cache."""
client = APIClient()
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {}}
def test_api_rooms_create_missing_callback_id():
"""Requests without a callback ID properly fail with a 400 status code."""
client = APIClient()
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{},
)
assert response.status_code == 400
def test_api_rooms_create_authenticated(reset_cache):
"""Authenticated users can also successfully retrieve room data using a valid callback ID"""
user = UserFactory()
client = APIClient()
client.force_login(user)
cache.set("room-creation-callback_123", {"slug": "my room"})
response = client.post(
"/api/v1.0/rooms/creation-callback/",
{
"callback_id": "123",
},
)
assert response.status_code == 200
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
+12
View File
@@ -272,6 +272,11 @@ class Base(Configuration):
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
environ_prefix=None,
),
"creation_callback": values.Value(
default="600/minute",
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
environ_prefix=None,
),
},
}
@@ -528,6 +533,13 @@ class Base(Configuration):
environ_prefix=None,
)
# Calendar integrations
ROOM_CREATION_CALLBACK_CACHE_TIMEOUT = values.PositiveIntegerValue(
600, # 10 minutes
environ_name="ROOM_CREATION_CALLBACK_CACHE_TIMEOUT",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
+2 -3
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.17"
version = "0.1.18"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -37,7 +37,7 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.5",
"django-timezone-field>=5.1",
"django==5.1.7",
"django==5.1.8",
"djangorestframework==3.15.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
@@ -53,7 +53,6 @@ dependencies = [
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.24.1",
"url-normalize==1.4.3",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
"livekit-api==0.8.2",
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "0.1.17",
"version": "0.1.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.17",
"version": "0.1.18",
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.4",
@@ -49,7 +49,7 @@
"postcss": "8.5.3",
"prettier": "3.5.3",
"typescript": "5.8.2",
"vite": "6.2.3",
"vite": "6.2.4",
"vite-tsconfig-paths": "5.1.4"
}
},
@@ -9571,9 +9571,9 @@
}
},
"node_modules/vite": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.17",
"version": "0.1.18",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -54,7 +54,7 @@
"postcss": "8.5.3",
"prettier": "3.5.3",
"typescript": "5.8.2",
"vite": "6.2.3",
"vite": "6.2.4",
"vite-tsconfig-paths": "5.1.4"
}
}
+5 -26
View File
@@ -5,7 +5,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClientProvider } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useLang } from 'hoofd'
import { Switch, Route, useLocation } from 'wouter'
import { Switch, Route } from 'wouter'
import { I18nProvider } from 'react-aria-components'
import { Layout } from './layout/Layout'
import { NotFoundScreen } from './components/NotFoundScreen'
@@ -13,35 +13,11 @@ import { routes } from './routes'
import './i18n/init'
import { queryClient } from '@/api/queryClient'
import { AppInitialization } from '@/components/AppInitialization'
import { SdkCreateButton } from './features/sdk/routes/CreateButton'
const SDK_BASE_ROUTE = '/sdk'
function App() {
const { i18n } = useTranslation()
useLang(i18n.language)
const [location] = useLocation()
const isSDKRoute = location.startsWith(SDK_BASE_ROUTE)
if (isSDKRoute) {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={null}>
<I18nProvider locale={i18n.language}>
<Switch>
<Route path={SDK_BASE_ROUTE} nest>
<Route path="/create-button">
<SdkCreateButton />
</Route>
</Route>
</Switch>
</I18nProvider>
</Suspense>
</QueryClientProvider>
)
}
return (
<QueryClientProvider client={queryClient}>
<AppInitialization />
@@ -55,7 +31,10 @@ function App() {
<Route component={NotFoundScreen} />
</Switch>
</Layout>
<ReactQueryDevtools initialIsOpen={false} buttonPosition="top-left" />
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-left"
/>
</I18nProvider>
</Suspense>
</QueryClientProvider>
+1
View File
@@ -4,4 +4,5 @@ export const keys = {
config: 'config',
requestEntry: 'requestEntry',
waitingParticipants: 'waitingParticipants',
roomCreationCallback: 'roomCreationCallback',
}
@@ -2,9 +2,13 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
import { useLocation } from 'wouter'
const SDK_BASE_ROUTE = '/sdk'
export const AppInitialization = () => {
const { data } = useConfig()
const [location] = useLocation()
const {
analytics = {},
@@ -12,8 +16,11 @@ export const AppInitialization = () => {
silence_livekit_debug_logs = false,
} = data || {}
useAnalytics(analytics)
useSupport(support)
const isSDKContext = location.includes(SDK_BASE_ROUTE)
useAnalytics({ ...analytics, isDisabled: isSDKContext })
useSupport({ ...support, isDisabled: isSDKContext })
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
@@ -2,7 +2,7 @@ import { css } from '@/styled-system/css'
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Text, A } from '@/primitives'
import { GRIST_FORM } from '@/utils/constants'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
export const FeedbackBanner = () => {
const { t } = useTranslation()
@@ -35,7 +35,7 @@ export const FeedbackBanner = () => {
gap: 0.25,
})}
>
<A href={GRIST_FORM} target="_blank" size="sm">
<A href={GRIST_FEEDBACKS_FORM} target="_blank" size="sm">
{t('feedback.cta')}
</A>
<RiExternalLinkLine size={16} aria-hidden="true" />
@@ -17,18 +17,19 @@ export const terminateAnalyticsSession = () => {
export type useAnalyticsProps = {
id?: string
host?: string
isDisabled?: boolean
}
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => {
const [location] = useLocation()
useEffect(() => {
if (!id || !host) return
if (!id || !host || isDisabled) return
if (posthog.__loaded) return
posthog.init(id, {
api_host: host,
person_profiles: 'always',
})
}, [id, host])
}, [id, host, isDisabled])
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
useEffect(() => {
@@ -8,10 +8,7 @@ import { Button, LinkButton } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// todo - extract in a proper env variable
const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'
import { BETA_USERS_FORM_URL } from '@/utils/constants'
const Heading = styled('h2', {
base: {
@@ -207,6 +204,7 @@ export const IntroSlider = () => {
{slide.isAvailableInBeta && (
<LinkButton
href={BETA_USERS_FORM_URL}
target="_blank"
tooltip={t('beta.tooltip')}
variant={'primary'}
size={'sm'}
+1 -14
View File
@@ -13,12 +13,11 @@ import { RiAddLine, RiLink } from '@remixicon/react'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { IntroSlider } from '@/features/home/components/IntroSlider'
import { MoreLink } from '@/features/home/components/MoreLink'
import { ReactNode, useEffect, useState } from 'react'
import { ReactNode, useState } from 'react'
import { css } from '@/styled-system/css'
import { menuRecipe } from '@/primitives/menuRecipe.ts'
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
import { SdkReverseClient } from '@/features/sdk/SdkReverseClient'
const Columns = ({ children }: { children?: ReactNode }) => {
return (
@@ -156,18 +155,6 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
const { user } = useUser()
/**
* Used for SDK popup to close automatically.
*/
useEffect(() => {
if (!user) {
return
}
SdkReverseClient.broadcastAuthentication()
}, [user])
return (
<UserAware>
<Screen>
@@ -88,6 +88,16 @@ export const MainNotificationToast = () => {
if (notification.data?.emoji)
handleEmoji(notification.data.emoji, participant)
break
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
toastQueue.add(
{
participant,
type: notification.type,
},
{ timeout: NotificationDuration.ALERT }
)
break
default:
return
}
@@ -6,4 +6,6 @@ export enum NotificationType {
LowerHand = 'lowerHand',
ReactionReceived = 'reactionReceived',
ParticipantWaiting = 'participantWaiting',
TranscriptionStarted = 'transcriptionStarted',
TranscriptionStopped = 'transcriptionStopped',
}
@@ -9,6 +9,7 @@ import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastTranscript } from './ToastTranscript'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -36,6 +37,10 @@ const renderToast = (
case NotificationType.LowerHand:
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
return <ToastTranscript key={toast.key} toast={toast} state={state} />
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -0,0 +1,33 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { NotificationType } from '../NotificationType'
export function ToastTranscript({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant
const type = props.toast.content.type
const key = `recording${type == NotificationType.TranscriptionStarted ? 'Started' : 'Stopped'}`
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
{t(key, {
name: participant.name,
})}
</HStack>
</StyledToastContainer>
)
}
@@ -5,17 +5,20 @@ import { ApiRoom } from './ApiRoom'
export interface CreateRoomParams {
slug: string
callbackId?: string
username?: string
}
const createRoom = ({
slug,
callbackId,
username = '',
}: CreateRoomParams): Promise<ApiRoom> => {
return fetchApi(`rooms/?username=${encodeURIComponent(username)}`, {
method: 'POST',
body: JSON.stringify({
name: slug,
callback_id: callbackId,
}),
})
}
@@ -1,43 +0,0 @@
import { css } from '@/styled-system/css'
import { RiRecordCircleLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useRoomContext } from '@livekit/components-react'
export const RecordingStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording' })
const room = useRoomContext()
if (!room?.isRecording) return
return (
<div
className={css({
display: 'flex',
position: 'fixed',
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.25rem 0.35rem',
backgroundColor: 'primaryDark.200',
borderColor: 'primaryDark.400',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<RiRecordCircleLine
size={20}
className={css({
color: 'white',
backgroundColor: 'danger.700',
padding: '3px',
borderRadius: '3px',
})}
/>
<Text variant={'sm'}>{t('label')}</Text>
</div>
)
}
@@ -3,15 +3,15 @@ import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Button, Div } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat'
import { Transcript } from './Transcript'
import { Effects } from './effects/Effects'
import { Admin } from './Admin'
import { Tools } from './Tools'
type StyledSidePanelProps = {
title: string
@@ -19,6 +19,8 @@ type StyledSidePanelProps = {
onClose: () => void
isClosed: boolean
closeButtonTooltip: string
isSubmenu: boolean
onBack: () => void
}
const StyledSidePanel = ({
@@ -27,6 +29,8 @@ const StyledSidePanel = ({
onClose,
isClosed,
closeButtonTooltip,
isSubmenu = false,
onBack,
}: StyledSidePanelProps) => (
<div
className={css({
@@ -61,9 +65,22 @@ const StyledSidePanel = ({
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : undefined,
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
>
{isSubmenu && (
<Button
variant="secondaryText"
size={'sm'}
square
className={css({ marginRight: '0.5rem' })}
onPress={onBack}
>
<RiArrowLeftLine size={20} />
</Button>
)}
{title}
</Heading>
<Div
@@ -114,19 +131,26 @@ export const SidePanel = () => {
isEffectsOpen,
isChatOpen,
isSidePanelOpen,
isTranscriptOpen,
isToolsOpen,
isAdminOpen,
isSubPanelOpen,
activeSubPanelId,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
return (
<StyledSidePanel
title={t(`heading.${activePanelId}`)}
onClose={() => (layoutStore.activePanelId = null)}
title={t(`heading.${activeSubPanelId || activePanelId}`)}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activePanelId}`),
content: t(`content.${activeSubPanelId || activePanelId}`),
})}
isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen}
onBack={() => (layoutStore.activeSubPanelId = null)}
>
<Panel isOpen={isParticipantsOpen}>
<ParticipantsList />
@@ -137,8 +161,8 @@ export const SidePanel = () => {
<Panel isOpen={isChatOpen}>
<Chat />
</Panel>
<Panel isOpen={isTranscriptOpen}>
<Transcript />
<Panel isOpen={isToolsOpen}>
<Tools />
</Panel>
<Panel isOpen={isAdminOpen}>
<Admin />
@@ -0,0 +1,113 @@
import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { CRISP_HELP_ARTICLE_MORE_TOOLS } from '@/utils/constants'
import { ReactNode } from 'react'
import { Transcript } from './Transcript'
import { RiFileTextFill } from '@remixicon/react'
import { useIsTranscriptEnabled } from '../hooks/useIsTranscriptEnabled'
import { useSidePanel } from '../hooks/useSidePanel'
export interface ToolsButtonProps {
icon: ReactNode
title: string
description: string
onPress: () => void
}
const ToolButton = ({
icon,
title,
description,
onPress,
}: ToolsButtonProps) => {
return (
<RACButton
className={css({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start',
paddingY: '0.5rem',
paddingX: '0.75rem 1.5rem',
borderRadius: '5px',
gap: '1.25rem',
width: 'full',
textAlign: 'start',
'&[data-hovered]': {
backgroundColor: 'primary.50',
cursor: 'pointer',
},
})}
onPress={onPress}
>
<div
className={css({
height: '50px',
minWidth: '50px',
borderRadius: '25px',
backgroundColor: 'primary.800',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
{icon}
</div>
<div>
<Text margin={false} as="h3">
{title}
</Text>
<Text as="p" variant="smNote" wrap="pretty">
{description}
</Text>
</div>
</RACButton>
)
}
export const Tools = () => {
const { openTranscript, isTranscriptOpen } = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
const isTranscriptEnabled = useIsTranscriptEnabled()
if (isTranscriptOpen && isTranscriptEnabled) {
return <Transcript />
}
return (
<Div
display="flex"
overflowY="scroll"
padding="0 0.75rem"
flexGrow={1}
flexDirection="column"
alignItems="start"
>
<Text
variant="note"
wrap="balance"
className={css({
textStyle: 'sm',
paddingX: '0.75rem',
})}
margin="md"
>
{t('body')}{' '}
<A href={CRISP_HELP_ARTICLE_MORE_TOOLS} target="_blank">
{t('moreLink')}
</A>
.
</Text>
{isTranscriptEnabled && (
<ToolButton
icon={<RiFileTextFill size={24} color="white" />}
title={t('tools.transcript.title')}
description={t('tools.transcript.body')}
onPress={() => openTranscript()}
/>
)}
</Div>
)
}
@@ -1,10 +1,7 @@
import { Button, Div, H, Text } from '@/primitives'
import { A, Button, Div, LinkButton, Text } from '@/primitives'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { css } from '@/styled-system/css'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import { RiRecordCircleLine, RiStopCircleLine } from '@remixicon/react'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
@@ -12,22 +9,34 @@ import {
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
import { NotificationType } from '@/features/notifications/NotificationType'
import { useSnapshot } from 'valtio/index'
import {
TranscriptionStatus,
transcriptionStore,
} from '@/stores/transcription.ts'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import {
BETA_USERS_FORM_URL,
CRISP_HELP_ARTICLE_TRANSCRIPT,
} from '@/utils/constants'
export const Transcript = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
const hasTranscriptAccess = useHasTranscriptAccess()
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const transcriptionSnap = useSnapshot(transcriptionStore)
const room = useRoomContext()
useEffect(() => {
@@ -40,6 +49,17 @@ export const Transcript = () => {
}
}, [room])
const notifyParticipant = async (status: NotificationType) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
type: status,
}
const data = encoder.encode(JSON.stringify(payload))
await room.localParticipant.publishData(data, {
reliable: true,
})
}
const handleTranscript = async () => {
if (!roomId) {
console.warn('No room ID found')
@@ -49,8 +69,12 @@ export const Transcript = () => {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
await notifyParticipant(NotificationType.TranscriptionStopped)
transcriptionStore.status = TranscriptionStatus.STOPPING
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
await notifyParticipant(NotificationType.TranscriptionStarted)
transcriptionStore.status = TranscriptionStatus.STARTING
}
} catch (error) {
console.error('Failed to handle transcript:', error)
@@ -58,7 +82,13 @@ export const Transcript = () => {
}
}
if (!hasTranscriptAccess) return
const isDisabled = useMemo(
() =>
isLoading ||
transcriptionSnap.status === TranscriptionStatus.STARTING ||
transcriptionSnap.status === TranscriptionStatus.STOPPING,
[isLoading, transcriptionSnap]
)
return (
<Div
@@ -69,38 +99,98 @@ export const Transcript = () => {
flexDirection="column"
alignItems="center"
>
<img src={thirdSlide} alt={'wip'} />
{room.isRecording ? (
<img
src={thirdSlide}
alt={''}
className={css({
minHeight: '309px',
marginBottom: '1rem',
})}
/>
{!hasTranscriptAccess ? (
<>
<H lvl={2}>{t('stop.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('stop.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
<RiStopCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('stop.button')}
</Button>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
>
{t('beta.button')}
</LinkButton>
</>
) : (
<>
<H lvl={2}>{t('start.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('start.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="start-transcript"
>
<RiRecordCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('start.button')}
</Button>
{room.isRecording ? (
<>
<Text>{t('stop.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('stop.body')}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
</>
)}
</Div>
@@ -0,0 +1,103 @@
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio/index'
import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner'
import { useEffect, useMemo } from 'react'
import { Text } from '@/primitives'
import { RemoteParticipant, RoomEvent } from 'livekit-client'
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
import { NotificationType } from '@/features/notifications/NotificationType'
import { TranscriptionStatus, transcriptionStore } from '@/stores/transcription'
export const TranscriptStateToast = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'recording.transcript' })
const room = useRoomContext()
const transcriptionSnap = useSnapshot(transcriptionStore)
useEffect(() => {
if (room.isRecording) {
transcriptionStore.status = TranscriptionStatus.STARTED
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
participant?: RemoteParticipant
) => {
const notification = decodeNotificationDataReceived(payload)
if (!participant || !notification) return
switch (notification.type) {
case NotificationType.TranscriptionStarted:
transcriptionStore.status = TranscriptionStatus.STARTING
break
case NotificationType.TranscriptionStopped:
transcriptionStore.status = TranscriptionStatus.STOPPING
break
default:
return
}
}
const handleRecordingStatusChanged = (status: boolean) => {
transcriptionStore.status = status
? TranscriptionStatus.STARTED
: TranscriptionStatus.STOPPED
}
room.on(RoomEvent.DataReceived, handleDataReceived)
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived)
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room])
const key = useMemo(() => {
switch (transcriptionSnap.status) {
case TranscriptionStatus.STOPPING:
return 'stopping'
case TranscriptionStatus.STARTING:
return 'starting'
default:
return 'started'
}
}, [transcriptionSnap])
if (transcriptionSnap.status == TranscriptionStatus.STOPPED) return
return (
<div
className={css({
display: 'flex',
position: 'fixed',
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.75rem 0.75rem',
backgroundColor: 'primaryDark.100',
borderColor: 'white',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<Spinner size={20} variant="dark" />
<Text
variant={'sm'}
className={css({
fontWeight: '500 !important',
})}
>
{t(key)}
</Text>
</div>
)
}
@@ -2,14 +2,14 @@ import { RiMegaphoneLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { GRIST_FORM } from '@/utils/constants'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
export const FeedbackMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
return (
<MenuItem
href={GRIST_FORM}
href={GRIST_FEEDBACKS_FORM}
target="_blank"
className={menuRecipe({ icon: true, variant: 'dark' }).item}
>
@@ -4,6 +4,7 @@ import { FullScreenMenuItem } from './FullScreenMenuItem'
import { SettingsMenuItem } from './SettingsMenuItem'
import { FeedbackMenuItem } from './FeedbackMenuItem'
import { EffectsMenuItem } from './EffectsMenuItem'
import { SupportMenuItem } from './SupportMenuItem'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = () => {
@@ -20,6 +21,7 @@ export const OptionsMenuItems = () => {
</MenuSection>
<Separator />
<MenuSection>
<SupportMenuItem />
<FeedbackMenuItem />
<SettingsMenuItem />
</MenuSection>
@@ -0,0 +1,27 @@
import { RiQuestionLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { Crisp } from 'crisp-sdk-web'
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
export const SupportMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const isSupportEnabled = useIsSupportEnabled()
if (!isSupportEnabled || !Crisp) {
return
}
return (
<MenuItem
className={menuRecipe({ icon: true, variant: 'dark' }).item}
onAction={() => {
Crisp?.chat.open()
}}
>
<RiQuestionLine size={20} />
{t('support')}
</MenuItem>
)
}
@@ -1,60 +0,0 @@
import { ToggleButton } from '@/primitives'
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'
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const [isOpened, setIsOpened] = useState(() => {
return window?.$crisp?.is?.('chat:opened') || false
})
useEffect(() => {
if (!Crisp) {
return
}
Crisp.chat.onChatOpened(() => {
setIsOpened(true)
})
Crisp.chat.onChatClosed(() => {
setIsOpened(false)
})
return () => {
Crisp.chat.offChatOpened()
Crisp.chat.offChatClosed()
}
}, [])
const isSupportEnabled = useIsSupportEnabled()
if (!isSupportEnabled) {
return
}
return (
<ToggleButton
square
variant="primaryTextDark"
tooltip={t('support')}
aria-label={t('support')}
isSelected={isOpened}
onPress={(e) => {
if (isOpened) {
Crisp.chat.close()
} else {
Crisp.chat.open()
}
onPress?.(e)
}}
data-attr="controls-support"
{...props}
>
<RiQuestionLine />
</ToggleButton>
)
}
@@ -1,24 +1,19 @@
import { ToggleButton } from '@/primitives'
import { RiBardLine } from '@remixicon/react'
import { RiShapesLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useSidePanel } from '../../hooks/useSidePanel'
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
import { css } from '@/styled-system/css'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const TranscriptToggle = ({
export const ToolsToggle = ({
variant = 'primaryTextDark',
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
const { t } = useTranslation('rooms', { keyPrefix: 'controls.tools' })
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
const tooltipLabel = isTranscriptOpen ? 'open' : 'closed'
const hasTranscriptAccess = useHasTranscriptAccess()
if (!hasTranscriptAccess) return
const { isToolsOpen, toggleTools } = useSidePanel()
const tooltipLabel = isToolsOpen ? 'open' : 'closed'
return (
<div
@@ -32,15 +27,15 @@ export const TranscriptToggle = ({
variant={variant}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isTranscriptOpen}
isSelected={isToolsOpen}
onPress={(e) => {
toggleTranscript()
toggleTools()
onPress?.(e)
}}
{...props}
data-attr="toggle-transcript"
data-attr="toggle-tools"
>
<RiBardLine />
<RiShapesLine />
</ToggleButton>
</div>
)
@@ -5,53 +5,74 @@ export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TRANSCRIPT = 'transcript',
TOOLS = 'tools',
ADMIN = 'admin',
}
export enum SubPanelId {
TRANSCRIPT = 'transcript',
}
export const useSidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const activePanelId = layoutSnap.activePanelId
const activeSubPanelId = layoutSnap.activeSubPanelId
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId == PanelId.EFFECTS
const isChatOpen = activePanelId == PanelId.CHAT
const isTranscriptOpen = activePanelId == PanelId.TRANSCRIPT
const isToolsOpen = activePanelId == PanelId.TOOLS
const isAdminOpen = activePanelId == PanelId.ADMIN
const isTranscriptOpen = activeSubPanelId == SubPanelId.TRANSCRIPT
const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId
const toggleAdmin = () => {
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleParticipants = () => {
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleChat = () => {
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleEffects = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleTranscript = () => {
layoutStore.activePanelId = isTranscriptOpen ? null : PanelId.TRANSCRIPT
const toggleTools = () => {
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const openTranscript = () => {
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
layoutStore.activePanelId = PanelId.TOOLS
}
return {
activePanelId,
activeSubPanelId,
toggleParticipants,
toggleChat,
toggleEffects,
toggleTranscript,
toggleTools,
toggleAdmin,
openTranscript,
isSubPanelOpen,
isChatOpen,
isParticipantsOpen,
isEffectsOpen,
isSidePanelOpen,
isTranscriptOpen,
isToolsOpen,
isAdminOpen,
isTranscriptOpen,
}
}
@@ -14,15 +14,15 @@ import {
RiMore2Line,
RiSettings3Line,
} from '@remixicon/react'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
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'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
export function MobileControlBar({
@@ -134,11 +134,7 @@ export function MobileControlBar({
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<TranscriptToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<SupportToggle
<ToolsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
@@ -155,7 +151,7 @@ export function MobileControlBar({
<RiAccountBoxLine size={20} />
</Button>
<LinkButton
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
href={GRIST_FEEDBACKS_FORM}
variant="primaryTextDark"
tooltip={t('options.items.feedback')}
aria-label={t('options.items.feedback')}
@@ -1,8 +1,7 @@
import { css } from '@/styled-system/css'
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'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { AdminToggle } from '../../components/AdminToggle'
import { useSize } from '../../hooks/useResizeObserver'
import { useState, RefObject } from 'react'
@@ -14,13 +13,15 @@ import { useTranslation } from 'react-i18next'
const CONTROL_BAR_BREAKPOINT = 1100
const NavigationControls = ({ onPress }: Partial<ToggleButtonProps>) => (
const NavigationControls = ({
onPress,
tooltipType = 'instant',
}: Partial<ToggleButtonProps>) => (
<>
<ChatToggle onPress={onPress} />
<ParticipantsToggle onPress={onPress} />
<TranscriptToggle onPress={onPress} />
<SupportToggle onPress={onPress} />
<AdminToggle onPress={onPress} />
<ChatToggle onPress={onPress} tooltipType={tooltipType} />
<ParticipantsToggle onPress={onPress} tooltipType={tooltipType} />
<ToolsToggle onPress={onPress} tooltipType={tooltipType} />
<AdminToggle onPress={onPress} tooltipType={tooltipType} />
</>
)
@@ -55,7 +56,7 @@ export const LateralMenu = () => {
gap: '0.5rem',
})}
>
<NavigationControls onPress={handleClose} />
<NavigationControls onPress={handleClose} tooltipType="delayed" />
</Dialog>
</Popover>
</DialogTrigger>
@@ -28,7 +28,7 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '../components/RecordingStateToast'
import { TranscriptStateToast } from '../components/TranscriptStateToast'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
const LayoutWrapper = styled(
@@ -231,7 +231,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)}
<RoomAudioRenderer />
<ConnectionStateToast />
<RecordingStateToast />
<TranscriptStateToast />
</div>
)
}
@@ -1,84 +0,0 @@
import { authUrl, useUser } from '../auth'
export enum ClientMessageType {
ROOM_CREATED = 'ROOM_CREATED',
}
export class SdkReverseClient {
/**
* IDEA: Use API Key. Must be based on some sort of credentials? No needs for now as there are no security
* plausible at the moment.
*/
static getAllowTargetOrigin() {
return '*'
}
static post(type: ClientMessageType, data: unknown = {}) {
window.parent.postMessage(
{
type,
data,
},
SdkReverseClient.getAllowTargetOrigin()
)
}
static broadcastAuthentication() {
const bc = new BroadcastChannel('APP_CHANNEL')
bc.postMessage({ type: 'AUTHENTICATED' })
/**
* This means the parent window has authenticated has successfully refetched user, then we can close the popup.
*/
bc.onmessage = (event) => {
if (event.data.type === 'AUTHENTICATED_ACK') {
window.close()
}
}
}
static waitForAuthenticationAck() {
return new Promise<void>((resolve) => {
const bc = new BroadcastChannel('APP_CHANNEL')
bc.onmessage = async (event) => {
if (event.data.type === 'AUTHENTICATED') {
resolve()
bc.postMessage({ type: 'AUTHENTICATED_ACK' })
}
}
})
}
}
/**
* Returns a function to be awaited in order to make sure the user is logged in.
* If not logged-in it opens a popup with the connection flow, the promise returned is resolved
* once logged-in.
*
* To be used in SDK scope.
*/
export function useEnsureAuth() {
const { isLoggedIn, refetch } = useUser({
fetchUserOptions: { attemptSilent: false },
})
const startSSO = () => {
return new Promise<void>((resolve) => {
SdkReverseClient.waitForAuthenticationAck().then(async () => {
await refetch()
resolve()
})
const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=400,height=900,left=100,top=100`
window.open(new URL('authenticate/', authUrl()).href, '', params)
})
}
const ensureAuth = async () => {
if (!isLoggedIn) {
await startSSO()
}
}
return { ensureAuth }
}
@@ -0,0 +1,35 @@
import { fetchApi } from '@/api/fetchApi'
import { useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import { CallbackCreationRoomData } from '../utils/types'
export type CallbackResponse = {
status: string
room: CallbackCreationRoomData
}
export const fetchRoomGenerationState = async ({
callbackId,
}: {
callbackId: string
}) => {
return fetchApi<CallbackResponse>(`/rooms/creation-callback/`, {
method: 'POST',
body: JSON.stringify({
callback_id: callbackId,
}),
})
}
export const useRoomCreationCallback = ({
callbackId = '',
}: {
callbackId?: string
}) => {
return useQuery({
queryKey: [keys.roomCreationCallback, callbackId],
queryFn: () => fetchRoomGenerationState({ callbackId }),
enabled: !!callbackId,
refetchInterval: 1000,
})
}
@@ -1,103 +0,0 @@
import { Button } from '@/primitives/Button'
import { useTranslation } from 'react-i18next'
import { usePersistentUserChoices } from '@livekit/components-react'
import { useState } from 'react'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { css } from '@/styled-system/css'
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
import { VisioIcon } from '@/assets/VisioIcon'
import { generateRoomId, useCreateRoom } from '../../rooms'
import {
ClientMessageType,
SdkReverseClient,
useEnsureAuth,
} from '../SdkReverseClient'
export const SdkCreateButton = () => {
const { t } = useTranslation('sdk', { keyPrefix: 'createButton' })
const [roomUrl, setRoomUrl] = useState<string>()
const [isLoading, setIsLoading] = useState(false)
const {
userChoices: { username },
} = usePersistentUserChoices()
const { mutateAsync: createRoom } = useCreateRoom()
const { ensureAuth } = useEnsureAuth()
const submitCreateRoom = async () => {
setIsLoading(true)
const slug = generateRoomId()
const data = await createRoom({ slug, username })
const roomUrlTmp = getRouteUrl('room', data.slug)
setRoomUrl(roomUrlTmp)
setIsLoading(false)
SdkReverseClient.post(ClientMessageType.ROOM_CREATED, {
url: roomUrlTmp,
})
}
const submit = async () => {
await ensureAuth()
submitCreateRoom()
}
return (
<div
className={css({
paddingTop: '3px',
paddingLeft: '3px',
})}
>
{roomUrl ? (
<RoomUrl roomUrl={roomUrl} />
) : (
<Button
variant="primaryDark"
aria-label={t('label')}
onPress={submit}
data-attr="sdk-create"
loading={isLoading}
icon={<VisioIcon />}
>
{t('label')}
</Button>
)}
</div>
)
}
const RoomUrl = ({ roomUrl }: { roomUrl: string }) => {
const [isCopied, setIsCopied] = useState(false)
const copy = () => {
navigator.clipboard.writeText(roomUrl!)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 1000)
}
return (
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
})}
>
<span
className={css({
color: 'greyscale.600',
})}
>
{roomUrl}
</span>
<Button
variant={isCopied ? 'success' : 'quaternaryText'}
data-attr="sdk-create-copy"
onPress={copy}
square
>
{isCopied ? <RiCheckLine /> : <RiFileCopyLine />}
</Button>
</div>
)
}
@@ -0,0 +1,171 @@
import { Button } from '@/primitives/Button'
import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { HStack, VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { RiCloseLine, RiFileCopyLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { Spinner } from '@/primitives/Spinner'
import { buttonRecipe } from '@/primitives/buttonRecipe'
import { VisioIcon } from '@/assets/VisioIcon'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { useRoomCreationCallback } from '../api/useRoomCreationCallback'
import { PopupManager } from '../utils/PopupManager'
import { CallbackCreationRoomData } from '../utils/types'
import { useSearchParams } from 'wouter'
const popupManager = new PopupManager()
export const CreateMeetingButton = () => {
const { t } = useTranslation('sdk', { keyPrefix: 'createMeeting' })
const [searchParams] = useSearchParams()
const [callbackId, setCallbackId] = useState<string | undefined>(undefined)
const [isPending, setIsPending] = useState(false)
const initialRoom = useMemo(() => {
const roomSlug = searchParams.get('slug')
if (!roomSlug) return undefined
return {
slug: roomSlug.trim(), // Trim whitespace for safety
}
}, [searchParams])
const [room, setRoom] = useState<CallbackCreationRoomData | undefined>(
initialRoom
)
const { data } = useRoomCreationCallback({ callbackId })
const roomUrl = useMemo(() => {
if (room?.slug) return getRouteUrl('room', room.slug)
}, [room])
useEffect(() => {
if (!data?.room?.slug) return
setRoom(data.room)
setCallbackId(undefined)
setIsPending(false)
popupManager.sendRoomData({
room: {
url: getRouteUrl('room', data.room.slug),
...data.room,
},
})
}, [data])
useEffect(() => {
popupManager.setupMessageListener(
(id) => setCallbackId(id),
(data) => {
setRoom(data)
setIsPending(false)
}
)
return () => popupManager.cleanup()
}, [])
const resetState = () => {
setRoom(undefined)
setCallbackId(undefined)
setIsPending(false)
popupManager.clearState()
}
if (isPending) {
return (
<div>
<Spinner size={34} />
</div>
)
}
return (
<div
className="p-6"
style={{
display: 'flex',
justifyContent: 'start',
alignItems: 'start',
border: 'none',
}}
>
{roomUrl && room?.slug ? (
<VStack justify={'start'} alignItems={'start'} gap={0.25}>
<HStack>
<Link
className={buttonRecipe({ size: 'sm' })}
href={roomUrl}
target="_blank"
style={{
textWrap: 'nowrap',
}}
>
<VisioIcon />
{t('joinButton')}
</Link>
<HStack gap={0}>
<Button
variant="quaternaryText"
square
icon={<RiFileCopyLine />}
tooltip={t('copyLinkTooltip')}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
}}
/>
{searchParams.get('readOnly') === 'false' && (
<Button
variant="quaternaryText"
square
icon={<RiCloseLine />}
onPress={resetState}
aria-label={t('resetLabel')}
/>
)}
</HStack>
</HStack>
<VStack justify={'start'} alignItems="start" gap={0.25}>
<Text variant={'smNote'} margin={false} centered={false}>
{roomUrl.replace('https://', '')}
</Text>
<Text variant={'smNote'} margin={false} centered={false}>
{t('participantLimit')}
</Text>
</VStack>
</VStack>
) : (
<div
className={css({
minHeight: '46px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
})}
>
{/*
* Using popup for Visio to access session cookies (blocked in iframes).
* If authenticated: Popup creates room and returns data directly.
* If not: Popup sends callbackId, redirects to login, then backend
* associates new room with callbackId after authentication.
*/}
<Button
onPress={() => {
setIsPending(true)
popupManager.createPopupWindow(() => {
setIsPending(false)
})
}}
size="sm"
>
<VisioIcon />
{t('createButton')}
</Button>
</div>
)}
</div>
)
}
@@ -0,0 +1,76 @@
import { useEffect, useMemo } from 'react'
import { css } from '@/styled-system/css'
import { generateRoomId, useCreateRoom } from '../../rooms'
import { useUser } from '@/features/auth'
import { Spinner } from '@/primitives/Spinner'
import { CallbackIdHandler } from '../utils/CallbackIdHandler'
import { PopupWindow } from '../utils/PopupWindow'
const callbackIdHandler = new CallbackIdHandler()
const popupWindow = new PopupWindow()
export const CreatePopup = () => {
const { isLoggedIn } = useUser({ fetchUserOptions: { attemptSilent: false } })
const { mutateAsync: createRoom } = useCreateRoom()
const callbackId = useMemo(() => callbackIdHandler.getOrCreate(), [])
/**
* Handle unauthenticated users by redirecting to login
*
* When redirecting to authentication, the window.location change breaks the connection
* between this popup and its parent window. We need to send the callbackId to the parent
* before redirecting so it can re-establish connection after authentication completes.
* This prevents the popup from becoming orphaned and ensures state consistency.
*/
useEffect(() => {
if (isLoggedIn === false) {
// redirection loses the connection to the manager
// prevent it passing an async callback id
popupWindow.sendCallbackId(callbackId, () => {
popupWindow.navigateToAuthentication()
})
}
}, [isLoggedIn, callbackId])
/**
* Automatically create meeting room once user is authenticated
* This effect will trigger either immediately if the user is already logged in,
* or after successful authentication and return to this popup
*/
useEffect(() => {
const createMeetingRoom = async () => {
try {
const slug = generateRoomId()
const roomData = await createRoom({
slug,
callbackId,
})
// Send room data back to parent window and clean up resources
popupWindow.sendRoomData(roomData, () => {
callbackIdHandler.clear()
popupWindow.close()
})
} catch (error) {
console.error('Failed to create meeting room:', error)
}
}
if (isLoggedIn && callbackId) {
createMeetingRoom()
}
}, [isLoggedIn, callbackId, createRoom])
return (
<div
className={css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
})}
>
<Spinner />
</div>
)
}
@@ -0,0 +1,45 @@
export class CallbackIdHandler {
private storageKey = 'popup_callback_id'
private generateId(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
)
}
/**
* Gets an existing callback ID or creates a new one
*/
public getOrCreate(): string {
const existingId = this.get()
if (existingId) {
return existingId
}
const newId = this.generateId()
this.set(newId)
return newId
}
/**
* Gets the current callback ID if one exists
*/
public get(): string | null {
return sessionStorage.getItem(this.storageKey)
}
/**
* Sets a callback ID
*/
private set(id: string): void {
sessionStorage.setItem(this.storageKey, id)
}
/**
* Removes the current callback ID
*/
public clear(): void {
sessionStorage.removeItem(this.storageKey)
}
}
@@ -0,0 +1,77 @@
import { getRouteUrl } from '@/navigation/getRouteUrl'
import {
CallbackCreationRoomData,
ClientMessageType,
PopupMessageData,
PopupMessageType,
} from './types'
export class PopupManager {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private messageHandler: (event: MessageEvent<any>) => void = () => {}
public createPopupWindow(onFailure: () => void) {
const popupWindow = window.open(
`${window.location.origin}/sdk/create-popup`,
'CreatePopupWindow',
`status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes`
)
if (popupWindow) {
popupWindow.focus()
} else {
onFailure()
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private messageParent(type: ClientMessageType, data: any) {
window?.parent.postMessage(
{
type: type,
data: data,
},
'*'
)
}
public clearState() {
this.messageParent(ClientMessageType.STATE_CLEAR, {})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public sendRoomData(data: { room: any }) {
this.messageParent(ClientMessageType.ROOM_CREATED, data)
}
public setupMessageListener(
onCallbackId: (id: string) => void,
onRoomData: (data: CallbackCreationRoomData) => void
) {
this.messageHandler = (event) => {
const data = event.data as PopupMessageData
// Skip messages from untrusted sources
if (data.source !== window.location.origin) return
switch (data.type) {
case PopupMessageType.CALLBACK_ID:
onCallbackId(data.callbackId as string)
return
case PopupMessageType.ROOM_DATA:
if (!data?.room) return
onRoomData(data.room)
this.sendRoomData({
room: {
url: getRouteUrl('room', data.room.slug),
...data.room,
},
})
return
}
}
window.addEventListener('message', this.messageHandler)
}
public cleanup() {
window.removeEventListener('message', this.messageHandler)
}
}
@@ -0,0 +1,50 @@
import { authUrl } from '@/features/auth'
import { PopupMessageType, CallbackCreationRoomData } from './types'
export class PopupWindow {
private sendMessageToManager(
type: PopupMessageType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any,
callback?: () => void
) {
if (!window.opener) {
console.error('No manager window found')
window.close()
return
}
window.opener.postMessage(
{
source: window.location.origin,
type,
...data,
},
window.location.origin
)
callback?.()
}
public sendRoomData(data: CallbackCreationRoomData, callback?: () => void) {
this.sendMessageToManager(
PopupMessageType.ROOM_DATA,
{ room: { slug: data.slug } },
callback
)
}
public sendCallbackId(callbackId: string, callback?: () => void) {
this.sendMessageToManager(
PopupMessageType.CALLBACK_ID,
{ callbackId },
callback
)
}
public close() {
window.close()
}
public navigateToAuthentication() {
window.location.href = authUrl({})
}
}
@@ -0,0 +1,20 @@
export type CallbackCreationRoomData = {
slug: string
}
export enum ClientMessageType {
ROOM_CREATED = 'ROOM_CREATED',
STATE_CLEAR = 'STATE_CLEAR',
}
export interface PopupMessageData {
type: PopupMessageType
source: string
callbackId?: string
room?: CallbackCreationRoomData
}
export enum PopupMessageType {
CALLBACK_ID,
ROOM_DATA,
}
@@ -18,15 +18,16 @@ export const terminateSupportSession = () => {
export type useSupportProps = {
id?: string
isDisabled?: boolean
}
// Configure Crisp chat for real-time support across all pages.
export const useSupport = ({ id }: useSupportProps) => {
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
useEffect(() => {
if (!id || Crisp.isCrispInjected()) return
if (!id || Crisp.isCrispInjected() || isDisabled) return
Crisp.configure(id)
Crisp.setHideOnMobile(true)
}, [id])
}, [id, isDisabled])
return null
}
@@ -21,5 +21,7 @@
"several": "",
"open": "",
"accept": ""
}
},
"recordingStarted": "",
"recordingStopped": ""
}
+29 -7
View File
@@ -104,7 +104,7 @@
"open": "",
"closed": ""
},
"transcript": {
"tools": {
"open": "",
"closed": ""
},
@@ -112,7 +112,6 @@
"open": "",
"closed": ""
},
"support": "",
"moreOptions": "",
"reactions": {
"button": "",
@@ -131,7 +130,8 @@
"fullscreen": {
"enter": "",
"exit": ""
}
},
"support": ""
}
},
"effects": {
@@ -169,30 +169,48 @@
"effects": "",
"chat": "",
"transcript": "",
"admin": ""
"admin": "",
"tools": ""
},
"content": {
"participants": "",
"effects": "",
"chat": "",
"transcript": "",
"admin": ""
"admin": "",
"tools": ""
},
"closeButton": ""
},
"chat": {
"disclaimer": ""
},
"moreTools": {
"body": "",
"moreLink": "",
"tools": {
"transcript": {
"title": "",
"body": ""
}
}
},
"transcript": {
"start": {
"heading": "",
"body": "",
"button": ""
"button": "",
"linkMore": ""
},
"stop": {
"heading": "",
"body": "",
"button": ""
},
"beta": {
"heading": "",
"body": "",
"button": ""
}
},
"admin": {
@@ -275,7 +293,11 @@
}
},
"recording": {
"label": ""
"transcript": {
"started": "",
"starting": "",
"stopping": ""
}
},
"participantTileFocus": {
"pin": {
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": ""
"createMeeting": {
"createButton": "",
"joinButton": "",
"copyLinkTooltip": "",
"resetLabel": "",
"participantLimit": "",
"popupBlocked": ""
}
}
@@ -21,5 +21,7 @@
"several": "Several people want to join this call.",
"open": "Open",
"accept": "Accept"
}
},
"recordingStarted": "{{name}} started the meeting transcription.",
"recordingStopped": "{{name}} stopped the meeting transcription."
}
+38 -16
View File
@@ -103,15 +103,14 @@
"open": "Hide everyone",
"closed": "See everyone"
},
"transcript": {
"open": "Hide AI assistant",
"closed": "Show AI assistant"
"tools": {
"open": "Hide more tools",
"closed": "Show more tools"
},
"admin": {
"open": "Hide admin",
"closed": "Open admin"
},
"support": "Support",
"moreOptions": "More options",
"reactions": {
"button": "Send reaction",
@@ -130,7 +129,8 @@
"fullscreen": {
"enter": "Fullscreen",
"exit": "Exit fullscreen mode"
}
},
"support": "Contact support"
}
},
"effects": {
@@ -167,31 +167,49 @@
"participants": "Participants",
"effects": "Effects",
"chat": "Messages in the chat",
"transcript": "AI Assistant",
"admin": "Admin settings"
"transcript": "Transcription",
"admin": "Admin settings",
"tools": "More tools"
},
"content": {
"participants": "participants",
"effects": "effects",
"chat": "messages",
"transcript": "AI assistant",
"admin": "Admin settings"
"transcript": "Transcription",
"admin": "admin settings",
"tools": "more tools"
},
"closeButton": "Hide {{content}}"
},
"chat": {
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"moreTools": {
"body": "Access more tools in Visio to enhance your meetings,",
"moreLink": "learn more",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcribe your meeting for later"
}
}
},
"transcript": {
"start": {
"heading": "Start the Assistant!",
"body": "The assistant automatically starts recording your meeting audio (limited to 1 hour). At the end, you'll receive a clear and concise summary of the discussion directly via email.",
"button": "Start"
"heading": "Transcribe this call",
"body": "Automatically transcribe this call and receive the summary in Docs.",
"button": "Start transcription",
"linkMore": "Learn more"
},
"stop": {
"heading": "Recording in Progress...",
"body": "Your meeting is currently being recorded. You will receive a summary via email once the meeting ends.",
"button": "Stop Recording"
"heading": "Transcription in progress...",
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
"button": "Stop transcription"
},
"beta": {
"heading": "Become a beta tester",
"body": "Record your meeting for later. You will receive a summary by email once the meeting is finished.",
"button": "Sign up"
}
},
"admin": {
@@ -274,7 +292,11 @@
}
},
"recording": {
"label": "Recording"
"transcript": {
"started": "Transcribing",
"starting": "Transcription starting",
"stopping": "Transcription stopping"
}
},
"participantTileFocus": {
"pin": {
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Create a Visio link"
"createMeeting": {
"createButton": "Create",
"joinButton": "Join with Visio",
"copyLinkTooltip": "Copy link",
"resetLabel": "Reset",
"participantLimit": "Up to 150 participants.",
"popupBlocked": "Popup was blocked. Please allow popups for this site."
}
}
@@ -21,5 +21,7 @@
"several": "Plusieurs personnes souhaitent participer à cet appel.",
"open": "Afficher",
"accept": "Accepter"
}
},
"recordingStarted": "{{name}} a démarré la transcription de la réunion.",
"recordingStopped": "{{name}} a arrêté la transcription de la réunion."
}
+38 -16
View File
@@ -103,15 +103,14 @@
"open": "Masquer les participants",
"closed": "Afficher les participants"
},
"transcript": {
"open": "Masquer l'assistant IA",
"closed": "Afficher l'assistant IA"
"tools": {
"open": "Masquer plus d'outils",
"closed": "Afficher plus d'outils"
},
"admin": {
"open": "Masquer l'admin",
"closed": "Afficher l'admin"
},
"support": "Support",
"moreOptions": "Plus d'options",
"reactions": {
"button": "Envoyer une réaction",
@@ -130,7 +129,8 @@
"fullscreen": {
"enter": "Plein écran",
"exit": "Quitter le mode plein écran"
}
},
"support": "Contacter l'aide"
}
},
"effects": {
@@ -167,31 +167,49 @@
"participants": "Participants",
"effects": "Effets",
"chat": "Messages dans l'appel",
"transcript": "Assistant IA",
"admin": "Commandes de l'organisateur"
"transcript": "Transcription",
"admin": "Commandes de l'organisateur",
"tools": "Plus d'outils"
},
"content": {
"participants": "les participants",
"effects": "les effets",
"chat": "les messages",
"transcript": "l'assistant IA",
"admin": "Commandes de l'organisateur"
"transcript": "transcription",
"admin": "commandes de l'organisateur",
"tools": "plus d'outils"
},
"closeButton": "Masquer {{content}}"
},
"chat": {
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"moreTools": {
"body": "Accèder à d'avantage d'outils dans Visio pour améliorer vos réunions,",
"moreLink": "en savoir plus",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcrivez votre réunion pour plus tard"
}
}
},
"transcript": {
"start": {
"heading": "Démarrer l'assistant !",
"body": "L'assistant démarre automatiquement l'enregistrement sonore de votre réunion (limité à 1h). À la fin, vous recevrez un résumé clair et concis des échanges directement par e-mail.",
"button": "Démarrer"
"heading": "Transcrire cet appel",
"body": "Transcrivez cet appel automatiquement et recevez le compte rendu dans Docs.",
"button": "Démarrer la transcription",
"linkMore": "En savoir plus"
},
"stop": {
"heading": "Enregistrement en cours …",
"body": "L'enregistrement de votre réunion est en cours. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Arrêter l'enregistrement"
"heading": "Transcription en cours …",
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
"button": "Arrêter la transcription"
},
"beta": {
"heading": "Devenez beta testeur",
"body": "Enregistrer votre réunion pour plus tard. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Inscrivez-vous"
}
},
"admin": {
@@ -274,7 +292,11 @@
}
},
"recording": {
"label": "Enregistrement"
"transcript": {
"started": "Transcription en cours",
"starting": "Démarrage de la transcription",
"stopping": "Arrêt de la transcription"
}
},
"participantTileFocus": {
"pin": {
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Créer un lien Visio"
"createMeeting": {
"createButton": "Créer un lien",
"joinButton": "Participer avec Visio",
"copyLinkTooltip": "Copier le lien",
"resetLabel": "Réinitialiser",
"participantLimit": "Jusqu'à 150 participants.",
"popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site."
}
}
@@ -21,5 +21,7 @@
"several": "Meerdere mensen willen deelnemen aan dit gesprek.",
"open": "Openen",
"accept": "Accepteren"
}
},
"recordingStarted": "{{name}} is de transcriptie van de vergadering gestart.",
"recordingStopped": "{{name}} heeft de transcriptie van de vergadering gestopt."
}
+38 -16
View File
@@ -103,15 +103,14 @@
"open": "Verberg iedereen",
"closed": "Toon iedereen"
},
"transcript": {
"open": "Verberg AI-assistent",
"closed": "Toon AI-assistant"
"tools": {
"open": "Meer tools verbergen",
"closed": "Meer tools weergeven"
},
"admin": {
"open": "Verberg beheerder",
"closed": "Toon beheerder"
},
"support": "Ondersteuning",
"moreOptions": "Meer opties",
"reactions": {
"button": "Stuur reactie",
@@ -130,7 +129,8 @@
"fullscreen": {
"enter": "Volledig scherm",
"exit": "Stop volledig scherm stand"
}
},
"support": "Contact ondersteuning"
}
},
"effects": {
@@ -167,31 +167,49 @@
"participants": "Deelnemers",
"effects": "Effecten",
"chat": "Berichten in de chat",
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
"transcript": "Transcriptie",
"admin": "Beheerdersbediening",
"tools": "Meer tools"
},
"content": {
"participants": "deelnemers",
"effects": "effecten",
"chat": "berichten",
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
"transcript": "transcriptie",
"admin": "beheerdersbediening",
"tools": "meer tools"
},
"closeButton": "Verberg {{content}}"
},
"chat": {
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek."
},
"moreTools": {
"body": "Toegang tot meer tools in Visio om je vergaderingen te verbeteren,",
"moreLink": "lees meer",
"tools": {
"transcript": {
"title": "Transcriptie",
"body": "Transcribeer je vergadering voor later"
}
}
},
"transcript": {
"start": {
"heading": "Start de assistent!",
"body": "De assistent begint automatisch de audio van uw vergadering op te nemen (beperkt tot 1 uur). Na afloop krijgt u direct een heldere en beknopte samenvatting van de discussies in uw e-mail.",
"button": "Start"
"heading": "Transcribeer dit gesprek",
"body": "Transcribeer dit gesprek automatisch en ontvang het verslag in Docs.",
"button": "Transcriptie starten",
"linkMore": "Meer informatie"
},
"stop": {
"heading": "Opname loopt ...",
"body": "Uw vergadering wordt momenteel opgenomen. U ontvangt een samenvatting via e-mail, zo gauw de vergarding gesloten wordt.",
"button": "Stop met opname"
"heading": "Transcriptie bezig...",
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
"button": "Transcriptie stoppen"
},
"beta": {
"heading": "Word betatester",
"body": "Neem uw vergadering op voor later. U ontvangt een samenvatting per e-mail zodra de vergadering is afgelopen.",
"button": "Aanmelden"
}
},
"admin": {
@@ -274,7 +292,11 @@
}
},
"recording": {
"label": "Opnemen"
"transcript": {
"started": "Transcriptie bezig",
"starting": "Transcriptie begint",
"stopping": "Transcriptie stopt"
}
},
"participantTileFocus": {
"pin": {
+7 -2
View File
@@ -1,5 +1,10 @@
{
"createButton": {
"label": "Maak een Visio link"
"createMeeting": {
"createButton": "Aanmaken",
"joinButton": "Deelnemen met Visio",
"copyLinkTooltip": "Link kopiëren",
"resetLabel": "Resetten",
"participantLimit": "Tot 150 deelnemers.",
"popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site."
}
}
@@ -9,6 +9,7 @@ type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
target?: string
}
export const LinkButton = ({
+11 -5
View File
@@ -1,7 +1,13 @@
import { ProgressBar } from 'react-aria-components'
import { css } from '@/styled-system/css'
export const Spinner = () => {
export const Spinner = ({
size = 56,
variant = 'light',
}: {
size?: number
variant?: 'light' | 'dark'
}) => {
const center = 14
const strokeWidth = 3
const r = 14 - strokeWidth
@@ -11,8 +17,8 @@ export const Spinner = () => {
{({ percentage }) => (
<>
<svg
width={56}
height={56}
width={size}
height={size}
viewBox="0 0 28 28"
fill="none"
strokeWidth={strokeWidth}
@@ -25,7 +31,7 @@ export const Spinner = () => {
strokeDashoffset={0}
strokeLinecap="round"
className={css({
stroke: 'primary.100',
stroke: variant == 'light' ? 'primary.100' : 'primaryDark.100',
})}
style={{}}
/>
@@ -37,7 +43,7 @@ export const Spinner = () => {
strokeDashoffset={percentage && c - (percentage / 100) * c}
strokeLinecap="round"
className={css({
stroke: 'primary.800',
stroke: variant == 'light' ? 'primary.800' : 'white',
})}
style={{
animation: `rotate 1s ease-in-out infinite`,
+4
View File
@@ -53,6 +53,10 @@ export const text = cva({
note: {
color: 'default.subtle-text',
},
smNote: {
color: 'default.subtle-text',
textStyle: 'sm',
},
inherits: {},
},
centered: {
@@ -42,6 +42,7 @@ export const buttonRecipe = cva({
primary: {
backgroundColor: 'primary.800',
color: 'white',
fontWeight: 'medium !important',
'&[data-hovered]': {
backgroundColor: 'primary.action',
},
@@ -56,6 +57,7 @@ export const buttonRecipe = cva({
secondary: {
backgroundColor: 'white',
color: 'primary.800',
fontWeight: 'medium !important',
borderColor: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
@@ -113,6 +115,7 @@ export const buttonRecipe = cva({
},
tertiary: {
backgroundColor: 'primary.100',
fontWeight: 'medium !important',
color: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -120,9 +123,14 @@ export const buttonRecipe = cva({
'&[data-pressed]': {
backgroundColor: 'primary.300',
},
'&[data-disabled]': {
backgroundColor: 'transparent',
color: 'primary.400',
},
},
tertiaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'primary.900',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -133,6 +141,7 @@ export const buttonRecipe = cva({
},
primaryDark: {
backgroundColor: 'primaryDark.100',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.900',
@@ -149,6 +158,7 @@ export const buttonRecipe = cva({
},
secondaryDark: {
backgroundColor: 'primaryDark.50',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.200',
@@ -164,6 +174,7 @@ export const buttonRecipe = cva({
},
primaryTextDark: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'white',
'&[data-hovered]': {
backgroundColor: 'primaryDark.100',
@@ -179,6 +190,7 @@ export const buttonRecipe = cva({
},
quaternaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'greyscale.600',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
+15 -1
View File
@@ -3,6 +3,8 @@ import { HomeRoute } from '@/features/home'
import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute'
import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService'
import { CreatePopup } from '@/features/sdk/routes/CreatePopup'
import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton'
export const routes: Record<
| 'home'
@@ -10,7 +12,9 @@ export const routes: Record<
| 'feedback'
| 'legalTerms'
| 'accessibility'
| 'termsOfService',
| 'termsOfService'
| 'sdkCreatePopup'
| 'sdkCreateButton',
{
name: RouteName
path: RegExp | string
@@ -50,6 +54,16 @@ export const routes: Record<
path: '/conditions-utilisation',
Component: TermsOfServiceRoute,
},
sdkCreatePopup: {
name: 'sdkCreatePopup',
path: '/sdk/create-popup',
Component: CreatePopup,
},
sdkCreateButton: {
name: 'sdkCreateButton',
path: '/sdk/create-button',
Component: CreateMeetingButton,
},
}
export type RouteName = keyof typeof routes
+6 -1
View File
@@ -1,14 +1,19 @@
import { proxy } from 'valtio'
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
import {
PanelId,
SubPanelId,
} from '@/features/rooms/livekit/hooks/useSidePanel'
type State = {
showHeader: boolean
showFooter: boolean
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
}
export const layoutStore = proxy<State>({
showHeader: false,
showFooter: false,
activePanelId: null,
activeSubPanelId: null,
})
+16
View File
@@ -0,0 +1,16 @@
import { proxy } from 'valtio'
export enum TranscriptionStatus {
STARTING,
STARTED,
STOPPING,
STOPPED,
}
type State = {
status: TranscriptionStatus
}
export const transcriptionStore = proxy<State>({
status: TranscriptionStatus.STOPPED,
})
+11 -2
View File
@@ -1,2 +1,11 @@
export const GRIST_FORM =
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4' as const
export const GRIST_FEEDBACKS_FORM =
'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26' as const
export const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17' as const
export const CRISP_HELP_ARTICLE_MORE_TOOLS =
'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23' as const
export const CRISP_HELP_ARTICLE_TRANSCRIPT =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x' as const
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.17",
"version": "0.1.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.17",
"version": "0.1.18",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.17",
"version": "0.1.18",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+1 -1
View File
@@ -24,6 +24,6 @@
"globals": "15.14.0",
"typescript": "5.7.3",
"typescript-eslint": "8.18.2",
"vite": "6.0.11"
"vite": "6.2.5"
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ function App() {
</div>
<div className="group">
<label>Visioconference</label>
<VisioCreateButton onRoomCreated={setRoomUrl} />
<VisioCreateButton
onRoomCreated={(data) => setRoomUrl(data.url)}
onClear={() => setRoomUrl("")}
/>
</div>
<div className="group">
<label>Description</label>
+53
View File
@@ -72,3 +72,56 @@ button {
font-weight: 300;
cursor: pointer;
}
.create-meeting {
background-color: rgb(45, 45, 70);
color: white;
height: 46px;
border-radius: 4px;
font-size: 16px;
font-weight: 300;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 0.75rem;
border: none;
}
.create-meeting svg path {
fill: white !important;
}
.icon {
color: white !important;
}
.join-button {
background-color: rgb(45, 45, 70);
color: white;
height: 46px;
border-radius: 4px;
font-size: 16px;
font-weight: 300;
cursor: pointer;
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
width: fit-content;
padding: 0 1rem;
text-decoration: none;
}
.join-button svg path {
fill: white !important;
}
.join-link {
padding-top: 0.5rem;
}
.join-link svg path {
fill: white !important;
}
+1
View File
@@ -0,0 +1 @@
VITE_VISIO_SDK_URL=https://meet.127.0.0.1.nip.io/sdk
+1 -1
View File
@@ -46,7 +46,7 @@
"sass": "1.83.4",
"prettier": "3.4.2",
"typescript": "5.4.5",
"vite": "5.4.12",
"vite": "5.4.16",
"vite-plugin-dts": "3.9.1"
}
}
+8
View File
@@ -4,4 +4,12 @@ export type ConfigType = typeof DEFAULT_CONFIG
export enum ClientMessageType {
ROOM_CREATED = 'ROOM_CREATED',
STATE_CLEAR = 'STATE_CLEAR',
}
export type RoomData = {
slug: string
url: string
phone?: string
code?: string
}
@@ -1,11 +1,17 @@
import { DEFAULT_CONFIG } from '@/Config'
import { ClientMessageType } from '@/Types'
import { useEffect } from 'react'
import { ClientMessageType, RoomData } from '@/Types'
import { DEFAULT_CONFIG } from '@/Config'
export const VisioCreateButton = ({
onRoomCreated,
onClear,
readOnly = false,
slug,
}: {
onRoomCreated: (roomUrl: string) => void
onRoomCreated: (roomData: RoomData) => void
onClear: () => void
readOnly?: boolean
slug?: string
}) => {
useEffect(() => {
const onMessage = (event: MessageEvent) => {
@@ -13,27 +19,30 @@ export const VisioCreateButton = ({
if (event.origin !== new URL(DEFAULT_CONFIG.url).origin) {
return
}
if (event.data.type === ClientMessageType.ROOM_CREATED) {
const data = event.data.data
const roomUrl = data.url
onRoomCreated(roomUrl)
const { type, data } = event.data
if (type == ClientMessageType.ROOM_CREATED && data?.room) {
onRoomCreated(data.room)
} else if (type == ClientMessageType.STATE_CLEAR) {
onClear()
}
}
window.addEventListener('message', onMessage)
return () => {
window.removeEventListener('message', onMessage)
}
}, [onRoomCreated])
}, [onRoomCreated, onClear])
return (
// eslint-disable-next-line jsx-a11y/iframe-has-title
<iframe
allow="clipboard-read; clipboard-write"
src={DEFAULT_CONFIG.url + '/create-button'}
src={
DEFAULT_CONFIG.url +
`/create-button?readOnly=${readOnly}${slug ? '&slug=' + slug : ''}`
}
style={{
width: '100%',
height: '52px',
height: '100px',
border: 'none',
}}
></iframe>
+116 -116
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.17",
"version": "0.1.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.17",
"version": "0.1.18",
"license": "ISC",
"workspaces": [
"./library",
@@ -30,13 +30,13 @@
"globals": "15.14.0",
"typescript": "5.7.3",
"typescript-eslint": "8.18.2",
"vite": "6.0.11"
"vite": "6.2.5"
}
},
"consumer/node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
@@ -51,9 +51,9 @@
}
},
"consumer/node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
@@ -68,9 +68,9 @@
}
},
"consumer/node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
@@ -85,9 +85,9 @@
}
},
"consumer/node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
@@ -102,9 +102,9 @@
}
},
"consumer/node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
@@ -119,9 +119,9 @@
}
},
"consumer/node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
@@ -136,9 +136,9 @@
}
},
"consumer/node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
@@ -153,9 +153,9 @@
}
},
"consumer/node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
@@ -170,9 +170,9 @@
}
},
"consumer/node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
@@ -187,9 +187,9 @@
}
},
"consumer/node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
@@ -204,9 +204,9 @@
}
},
"consumer/node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
@@ -221,9 +221,9 @@
}
},
"consumer/node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
@@ -238,9 +238,9 @@
}
},
"consumer/node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
@@ -255,9 +255,9 @@
}
},
"consumer/node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
@@ -272,9 +272,9 @@
}
},
"consumer/node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
@@ -289,9 +289,9 @@
}
},
"consumer/node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
@@ -306,9 +306,9 @@
}
},
"consumer/node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
@@ -323,9 +323,9 @@
}
},
"consumer/node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
@@ -340,9 +340,9 @@
}
},
"consumer/node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
@@ -357,9 +357,9 @@
}
},
"consumer/node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
@@ -374,9 +374,9 @@
}
},
"consumer/node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
@@ -391,9 +391,9 @@
}
},
"consumer/node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
@@ -408,9 +408,9 @@
}
},
"consumer/node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
@@ -456,9 +456,9 @@
}
},
"consumer/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -469,31 +469,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"consumer/node_modules/eslint-plugin-react-hooks": {
@@ -547,15 +547,15 @@
}
},
"consumer/node_modules/vite": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.24.2",
"postcss": "^8.4.49",
"rollup": "^4.23.0"
"esbuild": "^0.25.0",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
},
"bin": {
"vite": "bin/vite.js"
@@ -641,7 +641,7 @@
"prettier": "3.4.2",
"sass": "1.83.4",
"typescript": "5.4.5",
"vite": "5.4.12",
"vite": "5.4.16",
"vite-plugin-dts": "3.9.1"
}
},
@@ -1285,9 +1285,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
@@ -1320,9 +1320,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
@@ -7296,9 +7296,9 @@
}
},
"node_modules/vite": {
"version": "5.4.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
"version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.17",
"version": "0.1.18",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.17"
version = "0.1.18"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+1 -1
View File
@@ -79,7 +79,7 @@ def format_segments(transcription_data):
text = segment.get('text', '')
if text:
if speaker != previous_speaker:
formatted_output += f"\n\n *{speaker}*: {text}"
formatted_output += f"\n\n **{speaker}**: {text}"
else:
formatted_output += f" {text}"
previous_speaker = speaker