@agent-native/core 0.128.1 → 0.128.2
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 +6 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/server/email-template.ts +33 -0
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/clips/AGENTS.md +6 -1
- package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
- package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
- package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
- package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
- package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
- package/corpus/templates/clips/app/global.css +23 -0
- package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
- package/corpus/templates/clips/app/routes/_app.tsx +2 -0
- package/corpus/templates/clips/app/routes/import.tsx +396 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
- package/corpus/templates/clips/app/routes/record.tsx +13 -71
- package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
- package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/AGENTS.md +8 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
- package/corpus/templates/content/actions/_property-utils.ts +32 -2
- package/corpus/templates/content/actions/configure-document-property.ts +12 -2
- package/corpus/templates/content/actions/delete-document-property.ts +13 -3
- package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
- package/corpus/templates/content/actions/list-document-properties.ts +12 -3
- package/corpus/templates/content/actions/move-database-item.ts +74 -55
- package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
- package/corpus/templates/content/actions/set-document-property.ts +10 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
- package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
- package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
- package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
- package/corpus/templates/content/app/i18n-data.ts +261 -3
- package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
- package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
- package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
- package/corpus/templates/content/shared/api.ts +28 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/email-template.d.ts +7 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +24 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/package.json +4 -3
- package/src/server/email-template.ts +33 -0
- package/src/templates/chat/_gitignore +2 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentNativePath,
|
|
3
|
+
appBasePath,
|
|
4
|
+
} from "@agent-native/core/client/api-path";
|
|
5
|
+
import { callAction } from "@agent-native/core/client/hooks";
|
|
6
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import {
|
|
8
|
+
IconArrowLeft,
|
|
9
|
+
IconCheck,
|
|
10
|
+
IconLink,
|
|
11
|
+
IconSparkles,
|
|
12
|
+
IconUpload,
|
|
13
|
+
IconVideo,
|
|
14
|
+
} from "@tabler/icons-react";
|
|
15
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
16
|
+
import {
|
|
17
|
+
useCallback,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
type FormEvent,
|
|
23
|
+
} from "react";
|
|
24
|
+
import { Link, useLocation, useNavigate } from "react-router";
|
|
25
|
+
import { toast } from "sonner";
|
|
26
|
+
|
|
27
|
+
import { StorageSetupCard } from "@/components/recorder/storage-setup-card";
|
|
28
|
+
import { Button } from "@/components/ui/button";
|
|
29
|
+
import { Input } from "@/components/ui/input";
|
|
30
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
31
|
+
import {
|
|
32
|
+
VIDEO_STORAGE_STATUS_KEY,
|
|
33
|
+
useVideoStorageStatus,
|
|
34
|
+
type VideoStorageStatus,
|
|
35
|
+
} from "@/hooks/use-video-storage-status";
|
|
36
|
+
import enMessages from "@/i18n/en-US";
|
|
37
|
+
import { cn } from "@/lib/utils";
|
|
38
|
+
|
|
39
|
+
type ImportPhase = "form" | "importing" | "done" | "leaving";
|
|
40
|
+
|
|
41
|
+
export function meta() {
|
|
42
|
+
return [{ title: enMessages.importRoute.pageTitle }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function recordingLink(recordingId: string): string {
|
|
46
|
+
const path = `${appBasePath()}/r/${encodeURIComponent(recordingId)}`;
|
|
47
|
+
if (typeof window === "undefined") return path;
|
|
48
|
+
return new URL(path, window.location.origin).toString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function copyRecordingLink(recordingId: string): Promise<void> {
|
|
52
|
+
if (typeof navigator === "undefined") return;
|
|
53
|
+
if (!navigator.clipboard?.writeText) return;
|
|
54
|
+
await navigator.clipboard
|
|
55
|
+
.writeText(recordingLink(recordingId))
|
|
56
|
+
.catch(() => undefined);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function writeNavigateAppState(recordingId: string): Promise<void> {
|
|
60
|
+
await fetch(agentNativePath("/_agent-native/application-state/navigate"), {
|
|
61
|
+
method: "PUT",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ view: "recording", recordingId }),
|
|
64
|
+
}).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function userFacingActionErrorMessage(error: string): string {
|
|
68
|
+
return error.replace(/^Action [a-z0-9-]+ failed:\s*/i, "").trim() || error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ImportPanelSkeleton() {
|
|
72
|
+
return (
|
|
73
|
+
<div className="mx-auto w-full max-w-lg overflow-hidden rounded-2xl border border-border bg-card p-6 shadow-lg">
|
|
74
|
+
<Skeleton className="h-12 w-full rounded-lg" />
|
|
75
|
+
<Skeleton className="mt-3 h-12 w-full rounded-lg" />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default function ImportRoute() {
|
|
81
|
+
const t = useT();
|
|
82
|
+
const navigate = useNavigate();
|
|
83
|
+
const location = useLocation();
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
const storageQuery = useVideoStorageStatus();
|
|
86
|
+
const storageConfigured: boolean | null = storageQuery.isLoading
|
|
87
|
+
? null
|
|
88
|
+
: !!storageQuery.data?.configured;
|
|
89
|
+
|
|
90
|
+
const spaceIdFromUrl = useMemo(() => {
|
|
91
|
+
const params = new URLSearchParams(location.search);
|
|
92
|
+
return params.get("spaceId") || null;
|
|
93
|
+
}, [location.search]);
|
|
94
|
+
const folderIdFromUrl = useMemo(() => {
|
|
95
|
+
const params = new URLSearchParams(location.search);
|
|
96
|
+
return params.get("folderId") || null;
|
|
97
|
+
}, [location.search]);
|
|
98
|
+
|
|
99
|
+
const recordHref = useMemo(() => {
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
|
|
102
|
+
if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
|
|
103
|
+
const qs = params.toString();
|
|
104
|
+
return qs ? `/record?${qs}` : "/record";
|
|
105
|
+
}, [spaceIdFromUrl, folderIdFromUrl]);
|
|
106
|
+
|
|
107
|
+
const uploadHref = useMemo(() => {
|
|
108
|
+
const params = new URLSearchParams();
|
|
109
|
+
if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
|
|
110
|
+
if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
|
|
111
|
+
params.set("autoUpload", "1");
|
|
112
|
+
return `/record?${params.toString()}`;
|
|
113
|
+
}, [spaceIdFromUrl, folderIdFromUrl]);
|
|
114
|
+
|
|
115
|
+
const [loomUrl, setLoomUrl] = useState("");
|
|
116
|
+
const [phase, setPhase] = useState<ImportPhase>("form");
|
|
117
|
+
const [loomError, setLoomError] = useState<string | null>(null);
|
|
118
|
+
const [stageIndex, setStageIndex] = useState(0);
|
|
119
|
+
const [progress, setProgress] = useState(0);
|
|
120
|
+
const stageTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
121
|
+
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
122
|
+
const timeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
|
123
|
+
|
|
124
|
+
const busy = phase !== "form";
|
|
125
|
+
|
|
126
|
+
const importStages = useMemo(
|
|
127
|
+
() => [
|
|
128
|
+
t("importRoute.stageFetching"),
|
|
129
|
+
t("importRoute.stageUploading"),
|
|
130
|
+
t("importRoute.stageTranscript"),
|
|
131
|
+
t("importRoute.stageFinalizing"),
|
|
132
|
+
],
|
|
133
|
+
[t],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const benefits = useMemo(
|
|
137
|
+
() => [
|
|
138
|
+
t("importRoute.benefitTranscript"),
|
|
139
|
+
t("importRoute.benefitQueryable"),
|
|
140
|
+
t("importRoute.benefitSummaries"),
|
|
141
|
+
t("importRoute.benefitPrimitive"),
|
|
142
|
+
],
|
|
143
|
+
[t],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const clearTimers = useCallback(() => {
|
|
147
|
+
if (stageTimerRef.current) clearInterval(stageTimerRef.current);
|
|
148
|
+
if (progressTimerRef.current) clearInterval(progressTimerRef.current);
|
|
149
|
+
stageTimerRef.current = null;
|
|
150
|
+
progressTimerRef.current = null;
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
return () => {
|
|
155
|
+
clearTimers();
|
|
156
|
+
timeoutsRef.current.forEach((id) => clearTimeout(id));
|
|
157
|
+
};
|
|
158
|
+
}, [clearTimers]);
|
|
159
|
+
|
|
160
|
+
const handleSubmit = useCallback(
|
|
161
|
+
async (event: FormEvent<HTMLFormElement>) => {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const url = loomUrl.trim();
|
|
164
|
+
if (!url || busy) return;
|
|
165
|
+
|
|
166
|
+
setLoomError(null);
|
|
167
|
+
setPhase("importing");
|
|
168
|
+
setStageIndex(0);
|
|
169
|
+
setProgress(6);
|
|
170
|
+
stageTimerRef.current = setInterval(() => {
|
|
171
|
+
setStageIndex((prev) => Math.min(prev + 1, importStages.length - 1));
|
|
172
|
+
}, 1300);
|
|
173
|
+
// Ease the accent bar toward ~92% while the request is in flight; the
|
|
174
|
+
// real completion snaps it to 100%.
|
|
175
|
+
progressTimerRef.current = setInterval(() => {
|
|
176
|
+
setProgress((p) => (p >= 92 ? p : p + Math.max(0.6, (92 - p) * 0.07)));
|
|
177
|
+
}, 120);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const result = (await callAction(
|
|
181
|
+
"import-loom-recording" as any,
|
|
182
|
+
{
|
|
183
|
+
url,
|
|
184
|
+
spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
|
|
185
|
+
folderId: folderIdFromUrl ?? undefined,
|
|
186
|
+
} as any,
|
|
187
|
+
)) as {
|
|
188
|
+
recordingId?: string;
|
|
189
|
+
status?: string;
|
|
190
|
+
storageSetupRequired?: boolean;
|
|
191
|
+
};
|
|
192
|
+
const recordingId = result?.recordingId;
|
|
193
|
+
if (!recordingId) {
|
|
194
|
+
throw new Error("Loom import did not return a recording id.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clearTimers();
|
|
198
|
+
setStageIndex(importStages.length - 1);
|
|
199
|
+
setProgress(100);
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
result?.storageSetupRequired ||
|
|
203
|
+
result?.status === "waiting_storage"
|
|
204
|
+
) {
|
|
205
|
+
toast.info(t("recordRoute.storageNeededToFinishLoomImport"), {
|
|
206
|
+
description: t("recordRoute.connectStorageToRetryLoom"),
|
|
207
|
+
duration: 12_000,
|
|
208
|
+
});
|
|
209
|
+
await writeNavigateAppState(recordingId);
|
|
210
|
+
navigate(`/r/${recordingId}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await copyRecordingLink(recordingId);
|
|
215
|
+
await writeNavigateAppState(recordingId);
|
|
216
|
+
|
|
217
|
+
// Linger on the "done" reveal, then fade out into the clip.
|
|
218
|
+
setPhase("done");
|
|
219
|
+
timeoutsRef.current.push(
|
|
220
|
+
setTimeout(() => setPhase("leaving"), 1900),
|
|
221
|
+
setTimeout(() => navigate(`/r/${recordingId}`), 2300),
|
|
222
|
+
);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
clearTimers();
|
|
225
|
+
setProgress(0);
|
|
226
|
+
setPhase("form");
|
|
227
|
+
setLoomError(
|
|
228
|
+
err instanceof Error
|
|
229
|
+
? userFacingActionErrorMessage(err.message)
|
|
230
|
+
: t("recordRoute.couldNotImportLoom"),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[
|
|
235
|
+
busy,
|
|
236
|
+
clearTimers,
|
|
237
|
+
folderIdFromUrl,
|
|
238
|
+
importStages.length,
|
|
239
|
+
loomUrl,
|
|
240
|
+
navigate,
|
|
241
|
+
spaceIdFromUrl,
|
|
242
|
+
t,
|
|
243
|
+
],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const markStorageConfigured = useCallback(() => {
|
|
247
|
+
queryClient.setQueryData<VideoStorageStatus>(
|
|
248
|
+
VIDEO_STORAGE_STATUS_KEY,
|
|
249
|
+
(prev) => ({
|
|
250
|
+
configured: true,
|
|
251
|
+
activeProvider: prev?.activeProvider ?? null,
|
|
252
|
+
builderConfigured: prev?.builderConfigured ?? false,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
}, [queryClient]);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className="relative min-h-screen bg-background">
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
aria-label={t("recordRoute.backToLibrary")}
|
|
262
|
+
onClick={() => navigate("/library")}
|
|
263
|
+
className="fixed start-4 top-4 z-30 inline-flex h-9 w-9 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
264
|
+
>
|
|
265
|
+
<IconArrowLeft className="h-5 w-5 rtl:-scale-x-100" />
|
|
266
|
+
</button>
|
|
267
|
+
|
|
268
|
+
<div className="flex min-h-screen flex-col items-center justify-center px-4 py-10">
|
|
269
|
+
<div className="mb-3 flex items-center gap-2 text-primary">
|
|
270
|
+
<IconLink className="h-6 w-6" />
|
|
271
|
+
<span className="text-sm font-medium uppercase tracking-wide">
|
|
272
|
+
{t("importRoute.title")}
|
|
273
|
+
</span>
|
|
274
|
+
</div>
|
|
275
|
+
<p className="mb-6 max-w-md text-center text-sm text-muted-foreground">
|
|
276
|
+
{t("importRoute.helperText")}
|
|
277
|
+
</p>
|
|
278
|
+
|
|
279
|
+
<div className="mx-auto w-full max-w-lg">
|
|
280
|
+
{storageConfigured === null ? (
|
|
281
|
+
<ImportPanelSkeleton />
|
|
282
|
+
) : storageConfigured ? (
|
|
283
|
+
<div
|
|
284
|
+
className={cn(
|
|
285
|
+
"overflow-hidden rounded-2xl border border-border bg-card shadow-lg transition-opacity duration-300",
|
|
286
|
+
phase === "leaving" && "opacity-0",
|
|
287
|
+
)}
|
|
288
|
+
>
|
|
289
|
+
{busy ? (
|
|
290
|
+
<div className="h-1 w-full overflow-hidden bg-muted">
|
|
291
|
+
<div
|
|
292
|
+
className="h-full bg-primary transition-[width] duration-200 ease-out"
|
|
293
|
+
style={{ width: `${Math.min(100, progress)}%` }}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
) : null}
|
|
297
|
+
<div className="p-6">
|
|
298
|
+
{phase === "importing" ? (
|
|
299
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
300
|
+
<IconSparkles className="h-6 w-6 animate-pulse text-primary" />
|
|
301
|
+
<p
|
|
302
|
+
key={stageIndex}
|
|
303
|
+
className="text-base font-semibold text-foreground"
|
|
304
|
+
style={{ animation: "clips-benefit-in 260ms ease-out" }}
|
|
305
|
+
>
|
|
306
|
+
{importStages[stageIndex]}
|
|
307
|
+
</p>
|
|
308
|
+
<p className="max-w-xs text-xs text-muted-foreground">
|
|
309
|
+
{t("importRoute.importingSubtitle")}
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
) : phase === "done" || phase === "leaving" ? (
|
|
313
|
+
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
|
314
|
+
<div className="flex items-center gap-2 text-primary">
|
|
315
|
+
<IconSparkles className="h-6 w-6" />
|
|
316
|
+
<p className="text-base font-semibold text-foreground">
|
|
317
|
+
{t("importRoute.doneHeading")}
|
|
318
|
+
</p>
|
|
319
|
+
</div>
|
|
320
|
+
<ul className="flex flex-col items-start gap-2 text-start">
|
|
321
|
+
{benefits.map((benefit, index) => (
|
|
322
|
+
<li
|
|
323
|
+
key={benefit}
|
|
324
|
+
className="flex items-center gap-2 text-sm font-medium text-foreground"
|
|
325
|
+
style={{
|
|
326
|
+
animation: "clips-benefit-in 220ms ease-out both",
|
|
327
|
+
animationDelay: `${index * 110}ms`,
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<IconCheck className="h-4 w-4 shrink-0 text-primary" />
|
|
331
|
+
{benefit}
|
|
332
|
+
</li>
|
|
333
|
+
))}
|
|
334
|
+
</ul>
|
|
335
|
+
</div>
|
|
336
|
+
) : (
|
|
337
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
338
|
+
<Input
|
|
339
|
+
autoFocus
|
|
340
|
+
value={loomUrl}
|
|
341
|
+
onChange={(event) => {
|
|
342
|
+
setLoomUrl(event.target.value);
|
|
343
|
+
setLoomError(null);
|
|
344
|
+
}}
|
|
345
|
+
placeholder={t("importRoute.urlPlaceholder")}
|
|
346
|
+
className="h-12 text-base"
|
|
347
|
+
inputMode="url"
|
|
348
|
+
/>
|
|
349
|
+
<Button
|
|
350
|
+
type="submit"
|
|
351
|
+
className="h-12 w-full gap-2"
|
|
352
|
+
disabled={!loomUrl.trim()}
|
|
353
|
+
>
|
|
354
|
+
<IconLink className="h-4 w-4" />
|
|
355
|
+
{t("importRoute.cta")}
|
|
356
|
+
</Button>
|
|
357
|
+
{loomError ? (
|
|
358
|
+
<p className="text-xs leading-relaxed text-destructive">
|
|
359
|
+
{loomError}
|
|
360
|
+
</p>
|
|
361
|
+
) : null}
|
|
362
|
+
</form>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{!busy ? (
|
|
367
|
+
<div className="flex items-center justify-center gap-4 border-t border-border px-6 py-4">
|
|
368
|
+
<Link
|
|
369
|
+
to={uploadHref}
|
|
370
|
+
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
371
|
+
>
|
|
372
|
+
<IconUpload className="h-3.5 w-3.5" />
|
|
373
|
+
{t("preRecord.uploadVideo")}
|
|
374
|
+
</Link>
|
|
375
|
+
<Link
|
|
376
|
+
to={recordHref}
|
|
377
|
+
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
378
|
+
>
|
|
379
|
+
<IconVideo className="h-3.5 w-3.5" />
|
|
380
|
+
{t("preRecord.recordNew")}
|
|
381
|
+
</Link>
|
|
382
|
+
</div>
|
|
383
|
+
) : null}
|
|
384
|
+
</div>
|
|
385
|
+
) : (
|
|
386
|
+
<StorageSetupCard
|
|
387
|
+
onConfigured={markStorageConfigured}
|
|
388
|
+
connectSource="clips_import_storage_setup_card"
|
|
389
|
+
connectFlow="import"
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -552,18 +552,20 @@ export default function RecordingPage() {
|
|
|
552
552
|
setRetryingFinalize(true);
|
|
553
553
|
setProcessingTimeout(false);
|
|
554
554
|
try {
|
|
555
|
-
const
|
|
556
|
-
|
|
555
|
+
const isUrlImportRetry =
|
|
556
|
+
isLoomRecording ||
|
|
557
|
+
recording?.sourceAppName?.trim().toLowerCase() === "video link";
|
|
558
|
+
const actionPath = isUrlImportRetry
|
|
557
559
|
? "/_agent-native/actions/import-loom-recording"
|
|
558
560
|
: "/_agent-native/actions/finalize-recording";
|
|
559
|
-
if (
|
|
561
|
+
if (isUrlImportRetry && !recording?.sourceWindowTitle) {
|
|
560
562
|
throw new Error(t("recordingPage.loomMissingUrl"));
|
|
561
563
|
}
|
|
562
564
|
const res = await fetch(agentNativePath(actionPath), {
|
|
563
565
|
method: "POST",
|
|
564
566
|
headers: { "Content-Type": "application/json" },
|
|
565
567
|
body: JSON.stringify(
|
|
566
|
-
|
|
568
|
+
isUrlImportRetry
|
|
567
569
|
? {
|
|
568
570
|
recordingId,
|
|
569
571
|
url: recording?.sourceWindowTitle,
|
|
@@ -593,14 +595,14 @@ export default function RecordingPage() {
|
|
|
593
595
|
});
|
|
594
596
|
return;
|
|
595
597
|
}
|
|
596
|
-
if (
|
|
598
|
+
if (isUrlImportRetry && result?.status === "processing") {
|
|
597
599
|
// Download + reupload now run as a background job; this request only
|
|
598
600
|
// confirms the retry was accepted, not that the clip is ready yet.
|
|
599
601
|
toast.info(t("recordingPage.importingLoom"));
|
|
600
602
|
return;
|
|
601
603
|
}
|
|
602
604
|
toast.success(
|
|
603
|
-
|
|
605
|
+
isUrlImportRetry
|
|
604
606
|
? t("recordingPage.loomImportResumed")
|
|
605
607
|
: t("recordingPage.clipUploadResumed"),
|
|
606
608
|
);
|
|
@@ -522,10 +522,6 @@ function friendlyRecordingErrorMessage(error: string): string {
|
|
|
522
522
|
return error;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
function userFacingActionErrorMessage(error: string): string {
|
|
526
|
-
return error.replace(/^Action [a-z0-9-]+ failed:\s*/i, "").trim() || error;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
525
|
interface PendingRecording {
|
|
530
526
|
id: string;
|
|
531
527
|
uploadChunkUrl: string;
|
|
@@ -804,7 +800,6 @@ export default function RecordRoute() {
|
|
|
804
800
|
// the live camera bubble is hidden during full-screen recording.
|
|
805
801
|
const [resolvedDisplaySurface, setResolvedDisplaySurface] =
|
|
806
802
|
useState<DisplaySurface | null>(null);
|
|
807
|
-
const [loomImporting, setLoomImporting] = useState(false);
|
|
808
803
|
const [recordingMode, setRecordingMode] =
|
|
809
804
|
useState<RecordingMode>("screen+camera");
|
|
810
805
|
// Surfaced during the post-stop compression pass so the spinner can show
|
|
@@ -834,6 +829,17 @@ export default function RecordRoute() {
|
|
|
834
829
|
const params = new URLSearchParams(location.search);
|
|
835
830
|
return params.get("folderId") || null;
|
|
836
831
|
}, [location.search]);
|
|
832
|
+
const autoOpenUploadFromUrl = useMemo(() => {
|
|
833
|
+
const params = new URLSearchParams(location.search);
|
|
834
|
+
return params.get("autoUpload") === "1";
|
|
835
|
+
}, [location.search]);
|
|
836
|
+
const importLoomHref = useMemo(() => {
|
|
837
|
+
const params = new URLSearchParams();
|
|
838
|
+
if (spaceIdFromUrl) params.set("spaceId", spaceIdFromUrl);
|
|
839
|
+
if (folderIdFromUrl) params.set("folderId", folderIdFromUrl);
|
|
840
|
+
const qs = params.toString();
|
|
841
|
+
return qs ? `/import?${qs}` : "/import";
|
|
842
|
+
}, [spaceIdFromUrl, folderIdFromUrl]);
|
|
837
843
|
const storageConfigured: boolean | null = storageQuery.isLoading
|
|
838
844
|
? null
|
|
839
845
|
: !!storageQuery.data?.configured;
|
|
@@ -1816,70 +1822,6 @@ export default function RecordRoute() {
|
|
|
1816
1822
|
[markStorageConfigured, navigate, probeVideoMetadata, showSavedToast],
|
|
1817
1823
|
);
|
|
1818
1824
|
|
|
1819
|
-
const importLoom = useCallback(
|
|
1820
|
-
async (url: string) => {
|
|
1821
|
-
startSessionRef.current += 1;
|
|
1822
|
-
fileUploadAbortRef.current?.abort(makeAbortError("Upload cancelled"));
|
|
1823
|
-
fileUploadAbortRef.current = null;
|
|
1824
|
-
setError(null);
|
|
1825
|
-
setLoomImporting(true);
|
|
1826
|
-
|
|
1827
|
-
try {
|
|
1828
|
-
const result = (await callAction(
|
|
1829
|
-
"import-loom-recording" as any,
|
|
1830
|
-
{
|
|
1831
|
-
url,
|
|
1832
|
-
spaceIds: spaceIdFromUrl ? [spaceIdFromUrl] : undefined,
|
|
1833
|
-
folderId: folderIdFromUrl ?? undefined,
|
|
1834
|
-
} as any,
|
|
1835
|
-
)) as {
|
|
1836
|
-
recordingId?: string;
|
|
1837
|
-
status?: string;
|
|
1838
|
-
storageSetupRequired?: boolean;
|
|
1839
|
-
};
|
|
1840
|
-
const recordingId = result?.recordingId;
|
|
1841
|
-
if (!recordingId) {
|
|
1842
|
-
throw new Error("Loom import did not return a recording id.");
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
if (
|
|
1846
|
-
result?.storageSetupRequired ||
|
|
1847
|
-
result?.status === "waiting_storage"
|
|
1848
|
-
) {
|
|
1849
|
-
toast.info(t("recordRoute.storageNeededToFinishLoomImport"), {
|
|
1850
|
-
description: t("recordRoute.connectStorageToRetryLoom"),
|
|
1851
|
-
duration: 12_000,
|
|
1852
|
-
});
|
|
1853
|
-
} else if (result?.status === "processing") {
|
|
1854
|
-
// The video download + reupload run as a background job (see
|
|
1855
|
-
// import-loom-recording.ts) so this request stays fast regardless of
|
|
1856
|
-
// Loom video length; the recording page polls until it is ready.
|
|
1857
|
-
toast.info(t("recordingPage.importingLoom"));
|
|
1858
|
-
} else {
|
|
1859
|
-
showSavedToast(
|
|
1860
|
-
t("recordRoute.loomImported"),
|
|
1861
|
-
await copyRecordingShareLink(recordingId),
|
|
1862
|
-
recordingId,
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
await writeAppState("navigate", {
|
|
1866
|
-
view: "recording",
|
|
1867
|
-
recordingId,
|
|
1868
|
-
}).catch(() => {});
|
|
1869
|
-
navigate(`/r/${recordingId}`);
|
|
1870
|
-
} catch (err) {
|
|
1871
|
-
throw new Error(
|
|
1872
|
-
err instanceof Error
|
|
1873
|
-
? userFacingActionErrorMessage(err.message)
|
|
1874
|
-
: t("recordRoute.couldNotImportLoom"),
|
|
1875
|
-
);
|
|
1876
|
-
} finally {
|
|
1877
|
-
setLoomImporting(false);
|
|
1878
|
-
}
|
|
1879
|
-
},
|
|
1880
|
-
[folderIdFromUrl, navigate, spaceIdFromUrl, showSavedToast],
|
|
1881
|
-
);
|
|
1882
|
-
|
|
1883
1825
|
const saveBrowserDiagnostics = useCallback(
|
|
1884
1826
|
async (recordingId: string) => {
|
|
1885
1827
|
const capture = browserDiagnosticsRef.current;
|
|
@@ -2547,10 +2489,10 @@ export default function RecordRoute() {
|
|
|
2547
2489
|
initialRecorderOptions.surface
|
|
2548
2490
|
}
|
|
2549
2491
|
onUpload={uploadFile}
|
|
2550
|
-
|
|
2551
|
-
importingLoom={loomImporting}
|
|
2492
|
+
importLoomHref={importLoomHref}
|
|
2552
2493
|
cameraSize={cameraSize}
|
|
2553
2494
|
onCameraSizeChange={handleCameraSizeChange}
|
|
2495
|
+
autoOpenUpload={autoOpenUploadFromUrl}
|
|
2554
2496
|
/>
|
|
2555
2497
|
) : (
|
|
2556
2498
|
<StorageSetupCard
|