@ai-matrx/meet 0.11.41 → 0.11.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -160,6 +160,8 @@ var MEET_ROUTES = {
160
160
  noteTakerStop: "/api/v1/meet/agent/stop",
161
161
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
162
162
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
163
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
164
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
163
165
  /** A host ends the meeting for everyone. Served by MRI-A6. */
164
166
  meetingEnd: "/api/v1/meet/end",
165
167
  /**
@@ -255,6 +257,31 @@ function recordingLabel(state) {
255
257
  }
256
258
  }
257
259
 
260
+ // src/core/live-policy.ts
261
+ var DEFAULT_LIVE_POLICY = {
262
+ chatMode: "everyone",
263
+ avLock: "off",
264
+ timeLimitMinutes: 0,
265
+ timeLimitWarningMinutes: 5,
266
+ recordingConsent: "notice"
267
+ };
268
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
269
+ function isLockedFor(policy, role, kind) {
270
+ if (MODERATORS.has(role)) return false;
271
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
272
+ }
273
+ function consentPending(policy, role, recordingOn, given) {
274
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
275
+ }
276
+ function canChat(policy, role) {
277
+ if (policy.chatMode === "everyone") return true;
278
+ return policy.chatMode === "hosts_only" && MODERATORS.has(role);
279
+ }
280
+ function minutesLeft(policy, startedAtMs, nowMs) {
281
+ if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
282
+ return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
283
+ }
284
+
258
285
  // src/core/attendance.ts
259
286
  var IDLE_MEDIA_REGION = {
260
287
  permission: { microphone: "unknown", camera: "unknown" },
@@ -347,9 +374,17 @@ function autoAssign(identities, rooms, exclude = []) {
347
374
  return out;
348
375
  }
349
376
  function breakoutRemainingMs(plan, now) {
350
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
351
- const end = Date.parse(plan.endsAt);
352
- return Number.isNaN(end) ? null : Math.max(0, end - now);
377
+ if (plan === null || plan.state !== "open") return null;
378
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
379
+ if (ends.length === 0) return null;
380
+ return Math.max(0, Math.min(...ends) - now);
381
+ }
382
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
383
+ function breakoutClosingMs(plan, now) {
384
+ if (plan === null || plan.state !== "open") return null;
385
+ const remaining = breakoutRemainingMs(plan, now);
386
+ if (remaining === null) return null;
387
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
353
388
  }
354
389
  function defaultCaptionLanguage(settings, viewerLanguage) {
355
390
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -557,11 +592,13 @@ function webMediaPlatform() {
557
592
 
558
593
  // src/core/observation.ts
559
594
  function observedRole(role, standing) {
560
- if (role === "host" || role === "cohost") return role;
595
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
561
596
  return isLinkFollower(standing) ? "guest" : "participant";
562
597
  }
563
- function observedDevice(track, permission = "unknown") {
598
+ function observedDevice(track, permission = "unknown", held = null) {
599
+ if (held === "locked") return "locked";
564
600
  if (track.enabled) return "on";
601
+ if (held === "muted_by_host") return "muted_by_host";
565
602
  const reason = deviceUnavailableReason(track, permission);
566
603
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
567
604
  }
@@ -598,10 +635,20 @@ function observe(input) {
598
635
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
599
636
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
600
637
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
601
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
602
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
638
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
639
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
603
640
  };
604
641
  }
642
+ function lockedFor(snapshot, kind, inCall) {
643
+ if (!inCall) return null;
644
+ const role = effectiveRole(snapshot);
645
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
646
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
647
+ return "locked";
648
+ }
649
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
650
+ return null;
651
+ }
605
652
  function staticObservation(phase, reason = null) {
606
653
  return {
607
654
  phase,
@@ -704,7 +751,7 @@ function exactNoteTakerParticipant(meeting, participants) {
704
751
  }
705
752
  function noteTakerStatus(args) {
706
753
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
707
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
754
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
708
755
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
709
756
  const recorded = readState(row["state"]);
710
757
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -1457,6 +1504,45 @@ function useMeetAi() {
1457
1504
  }, [ai, meeting, dispatchNoteTaker])
1458
1505
  };
1459
1506
  }
1507
+ function useCaptions() {
1508
+ const host = useMeetHost();
1509
+ const snapshot = useMeetSnapshot();
1510
+ const ai = useMeetAi();
1511
+ const on = snapshot?.captionsOn === true;
1512
+ const refusal = snapshot?.captionsRefusal ?? null;
1513
+ const taker = ai.noteTaker;
1514
+ const meeting = snapshot?.meeting ?? null;
1515
+ const request = useCallback2(() => {
1516
+ if (host === null || meeting === null) return;
1517
+ void host.ai.requestCaptions({ meetingId: meeting.id });
1518
+ }, [host, meeting]);
1519
+ const toggle = useCallback2(() => {
1520
+ if (host === null) return;
1521
+ const next = !on;
1522
+ host.store.patch({ captionsOn: next, captionsRefusal: null });
1523
+ if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
1524
+ }, [host, on, taker.state, request]);
1525
+ let state = "off";
1526
+ let reason = null;
1527
+ let remedy = null;
1528
+ if (on) {
1529
+ if (taker.state === "transcribing" && taker.present) {
1530
+ state = "live";
1531
+ } else if (taker.state === "joining") {
1532
+ state = "starting";
1533
+ } else if (refusal !== null && refusal.reason.length > 0) {
1534
+ state = "unavailable";
1535
+ reason = refusal.reason;
1536
+ remedy = refusal.remedy.length > 0 ? refusal.remedy : null;
1537
+ } else if (taker.state === "ended" || taker.state === "handoff_pending") {
1538
+ state = "unavailable";
1539
+ reason = noteTakerLabel(taker);
1540
+ } else {
1541
+ state = "starting";
1542
+ }
1543
+ }
1544
+ return { on, state, reason, remedy, toggle };
1545
+ }
1460
1546
  function useEndMeeting() {
1461
1547
  const host = useMeetHost();
1462
1548
  const snapshot = useMeetSnapshot();
@@ -1494,7 +1580,7 @@ function useConsentNotice() {
1494
1580
  const snapshot = useMeetSnapshot();
1495
1581
  const agent = noteTakerParticipant(snapshot?.participants ?? []);
1496
1582
  if (agent === null) return null;
1497
- return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
1583
+ return `AI notes are on. ${agent.displayName} is in this meeting, transcribing what is said and writing notes everyone here can read.`;
1498
1584
  }
1499
1585
  function useMediaSupervisor() {
1500
1586
  const host = useMeetHost();
@@ -1742,6 +1828,38 @@ function useGuestRecordAccess(meeting) {
1742
1828
  )
1743
1829
  };
1744
1830
  }
1831
+ function useLivePolicy() {
1832
+ const snapshot = useMeetSnapshot();
1833
+ const role = useRole();
1834
+ const [now, setNow] = useState2(() => Date.now());
1835
+ const limited = (snapshot?.livePolicy.timeLimitMinutes ?? 0) > 0;
1836
+ useEffect3(() => {
1837
+ if (!limited) return void 0;
1838
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1839
+ return () => clearInterval(timer);
1840
+ }, [limited]);
1841
+ const policy = snapshot?.livePolicy ?? DEFAULT_LIVE_POLICY;
1842
+ const left = minutesLeft(policy, snapshot?.sessionStartedAt ?? null, now);
1843
+ const host = useMeetHost();
1844
+ const pending = consentPending(policy, role, snapshot?.recording === "recording", snapshot?.recordingConsentGiven === true);
1845
+ const viewOnly = pending && policy.recordingConsent === "view_only_until_agree";
1846
+ const idle = snapshot !== null && snapshot.recording !== "recording" && snapshot.recording !== "starting" && snapshot.recording !== "stopping";
1847
+ const given = snapshot?.recordingConsentGiven === true;
1848
+ const store = host?.store ?? null;
1849
+ useEffect3(() => {
1850
+ if (idle && given) store?.patch({ recordingConsentGiven: false });
1851
+ }, [idle, given, store]);
1852
+ return {
1853
+ canChat: canChat(policy, role),
1854
+ chatMode: policy.chatMode,
1855
+ micLocked: isLockedFor(policy, role, "microphone") || viewOnly,
1856
+ cameraLocked: isLockedFor(policy, role, "camera") || viewOnly,
1857
+ consent: pending ? policy.recordingConsent === "ok_or_leave" ? "ok_or_leave" : "view_only_until_agree" : null,
1858
+ agree: () => store?.patch({ recordingConsentGiven: true }),
1859
+ minutesLeft: left,
1860
+ timeWarning: left !== null && left <= policy.timeLimitWarningMinutes
1861
+ };
1862
+ }
1745
1863
 
1746
1864
  // src/react/collab-hooks.ts
1747
1865
  import { useEffect as useEffect4, useRef as useRef4 } from "react";
@@ -1957,6 +2075,18 @@ function SparklesIcon(props) {
1957
2075
  /* @__PURE__ */ jsx4("path", { d: "M19 15l.9 2.1 2.1.9-2.1.9-.9 2.1-.9-2.1-2.1-.9 2.1-.9L19 15Z" })
1958
2076
  ] });
