@ai-matrx/meet 0.7.5 → 0.7.7

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.
package/dist/react.cjs CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  // src/react.ts
33
33
  var react_exports = {};
34
34
  __export(react_exports, {
35
+ ATTENDANCE_CSV_HEADER: () => ATTENDANCE_CSV_HEADER,
35
36
  AiPanel: () => AiPanel,
36
37
  AttendanceNotice: () => AttendanceNotice,
37
38
  Avatar: () => Avatar,
@@ -135,6 +136,7 @@ __export(react_exports, {
135
136
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
136
137
  WifiLowIcon: () => WifiLowIcon,
137
138
  absenceSentence: () => absenceSentence,
139
+ activeWrapUpSummary: () => activeWrapUpSummary,
138
140
  announceMeeting: () => announceMeeting,
139
141
  applyRoomEvent: () => applyRoomEvent,
140
142
  asCallId: () => asCallId,
@@ -143,6 +145,9 @@ __export(react_exports, {
143
145
  asParticipantIdentity: () => asParticipantIdentity,
144
146
  asRoomName: () => asRoomName,
145
147
  asUserId: () => asUserId,
148
+ attendanceCsv: () => attendanceCsv,
149
+ attendanceReport: () => attendanceReport,
150
+ attendanceTotals: () => attendanceTotals,
146
151
  callInviteAction: () => callInviteAction,
147
152
  callInviteText: () => callInviteText,
148
153
  callStateLabel: () => callStateLabel,
@@ -151,6 +156,7 @@ __export(react_exports, {
151
156
  canTransition: () => canTransition,
152
157
  captionSegment: () => captionSegment,
153
158
  changesPatch: () => changesPatch,
159
+ clockPosition: () => clockPosition,
154
160
  computeStage: () => computeStage,
155
161
  countLabel: () => countLabel,
156
162
  createCallCenter: () => createCallCenter,
@@ -191,11 +197,14 @@ __export(react_exports, {
191
197
  learnName: () => learnName,
192
198
  learnNames: () => learnNames,
193
199
  listenOnlyNotice: () => listenOnlyNotice,
200
+ loadFollowUpDraft: () => loadFollowUpDraft,
194
201
  loadGuestMeetingRecord: () => loadGuestMeetingRecord,
202
+ loadMeetingChat: () => loadMeetingChat,
195
203
  loadMeetingRecord: () => loadMeetingRecord,
196
204
  mapConnectionState: () => mapConnectionState,
197
205
  mapQuality: () => mapQuality,
198
206
  meetingInviteAction: () => meetingInviteAction,
207
+ meetingLength: () => meetingLength,
199
208
  meetingLink: () => meetingLink,
200
209
  meetingPath: () => meetingPath,
201
210
  meetingSlug: () => meetingSlug,
@@ -210,16 +219,19 @@ __export(react_exports, {
210
219
  noteTakerLabel: () => noteTakerLabel,
211
220
  noteTakerParticipant: () => noteTakerParticipant,
212
221
  noteTakerStatus: () => noteTakerStatus,
222
+ notesDocument: () => notesDocument,
213
223
  orderParticipants: () => orderParticipants,
214
224
  parseRoomToken: () => parseRoomToken,
215
225
  participantSummary: () => participantSummary,
216
226
  playableRecordingFileId: () => playableRecordingFileId,
217
227
  projectInvitee: () => projectInvitee,
228
+ projectLibraryRecording: () => projectLibraryRecording,
218
229
  projectNote: () => projectNote,
219
230
  projectOccurrence: () => projectOccurrence,
220
231
  projectOverride: () => projectOverride,
221
232
  projectParticipant: () => projectParticipant,
222
233
  projectRsvpInvitation: () => projectRsvpInvitation,
234
+ projectSearchHit: () => projectSearchHit,
223
235
  projectSegment: () => projectSegment,
224
236
  projectUpcoming: () => projectUpcoming,
225
237
  pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
@@ -227,11 +239,17 @@ __export(react_exports, {
227
239
  readableTranscript: () => readableTranscript,
228
240
  recordingLabel: () => recordingLabel,
229
241
  recordingNotice: () => recordingNotice,
242
+ recordingOffsetMs: () => recordingOffsetMs,
230
243
  roleOf: () => roleOf,
231
244
  roomTopic: () => roomTopic,
245
+ searchTranscript: () => searchTranscript,
246
+ sendFollowUp: () => sendFollowUp,
232
247
  setGuestRecordAccess: () => setGuestRecordAccess,
233
248
  settingsPatch: () => settingsPatch,
234
249
  snapshotParticipant: () => snapshotParticipant,
250
+ snippetRuns: () => snippetRuns,
251
+ summarizeMeetingRecordRestrictions: () => summarizeMeetingRecordRestrictions,
252
+ transcriptDocument: () => transcriptDocument,
235
253
  unavailableFrom: () => unavailableFrom,
236
254
  useAudioLevel: () => useAudioLevel,
237
255
  useCalls: () => useCalls,
@@ -267,7 +285,8 @@ __export(react_exports, {
267
285
  useStage: () => useStage,
268
286
  useTrack: () => useTrack,
269
287
  userTopic: () => userTopic,
270
- wasRefused: () => wasRefused
288
+ wasRefused: () => wasRefused,
289
+ wrapUpStatus: () => wrapUpStatus
271
290
  });
272
291
  module.exports = __toCommonJS(react_exports);
273
292
 
@@ -469,6 +488,12 @@ var MEET_ROUTES = {
469
488
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
470
489
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
471
490
  meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
491
+ /**
492
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
493
+ * body from the wrap-up, who it can go to); `POST` = send what the host
494
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
495
+ */
496
+ meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up",
472
497
  /**
473
498
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
474
499
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -598,10 +623,11 @@ function createTokenClient(options) {
598
623
  throw error;
599
624
  }
600
625
  if (response.status === 401 || response.status === 403) {
626
+ const said = await serverSentence(response);
601
627
  throw new MeetError(
602
628
  "forbidden",
603
- `meet.token: the server refused this join (${response.status})`,
604
- "Authorization is the server's (R5). The joiner is not a member of this meeting's organization, or the meeting is locked \u2014 never mint around it on the client."
629
+ said?.message ?? "You can't join this meeting.",
630
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
605
631
  );
606
632
  }
607
633
  if (response.status === 404) {
@@ -612,12 +638,7 @@ function createTokenClient(options) {
612
638
  );
613
639
  }
614
640
  if (response.status === 410) {
615
- let reason = "This meeting was cancelled.";
616
- try {
617
- const body = await response.json();
618
- if (typeof body?.detail?.message === "string") reason = body.detail.message;
619
- } catch {
620
- }
641
+ const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
621
642
  throw new MeetError(
622
643
  "cancelled",
623
644
  reason,
@@ -625,10 +646,11 @@ function createTokenClient(options) {
625
646
  );
626
647
  }
627
648
  if (response.status === 423) {
649
+ const said = await serverSentence(response);
628
650
  throw new MeetError(
629
651
  "room-locked",
630
- "meet.token: the meeting is locked",
631
- "The host locked the room. Ask them to unlock or admit you; this is a normal state, not a failure."
652
+ said?.message ?? "The meeting is locked.",
653
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
632
654
  );
633
655
  }
634
656
  if (response.status === 503) {
@@ -699,6 +721,19 @@ function placeholder(roomName) {
699
721
  lobby: false
700
722
  };
701
723
  }
724
+ async function serverSentence(response) {
725
+ try {
726
+ const body = await response.json();
727
+ const source = body !== null && typeof body.detail === "object" && body.detail !== null ? body.detail : body;
728
+ if (source === null) return null;
729
+ const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
730
+ if (message === null) return null;
731
+ const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
732
+ return { message, remedy };
733
+ } catch {
734
+ return null;
735
+ }
736
+ }
702
737
 
703
738
  // src/core/ai.ts
704
739
  var MEET_ANSWER_EVENTS = {
@@ -1142,7 +1177,7 @@ var DEFAULT_RING_TIMEOUT_MS = 45e3;
1142
1177
  function createCallCenter(options) {
1143
1178
  const { manager, repository, identity } = options;
1144
1179
  const timers = options.timers ?? {
1145
- setTimeout: (run, ms) => setTimeout(run, ms),
1180
+ setTimeout: (run, ms2) => setTimeout(run, ms2),
1146
1181
  clearTimeout: (handle) => clearTimeout(handle),
1147
1182
  now: () => Date.now()
1148
1183
  };
@@ -1810,6 +1845,7 @@ var EMPTY = {
1810
1845
  recordingId: null,
1811
1846
  chat: [],
1812
1847
  chatLoaded: false,
1848
+ chatHistoryError: null,
1813
1849
  reactions: [],
1814
1850
  spotlight: null,
1815
1851
  moderationNotice: null,
@@ -1919,7 +1955,7 @@ function createMeetStore(initial = {}) {
1919
1955
  state.names,
1920
1956
  messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
1921
1957
  );
1922
- commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
1958
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
1923
1959
  },
1924
1960
  settleChat(localId, next) {
1925
1961
  const index = state.chat.findIndex((message) => message.id === localId);
@@ -2079,7 +2115,9 @@ function createMeetingSession(options) {
2079
2115
  return identity.userId;
2080
2116
  }
2081
2117
  function knockIdentity() {
2082
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
2118
+ return asParticipantIdentity(
2119
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2120
+ );
2083
2121
  }
2084
2122
  function requireMeeting(operation) {
2085
2123
  const meeting = store.snapshot().meeting;
@@ -2114,8 +2152,9 @@ function createMeetingSession(options) {
2114
2152
  verdict.admitted ? "admitted" : "denied",
2115
2153
  verdict.by === null ? null : verdict.by
2116
2154
  );
2117
- if (verdict.admitted && decided === knockIdentity()) {
2118
- options.onAdmitted?.();
2155
+ if (decided === knockIdentity()) {
2156
+ if (verdict.admitted) options.onAdmitted?.();
2157
+ else options.onDenied?.();
2119
2158
  }
2120
2159
  }
2121
2160
  },
@@ -2452,7 +2491,16 @@ var RPCS = {
2452
2491
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2453
2492
  meetingOccurrences: "meet_meeting_occurrences",
2454
2493
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
2455
- occurrencesBetween: "meet_occurrences_between"
2494
+ occurrencesBetween: "meet_occurrences_between",
2495
+ /*
2496
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
2497
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
2498
+ * ceiling, never the view.
2499
+ */
2500
+ /** The recordings library. */
2501
+ myRecordings: "meet_my_recordings",
2502
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2503
+ search: "meet_search"
2456
2504
  };
2457
2505
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2458
2506
  var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
@@ -3157,12 +3205,117 @@ function createMeetRepository(options) {
3157
3205
  );
3158
3206
  },
3159
3207
  async recordings(meetingId) {
3160
- const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: false });
3208
+ const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("started_at", { ascending: false });
3161
3209
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
3162
3210
  return (response.data ?? []).map(projectRecording);
3211
+ },
3212
+ async myRecordings({ includeArchived = false, limit = 200 } = {}) {
3213
+ const rows = await guard(
3214
+ "meet.myRecordings",
3215
+ () => db.rpc(RPCS.myRecordings, { p_include_archived: includeArchived, p_limit: limit })
3216
+ );
3217
+ return rows.map(projectLibraryRecording);
3218
+ },
3219
+ async searchMeetings(query, limit = 60) {
3220
+ if (query.trim().length === 0) return [];
3221
+ const rows = await guard(
3222
+ "meet.searchMeetings",
3223
+ () => db.rpc(RPCS.search, { p_query: query, p_limit: limit })
3224
+ );
3225
+ return rows.map(projectSearchHit);
3226
+ },
3227
+ async renameRecording(recordingId, title) {
3228
+ const clean = title === null ? null : title.trim().slice(0, 200) || null;
3229
+ const response = await db.from(TABLES.recordings).update({ title: clean }).eq("id", recordingId).select("id");
3230
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.renameRecording");
3231
+ if ((response.data ?? []).length === 0) {
3232
+ throw normalizeMeetError(
3233
+ { message: "permission denied: only the meeting's host or an editor can rename it", code: "42501" },
3234
+ "meet.renameRecording"
3235
+ );
3236
+ }
3237
+ },
3238
+ async setRecordingArchived(recordingId, archived) {
3239
+ const response = await db.from(TABLES.recordings).update({ deleted_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", recordingId).select("id");
3240
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setRecordingArchived");
3241
+ if ((response.data ?? []).length === 0) {
3242
+ throw normalizeMeetError(
3243
+ { message: "permission denied: only the meeting's host or an editor can archive it", code: "42501" },
3244
+ "meet.setRecordingArchived"
3245
+ );
3246
+ }
3163
3247
  }
3164
3248
  };
3165
3249
  }
3250
+ var LIBRARY_STATES = [
3251
+ "idle",
3252
+ "starting",
3253
+ "recording",
3254
+ "stopping",
3255
+ "processing",
3256
+ "available",
3257
+ "expired",
3258
+ "failed"
3259
+ ];
3260
+ function projectLibraryRecording(row) {
3261
+ const operation = "meet.projectLibraryRecording";
3262
+ const state = text(row, "state");
3263
+ const role = text(row, "my_role");
3264
+ return {
3265
+ id: requireText(row, "recording_id", operation),
3266
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3267
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3268
+ meetingSlug: requireText(row, "meeting_slug", operation),
3269
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
3270
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
3271
+ title: text(row, "recording_title"),
3272
+ fileId: text(row, "file_id"),
3273
+ startedAt: text(row, "started_at"),
3274
+ endedAt: text(row, "ended_at"),
3275
+ durationMs: num(row, "duration_ms"),
3276
+ sizeBytes: num(row, "size_bytes"),
3277
+ failureReason: text(row, "failure_reason"),
3278
+ archivedAt: text(row, "deleted_at"),
3279
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
3280
+ };
3281
+ }
3282
+ var SEARCH_SOURCES = [
3283
+ "title",
3284
+ "summary",
3285
+ "decision",
3286
+ "action_item",
3287
+ "note",
3288
+ "transcript"
3289
+ ];
3290
+ function projectSearchHit(row) {
3291
+ const operation = "meet.projectSearchHit";
3292
+ const source = text(row, "source");
3293
+ return {
3294
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3295
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3296
+ meetingSlug: requireText(row, "meeting_slug", operation),
3297
+ meetingAt: text(row, "meeting_at"),
3298
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
3299
+ refId: requireText(row, "ref_id", operation),
3300
+ speaker: text(row, "speaker"),
3301
+ snippet: text(row, "snippet") ?? "",
3302
+ at: text(row, "said_at"),
3303
+ rank: num(row, "rank") ?? 0
3304
+ };
3305
+ }
3306
+ function snippetRuns(snippet) {
3307
+ const runs = [];
3308
+ const pattern = /⟦([^⟧]*)⟧/g;
3309
+ let last = 0;
3310
+ for (const match of snippet.matchAll(pattern)) {
3311
+ const start = match.index ?? 0;
3312
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
3313
+ runs.push({ text: match[1] ?? "", hit: true });
3314
+ last = start + match[0].length;
3315
+ }
3316
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
3317
+ return runs;
3318
+ }
3166
3319
 
3167
3320
  // src/core/session-identity.ts
3168
3321
  var RESOLVING = Object.freeze({
@@ -4793,6 +4946,11 @@ function MeetRuntime(props) {
4793
4946
  // publish nor subscribe, so the host saying yes has to mint a new one.
4794
4947
  onAdmitted: () => {
4795
4948
  void engine.rejoin();
4949
+ },
4950
+ // A "no" is ALSO a mint: the server refuses it with its own sentence, which
4951
+ // turns the waiting room into the pre-join screen showing that sentence.
4952
+ onDenied: () => {
4953
+ void engine.rejoin().catch(() => void 0);
4796
4954
  }
4797
4955
  });
4798
4956
  const resolveRoomToken = () => {
@@ -4815,6 +4973,10 @@ function MeetRuntime(props) {
4815
4973
  chatReadFor = snapshot.meeting.id;
4816
4974
  void inRoom.loadChat().catch((error) => {
4817
4975
  const normalized = error;
4976
+ store.patch({
4977
+ chatLoaded: true,
4978
+ chatHistoryError: normalized.remedy ?? "Messages sent before you joined could not be read. New messages still arrive."
4979
+ });
4818
4980
  report({
4819
4981
  level: "warn",
4820
4982
  message: normalized.message ?? "meet: the saved chat could not be read",
@@ -6914,22 +7076,27 @@ function MeetingOverlays() {
6914
7076
  const room = useRoom();
6915
7077
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
6916
7078
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
6917
- notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6918
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
6919
- notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6920
- "button",
6921
- {
6922
- type: "button",
6923
- className: "mx-meet__primary mx-meet__primary--small",
6924
- onClick: () => {
6925
- dismiss();
6926
- void room.toggleMicrophone();
6927
- },
6928
- children: "Unmute"
6929
- }
6930
- ) : null,
6931
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
6932
- ] }) : null
7079
+ notice !== null && notice.kind !== "removed" ? (
7080
+ // A host's request ("the host is asking you to unmute"), not an error:
7081
+ // an assertive status, so it is announced at once without being filed
7082
+ // as a failure (design-system error-box census).
7083
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "aria-live": "assertive", children: [
7084
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
7085
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7086
+ "button",
7087
+ {
7088
+ type: "button",
7089
+ className: "mx-meet__primary mx-meet__primary--small",
7090
+ onClick: () => {
7091
+ dismiss();
7092
+ void room.toggleMicrophone();
7093
+ },
7094
+ children: "Unmute"
7095
+ }
7096
+ ) : null,
7097
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
7098
+ ] })
7099
+ ) : null
6933
7100
  ] });
6934
7101
  }
6935
7102
  function ShortcutsSheet({ onClose }) {
@@ -7006,7 +7173,8 @@ function ChatPanel({
7006
7173
  };
7007
7174
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Chat", onClose, children: [
7008
7175
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__chat", ref: listRef, children: [
7009
- snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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,
7176
+ snapshot?.chatHistoryError != null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: snapshot.chatHistoryError }) : null,
7177
+ snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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,
7010
7178
  messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatLine, { message, local, people }, message.id))
7011
7179
  ] }),
7012
7180
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -7472,7 +7640,7 @@ function EffectsSettings({ preview }) {
7472
7640
  const effects = useMediaEffects();
7473
7641
  const [error, setError] = (0, import_react4.useState)(null);
7474
7642
  const fileRef = (0, import_react4.useRef)(null);
7475
- const supported = effects.backgroundsSupported || preview != null;
7643
+ const supported = effects.backgroundsSupported || preview != null || videoEffectsSupported();
7476
7644
  const apply = (next) => {
7477
7645
  setError(null);
7478
7646
  void effects.set(next);
@@ -7867,7 +8035,7 @@ function PreJoinBody({
7867
8035
  }
7868
8036
  ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
7869
8037
  mode === "video" && preview.error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
7870
- microphone ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter mx-meet__meter--preview", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
8038
+ microphone && micStream !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter mx-meet__meter--preview", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
7871
8039
  ] }),
7872
8040
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
7873
8041
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
@@ -7983,7 +8151,9 @@ function useCameraPreview(deviceId2, effects) {
7983
8151
  }).catch((thrown) => {
7984
8152
  if (!live) return;
7985
8153
  const failure = thrown;
7986
- setError([failure.message, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
8154
+ const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
8155
+ const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
8156
+ setError([sentence, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7987
8157
  });
7988
8158
  return () => {
7989
8159
  live = false;
@@ -8859,6 +9029,341 @@ function createMeetingInviteHandler(options) {
8859
9029
  };
8860
9030
  }
8861
9031
 
9032
+ // src/core/after.ts
9033
+ var import_format4 = require("@ai-matrx/kit/format");
9034
+ function kindOf(person) {
9035
+ if (person.isAgent) return "assistant";
9036
+ if (person.role === "host") return "host";
9037
+ if (person.role === "cohost") return "cohost";
9038
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
9039
+ return "member";
9040
+ }
9041
+ function ms(iso) {
9042
+ if (iso === null) return null;
9043
+ const at = new Date(iso).getTime();
9044
+ return Number.isNaN(at) ? null : at;
9045
+ }
9046
+ function attendanceReport(meeting, attendees, names = {}) {
9047
+ const end = ms(meeting.endedAt);
9048
+ const rows = attendees.map((person) => {
9049
+ const joined = ms(person.joinedAt);
9050
+ const left = ms(person.leftAt);
9051
+ const until = left ?? end;
9052
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
9053
+ return {
9054
+ id: person.id,
9055
+ identity: person.identity,
9056
+ name: displayNameFor(names, person.identity, person.displayName),
9057
+ userId: person.userId,
9058
+ kind: kindOf(person),
9059
+ joinedAt: person.joinedAt,
9060
+ leftAt: person.leftAt,
9061
+ durationMs: duration,
9062
+ durationToEnd: duration !== null && left === null,
9063
+ admission: person.admissionState
9064
+ };
9065
+ });
9066
+ const rank = {
9067
+ host: 0,
9068
+ cohost: 1,
9069
+ member: 2,
9070
+ "signed-in guest": 3,
9071
+ guest: 4,
9072
+ assistant: 5
9073
+ };
9074
+ return rows.sort((a, b) => {
9075
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
9076
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
9077
+ });
9078
+ }
9079
+ function attendanceTotals(rows) {
9080
+ const humans = rows.filter((row) => row.kind !== "assistant");
9081
+ const attended = humans.filter((row) => row.joinedAt !== null);
9082
+ const timed = attended.filter((row) => row.durationMs !== null);
9083
+ return {
9084
+ people: attended.length,
9085
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
9086
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
9087
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
9088
+ };
9089
+ }
9090
+ function csvCell(value) {
9091
+ if (value === null) return "";
9092
+ const text3 = String(value);
9093
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
9094
+ }
9095
+ var ATTENDANCE_CSV_HEADER = [
9096
+ "Meeting",
9097
+ "Meeting started",
9098
+ "Name",
9099
+ "Attended as",
9100
+ "Joined",
9101
+ "Left",
9102
+ "Minutes in the meeting",
9103
+ "Admission"
9104
+ ];
9105
+ function attendanceCsv(reports) {
9106
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
9107
+ for (const { meeting, rows } of reports) {
9108
+ for (const row of rows) {
9109
+ lines.push(
9110
+ [
9111
+ meeting.title,
9112
+ meeting.startedAt ?? meeting.scheduledFor,
9113
+ row.name,
9114
+ row.kind,
9115
+ row.joinedAt,
9116
+ row.leftAt,
9117
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
9118
+ row.admission
9119
+ ].map(csvCell).join(",")
9120
+ );
9121
+ }
9122
+ }
9123
+ return `${lines.join("\r\n")}\r
9124
+ `;
9125
+ }
9126
+ function recordingOffsetMs(at, recording) {
9127
+ if (recording === null || recording.state !== "available") return null;
9128
+ const start = ms(recording.startedAt);
9129
+ const moment = ms(at);
9130
+ if (start === null || moment === null || moment < start) return null;
9131
+ const end = ms(recording.endedAt);
9132
+ if (end !== null && moment > end) return null;
9133
+ return moment - start;
9134
+ }
9135
+ function clockPosition(offsetMs) {
9136
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
9137
+ const h = Math.floor(total / 3600);
9138
+ const m = Math.floor(total % 3600 / 60);
9139
+ const s = total % 60;
9140
+ const ss = String(s).padStart(2, "0");
9141
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
9142
+ }
9143
+ function searchTranscript(segments, query, names = {}) {
9144
+ const needle = fold(query.trim());
9145
+ if (needle.length === 0) return [];
9146
+ const out = [];
9147
+ for (const segment of segments) {
9148
+ const hay = fold(segment.text);
9149
+ const ranges = [];
9150
+ let from = 0;
9151
+ for (; ; ) {
9152
+ const at = hay.indexOf(needle, from);
9153
+ if (at < 0) break;
9154
+ ranges.push([at, at + needle.length]);
9155
+ from = at + needle.length;
9156
+ }
9157
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
9158
+ if (ranges.length > 0 || speaker.includes(needle)) {
9159
+ out.push({ segmentId: segment.id, ranges });
9160
+ }
9161
+ }
9162
+ return out;
9163
+ }
9164
+ function fold(value) {
9165
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
9166
+ }
9167
+ function clockOfDay(iso, options) {
9168
+ const at = new Date(iso);
9169
+ if (Number.isNaN(at.getTime())) return "";
9170
+ return at.toLocaleTimeString(options.locale, {
9171
+ hour: "numeric",
9172
+ minute: "2-digit",
9173
+ second: "2-digit",
9174
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
9175
+ });
9176
+ }
9177
+ function vttStamp(offsetMs) {
9178
+ const total = Math.max(0, Math.round(offsetMs));
9179
+ const h = Math.floor(total / 36e5);
9180
+ const m = Math.floor(total % 36e5 / 6e4);
9181
+ const s = Math.floor(total % 6e4 / 1e3);
9182
+ const milli = total % 1e3;
9183
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
9184
+ }
9185
+ function transcriptDocument(bundle, format, options = {}) {
9186
+ const names = options.names ?? bundle.names;
9187
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
9188
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
9189
+ if (format === "vtt") {
9190
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
9191
+ const zero = ms(zeroIso) ?? 0;
9192
+ const cues = ["WEBVTT", ""];
9193
+ lines.forEach((line, index) => {
9194
+ const start = (ms(line.startedAt) ?? zero) - zero;
9195
+ if (start < 0) return;
9196
+ const next = lines[index + 1];
9197
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
9198
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
9199
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
9200
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
9201
+ cues.push("");
9202
+ });
9203
+ return cues.join("\n");
9204
+ }
9205
+ const out = [];
9206
+ const title = bundle.meeting.title;
9207
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
9208
+ out.push("");
9209
+ let last = null;
9210
+ for (const line of lines) {
9211
+ if (line.identity !== last) {
9212
+ if (last !== null) out.push("");
9213
+ const who = speaker(line);
9214
+ const clock = clockOfDay(line.startedAt, options);
9215
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
9216
+ last = line.identity;
9217
+ }
9218
+ out.push(line.text.trim());
9219
+ }
9220
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
9221
+ return `${out.join("\n")}
9222
+ `;
9223
+ }
9224
+ function bullet(note) {
9225
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
9226
+ return `- ${note.text.trim()}${owner}`;
9227
+ }
9228
+ function notesDocument(bundle, options = {}) {
9229
+ const out = [`# ${bundle.meeting.title}`, ""];
9230
+ if (options.when) out.push(`_${options.when}_`, "");
9231
+ const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
9232
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
9233
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
9234
+ out.push("## Decisions", "");
9235
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
9236
+ out.push("## Action items", "");
9237
+ out.push(
9238
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
9239
+ ""
9240
+ );
9241
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
9242
+ return out.join("\n");
9243
+ }
9244
+ function meetingLength(meeting) {
9245
+ const start = ms(meeting.startedAt);
9246
+ const end = ms(meeting.endedAt);
9247
+ if (start === null || end === null || end < start) return null;
9248
+ return (0, import_format4.formatDurationMs)(end - start, { style: "long" });
9249
+ }
9250
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
9251
+ const operation = "meet.loadMeetingChat";
9252
+ let response;
9253
+ try {
9254
+ response = await api.request({
9255
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
9256
+ method: "GET",
9257
+ operation,
9258
+ requireAuth: roomToken === null,
9259
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
9260
+ });
9261
+ } catch (error) {
9262
+ return {
9263
+ messages: [],
9264
+ refusal: {
9265
+ message: `The chat could not be read (${error.message}).`,
9266
+ remedy: error.remedy ?? "Reload the page."
9267
+ }
9268
+ };
9269
+ }
9270
+ if (!response.ok) {
9271
+ let body = null;
9272
+ try {
9273
+ body = await response.json();
9274
+ } catch {
9275
+ body = null;
9276
+ }
9277
+ const said = readServerRefusal(body);
9278
+ return {
9279
+ messages: [],
9280
+ refusal: {
9281
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
9282
+ remedy: said?.remedy || "Reload the page."
9283
+ }
9284
+ };
9285
+ }
9286
+ const payload = await response.json();
9287
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
9288
+ return {
9289
+ messages: rows.map(
9290
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
9291
+ ).filter((message) => message !== null),
9292
+ refusal: null
9293
+ };
9294
+ }
9295
+ async function refusalOf(response, operation) {
9296
+ let body = null;
9297
+ try {
9298
+ body = await response.json();
9299
+ } catch {
9300
+ body = null;
9301
+ }
9302
+ const said = readServerRefusal(body);
9303
+ return new MeetError(
9304
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9305
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9306
+ said?.remedy || "Nothing was sent. Try again."
9307
+ );
9308
+ }
9309
+ function asRecipient(row) {
9310
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9311
+ return {
9312
+ key: String(row["key"] ?? ""),
9313
+ name: String(row["name"] ?? ""),
9314
+ email: typeof row["email"] === "string" ? row["email"] : null,
9315
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
9316
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
9317
+ reachable: row["reachable"] !== false
9318
+ };
9319
+ }
9320
+ async function loadFollowUpDraft(api, meetingId) {
9321
+ const operation = "meet.loadFollowUpDraft";
9322
+ const response = await api.request({
9323
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9324
+ method: "GET",
9325
+ requireAuth: true,
9326
+ operation
9327
+ });
9328
+ if (!response.ok) throw await refusalOf(response, operation);
9329
+ const body = await response.json();
9330
+ const last = body["last_sent"];
9331
+ return {
9332
+ subject: String(body["subject"] ?? ""),
9333
+ body: String(body["body"] ?? ""),
9334
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
9335
+ (row) => asRecipient(row)
9336
+ ),
9337
+ wrapUpWritten: body["wrap_up_written"] === true,
9338
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
9339
+ };
9340
+ }
9341
+ async function sendFollowUp(api, meetingId, input) {
9342
+ const operation = "meet.sendFollowUp";
9343
+ const response = await api.request({
9344
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9345
+ method: "POST",
9346
+ requireAuth: true,
9347
+ operation,
9348
+ body: {
9349
+ subject: input.subject,
9350
+ body: input.body,
9351
+ recipient_keys: [...input.recipientKeys],
9352
+ dry_run: input.dryRun === true
9353
+ }
9354
+ });
9355
+ if (!response.ok) throw await refusalOf(response, operation);
9356
+ const body = await response.json();
9357
+ return {
9358
+ dryRun: body["dry_run"] === true,
9359
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
9360
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
9361
+ key: String(row["key"] ?? ""),
9362
+ reason: String(row["reason"] ?? "")
9363
+ })) : []
9364
+ };
9365
+ }
9366
+
8862
9367
  // src/core/management.ts
8863
9368
  async function refusal(response, operation) {
8864
9369
  let body = null;