@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.js CHANGED
@@ -212,6 +212,12 @@ var MEET_ROUTES = {
212
212
  meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
213
213
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
214
214
  meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
215
+ /**
216
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
217
+ * body from the wrap-up, who it can go to); `POST` = send what the host
218
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
219
+ */
220
+ meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up",
215
221
  /**
216
222
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
217
223
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -341,10 +347,11 @@ function createTokenClient(options) {
341
347
  throw error;
342
348
  }
343
349
  if (response.status === 401 || response.status === 403) {
350
+ const said = await serverSentence(response);
344
351
  throw new MeetError(
345
352
  "forbidden",
346
- `meet.token: the server refused this join (${response.status})`,
347
- "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."
353
+ said?.message ?? "You can't join this meeting.",
354
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
348
355
  );
349
356
  }
350
357
  if (response.status === 404) {
@@ -355,12 +362,7 @@ function createTokenClient(options) {
355
362
  );
356
363
  }
357
364
  if (response.status === 410) {
358
- let reason = "This meeting was cancelled.";
359
- try {
360
- const body = await response.json();
361
- if (typeof body?.detail?.message === "string") reason = body.detail.message;
362
- } catch {
363
- }
365
+ const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
364
366
  throw new MeetError(
365
367
  "cancelled",
366
368
  reason,
@@ -368,10 +370,11 @@ function createTokenClient(options) {
368
370
  );
369
371
  }
370
372
  if (response.status === 423) {
373
+ const said = await serverSentence(response);
371
374
  throw new MeetError(
372
375
  "room-locked",
373
- "meet.token: the meeting is locked",
374
- "The host locked the room. Ask them to unlock or admit you; this is a normal state, not a failure."
376
+ said?.message ?? "The meeting is locked.",
377
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
375
378
  );
376
379
  }
377
380
  if (response.status === 503) {
@@ -442,6 +445,19 @@ function placeholder(roomName) {
442
445
  lobby: false
443
446
  };
444
447
  }
448
+ async function serverSentence(response) {
449
+ try {
450
+ const body = await response.json();
451
+ const source = body !== null && typeof body.detail === "object" && body.detail !== null ? body.detail : body;
452
+ if (source === null) return null;
453
+ const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
454
+ if (message === null) return null;
455
+ const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
456
+ return { message, remedy };
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
445
461
 
446
462
  // src/core/ai.ts
447
463
  var MEET_ANSWER_EVENTS = {
@@ -885,7 +901,7 @@ var DEFAULT_RING_TIMEOUT_MS = 45e3;
885
901
  function createCallCenter(options) {
886
902
  const { manager, repository, identity } = options;
887
903
  const timers = options.timers ?? {
888
- setTimeout: (run, ms) => setTimeout(run, ms),
904
+ setTimeout: (run, ms2) => setTimeout(run, ms2),
889
905
  clearTimeout: (handle) => clearTimeout(handle),
890
906
  now: () => Date.now()
891
907
  };
@@ -1553,6 +1569,7 @@ var EMPTY = {
1553
1569
  recordingId: null,
1554
1570
  chat: [],
1555
1571
  chatLoaded: false,
1572
+ chatHistoryError: null,
1556
1573
  reactions: [],
1557
1574
  spotlight: null,
1558
1575
  moderationNotice: null,
@@ -1662,7 +1679,7 @@ function createMeetStore(initial = {}) {
1662
1679
  state.names,
1663
1680
  messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
1664
1681
  );
1665
- commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
1682
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
1666
1683
  },
1667
1684
  settleChat(localId, next) {
1668
1685
  const index = state.chat.findIndex((message) => message.id === localId);
@@ -1822,7 +1839,9 @@ function createMeetingSession(options) {
1822
1839
  return identity.userId;
1823
1840
  }
1824
1841
  function knockIdentity() {
1825
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
1842
+ return asParticipantIdentity(
1843
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
1844
+ );
1826
1845
  }
1827
1846
  function requireMeeting(operation) {
1828
1847
  const meeting = store.snapshot().meeting;
@@ -1857,8 +1876,9 @@ function createMeetingSession(options) {
1857
1876
  verdict.admitted ? "admitted" : "denied",
1858
1877
  verdict.by === null ? null : verdict.by
1859
1878
  );
1860
- if (verdict.admitted && decided === knockIdentity()) {
1861
- options.onAdmitted?.();
1879
+ if (decided === knockIdentity()) {
1880
+ if (verdict.admitted) options.onAdmitted?.();
1881
+ else options.onDenied?.();
1862
1882
  }
1863
1883
  }
1864
1884
  },
@@ -2195,7 +2215,16 @@ var RPCS = {
2195
2215
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2196
2216
  meetingOccurrences: "meet_meeting_occurrences",
2197
2217
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
2198
- occurrencesBetween: "meet_occurrences_between"
2218
+ occurrencesBetween: "meet_occurrences_between",
2219
+ /*
2220
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
2221
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
2222
+ * ceiling, never the view.
2223
+ */
2224
+ /** The recordings library. */
2225
+ myRecordings: "meet_my_recordings",
2226
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2227
+ search: "meet_search"
2199
2228
  };
