mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 12:49:06 +00:00
Merge branch 'main' into mathieu/calendar-integration-rebased2
This commit is contained in:
@@ -2,7 +2,6 @@ from datetime import datetime
|
|||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from fastapi import HTTPException
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
@@ -233,23 +232,6 @@ class MeetingController:
|
|||||||
return None
|
return None
|
||||||
return Meeting(**result)
|
return Meeting(**result)
|
||||||
|
|
||||||
async def get_by_id_for_http(self, meeting_id: str, user_id: str | None) -> Meeting:
|
|
||||||
"""
|
|
||||||
Get a meeting by ID for HTTP request.
|
|
||||||
|
|
||||||
If not found, it will raise a 404 error.
|
|
||||||
"""
|
|
||||||
query = meetings.select().where(meetings.c.id == meeting_id)
|
|
||||||
result = await get_database().fetch_one(query)
|
|
||||||
if not result:
|
|
||||||
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
||||||
|
|
||||||
meeting = Meeting(**result)
|
|
||||||
if result["user_id"] != user_id:
|
|
||||||
meeting.host_room_url = ""
|
|
||||||
|
|
||||||
return meeting
|
|
||||||
|
|
||||||
async def get_by_calendar_event(self, calendar_event_id: str) -> Meeting | None:
|
async def get_by_calendar_event(self, calendar_event_id: str) -> Meeting | None:
|
||||||
query = meetings.select().where(
|
query = meetings.select().where(
|
||||||
meetings.c.calendar_event_id == calendar_event_id
|
meetings.c.calendar_event_id == calendar_event_id
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from pydantic import (
|
|||||||
|
|
||||||
from reflector.db import get_database
|
from reflector.db import get_database
|
||||||
from reflector.db.rooms import rooms
|
from reflector.db.rooms import rooms
|
||||||
from reflector.db.transcripts import SourceKind, transcripts
|
from reflector.db.transcripts import SourceKind, TranscriptStatus, transcripts
|
||||||
from reflector.db.utils import is_postgresql
|
from reflector.db.utils import is_postgresql
|
||||||
from reflector.logger import logger
|
from reflector.logger import logger
|
||||||
from reflector.utils.string import NonEmptyString, try_parse_non_empty_string
|
from reflector.utils.string import NonEmptyString, try_parse_non_empty_string
|
||||||
@@ -161,7 +161,7 @@ class SearchResult(BaseModel):
|
|||||||
room_name: str | None = None
|
room_name: str | None = None
|
||||||
source_kind: SourceKind
|
source_kind: SourceKind
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
status: str = Field(..., min_length=1)
|
status: TranscriptStatus = Field(..., min_length=1)
|
||||||
rank: float = Field(..., ge=0, le=1)
|
rank: float = Field(..., ge=0, le=1)
|
||||||
duration: NonNegativeFloat | None = Field(..., description="Duration in seconds")
|
duration: NonNegativeFloat | None = Field(..., description="Duration in seconds")
|
||||||
search_snippets: list[str] = Field(
|
search_snippets: list[str] = Field(
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class FileDiarizationModalProcessor(FileDiarizationProcessor):
|
|||||||
"audio_file_url": data.audio_url,
|
"audio_file_url": data.audio_url,
|
||||||
"timestamp": 0,
|
"timestamp": 0,
|
||||||
},
|
},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
diarization_data = response.json()["diarization"]
|
diarization_data = response.json()["diarization"]
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class FileTranscriptModalProcessor(FileTranscriptProcessor):
|
|||||||
"language": data.language,
|
"language": data.language,
|
||||||
"batch": True,
|
"batch": True,
|
||||||
},
|
},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
|||||||
@@ -244,14 +244,10 @@ async def rooms_create_meeting(
|
|||||||
except (asyncpg.exceptions.UniqueViolationError, sqlite3.IntegrityError):
|
except (asyncpg.exceptions.UniqueViolationError, sqlite3.IntegrityError):
|
||||||
# Another request already created a meeting for this room
|
# Another request already created a meeting for this room
|
||||||
# Log this race condition occurrence
|
# Log this race condition occurrence
|
||||||
logger.info(
|
|
||||||
"Race condition detected for room %s - fetching existing meeting",
|
|
||||||
room.name,
|
|
||||||
)
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Whereby meeting %s was created but not used (resource leak) for room %s",
|
"Race condition detected for room %s and meeting %s - fetching existing meeting",
|
||||||
whereby_meeting["meetingId"],
|
|
||||||
room.name,
|
room.name,
|
||||||
|
whereby_meeting["meetingId"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch the meeting that was created by the other request
|
# Fetch the meeting that was created by the other request
|
||||||
@@ -261,7 +257,9 @@ async def rooms_create_meeting(
|
|||||||
if meeting is None:
|
if meeting is None:
|
||||||
# Edge case: meeting was created but expired/deleted between checks
|
# Edge case: meeting was created but expired/deleted between checks
|
||||||
logger.error(
|
logger.error(
|
||||||
"Meeting disappeared after race condition for room %s", room.name
|
"Meeting disappeared after race condition for room %s",
|
||||||
|
room.name,
|
||||||
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503, detail="Unable to join meeting - please try again"
|
status_code=503, detail="Unable to join meeting - please try again"
|
||||||
|
|||||||
@@ -350,8 +350,6 @@ async def transcript_update(
|
|||||||
transcript = await transcripts_controller.get_by_id_for_http(
|
transcript = await transcripts_controller.get_by_id_for_http(
|
||||||
transcript_id, user_id=user_id
|
transcript_id, user_id=user_id
|
||||||
)
|
)
|
||||||
if not transcript:
|
|
||||||
raise HTTPException(status_code=404, detail="Transcript not found")
|
|
||||||
values = info.dict(exclude_unset=True)
|
values = info.dict(exclude_unset=True)
|
||||||
updated_transcript = await transcripts_controller.update(transcript, values)
|
updated_transcript = await transcripts_controller.update(transcript, values)
|
||||||
return updated_transcript
|
return updated_transcript
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ else:
|
|||||||
"task": "reflector.worker.ics_sync.sync_all_ics_calendars",
|
"task": "reflector.worker.ics_sync.sync_all_ics_calendars",
|
||||||
"schedule": 60.0, # Run every minute to check which rooms need sync
|
"schedule": 60.0, # Run every minute to check which rooms need sync
|
||||||
},
|
},
|
||||||
"pre_create_upcoming_meetings": {
|
"create_upcoming_meetings": {
|
||||||
"task": "reflector.worker.ics_sync.pre_create_upcoming_meetings",
|
"task": "reflector.worker.ics_sync.create_upcoming_meetings",
|
||||||
"schedule": 30.0, # Run every 30 seconds to pre-create meetings
|
"schedule": 30.0, # Run every 30 seconds to create upcoming meetings
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,9 @@ async def create_upcoming_meetings():
|
|||||||
)
|
)
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
await create_upcoming_meetings_for_event(event)
|
await create_upcoming_meetings_for_event(
|
||||||
|
event, create_window, room_id, room
|
||||||
|
)
|
||||||
logger.info("Completed pre-creation check for upcoming meetings")
|
logger.info("Completed pre-creation check for upcoming meetings")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from reflector.db.calendar_events import calendar_events_controller
|
|||||||
from reflector.db.rooms import rooms_controller
|
from reflector.db.rooms import rooms_controller
|
||||||
from reflector.worker.ics_sync import (
|
from reflector.worker.ics_sync import (
|
||||||
_should_sync,
|
_should_sync,
|
||||||
_sync_all_ics_calendars_async,
|
sync_all_ics_calendars,
|
||||||
_sync_room_ics_async,
|
sync_room_ics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ async def test_sync_room_ics_task():
|
|||||||
) as mock_fetch:
|
) as mock_fetch:
|
||||||
mock_fetch.return_value = ics_content
|
mock_fetch.return_value = ics_content
|
||||||
|
|
||||||
await _sync_room_ics_async(room.id)
|
await sync_room_ics(room.id)
|
||||||
|
|
||||||
events = await calendar_events_controller.get_by_room(room.id)
|
events = await calendar_events_controller.get_by_room(room.id)
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
@@ -124,7 +124,7 @@ async def test_sync_all_ics_calendars():
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
||||||
await _sync_all_ics_calendars_async()
|
await sync_all_ics_calendars()
|
||||||
|
|
||||||
assert mock_delay.call_count == 2
|
assert mock_delay.call_count == 2
|
||||||
called_room_ids = [call.args[0] for call in mock_delay.call_args_list]
|
called_room_ids = [call.args[0] for call in mock_delay.call_args_list]
|
||||||
@@ -196,7 +196,7 @@ async def test_sync_respects_fetch_interval():
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
||||||
await _sync_all_ics_calendars_async()
|
await sync_all_ics_calendars()
|
||||||
|
|
||||||
assert mock_delay.call_count == 1
|
assert mock_delay.call_count == 1
|
||||||
assert mock_delay.call_args[0][0] == room2.id
|
assert mock_delay.call_args[0][0] == room2.id
|
||||||
@@ -224,7 +224,7 @@ async def test_sync_handles_errors_gracefully():
|
|||||||
) as mock_fetch:
|
) as mock_fetch:
|
||||||
mock_fetch.side_effect = Exception("Network error")
|
mock_fetch.side_effect = Exception("Network error")
|
||||||
|
|
||||||
await _sync_room_ics_async(room.id)
|
await sync_room_ics(room.id)
|
||||||
|
|
||||||
events = await calendar_events_controller.get_by_room(room.id)
|
events = await calendar_events_controller.get_by_room(room.id)
|
||||||
assert len(events) == 0
|
assert len(events) == 0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ async def test_empty_transcript_title_only_match():
|
|||||||
"id": test_id,
|
"id": test_id,
|
||||||
"name": "Empty Transcript",
|
"name": "Empty Transcript",
|
||||||
"title": "Empty Meeting",
|
"title": "Empty Meeting",
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"duration": 0.0,
|
"duration": 0.0,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
@@ -109,7 +109,7 @@ async def test_search_with_long_summary():
|
|||||||
"id": test_id,
|
"id": test_id,
|
||||||
"name": "Test Long Summary",
|
"name": "Test Long Summary",
|
||||||
"title": "Regular Meeting",
|
"title": "Regular Meeting",
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"duration": 1800.0,
|
"duration": 1800.0,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
@@ -165,7 +165,7 @@ async def test_postgresql_search_with_data():
|
|||||||
"id": test_id,
|
"id": test_id,
|
||||||
"name": "Test Search Transcript",
|
"name": "Test Search Transcript",
|
||||||
"title": "Engineering Planning Meeting Q4 2024",
|
"title": "Engineering Planning Meeting Q4 2024",
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"duration": 1800.0,
|
"duration": 1800.0,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
@@ -221,7 +221,7 @@ We need to implement PostgreSQL tsvector for better performance.""",
|
|||||||
test_result = next((r for r in results if r.id == test_id), None)
|
test_result = next((r for r in results if r.id == test_id), None)
|
||||||
if test_result:
|
if test_result:
|
||||||
assert test_result.title == "Engineering Planning Meeting Q4 2024"
|
assert test_result.title == "Engineering Planning Meeting Q4 2024"
|
||||||
assert test_result.status == "completed"
|
assert test_result.status == "ended"
|
||||||
assert test_result.duration == 1800.0
|
assert test_result.duration == 1800.0
|
||||||
assert 0 <= test_result.rank <= 1, "Rank should be normalized to 0-1"
|
assert 0 <= test_result.rank <= 1, "Rank should be normalized to 0-1"
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ def mock_db_result():
|
|||||||
"title": "Test Transcript",
|
"title": "Test Transcript",
|
||||||
"created_at": datetime(2024, 6, 15, tzinfo=timezone.utc),
|
"created_at": datetime(2024, 6, 15, tzinfo=timezone.utc),
|
||||||
"duration": 3600.0,
|
"duration": 3600.0,
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"user_id": "test-user",
|
"user_id": "test-user",
|
||||||
"room_id": "room1",
|
"room_id": "room1",
|
||||||
"source_kind": SourceKind.LIVE,
|
"source_kind": SourceKind.LIVE,
|
||||||
@@ -433,7 +433,7 @@ class TestSearchResultModel:
|
|||||||
room_id="room-456",
|
room_id="room-456",
|
||||||
source_kind=SourceKind.ROOM,
|
source_kind=SourceKind.ROOM,
|
||||||
created_at=datetime(2024, 6, 15, tzinfo=timezone.utc),
|
created_at=datetime(2024, 6, 15, tzinfo=timezone.utc),
|
||||||
status="completed",
|
status="ended",
|
||||||
rank=0.85,
|
rank=0.85,
|
||||||
duration=1800.5,
|
duration=1800.5,
|
||||||
search_snippets=["snippet 1", "snippet 2"],
|
search_snippets=["snippet 1", "snippet 2"],
|
||||||
@@ -443,7 +443,7 @@ class TestSearchResultModel:
|
|||||||
assert result.title == "Test Title"
|
assert result.title == "Test Title"
|
||||||
assert result.user_id == "user-123"
|
assert result.user_id == "user-123"
|
||||||
assert result.room_id == "room-456"
|
assert result.room_id == "room-456"
|
||||||
assert result.status == "completed"
|
assert result.status == "ended"
|
||||||
assert result.rank == 0.85
|
assert result.rank == 0.85
|
||||||
assert result.duration == 1800.5
|
assert result.duration == 1800.5
|
||||||
assert len(result.search_snippets) == 2
|
assert len(result.search_snippets) == 2
|
||||||
@@ -474,7 +474,7 @@ class TestSearchResultModel:
|
|||||||
id="test-id",
|
id="test-id",
|
||||||
source_kind=SourceKind.LIVE,
|
source_kind=SourceKind.LIVE,
|
||||||
created_at=datetime(2024, 6, 15, 12, 30, 45, tzinfo=timezone.utc),
|
created_at=datetime(2024, 6, 15, 12, 30, 45, tzinfo=timezone.utc),
|
||||||
status="completed",
|
status="ended",
|
||||||
rank=0.9,
|
rank=0.9,
|
||||||
duration=None,
|
duration=None,
|
||||||
search_snippets=[],
|
search_snippets=[],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async def test_long_summary_snippet_prioritization():
|
|||||||
"id": test_id,
|
"id": test_id,
|
||||||
"name": "Test Snippet Priority",
|
"name": "Test Snippet Priority",
|
||||||
"title": "Meeting About Projects",
|
"title": "Meeting About Projects",
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"duration": 1800.0,
|
"duration": 1800.0,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
@@ -106,7 +106,7 @@ async def test_long_summary_only_search():
|
|||||||
"id": test_id,
|
"id": test_id,
|
||||||
"name": "Test Long Only",
|
"name": "Test Long Only",
|
||||||
"title": "Standard Meeting",
|
"title": "Standard Meeting",
|
||||||
"status": "completed",
|
"status": "ended",
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"duration": 1800.0,
|
"duration": 1800.0,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
FaMicrophone,
|
FaMicrophone,
|
||||||
FaGear,
|
FaGear,
|
||||||
} from "react-icons/fa6";
|
} from "react-icons/fa6";
|
||||||
|
import { TranscriptStatus } from "../../../lib/transcript";
|
||||||
|
|
||||||
interface TranscriptStatusIconProps {
|
interface TranscriptStatusIconProps {
|
||||||
status: string;
|
status: TranscriptStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TranscriptStatusIcon({
|
export default function TranscriptStatusIcon({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useParticipants from "../../useParticipants";
|
|||||||
import { Box, Flex, Text, Accordion } from "@chakra-ui/react";
|
import { Box, Flex, Text, Accordion } from "@chakra-ui/react";
|
||||||
import { featureEnabled } from "../../../../domainContext";
|
import { featureEnabled } from "../../../../domainContext";
|
||||||
import { TopicItem } from "./TopicItem";
|
import { TopicItem } from "./TopicItem";
|
||||||
|
import { TranscriptStatus } from "../../../../lib/transcript";
|
||||||
|
|
||||||
type TopicListProps = {
|
type TopicListProps = {
|
||||||
topics: Topic[];
|
topics: Topic[];
|
||||||
@@ -14,7 +15,7 @@ type TopicListProps = {
|
|||||||
];
|
];
|
||||||
autoscroll: boolean;
|
autoscroll: boolean;
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
status: string;
|
status: TranscriptStatus | null;
|
||||||
currentTranscriptText: any;
|
currentTranscriptText: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import ParticipantList from "./participantList";
|
|||||||
import type { components } from "../../../../reflector-api";
|
import type { components } from "../../../../reflector-api";
|
||||||
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
|
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
|
||||||
import { SelectedText, selectedTextIsTimeSlice } from "./types";
|
import { SelectedText, selectedTextIsTimeSlice } from "./types";
|
||||||
import { useTranscriptUpdate } from "../../../../lib/apiHooks";
|
import {
|
||||||
import useTranscript from "../../useTranscript";
|
useTranscriptGet,
|
||||||
|
useTranscriptUpdate,
|
||||||
|
} from "../../../../lib/apiHooks";
|
||||||
import { useError } from "../../../../(errors)/errorContext";
|
import { useError } from "../../../../(errors)/errorContext";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Box, Grid } from "@chakra-ui/react";
|
import { Box, Grid } from "@chakra-ui/react";
|
||||||
@@ -25,7 +27,7 @@ export default function TranscriptCorrect({
|
|||||||
params: { transcriptId },
|
params: { transcriptId },
|
||||||
}: TranscriptCorrect) {
|
}: TranscriptCorrect) {
|
||||||
const updateTranscriptMutation = useTranscriptUpdate();
|
const updateTranscriptMutation = useTranscriptUpdate();
|
||||||
const transcript = useTranscript(transcriptId);
|
const transcript = useTranscriptGet(transcriptId);
|
||||||
const stateCurrentTopic = useState<GetTranscriptTopic>();
|
const stateCurrentTopic = useState<GetTranscriptTopic>();
|
||||||
const [currentTopic, _sct] = stateCurrentTopic;
|
const [currentTopic, _sct] = stateCurrentTopic;
|
||||||
const stateSelectedText = useState<SelectedText>();
|
const stateSelectedText = useState<SelectedText>();
|
||||||
@@ -36,7 +38,7 @@ export default function TranscriptCorrect({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const markAsDone = async () => {
|
const markAsDone = async () => {
|
||||||
if (transcript.response && !transcript.response.reviewed) {
|
if (transcript.data && !transcript.data.reviewed) {
|
||||||
try {
|
try {
|
||||||
await updateTranscriptMutation.mutateAsync({
|
await updateTranscriptMutation.mutateAsync({
|
||||||
params: {
|
params: {
|
||||||
@@ -114,7 +116,7 @@ export default function TranscriptCorrect({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{transcript.response && !transcript.response?.reviewed && (
|
{transcript.data && !transcript.data?.reviewed && (
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
<button
|
<button
|
||||||
className="p-2 px-4 rounded bg-green-400"
|
className="p-2 px-4 rounded bg-green-400"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Modal from "../modal";
|
import Modal from "../modal";
|
||||||
import useTranscript from "../useTranscript";
|
|
||||||
import useTopics from "../useTopics";
|
import useTopics from "../useTopics";
|
||||||
import useWaveform from "../useWaveform";
|
import useWaveform from "../useWaveform";
|
||||||
import useMp3 from "../useMp3";
|
import useMp3 from "../useMp3";
|
||||||
@@ -12,6 +11,8 @@ import TranscriptTitle from "../transcriptTitle";
|
|||||||
import Player from "../player";
|
import Player from "../player";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Box, Flex, Grid, GridItem, Skeleton, Text } from "@chakra-ui/react";
|
import { Box, Flex, Grid, GridItem, Skeleton, Text } from "@chakra-ui/react";
|
||||||
|
import { useTranscriptGet } from "../../../lib/apiHooks";
|
||||||
|
import { TranscriptStatus } from "../../../lib/transcript";
|
||||||
|
|
||||||
type TranscriptDetails = {
|
type TranscriptDetails = {
|
||||||
params: {
|
params: {
|
||||||
@@ -22,11 +23,15 @@ type TranscriptDetails = {
|
|||||||
export default function TranscriptDetails(details: TranscriptDetails) {
|
export default function TranscriptDetails(details: TranscriptDetails) {
|
||||||
const transcriptId = details.params.transcriptId;
|
const transcriptId = details.params.transcriptId;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const statusToRedirect = ["idle", "recording", "processing"];
|
const statusToRedirect = [
|
||||||
|
"idle",
|
||||||
|
"recording",
|
||||||
|
"processing",
|
||||||
|
] satisfies TranscriptStatus[] as TranscriptStatus[];
|
||||||
|
|
||||||
const transcript = useTranscript(transcriptId);
|
const transcript = useTranscriptGet(transcriptId);
|
||||||
const transcriptStatus = transcript.response?.status;
|
const waiting =
|
||||||
const waiting = statusToRedirect.includes(transcriptStatus || "");
|
transcript.data && statusToRedirect.includes(transcript.data.status);
|
||||||
|
|
||||||
const mp3 = useMp3(transcriptId, waiting);
|
const mp3 = useMp3(transcriptId, waiting);
|
||||||
const topics = useTopics(transcriptId);
|
const topics = useTopics(transcriptId);
|
||||||
@@ -56,7 +61,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transcript?.loading || topics?.loading) {
|
if (transcript?.isLoading || topics?.loading) {
|
||||||
return <Modal title="Loading" text={"Loading transcript..."} />;
|
return <Modal title="Loading" text={"Loading transcript..."} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +91,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
|||||||
useActiveTopic={useActiveTopic}
|
useActiveTopic={useActiveTopic}
|
||||||
waveform={waveform.waveform}
|
waveform={waveform.waveform}
|
||||||
media={mp3.media}
|
media={mp3.media}
|
||||||
mediaDuration={transcript.response?.duration || null}
|
mediaDuration={transcript.data?.duration || null}
|
||||||
/>
|
/>
|
||||||
) : !mp3.loading && (waveform.error || mp3.error) ? (
|
) : !mp3.loading && (waveform.error || mp3.error) ? (
|
||||||
<Box p={4} bg="red.100" borderRadius="md">
|
<Box p={4} bg="red.100" borderRadius="md">
|
||||||
@@ -116,10 +121,10 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
|||||||
<Flex direction="column" gap={0}>
|
<Flex direction="column" gap={0}>
|
||||||
<Flex alignItems="center" gap={2}>
|
<Flex alignItems="center" gap={2}>
|
||||||
<TranscriptTitle
|
<TranscriptTitle
|
||||||
title={transcript.response?.title || "Unnamed Transcript"}
|
title={transcript.data?.title || "Unnamed Transcript"}
|
||||||
transcriptId={transcriptId}
|
transcriptId={transcriptId}
|
||||||
onUpdate={(newTitle) => {
|
onUpdate={(newTitle) => {
|
||||||
transcript.reload();
|
transcript.refetch().then(() => {});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -136,23 +141,23 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
|||||||
useActiveTopic={useActiveTopic}
|
useActiveTopic={useActiveTopic}
|
||||||
autoscroll={false}
|
autoscroll={false}
|
||||||
transcriptId={transcriptId}
|
transcriptId={transcriptId}
|
||||||
status={transcript.response?.status}
|
status={transcript.data?.status || null}
|
||||||
currentTranscriptText=""
|
currentTranscriptText=""
|
||||||
/>
|
/>
|
||||||
{transcript.response && topics.topics ? (
|
{transcript.data && topics.topics ? (
|
||||||
<>
|
<>
|
||||||
<FinalSummary
|
<FinalSummary
|
||||||
transcriptResponse={transcript.response}
|
transcriptResponse={transcript.data}
|
||||||
topicsResponse={topics.topics}
|
topicsResponse={topics.topics}
|
||||||
onUpdate={(newSummary) => {
|
onUpdate={() => {
|
||||||
transcript.reload();
|
transcript.refetch();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Flex justify={"center"} alignItems={"center"} h={"100%"}>
|
<Flex justify={"center"} alignItems={"center"} h={"100%"}>
|
||||||
<div className="flex flex-col h-full justify-center content-center">
|
<div className="flex flex-col h-full justify-center content-center">
|
||||||
{transcript.response.status == "processing" ? (
|
{transcript?.data?.status == "processing" ? (
|
||||||
<Text>Loading Transcript</Text>
|
<Text>Loading Transcript</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Recorder from "../../recorder";
|
import Recorder from "../../recorder";
|
||||||
import { TopicList } from "../_components/TopicList";
|
import { TopicList } from "../_components/TopicList";
|
||||||
import useTranscript from "../../useTranscript";
|
|
||||||
import { useWebSockets } from "../../useWebSockets";
|
import { useWebSockets } from "../../useWebSockets";
|
||||||
import { Topic } from "../../webSocketTypes";
|
import { Topic } from "../../webSocketTypes";
|
||||||
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
||||||
@@ -11,6 +10,8 @@ import useMp3 from "../../useMp3";
|
|||||||
import WaveformLoading from "../../waveformLoading";
|
import WaveformLoading from "../../waveformLoading";
|
||||||
import { Box, Text, Grid, Heading, VStack, Flex } from "@chakra-ui/react";
|
import { Box, Text, Grid, Heading, VStack, Flex } from "@chakra-ui/react";
|
||||||
import LiveTrancription from "../../liveTranscription";
|
import LiveTrancription from "../../liveTranscription";
|
||||||
|
import { useTranscriptGet } from "../../../../lib/apiHooks";
|
||||||
|
import { TranscriptStatus } from "../../../../lib/transcript";
|
||||||
|
|
||||||
type TranscriptDetails = {
|
type TranscriptDetails = {
|
||||||
params: {
|
params: {
|
||||||
@@ -19,7 +20,7 @@ type TranscriptDetails = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TranscriptRecord = (details: TranscriptDetails) => {
|
const TranscriptRecord = (details: TranscriptDetails) => {
|
||||||
const transcript = useTranscript(details.params.transcriptId);
|
const transcript = useTranscriptGet(details.params.transcriptId);
|
||||||
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
||||||
const useActiveTopic = useState<Topic | null>(null);
|
const useActiveTopic = useState<Topic | null>(null);
|
||||||
|
|
||||||
@@ -29,8 +30,8 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [status, setStatus] = useState(
|
const [status, setStatus] = useState<TranscriptStatus>(
|
||||||
webSockets.status.value || transcript.response?.status || "idle",
|
webSockets.status?.value || transcript.data?.status || "idle",
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,7 +42,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//TODO HANDLE ERROR STATUS BETTER
|
//TODO HANDLE ERROR STATUS BETTER
|
||||||
const newStatus =
|
const newStatus =
|
||||||
webSockets.status.value || transcript.response?.status || "idle";
|
webSockets.status?.value || transcript.data?.status || "idle";
|
||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
||||||
console.log(newStatus, "redirecting");
|
console.log(newStatus, "redirecting");
|
||||||
@@ -49,7 +50,7 @@ const TranscriptRecord = (details: TranscriptDetails) => {
|
|||||||
const newUrl = "/transcripts/" + details.params.transcriptId;
|
const newUrl = "/transcripts/" + details.params.transcriptId;
|
||||||
router.replace(newUrl);
|
router.replace(newUrl);
|
||||||
}
|
}
|
||||||
}, [webSockets.status.value, transcript.response?.status]);
|
}, [webSockets.status?.value, transcript.data?.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webSockets.waveform && webSockets.waveform) mp3.getNow();
|
if (webSockets.waveform && webSockets.waveform) mp3.getNow();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useTranscript from "../../useTranscript";
|
|
||||||
import { useWebSockets } from "../../useWebSockets";
|
import { useWebSockets } from "../../useWebSockets";
|
||||||
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import useMp3 from "../../useMp3";
|
import useMp3 from "../../useMp3";
|
||||||
import { Center, VStack, Text, Heading, Button } from "@chakra-ui/react";
|
import { Center, VStack, Text, Heading, Button } from "@chakra-ui/react";
|
||||||
import FileUploadButton from "../../fileUploadButton";
|
import FileUploadButton from "../../fileUploadButton";
|
||||||
|
import { useTranscriptGet } from "../../../../lib/apiHooks";
|
||||||
|
|
||||||
type TranscriptUpload = {
|
type TranscriptUpload = {
|
||||||
params: {
|
params: {
|
||||||
@@ -15,7 +15,7 @@ type TranscriptUpload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TranscriptUpload = (details: TranscriptUpload) => {
|
const TranscriptUpload = (details: TranscriptUpload) => {
|
||||||
const transcript = useTranscript(details.params.transcriptId);
|
const transcript = useTranscriptGet(details.params.transcriptId);
|
||||||
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
const [transcriptStarted, setTranscriptStarted] = useState(false);
|
||||||
|
|
||||||
const webSockets = useWebSockets(details.params.transcriptId);
|
const webSockets = useWebSockets(details.params.transcriptId);
|
||||||
@@ -25,13 +25,13 @@ const TranscriptUpload = (details: TranscriptUpload) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [status_, setStatus] = useState(
|
const [status_, setStatus] = useState(
|
||||||
webSockets.status.value || transcript.response?.status || "idle",
|
webSockets.status?.value || transcript.data?.status || "idle",
|
||||||
);
|
);
|
||||||
|
|
||||||
// status is obviously done if we have transcript
|
// status is obviously done if we have transcript
|
||||||
const status =
|
const status =
|
||||||
!transcript.loading && transcript.response?.status === "ended"
|
!transcript.isLoading && transcript.data?.status === "ended"
|
||||||
? transcript.response?.status
|
? transcript.data?.status
|
||||||
: status_;
|
: status_;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -43,9 +43,9 @@ const TranscriptUpload = (details: TranscriptUpload) => {
|
|||||||
//TODO HANDLE ERROR STATUS BETTER
|
//TODO HANDLE ERROR STATUS BETTER
|
||||||
// TODO deprecate webSockets.status.value / depend on transcript.response?.status from query lib
|
// TODO deprecate webSockets.status.value / depend on transcript.response?.status from query lib
|
||||||
const newStatus =
|
const newStatus =
|
||||||
transcript.response?.status === "ended"
|
transcript.data?.status === "ended"
|
||||||
? "ended"
|
? "ended"
|
||||||
: webSockets.status.value || transcript.response?.status || "idle";
|
: webSockets.status?.value || transcript.data?.status || "idle";
|
||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
if (newStatus && (newStatus == "ended" || newStatus == "error")) {
|
||||||
console.log(newStatus, "redirecting");
|
console.log(newStatus, "redirecting");
|
||||||
@@ -53,7 +53,7 @@ const TranscriptUpload = (details: TranscriptUpload) => {
|
|||||||
const newUrl = "/transcripts/" + details.params.transcriptId;
|
const newUrl = "/transcripts/" + details.params.transcriptId;
|
||||||
router.replace(newUrl);
|
router.replace(newUrl);
|
||||||
}
|
}
|
||||||
}, [webSockets.status.value, transcript.response?.status]);
|
}, [webSockets.status?.value, transcript.data?.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webSockets.waveform && webSockets.waveform) mp3.getNow();
|
if (webSockets.waveform && webSockets.waveform) mp3.getNow();
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import useAudioDevice from "./useAudioDevice";
|
|||||||
import { Box, Flex, IconButton, Menu, RadioGroup } from "@chakra-ui/react";
|
import { Box, Flex, IconButton, Menu, RadioGroup } from "@chakra-ui/react";
|
||||||
import { LuScreenShare, LuMic, LuPlay, LuCircleStop } from "react-icons/lu";
|
import { LuScreenShare, LuMic, LuPlay, LuCircleStop } from "react-icons/lu";
|
||||||
import { RECORD_A_MEETING_URL } from "../../api/urls";
|
import { RECORD_A_MEETING_URL } from "../../api/urls";
|
||||||
|
import { TranscriptStatus } from "../../lib/transcript";
|
||||||
|
|
||||||
type RecorderProps = {
|
type RecorderProps = {
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
status: string;
|
status: TranscriptStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Recorder(props: RecorderProps) {
|
export default function Recorder(props: RecorderProps) {
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import type { components } from "../../reflector-api";
|
|
||||||
import { useTranscriptGet } from "../../lib/apiHooks";
|
|
||||||
|
|
||||||
type GetTranscript = components["schemas"]["GetTranscript"];
|
|
||||||
|
|
||||||
type ErrorTranscript = {
|
|
||||||
error: Error;
|
|
||||||
loading: false;
|
|
||||||
response: null;
|
|
||||||
reload: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoadingTranscript = {
|
|
||||||
response: null;
|
|
||||||
loading: true;
|
|
||||||
error: false;
|
|
||||||
reload: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SuccessTranscript = {
|
|
||||||
response: GetTranscript;
|
|
||||||
loading: false;
|
|
||||||
error: null;
|
|
||||||
reload: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useTranscript = (
|
|
||||||
id: string | null,
|
|
||||||
): ErrorTranscript | LoadingTranscript | SuccessTranscript => {
|
|
||||||
const { data, isLoading, error, refetch } = useTranscriptGet(id);
|
|
||||||
|
|
||||||
// Map to the expected return format
|
|
||||||
if (isLoading) {
|
|
||||||
return {
|
|
||||||
response: null,
|
|
||||||
loading: true,
|
|
||||||
error: false,
|
|
||||||
reload: refetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
error: error as Error,
|
|
||||||
loading: false,
|
|
||||||
response: null,
|
|
||||||
reload: refetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if data is undefined or null
|
|
||||||
if (!data) {
|
|
||||||
return {
|
|
||||||
response: null,
|
|
||||||
loading: true,
|
|
||||||
error: false,
|
|
||||||
reload: refetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
response: data,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
reload: refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useTranscript;
|
|
||||||
@@ -16,7 +16,7 @@ export type UseWebSockets = {
|
|||||||
title: string;
|
title: string;
|
||||||
topics: Topic[];
|
topics: Topic[];
|
||||||
finalSummary: FinalSummary;
|
finalSummary: FinalSummary;
|
||||||
status: Status;
|
status: Status | null;
|
||||||
waveform: AudioWaveform | null;
|
waveform: AudioWaveform | null;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
};
|
};
|
||||||
@@ -34,7 +34,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
|||||||
const [finalSummary, setFinalSummary] = useState<FinalSummary>({
|
const [finalSummary, setFinalSummary] = useState<FinalSummary>({
|
||||||
summary: "",
|
summary: "",
|
||||||
});
|
});
|
||||||
const [status, setStatus] = useState<Status>({ value: "" });
|
const [status, setStatus] = useState<Status | null>(null);
|
||||||
const { setError } = useError();
|
const { setError } = useError();
|
||||||
|
|
||||||
const { websocket_url: websocketUrl } = useContext(DomainContext);
|
const { websocket_url: websocketUrl } = useContext(DomainContext);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { components } from "../../reflector-api";
|
import type { components } from "../../reflector-api";
|
||||||
|
import type { TranscriptStatus } from "../../lib/transcript";
|
||||||
|
|
||||||
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
|
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export type FinalSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Status = {
|
export type Status = {
|
||||||
value: string;
|
value: TranscriptStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TranslatedTopic = {
|
export type TranslatedTopic = {
|
||||||
|
|||||||
@@ -389,7 +389,6 @@ export default function MeetingSelection({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Box, Button, HStack, Icon, Spinner, Text, VStack } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useRoomGetByName, useRoomJoinMeeting, useMeetingAudioConsent } from "../../lib/apiHooks";
|
import {
|
||||||
|
useRoomGetByName,
|
||||||
|
useRoomJoinMeeting,
|
||||||
|
useMeetingAudioConsent,
|
||||||
|
} from "../../lib/apiHooks";
|
||||||
import { useRecordingConsent } from "../../recordingConsentContext";
|
import { useRecordingConsent } from "../../recordingConsentContext";
|
||||||
import { toaster } from "../../components/ui/toaster";
|
import { toaster } from "../../components/ui/toaster";
|
||||||
import { FaBars } from "react-icons/fa6";
|
import { FaBars } from "react-icons/fa6";
|
||||||
@@ -248,8 +260,10 @@ export default function MeetingPage({ params }: MeetingPageProps) {
|
|||||||
const joinMeetingMutation = useRoomJoinMeeting();
|
const joinMeetingMutation = useRoomJoinMeeting();
|
||||||
|
|
||||||
const room = roomQuery.data;
|
const room = roomQuery.data;
|
||||||
const isLoading = roomQuery.isLoading || (!attemptedJoin && room && !joinMeetingMutation.data);
|
const isLoading =
|
||||||
|
roomQuery.isLoading ||
|
||||||
|
(!attemptedJoin && room && !joinMeetingMutation.data);
|
||||||
|
|
||||||
// Try to join the meeting when room is loaded
|
// Try to join the meeting when room is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (room && !attemptedJoin && !joinMeetingMutation.isPending) {
|
if (room && !attemptedJoin && !joinMeetingMutation.isPending) {
|
||||||
@@ -326,10 +340,7 @@ export default function MeetingPage({ params }: MeetingPageProps) {
|
|||||||
style={{ width: "100vw", height: "100vh" }}
|
style={{ width: "100vw", height: "100vh" }}
|
||||||
/>
|
/>
|
||||||
{recordingType && recordingTypeRequiresConsent(recordingType) && (
|
{recordingType && recordingTypeRequiresConsent(recordingType) && (
|
||||||
<ConsentDialogButton
|
<ConsentDialogButton meetingId={meetingId} wherebyRef={wherebyRef} />
|
||||||
meetingId={meetingId}
|
|
||||||
wherebyRef={wherebyRef}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -339,10 +350,7 @@ export default function MeetingPage({ params }: MeetingPageProps) {
|
|||||||
// But keeping it as a fallback
|
// But keeping it as a fallback
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" minH="100vh">
|
<Box display="flex" flexDirection="column" minH="100vh">
|
||||||
<MinimalHeader
|
<MinimalHeader roomName={roomName} displayName={room?.name} />
|
||||||
roomName={roomName}
|
|
||||||
displayName={room?.name}
|
|
||||||
/>
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ import { toaster } from "../components/ui/toaster";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { useRecordingConsent } from "../recordingConsentContext";
|
import { useRecordingConsent } from "../recordingConsentContext";
|
||||||
import { useMeetingAudioConsent, useRoomGetByName, useRoomActiveMeetings, useRoomUpcomingMeetings, useRoomsCreateMeeting } from "../lib/apiHooks";
|
import {
|
||||||
|
useMeetingAudioConsent,
|
||||||
|
useRoomGetByName,
|
||||||
|
useRoomActiveMeetings,
|
||||||
|
useRoomUpcomingMeetings,
|
||||||
|
useRoomsCreateMeeting,
|
||||||
|
} from "../lib/apiHooks";
|
||||||
import type { components } from "../reflector-api";
|
import type { components } from "../reflector-api";
|
||||||
import MeetingSelection from "./MeetingSelection";
|
import MeetingSelection from "./MeetingSelection";
|
||||||
import useRoomMeeting from "./useRoomMeeting";
|
import useRoomMeeting from "./useRoomMeeting";
|
||||||
@@ -281,12 +287,11 @@ export default function Room(details: RoomDetails) {
|
|||||||
const roomUrl =
|
const roomUrl =
|
||||||
roomMeeting?.response?.host_room_url || roomMeeting?.response?.room_url;
|
roomMeeting?.response?.host_room_url || roomMeeting?.response?.room_url;
|
||||||
|
|
||||||
const isLoading = status === "loading" || roomQuery.isLoading || roomMeeting?.loading;
|
const isLoading =
|
||||||
|
status === "loading" || roomQuery.isLoading || roomMeeting?.loading;
|
||||||
|
|
||||||
const isOwner =
|
const isOwner =
|
||||||
isAuthenticated && room
|
isAuthenticated && room ? auth.user?.id === room.user_id : false;
|
||||||
? auth.user?.id === room.user_id
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const meetingId = roomMeeting?.response?.id;
|
const meetingId = roomMeeting?.response?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// not useEffect, we need it ASAP
|
// not useEffect, we need it ASAP
|
||||||
|
// apparently, still no guarantee this code runs before mutations are fired
|
||||||
configureApiAuth(
|
configureApiAuth(
|
||||||
contextValue.status === "authenticated" ? contextValue.accessToken : null,
|
contextValue.status === "authenticated"
|
||||||
|
? contextValue.accessToken
|
||||||
|
: contextValue.status === "loading"
|
||||||
|
? undefined
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import "@whereby.com/browser-sdk/embed";
|
|||||||
import { Box, Button, HStack, Text, Link } from "@chakra-ui/react";
|
import { Box, Button, HStack, Text, Link } from "@chakra-ui/react";
|
||||||
import { toaster } from "../components/ui/toaster";
|
import { toaster } from "../components/ui/toaster";
|
||||||
|
|
||||||
interface WherebyEmbedProps {
|
interface WherebyWebinarEmbedProps {
|
||||||
roomUrl: string;
|
roomUrl: string;
|
||||||
onLeave?: () => void;
|
onLeave?: () => void;
|
||||||
isWebinar?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// used for both webinars and meetings
|
// used for webinars only
|
||||||
export default function WherebyWebinarEmbed({
|
export default function WherebyWebinarEmbed({
|
||||||
roomUrl,
|
roomUrl,
|
||||||
onLeave,
|
onLeave,
|
||||||
isWebinar = false,
|
}: WherebyWebinarEmbedProps) {
|
||||||
}: WherebyEmbedProps) {
|
|
||||||
const wherebyRef = useRef<HTMLElement>(null);
|
const wherebyRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
// TODO extract common toast logic / styles to be used by consent toast on normal rooms
|
// TODO extract common toast logic / styles to be used by consent toast on normal rooms
|
||||||
@@ -28,8 +26,7 @@ export default function WherebyWebinarEmbed({
|
|||||||
<Box p={4} bg="white" borderRadius="md" boxShadow="md">
|
<Box p={4} bg="white" borderRadius="md" boxShadow="md">
|
||||||
<HStack justifyContent="space-between" alignItems="center">
|
<HStack justifyContent="space-between" alignItems="center">
|
||||||
<Text>
|
<Text>
|
||||||
This {isWebinar ? "webinar" : "meeting"} is being recorded. By
|
This webinar is being recorded. By continuing, you agree to our{" "}
|
||||||
continuing, you agree to our{" "}
|
|
||||||
<Link
|
<Link
|
||||||
href="https://monadical.com/privacy"
|
href="https://monadical.com/privacy"
|
||||||
color="blue.600"
|
color="blue.600"
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import type { paths } from "../reflector-api";
|
import type { paths } from "../reflector-api";
|
||||||
import {
|
|
||||||
queryOptions,
|
|
||||||
useMutation,
|
|
||||||
useQuery,
|
|
||||||
useSuspenseQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import createFetchClient from "openapi-react-query";
|
import createFetchClient from "openapi-react-query";
|
||||||
import { assertExistsAndNonEmptyString } from "./utils";
|
import { assertExistsAndNonEmptyString } from "./utils";
|
||||||
import { isBuildPhase } from "./next";
|
import { isBuildPhase } from "./next";
|
||||||
@@ -16,18 +10,31 @@ const API_URL = !isBuildPhase
|
|||||||
? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL)
|
? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL)
|
||||||
: "http://localhost";
|
: "http://localhost";
|
||||||
|
|
||||||
// Create the base openapi-fetch client with a default URL
|
|
||||||
// The actual URL will be set via middleware in AuthProvider
|
|
||||||
export const client = createClient<paths>({
|
export const client = createClient<paths>({
|
||||||
baseUrl: API_URL,
|
baseUrl: API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const $api = createFetchClient<paths>(client);
|
const waitForAuthTokenDefinitivePresenceOrAbscence = async () => {
|
||||||
|
let tries = 0;
|
||||||
let currentAuthToken: string | null | undefined = null;
|
let time = 0;
|
||||||
|
const STEP = 100;
|
||||||
|
while (currentAuthToken === undefined) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, STEP));
|
||||||
|
time += STEP;
|
||||||
|
tries++;
|
||||||
|
// most likely first try is more than enough, if it's more there's already something weird happens
|
||||||
|
if (tries > 10) {
|
||||||
|
// even when there's no auth assumed at all, we probably should explicitly call configureApiAuth(null)
|
||||||
|
throw new Error(
|
||||||
|
`Could not get auth token definitive presence/absence in ${time}ms. not calling configureApiAuth?`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
client.use({
|
client.use({
|
||||||
onRequest({ request }) {
|
async onRequest({ request }) {
|
||||||
|
await waitForAuthTokenDefinitivePresenceOrAbscence();
|
||||||
if (currentAuthToken) {
|
if (currentAuthToken) {
|
||||||
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
|
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
|
||||||
}
|
}
|
||||||
@@ -44,7 +51,13 @@ client.use({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const $api = createFetchClient<paths>(client);
|
||||||
|
|
||||||
|
let currentAuthToken: string | null | undefined = undefined;
|
||||||
|
|
||||||
// the function contract: lightweight, idempotent
|
// the function contract: lightweight, idempotent
|
||||||
export const configureApiAuth = (token: string | null | undefined) => {
|
export const configureApiAuth = (token: string | null | undefined) => {
|
||||||
|
// watch only for the initial loading; "reloading" state assumes token presence/absence
|
||||||
|
if (token === undefined && currentAuthToken !== undefined) return;
|
||||||
currentAuthToken = token;
|
currentAuthToken = token;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,8 +96,6 @@ export function useTranscriptProcess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTranscriptGet(transcriptId: string | null) {
|
export function useTranscriptGet(transcriptId: string | null) {
|
||||||
const { isAuthenticated } = useAuthReady();
|
|
||||||
|
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
"/v1/transcripts/{transcript_id}",
|
"/v1/transcripts/{transcript_id}",
|
||||||
@@ -109,7 +107,7 @@ export function useTranscriptGet(transcriptId: string | null) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!transcriptId && isAuthenticated,
|
enabled: !!transcriptId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -292,18 +290,16 @@ export function useTranscriptUploadAudio() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTranscriptWaveform(transcriptId: string | null) {
|
export function useTranscriptWaveform(transcriptId: string | null) {
|
||||||
const { isAuthenticated } = useAuthReady();
|
|
||||||
|
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
"/v1/transcripts/{transcript_id}/audio/waveform",
|
"/v1/transcripts/{transcript_id}/audio/waveform",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
path: { transcript_id: transcriptId || "" },
|
path: { transcript_id: transcriptId! },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!transcriptId && isAuthenticated,
|
enabled: !!transcriptId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -316,7 +312,7 @@ export function useTranscriptMP3(transcriptId: string | null) {
|
|||||||
"/v1/transcripts/{transcript_id}/audio/mp3",
|
"/v1/transcripts/{transcript_id}/audio/mp3",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
path: { transcript_id: transcriptId || "" },
|
path: { transcript_id: transcriptId! },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -326,8 +322,6 @@ export function useTranscriptMP3(transcriptId: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTranscriptTopics(transcriptId: string | null) {
|
export function useTranscriptTopics(transcriptId: string | null) {
|
||||||
const { isAuthenticated } = useAuthReady();
|
|
||||||
|
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
"/v1/transcripts/{transcript_id}/topics",
|
"/v1/transcripts/{transcript_id}/topics",
|
||||||
@@ -337,7 +331,7 @@ export function useTranscriptTopics(transcriptId: string | null) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!transcriptId && isAuthenticated,
|
enabled: !!transcriptId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
www/app/lib/transcript.ts
Normal file
5
www/app/lib/transcript.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { components } from "../reflector-api";
|
||||||
|
|
||||||
|
type ApiTranscriptStatus = components["schemas"]["GetTranscript"]["status"];
|
||||||
|
|
||||||
|
export type TranscriptStatus = ApiTranscriptStatus;
|
||||||
13
www/app/reflector-api.d.ts
vendored
13
www/app/reflector-api.d.ts
vendored
@@ -1243,8 +1243,17 @@ export interface components {
|
|||||||
source_kind: components["schemas"]["SourceKind"];
|
source_kind: components["schemas"]["SourceKind"];
|
||||||
/** Created At */
|
/** Created At */
|
||||||
created_at: string;
|
created_at: string;
|
||||||
/** Status */
|
/**
|
||||||
status: string;
|
* Status
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
status:
|
||||||
|
| "idle"
|
||||||
|
| "uploaded"
|
||||||
|
| "recording"
|
||||||
|
| "processing"
|
||||||
|
| "error"
|
||||||
|
| "ended";
|
||||||
/** Rank */
|
/** Rank */
|
||||||
rank: number;
|
rank: number;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -150,15 +150,7 @@ export default function WebinarPage(details: WebinarDetails) {
|
|||||||
|
|
||||||
if (status === WebinarStatus.Live) {
|
if (status === WebinarStatus.Live) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>{roomUrl && <WherebyEmbed roomUrl={roomUrl} onLeave={handleLeave} />}</>
|
||||||
{roomUrl && (
|
|
||||||
<WherebyEmbed
|
|
||||||
roomUrl={roomUrl}
|
|
||||||
onLeave={handleLeave}
|
|
||||||
isWebinar={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status === WebinarStatus.Ended) {
|
if (status === WebinarStatus.Ended) {
|
||||||
|
|||||||
Reference in New Issue
Block a user