1959
2077
  }
2078
+ function CaptionsIcon(props) {
2079
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
2080
+ /* @__PURE__ */ jsx4("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
2081
+ /* @__PURE__ */ jsx4("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" })
2082
+ ] });
2083
+ }
2084
+ function NotesIcon(props) {
2085
+ return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
2086
+ /* @__PURE__ */ jsx4("path", { d: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1Z" }),
2087
+ /* @__PURE__ */ jsx4("path", { d: "M14 7l3 3" })
2088
+ ] });
2089
+ }
1960
2090
  function LockIcon(props) {
1961
2091
  return /* @__PURE__ */ jsxs(Icon, { ...props, children: [
1962
2092
  /* @__PURE__ */ jsx4("rect", { x: "4", y: "10", width: "16", height: "11", rx: "2" }),
@@ -2875,7 +3005,7 @@ function BreakoutRun({ plan }) {
2875
3005
  type: "button",
2876
3006
  className: "mx-meet__primary mx-meet__primary--small",
2877
3007
  onClick: () => void act({ action: "breakout_close" }).then(show),
2878
- children: "Bring everyone back"
3008
+ children: "Close rooms"
2879
3009
  }
2880
3010
  ) : null
2881
3011
  ] });
@@ -2893,9 +3023,14 @@ function BreakoutBanner() {
2893
3023
  const { collab, act, moveTo, localIdentity, breakoutRoomId } = useCollab();
2894
3024
  const plan = collab?.breakout ?? null;
2895
3025
  const open = plan?.state === "open" ? plan : null;
2896
- const now = useNow(open?.endsAt != null);
3026
+ const now = useNow(open?.endsAt != null || open?.closingAt != null);
2897
3027
  const remaining = breakoutRemainingMs(open, now);
3028
+ const closing = breakoutClosingMs(open, now);
2898
3029
  const assigned = assignedRoom(open, localIdentity);
3030
+ const [later, setLater] = useState3(null);
3031
+ const [autoSince, setAutoSince] = useState3(null);
3032
+ const AUTO_MOVE_MS = 1e4;
3033
+ const autoLeft = open?.moveMode === "auto" && assigned !== null && autoSince?.id === open.id && later !== open.id ? Math.max(0, AUTO_MOVE_MS - (now - autoSince.at)) : null;
2899
3034
  const [dismissedBroadcast, setDismissedBroadcast] = useState3(null);
2900
3035
  const [failure, setFailure] = useState3(null);
2901
3036
  const expireAsked = useRef5(null);
@@ -2908,6 +3043,19 @@ function BreakoutBanner() {
2908
3043
  if ((here ?? null) === request.roomId) return;
2909
3044
  void moveTo(request.roomId).then((result) => setFailure(result.ok ? null : result.message));
2910
3045
  }, [moveAt, host, moveTo]);