2200
2229
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2201
2230
  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";
@@ -2900,12 +2929,117 @@ function createMeetRepository(options) {
2900
2929
  );
2901
2930
  },
2902
2931
  async recordings(meetingId) {
2903
- const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: false });
2932
+ const response = await db.from(TABLES.recordings).select(RECORDING_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("started_at", { ascending: false });
2904
2933
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
2905
2934
  return (response.data ?? []).map(projectRecording);
2935
+ },
2936
+ async myRecordings({ includeArchived = false, limit = 200 } = {}) {
2937
+ const rows = await guard(
2938
+ "meet.myRecordings",
2939
+ () => db.rpc(RPCS.myRecordings, { p_include_archived: includeArchived, p_limit: limit })
2940
+ );
2941
+ return rows.map(projectLibraryRecording);
2942
+ },
2943
+ async searchMeetings(query, limit = 60) {
2944
+ if (query.trim().length === 0) return [];
2945
+ const rows = await guard(
2946
+ "meet.searchMeetings",
2947
+ () => db.rpc(RPCS.search, { p_query: query, p_limit: limit })
2948
+ );
2949
+ return rows.map(projectSearchHit);
2950
+ },
2951
+ async renameRecording(recordingId, title) {
2952
+ const clean = title === null ? null : title.trim().slice(0, 200) || null;
2953
+ const response = await db.from(TABLES.recordings).update({ title: clean }).eq("id", recordingId).select("id");
2954
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.renameRecording");
2955
+ if ((response.data ?? []).length === 0) {
2956
+ throw normalizeMeetError(
2957
+ { message: "permission denied: only the meeting's host or an editor can rename it", code: "42501" },
2958
+ "meet.renameRecording"
2959
+ );
2960
+ }
2961
+ },
2962
+ async setRecordingArchived(recordingId, archived) {
2963
+ const response = await db.from(TABLES.recordings).update({ deleted_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", recordingId).select("id");
2964
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setRecordingArchived");
2965
+ if ((response.data ?? []).length === 0) {
2966
+ throw normalizeMeetError(
2967
+ { message: "permission denied: only the meeting's host or an editor can archive it", code: "42501" },
2968
+ "meet.setRecordingArchived"
2969
+ );
2970
+ }
2906
2971
  }
2907
2972
  };
2908
2973
  }
