mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 04:09:26 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d537a4449a | |||
| 50c9304afe | |||
| 37fe23c0f7 | |||
| 255da4bf60 | |||
| 6dccb507d2 | |||
| d202a025e7 | |||
| ac9ba0df62 | |||
| 91562d049c | |||
| 60321296e5 | |||
| ae06873ff5 | |||
| 70ffb758c7 | |||
| 33a94da636 | |||
| aa6757601d | |||
| c26b1b711c | |||
| 3ee33fc2ec | |||
| d43b2857c1 | |||
| 3b1b644ef9 | |||
| ab7a0e1ec2 | |||
| 86a4d68daa | |||
| de3df889ba | |||
| 7c9fc15359 | |||
| a6a67f2752 | |||
| 00eb52cd3f | |||
| 6324be9fd3 | |||
| 66f307b7e8 | |||
| 506b3978e1 | |||
| db65aef56e | |||
| 4e1a4be650 |
@@ -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).
|
||||
|
||||
We’re continuously adding new features to enhance your experience, with the latest updates coming soon!
|
||||
|
||||
@@ -61,31 +61,31 @@ We’re 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
|
||||
|
||||
We’re 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.
|
||||
We’re 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. We’re committed to making Visio as accessible and easy to use as proprietary solutions, ensuring it meets the highest standards.
|
||||
Our users come first. We’re 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 |
@@ -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")
|
||||
|
||||
@@ -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"}}
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+6
-6
@@ -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": {
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
-2
@@ -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}
|
||||
>
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
+27
@@ -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>
|
||||
)
|
||||
}
|
||||
+9
-14
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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,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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VITE_VISIO_SDK_URL=https://meet.127.0.0.1.nip.io/sdk
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+116
-116
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user