3046
+ const joinAssigned = async () => {
3047
+ if (assigned === null) return;
3048
+ const said = await act({ action: "breakout_join", roomId: assigned.id });
3049
+ const result = said.ok ? await moveTo(assigned.id) : said;
3050
+ setFailure(result.ok ? null : result.message);
3051
+ };
3052
+ useEffect5(() => {
3053
+ if (open === null || open.moveMode !== "auto" || assigned === null || breakoutRoomId !== null) return;
3054
+ if (autoSince?.id !== open.id) setAutoSince({ id: open.id, at: Date.now() });
3055
+ }, [open, assigned, breakoutRoomId, autoSince]);
3056
+ useEffect5(() => {
3057
+ if (autoLeft === 0) void joinAssigned();
3058
+ }, [autoLeft]);
2911
3059
  useEffect5(() => {
2912
3060
  if (open === null || remaining === null || remaining > 0) return;
2913
3061
  const asked = expireAsked.current;
@@ -2921,27 +3069,25 @@ function BreakoutBanner() {
2921
3069
  if (open === null && failure === null) return null;
2922
3070
  return /* @__PURE__ */ jsxs2("div", { className: "mx-meet__breakout-banner", role: "status", children: [
2923
3071
  failure !== null ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__collab-error", children: failure }) : null,
3072
+ closing !== null ? /* @__PURE__ */ jsxs2("span", { role: "status", "data-meet-notice": "breakout-closing", children: [
3073
+ "Rooms closing in ",
3074
+ Math.ceil(closing / 1e3),
3075
+ room !== null ? " \u2014 you return to the main meeting" : ""
3076
+ ] }) : null,
2924
3077
  room !== null ? /* @__PURE__ */ jsxs2("span", { children: [
2925
3078
  /* @__PURE__ */ jsx5("strong", { children: room.name }),
2926
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
3079
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
2927
3080
  /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void moveTo(null), children: "Return to main room" })
3081
+ ] }) : assigned !== null && later !== open?.id ? /* @__PURE__ */ jsxs2("span", { children: [
3082
+ autoLeft !== null ? `You're being moved to ${assigned.name} in ${Math.ceil(autoLeft / 1e3)} s` : `You're invited to ${assigned.name}`,
3083
+ remaining !== null && closing === null ? ` \xB7 ${clock(remaining)} left` : "",
3084
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void joinAssigned(), children: "Join room" }),
3085
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => setLater(open?.id ?? null), children: "Later" })
2928
3086
  ] }) : assigned !== null ? /* @__PURE__ */ jsxs2("span", { children: [
2929
- "You are assigned to ",
2930
- /* @__PURE__ */ jsx5("strong", { children: assigned.name }),
2931
- remaining !== null ? ` \xB7 ${clock(remaining)} left` : "",
2932
- /* @__PURE__ */ jsx5(
2933
- "button",
2934
- {
2935
- type: "button",
2936
- className: "mx-meet__primary mx-meet__primary--small",
2937
- onClick: () => void act({ action: "breakout_join", roomId: assigned.id }).then(async (said) => {
2938
- const result = said.ok ? await moveTo(assigned.id) : said;
2939
- setFailure(result.ok ? null : result.message);
2940
- }),
2941
- children: "Join"
2942
- }
2943
- )
2944
- ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Breakout rooms are open \u2014 pick one under Activities." }) : null,
3087
+ assigned.name,
3088
+ " is open",
3089
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "mx-meet__chip", onClick: () => void joinAssigned(), children: "Join room" })
3090
+ ] }) : open?.allowChoose ? /* @__PURE__ */ jsx5("span", { children: "Rooms are open \u2014 pick one under Activities." }) : null,
2945
3091
  showBroadcast ? /* @__PURE__ */ jsxs2("span", { className: "mx-meet__breakout-broadcast", children: [
2946
3092
  /* @__PURE__ */ jsxs2("strong", { children: [
2947
3093
  broadcast.by,
@@ -2989,6 +3135,33 @@ function CaptionLanguageSettings() {
2989
3135
  collab?.captionTranslationError != null ? /* @__PURE__ */ jsx5("p", { className: "mx-meet__panel-note mx-meet__collab-error", children: collab.captionTranslationError }) : null
2990
3136
  ] });
2991
3137
  }
3138
+ function CaptionsControl() {
3139
+ const captions = useCaptions();
3140
+ return /* @__PURE__ */ jsx5(
3141
+ ControlButton,
3142
+ {
3143
+ label: captions.on ? "Turn off captions" : "Turn on captions",
3144
+ active: captions.on,
3145
+ pressed: captions.on,
3146
+ onClick: captions.toggle,
3147
+ children: /* @__PURE__ */ jsx5(CaptionsIcon, {})
3148
+ }
3149
+ );
3150
+ }
3151
+ function TakeNotesControl() {
3152
+ const ai = useMeetAi();
3153
+ if (!ai.canControlNoteTaker) return null;
3154
+ const running = ai.noteTaker.state === "joining" || ai.noteTaker.state === "transcribing";
3155
+ return /* @__PURE__ */ jsx5(
3156
+ ControlButton,
3157
+ {
3158
+ label: running ? "Stop taking notes" : "Take notes for me",
3159
+ active: running,
3160
+ onClick: () => void (running ? ai.stopNoteTaker() : ai.startNoteTaker()),
3161
+ children: /* @__PURE__ */ jsx5(NotesIcon, {})
3162
+ }
3163
+ );
3164
+ }
2992
3165
 
2993
3166
  // src/skins/meet/components.tsx
2994
3167
  import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -3039,6 +3212,7 @@ function ControlButton({
3039
3212
  label,
3040
3213
  active = false,
3041
3214
  danger = false,
3215
+ pressed,
3042
3216
  onClick,
3043
3217
  children
3044
3218
  }) {
@@ -3053,26 +3227,49 @@ function ControlButton({
3053
3227
  ].filter(Boolean).join(" "),
3054
3228
  "aria-label": label,
3055
3229
  title: label,
3230
+ ...pressed !== void 0 ? { "aria-pressed": pressed } : {},
3056
3231
  onClick,
3057
3232
  children
3058
3233
  }
3059
3234
  );
3060
3235
  }
3061
3236
  function RecordingIndicator() {
3062
- const { state, label } = useRecording();
3063
- if (label === null) return null;
3064
- return /* @__PURE__ */ jsxs3(
3065
- "span",
3066
- {
3067
- className: `mx-meet__recording mx-meet__recording--${state}`,
3068
- role: "status",
3069
- "aria-live": "polite",
3070
- children: [
3071
- /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
3072
- label
3073
- ]
3237
+ const { state, label, canStart } = useRecording();
3238
+ const snapshot = useMeetSnapshot();
3239
+ const previous = useRef6(state);
3240
+ const [stoppedAt, setStoppedAt] = useState4(null);
3241
+ useEffect6(() => {
3242
+ const was = previous.current;
3243
+ previous.current = state;
3244
+ if ((was === "recording" || was === "starting") && state !== "recording" && state !== "starting") {
3245
+ setStoppedAt(Date.now());
3246
+ const timer = setTimeout(() => setStoppedAt(null), 1e4);
3247
+ return () => clearTimeout(timer);
3074
3248
  }
3075
- );
3249
+ return void 0;
3250
+ }, [state]);
3251
+ const meetingId = snapshot?.meeting?.id ?? null;
3252
+ if (label === null && stoppedAt === null) return null;
3253
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
3254
+ label !== null ? /* @__PURE__ */ jsxs3(
3255
+ "span",
3256
+ {
3257
+ className: `mx-meet__recording mx-meet__recording--${state}`,
3258
+ role: "status",
3259
+ "aria-live": "polite",
3260
+ children: [
3261
+ /* @__PURE__ */ jsx6(RecordIcon, { width: 10, height: 10 }),
3262
+ label
3263
+ ]
3264
+ }
3265
+ ) : null,
3266
+ stoppedAt !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-stopped", children: "Recording stopped" }) : null,
3267
+ state === "processing" ? (
3268
+ // rec-processing: the host is told it is being made ready (the indicator says Processing…).
3269
+ /* @__PURE__ */ jsx6("span", { className: "mx-meet__recording", role: "status", "data-meet-notice": "recording-processing", children: "Your recording is being processed. A link appears here when it is ready." })
3270
+ ) : null,
3271
+ state === "available" && canStart && meetingId !== null ? /* @__PURE__ */ jsx6("a", { className: "mx-meet__link", href: `/meetings/${meetingId}`, target: "_blank", rel: "noreferrer", children: "View recording" }) : null
3272
+ ] });
3076
3273
  }
