@agent-native/core 0.114.4 → 0.114.5
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -1
- package/corpus/templates/clips/AGENTS.md +14 -4
- package/corpus/templates/clips/actions/update-meeting.ts +13 -3
- package/corpus/templates/clips/actions/view-screen.ts +1 -0
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +2 -1
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +96 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +11 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +85 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +11 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -7
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +241 -135
- package/corpus/templates/clips/changelog/2026-07-20-meeting-share-links-can-include-the-full-transcript-with-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-recording-retries-the-default-mac-microphone-when-a-saved-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-shared-clips-now-tell-agents-to-wait-while-uploads-and-trans.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-join-url.ts +1 -33
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +65 -2
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -21
- package/corpus/templates/clips/server/plugins/auth.ts +1 -0
- package/corpus/templates/clips/server/plugins/db.ts +6 -0
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +13 -3
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +152 -0
- package/corpus/templates/clips/shared/agent-context.ts +60 -0
- package/corpus/templates/clips/shared/meeting-join-url.ts +36 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/package.json +2 -2
|
@@ -1,91 +1,71 @@
|
|
|
1
1
|
import { appPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
import { useSession } from "@agent-native/core/client/hooks";
|
|
2
3
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
4
|
import { PoweredByBadge } from "@agent-native/core/client/ui";
|
|
4
|
-
import { getRequestUserEmail } from "@agent-native/core/server";
|
|
5
|
-
import { resolveAccess } from "@agent-native/core/sharing";
|
|
6
5
|
import {
|
|
7
6
|
IconCalendar,
|
|
7
|
+
IconCheck,
|
|
8
|
+
IconCopy,
|
|
8
9
|
IconExternalLink,
|
|
9
10
|
IconListCheck,
|
|
11
|
+
IconNotes,
|
|
10
12
|
IconUsers,
|
|
11
13
|
IconWand,
|
|
12
14
|
} from "@tabler/icons-react";
|
|
15
|
+
import { useQuery } from "@tanstack/react-query";
|
|
13
16
|
import { and, eq, isNull } from "drizzle-orm";
|
|
14
|
-
import { useEffect, useRef } from "react";
|
|
17
|
+
import { useEffect, useRef, useState } from "react";
|
|
15
18
|
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
|
|
16
|
-
import { useLoaderData,
|
|
19
|
+
import { useLoaderData, useParams } from "react-router";
|
|
20
|
+
import { toast } from "sonner";
|
|
17
21
|
|
|
18
22
|
import {
|
|
19
23
|
AttendeeStack,
|
|
20
24
|
type AttendeeStackParticipant,
|
|
21
25
|
} from "@/components/meetings/attendee-stack";
|
|
26
|
+
import { TranscriptBubbles } from "@/components/meetings/transcript-bubbles";
|
|
22
27
|
import { Button } from "@/components/ui/button";
|
|
23
28
|
import { Spinner } from "@/components/ui/spinner";
|
|
24
29
|
import enMessages from "@/i18n/en-US";
|
|
30
|
+
import {
|
|
31
|
+
fetchPublicMeeting,
|
|
32
|
+
type PublicMeeting,
|
|
33
|
+
type PublicMeetingResult,
|
|
34
|
+
type PublicMeetingTranscript,
|
|
35
|
+
} from "@/lib/public-meeting";
|
|
25
36
|
|
|
26
37
|
import { getDb, schema } from "../../server/db";
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
id: string;
|
|
30
|
-
text: string;
|
|
31
|
-
assigneeEmail: string | null;
|
|
32
|
-
completedAt: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface Participant {
|
|
36
|
-
email: string;
|
|
37
|
-
name: string | null;
|
|
38
|
-
isOrganizer: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface Bullet {
|
|
42
|
-
text: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ShareMeetingData {
|
|
46
|
-
id: string;
|
|
47
|
-
title: string;
|
|
48
|
-
scheduledStart: string | null;
|
|
49
|
-
summaryMd: string;
|
|
50
|
-
bullets: Bullet[];
|
|
51
|
-
participants: Participant[];
|
|
52
|
-
actionItems: ActionItem[];
|
|
53
|
-
actualStart: string | null;
|
|
54
|
-
actualEnd: string | null;
|
|
55
|
-
transcriptStatus: string | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type LoaderData = { meeting: ShareMeetingData } | { meeting: null };
|
|
39
|
+
type LoaderData = { meeting: PublicMeeting | null };
|
|
59
40
|
|
|
60
41
|
export async function loader({
|
|
61
42
|
params,
|
|
62
43
|
}: LoaderFunctionArgs): Promise<LoaderData> {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
44
|
+
const meetingId = params.meetingId;
|
|
45
|
+
if (!meetingId) return { meeting: null };
|
|
65
46
|
|
|
66
|
-
const [
|
|
47
|
+
const [meeting] = await getDb()
|
|
67
48
|
.select({
|
|
68
49
|
id: schema.meetings.id,
|
|
69
50
|
title: schema.meetings.title,
|
|
70
51
|
scheduledStart: schema.meetings.scheduledStart,
|
|
71
|
-
visibility: schema.meetings.visibility,
|
|
72
52
|
summaryMd: schema.meetings.summaryMd,
|
|
73
53
|
bulletsJson: schema.meetings.bulletsJson,
|
|
74
54
|
actualStart: schema.meetings.actualStart,
|
|
75
55
|
actualEnd: schema.meetings.actualEnd,
|
|
76
56
|
transcriptStatus: schema.meetings.transcriptStatus,
|
|
57
|
+
visibility: schema.meetings.visibility,
|
|
77
58
|
})
|
|
78
59
|
.from(schema.meetings)
|
|
79
|
-
.where(
|
|
60
|
+
.where(
|
|
61
|
+
and(
|
|
62
|
+
eq(schema.meetings.id, meetingId),
|
|
63
|
+
eq(schema.meetings.visibility, "public"),
|
|
64
|
+
isNull(schema.meetings.trashedAt),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
80
67
|
.limit(1);
|
|
81
|
-
|
|
82
|
-
if (!row) return { meeting: null };
|
|
83
|
-
|
|
84
|
-
if (row.visibility !== "public") {
|
|
85
|
-
const userEmail = getRequestUserEmail();
|
|
86
|
-
const access = userEmail ? await resolveAccess("meeting", id) : null;
|
|
87
|
-
if (!access) return { meeting: null };
|
|
88
|
-
}
|
|
68
|
+
if (!meeting) return { meeting: null };
|
|
89
69
|
|
|
90
70
|
const [participants, actionItems] = await Promise.all([
|
|
91
71
|
getDb()
|
|
@@ -95,7 +75,7 @@ export async function loader({
|
|
|
95
75
|
isOrganizer: schema.meetingParticipants.isOrganizer,
|
|
96
76
|
})
|
|
97
77
|
.from(schema.meetingParticipants)
|
|
98
|
-
.where(eq(schema.meetingParticipants.meetingId,
|
|
78
|
+
.where(eq(schema.meetingParticipants.meetingId, meetingId)),
|
|
99
79
|
getDb()
|
|
100
80
|
.select({
|
|
101
81
|
id: schema.meetingActionItems.id,
|
|
@@ -104,38 +84,45 @@ export async function loader({
|
|
|
104
84
|
completedAt: schema.meetingActionItems.completedAt,
|
|
105
85
|
})
|
|
106
86
|
.from(schema.meetingActionItems)
|
|
107
|
-
.where(eq(schema.meetingActionItems.meetingId,
|
|
87
|
+
.where(eq(schema.meetingActionItems.meetingId, meetingId)),
|
|
108
88
|
]);
|
|
109
89
|
|
|
110
|
-
let bullets:
|
|
90
|
+
let bullets: PublicMeeting["bullets"] = [];
|
|
111
91
|
try {
|
|
112
|
-
const parsed = JSON.parse(
|
|
113
|
-
if (Array.isArray(parsed))
|
|
92
|
+
const parsed = JSON.parse(meeting.bulletsJson);
|
|
93
|
+
if (Array.isArray(parsed)) {
|
|
94
|
+
bullets = parsed.filter(
|
|
95
|
+
(bullet): bullet is { text: string } =>
|
|
96
|
+
typeof bullet === "object" &&
|
|
97
|
+
bullet !== null &&
|
|
98
|
+
typeof bullet.text === "string",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
114
101
|
} catch {}
|
|
115
102
|
|
|
116
103
|
return {
|
|
117
104
|
meeting: {
|
|
118
|
-
id:
|
|
119
|
-
title:
|
|
120
|
-
scheduledStart:
|
|
121
|
-
summaryMd:
|
|
105
|
+
id: meeting.id,
|
|
106
|
+
title: meeting.title,
|
|
107
|
+
scheduledStart: meeting.scheduledStart,
|
|
108
|
+
summaryMd: meeting.summaryMd,
|
|
122
109
|
bullets,
|
|
123
110
|
participants,
|
|
124
111
|
actionItems,
|
|
125
|
-
actualStart:
|
|
126
|
-
actualEnd:
|
|
127
|
-
transcriptStatus:
|
|
112
|
+
actualStart: meeting.actualStart,
|
|
113
|
+
actualEnd: meeting.actualEnd,
|
|
114
|
+
transcriptStatus: meeting.transcriptStatus,
|
|
128
115
|
},
|
|
129
116
|
};
|
|
130
117
|
}
|
|
131
118
|
|
|
132
119
|
export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
|
|
133
|
-
const
|
|
134
|
-
const title =
|
|
135
|
-
? `${
|
|
120
|
+
const meeting = loaderData?.meeting;
|
|
121
|
+
const title = meeting?.title
|
|
122
|
+
? `${meeting.title} · Clips`
|
|
136
123
|
: enMessages.shareMeeting.pageTitle;
|
|
137
|
-
const description =
|
|
138
|
-
? `AI meeting notes for "${
|
|
124
|
+
const description = meeting?.title
|
|
125
|
+
? `AI meeting notes for "${meeting.title}"`
|
|
139
126
|
: enMessages.shareMeeting.description;
|
|
140
127
|
return [
|
|
141
128
|
{ title },
|
|
@@ -147,8 +134,8 @@ export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
|
|
|
147
134
|
|
|
148
135
|
export function HydrateFallback() {
|
|
149
136
|
return (
|
|
150
|
-
<div className="flex items-center justify-center
|
|
151
|
-
<Spinner className="
|
|
137
|
+
<div className="flex h-screen w-full items-center justify-center bg-background">
|
|
138
|
+
<Spinner className="size-8 text-muted-foreground" />
|
|
152
139
|
</div>
|
|
153
140
|
);
|
|
154
141
|
}
|
|
@@ -168,60 +155,103 @@ function formatDateTime(iso?: string | null): string {
|
|
|
168
155
|
}
|
|
169
156
|
}
|
|
170
157
|
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Silent client-side revalidation while a meeting is still live or its notes
|
|
176
|
-
* haven't landed yet — the loader is SSR-only otherwise (M9), so a share link
|
|
177
|
-
* opened mid-meeting would never update without a manual reload. Polls the
|
|
178
|
-
* same access-checked loader (via useRevalidator, not a new endpoint) every
|
|
179
|
-
* 5s and stops once notes arrive or after 30 minutes.
|
|
180
|
-
*/
|
|
181
|
-
function useMeetingShareRevalidation(meeting: ShareMeetingData | null) {
|
|
182
|
-
const revalidator = useRevalidator();
|
|
183
|
-
const startedAtRef = useRef<number | null>(null);
|
|
184
|
-
|
|
185
|
-
const isLive = !!meeting && !!meeting.actualStart && !meeting.actualEnd;
|
|
158
|
+
function shouldPollMeeting(meeting: PublicMeeting): boolean {
|
|
159
|
+
const isLive = !!meeting.actualStart && !meeting.actualEnd;
|
|
186
160
|
const transcriptPending =
|
|
187
|
-
meeting
|
|
188
|
-
meeting
|
|
161
|
+
meeting.transcriptStatus === "in_progress" ||
|
|
162
|
+
meeting.transcriptStatus === "pending" ||
|
|
163
|
+
meeting.transcript?.status === "pending";
|
|
189
164
|
const notesAbsentWhileReady =
|
|
190
|
-
!!meeting &&
|
|
191
165
|
meeting.transcriptStatus === "ready" &&
|
|
192
166
|
!meeting.summaryMd &&
|
|
193
167
|
meeting.bullets.length === 0 &&
|
|
194
168
|
meeting.actionItems.length === 0;
|
|
195
|
-
|
|
169
|
+
return isLive || transcriptPending || notesAbsentWhileReady;
|
|
170
|
+
}
|
|
196
171
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}, REVALIDATE_INTERVAL_MS);
|
|
213
|
-
return () => window.clearInterval(interval);
|
|
214
|
-
}, [shouldPoll, revalidator]);
|
|
172
|
+
function transcriptCopyText(
|
|
173
|
+
transcript: PublicMeetingTranscript,
|
|
174
|
+
meLabel: string,
|
|
175
|
+
themLabel: string,
|
|
176
|
+
): string {
|
|
177
|
+
if (transcript.segments.length > 0) {
|
|
178
|
+
return transcript.segments
|
|
179
|
+
.map((segment) => {
|
|
180
|
+
const speaker =
|
|
181
|
+
segment.speaker || (segment.source === "mic" ? meLabel : themLabel);
|
|
182
|
+
return `${speaker}: ${segment.text}`;
|
|
183
|
+
})
|
|
184
|
+
.join("\n");
|
|
185
|
+
}
|
|
186
|
+
return transcript.fullText?.trim() ?? "";
|
|
215
187
|
}
|
|
216
188
|
|
|
189
|
+
const REVALIDATE_INTERVAL_MS = 5_000;
|
|
190
|
+
const REVALIDATE_MAX_DURATION_MS = 30 * 60 * 1000;
|
|
191
|
+
|
|
217
192
|
export default function ShareMeetingRoute() {
|
|
218
193
|
const t = useT();
|
|
219
|
-
const
|
|
220
|
-
|
|
194
|
+
const loaderData = useLoaderData<LoaderData>();
|
|
195
|
+
const { meetingId } = useParams<{ meetingId: string }>();
|
|
196
|
+
const { session, isLoading: sessionLoading } = useSession();
|
|
197
|
+
const pollingStartedAtRef = useRef<number | null>(null);
|
|
198
|
+
const [transcriptCopied, setTranscriptCopied] = useState(false);
|
|
199
|
+
const initialMeetingResult: PublicMeetingResult | undefined =
|
|
200
|
+
loaderData.meeting
|
|
201
|
+
? {
|
|
202
|
+
ok: true,
|
|
203
|
+
status: 200,
|
|
204
|
+
data: { meeting: loaderData.meeting, viewer: null },
|
|
205
|
+
}
|
|
206
|
+
: undefined;
|
|
221
207
|
|
|
222
|
-
|
|
208
|
+
const meetingQuery = useQuery({
|
|
209
|
+
queryKey: [
|
|
210
|
+
"public-meeting",
|
|
211
|
+
meetingId,
|
|
212
|
+
session?.email ?? null,
|
|
213
|
+
session?.orgId ?? null,
|
|
214
|
+
],
|
|
215
|
+
queryFn: ({ signal }) => fetchPublicMeeting(meetingId ?? "", { signal }),
|
|
216
|
+
enabled: !!meetingId && !sessionLoading,
|
|
217
|
+
initialData: initialMeetingResult,
|
|
218
|
+
refetchInterval: (query) => {
|
|
219
|
+
const result = query.state.data;
|
|
220
|
+
const payload = result?.data;
|
|
221
|
+
const meeting =
|
|
222
|
+
result?.ok && payload && "meeting" in payload ? payload.meeting : null;
|
|
223
|
+
if (!meeting || !shouldPollMeeting(meeting)) {
|
|
224
|
+
pollingStartedAtRef.current = null;
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (pollingStartedAtRef.current == null) {
|
|
228
|
+
pollingStartedAtRef.current = Date.now();
|
|
229
|
+
}
|
|
230
|
+
return Date.now() - pollingStartedAtRef.current <
|
|
231
|
+
REVALIDATE_MAX_DURATION_MS
|
|
232
|
+
? REVALIDATE_INTERVAL_MS
|
|
233
|
+
: false;
|
|
234
|
+
},
|
|
235
|
+
refetchIntervalInBackground: false,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const payload = meetingQuery.data?.data;
|
|
239
|
+
const meeting =
|
|
240
|
+
meetingQuery.data?.ok && payload && "meeting" in payload
|
|
241
|
+
? payload.meeting
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (meeting?.title) document.title = `${meeting.title} · Clips`;
|
|
246
|
+
}, [meeting?.title]);
|
|
247
|
+
|
|
248
|
+
if (!meeting && (sessionLoading || meetingQuery.isLoading)) {
|
|
249
|
+
return <HydrateFallback />;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!meeting) {
|
|
223
253
|
return (
|
|
224
|
-
<div className="flex flex-col items-center justify-center
|
|
254
|
+
<div className="flex h-screen w-full flex-col items-center justify-center gap-3 bg-background px-4 text-center">
|
|
225
255
|
<p className="text-sm text-muted-foreground">
|
|
226
256
|
{t("shareMeeting.unavailable")}
|
|
227
257
|
</p>
|
|
@@ -230,41 +260,64 @@ export default function ShareMeetingRoute() {
|
|
|
230
260
|
);
|
|
231
261
|
}
|
|
232
262
|
|
|
233
|
-
const { meeting } = data;
|
|
234
|
-
const { bullets } = meeting;
|
|
235
263
|
const hasNotes =
|
|
236
|
-
!!meeting.summaryMd ||
|
|
237
|
-
|
|
264
|
+
!!meeting.summaryMd ||
|
|
265
|
+
meeting.bullets.length > 0 ||
|
|
266
|
+
meeting.actionItems.length > 0;
|
|
238
267
|
const attendees: AttendeeStackParticipant[] = meeting.participants.map(
|
|
239
|
-
(
|
|
268
|
+
(participant) => ({
|
|
269
|
+
email: participant.email,
|
|
270
|
+
name: participant.name ?? undefined,
|
|
271
|
+
}),
|
|
240
272
|
);
|
|
273
|
+
const transcript = meeting.transcript;
|
|
274
|
+
const copyText = transcript
|
|
275
|
+
? transcriptCopyText(
|
|
276
|
+
transcript,
|
|
277
|
+
t("transcriptBubbles.me"),
|
|
278
|
+
t("transcriptBubbles.them"),
|
|
279
|
+
)
|
|
280
|
+
: "";
|
|
281
|
+
|
|
282
|
+
const handleCopyTranscript = async () => {
|
|
283
|
+
if (!copyText) return;
|
|
284
|
+
try {
|
|
285
|
+
await navigator.clipboard.writeText(copyText);
|
|
286
|
+
setTranscriptCopied(true);
|
|
287
|
+
toast.success(t("shareMeeting.transcriptCopied"));
|
|
288
|
+
window.setTimeout(() => setTranscriptCopied(false), 1_500);
|
|
289
|
+
} catch {
|
|
290
|
+
toast.error(t("shareMeeting.copyTranscriptFailed"));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
241
293
|
|
|
242
294
|
return (
|
|
243
295
|
<div className="min-h-screen bg-background">
|
|
244
296
|
<header className="border-b border-border">
|
|
245
|
-
<div className="max-w-2xl
|
|
297
|
+
<div className="mx-auto flex max-w-2xl items-center gap-3 px-4 py-3">
|
|
246
298
|
<h1 className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
247
299
|
{meeting.title || t("meetingDetail.untitledMeeting")}
|
|
248
300
|
</h1>
|
|
249
301
|
<Button variant="ghost" size="sm" asChild className="shrink-0">
|
|
250
302
|
<a href={appPath("/")} className="gap-1.5">
|
|
251
303
|
{t("shareMeeting.tryClips")}
|
|
252
|
-
<IconExternalLink className="
|
|
304
|
+
<IconExternalLink className="size-3.5" />
|
|
253
305
|
</a>
|
|
254
306
|
</Button>
|
|
255
307
|
</div>
|
|
256
308
|
</header>
|
|
257
|
-
|
|
258
|
-
|
|
309
|
+
|
|
310
|
+
<main className="mx-auto max-w-2xl px-4 py-8">
|
|
311
|
+
<div className="mb-8 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
259
312
|
{meeting.scheduledStart && (
|
|
260
313
|
<span className="inline-flex items-center gap-1">
|
|
261
|
-
<IconCalendar className="
|
|
314
|
+
<IconCalendar className="size-3.5" />
|
|
262
315
|
{formatDateTime(meeting.scheduledStart)}
|
|
263
316
|
</span>
|
|
264
317
|
)}
|
|
265
318
|
{attendees.length > 0 && (
|
|
266
319
|
<span className="inline-flex items-center gap-1.5">
|
|
267
|
-
<IconUsers className="
|
|
320
|
+
<IconUsers className="size-3.5" />
|
|
268
321
|
<AttendeeStack participants={attendees} max={5} size="xs" />
|
|
269
322
|
<span>
|
|
270
323
|
{t("shareMeeting.attendees", { count: attendees.length })}
|
|
@@ -274,37 +327,37 @@ export default function ShareMeetingRoute() {
|
|
|
274
327
|
</div>
|
|
275
328
|
|
|
276
329
|
{!hasNotes ? (
|
|
277
|
-
<p className="text-sm text-muted-foreground
|
|
330
|
+
<p className="text-sm italic text-muted-foreground">
|
|
278
331
|
{t("shareMeeting.noAiNotes")}
|
|
279
332
|
</p>
|
|
280
333
|
) : (
|
|
281
334
|
<div className="space-y-8">
|
|
282
335
|
{meeting.summaryMd && (
|
|
283
336
|
<section>
|
|
284
|
-
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground
|
|
285
|
-
<IconWand className="
|
|
337
|
+
<div className="mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
338
|
+
<IconWand className="size-3.5" />
|
|
286
339
|
{t("shareMeeting.summary")}
|
|
287
340
|
</div>
|
|
288
|
-
<div className="text-sm leading-relaxed
|
|
341
|
+
<div className="whitespace-pre-wrap text-sm leading-relaxed">
|
|
289
342
|
{meeting.summaryMd}
|
|
290
343
|
</div>
|
|
291
344
|
</section>
|
|
292
345
|
)}
|
|
293
346
|
|
|
294
|
-
{bullets.length > 0 && (
|
|
347
|
+
{meeting.bullets.length > 0 && (
|
|
295
348
|
<section>
|
|
296
|
-
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground
|
|
297
|
-
<IconWand className="
|
|
349
|
+
<div className="mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
350
|
+
<IconWand className="size-3.5" />
|
|
298
351
|
{t("shareMeeting.keyPoints")}
|
|
299
352
|
</div>
|
|
300
353
|
<ul className="space-y-2">
|
|
301
|
-
{bullets.map((
|
|
354
|
+
{meeting.bullets.map((bullet, index) => (
|
|
302
355
|
<li
|
|
303
|
-
key={
|
|
356
|
+
key={index}
|
|
304
357
|
className="flex gap-2 text-sm leading-relaxed text-muted-foreground"
|
|
305
358
|
>
|
|
306
359
|
<span>•</span>
|
|
307
|
-
<span className="flex-1">{
|
|
360
|
+
<span className="flex-1">{bullet.text}</span>
|
|
308
361
|
</li>
|
|
309
362
|
))}
|
|
310
363
|
</ul>
|
|
@@ -313,17 +366,20 @@ export default function ShareMeetingRoute() {
|
|
|
313
366
|
|
|
314
367
|
{meeting.actionItems.length > 0 && (
|
|
315
368
|
<section>
|
|
316
|
-
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground
|
|
317
|
-
<IconListCheck className="
|
|
369
|
+
<div className="mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
370
|
+
<IconListCheck className="size-3.5" />
|
|
318
371
|
{t("shareMeeting.actionItems")}
|
|
319
372
|
</div>
|
|
320
373
|
<ul className="space-y-2">
|
|
321
|
-
{meeting.actionItems.map((item
|
|
322
|
-
<li
|
|
374
|
+
{meeting.actionItems.map((item) => (
|
|
375
|
+
<li
|
|
376
|
+
key={item.id}
|
|
377
|
+
className="flex gap-2 text-sm leading-relaxed"
|
|
378
|
+
>
|
|
323
379
|
<span
|
|
324
380
|
className={
|
|
325
381
|
item.completedAt
|
|
326
|
-
? "
|
|
382
|
+
? "text-muted-foreground line-through"
|
|
327
383
|
: ""
|
|
328
384
|
}
|
|
329
385
|
>
|
|
@@ -342,10 +398,60 @@ export default function ShareMeetingRoute() {
|
|
|
342
398
|
</div>
|
|
343
399
|
)}
|
|
344
400
|
|
|
401
|
+
{transcript && (
|
|
402
|
+
<section className="mt-10">
|
|
403
|
+
<div className="mb-3 flex items-center justify-between gap-3">
|
|
404
|
+
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
405
|
+
<IconNotes className="size-3.5" />
|
|
406
|
+
{t("shareMeeting.transcript")}
|
|
407
|
+
</div>
|
|
408
|
+
{copyText && (
|
|
409
|
+
<Button
|
|
410
|
+
type="button"
|
|
411
|
+
variant="ghost"
|
|
412
|
+
size="sm"
|
|
413
|
+
className="h-7 gap-1.5"
|
|
414
|
+
onClick={handleCopyTranscript}
|
|
415
|
+
>
|
|
416
|
+
{transcriptCopied ? (
|
|
417
|
+
<IconCheck className="size-3.5" />
|
|
418
|
+
) : (
|
|
419
|
+
<IconCopy className="size-3.5" />
|
|
420
|
+
)}
|
|
421
|
+
{t("shareMeeting.copyTranscript")}
|
|
422
|
+
</Button>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
{transcript.segments.length > 0 ? (
|
|
426
|
+
<div className="flex h-[36rem] flex-col overflow-hidden rounded-lg border border-border">
|
|
427
|
+
<TranscriptBubbles
|
|
428
|
+
segments={transcript.segments}
|
|
429
|
+
isLive={false}
|
|
430
|
+
recordingId={null}
|
|
431
|
+
onSeek={() => {}}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
) : transcript.fullText ? (
|
|
435
|
+
<div className="whitespace-pre-wrap rounded-lg border border-border p-4 text-sm leading-relaxed">
|
|
436
|
+
{transcript.fullText}
|
|
437
|
+
</div>
|
|
438
|
+
) : (
|
|
439
|
+
<div className="flex min-h-40 flex-col overflow-hidden rounded-lg border border-border">
|
|
440
|
+
<TranscriptBubbles
|
|
441
|
+
segments={[]}
|
|
442
|
+
isLive={false}
|
|
443
|
+
recordingId={null}
|
|
444
|
+
onSeek={() => {}}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</section>
|
|
449
|
+
)}
|
|
450
|
+
|
|
345
451
|
<div className="mt-12">
|
|
346
452
|
<PoweredByBadge />
|
|
347
453
|
</div>
|
|
348
|
-
</
|
|
454
|
+
</main>
|
|
349
455
|
</div>
|
|
350
456
|
);
|
|
351
457
|
}
|
|
@@ -1,33 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function isZoomMeetingHost(hostname: string): boolean {
|
|
4
|
-
return hostname === "zoom.us" || hostname.endsWith(".zoom.us");
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resolve a calendar join URL to the native desktop-app URI when the provider
|
|
9
|
-
* supports one. Unknown providers and unsupported Zoom URL shapes stay on
|
|
10
|
-
* their original HTTPS URL.
|
|
11
|
-
*/
|
|
12
|
-
export function resolveDesktopMeetingJoinUrl(joinUrl: string): string {
|
|
13
|
-
try {
|
|
14
|
-
const url = new URL(joinUrl);
|
|
15
|
-
if (url.protocol !== "https:" || !isZoomMeetingHost(url.hostname)) {
|
|
16
|
-
return joinUrl;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const meetingNumber = ZOOM_JOIN_PATH.exec(url.pathname)?.[1];
|
|
20
|
-
if (!meetingNumber) return joinUrl;
|
|
21
|
-
|
|
22
|
-
const params = new URLSearchParams({
|
|
23
|
-
action: "join",
|
|
24
|
-
confno: meetingNumber,
|
|
25
|
-
});
|
|
26
|
-
const passcode = url.searchParams.get("pwd");
|
|
27
|
-
if (passcode) params.set("pwd", passcode);
|
|
28
|
-
|
|
29
|
-
return `zoommtg://${url.hostname}/join?${params.toString()}`;
|
|
30
|
-
} catch {
|
|
31
|
-
return joinUrl;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
export { resolveNativeMeetingJoinUrl as resolveDesktopMeetingJoinUrl } from "../../../shared/meeting-join-url";
|