2974
+ var LIBRARY_STATES = [
2975
+ "idle",
2976
+ "starting",
2977
+ "recording",
2978
+ "stopping",
2979
+ "processing",
2980
+ "available",
2981
+ "expired",
2982
+ "failed"
2983
+ ];
2984
+ function projectLibraryRecording(row) {
2985
+ const operation = "meet.projectLibraryRecording";
2986
+ const state = text(row, "state");
2987
+ const role = text(row, "my_role");
2988
+ return {
2989
+ id: requireText(row, "recording_id", operation),
2990
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2991
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
2992
+ meetingSlug: requireText(row, "meeting_slug", operation),
2993
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
2994
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
2995
+ title: text(row, "recording_title"),
2996
+ fileId: text(row, "file_id"),
2997
+ startedAt: text(row, "started_at"),
2998
+ endedAt: text(row, "ended_at"),
2999
+ durationMs: num(row, "duration_ms"),
3000
+ sizeBytes: num(row, "size_bytes"),
3001
+ failureReason: text(row, "failure_reason"),
3002
+ archivedAt: text(row, "deleted_at"),
3003
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
3004
+ };
3005
+ }
3006
+ var SEARCH_SOURCES = [
3007
+ "title",
3008
+ "summary",
3009
+ "decision",
3010
+ "action_item",
3011
+ "note",
3012
+ "transcript"
3013
+ ];
3014
+ function projectSearchHit(row) {
3015
+ const operation = "meet.projectSearchHit";
3016
+ const source = text(row, "source");
3017
+ return {
3018
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
3019
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
3020
+ meetingSlug: requireText(row, "meeting_slug", operation),
3021
+ meetingAt: text(row, "meeting_at"),
3022
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
3023
+ refId: requireText(row, "ref_id", operation),
3024
+ speaker: text(row, "speaker"),
3025
+ snippet: text(row, "snippet") ?? "",
3026
+ at: text(row, "said_at"),
3027
+ rank: num(row, "rank") ?? 0
3028
+ };
3029
+ }
3030
+ function snippetRuns(snippet) {
3031
+ const runs = [];
3032
+ const pattern = /⟦([^⟧]*)⟧/g;
3033
+ let last = 0;
3034
+ for (const match of snippet.matchAll(pattern)) {
3035
+ const start = match.index ?? 0;
3036
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
3037
+ runs.push({ text: match[1] ?? "", hit: true });
3038
+ last = start + match[0].length;
3039
+ }
3040
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
3041
+ return runs;
3042
+ }
2909
3043
 
2910
3044
  // src/core/session-identity.ts