3077
3274
  function ConnectionDot({ quality }) {
3078
3275
  if (quality === "excellent" || quality === "good" || quality === "unknown") return null;
@@ -3103,6 +3300,7 @@ function ParticipantTile({
3103
3300
  const showVideo = participant.cameraEnabled || participant.screenSharing;
3104
3301
  const pinnedHere = snapshot?.pinned === participant.identity;
3105
3302
  const spotlit = snapshot?.spotlight === participant.identity;
3303
+ const presenting = snapshot?.presenter === participant.identity;
3106
3304
  const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
3107
3305
  return /* @__PURE__ */ jsxs3(
3108
3306
  "div",
@@ -3114,6 +3312,7 @@ function ParticipantTile({
3114
3312
  ].filter(Boolean).join(" "),
3115
3313
  "data-identity": participant.identity,
3116
3314
  ...participantAttributes(participant),
3315
+ ...presenting ? { "data-meet-participant-presenting": "true" } : {},
3117
3316
  children: [
3118
3317
  showVideo ? /* @__PURE__ */ jsx6(
3119
3318
  "video",
@@ -3125,6 +3324,7 @@ function ParticipantTile({
3125
3324
  muted: participant.isLocal
3126
3325
  }
3127
3326
  ) : /* @__PURE__ */ jsx6("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx6(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
3327
+ presenting ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__tile-presenting", role: "status", children: participant.isLocal ? "You are presenting" : `${participant.displayName} is presenting` }) : null,
3128
3328
  participant.handRaised ? /* @__PURE__ */ jsxs3("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
3129
3329
  /* @__PURE__ */ jsx6(HandIcon, { width: 14, height: 14 }),
3130
3330
  handPosition > 0 ? handPosition : null
@@ -3225,6 +3425,7 @@ function ControlBar({ onLeave }) {
3225
3425
  const controls = useHostControls();
3226
3426
  const queue = useHandQueue();
3227
3427
  const micUnavailable = useLocalMedia().microphone.unavailable;
3428
+ const live = useLivePolicy();
3228
3429
  const unread = useUnreadChat(panel === "chat");
3229
3430
  const [whiteboard, setWhiteboard] = useState4(false);
3230
3431
  const app = useMeetAppPanel();
@@ -3292,7 +3493,7 @@ function ControlBar({ onLeave }) {
3292
3493
  panel !== "people" ? /* @__PURE__ */ jsx6(KnockNotice, { onView: () => setPanel("people") }) : null,
3293
3494
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ jsx6(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3294
3495
  /* @__PURE__ */ jsxs3("div", { className: "mx-meet__controls", children: [
3295
- /* @__PURE__ */ jsx6(
3496
+ live.micLocked ? null : /* @__PURE__ */ jsx6(
3296
3497
  ControlButton,
3297
3498
  {
3298
3499
  label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
@@ -3301,7 +3502,7 @@ function ControlBar({ onLeave }) {
3301
3502
  children: room.local?.micEnabled === true ? /* @__PURE__ */ jsx6(MicIcon, {}) : /* @__PURE__ */ jsx6(MicOffIcon, {})
3302
3503
  }
3303
3504
  ),
3304
- /* @__PURE__ */ jsx6(
3505
+ live.cameraLocked ? null : /* @__PURE__ */ jsx6(
3305
3506
  ControlButton,
3306
3507
  {
3307
3508
  label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
@@ -3310,6 +3511,7 @@ function ControlBar({ onLeave }) {
3310
3511
  children: room.local?.cameraEnabled === true ? /* @__PURE__ */ jsx6(CameraIcon, {}) : /* @__PURE__ */ jsx6(CameraOffIcon, {})
3311
3512
  }
3312
3513
  ),
3514
+ /* @__PURE__ */ jsx6(CaptionsControl, {}),
3313
3515
  room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ jsx6(
3314
3516
  ControlButton,
3315
3517
  {
@@ -3374,6 +3576,7 @@ function ControlBar({ onLeave }) {
3374
3576
  onOpen: openActivity
3375
3577
  }
3376
3578
  ),
3579
+ /* @__PURE__ */ jsx6(TakeNotesControl, {}),
3377
3580
  /* @__PURE__ */ jsx6(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
3378
3581
  recording.canStart ? /* @__PURE__ */ jsx6(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
3379
3582
  recording.canStop ? /* @__PURE__ */ jsx6(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ jsx6(RecordIcon, {}) }) : null,
@@ -3429,6 +3632,23 @@ function ControlBar({ onLeave }) {
3429
3632
  ]
3430
3633
  }
3431
3634
  ),
3635
+ /* @__PURE__ */ jsxs3(
3636
+ MenuItem,
3637
+ {
3638
+ onSelect: () => {
3639
+ setPopover("none");
3640
+ void controls.run({
3641
+ action: "set_av_lock",
3642
+ value: snapshot?.livePolicy.avLock === "both" ? "off" : "both"
3643
+ });
3644
+ },
3645
+ children: [
3646
+ /* @__PURE__ */ jsx6(MicOffIcon, { width: 16, height: 16 }),
3647
+ " ",
3648
+ snapshot?.livePolicy.avLock === "both" ? "Turn on attendees' microphone and camera" : "Turn off attendees' microphone and camera"
3649
+ ]
3650
+ }
3651
+ ),
3432
3652
  /* @__PURE__ */ jsxs3(
3433
3653
  MenuItem,
3434
3654
  {
@@ -3756,9 +3976,45 @@ function MeetingOverlays() {
3756
3976
  const reactions = useReactions();
3757
3977
  const { notice, dismiss } = useModerationNotice();
3758
3978
  const room = useRoom();
3979
+ const live = useLivePolicy();
3980
+ const snapshot = useMeetSnapshot();
3981
+ const controls = useHostControls();
3982
+ const isHostNow = useRole() === "host";
3759
3983
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
3760
3984
  /* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
3761
3985
  /* @__PURE__ */ jsx6(MediaNotices, {}),
3986
+ snapshot !== null && snapshot.phase === "in_call" && snapshot.participants.length >= 2 && !snapshot.participants.some((p) => p.role === "host" || p.role === "cohost") ? (
3987
+ // meeting-without-host: the call goes on; host-only controls are simply not on screen.
3988
+ /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "host-absent", children: "The host has left the meeting. Host controls are not available until a host is back." })
3989
+ ) : null,
3990
+ live.consent !== null ? /* @__PURE__ */ jsx6("div", { className: "mx-meet__sheet-backdrop", children: /* @__PURE__ */ jsxs3("div", { className: "mx-meet__sheet", role: "alertdialog", "aria-label": "This meeting is being recorded", "data-meet-notice": "recording-consent", children: [
3991
+ /* @__PURE__ */ jsx6("h2", { className: "mx-meet__panel-title", children: "This meeting is being recorded" }),
3992
+ /* @__PURE__ */ jsx6("p", { className: "mx-meet__confirm-text", children: live.consent === "ok_or_leave" ? "By staying you agree to be recorded. You can leave now if you do not agree." : "Until you agree to the recording you can watch and listen, but not use your microphone or camera." }),
3993
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__menu-actions", children: [
3994
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__send", onClick: live.agree, children: live.consent === "ok_or_leave" ? "OK" : "I agree" }),
3995
+ live.consent === "ok_or_leave" ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void room.leave(), children: "Leave meeting" }) : null
3996
+ ] })
3997
+ ] }) }) : null,
3998
+ snapshot?.canReclaim === true && !isHostNow ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "host-reclaim", children: [
3999
+ /* @__PURE__ */ jsx6("span", { children: "You started this meeting. Take back the host?" }),
4000
+ /* @__PURE__ */ jsx6(
4001
+ "button",
4002
+ {
4003
+ type: "button",
4004
+ className: "mx-meet__primary mx-meet__primary--small",
4005
+ onClick: () => void controls.run({ action: "reclaim_host" }),
4006
+ children: "Reclaim host"
4007
+ }
4008
+ )
4009
+ ] }) : null,
4010
+ live.micLocked || live.cameraLocked ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "mic-locked", children: live.micLocked && live.cameraLocked ? "Your microphone and camera are turned off by the organizer." : live.micLocked ? "Your microphone is turned off by the organizer." : "Your camera is turned off by the organizer." }) : null,
4011
+ live.timeWarning && live.minutesLeft !== null && live.minutesLeft > 0 ? /* @__PURE__ */ jsxs3("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "time-warning", children: [
4012
+ "This meeting will end in ",
4013
+ live.minutesLeft,
4014
+ " ",
4015
+ live.minutesLeft === 1 ? "minute" : "minutes",
4016
+ " (time limit)."
4017
+ ] }) : null,
3762
4018
  notice !== null && notice.kind !== "removed" ? (
3763
4019
  // A host's request ("the host is asking you to unmute"), not an error:
3764
4020
  // an assertive status, so it is announced at once without being filed
@@ -3849,6 +4105,7 @@ function ChatPanel({
3849
4105
  }) {
3850
4106
  const snapshot = useMeetSnapshot();
3851
4107
  const room = useRoom();
4108
+ const live = useLivePolicy();
3852
4109
  const [draft, setDraft] = useState4("");
3853
4110
  const [recipient, setRecipient] = useState4(to);
3854
4111
  const listRef = useRef6(null);
@@ -3873,7 +4130,7 @@ function ChatPanel({
3873
4130
  snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx6("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ jsx6("li", { className: "mx-meet__panel-note", children: "No messages yet. Messages to everyone stay with the meeting, so people who join later see them." }) : null,
3874
4131
  messages.map((message) => /* @__PURE__ */ jsx6(ChatLine, { message, local, people }, message.id))
3875
4132
  ] }),
3876
- /* @__PURE__ */ jsxs3(
4133
+ !live.canChat ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "chat-restricted", children: live.chatMode === "off" ? "Chat is turned off for this meeting." : "Chat is limited to the host and co-hosts in this meeting." }) : /* @__PURE__ */ jsxs3(
3877
4134
  "form",
3878
4135
  {
3879
4136
  className: "mx-meet__composer mx-meet__composer--chat",
@@ -4056,7 +4313,8 @@ function PeoplePanel({
4056
4313
  }
4057
4314
  ),
4058
4315
  isNoteTaker(participant) ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
4059
- snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
4316
+ snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Spotlight" }) : null,
4317
+ snapshot?.presenter === participant.identity ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", "data-meet-presenting": "true", children: "Presenting" }) : null
4060
4318
  ] }),
4061
4319
  participant.handRaised ? /* @__PURE__ */ jsx6(HandIcon, { width: 14, height: 14 }) : null,
4062
4320
  !participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
@@ -4244,6 +4502,13 @@ function PersonMenu({
4244
4502
  /* @__PURE__ */ jsx6(PeopleIcon, { width: 16, height: 16 }),
4245
4503
  " Make co-host"
4246
4504
  ] }) : null,
4505
+ !participant.isAgent && participant.role !== "cohost" && participant.role !== "host" ? participant.role === "presenter" ? /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "revoke_presenter", identity: participant.identity }), children: [
4506
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
4507
+ " Remove presenter"
4508
+ ] }) : /* @__PURE__ */ jsxs3(MenuItem, { onSelect: () => act({ action: "make_presenter", identity: participant.identity }), children: [
4509
+ /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
4510
+ " Make presenter"
4511
+ ] }) : null,
4247
4512
  !participant.isAgent ? /* @__PURE__ */ jsxs3(
4248
4513
  MenuItem,
4249
4514
  {
@@ -4749,8 +5014,19 @@ function Captions({ lines = 3 }) {
4749
5014
  const ai = useMeetAi();
4750
5015
  const visible = ai.transcript.slice(-lines);
4751
5016
  const translation = useCaptionTranslation(visible);
4752
- if (visible.length === 0) return null;
4753
- return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", children: [
5017
+ const captions = useCaptions();
5018
+ if (!captions.on) return null;
5019
+ if (captions.state === "unavailable") {
5020
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ jsxs3("p", { className: "mx-meet__caption", children: [
5021
+ "Captions are unavailable. ",
5022
+ captions.reason,
5023
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
5024
+ ] }) });
5025
+ }
5026
+ if (visible.length === 0) {
5027
+ return /* @__PURE__ */ jsx6("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": captions.state, children: /* @__PURE__ */ jsx6("p", { className: "mx-meet__caption mx-meet__caption--interim", children: captions.state === "starting" ? "Starting captions. They appear as people speak." : "Captions are on. They appear as people speak." }) });
5028
+ }
5029
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": "live", children: [
4754
5030
  visible.map((line) => {
4755
5031
  const translated = translation.language === null ? void 0 : translation.translations[line.id];
4756
5032
  return /* @__PURE__ */ jsxs3(
@@ -4773,7 +5049,7 @@ function Captions({ lines = 3 }) {
4773
5049
  function ConsentNotice() {
4774
5050
  const notice = useConsentNotice();
4775
5051
  if (notice === null) return null;
4776
- return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", children: notice });
5052
+ return /* @__PURE__ */ jsx6("p", { className: "mx-meet__consent", role: "status", "data-meet-notice": "notes-started", children: notice });
4777
5053
  }
4778
5054
  function AttendanceNotice() {
4779
5055
  const media = useLocalMedia();
@@ -5487,6 +5763,7 @@ export {
5487
5763
  CameraOffIcon,
5488
5764
  CaptionLanguageSettings,
5489
5765
  Captions,
5766
+ CaptionsIcon,
5490
5767
  ChatIcon,
5491
5768
  ChatPanel,
5492
5769
  CheckIcon,
@@ -5510,6 +5787,7 @@ export {
5510
5787
  MicIcon,
5511
5788
  MicOffIcon,
5512
5789
  MoreIcon,
5790
+ NotesIcon,
5513
5791
  PanelShell,
5514
5792
  ParticipantTile,
5515
5793
  PencilIcon,