From 575f20fee23bf6be54e86ffac3235d20a0e6d25b Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 18 Aug 2025 19:29:56 -0600 Subject: [PATCH] feat: implement frontend for calendar integration (Phase 3 & 4) - Created MeetingSelection component for choosing between multiple active meetings - Shows both active meetings and upcoming calendar events (30 min ahead) - Displays meeting metadata with privacy controls (owner-only details) - Supports creation of unscheduled meetings alongside calendar meetings - Added waiting page for users joining before scheduled start time - Shows countdown timer until meeting begins - Auto-transitions to meeting when calendar event becomes active - Handles early joining with proper routing - Created collapsible info panel showing meeting details - Displays calendar metadata (title, description, attendees) - Shows participant count and duration - Privacy-aware: sensitive info only visible to room owners - Integrated ICS settings into room configuration dialog - Test connection functionality with immediate feedback - Manual sync trigger with detailed results - Shows last sync time and ETag for monitoring - Configurable sync intervals (1 min to 1 hour) - New /room/{roomName} route for meeting selection - Waiting room at /room/{roomName}/wait?eventId={id} - Classic room page at /{roomName} with meeting info - Uses sessionStorage to pass selected meeting between pages - Added new endpoints for active/upcoming meetings - Regenerated TypeScript client with latest OpenAPI spec - Proper error handling and loading states - Auto-refresh every 30 seconds for live updates - Color-coded badges for meeting status - Attendee status indicators (accepted/declined/tentative) - Responsive design with Chakra UI components - Clear visual hierarchy between active and upcoming meetings - Smart truncation for long attendee lists This completes the frontend implementation for calendar integration, enabling users to seamlessly join scheduled meetings from their calendar applications. --- PLAN.md | 74 +- .../(app)/rooms/_components/ICSSettings.tsx | 258 +++ www/app/(app)/rooms/page.tsx | 49 + www/app/[roomName]/MeetingInfo.tsx | 203 ++ www/app/[roomName]/MeetingSelection.tsx | 335 +++ www/app/[roomName]/page.tsx | 38 +- www/app/[roomName]/useRoomMeeting.tsx | 14 + www/app/api/schemas.gen.ts | 1937 +++++++++++++++++ www/app/api/services.gen.ts | 1043 +++++++++ www/app/api/types.gen.ts | 1243 +++++++++++ www/app/room/[roomName]/page.tsx | 147 ++ www/app/room/[roomName]/wait/page.tsx | 277 +++ 12 files changed, 5606 insertions(+), 12 deletions(-) create mode 100644 www/app/(app)/rooms/_components/ICSSettings.tsx create mode 100644 www/app/[roomName]/MeetingInfo.tsx create mode 100644 www/app/[roomName]/MeetingSelection.tsx create mode 100644 www/app/room/[roomName]/page.tsx create mode 100644 www/app/room/[roomName]/wait/page.tsx diff --git a/PLAN.md b/PLAN.md index 1d1dde11..0648d5a0 100644 --- a/PLAN.md +++ b/PLAN.md @@ -184,20 +184,43 @@ ICS calendar URLs are attached to rooms (not users) to enable automatic meeting - Simple HTTP fetching without unnecessary validation - Proper TypedDict typing for event data structures - Supports any standard ICS format -4. ⚠️ API endpoints for ICS configuration (partial) + - Event matching on full room URL only +4. ✅ API endpoints for ICS configuration - Room model updated to support ICS fields via existing PATCH endpoint - - Dedicated ICS endpoints still pending -5. ⚠️ Celery background tasks for periodic sync (pending) + - POST /v1/rooms/{room_name}/ics/sync - Trigger manual sync (owner only) + - GET /v1/rooms/{room_name}/ics/status - Get sync status (owner only) + - GET /v1/rooms/{room_name}/meetings - List meetings with privacy controls + - GET /v1/rooms/{room_name}/meetings/upcoming - List upcoming meetings +5. ✅ Celery background tasks for periodic sync + - sync_room_ics - Sync individual room calendar + - sync_all_ics_calendars - Check all rooms and queue sync based on fetch intervals + - pre_create_upcoming_meetings - Pre-create Whereby meetings 1 minute before start + - Tasks scheduled in beat schedule (every minute for checking, respects individual intervals) 6. ✅ Tests written and passing - 6 tests for Room ICS fields - 7 tests for CalendarEvent model - - All 13 tests passing + - 7 tests for ICS sync service + - 11 tests for API endpoints + - 6 tests for background tasks + - All 31 ICS-related tests passing -### Phase 2: Meeting Management (Week 2) -1. Update meeting lifecycle logic -2. Support multiple active meetings -3. Implement grace period logic -4. Link meetings to calendar events +### Phase 2: Meeting Management (Week 2) ✅ COMPLETED (2025-08-19) +1. ✅ Updated meeting lifecycle logic with grace period support + - 15-minute grace period after last participant leaves + - Automatic reactivation when participants rejoin + - Force close calendar meetings 30 minutes after scheduled end +2. ✅ Support multiple active meetings per room + - Removed unique constraint on active meetings + - Added get_all_active_for_room() method + - Added get_active_by_calendar_event() method +3. ✅ Implemented grace period logic + - Added last_participant_left_at and grace_period_minutes fields + - Process meetings task handles grace period checking + - Whereby webhooks clear grace period on participant join +4. ✅ Link meetings to calendar events + - Pre-created meetings properly linked via calendar_event_id + - Calendar metadata stored with meeting + - API endpoints for listing and joining specific meetings ### Phase 3: Frontend Meeting Selection (Week 3) 1. Build meeting selection page @@ -232,6 +255,25 @@ ICS calendar URLs are attached to rooms (not users) to enable automatic meeting 9. **Configurable fetch interval** - Balance between freshness and server load 10. **ICS over CalDAV** - Simpler implementation, wider compatibility, no complex auth +## Phase 2 Implementation Files + +### Database Migrations +- `/server/migrations/versions/6025e9b2bef2_remove_one_active_meeting_per_room_.py` - Remove unique constraint +- `/server/migrations/versions/d4a1c446458c_add_grace_period_fields_to_meeting.py` - Add grace period fields + +### Updated Models +- `/server/reflector/db/meetings.py` - Added grace period fields and new query methods + +### Updated Services +- `/server/reflector/worker/process.py` - Enhanced with grace period logic and multiple meeting support + +### Updated API +- `/server/reflector/views/rooms.py` - Added endpoints for listing active meetings and joining specific meetings +- `/server/reflector/views/whereby.py` - Clear grace period on participant join + +### Tests +- `/server/tests/test_multiple_active_meetings.py` - Comprehensive tests for Phase 2 features (5 tests) + ## Phase 1 Implementation Files Created ### Database Models @@ -240,11 +282,21 @@ ICS calendar URLs are attached to rooms (not users) to enable automatic meeting - `/server/reflector/db/meetings.py` - Updated with calendar_event_id and calendar_metadata (JSONB) ### Services -- `/server/reflector/services/ics_sync.py` - ICS fetching with TypedDict for proper typing +- `/server/reflector/services/ics_sync.py` - ICS fetching and parsing with TypedDict for proper typing + +### API Endpoints +- `/server/reflector/views/rooms.py` - Added ICS management endpoints with privacy controls + +### Background Tasks +- `/server/reflector/worker/ics_sync.py` - Celery tasks for automatic periodic sync +- `/server/reflector/worker/app.py` - Updated beat schedule for ICS tasks ### Tests - `/server/tests/test_room_ics.py` - Room model ICS fields tests (6 tests) - `/server/tests/test_calendar_event.py` - CalendarEvent model tests (7 tests) +- `/server/tests/test_ics_sync.py` - ICS sync service tests (7 tests) +- `/server/tests/test_room_ics_api.py` - API endpoint tests (11 tests) +- `/server/tests/test_ics_background_tasks.py` - Background task tests (6 tests) ### Key Design Decisions - No encryption needed - ICS URLs are read-only access @@ -252,6 +304,8 @@ ICS calendar URLs are attached to rooms (not users) to enable automatic meeting - Proper TypedDict typing for event data structures - Removed unnecessary URL validation and webcal handling - calendar_metadata in meetings stores flexible calendar data (organizer, recurrence, etc) +- Background tasks query all rooms directly to avoid filtering issues +- Sync intervals respected per-room configuration ## Implementation Approach diff --git a/www/app/(app)/rooms/_components/ICSSettings.tsx b/www/app/(app)/rooms/_components/ICSSettings.tsx new file mode 100644 index 00000000..715f312e --- /dev/null +++ b/www/app/(app)/rooms/_components/ICSSettings.tsx @@ -0,0 +1,258 @@ +import { + VStack, + HStack, + Field, + Input, + Select, + Checkbox, + Button, + Text, + Alert, + AlertIcon, + AlertTitle, + Badge, + createListCollection, + Spinner, +} from "@chakra-ui/react"; +import { useState } from "react"; +import { FaSync, FaCheckCircle, FaExclamationCircle } from "react-icons/fa"; +import useApi from "../../../lib/useApi"; + +interface ICSSettingsProps { + roomId?: string; + roomName?: string; + icsUrl?: string; + icsEnabled?: boolean; + icsFetchInterval?: number; + icsLastSync?: string; + icsLastEtag?: string; + onChange: (settings: Partial) => void; + isOwner?: boolean; +} + +export interface ICSSettingsData { + ics_url: string; + ics_enabled: boolean; + ics_fetch_interval: number; +} + +const fetchIntervalOptions = [ + { label: "1 minute", value: "1" }, + { label: "5 minutes", value: "5" }, + { label: "10 minutes", value: "10" }, + { label: "30 minutes", value: "30" }, + { label: "1 hour", value: "60" }, +]; + +export default function ICSSettings({ + roomId, + roomName, + icsUrl = "", + icsEnabled = false, + icsFetchInterval = 5, + icsLastSync, + icsLastEtag, + onChange, + isOwner = true, +}: ICSSettingsProps) { + const [syncStatus, setSyncStatus] = useState< + "idle" | "syncing" | "success" | "error" + >("idle"); + const [syncMessage, setSyncMessage] = useState(""); + const [testResult, setTestResult] = useState(""); + const api = useApi(); + + const fetchIntervalCollection = createListCollection({ + items: fetchIntervalOptions, + }); + + const handleTestConnection = async () => { + if (!api || !icsUrl || !roomName) return; + + setSyncStatus("syncing"); + setTestResult(""); + + try { + // First update the room with the ICS URL + await api.v1RoomsPartialUpdate({ + roomId: roomId || roomName, + requestBody: { + ics_url: icsUrl, + ics_enabled: true, + ics_fetch_interval: icsFetchInterval, + }, + }); + + // Then trigger a sync + const result = await api.v1RoomsTriggerIcsSync({ roomName }); + + if (result.status === "success") { + setSyncStatus("success"); + setTestResult( + `Successfully synced! Found ${result.events_found} events.`, + ); + } else { + setSyncStatus("error"); + setTestResult(result.error || "Sync failed"); + } + } catch (err: any) { + setSyncStatus("error"); + setTestResult(err.body?.detail || "Failed to test ICS connection"); + } + }; + + const handleManualSync = async () => { + if (!api || !roomName) return; + + setSyncStatus("syncing"); + setSyncMessage(""); + + try { + const result = await api.v1RoomsTriggerIcsSync({ roomName }); + + if (result.status === "success") { + setSyncStatus("success"); + setSyncMessage( + `Sync complete! Found ${result.events_found} events, ` + + `created ${result.events_created}, updated ${result.events_updated}.`, + ); + } else { + setSyncStatus("error"); + setSyncMessage(result.error || "Sync failed"); + } + } catch (err: any) { + setSyncStatus("error"); + setSyncMessage(err.body?.detail || "Failed to sync calendar"); + } + + // Clear status after 5 seconds + setTimeout(() => { + setSyncStatus("idle"); + setSyncMessage(""); + }, 5000); + }; + + if (!isOwner) { + return null; // ICS settings only visible to room owner + } + + return ( + + + Calendar Integration (ICS) + + + + onChange({ ics_enabled: e.checked })} + > + + + + + Enable ICS calendar sync + + + + {icsEnabled && ( + <> + + ICS Calendar URL + onChange({ ics_url: e.target.value })} + /> + + Enter the ICS URL from Google Calendar, Outlook, or other calendar + services + + + + + Sync Interval + { + const value = parseInt(details.value[0]); + onChange({ ics_fetch_interval: value }); + }} + > + + + + + {fetchIntervalOptions.map((option) => ( + + {option.label} + + ))} + + + + How often to check for calendar updates + + + + {icsUrl && ( + + + + {roomName && icsLastSync && ( + + )} + + )} + + {testResult && ( + + + {testResult} + + )} + + {syncMessage && ( + + + {syncMessage} + + )} + + {icsLastSync && ( + + + + Last sync: {new Date(icsLastSync).toLocaleString()} + + {icsLastEtag && ( + + ETag: {icsLastEtag.slice(0, 8)}... + + )} + + )} + + )} + + ); +} diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 8b1378df..3e6a28b0 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -31,6 +31,7 @@ import { import { RoomList } from "./_components/RoomList"; import { PaginationPage } from "../browse/_components/Pagination"; import { assertExists } from "../../lib/utils"; +import ICSSettings from "./_components/ICSSettings"; type Room = components["schemas"]["Room"]; @@ -70,6 +71,9 @@ const roomInitialState = { isShared: false, webhookUrl: "", webhookSecret: "", + icsUrl: "", + icsEnabled: false, + icsFetchInterval: 5, }; export default function RoomsList() { @@ -275,6 +279,9 @@ export default function RoomsList() { is_shared: room.isShared, webhook_url: room.webhookUrl, webhook_secret: room.webhookSecret, + ics_url: room.icsUrl, + ics_enabled: room.icsEnabled, + ics_fetch_interval: room.icsFetchInterval, }; if (isEditing) { @@ -316,6 +323,22 @@ export default function RoomsList() { setShowWebhookSecret(false); setWebhookTestResult(null); + setRoom({ + name: roomData.name, + zulipAutoPost: roomData.zulip_auto_post, + zulipStream: roomData.zulip_stream, + zulipTopic: roomData.zulip_topic, + isLocked: roomData.is_locked, + roomMode: roomData.room_mode, + recordingType: roomData.recording_type, + recordingTrigger: roomData.recording_trigger, + isShared: roomData.is_shared, + webhookUrl: roomData.webhook_url || "", + webhookSecret: roomData.webhook_secret || "", + icsUrl: roomData.ics_url || "", + icsEnabled: roomData.ics_enabled || false, + icsFetchInterval: roomData.ics_fetch_interval || 5, + }); setEditRoomId(roomId); setIsEditing(true); setNameError(""); @@ -763,6 +786,32 @@ export default function RoomsList() { Shared room + + { + setRoom({ + ...room, + icsUrl: + settings.ics_url !== undefined + ? settings.ics_url + : room.icsUrl, + icsEnabled: + settings.ics_enabled !== undefined + ? settings.ics_enabled + : room.icsEnabled, + icsFetchInterval: + settings.ics_fetch_interval !== undefined + ? settings.ics_fetch_interval + : room.icsFetchInterval, + }); + }} + isOwner={true} + /> + + + + ))} + + + )} + + {/* Upcoming Meetings */} + {upcomingEvents.length > 0 && ( + <> + + Upcoming Meetings + + + {upcomingEvents.map((event) => ( + + + + + + + + {event.title || "Scheduled Meeting"} + + + {formatCountdown(event.start_time)} + + + + {isOwner && event.description && ( + + {event.description} + + )} + + + + {formatDateTime(event.start_time)} -{" "} + {formatDateTime(event.end_time)} + + + + {isOwner && event.attendees && ( + + {event.attendees + .slice(0, 3) + .map((attendee: any, idx: number) => ( + + {attendee.name || attendee.email} + + ))} + {event.attendees.length > 3 && ( + + +{event.attendees.length - 3} more + + )} + + )} + + + + + + + ))} + + + )} + + + + {/* Create Unscheduled Meeting */} + + + + + Start an Unscheduled Meeting + + Create a new meeting room that's not on the calendar + + + + + + + + + ); +} diff --git a/www/app/[roomName]/page.tsx b/www/app/[roomName]/page.tsx index 0130588b..1e216813 100644 --- a/www/app/[roomName]/page.tsx +++ b/www/app/[roomName]/page.tsx @@ -24,10 +24,13 @@ import { notFound } from "next/navigation"; import { useRecordingConsent } from "../recordingConsentContext"; import { useMeetingAudioConsent } from "../lib/apiHooks"; import type { components } from "../reflector-api"; +import useApi from "../lib/useApi"; +import { FaBars, FaInfoCircle } from "react-icons/fa6"; +import MeetingInfo from "./MeetingInfo"; +import { useAuth } from "../lib/AuthProvider"; type Meeting = components["schemas"]["Meeting"]; -import { FaBars } from "react-icons/fa6"; -import { useAuth } from "../lib/AuthProvider"; +type Room = components["schemas"]["Room"]; export type RoomDetails = { params: { @@ -263,6 +266,9 @@ export default function Room(details: RoomDetails) { const status = useAuth().status; const isAuthenticated = status === "authenticated"; const isLoading = status === "loading" || meeting.loading; + const [showMeetingInfo, setShowMeetingInfo] = useState(false); + const [room, setRoom] = useState(null); + const api = useApi(); const roomUrl = meeting?.response?.host_room_url ? meeting?.response?.host_room_url @@ -276,6 +282,15 @@ export default function Room(details: RoomDetails) { router.push("/browse"); }, [router]); + // Fetch room details + useEffect(() => { + if (!api || !roomName) return; + + api.v1RoomsRetrieve({ roomName }).then(setRoom).catch(console.error); + }, [api, roomName]); + + const isOwner = session?.user?.id === room?.user_id; + useEffect(() => { if ( !isLoading && @@ -327,6 +342,25 @@ export default function Room(details: RoomDetails) { wherebyRef={wherebyRef} /> )} + {meeting?.response && ( + <> + + {showMeetingInfo && ( + + )} + + )} )} diff --git a/www/app/[roomName]/useRoomMeeting.tsx b/www/app/[roomName]/useRoomMeeting.tsx index 93491a05..5e26c166 100644 --- a/www/app/[roomName]/useRoomMeeting.tsx +++ b/www/app/[roomName]/useRoomMeeting.tsx @@ -40,6 +40,20 @@ const useRoomMeeting = ( useEffect(() => { if (!roomName) return; + // Check if meeting was pre-selected from meeting selection page + const storedMeeting = sessionStorage.getItem(`meeting_${roomName}`); + if (storedMeeting) { + try { + const meeting = JSON.parse(storedMeeting); + sessionStorage.removeItem(`meeting_${roomName}`); // Clean up + setResponse(meeting); + setLoading(false); + return; + } catch (e) { + console.error("Failed to parse stored meeting:", e); + } + } + const createMeeting = async () => { try { const result = await createMeetingMutation.mutateAsync({ diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index e69de29b..3e9f21c0 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -0,0 +1,1937 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const $AudioWaveform = { + properties: { + data: { + items: { + type: "number", + }, + type: "array", + title: "Data", + }, + }, + type: "object", + required: ["data"], + title: "AudioWaveform", +} as const; + +export const $Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post = + { + properties: { + chunk: { + type: "string", + format: "binary", + title: "Chunk", + }, + }, + type: "object", + required: ["chunk"], + title: + "Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post", + } as const; + +export const $CalendarEventResponse = { + properties: { + id: { + type: "string", + title: "Id", + }, + room_id: { + type: "string", + title: "Room Id", + }, + ics_uid: { + type: "string", + title: "Ics Uid", + }, + title: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Title", + }, + description: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Description", + }, + start_time: { + type: "string", + format: "date-time", + title: "Start Time", + }, + end_time: { + type: "string", + format: "date-time", + title: "End Time", + }, + attendees: { + anyOf: [ + { + items: { + additionalProperties: true, + type: "object", + }, + type: "array", + }, + { + type: "null", + }, + ], + title: "Attendees", + }, + location: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Location", + }, + last_synced: { + type: "string", + format: "date-time", + title: "Last Synced", + }, + created_at: { + type: "string", + format: "date-time", + title: "Created At", + }, + updated_at: { + type: "string", + format: "date-time", + title: "Updated At", + }, + }, + type: "object", + required: [ + "id", + "room_id", + "ics_uid", + "start_time", + "end_time", + "last_synced", + "created_at", + "updated_at", + ], + title: "CalendarEventResponse", +} as const; + +export const $CreateParticipant = { + properties: { + speaker: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Speaker", + }, + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["name"], + title: "CreateParticipant", +} as const; + +export const $CreateRoom = { + properties: { + name: { + type: "string", + title: "Name", + }, + zulip_auto_post: { + type: "boolean", + title: "Zulip Auto Post", + }, + zulip_stream: { + type: "string", + title: "Zulip Stream", + }, + zulip_topic: { + type: "string", + title: "Zulip Topic", + }, + is_locked: { + type: "boolean", + title: "Is Locked", + }, + room_mode: { + type: "string", + title: "Room Mode", + }, + recording_type: { + type: "string", + title: "Recording Type", + }, + recording_trigger: { + type: "string", + title: "Recording Trigger", + }, + is_shared: { + type: "boolean", + title: "Is Shared", + }, + ics_url: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Ics Url", + }, + ics_fetch_interval: { + type: "integer", + title: "Ics Fetch Interval", + default: 300, + }, + ics_enabled: { + type: "boolean", + title: "Ics Enabled", + default: false, + }, + }, + type: "object", + required: [ + "name", + "zulip_auto_post", + "zulip_stream", + "zulip_topic", + "is_locked", + "room_mode", + "recording_type", + "recording_trigger", + "is_shared", + ], + title: "CreateRoom", +} as const; + +export const $CreateTranscript = { + properties: { + name: { + type: "string", + title: "Name", + }, + source_language: { + type: "string", + title: "Source Language", + default: "en", + }, + target_language: { + type: "string", + title: "Target Language", + default: "en", + }, + }, + type: "object", + required: ["name"], + title: "CreateTranscript", +} as const; + +export const $DeletionStatus = { + properties: { + status: { + type: "string", + title: "Status", + }, + }, + type: "object", + required: ["status"], + title: "DeletionStatus", +} as const; + +export const $GetTranscript = { + properties: { + id: { + type: "string", + title: "Id", + }, + user_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "User Id", + }, + name: { + type: "string", + title: "Name", + }, + status: { + type: "string", + title: "Status", + }, + locked: { + type: "boolean", + title: "Locked", + }, + duration: { + type: "number", + title: "Duration", + }, + title: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Title", + }, + short_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Short Summary", + }, + long_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Long Summary", + }, + created_at: { + type: "string", + title: "Created At", + }, + share_mode: { + type: "string", + title: "Share Mode", + default: "private", + }, + source_language: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Source Language", + }, + target_language: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Target Language", + }, + reviewed: { + type: "boolean", + title: "Reviewed", + }, + meeting_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Meeting Id", + }, + source_kind: { + $ref: "#/components/schemas/SourceKind", + }, + room_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Id", + }, + room_name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Name", + }, + audio_deleted: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Audio Deleted", + }, + participants: { + anyOf: [ + { + items: { + $ref: "#/components/schemas/TranscriptParticipant", + }, + type: "array", + }, + { + type: "null", + }, + ], + title: "Participants", + }, + }, + type: "object", + required: [ + "id", + "user_id", + "name", + "status", + "locked", + "duration", + "title", + "short_summary", + "long_summary", + "created_at", + "source_language", + "target_language", + "reviewed", + "meeting_id", + "source_kind", + "participants", + ], + title: "GetTranscript", +} as const; + +export const $GetTranscriptMinimal = { + properties: { + id: { + type: "string", + title: "Id", + }, + user_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "User Id", + }, + name: { + type: "string", + title: "Name", + }, + status: { + type: "string", + title: "Status", + }, + locked: { + type: "boolean", + title: "Locked", + }, + duration: { + type: "number", + title: "Duration", + }, + title: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Title", + }, + short_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Short Summary", + }, + long_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Long Summary", + }, + created_at: { + type: "string", + title: "Created At", + }, + share_mode: { + type: "string", + title: "Share Mode", + default: "private", + }, + source_language: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Source Language", + }, + target_language: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Target Language", + }, + reviewed: { + type: "boolean", + title: "Reviewed", + }, + meeting_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Meeting Id", + }, + source_kind: { + $ref: "#/components/schemas/SourceKind", + }, + room_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Id", + }, + room_name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Name", + }, + audio_deleted: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Audio Deleted", + }, + }, + type: "object", + required: [ + "id", + "user_id", + "name", + "status", + "locked", + "duration", + "title", + "short_summary", + "long_summary", + "created_at", + "source_language", + "target_language", + "reviewed", + "meeting_id", + "source_kind", + ], + title: "GetTranscriptMinimal", +} as const; + +export const $GetTranscriptSegmentTopic = { + properties: { + text: { + type: "string", + title: "Text", + }, + start: { + type: "number", + title: "Start", + }, + speaker: { + type: "integer", + title: "Speaker", + }, + }, + type: "object", + required: ["text", "start", "speaker"], + title: "GetTranscriptSegmentTopic", +} as const; + +export const $GetTranscriptTopic = { + properties: { + id: { + type: "string", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + summary: { + type: "string", + title: "Summary", + }, + timestamp: { + type: "number", + title: "Timestamp", + }, + duration: { + anyOf: [ + { + type: "number", + }, + { + type: "null", + }, + ], + title: "Duration", + }, + transcript: { + type: "string", + title: "Transcript", + }, + segments: { + items: { + $ref: "#/components/schemas/GetTranscriptSegmentTopic", + }, + type: "array", + title: "Segments", + default: [], + }, + }, + type: "object", + required: ["id", "title", "summary", "timestamp", "duration", "transcript"], + title: "GetTranscriptTopic", +} as const; + +export const $GetTranscriptTopicWithWords = { + properties: { + id: { + type: "string", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + summary: { + type: "string", + title: "Summary", + }, + timestamp: { + type: "number", + title: "Timestamp", + }, + duration: { + anyOf: [ + { + type: "number", + }, + { + type: "null", + }, + ], + title: "Duration", + }, + transcript: { + type: "string", + title: "Transcript", + }, + segments: { + items: { + $ref: "#/components/schemas/GetTranscriptSegmentTopic", + }, + type: "array", + title: "Segments", + default: [], + }, + words: { + items: { + $ref: "#/components/schemas/Word", + }, + type: "array", + title: "Words", + default: [], + }, + }, + type: "object", + required: ["id", "title", "summary", "timestamp", "duration", "transcript"], + title: "GetTranscriptTopicWithWords", +} as const; + +export const $GetTranscriptTopicWithWordsPerSpeaker = { + properties: { + id: { + type: "string", + title: "Id", + }, + title: { + type: "string", + title: "Title", + }, + summary: { + type: "string", + title: "Summary", + }, + timestamp: { + type: "number", + title: "Timestamp", + }, + duration: { + anyOf: [ + { + type: "number", + }, + { + type: "null", + }, + ], + title: "Duration", + }, + transcript: { + type: "string", + title: "Transcript", + }, + segments: { + items: { + $ref: "#/components/schemas/GetTranscriptSegmentTopic", + }, + type: "array", + title: "Segments", + default: [], + }, + words_per_speaker: { + items: { + $ref: "#/components/schemas/SpeakerWords", + }, + type: "array", + title: "Words Per Speaker", + default: [], + }, + }, + type: "object", + required: ["id", "title", "summary", "timestamp", "duration", "transcript"], + title: "GetTranscriptTopicWithWordsPerSpeaker", +} as const; + +export const $HTTPValidationError = { + properties: { + detail: { + items: { + $ref: "#/components/schemas/ValidationError", + }, + type: "array", + title: "Detail", + }, + }, + type: "object", + title: "HTTPValidationError", +} as const; + +export const $ICSStatus = { + properties: { + status: { + type: "string", + title: "Status", + }, + last_sync: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Last Sync", + }, + next_sync: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Next Sync", + }, + last_etag: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Last Etag", + }, + events_count: { + type: "integer", + title: "Events Count", + default: 0, + }, + }, + type: "object", + required: ["status"], + title: "ICSStatus", +} as const; + +export const $ICSSyncResult = { + properties: { + status: { + type: "string", + title: "Status", + }, + hash: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Hash", + }, + events_found: { + type: "integer", + title: "Events Found", + default: 0, + }, + events_created: { + type: "integer", + title: "Events Created", + default: 0, + }, + events_updated: { + type: "integer", + title: "Events Updated", + default: 0, + }, + events_deleted: { + type: "integer", + title: "Events Deleted", + default: 0, + }, + error: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Error", + }, + }, + type: "object", + required: ["status"], + title: "ICSSyncResult", +} as const; + +export const $Meeting = { + properties: { + id: { + type: "string", + title: "Id", + }, + room_name: { + type: "string", + title: "Room Name", + }, + room_url: { + type: "string", + title: "Room Url", + }, + host_room_url: { + type: "string", + title: "Host Room Url", + }, + start_date: { + type: "string", + format: "date-time", + title: "Start Date", + }, + end_date: { + type: "string", + format: "date-time", + title: "End Date", + }, + recording_type: { + type: "string", + enum: ["none", "local", "cloud"], + title: "Recording Type", + default: "cloud", + }, + }, + type: "object", + required: [ + "id", + "room_name", + "room_url", + "host_room_url", + "start_date", + "end_date", + ], + title: "Meeting", +} as const; + +export const $MeetingConsentRequest = { + properties: { + consent_given: { + type: "boolean", + title: "Consent Given", + }, + }, + type: "object", + required: ["consent_given"], + title: "MeetingConsentRequest", +} as const; + +export const $Page_GetTranscriptMinimal_ = { + properties: { + items: { + items: { + $ref: "#/components/schemas/GetTranscriptMinimal", + }, + type: "array", + title: "Items", + }, + total: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Total", + }, + page: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Page", + }, + size: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Size", + }, + pages: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Pages", + }, + }, + type: "object", + required: ["items", "page", "size"], + title: "Page[GetTranscriptMinimal]", +} as const; + +export const $Page_Room_ = { + properties: { + items: { + items: { + $ref: "#/components/schemas/Room", + }, + type: "array", + title: "Items", + }, + total: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Total", + }, + page: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Page", + }, + size: { + anyOf: [ + { + type: "integer", + minimum: 1, + }, + { + type: "null", + }, + ], + title: "Size", + }, + pages: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Pages", + }, + }, + type: "object", + required: ["items", "page", "size"], + title: "Page[Room]", +} as const; + +export const $Participant = { + properties: { + id: { + type: "string", + title: "Id", + }, + speaker: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Speaker", + }, + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["id", "speaker", "name"], + title: "Participant", +} as const; + +export const $Room = { + properties: { + id: { + type: "string", + title: "Id", + }, + name: { + type: "string", + title: "Name", + }, + user_id: { + type: "string", + title: "User Id", + }, + created_at: { + type: "string", + format: "date-time", + title: "Created At", + }, + zulip_auto_post: { + type: "boolean", + title: "Zulip Auto Post", + }, + zulip_stream: { + type: "string", + title: "Zulip Stream", + }, + zulip_topic: { + type: "string", + title: "Zulip Topic", + }, + is_locked: { + type: "boolean", + title: "Is Locked", + }, + room_mode: { + type: "string", + title: "Room Mode", + }, + recording_type: { + type: "string", + title: "Recording Type", + }, + recording_trigger: { + type: "string", + title: "Recording Trigger", + }, + is_shared: { + type: "boolean", + title: "Is Shared", + }, + ics_url: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Ics Url", + }, + ics_fetch_interval: { + type: "integer", + title: "Ics Fetch Interval", + default: 300, + }, + ics_enabled: { + type: "boolean", + title: "Ics Enabled", + default: false, + }, + ics_last_sync: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Ics Last Sync", + }, + ics_last_etag: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Ics Last Etag", + }, + }, + type: "object", + required: [ + "id", + "name", + "user_id", + "created_at", + "zulip_auto_post", + "zulip_stream", + "zulip_topic", + "is_locked", + "room_mode", + "recording_type", + "recording_trigger", + "is_shared", + ], + title: "Room", +} as const; + +export const $RtcOffer = { + properties: { + sdp: { + type: "string", + title: "Sdp", + }, + type: { + type: "string", + title: "Type", + }, + }, + type: "object", + required: ["sdp", "type"], + title: "RtcOffer", +} as const; + +export const $SearchResponse = { + properties: { + results: { + items: { + $ref: "#/components/schemas/SearchResult", + }, + type: "array", + title: "Results", + }, + total: { + type: "integer", + minimum: 0, + title: "Total", + description: "Total number of search results", + }, + query: { + type: "string", + minLength: 1, + title: "Query", + description: "Search query text", + }, + limit: { + type: "integer", + maximum: 100, + minimum: 1, + title: "Limit", + description: "Results per page", + }, + offset: { + type: "integer", + minimum: 0, + title: "Offset", + description: "Number of results to skip", + }, + }, + type: "object", + required: ["results", "total", "query", "limit", "offset"], + title: "SearchResponse", +} as const; + +export const $SearchResult = { + properties: { + id: { + type: "string", + minLength: 1, + title: "Id", + }, + title: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Title", + }, + user_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "User Id", + }, + room_id: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Id", + }, + created_at: { + type: "string", + title: "Created At", + }, + status: { + type: "string", + minLength: 1, + title: "Status", + }, + rank: { + type: "number", + maximum: 1, + minimum: 0, + title: "Rank", + }, + duration: { + anyOf: [ + { + type: "number", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Duration", + description: "Duration in seconds", + }, + search_snippets: { + items: { + type: "string", + }, + type: "array", + title: "Search Snippets", + description: "Text snippets around search matches", + }, + }, + type: "object", + required: [ + "id", + "created_at", + "status", + "rank", + "duration", + "search_snippets", + ], + title: "SearchResult", + description: "Public search result model with computed fields.", +} as const; + +export const $SourceKind = { + type: "string", + enum: ["room", "live", "file"], + title: "SourceKind", +} as const; + +export const $SpeakerAssignment = { + properties: { + speaker: { + anyOf: [ + { + type: "integer", + minimum: 0, + }, + { + type: "null", + }, + ], + title: "Speaker", + }, + participant: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Participant", + }, + timestamp_from: { + type: "number", + title: "Timestamp From", + }, + timestamp_to: { + type: "number", + title: "Timestamp To", + }, + }, + type: "object", + required: ["timestamp_from", "timestamp_to"], + title: "SpeakerAssignment", +} as const; + +export const $SpeakerAssignmentStatus = { + properties: { + status: { + type: "string", + title: "Status", + }, + }, + type: "object", + required: ["status"], + title: "SpeakerAssignmentStatus", +} as const; + +export const $SpeakerMerge = { + properties: { + speaker_from: { + type: "integer", + title: "Speaker From", + }, + speaker_to: { + type: "integer", + title: "Speaker To", + }, + }, + type: "object", + required: ["speaker_from", "speaker_to"], + title: "SpeakerMerge", +} as const; + +export const $SpeakerWords = { + properties: { + speaker: { + type: "integer", + title: "Speaker", + }, + words: { + items: { + $ref: "#/components/schemas/Word", + }, + type: "array", + title: "Words", + }, + }, + type: "object", + required: ["speaker", "words"], + title: "SpeakerWords", +} as const; + +export const $Stream = { + properties: { + stream_id: { + type: "integer", + title: "Stream Id", + }, + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["stream_id", "name"], + title: "Stream", +} as const; + +export const $Topic = { + properties: { + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["name"], + title: "Topic", +} as const; + +export const $TranscriptParticipant = { + properties: { + id: { + type: "string", + title: "Id", + }, + speaker: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Speaker", + }, + name: { + type: "string", + title: "Name", + }, + }, + type: "object", + required: ["speaker", "name"], + title: "TranscriptParticipant", +} as const; + +export const $UpdateParticipant = { + properties: { + speaker: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Speaker", + }, + name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Name", + }, + }, + type: "object", + title: "UpdateParticipant", +} as const; + +export const $UpdateRoom = { + properties: { + name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Name", + }, + zulip_auto_post: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Zulip Auto Post", + }, + zulip_stream: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Zulip Stream", + }, + zulip_topic: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Zulip Topic", + }, + is_locked: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Is Locked", + }, + room_mode: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Room Mode", + }, + recording_type: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Recording Type", + }, + recording_trigger: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Recording Trigger", + }, + is_shared: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Is Shared", + }, + ics_url: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Ics Url", + }, + ics_fetch_interval: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Ics Fetch Interval", + }, + ics_enabled: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Ics Enabled", + }, + }, + type: "object", + title: "UpdateRoom", +} as const; + +export const $UpdateTranscript = { + properties: { + name: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Name", + }, + locked: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Locked", + }, + title: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Title", + }, + short_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Short Summary", + }, + long_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Long Summary", + }, + share_mode: { + anyOf: [ + { + type: "string", + enum: ["public", "semi-private", "private"], + }, + { + type: "null", + }, + ], + title: "Share Mode", + }, + participants: { + anyOf: [ + { + items: { + $ref: "#/components/schemas/TranscriptParticipant", + }, + type: "array", + }, + { + type: "null", + }, + ], + title: "Participants", + }, + reviewed: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Reviewed", + }, + audio_deleted: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Audio Deleted", + }, + }, + type: "object", + title: "UpdateTranscript", +} as const; + +export const $UserInfo = { + properties: { + sub: { + type: "string", + title: "Sub", + }, + email: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Email", + }, + email_verified: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Email Verified", + }, + }, + type: "object", + required: ["sub", "email", "email_verified"], + title: "UserInfo", +} as const; + +export const $ValidationError = { + properties: { + loc: { + items: { + anyOf: [ + { + type: "string", + }, + { + type: "integer", + }, + ], + }, + type: "array", + title: "Location", + }, + msg: { + type: "string", + title: "Message", + }, + type: { + type: "string", + title: "Error Type", + }, + }, + type: "object", + required: ["loc", "msg", "type"], + title: "ValidationError", +} as const; + +export const $WherebyWebhookEvent = { + properties: { + apiVersion: { + type: "string", + title: "Apiversion", + }, + id: { + type: "string", + title: "Id", + }, + createdAt: { + type: "string", + format: "date-time", + title: "Createdat", + }, + type: { + type: "string", + title: "Type", + }, + data: { + additionalProperties: true, + type: "object", + title: "Data", + }, + }, + type: "object", + required: ["apiVersion", "id", "createdAt", "type", "data"], + title: "WherebyWebhookEvent", +} as const; + +export const $Word = { + properties: { + text: { + type: "string", + title: "Text", + }, + start: { + type: "number", + minimum: 0, + title: "Start", + description: "Time in seconds with float part", + }, + end: { + type: "number", + minimum: 0, + title: "End", + description: "Time in seconds with float part", + }, + speaker: { + type: "integer", + title: "Speaker", + default: 0, + }, + }, + type: "object", + required: ["text", "start", "end"], + title: "Word", +} as const; diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index e69de29b..772dda30 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -0,0 +1,1043 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { CancelablePromise } from "./core/CancelablePromise"; +import type { BaseHttpRequest } from "./core/BaseHttpRequest"; +import type { + MetricsResponse, + V1MeetingAudioConsentData, + V1MeetingAudioConsentResponse, + V1RoomsListData, + V1RoomsListResponse, + V1RoomsCreateData, + V1RoomsCreateResponse, + V1RoomsUpdateData, + V1RoomsUpdateResponse, + V1RoomsDeleteData, + V1RoomsDeleteResponse, + V1RoomsCreateMeetingData, + V1RoomsCreateMeetingResponse, + V1RoomsSyncIcsData, + V1RoomsSyncIcsResponse, + V1RoomsIcsStatusData, + V1RoomsIcsStatusResponse, + V1RoomsListMeetingsData, + V1RoomsListMeetingsResponse, + V1RoomsListUpcomingMeetingsData, + V1RoomsListUpcomingMeetingsResponse, + V1RoomsListActiveMeetingsData, + V1RoomsListActiveMeetingsResponse, + V1RoomsJoinMeetingData, + V1RoomsJoinMeetingResponse, + V1TranscriptsListData, + V1TranscriptsListResponse, + V1TranscriptsCreateData, + V1TranscriptsCreateResponse, + V1TranscriptsSearchData, + V1TranscriptsSearchResponse, + V1TranscriptGetData, + V1TranscriptGetResponse, + V1TranscriptUpdateData, + V1TranscriptUpdateResponse, + V1TranscriptDeleteData, + V1TranscriptDeleteResponse, + V1TranscriptGetTopicsData, + V1TranscriptGetTopicsResponse, + V1TranscriptGetTopicsWithWordsData, + V1TranscriptGetTopicsWithWordsResponse, + V1TranscriptGetTopicsWithWordsPerSpeakerData, + V1TranscriptGetTopicsWithWordsPerSpeakerResponse, + V1TranscriptPostToZulipData, + V1TranscriptPostToZulipResponse, + V1TranscriptHeadAudioMp3Data, + V1TranscriptHeadAudioMp3Response, + V1TranscriptGetAudioMp3Data, + V1TranscriptGetAudioMp3Response, + V1TranscriptGetAudioWaveformData, + V1TranscriptGetAudioWaveformResponse, + V1TranscriptGetParticipantsData, + V1TranscriptGetParticipantsResponse, + V1TranscriptAddParticipantData, + V1TranscriptAddParticipantResponse, + V1TranscriptGetParticipantData, + V1TranscriptGetParticipantResponse, + V1TranscriptUpdateParticipantData, + V1TranscriptUpdateParticipantResponse, + V1TranscriptDeleteParticipantData, + V1TranscriptDeleteParticipantResponse, + V1TranscriptAssignSpeakerData, + V1TranscriptAssignSpeakerResponse, + V1TranscriptMergeSpeakerData, + V1TranscriptMergeSpeakerResponse, + V1TranscriptRecordUploadData, + V1TranscriptRecordUploadResponse, + V1TranscriptGetWebsocketEventsData, + V1TranscriptGetWebsocketEventsResponse, + V1TranscriptRecordWebrtcData, + V1TranscriptRecordWebrtcResponse, + V1TranscriptProcessData, + V1TranscriptProcessResponse, + V1UserMeResponse, + V1ZulipGetStreamsResponse, + V1ZulipGetTopicsData, + V1ZulipGetTopicsResponse, + V1WherebyWebhookData, + V1WherebyWebhookResponse, +} from "./types.gen"; + +export class DefaultService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + + /** + * Metrics + * Endpoint that serves Prometheus metrics. + * @returns unknown Successful Response + * @throws ApiError + */ + public metrics(): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/metrics", + }); + } + + /** + * Meeting Audio Consent + * @param data The data for the request. + * @param data.meetingId + * @param data.requestBody + * @returns unknown Successful Response + * @throws ApiError + */ + public v1MeetingAudioConsent( + data: V1MeetingAudioConsentData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/meetings/{meeting_id}/consent", + path: { + meeting_id: data.meetingId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms List + * @param data The data for the request. + * @param data.page Page number + * @param data.size Page size + * @returns Page_Room_ Successful Response + * @throws ApiError + */ + public v1RoomsList( + data: V1RoomsListData = {}, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms", + query: { + page: data.page, + size: data.size, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Create + * @param data The data for the request. + * @param data.requestBody + * @returns Room Successful Response + * @throws ApiError + */ + public v1RoomsCreate( + data: V1RoomsCreateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms", + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Update + * @param data The data for the request. + * @param data.roomId + * @param data.requestBody + * @returns Room Successful Response + * @throws ApiError + */ + public v1RoomsUpdate( + data: V1RoomsUpdateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/rooms/{room_id}", + path: { + room_id: data.roomId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Delete + * @param data The data for the request. + * @param data.roomId + * @returns DeletionStatus Successful Response + * @throws ApiError + */ + public v1RoomsDelete( + data: V1RoomsDeleteData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "DELETE", + url: "/v1/rooms/{room_id}", + path: { + room_id: data.roomId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Create Meeting + * @param data The data for the request. + * @param data.roomName + * @returns Meeting Successful Response + * @throws ApiError + */ + public v1RoomsCreateMeeting( + data: V1RoomsCreateMeetingData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms/{room_name}/meeting", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Sync Ics + * @param data The data for the request. + * @param data.roomName + * @returns ICSSyncResult Successful Response + * @throws ApiError + */ + public v1RoomsSyncIcs( + data: V1RoomsSyncIcsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms/{room_name}/ics/sync", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Ics Status + * @param data The data for the request. + * @param data.roomName + * @returns ICSStatus Successful Response + * @throws ApiError + */ + public v1RoomsIcsStatus( + data: V1RoomsIcsStatusData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms/{room_name}/ics/status", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms List Meetings + * @param data The data for the request. + * @param data.roomName + * @returns CalendarEventResponse Successful Response + * @throws ApiError + */ + public v1RoomsListMeetings( + data: V1RoomsListMeetingsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms/{room_name}/meetings", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms List Upcoming Meetings + * @param data The data for the request. + * @param data.roomName + * @param data.minutesAhead + * @returns CalendarEventResponse Successful Response + * @throws ApiError + */ + public v1RoomsListUpcomingMeetings( + data: V1RoomsListUpcomingMeetingsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms/{room_name}/meetings/upcoming", + path: { + room_name: data.roomName, + }, + query: { + minutes_ahead: data.minutesAhead, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms List Active Meetings + * List all active meetings for a room (supports multiple active meetings) + * @param data The data for the request. + * @param data.roomName + * @returns Meeting Successful Response + * @throws ApiError + */ + public v1RoomsListActiveMeetings( + data: V1RoomsListActiveMeetingsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/rooms/{room_name}/meetings/active", + path: { + room_name: data.roomName, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Rooms Join Meeting + * Join a specific meeting by ID + * @param data The data for the request. + * @param data.roomName + * @param data.meetingId + * @returns Meeting Successful Response + * @throws ApiError + */ + public v1RoomsJoinMeeting( + data: V1RoomsJoinMeetingData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/rooms/{room_name}/meetings/{meeting_id}/join", + path: { + room_name: data.roomName, + meeting_id: data.meetingId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcripts List + * @param data The data for the request. + * @param data.sourceKind + * @param data.roomId + * @param data.searchTerm + * @param data.page Page number + * @param data.size Page size + * @returns Page_GetTranscriptMinimal_ Successful Response + * @throws ApiError + */ + public v1TranscriptsList( + data: V1TranscriptsListData = {}, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts", + query: { + source_kind: data.sourceKind, + room_id: data.roomId, + search_term: data.searchTerm, + page: data.page, + size: data.size, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcripts Create + * @param data The data for the request. + * @param data.requestBody + * @returns GetTranscript Successful Response + * @throws ApiError + */ + public v1TranscriptsCreate( + data: V1TranscriptsCreateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts", + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcripts Search + * Full-text search across transcript titles and content. + * @param data The data for the request. + * @param data.q Search query text + * @param data.limit Results per page + * @param data.offset Number of results to skip + * @param data.roomId + * @returns SearchResponse Successful Response + * @throws ApiError + */ + public v1TranscriptsSearch( + data: V1TranscriptsSearchData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/search", + query: { + q: data.q, + limit: data.limit, + offset: data.offset, + room_id: data.roomId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get + * @param data The data for the request. + * @param data.transcriptId + * @returns GetTranscript Successful Response + * @throws ApiError + */ + public v1TranscriptGet( + data: V1TranscriptGetData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Update + * @param data The data for the request. + * @param data.transcriptId + * @param data.requestBody + * @returns GetTranscript Successful Response + * @throws ApiError + */ + public v1TranscriptUpdate( + data: V1TranscriptUpdateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/transcripts/{transcript_id}", + path: { + transcript_id: data.transcriptId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Delete + * @param data The data for the request. + * @param data.transcriptId + * @returns DeletionStatus Successful Response + * @throws ApiError + */ + public v1TranscriptDelete( + data: V1TranscriptDeleteData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "DELETE", + url: "/v1/transcripts/{transcript_id}", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Topics + * @param data The data for the request. + * @param data.transcriptId + * @returns GetTranscriptTopic Successful Response + * @throws ApiError + */ + public v1TranscriptGetTopics( + data: V1TranscriptGetTopicsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/topics", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Topics With Words + * @param data The data for the request. + * @param data.transcriptId + * @returns GetTranscriptTopicWithWords Successful Response + * @throws ApiError + */ + public v1TranscriptGetTopicsWithWords( + data: V1TranscriptGetTopicsWithWordsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/topics/with-words", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Topics With Words Per Speaker + * @param data The data for the request. + * @param data.transcriptId + * @param data.topicId + * @returns GetTranscriptTopicWithWordsPerSpeaker Successful Response + * @throws ApiError + */ + public v1TranscriptGetTopicsWithWordsPerSpeaker( + data: V1TranscriptGetTopicsWithWordsPerSpeakerData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker", + path: { + transcript_id: data.transcriptId, + topic_id: data.topicId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Post To Zulip + * @param data The data for the request. + * @param data.transcriptId + * @param data.stream + * @param data.topic + * @param data.includeTopics + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptPostToZulip( + data: V1TranscriptPostToZulipData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts/{transcript_id}/zulip", + path: { + transcript_id: data.transcriptId, + }, + query: { + stream: data.stream, + topic: data.topic, + include_topics: data.includeTopics, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Audio Mp3 + * @param data The data for the request. + * @param data.transcriptId + * @param data.token + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptHeadAudioMp3( + data: V1TranscriptHeadAudioMp3Data, + ): CancelablePromise { + return this.httpRequest.request({ + method: "HEAD", + url: "/v1/transcripts/{transcript_id}/audio/mp3", + path: { + transcript_id: data.transcriptId, + }, + query: { + token: data.token, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Audio Mp3 + * @param data The data for the request. + * @param data.transcriptId + * @param data.token + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptGetAudioMp3( + data: V1TranscriptGetAudioMp3Data, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/audio/mp3", + path: { + transcript_id: data.transcriptId, + }, + query: { + token: data.token, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Audio Waveform + * @param data The data for the request. + * @param data.transcriptId + * @returns AudioWaveform Successful Response + * @throws ApiError + */ + public v1TranscriptGetAudioWaveform( + data: V1TranscriptGetAudioWaveformData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/audio/waveform", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Participants + * @param data The data for the request. + * @param data.transcriptId + * @returns Participant Successful Response + * @throws ApiError + */ + public v1TranscriptGetParticipants( + data: V1TranscriptGetParticipantsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/participants", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Add Participant + * @param data The data for the request. + * @param data.transcriptId + * @param data.requestBody + * @returns Participant Successful Response + * @throws ApiError + */ + public v1TranscriptAddParticipant( + data: V1TranscriptAddParticipantData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts/{transcript_id}/participants", + path: { + transcript_id: data.transcriptId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Participant + * @param data The data for the request. + * @param data.transcriptId + * @param data.participantId + * @returns Participant Successful Response + * @throws ApiError + */ + public v1TranscriptGetParticipant( + data: V1TranscriptGetParticipantData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", + path: { + transcript_id: data.transcriptId, + participant_id: data.participantId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Update Participant + * @param data The data for the request. + * @param data.transcriptId + * @param data.participantId + * @param data.requestBody + * @returns Participant Successful Response + * @throws ApiError + */ + public v1TranscriptUpdateParticipant( + data: V1TranscriptUpdateParticipantData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", + path: { + transcript_id: data.transcriptId, + participant_id: data.participantId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Delete Participant + * @param data The data for the request. + * @param data.transcriptId + * @param data.participantId + * @returns DeletionStatus Successful Response + * @throws ApiError + */ + public v1TranscriptDeleteParticipant( + data: V1TranscriptDeleteParticipantData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "DELETE", + url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", + path: { + transcript_id: data.transcriptId, + participant_id: data.participantId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Assign Speaker + * @param data The data for the request. + * @param data.transcriptId + * @param data.requestBody + * @returns SpeakerAssignmentStatus Successful Response + * @throws ApiError + */ + public v1TranscriptAssignSpeaker( + data: V1TranscriptAssignSpeakerData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/transcripts/{transcript_id}/speaker/assign", + path: { + transcript_id: data.transcriptId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Merge Speaker + * @param data The data for the request. + * @param data.transcriptId + * @param data.requestBody + * @returns SpeakerAssignmentStatus Successful Response + * @throws ApiError + */ + public v1TranscriptMergeSpeaker( + data: V1TranscriptMergeSpeakerData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/transcripts/{transcript_id}/speaker/merge", + path: { + transcript_id: data.transcriptId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Record Upload + * @param data The data for the request. + * @param data.transcriptId + * @param data.chunkNumber + * @param data.totalChunks + * @param data.formData + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptRecordUpload( + data: V1TranscriptRecordUploadData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts/{transcript_id}/record/upload", + path: { + transcript_id: data.transcriptId, + }, + query: { + chunk_number: data.chunkNumber, + total_chunks: data.totalChunks, + }, + formData: data.formData, + mediaType: "multipart/form-data", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Get Websocket Events + * @param data The data for the request. + * @param data.transcriptId + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptGetWebsocketEvents( + data: V1TranscriptGetWebsocketEventsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/transcripts/{transcript_id}/events", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Record Webrtc + * @param data The data for the request. + * @param data.transcriptId + * @param data.requestBody + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptRecordWebrtc( + data: V1TranscriptRecordWebrtcData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts/{transcript_id}/record/webrtc", + path: { + transcript_id: data.transcriptId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Transcript Process + * @param data The data for the request. + * @param data.transcriptId + * @returns unknown Successful Response + * @throws ApiError + */ + public v1TranscriptProcess( + data: V1TranscriptProcessData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/transcripts/{transcript_id}/process", + path: { + transcript_id: data.transcriptId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * User Me + * @returns unknown Successful Response + * @throws ApiError + */ + public v1UserMe(): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/me", + }); + } + + /** + * Zulip Get Streams + * Get all Zulip streams. + * @returns Stream Successful Response + * @throws ApiError + */ + public v1ZulipGetStreams(): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/zulip/streams", + }); + } + + /** + * Zulip Get Topics + * Get all topics for a specific Zulip stream. + * @param data The data for the request. + * @param data.streamId + * @returns Topic Successful Response + * @throws ApiError + */ + public v1ZulipGetTopics( + data: V1ZulipGetTopicsData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "GET", + url: "/v1/zulip/streams/{stream_id}/topics", + path: { + stream_id: data.streamId, + }, + errors: { + 422: "Validation Error", + }, + }); + } + + /** + * Whereby Webhook + * @param data The data for the request. + * @param data.requestBody + * @returns unknown Successful Response + * @throws ApiError + */ + public v1WherebyWebhook( + data: V1WherebyWebhookData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "POST", + url: "/v1/whereby", + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } +} diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index e69de29b..08c1f201 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -0,0 +1,1243 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AudioWaveform = { + data: Array; +}; + +export type Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post = + { + chunk: Blob | File; + }; + +export type CalendarEventResponse = { + id: string; + room_id: string; + ics_uid: string; + title?: string | null; + description?: string | null; + start_time: string; + end_time: string; + attendees?: Array<{ + [key: string]: unknown; + }> | null; + location?: string | null; + last_synced: string; + created_at: string; + updated_at: string; +}; + +export type CreateParticipant = { + speaker?: number | null; + name: string; +}; + +export type CreateRoom = { + name: string; + zulip_auto_post: boolean; + zulip_stream: string; + zulip_topic: string; + is_locked: boolean; + room_mode: string; + recording_type: string; + recording_trigger: string; + is_shared: boolean; + ics_url?: string | null; + ics_fetch_interval?: number; + ics_enabled?: boolean; +}; + +export type CreateTranscript = { + name: string; + source_language?: string; + target_language?: string; +}; + +export type DeletionStatus = { + status: string; +}; + +export type GetTranscript = { + id: string; + user_id: string | null; + name: string; + status: string; + locked: boolean; + duration: number; + title: string | null; + short_summary: string | null; + long_summary: string | null; + created_at: string; + share_mode?: string; + source_language: string | null; + target_language: string | null; + reviewed: boolean; + meeting_id: string | null; + source_kind: SourceKind; + room_id?: string | null; + room_name?: string | null; + audio_deleted?: boolean | null; + participants: Array | null; +}; + +export type GetTranscriptMinimal = { + id: string; + user_id: string | null; + name: string; + status: string; + locked: boolean; + duration: number; + title: string | null; + short_summary: string | null; + long_summary: string | null; + created_at: string; + share_mode?: string; + source_language: string | null; + target_language: string | null; + reviewed: boolean; + meeting_id: string | null; + source_kind: SourceKind; + room_id?: string | null; + room_name?: string | null; + audio_deleted?: boolean | null; +}; + +export type GetTranscriptSegmentTopic = { + text: string; + start: number; + speaker: number; +}; + +export type GetTranscriptTopic = { + id: string; + title: string; + summary: string; + timestamp: number; + duration: number | null; + transcript: string; + segments?: Array; +}; + +export type GetTranscriptTopicWithWords = { + id: string; + title: string; + summary: string; + timestamp: number; + duration: number | null; + transcript: string; + segments?: Array; + words?: Array; +}; + +export type GetTranscriptTopicWithWordsPerSpeaker = { + id: string; + title: string; + summary: string; + timestamp: number; + duration: number | null; + transcript: string; + segments?: Array; + words_per_speaker?: Array; +}; + +export type HTTPValidationError = { + detail?: Array; +}; + +export type ICSStatus = { + status: string; + last_sync?: string | null; + next_sync?: string | null; + last_etag?: string | null; + events_count?: number; +}; + +export type ICSSyncResult = { + status: string; + hash?: string | null; + events_found?: number; + events_created?: number; + events_updated?: number; + events_deleted?: number; + error?: string | null; +}; + +export type Meeting = { + id: string; + room_name: string; + room_url: string; + host_room_url: string; + start_date: string; + end_date: string; + recording_type?: "none" | "local" | "cloud"; +}; + +export type recording_type = "none" | "local" | "cloud"; + +export type MeetingConsentRequest = { + consent_given: boolean; +}; + +export type Page_GetTranscriptMinimal_ = { + items: Array; + total?: number | null; + page: number | null; + size: number | null; + pages?: number | null; +}; + +export type Page_Room_ = { + items: Array; + total?: number | null; + page: number | null; + size: number | null; + pages?: number | null; +}; + +export type Participant = { + id: string; + speaker: number | null; + name: string; +}; + +export type Room = { + id: string; + name: string; + user_id: string; + created_at: string; + zulip_auto_post: boolean; + zulip_stream: string; + zulip_topic: string; + is_locked: boolean; + room_mode: string; + recording_type: string; + recording_trigger: string; + is_shared: boolean; + ics_url?: string | null; + ics_fetch_interval?: number; + ics_enabled?: boolean; + ics_last_sync?: string | null; + ics_last_etag?: string | null; +}; + +export type RtcOffer = { + sdp: string; + type: string; +}; + +export type SearchResponse = { + results: Array; + /** + * Total number of search results + */ + total: number; + /** + * Search query text + */ + query: string; + /** + * Results per page + */ + limit: number; + /** + * Number of results to skip + */ + offset: number; +}; + +/** + * Public search result model with computed fields. + */ +export type SearchResult = { + id: string; + title?: string | null; + user_id?: string | null; + room_id?: string | null; + created_at: string; + status: string; + rank: number; + /** + * Duration in seconds + */ + duration: number | null; + /** + * Text snippets around search matches + */ + search_snippets: Array; +}; + +export type SourceKind = "room" | "live" | "file"; + +export type SpeakerAssignment = { + speaker?: number | null; + participant?: string | null; + timestamp_from: number; + timestamp_to: number; +}; + +export type SpeakerAssignmentStatus = { + status: string; +}; + +export type SpeakerMerge = { + speaker_from: number; + speaker_to: number; +}; + +export type SpeakerWords = { + speaker: number; + words: Array; +}; + +export type Stream = { + stream_id: number; + name: string; +}; + +export type Topic = { + name: string; +}; + +export type TranscriptParticipant = { + id?: string; + speaker: number | null; + name: string; +}; + +export type UpdateParticipant = { + speaker?: number | null; + name?: string | null; +}; + +export type UpdateRoom = { + name?: string | null; + zulip_auto_post?: boolean | null; + zulip_stream?: string | null; + zulip_topic?: string | null; + is_locked?: boolean | null; + room_mode?: string | null; + recording_type?: string | null; + recording_trigger?: string | null; + is_shared?: boolean | null; + ics_url?: string | null; + ics_fetch_interval?: number | null; + ics_enabled?: boolean | null; +}; + +export type UpdateTranscript = { + name?: string | null; + locked?: boolean | null; + title?: string | null; + short_summary?: string | null; + long_summary?: string | null; + share_mode?: "public" | "semi-private" | "private" | null; + participants?: Array | null; + reviewed?: boolean | null; + audio_deleted?: boolean | null; +}; + +export type UserInfo = { + sub: string; + email: string | null; + email_verified: boolean | null; +}; + +export type ValidationError = { + loc: Array; + msg: string; + type: string; +}; + +export type WherebyWebhookEvent = { + apiVersion: string; + id: string; + createdAt: string; + type: string; + data: { + [key: string]: unknown; + }; +}; + +export type Word = { + text: string; + /** + * Time in seconds with float part + */ + start: number; + /** + * Time in seconds with float part + */ + end: number; + speaker?: number; +}; + +export type MetricsResponse = unknown; + +export type V1MeetingAudioConsentData = { + meetingId: string; + requestBody: MeetingConsentRequest; +}; + +export type V1MeetingAudioConsentResponse = unknown; + +export type V1RoomsListData = { + /** + * Page number + */ + page?: number; + /** + * Page size + */ + size?: number; +}; + +export type V1RoomsListResponse = Page_Room_; + +export type V1RoomsCreateData = { + requestBody: CreateRoom; +}; + +export type V1RoomsCreateResponse = Room; + +export type V1RoomsUpdateData = { + requestBody: UpdateRoom; + roomId: string; +}; + +export type V1RoomsUpdateResponse = Room; + +export type V1RoomsDeleteData = { + roomId: string; +}; + +export type V1RoomsDeleteResponse = DeletionStatus; + +export type V1RoomsCreateMeetingData = { + roomName: string; +}; + +export type V1RoomsCreateMeetingResponse = Meeting; + +export type V1RoomsSyncIcsData = { + roomName: string; +}; + +export type V1RoomsSyncIcsResponse = ICSSyncResult; + +export type V1RoomsIcsStatusData = { + roomName: string; +}; + +export type V1RoomsIcsStatusResponse = ICSStatus; + +export type V1RoomsListMeetingsData = { + roomName: string; +}; + +export type V1RoomsListMeetingsResponse = Array; + +export type V1RoomsListUpcomingMeetingsData = { + minutesAhead?: number; + roomName: string; +}; + +export type V1RoomsListUpcomingMeetingsResponse = Array; + +export type V1RoomsListActiveMeetingsData = { + roomName: string; +}; + +export type V1RoomsListActiveMeetingsResponse = Array; + +export type V1RoomsJoinMeetingData = { + meetingId: string; + roomName: string; +}; + +export type V1RoomsJoinMeetingResponse = Meeting; + +export type V1TranscriptsListData = { + /** + * Page number + */ + page?: number; + roomId?: string | null; + searchTerm?: string | null; + /** + * Page size + */ + size?: number; + sourceKind?: SourceKind | null; +}; + +export type V1TranscriptsListResponse = Page_GetTranscriptMinimal_; + +export type V1TranscriptsCreateData = { + requestBody: CreateTranscript; +}; + +export type V1TranscriptsCreateResponse = GetTranscript; + +export type V1TranscriptsSearchData = { + /** + * Results per page + */ + limit?: number; + /** + * Number of results to skip + */ + offset?: number; + /** + * Search query text + */ + q: string; + roomId?: string | null; +}; + +export type V1TranscriptsSearchResponse = SearchResponse; + +export type V1TranscriptGetData = { + transcriptId: string; +}; + +export type V1TranscriptGetResponse = GetTranscript; + +export type V1TranscriptUpdateData = { + requestBody: UpdateTranscript; + transcriptId: string; +}; + +export type V1TranscriptUpdateResponse = GetTranscript; + +export type V1TranscriptDeleteData = { + transcriptId: string; +}; + +export type V1TranscriptDeleteResponse = DeletionStatus; + +export type V1TranscriptGetTopicsData = { + transcriptId: string; +}; + +export type V1TranscriptGetTopicsResponse = Array; + +export type V1TranscriptGetTopicsWithWordsData = { + transcriptId: string; +}; + +export type V1TranscriptGetTopicsWithWordsResponse = + Array; + +export type V1TranscriptGetTopicsWithWordsPerSpeakerData = { + topicId: string; + transcriptId: string; +}; + +export type V1TranscriptGetTopicsWithWordsPerSpeakerResponse = + GetTranscriptTopicWithWordsPerSpeaker; + +export type V1TranscriptPostToZulipData = { + includeTopics: boolean; + stream: string; + topic: string; + transcriptId: string; +}; + +export type V1TranscriptPostToZulipResponse = unknown; + +export type V1TranscriptHeadAudioMp3Data = { + token?: string | null; + transcriptId: string; +}; + +export type V1TranscriptHeadAudioMp3Response = unknown; + +export type V1TranscriptGetAudioMp3Data = { + token?: string | null; + transcriptId: string; +}; + +export type V1TranscriptGetAudioMp3Response = unknown; + +export type V1TranscriptGetAudioWaveformData = { + transcriptId: string; +}; + +export type V1TranscriptGetAudioWaveformResponse = AudioWaveform; + +export type V1TranscriptGetParticipantsData = { + transcriptId: string; +}; + +export type V1TranscriptGetParticipantsResponse = Array; + +export type V1TranscriptAddParticipantData = { + requestBody: CreateParticipant; + transcriptId: string; +}; + +export type V1TranscriptAddParticipantResponse = Participant; + +export type V1TranscriptGetParticipantData = { + participantId: string; + transcriptId: string; +}; + +export type V1TranscriptGetParticipantResponse = Participant; + +export type V1TranscriptUpdateParticipantData = { + participantId: string; + requestBody: UpdateParticipant; + transcriptId: string; +}; + +export type V1TranscriptUpdateParticipantResponse = Participant; + +export type V1TranscriptDeleteParticipantData = { + participantId: string; + transcriptId: string; +}; + +export type V1TranscriptDeleteParticipantResponse = DeletionStatus; + +export type V1TranscriptAssignSpeakerData = { + requestBody: SpeakerAssignment; + transcriptId: string; +}; + +export type V1TranscriptAssignSpeakerResponse = SpeakerAssignmentStatus; + +export type V1TranscriptMergeSpeakerData = { + requestBody: SpeakerMerge; + transcriptId: string; +}; + +export type V1TranscriptMergeSpeakerResponse = SpeakerAssignmentStatus; + +export type V1TranscriptRecordUploadData = { + chunkNumber: number; + formData: Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post; + totalChunks: number; + transcriptId: string; +}; + +export type V1TranscriptRecordUploadResponse = unknown; + +export type V1TranscriptGetWebsocketEventsData = { + transcriptId: string; +}; + +export type V1TranscriptGetWebsocketEventsResponse = unknown; + +export type V1TranscriptRecordWebrtcData = { + requestBody: RtcOffer; + transcriptId: string; +}; + +export type V1TranscriptRecordWebrtcResponse = unknown; + +export type V1TranscriptProcessData = { + transcriptId: string; +}; + +export type V1TranscriptProcessResponse = unknown; + +export type V1UserMeResponse = UserInfo | null; + +export type V1ZulipGetStreamsResponse = Array; + +export type V1ZulipGetTopicsData = { + streamId: number; +}; + +export type V1ZulipGetTopicsResponse = Array; + +export type V1WherebyWebhookData = { + requestBody: WherebyWebhookEvent; +}; + +export type V1WherebyWebhookResponse = unknown; + +export type $OpenApiTs = { + "/metrics": { + get: { + res: { + /** + * Successful Response + */ + 200: unknown; + }; + }; + }; + "/v1/meetings/{meeting_id}/consent": { + post: { + req: V1MeetingAudioConsentData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms": { + get: { + req: V1RoomsListData; + res: { + /** + * Successful Response + */ + 200: Page_Room_; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + post: { + req: V1RoomsCreateData; + res: { + /** + * Successful Response + */ + 200: Room; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_id}": { + patch: { + req: V1RoomsUpdateData; + res: { + /** + * Successful Response + */ + 200: Room; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + delete: { + req: V1RoomsDeleteData; + res: { + /** + * Successful Response + */ + 200: DeletionStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meeting": { + post: { + req: V1RoomsCreateMeetingData; + res: { + /** + * Successful Response + */ + 200: Meeting; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/ics/sync": { + post: { + req: V1RoomsSyncIcsData; + res: { + /** + * Successful Response + */ + 200: ICSSyncResult; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/ics/status": { + get: { + req: V1RoomsIcsStatusData; + res: { + /** + * Successful Response + */ + 200: ICSStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meetings": { + get: { + req: V1RoomsListMeetingsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meetings/upcoming": { + get: { + req: V1RoomsListUpcomingMeetingsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meetings/active": { + get: { + req: V1RoomsListActiveMeetingsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/rooms/{room_name}/meetings/{meeting_id}/join": { + post: { + req: V1RoomsJoinMeetingData; + res: { + /** + * Successful Response + */ + 200: Meeting; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts": { + get: { + req: V1TranscriptsListData; + res: { + /** + * Successful Response + */ + 200: Page_GetTranscriptMinimal_; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + post: { + req: V1TranscriptsCreateData; + res: { + /** + * Successful Response + */ + 200: GetTranscript; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/search": { + get: { + req: V1TranscriptsSearchData; + res: { + /** + * Successful Response + */ + 200: SearchResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}": { + get: { + req: V1TranscriptGetData; + res: { + /** + * Successful Response + */ + 200: GetTranscript; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + patch: { + req: V1TranscriptUpdateData; + res: { + /** + * Successful Response + */ + 200: GetTranscript; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + delete: { + req: V1TranscriptDeleteData; + res: { + /** + * Successful Response + */ + 200: DeletionStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/topics": { + get: { + req: V1TranscriptGetTopicsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/topics/with-words": { + get: { + req: V1TranscriptGetTopicsWithWordsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker": { + get: { + req: V1TranscriptGetTopicsWithWordsPerSpeakerData; + res: { + /** + * Successful Response + */ + 200: GetTranscriptTopicWithWordsPerSpeaker; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/zulip": { + post: { + req: V1TranscriptPostToZulipData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/audio/mp3": { + head: { + req: V1TranscriptHeadAudioMp3Data; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + get: { + req: V1TranscriptGetAudioMp3Data; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/audio/waveform": { + get: { + req: V1TranscriptGetAudioWaveformData; + res: { + /** + * Successful Response + */ + 200: AudioWaveform; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/participants": { + get: { + req: V1TranscriptGetParticipantsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + post: { + req: V1TranscriptAddParticipantData; + res: { + /** + * Successful Response + */ + 200: Participant; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/participants/{participant_id}": { + get: { + req: V1TranscriptGetParticipantData; + res: { + /** + * Successful Response + */ + 200: Participant; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + patch: { + req: V1TranscriptUpdateParticipantData; + res: { + /** + * Successful Response + */ + 200: Participant; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + delete: { + req: V1TranscriptDeleteParticipantData; + res: { + /** + * Successful Response + */ + 200: DeletionStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/speaker/assign": { + patch: { + req: V1TranscriptAssignSpeakerData; + res: { + /** + * Successful Response + */ + 200: SpeakerAssignmentStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/speaker/merge": { + patch: { + req: V1TranscriptMergeSpeakerData; + res: { + /** + * Successful Response + */ + 200: SpeakerAssignmentStatus; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/record/upload": { + post: { + req: V1TranscriptRecordUploadData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/events": { + get: { + req: V1TranscriptGetWebsocketEventsData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/record/webrtc": { + post: { + req: V1TranscriptRecordWebrtcData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/transcripts/{transcript_id}/process": { + post: { + req: V1TranscriptProcessData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/me": { + get: { + res: { + /** + * Successful Response + */ + 200: UserInfo | null; + }; + }; + }; + "/v1/zulip/streams": { + get: { + res: { + /** + * Successful Response + */ + 200: Array; + }; + }; + }; + "/v1/zulip/streams/{stream_id}/topics": { + get: { + req: V1ZulipGetTopicsData; + res: { + /** + * Successful Response + */ + 200: Array; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + "/v1/whereby": { + post: { + req: V1WherebyWebhookData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; +}; diff --git a/www/app/room/[roomName]/page.tsx b/www/app/room/[roomName]/page.tsx new file mode 100644 index 00000000..da0667f8 --- /dev/null +++ b/www/app/room/[roomName]/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Box, Spinner, VStack, Text } from "@chakra-ui/react"; +import { useRouter } from "next/navigation"; +import useApi from "../../lib/useApi"; +import useSessionStatus from "../../lib/useSessionStatus"; +import MeetingSelection from "../../[roomName]/MeetingSelection"; +import { Meeting, Room } from "../../api"; + +interface RoomPageProps { + params: { + roomName: string; + }; +} + +export default function RoomPage({ params }: RoomPageProps) { + const { roomName } = params; + const router = useRouter(); + const api = useApi(); + const { data: session } = useSessionStatus(); + + const [room, setRoom] = useState(null); + const [loading, setLoading] = useState(true); + const [checkingMeetings, setCheckingMeetings] = useState(false); + + const isOwner = session?.user?.id === room?.user_id; + + useEffect(() => { + if (!api) return; + + const fetchRoom = async () => { + try { + // Get room details + const roomData = await api.v1RoomsRetrieve({ roomName }); + setRoom(roomData); + + // Check if we should show meeting selection + if (roomData.ics_enabled) { + setCheckingMeetings(true); + + // Check for active meetings + const activeMeetings = await api.v1RoomsListActiveMeetings({ + roomName, + }); + + // Check for upcoming meetings + const upcomingEvents = await api.v1RoomsListUpcomingMeetings({ + roomName, + minutesAhead: 30, + }); + + // If there's only one active meeting and no upcoming, auto-join + if (activeMeetings.length === 1 && upcomingEvents.length === 0) { + handleMeetingSelect(activeMeetings[0]); + } else if ( + activeMeetings.length === 0 && + upcomingEvents.length === 0 + ) { + // No meetings, create unscheduled + handleCreateUnscheduled(); + } + // Otherwise, show selection UI (handled by render) + } else { + // ICS not enabled, use traditional flow + handleCreateUnscheduled(); + } + } catch (err) { + console.error("Failed to fetch room:", err); + // Room not found or error + router.push("/rooms"); + } finally { + setLoading(false); + setCheckingMeetings(false); + } + }; + + fetchRoom(); + }, [api, roomName]); + + const handleMeetingSelect = (meeting: Meeting) => { + // Navigate to the classic room page with the meeting + // Store meeting in session storage for the classic page to use + sessionStorage.setItem(`meeting_${roomName}`, JSON.stringify(meeting)); + router.push(`/${roomName}`); + }; + + const handleCreateUnscheduled = async () => { + if (!api) return; + + try { + // Create a new unscheduled meeting + const meeting = await api.v1RoomsCreateMeeting({ roomName }); + handleMeetingSelect(meeting); + } catch (err) { + console.error("Failed to create meeting:", err); + } + }; + + if (loading || checkingMeetings) { + return ( + + + + {loading ? "Loading room..." : "Checking meetings..."} + + + ); + } + + if (!room) { + return ( + + Room not found + + ); + } + + // Show meeting selection if ICS is enabled and we have multiple options + if (room.ics_enabled) { + return ( + + + + ); + } + + // Should not reach here - redirected above + return null; +} diff --git a/www/app/room/[roomName]/wait/page.tsx b/www/app/room/[roomName]/wait/page.tsx new file mode 100644 index 00000000..999ffe0b --- /dev/null +++ b/www/app/room/[roomName]/wait/page.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { + Box, + VStack, + HStack, + Text, + Spinner, + Progress, + Card, + CardBody, + Button, + Icon, +} from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { FaClock, FaArrowLeft } from "react-icons/fa"; +import useApi from "../../../lib/useApi"; +import { CalendarEventResponse } from "../../../api"; + +interface WaitingPageProps { + params: { + roomName: string; + }; +} + +export default function WaitingPage({ params }: WaitingPageProps) { + const { roomName } = params; + const router = useRouter(); + const searchParams = useSearchParams(); + const eventId = searchParams.get("eventId"); + + const [event, setEvent] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(0); + const [loading, setLoading] = useState(true); + const [checkingMeeting, setCheckingMeeting] = useState(false); + const api = useApi(); + + useEffect(() => { + if (!api || !eventId) return; + + const fetchEvent = async () => { + try { + const events = await api.v1RoomsListUpcomingMeetings({ + roomName, + minutesAhead: 60, + }); + + const targetEvent = events.find((e) => e.id === eventId); + if (targetEvent) { + setEvent(targetEvent); + } else { + // Event not found or already started + router.push(`/room/${roomName}`); + } + } catch (err) { + console.error("Failed to fetch event:", err); + router.push(`/room/${roomName}`); + } finally { + setLoading(false); + } + }; + + fetchEvent(); + }, [api, eventId, roomName]); + + useEffect(() => { + if (!event) return; + + const updateCountdown = () => { + const now = new Date(); + const start = new Date(event.start_time); + const diff = Math.max(0, start.getTime() - now.getTime()); + + setTimeRemaining(diff); + + // Check if meeting has started + if (diff <= 0) { + checkForActiveMeeting(); + } + }; + + const checkForActiveMeeting = async () => { + if (!api || checkingMeeting) return; + + setCheckingMeeting(true); + try { + // Check for active meetings + const activeMeetings = await api.v1RoomsListActiveMeetings({ + roomName, + }); + + // Find meeting for this calendar event + const calendarMeeting = activeMeetings.find( + (m) => m.calendar_event_id === eventId, + ); + + if (calendarMeeting) { + // Meeting is now active, join it + const meeting = await api.v1RoomsJoinMeeting({ + roomName, + meetingId: calendarMeeting.id, + }); + + // Navigate to the meeting room + router.push(`/${roomName}?meetingId=${meeting.id}`); + } + } catch (err) { + console.error("Failed to check for active meeting:", err); + } finally { + setCheckingMeeting(false); + } + }; + + // Update countdown every second + const interval = setInterval(updateCountdown, 1000); + + // Check for meeting every 10 seconds when close to start time + let checkInterval: NodeJS.Timeout | null = null; + if (timeRemaining < 60000) { + // Less than 1 minute + checkInterval = setInterval(checkForActiveMeeting, 10000); + } + + updateCountdown(); // Initial update + + return () => { + clearInterval(interval); + if (checkInterval) clearInterval(checkInterval); + }; + }, [event, api, eventId, roomName, checkingMeeting]); + + const formatTime = (ms: number) => { + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`; + } + return `${minutes}:${seconds.toString().padStart(2, "0")}`; + }; + + const getProgressValue = () => { + if (!event) return 0; + + const now = new Date(); + const created = new Date(event.created_at); + const start = new Date(event.start_time); + const totalTime = start.getTime() - created.getTime(); + const elapsed = now.getTime() - created.getTime(); + + return Math.min(100, (elapsed / totalTime) * 100); + }; + + if (loading) { + return ( + + + + Loading meeting details... + + + ); + } + + if (!event) { + return ( + + + Meeting not found + + + + ); + } + + return ( + + + + + + + + + {event.title || "Scheduled Meeting"} + + + The meeting will start automatically when it's time + + + + + + {formatTime(timeRemaining)} + + + + + {event.description && ( + + + Meeting Description + + + {event.description} + + + )} + + + + Scheduled for {new Date(event.start_time).toLocaleString()} + + + {checkingMeeting && ( + + + + Checking if meeting has started... + + + )} + + + + + + + + ); +}