@agent-native/core 0.157.4 → 0.157.6
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/templates/clips/actions/create-meeting.ts +19 -1
- package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
- package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
- package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
- package/corpus/templates/clips/netlify.toml +4 -4
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
- package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +28 -11
- package/dist/notifications/routes.d.ts +1 -1
- package/docs/content/actions-access-control.mdx +226 -0
- package/docs/content/actions-advanced.mdx +152 -0
- package/docs/content/actions-defining.mdx +275 -0
- package/docs/content/actions-other-surfaces.mdx +167 -0
- package/docs/content/actions-overview.mdx +214 -0
- package/docs/content/actions-run-context.mdx +180 -0
- package/docs/content/agent-surfaces.mdx +1 -1
- package/docs/content/agent-teams.mdx +1 -1
- package/docs/content/agent-web-surfaces.mdx +1 -1
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/automation-connectors.mdx +1 -1
- package/docs/content/automations.mdx +1 -1
- package/docs/content/blueprint-installer.mdx +1 -1
- package/docs/content/cli-adapters.mdx +1 -1
- package/docs/content/client-agent-chat.mdx +1 -1
- package/docs/content/client-data.mdx +2 -2
- package/docs/content/client-overview.mdx +2 -2
- package/docs/content/components.mdx +1 -1
- package/docs/content/data-programs.mdx +2 -2
- package/docs/content/database.mdx +1 -1
- package/docs/content/deployment.mdx +1 -1
- package/docs/content/docs-components.mdx +14 -3
- package/docs/content/drop-in-agent.mdx +2 -2
- package/docs/content/durable-resume.mdx +1 -1
- package/docs/content/evals.mdx +1 -1
- package/docs/content/extensions.mdx +1 -1
- package/docs/content/external-agents-catalog.mdx +2 -2
- package/docs/content/external-agents.mdx +1 -1
- package/docs/content/generative-ui.mdx +1 -1
- package/docs/content/getting-started.mdx +1 -1
- package/docs/content/http-api.mdx +4 -4
- package/docs/content/human-approval.mdx +1 -1
- package/docs/content/key-concepts.mdx +1 -1
- package/docs/content/mcp-apps.mdx +1 -1
- package/docs/content/mcp-clients.mdx +1 -1
- package/docs/content/mcp-protocol.mdx +1 -1
- package/docs/content/native-chat-ui.mdx +1 -1
- package/docs/content/observability.mdx +1 -1
- package/docs/content/organizations-teams-permissions.mdx +1 -1
- package/docs/content/processors.mdx +1 -1
- package/docs/content/pure-agent-apps.mdx +1 -1
- package/docs/content/recurring-jobs.mdx +1 -1
- package/docs/content/server-database.mdx +1 -1
- package/docs/content/server-overview.mdx +3 -3
- package/docs/content/server-routes.mdx +2 -2
- package/docs/content/template-analytics-developers.mdx +1 -1
- package/docs/content/template-assets-developers.mdx +1 -1
- package/docs/content/template-brain-developers.mdx +1 -1
- package/docs/content/template-calendar-developers.mdx +1 -1
- package/docs/content/template-chat-first-edits.mdx +1 -1
- package/docs/content/template-chat.mdx +1 -1
- package/docs/content/template-content-developers.mdx +1 -1
- package/docs/content/template-forms-developers.mdx +1 -1
- package/docs/content/template-forms.mdx +1 -1
- package/docs/content/template-mail-developers.mdx +2 -2
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/tracking.mdx +1 -1
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/package.json +3 -3
- package/docs/content/actions.mdx +0 -750
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "@/components/ui/tooltip";
|
|
27
27
|
import { cn } from "@/lib/utils";
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { TranscriptSegmentRow } from "../transcript/transcript-segment-row";
|
|
30
30
|
|
|
31
31
|
export interface TranscriptSegment {
|
|
32
32
|
startMs: number;
|
|
@@ -299,9 +299,10 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
|
|
|
299
299
|
const isActive = displaySegments[activeIndex] === seg;
|
|
300
300
|
return (
|
|
301
301
|
<li key={seg.startMs}>
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
<TranscriptSegmentRow
|
|
303
|
+
startMs={seg.startMs}
|
|
304
|
+
active={isActive}
|
|
305
|
+
gutter="panel"
|
|
305
306
|
onClick={(event) => {
|
|
306
307
|
if (hasSelectionWithin(event.currentTarget)) return;
|
|
307
308
|
onSeek(seg.startMs);
|
|
@@ -311,24 +312,17 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
|
|
|
311
312
|
event.preventDefault();
|
|
312
313
|
onSeek(seg.startMs);
|
|
313
314
|
}}
|
|
314
|
-
className={cn(
|
|
315
|
-
"flex w-full cursor-pointer items-start gap-3 px-3 py-2 text-left hover:bg-accent/50",
|
|
316
|
-
isActive && "bg-accent",
|
|
317
|
-
)}
|
|
318
315
|
>
|
|
319
|
-
<span className="shrink-0 pt-0.5 font-mono text-[11px] tabular-nums text-muted-foreground">
|
|
320
|
-
{msToClock(seg.startMs)}
|
|
321
|
-
</span>
|
|
322
316
|
<span
|
|
323
317
|
className={cn(
|
|
324
|
-
"text-sm leading-
|
|
318
|
+
"text-sm leading-normal",
|
|
325
319
|
isActive ? "text-foreground" : "text-foreground/80",
|
|
326
320
|
)}
|
|
327
321
|
dangerouslySetInnerHTML={{
|
|
328
322
|
__html: highlight(seg.text, query),
|
|
329
323
|
}}
|
|
330
324
|
/>
|
|
331
|
-
</
|
|
325
|
+
</TranscriptSegmentRow>
|
|
332
326
|
</li>
|
|
333
327
|
);
|
|
334
328
|
})}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { KeyboardEvent, MouseEvent, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface TranscriptSegmentRowProps {
|
|
6
|
+
startMs: number;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** Momentary jump-to-segment flash, rendered as its own overlay layer so
|
|
10
|
+
* its fade-out is never racing the row's own hover/active transition. */
|
|
11
|
+
highlighted?: boolean;
|
|
12
|
+
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
13
|
+
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
14
|
+
segmentRef?: Ref<HTMLDivElement>;
|
|
15
|
+
tabIndex?: number;
|
|
16
|
+
gutter?: "content" | "panel";
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatTranscriptTimestamp(ms: number): string {
|
|
21
|
+
if (!Number.isFinite(ms) || ms < 0) return "0:00";
|
|
22
|
+
const total = Math.floor(ms / 1000);
|
|
23
|
+
const hours = Math.floor(total / 3600);
|
|
24
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
25
|
+
const seconds = total % 60;
|
|
26
|
+
if (hours > 0) {
|
|
27
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
28
|
+
}
|
|
29
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function TranscriptSegmentRow({
|
|
33
|
+
startMs,
|
|
34
|
+
children,
|
|
35
|
+
active = false,
|
|
36
|
+
highlighted = false,
|
|
37
|
+
onClick,
|
|
38
|
+
onKeyDown,
|
|
39
|
+
segmentRef,
|
|
40
|
+
tabIndex,
|
|
41
|
+
gutter = "content",
|
|
42
|
+
className,
|
|
43
|
+
}: TranscriptSegmentRowProps) {
|
|
44
|
+
const interactive = Boolean(onClick);
|
|
45
|
+
const rowTabIndex = tabIndex ?? (interactive ? 0 : undefined);
|
|
46
|
+
const isFocusable = rowTabIndex !== undefined;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
ref={segmentRef}
|
|
51
|
+
role={interactive ? "button" : isFocusable ? "group" : undefined}
|
|
52
|
+
tabIndex={rowTabIndex}
|
|
53
|
+
onClick={onClick}
|
|
54
|
+
onKeyDown={onKeyDown}
|
|
55
|
+
className={cn(
|
|
56
|
+
"group/segment relative flex items-baseline gap-4 rounded-md py-1.5 text-left text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
57
|
+
// The meeting notes view puts the AI summary front and center and the
|
|
58
|
+
// transcript is reference material you dip into as needed, so it
|
|
59
|
+
// carries less visual weight there. Faded foreground, not
|
|
60
|
+
// text-muted-foreground — that token sits at 45% lightness (same as
|
|
61
|
+
// disabled/placeholder text elsewhere), which reads as inactive
|
|
62
|
+
// rather than merely secondary. The player's transcript panel has no
|
|
63
|
+
// competing summary — it's the primary content, so it stays full
|
|
64
|
+
// strength.
|
|
65
|
+
gutter === "panel" ? "text-foreground" : "text-foreground/70",
|
|
66
|
+
gutter === "content" ? "-mx-2 px-2" : "w-full px-3",
|
|
67
|
+
interactive && "cursor-pointer",
|
|
68
|
+
active && "bg-accent",
|
|
69
|
+
interactive && !active && "hover:bg-accent/50",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{/* Overlay, not a shared background class — its own opacity fade can't
|
|
74
|
+
be raced by the row's hover/active transition (see highlighted prop). */}
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
className={cn(
|
|
78
|
+
"pointer-events-none absolute inset-0 rounded-md bg-yellow-400/15 transition-opacity ease-out",
|
|
79
|
+
highlighted ? "opacity-100 duration-200" : "opacity-0 duration-700",
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
<span className="min-w-0 flex-1 whitespace-pre-wrap">{children}</span>
|
|
83
|
+
<span
|
|
84
|
+
className={cn(
|
|
85
|
+
"pointer-events-none w-12 shrink-0 text-right font-mono text-[10px] tabular-nums text-muted-foreground/80 transition-opacity",
|
|
86
|
+
// The player's transcript list is scanned by timestamp to find a
|
|
87
|
+
// moment — it's primary information there, not hover-reveal
|
|
88
|
+
// supplementary detail like it is in the summary bullets.
|
|
89
|
+
gutter === "panel"
|
|
90
|
+
? "opacity-100"
|
|
91
|
+
: "opacity-0 group-hover/segment:opacity-100 group-focus-visible/segment:opacity-100 [@media(hover:none)]:opacity-100",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{formatTranscriptTimestamp(startMs)}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -1122,6 +1122,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1122
1122
|
searchNextMatch: "Next match",
|
|
1123
1123
|
searchClose: "Close search",
|
|
1124
1124
|
},
|
|
1125
|
+
bulletLink: {
|
|
1126
|
+
jumpToTranscript: "Jump to {{time}} in transcript",
|
|
1127
|
+
noMatchingMoment: "No matching moment found",
|
|
1128
|
+
},
|
|
1125
1129
|
editorLayout: {
|
|
1126
1130
|
trimmed: "Trimmed",
|
|
1127
1131
|
trimFailed: "Trim failed",
|
|
@@ -36,6 +36,10 @@ export interface PublicMeeting {
|
|
|
36
36
|
bullets: PublicMeetingBullet[];
|
|
37
37
|
participants: PublicMeetingParticipant[];
|
|
38
38
|
actionItems: PublicMeetingActionItem[];
|
|
39
|
+
/** Recording owner's email — resolves "mic" segments to the actual
|
|
40
|
+
* recorder for calendar meetings where they aren't the event organizer.
|
|
41
|
+
* Already no more exposed than the per-participant emails on this page. */
|
|
42
|
+
ownerEmail: string | null;
|
|
39
43
|
actualStart: string | null;
|
|
40
44
|
actualEnd: string | null;
|
|
41
45
|
transcriptStatus: string | null;
|
|
@@ -92,6 +92,7 @@ interface Bullet {
|
|
|
92
92
|
interface Meeting {
|
|
93
93
|
id: string;
|
|
94
94
|
title: string;
|
|
95
|
+
ownerEmail?: string | null;
|
|
95
96
|
scheduledStart: string;
|
|
96
97
|
scheduledEnd?: string | null;
|
|
97
98
|
actualStart?: string | null;
|
|
@@ -265,7 +266,7 @@ function ActionItemsByPerson({
|
|
|
265
266
|
key={
|
|
266
267
|
it.id ?? `${it.assigneeEmail ?? "?"}:${it.text}:${index}`
|
|
267
268
|
}
|
|
268
|
-
className="flex items-start gap-2 text-
|
|
269
|
+
className="flex items-start gap-2 text-sm leading-relaxed"
|
|
269
270
|
>
|
|
270
271
|
<button
|
|
271
272
|
type="button"
|
|
@@ -730,12 +731,6 @@ export default function MeetingDetailRoute() {
|
|
|
730
731
|
</Button>
|
|
731
732
|
</DropdownMenuTrigger>
|
|
732
733
|
<DropdownMenuContent align="end" className="w-44">
|
|
733
|
-
{hasSummary && !finalize.isPending && (
|
|
734
|
-
<DropdownMenuItem onSelect={handleFinalize}>
|
|
735
|
-
<IconRefresh className="mr-2 h-4 w-4" />
|
|
736
|
-
{t("meetingDetail.regenerateNotes")}
|
|
737
|
-
</DropdownMenuItem>
|
|
738
|
-
)}
|
|
739
734
|
{isLive && (
|
|
740
735
|
<DropdownMenuItem
|
|
741
736
|
onSelect={(event) => {
|
|
@@ -880,16 +875,37 @@ export default function MeetingDetailRoute() {
|
|
|
880
875
|
notesJustArrived && "animate-in fade-in duration-500",
|
|
881
876
|
)}
|
|
882
877
|
>
|
|
883
|
-
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-4
|
|
878
|
+
<div className="flex h-11 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
|
|
884
879
|
<div className="text-xs font-medium">
|
|
885
880
|
{t("meetingDetail.summary")}
|
|
886
881
|
</div>
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
<
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
882
|
+
<div className="flex items-center gap-2">
|
|
883
|
+
{finalize.isPending && (
|
|
884
|
+
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
885
|
+
<IconLoader2 className="h-3 w-3 animate-spin" />
|
|
886
|
+
{t("meetingDetail.working")}
|
|
887
|
+
</span>
|
|
888
|
+
)}
|
|
889
|
+
{canEdit && hasSummary && (
|
|
890
|
+
<Tooltip>
|
|
891
|
+
<TooltipTrigger asChild>
|
|
892
|
+
<Button
|
|
893
|
+
size="icon"
|
|
894
|
+
variant="ghost"
|
|
895
|
+
className="h-7 w-7 cursor-pointer"
|
|
896
|
+
aria-label={t("meetingDetail.regenerateNotes")}
|
|
897
|
+
disabled={finalize.isPending}
|
|
898
|
+
onClick={handleFinalize}
|
|
899
|
+
>
|
|
900
|
+
<IconRefresh className="h-3.5 w-3.5" />
|
|
901
|
+
</Button>
|
|
902
|
+
</TooltipTrigger>
|
|
903
|
+
<TooltipContent>
|
|
904
|
+
{t("meetingDetail.regenerateNotes")}
|
|
905
|
+
</TooltipContent>
|
|
906
|
+
</Tooltip>
|
|
907
|
+
)}
|
|
908
|
+
</div>
|
|
893
909
|
</div>
|
|
894
910
|
|
|
895
911
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
@@ -905,7 +921,7 @@ export default function MeetingDetailRoute() {
|
|
|
905
921
|
segments={segments}
|
|
906
922
|
onJumpTo={handleJumpToSegment}
|
|
907
923
|
>
|
|
908
|
-
<div className="flex gap-2 text-sm leading-relaxed text-
|
|
924
|
+
<div className="flex gap-2 text-sm leading-relaxed text-foreground">
|
|
909
925
|
<span>•</span>
|
|
910
926
|
<span className="flex-1">{b}</span>
|
|
911
927
|
</div>
|
|
@@ -934,13 +950,22 @@ export default function MeetingDetailRoute() {
|
|
|
934
950
|
|
|
935
951
|
{/* Transcript pane — plain agent-chat-style text layout */}
|
|
936
952
|
<div className="rounded-lg border border-border bg-background min-h-[480px] lg:min-h-0 overflow-hidden flex flex-col">
|
|
937
|
-
<
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
953
|
+
<TranscriptBubbles
|
|
954
|
+
segments={segments}
|
|
955
|
+
isLive={isLive}
|
|
956
|
+
participants={meeting.participants ?? []}
|
|
957
|
+
ownerEmail={meeting.ownerEmail}
|
|
958
|
+
registerScrollTo={(fn) => {
|
|
959
|
+
transcriptScrollToRef.current = fn;
|
|
960
|
+
}}
|
|
961
|
+
title={
|
|
962
|
+
<>
|
|
963
|
+
<IconNotes className="h-3.5 w-3.5" />
|
|
964
|
+
{t("meetingDetail.transcript")}
|
|
965
|
+
</>
|
|
966
|
+
}
|
|
967
|
+
headerActions={
|
|
968
|
+
segments.length > 0 && (
|
|
944
969
|
<Tooltip>
|
|
945
970
|
<TooltipTrigger asChild>
|
|
946
971
|
<Button
|
|
@@ -961,15 +986,8 @@ export default function MeetingDetailRoute() {
|
|
|
961
986
|
{t("meetingDetail.copyFullTranscript")}
|
|
962
987
|
</TooltipContent>
|
|
963
988
|
</Tooltip>
|
|
964
|
-
)
|
|
965
|
-
|
|
966
|
-
</div>
|
|
967
|
-
<TranscriptBubbles
|
|
968
|
-
segments={segments}
|
|
969
|
-
isLive={isLive}
|
|
970
|
-
registerScrollTo={(fn) => {
|
|
971
|
-
transcriptScrollToRef.current = fn;
|
|
972
|
-
}}
|
|
989
|
+
)
|
|
990
|
+
}
|
|
973
991
|
/>
|
|
974
992
|
</div>
|
|
975
993
|
</div>
|
|
@@ -86,6 +86,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
86
86
|
scheduledStart: schema.meetings.scheduledStart,
|
|
87
87
|
summaryMd: schema.meetings.summaryMd,
|
|
88
88
|
bulletsJson: schema.meetings.bulletsJson,
|
|
89
|
+
ownerEmail: schema.meetings.ownerEmail,
|
|
89
90
|
actualStart: schema.meetings.actualStart,
|
|
90
91
|
actualEnd: schema.meetings.actualEnd,
|
|
91
92
|
transcriptStatus: schema.meetings.transcriptStatus,
|
|
@@ -169,6 +170,15 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
169
170
|
}
|
|
170
171
|
} catch {}
|
|
171
172
|
|
|
173
|
+
// The owner's email is only safe to disclose here when it's already public
|
|
174
|
+
// via the attendee list — an unauthenticated viewer must never learn an
|
|
175
|
+
// account email that isn't otherwise visible on this page.
|
|
176
|
+
const ownerEmailIsPublic = participants.some(
|
|
177
|
+
(participant) =>
|
|
178
|
+
participant.email.trim().toLowerCase() ===
|
|
179
|
+
meeting.ownerEmail?.trim().toLowerCase(),
|
|
180
|
+
);
|
|
181
|
+
|
|
172
182
|
return shareMeetingLoaderData(
|
|
173
183
|
{
|
|
174
184
|
meeting: {
|
|
@@ -179,6 +189,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
179
189
|
bullets,
|
|
180
190
|
participants,
|
|
181
191
|
actionItems,
|
|
192
|
+
ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
|
|
182
193
|
actualStart: meeting.actualStart,
|
|
183
194
|
actualEnd: meeting.actualEnd,
|
|
184
195
|
transcriptStatus: meeting.transcriptStatus,
|
|
@@ -363,6 +374,7 @@ export default function ShareMeetingRoute() {
|
|
|
363
374
|
(participant) => ({
|
|
364
375
|
email: participant.email,
|
|
365
376
|
name: participant.name ?? undefined,
|
|
377
|
+
isOrganizer: participant.isOrganizer,
|
|
366
378
|
}),
|
|
367
379
|
);
|
|
368
380
|
const transcript = meeting.transcript;
|
|
@@ -449,7 +461,7 @@ export default function ShareMeetingRoute() {
|
|
|
449
461
|
{meeting.bullets.map((bullet, index) => (
|
|
450
462
|
<li
|
|
451
463
|
key={index}
|
|
452
|
-
className="flex gap-2 text-sm leading-relaxed text-
|
|
464
|
+
className="flex gap-2 text-sm leading-relaxed text-foreground"
|
|
453
465
|
>
|
|
454
466
|
<span>•</span>
|
|
455
467
|
<span className="flex-1">{bullet.text}</span>
|
|
@@ -522,6 +534,8 @@ export default function ShareMeetingRoute() {
|
|
|
522
534
|
<TranscriptBubbles
|
|
523
535
|
segments={transcript.segments}
|
|
524
536
|
isLive={false}
|
|
537
|
+
participants={attendees}
|
|
538
|
+
ownerEmail={meeting.ownerEmail}
|
|
525
539
|
/>
|
|
526
540
|
</div>
|
|
527
541
|
) : transcript.fullText ? (
|
|
@@ -14,10 +14,10 @@ AUTO_CREATE_DEFAULT_ORG = "1"
|
|
|
14
14
|
# Clips post-recording transcription/seekability workers.
|
|
15
15
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
16
16
|
# Netlify Functions run on linux/x64 (AWS Lambda). This tells the deploy build
|
|
17
|
-
# to bundle the matching ffmpeg-static binary into the server bundle so
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# to bundle the matching ffmpeg-static binary into the server bundle so frame
|
|
18
|
+
# extraction, WebM seekability, filmstrips, and audio-only transcription work
|
|
19
|
+
# in production. The deploy guard grants this known media-runtime payload its
|
|
20
|
+
# explicit allowance instead of treating it as accidental bundle growth.
|
|
21
21
|
AGENT_NATIVE_SERVERLESS_FFMPEG_ARCH = "x64"
|
|
22
22
|
|
|
23
23
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
@@ -148,6 +148,15 @@ export default defineEventHandler(async (event) => {
|
|
|
148
148
|
: [];
|
|
149
149
|
const role = access.role;
|
|
150
150
|
|
|
151
|
+
// The owner's email is only safe to disclose here when it's already
|
|
152
|
+
// public via the attendee list — an unauthenticated viewer must never
|
|
153
|
+
// learn an account email that isn't otherwise visible on this page.
|
|
154
|
+
const ownerEmailIsPublic = participants.some(
|
|
155
|
+
(participant) =>
|
|
156
|
+
participant.email.trim().toLowerCase() ===
|
|
157
|
+
meeting.ownerEmail?.trim().toLowerCase(),
|
|
158
|
+
);
|
|
159
|
+
|
|
151
160
|
return {
|
|
152
161
|
meeting: {
|
|
153
162
|
id: meeting.id,
|
|
@@ -160,6 +169,7 @@ export default defineEventHandler(async (event) => {
|
|
|
160
169
|
bullets: parseBullets(meeting.bulletsJson),
|
|
161
170
|
participants,
|
|
162
171
|
actionItems,
|
|
172
|
+
ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
|
|
163
173
|
...(meeting.shareTranscript
|
|
164
174
|
? {
|
|
165
175
|
transcript: transcript
|
|
@@ -3,6 +3,7 @@ export interface TranscriptSegment {
|
|
|
3
3
|
endMs: number;
|
|
4
4
|
text: string;
|
|
5
5
|
source?: "mic" | "system";
|
|
6
|
+
speaker?: string | null;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
const MAX_WORDS_PER_CAPTION = 7;
|
|
@@ -27,6 +28,8 @@ export function parseTranscriptSegments(
|
|
|
27
28
|
segment?.source === "mic" || segment?.source === "system"
|
|
28
29
|
? segment.source
|
|
29
30
|
: undefined,
|
|
31
|
+
speaker:
|
|
32
|
+
typeof segment?.speaker === "string" ? segment.speaker : undefined,
|
|
30
33
|
}))
|
|
31
34
|
.filter(
|
|
32
35
|
(segment) =>
|
|
@@ -43,6 +46,8 @@ export function parseTranscriptSegments(
|
|
|
43
46
|
export function buildCaptionSegmentsFromText(
|
|
44
47
|
text: string,
|
|
45
48
|
durationMs?: number | null,
|
|
49
|
+
source?: TranscriptSegment["source"],
|
|
50
|
+
speaker?: TranscriptSegment["speaker"],
|
|
46
51
|
): TranscriptSegment[] {
|
|
47
52
|
const chunks = splitIntoCaptionChunks(text);
|
|
48
53
|
if (chunks.length === 0) return [];
|
|
@@ -73,6 +78,8 @@ export function buildCaptionSegmentsFromText(
|
|
|
73
78
|
startMs: cursorMs,
|
|
74
79
|
endMs,
|
|
75
80
|
text: chunk,
|
|
81
|
+
source,
|
|
82
|
+
speaker,
|
|
76
83
|
};
|
|
77
84
|
cursorMs = endMs;
|
|
78
85
|
return segment;
|
|
@@ -98,7 +105,12 @@ export function normalizeTranscriptSegments({
|
|
|
98
105
|
.join(" ")
|
|
99
106
|
.trim();
|
|
100
107
|
if (validSegments.length <= 1) {
|
|
101
|
-
return buildCaptionSegmentsFromText(
|
|
108
|
+
return buildCaptionSegmentsFromText(
|
|
109
|
+
sourceText,
|
|
110
|
+
durationMs,
|
|
111
|
+
validSegments[0]?.source,
|
|
112
|
+
validSegments[0]?.speaker,
|
|
113
|
+
);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
return validSegments.flatMap(splitTimedSegmentIntoCaptions);
|
|
@@ -160,7 +172,13 @@ function splitTimedSegmentIntoCaptions(
|
|
|
160
172
|
latestEndMs,
|
|
161
173
|
Math.max(cursorMs + minChunkMs, cursorMs + rawDuration),
|
|
162
174
|
);
|
|
163
|
-
const next = {
|
|
175
|
+
const next = {
|
|
176
|
+
startMs: cursorMs,
|
|
177
|
+
endMs,
|
|
178
|
+
text: chunk,
|
|
179
|
+
source: segment.source,
|
|
180
|
+
speaker: segment.speaker,
|
|
181
|
+
};
|
|
164
182
|
cursorMs = endMs;
|
|
165
183
|
return next;
|
|
166
184
|
});
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
package/dist/deploy/build.js
CHANGED
|
@@ -2918,14 +2918,34 @@ export function bundleYjsRuntimeForServerlessOutput(serverDir, projectCwd) {
|
|
|
2918
2918
|
});
|
|
2919
2919
|
return bareImports;
|
|
2920
2920
|
}
|
|
2921
|
-
// Netlify's hard limit is 250MB unzipped per function;
|
|
2922
|
-
//
|
|
2923
|
-
// once the dead weight is out. Apps that legitimately ship the browser runtime
|
|
2924
|
-
// get its ~85MB on top, so the budget stays a regression signal rather than a
|
|
2925
|
-
// tax on Chromium-backed templates.
|
|
2921
|
+
// Netlify's hard limit is 250MB unzipped per function; keep 10MB of headroom
|
|
2922
|
+
// for packaging variance so a passing guard does not sit on the platform edge.
|
|
2926
2923
|
const NETLIFY_FUNCTION_SIZE_BUDGET_BYTES = 120 * 1024 * 1024;
|
|
2924
|
+
const NETLIFY_FUNCTION_HARD_LIMIT_BYTES = 240 * 1024 * 1024;
|
|
2927
2925
|
const NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES = 100 * 1024 * 1024;
|
|
2928
|
-
|
|
2926
|
+
// Clips intentionally ships ffmpeg-static for server-side frame extraction,
|
|
2927
|
+
// WebM seekability, filmstrips, and audio-only transcription. Keep that known
|
|
2928
|
+
// runtime payload separate from the ordinary bundle-growth budget.
|
|
2929
|
+
const NETLIFY_FFMPEG_RUNTIME_SIZE_ALLOWANCE_BYTES = 80 * 1024 * 1024;
|
|
2930
|
+
function hasBundledFfmpegStaticRuntime(functionDir) {
|
|
2931
|
+
if (!fs.existsSync(functionDir))
|
|
2932
|
+
return false;
|
|
2933
|
+
const binaryName = FFMPEG_STATIC_BINARY_NAMES[0];
|
|
2934
|
+
return fs.existsSync(path.join(functionDir, "node_modules", FFMPEG_STATIC_PACKAGE_NAME, binaryName));
|
|
2935
|
+
}
|
|
2936
|
+
function hasBundledServerlessBrowserRuntime(functionDir) {
|
|
2937
|
+
return fs.existsSync(path.join(functionDir, "node_modules", "@sparticuz", "chromium", "bin", "chromium.br"));
|
|
2938
|
+
}
|
|
2939
|
+
function netlifyFunctionSizeBudget(functionDir) {
|
|
2940
|
+
const allowance = (hasBundledServerlessBrowserRuntime(functionDir)
|
|
2941
|
+
? NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES
|
|
2942
|
+
: 0) +
|
|
2943
|
+
(hasBundledFfmpegStaticRuntime(functionDir)
|
|
2944
|
+
? NETLIFY_FFMPEG_RUNTIME_SIZE_ALLOWANCE_BYTES
|
|
2945
|
+
: 0);
|
|
2946
|
+
return Math.min(NETLIFY_FUNCTION_SIZE_BUDGET_BYTES + allowance, NETLIFY_FUNCTION_HARD_LIMIT_BYTES);
|
|
2947
|
+
}
|
|
2948
|
+
function reportNetlifyFunctionSizes(internalDir, failures) {
|
|
2929
2949
|
if (!fs.existsSync(internalDir))
|
|
2930
2950
|
return;
|
|
2931
2951
|
const functions = fs
|
|
@@ -2947,11 +2967,8 @@ function reportNetlifyFunctionSizes(projectCwd, internalDir, failures) {
|
|
|
2947
2967
|
console.log(`[deploy] Netlify functions: ${functions.length} (${functions
|
|
2948
2968
|
.map((fn) => `${fn.name} ${toMb(fn.size)}MB`)
|
|
2949
2969
|
.join(", ")}) — ${toMb(total)}MB uploaded in total.`);
|
|
2950
|
-
const budget = NETLIFY_FUNCTION_SIZE_BUDGET_BYTES +
|
|
2951
|
-
(findServerlessBrowserRuntimeConsumer(projectCwd)
|
|
2952
|
-
? NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES
|
|
2953
|
-
: 0);
|
|
2954
2970
|
for (const fn of functions) {
|
|
2971
|
+
const budget = netlifyFunctionSizeBudget(fn.dir);
|
|
2955
2972
|
if (fn.size <= budget)
|
|
2956
2973
|
continue;
|
|
2957
2974
|
// node_modules is always the biggest child and names nothing useful; the
|
|
@@ -3113,7 +3130,7 @@ export function assertSingleTemplateNetlifyBuildOutput(projectCwd) {
|
|
|
3113
3130
|
}
|
|
3114
3131
|
}
|
|
3115
3132
|
}
|
|
3116
|
-
reportNetlifyFunctionSizes(
|
|
3133
|
+
reportNetlifyFunctionSizes(internalDir, failures);
|
|
3117
3134
|
if (failures.length > 0) {
|
|
3118
3135
|
throw new Error("[deploy] Netlify deploy guard failed; refusing to publish an output " +
|
|
3119
3136
|
"that would likely serve Netlify 404s or blow the function size limit:\n" +
|