2911
3045
  var RESOLVING = Object.freeze({
@@ -4536,6 +4670,11 @@ function MeetRuntime(props) {
4536
4670
  // publish nor subscribe, so the host saying yes has to mint a new one.
4537
4671
  onAdmitted: () => {
4538
4672
  void engine.rejoin();
4673
+ },
4674
+ // A "no" is ALSO a mint: the server refuses it with its own sentence, which
4675
+ // turns the waiting room into the pre-join screen showing that sentence.
4676
+ onDenied: () => {
4677
+ void engine.rejoin().catch(() => void 0);
4539
4678
  }
4540
4679
  });
4541
4680
  const resolveRoomToken = () => {
@@ -4558,6 +4697,10 @@ function MeetRuntime(props) {
4558
4697
  chatReadFor = snapshot.meeting.id;
4559
4698
  void inRoom.loadChat().catch((error) => {
4560
4699
  const normalized = error;
4700
+ store.patch({
4701
+ chatLoaded: true,
4702
+ chatHistoryError: normalized.remedy ?? "Messages sent before you joined could not be read. New messages still arrive."
4703
+ });
4561
4704
  report({
4562
4705
  level: "warn",
4563
4706
  message: normalized.message ?? "meet: the saved chat could not be read",
@@ -6657,22 +6800,27 @@ function MeetingOverlays() {
6657
6800
  const room = useRoom();
6658
6801
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
6659
6802
  /* @__PURE__ */ jsx3(ReactionOverlay, { reactions }),
6660
- notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6661
- /* @__PURE__ */ jsx3("span", { children: notice.text }),
6662
- notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ jsx3(
6663
- "button",
6664
- {
6665
- type: "button",
6666
- className: "mx-meet__primary mx-meet__primary--small",
6667
- onClick: () => {
6668
- dismiss();
6669
- void room.toggleMicrophone();
6670
- },
6671
- children: "Unmute"
6672
- }
6673
- ) : null,
6674
- /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
6675
- ] }) : null
6803
+ notice !== null && notice.kind !== "removed" ? (
6804
+ // A host's request ("the host is asking you to unmute"), not an error:
6805
+ // an assertive status, so it is announced at once without being filed
6806
+ // as a failure (design-system error-box census).
6807
+ /* @__PURE__ */ jsxs2("div", { className: "mx-meet__notice-bar", role: "status", "aria-live": "assertive", children: [
6808
+ /* @__PURE__ */ jsx3("span", { children: notice.text }),
6809
+ notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ jsx3(
6810
+ "button",
6811
+ {
6812
+ type: "button",
6813
+ className: "mx-meet__primary mx-meet__primary--small",
6814
+ onClick: () => {
6815
+ dismiss();
6816
+ void room.toggleMicrophone();
6817
+ },
6818
+ children: "Unmute"
6819
+ }
6820
+ ) : null,
6821
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
6822
+ ] })
6823
+ ) : null
6676
6824
  ] });
6677
6825
  }
6678
6826
  function ShortcutsSheet({ onClose }) {
@@ -6749,7 +6897,8 @@ function ChatPanel({
6749
6897
  };
6750
6898
  return /* @__PURE__ */ jsxs2(PanelShell, { title: "Chat", onClose, children: [
6751
6899
  /* @__PURE__ */ jsxs2("ul", { className: "mx-meet__chat", ref: listRef, children: [
6752
- snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 ? /* @__PURE__ */ jsx3("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,
6900
+ snapshot?.chatHistoryError != null ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: snapshot.chatHistoryError }) : null,
6901
+ snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ jsx3("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ jsx3("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,
6753
6902
  messages.map((message) => /* @__PURE__ */ jsx3(ChatLine, { message, local, people }, message.id))
6754
6903
  ] }),
6755
6904
  /* @__PURE__ */ jsxs2(
@@ -7215,7 +7364,7 @@ function EffectsSettings({ preview }) {
7215
7364
  const effects = useMediaEffects();
7216
7365
  const [error, setError] = useState3(null);
7217
7366
  const fileRef = useRef3(null);
7218
- const supported = effects.backgroundsSupported || preview != null;
7367
+ const supported = effects.backgroundsSupported || preview != null || videoEffectsSupported();
7219
7368
  const apply = (next) => {
7220
7369
  setError(null);
7221
7370
  void effects.set(next);
@@ -7610,7 +7759,7 @@ function PreJoinBody({
7610
7759
  }
7611
7760
  ) : /* @__PURE__ */ jsx3("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ jsx3(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
7612
7761
  mode === "video" && preview.error !== null ? /* @__PURE__ */ jsx3("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
7613
- microphone ? /* @__PURE__ */ jsx3("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__ */ jsx3("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
7762
+ microphone && micStream !== null ? /* @__PURE__ */ jsx3("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__ */ jsx3("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
7614
7763
  ] }),
7615
7764
  /* @__PURE__ */ jsxs2("div", { className: "mx-meet__prejoin-panel", children: [
7616
7765
  /* @__PURE__ */ jsx3("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
@@ -7726,7 +7875,9 @@ function useCameraPreview(deviceId2, effects) {
7726
7875
  }).catch((thrown) => {
7727
7876
  if (!live) return;
7728
7877
  const failure = thrown;
7729
- setError([failure.message, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7878
+ const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
7879
+ const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
7880
+ setError([sentence, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
7730
7881
  });
7731
7882
  return () => {
7732
7883
  live = false;
@@ -8602,6 +8753,341 @@ function createMeetingInviteHandler(options) {
8602
8753
  };
8603
8754
  }
8604
8755
 
8756
+ // src/core/after.ts
8757
+ import { formatDurationMs as formatDurationMs3 } from "@ai-matrx/kit/format";
8758
+ function kindOf(person) {
8759
+ if (person.isAgent) return "assistant";
8760
+ if (person.role === "host") return "host";
8761
+ if (person.role === "cohost") return "cohost";
8762
+ if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
8763
+ return "member";
8764
+ }
8765
+ function ms(iso) {
8766
+ if (iso === null) return null;
8767
+ const at = new Date(iso).getTime();
8768
+ return Number.isNaN(at) ? null : at;
8769
+ }
8770
+ function attendanceReport(meeting, attendees, names = {}) {
8771
+ const end = ms(meeting.endedAt);
8772
+ const rows = attendees.map((person) => {
8773
+ const joined = ms(person.joinedAt);
8774
+ const left = ms(person.leftAt);
8775
+ const until = left ?? end;
8776
+ const duration = joined !== null && until !== null && until >= joined ? until - joined : null;
8777
+ return {
8778
+ id: person.id,
8779
+ identity: person.identity,
8780
+ name: displayNameFor(names, person.identity, person.displayName),
8781
+ userId: person.userId,
8782
+ kind: kindOf(person),
8783
+ joinedAt: person.joinedAt,
8784
+ leftAt: person.leftAt,
8785
+ durationMs: duration,
8786
+ durationToEnd: duration !== null && left === null,
8787
+ admission: person.admissionState
8788
+ };
8789
+ });
8790
+ const rank = {
8791
+ host: 0,
8792
+ cohost: 1,
8793
+ member: 2,
8794
+ "signed-in guest": 3,
8795
+ guest: 4,
8796
+ assistant: 5
8797
+ };
8798
+ return rows.sort((a, b) => {
8799
+ if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
8800
+ return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
8801
+ });
8802
+ }
8803
+ function attendanceTotals(rows) {
8804
+ const humans = rows.filter((row) => row.kind !== "assistant");
8805
+ const attended = humans.filter((row) => row.joinedAt !== null);
8806
+ const timed = attended.filter((row) => row.durationMs !== null);
8807
+ return {
8808
+ people: attended.length,
8809
+ guests: attended.filter((row) => row.kind === "guest" || row.kind === "signed-in guest").length,
8810
+ neverAdmitted: humans.filter((row) => row.joinedAt === null).length,
8811
+ averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
8812
+ };
8813
+ }
8814
+ function csvCell(value) {
8815
+ if (value === null) return "";
8816
+ const text3 = String(value);
8817
+ return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
8818
+ }
8819
+ var ATTENDANCE_CSV_HEADER = [
8820
+ "Meeting",
8821
+ "Meeting started",
8822
+ "Name",
8823
+ "Attended as",
8824
+ "Joined",
8825
+ "Left",
8826
+ "Minutes in the meeting",
8827
+ "Admission"
8828
+ ];
8829
+ function attendanceCsv(reports) {
8830
+ const lines = [ATTENDANCE_CSV_HEADER.join(",")];
8831
+ for (const { meeting, rows } of reports) {
8832
+ for (const row of rows) {
8833
+ lines.push(
8834
+ [
8835
+ meeting.title,
8836
+ meeting.startedAt ?? meeting.scheduledFor,
8837
+ row.name,
8838
+ row.kind,
8839
+ row.joinedAt,
8840
+ row.leftAt,
8841
+ row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
8842
+ row.admission
8843
+ ].map(csvCell).join(",")
8844
+ );
8845
+ }
8846
+ }
8847
+ return `${lines.join("\r\n")}\r
8848
+ `;
8849
+ }
8850
+ function recordingOffsetMs(at, recording) {
8851
+ if (recording === null || recording.state !== "available") return null;
8852
+ const start = ms(recording.startedAt);
8853
+ const moment = ms(at);
8854
+ if (start === null || moment === null || moment < start) return null;
8855
+ const end = ms(recording.endedAt);
8856
+ if (end !== null && moment > end) return null;
8857
+ return moment - start;
8858
+ }
8859
+ function clockPosition(offsetMs) {
8860
+ const total = Math.max(0, Math.floor(offsetMs / 1e3));
8861
+ const h = Math.floor(total / 3600);
8862
+ const m = Math.floor(total % 3600 / 60);
8863
+ const s = total % 60;
8864
+ const ss = String(s).padStart(2, "0");
8865
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8866
+ }
8867
+ function searchTranscript(segments, query, names = {}) {
8868
+ const needle = fold(query.trim());
8869
+ if (needle.length === 0) return [];
8870
+ const out = [];
8871
+ for (const segment of segments) {
8872
+ const hay = fold(segment.text);
8873
+ const ranges = [];
8874
+ let from = 0;
8875
+ for (; ; ) {
8876
+ const at = hay.indexOf(needle, from);
8877
+ if (at < 0) break;
8878
+ ranges.push([at, at + needle.length]);
8879
+ from = at + needle.length;
8880
+ }
8881
+ const speaker = fold(displayNameFor(names, segment.identity, segment.speaker));
8882
+ if (ranges.length > 0 || speaker.includes(needle)) {
8883
+ out.push({ segmentId: segment.id, ranges });
8884
+ }
8885
+ }
8886
+ return out;
8887
+ }
8888
+ function fold(value) {
8889
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, (mark) => mark.length === 1 ? "" : mark).toLowerCase();
8890
+ }
8891
+ function clockOfDay(iso, options) {
8892
+ const at = new Date(iso);
8893
+ if (Number.isNaN(at.getTime())) return "";
8894
+ return at.toLocaleTimeString(options.locale, {
8895
+ hour: "numeric",
8896
+ minute: "2-digit",
8897
+ second: "2-digit",
8898
+ ...options.timeZone !== void 0 ? { timeZone: options.timeZone } : {}
8899
+ });
8900
+ }
8901
+ function vttStamp(offsetMs) {
8902
+ const total = Math.max(0, Math.round(offsetMs));
8903
+ const h = Math.floor(total / 36e5);
8904
+ const m = Math.floor(total % 36e5 / 6e4);
8905
+ const s = Math.floor(total % 6e4 / 1e3);
8906
+ const milli = total % 1e3;
8907
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(milli).padStart(3, "0")}`;
8908
+ }
8909
+ function transcriptDocument(bundle, format, options = {}) {
8910
+ const names = options.names ?? bundle.names;
8911
+ const lines = bundle.transcript.filter((line) => line.text.trim().length > 0);
8912
+ const speaker = (line) => displayNameFor(names, line.identity, line.speaker);
8913
+ if (format === "vtt") {
8914
+ const zeroIso = bundle.recording?.state === "available" && bundle.recording.startedAt !== null ? bundle.recording.startedAt : bundle.meeting.startedAt ?? lines[0]?.startedAt ?? null;
8915
+ const zero = ms(zeroIso) ?? 0;
8916
+ const cues = ["WEBVTT", ""];
8917
+ lines.forEach((line, index) => {
8918
+ const start = (ms(line.startedAt) ?? zero) - zero;
8919
+ if (start < 0) return;
8920
+ const next = lines[index + 1];
8921
+ const nextStart = next !== void 0 ? (ms(next.startedAt) ?? zero) - zero : start + 4e3;
8922
+ const end = Math.max(start + 500, Math.min(nextStart, start + 1e4));
8923
+ cues.push(`${vttStamp(start)} --> ${vttStamp(end)}`);
8924
+ cues.push(`<v ${speaker(line).replace(/[<>]/g, "")}>${line.text.replace(/-->/g, "->")}`);
8925
+ cues.push("");
8926
+ });
8927
+ return cues.join("\n");
8928
+ }
8929
+ const out = [];
8930
+ const title = bundle.meeting.title;
8931
+ out.push(format === "md" ? `# ${title} \u2014 transcript` : `${title} \u2014 transcript`);
8932
+ out.push("");
8933
+ let last = null;
8934
+ for (const line of lines) {
8935
+ if (line.identity !== last) {
8936
+ if (last !== null) out.push("");
8937
+ const who = speaker(line);
8938
+ const clock = clockOfDay(line.startedAt, options);
8939
+ out.push(format === "md" ? `**${who}** \xB7 ${clock}` : `${who} (${clock})`);
8940
+ last = line.identity;
8941
+ }
8942
+ out.push(line.text.trim());
8943
+ }
8944
+ if (lines.length === 0) out.push("No transcript was captured for this meeting.");
8945
+ return `${out.join("\n")}
8946
+ `;
8947
+ }
8948
+ function bullet(note) {
8949
+ const owner = note.assigneeDisplayName !== null ? ` \u2014 ${note.assigneeDisplayName}` : "";
8950
+ return `- ${note.text.trim()}${owner}`;
8951
+ }
8952
+ function notesDocument(bundle, options = {}) {
8953
+ const out = [`# ${bundle.meeting.title}`, ""];
8954
+ if (options.when) out.push(`_${options.when}_`, "");
8955
+ const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
8956
+ if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
8957
+ out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
8958
+ out.push("## Decisions", "");
8959
+ out.push(...bundle.decisions.length > 0 ? bundle.decisions.map(bullet) : ["None recorded."], "");
8960
+ out.push("## Action items", "");
8961
+ out.push(
8962
+ ...bundle.actionItems.length > 0 ? bundle.actionItems.map(bullet) : ["None recorded."],
8963
+ ""
8964
+ );
8965
+ if (options.link) out.push(`[Open the meeting record](${options.link})`, "");
8966
+ return out.join("\n");
8967
+ }
8968
+ function meetingLength(meeting) {
8969
+ const start = ms(meeting.startedAt);
8970
+ const end = ms(meeting.endedAt);
8971
+ if (start === null || end === null || end < start) return null;
8972
+ return formatDurationMs3(end - start, { style: "long" });
8973
+ }
8974
+ async function loadMeetingChat(api, meetingId, roomToken = null) {
8975
+ const operation = "meet.loadMeetingChat";
8976
+ let response;
8977
+ try {
8978
+ response = await api.request({
8979
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
8980
+ method: "GET",
8981
+ operation,
8982
+ requireAuth: roomToken === null,
8983
+ ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
8984
+ });
8985
+ } catch (error) {
8986
+ return {
8987
+ messages: [],
8988
+ refusal: {
8989
+ message: `The chat could not be read (${error.message}).`,
8990
+ remedy: error.remedy ?? "Reload the page."
8991
+ }
8992
+ };
8993
+ }
8994
+ if (!response.ok) {
8995
+ let body = null;
8996
+ try {
8997
+ body = await response.json();
8998
+ } catch {
8999
+ body = null;
9000
+ }
9001
+ const said = readServerRefusal(body);
9002
+ return {
9003
+ messages: [],
9004
+ refusal: {
9005
+ message: said?.reason ?? `The chat could not be read (the server answered ${response.status}).`,
9006
+ remedy: said?.remedy || "Reload the page."
9007
+ }
9008
+ };
9009
+ }
9010
+ const payload = await response.json();
9011
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
9012
+ return {
9013
+ messages: rows.map(
9014
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
9015
+ ).filter((message) => message !== null),
9016
+ refusal: null
9017
+ };
9018
+ }
9019
+ async function refusalOf(response, operation) {
9020
+ let body = null;
9021
+ try {
9022
+ body = await response.json();
9023
+ } catch {
9024
+ body = null;
9025
+ }
9026
+ const said = readServerRefusal(body);
9027
+ return new MeetError(
9028
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
9029
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
9030
+ said?.remedy || "Nothing was sent. Try again."
9031
+ );
9032
+ }
9033
+ function asRecipient(row) {
9034
+ const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
9035
+ return {
9036
+ key: String(row["key"] ?? ""),
9037
+ name: String(row["name"] ?? ""),
9038
+ email: typeof row["email"] === "string" ? row["email"] : null,
9039
+ userId: typeof row["user_id"] === "string" ? row["user_id"] : null,
9040
+ sources: sources.filter((s) => s === "invitee" || s === "attendee"),
9041
+ reachable: row["reachable"] !== false
9042
+ };
9043
+ }
9044
+ async function loadFollowUpDraft(api, meetingId) {
9045
+ const operation = "meet.loadFollowUpDraft";
9046
+ const response = await api.request({
9047
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9048
+ method: "GET",
9049
+ requireAuth: true,
9050
+ operation
9051
+ });
9052
+ if (!response.ok) throw await refusalOf(response, operation);
9053
+ const body = await response.json();
9054
+ const last = body["last_sent"];
9055
+ return {
9056
+ subject: String(body["subject"] ?? ""),
9057
+ body: String(body["body"] ?? ""),
9058
+ recipients: (Array.isArray(body["recipients"]) ? body["recipients"] : []).map(
9059
+ (row) => asRecipient(row)
9060
+ ),
9061
+ wrapUpWritten: body["wrap_up_written"] === true,
9062
+ lastSent: last && typeof last["at"] === "string" ? { at: last["at"], count: typeof last["count"] === "number" ? last["count"] : 0 } : null
9063
+ };
9064
+ }
9065
+ async function sendFollowUp(api, meetingId, input) {
9066
+ const operation = "meet.sendFollowUp";
9067
+ const response = await api.request({
9068
+ path: meetingPath(MEET_ROUTES.meetingFollowUp, meetingId),
9069
+ method: "POST",
9070
+ requireAuth: true,
9071
+ operation,
9072
+ body: {
9073
+ subject: input.subject,
9074
+ body: input.body,
9075
+ recipient_keys: [...input.recipientKeys],
9076
+ dry_run: input.dryRun === true
9077
+ }
9078
+ });
9079
+ if (!response.ok) throw await refusalOf(response, operation);
9080
+ const body = await response.json();
9081
+ return {
9082
+ dryRun: body["dry_run"] === true,
9083
+ queued: Array.isArray(body["queued"]) ? body["queued"] : [],
9084
+ skipped: Array.isArray(body["skipped"]) ? body["skipped"].map((row) => ({
9085
+ key: String(row["key"] ?? ""),
9086
+ reason: String(row["reason"] ?? "")
9087
+ })) : []
9088
+ };
9089
+ }
9090
+
8605
9091
  // src/core/management.ts
8606
9092
  async function refusal(response, operation) {
8607
9093
  let body = null;
@@ -8653,6 +9139,7 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
8653
9139
  return await response.json();
8654
9140
  }
8655
9141
  export {
9142
+ ATTENDANCE_CSV_HEADER,
8656
9143
  AiPanel,
8657
9144
  AttendanceNotice,
8658
9145
  Avatar,
@@ -8756,6 +9243,7 @@ export {
8756
9243
  TRANSCRIPTION_TOPIC,
8757
9244
  WifiLowIcon,
8758
9245
  absenceSentence,
9246
+ activeWrapUpSummary,
8759
9247
  announceMeeting,
8760
9248
  applyRoomEvent,
8761
9249
  asCallId,
@@ -8764,6 +9252,9 @@ export {
8764
9252
  asParticipantIdentity,
8765
9253
  asRoomName,
8766
9254
  asUserId,
9255
+ attendanceCsv,
9256
+ attendanceReport,
9257
+ attendanceTotals,
8767
9258
  callInviteAction,
8768
9259
  callInviteText,
8769
9260
  callStateLabel,
@@ -8772,6 +9263,7 @@ export {
8772
9263
  canTransition,
8773
9264
  captionSegment,
8774
9265
  changesPatch,
9266
+ clockPosition,
8775
9267
  computeStage,
8776
9268
  countLabel,
8777
9269
  createCallCenter,
@@ -8812,11 +9304,14 @@ export {
8812
9304
  learnName,
8813
9305
  learnNames,
8814
9306
  listenOnlyNotice,
9307
+ loadFollowUpDraft,
8815
9308
  loadGuestMeetingRecord,
9309
+ loadMeetingChat,
8816
9310
  loadMeetingRecord,
8817
9311
  mapConnectionState,
8818
9312
  mapQuality,
8819
9313
  meetingInviteAction,
9314
+ meetingLength,
8820
9315
  meetingLink,
8821
9316
  meetingPath,
8822
9317
  meetingSlug,
@@ -8831,16 +9326,19 @@ export {
8831
9326
  noteTakerLabel,
8832
9327
  noteTakerParticipant,
8833
9328
  noteTakerStatus,
9329
+ notesDocument,
8834
9330
  orderParticipants,
8835
9331
  parseRoomToken,
8836
9332
  participantSummary,
8837
9333
  playableRecordingFileId,
8838
9334
  projectInvitee,
9335
+ projectLibraryRecording,
8839
9336
  projectNote,
8840
9337
  projectOccurrence,
8841
9338
  projectOverride,
8842
9339
  projectParticipant,
8843
9340
  projectRsvpInvitation,
9341
+ projectSearchHit,
8844
9342
  projectSegment,
8845
9343
  projectUpcoming,
8846
9344
  pushMeetingToGoogleCalendar,
@@ -8848,11 +9346,17 @@ export {
8848
9346
  readableTranscript,
8849
9347
  recordingLabel,
8850
9348
  recordingNotice,
9349
+ recordingOffsetMs,
8851
9350
  roleOf,
8852
9351
  roomTopic,
9352
+ searchTranscript,
9353
+ sendFollowUp,
8853
9354
  setGuestRecordAccess,
8854
9355
  settingsPatch,
8855
9356
  snapshotParticipant,
9357
+ snippetRuns,
9358
+ summarizeMeetingRecordRestrictions,
9359
+ transcriptDocument,
8856
9360
  unavailableFrom,
8857
9361
  useAudioLevel,
8858
9362
  useCalls,
@@ -8888,6 +9392,7 @@ export {
8888
9392
  useStage,
8889
9393
  useTrack,
8890
9394
  userTopic,
8891
- wasRefused
9395
+ wasRefused,
9396
+ wrapUpStatus
8892
9397
  };
8893
9398
  //# sourceMappingURL=react.js.map