@ai-matrx/meet 0.7.6 → 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
  };
@@ -1823,7 +1839,9 @@ function createMeetingSession(options) {
1823
1839
  return identity.userId;
1824
1840
  }
1825
1841
  function knockIdentity() {
1826
- return asParticipantIdentity(identity.userId ?? `guest:${identity.deviceId}`);
1842
+ return asParticipantIdentity(
1843
+ identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
1844
+ );
1827
1845
  }
1828
1846
  function requireMeeting(operation) {
1829
1847
  const meeting = store.snapshot().meeting;
@@ -1858,8 +1876,9 @@ function createMeetingSession(options) {
1858
1876
  verdict.admitted ? "admitted" : "denied",
1859
1877
  verdict.by === null ? null : verdict.by
1860
1878
  );
1861
- if (verdict.admitted && decided === knockIdentity()) {
1862
- options.onAdmitted?.();
1879
+ if (decided === knockIdentity()) {
1880
+ if (verdict.admitted) options.onAdmitted?.();
1881
+ else options.onDenied?.();
1863
1882
  }
1864
1883
  }
1865
1884
  },
@@ -2196,7 +2215,16 @@ var RPCS = {
2196
2215
  /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
2197
2216
  meetingOccurrences: "meet_meeting_occurrences",
2198
2217
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
2199
- 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"
2200
2228
  };
2201
2229
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2202
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";
@@ -2901,12 +2929,117 @@ function createMeetRepository(options) {
2901
2929
  );
2902
2930
  },
2903
2931
  async recordings(meetingId) {
2904
- 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 });
2905
2933
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.recordings");
2906
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
+ }
2907
2971
  }
2908
2972
  };
2909
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
+ }
2910
3043
 
2911
3044
  // src/core/session-identity.ts
2912
3045
  var RESOLVING = Object.freeze({
@@ -4537,6 +4670,11 @@ function MeetRuntime(props) {
4537
4670
  // publish nor subscribe, so the host saying yes has to mint a new one.
4538
4671
  onAdmitted: () => {
4539
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);
4540
4678
  }
4541
4679
  });
4542
4680
  const resolveRoomToken = () => {
@@ -6662,22 +6800,27 @@ function MeetingOverlays() {
6662
6800
  const room = useRoom();
6663
6801
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
6664
6802
  /* @__PURE__ */ jsx3(ReactionOverlay, { reactions }),
6665
- notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ jsxs2("div", { className: "mx-meet__notice-bar", role: "alert", children: [
6666
- /* @__PURE__ */ jsx3("span", { children: notice.text }),
6667
- notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ jsx3(
6668
- "button",
6669
- {
6670
- type: "button",
6671
- className: "mx-meet__primary mx-meet__primary--small",
6672
- onClick: () => {
6673
- dismiss();
6674
- void room.toggleMicrophone();
6675
- },
6676
- children: "Unmute"
6677
- }
6678
- ) : null,
6679
- /* @__PURE__ */ jsx3("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ jsx3(CloseIcon, { width: 14, height: 14 }) })
6680
- ] }) : 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
6681
6824
  ] });
6682
6825
  }
6683
6826
  function ShortcutsSheet({ onClose }) {
@@ -8610,6 +8753,341 @@ function createMeetingInviteHandler(options) {
8610
8753
  };
8611
8754
  }
8612
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
+
8613
9091
  // src/core/management.ts
8614
9092
  async function refusal(response, operation) {
8615
9093
  let body = null;
@@ -8661,6 +9139,7 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
8661
9139
  return await response.json();
8662
9140
  }
8663
9141
  export {
9142
+ ATTENDANCE_CSV_HEADER,
8664
9143
  AiPanel,
8665
9144
  AttendanceNotice,
8666
9145
  Avatar,
@@ -8764,6 +9243,7 @@ export {
8764
9243
  TRANSCRIPTION_TOPIC,
8765
9244
  WifiLowIcon,
8766
9245
  absenceSentence,
9246
+ activeWrapUpSummary,
8767
9247
  announceMeeting,
8768
9248
  applyRoomEvent,
8769
9249
  asCallId,
@@ -8772,6 +9252,9 @@ export {
8772
9252
  asParticipantIdentity,
8773
9253
  asRoomName,
8774
9254
  asUserId,
9255
+ attendanceCsv,
9256
+ attendanceReport,
9257
+ attendanceTotals,
8775
9258
  callInviteAction,
8776
9259
  callInviteText,
8777
9260
  callStateLabel,
@@ -8780,6 +9263,7 @@ export {
8780
9263
  canTransition,
8781
9264
  captionSegment,
8782
9265
  changesPatch,
9266
+ clockPosition,
8783
9267
  computeStage,
8784
9268
  countLabel,
8785
9269
  createCallCenter,
@@ -8820,11 +9304,14 @@ export {
8820
9304
  learnName,
8821
9305
  learnNames,
8822
9306
  listenOnlyNotice,
9307
+ loadFollowUpDraft,
8823
9308
  loadGuestMeetingRecord,
9309
+ loadMeetingChat,
8824
9310
  loadMeetingRecord,
8825
9311
  mapConnectionState,
8826
9312
  mapQuality,
8827
9313
  meetingInviteAction,
9314
+ meetingLength,
8828
9315
  meetingLink,
8829
9316
  meetingPath,
8830
9317
  meetingSlug,
@@ -8839,16 +9326,19 @@ export {
8839
9326
  noteTakerLabel,
8840
9327
  noteTakerParticipant,
8841
9328
  noteTakerStatus,
9329
+ notesDocument,
8842
9330
  orderParticipants,
8843
9331
  parseRoomToken,
8844
9332
  participantSummary,
8845
9333
  playableRecordingFileId,
8846
9334
  projectInvitee,
9335
+ projectLibraryRecording,
8847
9336
  projectNote,
8848
9337
  projectOccurrence,
8849
9338
  projectOverride,
8850
9339
  projectParticipant,
8851
9340
  projectRsvpInvitation,
9341
+ projectSearchHit,
8852
9342
  projectSegment,
8853
9343
  projectUpcoming,
8854
9344
  pushMeetingToGoogleCalendar,
@@ -8856,11 +9346,17 @@ export {
8856
9346
  readableTranscript,
8857
9347
  recordingLabel,
8858
9348
  recordingNotice,
9349
+ recordingOffsetMs,
8859
9350
  roleOf,
8860
9351
  roomTopic,
9352
+ searchTranscript,
9353
+ sendFollowUp,
8861
9354
  setGuestRecordAccess,
8862
9355
  settingsPatch,
8863
9356
  snapshotParticipant,
9357
+ snippetRuns,
9358
+ summarizeMeetingRecordRestrictions,
9359
+ transcriptDocument,
8864
9360
  unavailableFrom,
8865
9361
  useAudioLevel,
8866
9362
  useCalls,
@@ -8896,6 +9392,7 @@ export {
8896
9392
  useStage,
8897
9393
  useTrack,
8898
9394
  userTopic,
8899
- wasRefused
9395
+ wasRefused,
9396
+ wrapUpStatus
8900
9397
  };
8901
9398
  //# sourceMappingURL=react.js.map