@agent-native/core 0.102.0 → 0.102.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 +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +7 -0
- package/corpus/core/src/client/AssistantChat.tsx +7 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +1 -1
- package/corpus/core/src/client/chat/message-components.tsx +17 -3
- package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
- package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
- package/corpus/core/src/client/error-format.ts +24 -0
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
- package/corpus/templates/analytics/changelog/2026-07-15-fixed-the-agent-discarding-a-successful-dashboard-edit-and-r.md +6 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +30 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +15 -0
- package/corpus/templates/assets/app/routes/library.tsx +69 -0
- package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
- package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
- package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
- package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
- package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
- package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +22 -11
- package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
- package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
- package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
- package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +86 -46
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
- package/corpus/templates/clips/app/components/player/video-player.tsx +45 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +2 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +2 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +2 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +2 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +2 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +2 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +2 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +2 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_index.tsx +1 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +71 -31
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
- package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
- package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +10 -2
- package/corpus/templates/content/changelog/2026-07-15-page-and-database-descriptions-now-use-a-clean-single-line-f.md +6 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
- package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
- package/corpus/templates/slides/actions/import-file.ts +4 -1
- package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
- package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -0
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +7 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +8 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +1 -1
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +4 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +10 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.js +58 -6
- package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/client/composer/asset-picker-url.d.ts +16 -0
- package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
- package/dist/client/composer/asset-picker-url.js +64 -0
- package/dist/client/composer/asset-picker-url.js.map +1 -0
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +6 -1
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
IconPhotoEdit,
|
|
5
5
|
IconUpload,
|
|
6
6
|
IconLoader2,
|
|
7
|
+
IconX,
|
|
7
8
|
} from "@tabler/icons-react";
|
|
8
9
|
import { useEffect, useRef, useState } from "react";
|
|
9
10
|
import { toast } from "sonner";
|
|
@@ -55,6 +56,10 @@ export function ThumbnailPicker({
|
|
|
55
56
|
const [gifDataUrl, setGifDataUrl] = useState<string | null>(null);
|
|
56
57
|
|
|
57
58
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
59
|
+
const framePreviewRef = useRef<HTMLVideoElement | null>(null);
|
|
60
|
+
const gifVideoRef = useRef<HTMLVideoElement | null>(null);
|
|
61
|
+
const gifPreviewRef = useRef<HTMLVideoElement | null>(null);
|
|
62
|
+
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
58
63
|
|
|
59
64
|
const mutation = useActionMutation("set-thumbnail");
|
|
60
65
|
|
|
@@ -65,6 +70,7 @@ export function ThumbnailPicker({
|
|
|
65
70
|
setFrameDataUrl(null);
|
|
66
71
|
setGifDataUrl(null);
|
|
67
72
|
setGifProgress(null);
|
|
73
|
+
if (uploadInputRef.current) uploadInputRef.current.value = "";
|
|
68
74
|
}
|
|
69
75
|
}, [open]);
|
|
70
76
|
|
|
@@ -98,6 +104,18 @@ export function ThumbnailPicker({
|
|
|
98
104
|
}
|
|
99
105
|
};
|
|
100
106
|
|
|
107
|
+
const seekPreview = (video: HTMLVideoElement | null, timeMs: number) => {
|
|
108
|
+
if (!video || !Number.isFinite(timeMs)) return;
|
|
109
|
+
const apply = () => {
|
|
110
|
+
video.currentTime = Math.max(0, timeMs) / 1000;
|
|
111
|
+
};
|
|
112
|
+
if (video.readyState === HTMLMediaElement.HAVE_NOTHING) {
|
|
113
|
+
video.addEventListener("loadedmetadata", apply, { once: true });
|
|
114
|
+
} else {
|
|
115
|
+
apply();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
101
119
|
const handleGifGenerate = async () => {
|
|
102
120
|
if (!videoUrl) return;
|
|
103
121
|
try {
|
|
@@ -187,6 +205,7 @@ export function ThumbnailPicker({
|
|
|
187
205
|
<TabsContent value="upload" className="py-4 space-y-3">
|
|
188
206
|
<Label>{t("thumbnailPicker.uploadImage")}</Label>
|
|
189
207
|
<input
|
|
208
|
+
ref={uploadInputRef}
|
|
190
209
|
type="file"
|
|
191
210
|
accept="image/*"
|
|
192
211
|
onChange={(e) => {
|
|
@@ -196,14 +215,39 @@ export function ThumbnailPicker({
|
|
|
196
215
|
reader.onload = () => setUploadDataUrl(reader.result as string);
|
|
197
216
|
reader.readAsDataURL(file);
|
|
198
217
|
}}
|
|
199
|
-
className="
|
|
218
|
+
className="sr-only"
|
|
200
219
|
/>
|
|
220
|
+
<Button
|
|
221
|
+
type="button"
|
|
222
|
+
variant="secondary"
|
|
223
|
+
size="sm"
|
|
224
|
+
onClick={() => uploadInputRef.current?.click()}
|
|
225
|
+
>
|
|
226
|
+
<IconUpload className="mr-1 h-4 w-4" />
|
|
227
|
+
{t("shareDialog.chooseFile")}
|
|
228
|
+
</Button>
|
|
201
229
|
{uploadDataUrl ? (
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
<div className="relative w-fit">
|
|
231
|
+
<img
|
|
232
|
+
src={uploadDataUrl}
|
|
233
|
+
alt={t("thumbnailPicker.uploadedPreview")}
|
|
234
|
+
className="max-h-60 rounded border border-border"
|
|
235
|
+
/>
|
|
236
|
+
<Button
|
|
237
|
+
type="button"
|
|
238
|
+
variant="secondary"
|
|
239
|
+
size="icon"
|
|
240
|
+
className="absolute right-1 top-1 h-7 w-7"
|
|
241
|
+
aria-label={t("shareDialog.remove")}
|
|
242
|
+
onClick={() => {
|
|
243
|
+
setUploadDataUrl(null);
|
|
244
|
+
if (uploadInputRef.current)
|
|
245
|
+
uploadInputRef.current.value = "";
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<IconX className="h-4 w-4" />
|
|
249
|
+
</Button>
|
|
250
|
+
</div>
|
|
207
251
|
) : currentThumbnailUrl ? (
|
|
208
252
|
<img
|
|
209
253
|
src={currentThumbnailUrl}
|
|
@@ -235,7 +279,13 @@ export function ThumbnailPicker({
|
|
|
235
279
|
max={Math.max(1000, durationMs)}
|
|
236
280
|
step={100}
|
|
237
281
|
value={[frameTime]}
|
|
238
|
-
onValueChange={([v]) =>
|
|
282
|
+
onValueChange={([v]) => {
|
|
283
|
+
const next = v ?? 0;
|
|
284
|
+
setFrameTime(next);
|
|
285
|
+
setFrameDataUrl(null);
|
|
286
|
+
seekPreview(videoRef.current, next);
|
|
287
|
+
seekPreview(framePreviewRef.current, next);
|
|
288
|
+
}}
|
|
239
289
|
/>
|
|
240
290
|
<Button
|
|
241
291
|
size="sm"
|
|
@@ -256,6 +306,15 @@ export function ThumbnailPicker({
|
|
|
256
306
|
alt={t("thumbnailPicker.capturedFrame")}
|
|
257
307
|
className="w-full rounded border border-border mt-1"
|
|
258
308
|
/>
|
|
309
|
+
) : videoUrl ? (
|
|
310
|
+
<video
|
|
311
|
+
ref={framePreviewRef}
|
|
312
|
+
src={videoUrl}
|
|
313
|
+
className="w-full rounded border border-border bg-black mt-1"
|
|
314
|
+
crossOrigin="anonymous"
|
|
315
|
+
preload="auto"
|
|
316
|
+
muted
|
|
317
|
+
/>
|
|
259
318
|
) : (
|
|
260
319
|
<div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
|
|
261
320
|
{t("thumbnailPicker.capturePreview")}
|
|
@@ -269,6 +328,7 @@ export function ThumbnailPicker({
|
|
|
269
328
|
<div className="grid grid-cols-2 gap-4">
|
|
270
329
|
<div>
|
|
271
330
|
<video
|
|
331
|
+
ref={gifVideoRef}
|
|
272
332
|
src={videoUrl ?? undefined}
|
|
273
333
|
className="w-full rounded border border-border bg-black"
|
|
274
334
|
crossOrigin="anonymous"
|
|
@@ -287,7 +347,13 @@ export function ThumbnailPicker({
|
|
|
287
347
|
max={Math.max(0, durationMs - gifDuration)}
|
|
288
348
|
step={100}
|
|
289
349
|
value={[gifStart]}
|
|
290
|
-
onValueChange={([v]) =>
|
|
350
|
+
onValueChange={([v]) => {
|
|
351
|
+
const next = v ?? 0;
|
|
352
|
+
setGifStart(next);
|
|
353
|
+
setGifDataUrl(null);
|
|
354
|
+
seekPreview(gifVideoRef.current, next);
|
|
355
|
+
seekPreview(gifPreviewRef.current, next);
|
|
356
|
+
}}
|
|
291
357
|
/>
|
|
292
358
|
</div>
|
|
293
359
|
<div>
|
|
@@ -301,7 +367,10 @@ export function ThumbnailPicker({
|
|
|
301
367
|
max={10000}
|
|
302
368
|
step={100}
|
|
303
369
|
value={[gifDuration]}
|
|
304
|
-
onValueChange={([v]) =>
|
|
370
|
+
onValueChange={([v]) => {
|
|
371
|
+
setGifDuration(v ?? 500);
|
|
372
|
+
setGifDataUrl(null);
|
|
373
|
+
}}
|
|
305
374
|
/>
|
|
306
375
|
</div>
|
|
307
376
|
<Button
|
|
@@ -331,6 +400,15 @@ export function ThumbnailPicker({
|
|
|
331
400
|
alt={t("thumbnailPicker.animatedPreview")}
|
|
332
401
|
className="w-full rounded border border-border mt-1"
|
|
333
402
|
/>
|
|
403
|
+
) : videoUrl ? (
|
|
404
|
+
<video
|
|
405
|
+
ref={gifPreviewRef}
|
|
406
|
+
src={videoUrl}
|
|
407
|
+
className="w-full rounded border border-border bg-black mt-1"
|
|
408
|
+
crossOrigin="anonymous"
|
|
409
|
+
preload="auto"
|
|
410
|
+
muted
|
|
411
|
+
/>
|
|
334
412
|
) : (
|
|
335
413
|
<div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
|
|
336
414
|
{t("thumbnailPicker.generateGifPreview")}
|
|
@@ -13,6 +13,7 @@ export interface TrimHandlesProps {
|
|
|
13
13
|
/** Fires when the user releases the mouse after drag. */
|
|
14
14
|
onCommit?: (value: { startMs: number; endMs: number }) => void;
|
|
15
15
|
durationMs: number;
|
|
16
|
+
splitPoints?: number[];
|
|
16
17
|
className?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -44,6 +45,7 @@ export function TrimHandles({
|
|
|
44
45
|
onChange,
|
|
45
46
|
onCommit,
|
|
46
47
|
durationMs,
|
|
48
|
+
splitPoints = [],
|
|
47
49
|
className,
|
|
48
50
|
}: TrimHandlesProps) {
|
|
49
51
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -111,6 +113,9 @@ export function TrimHandles({
|
|
|
111
113
|
|
|
112
114
|
const startX = (value.startMs / Math.max(durationMs, 1)) * width;
|
|
113
115
|
const endX = (value.endMs / Math.max(durationMs, 1)) * width;
|
|
116
|
+
const visibleSplitPoints = splitPoints.filter(
|
|
117
|
+
(ms) => ms > value.startMs && ms < value.endMs, // i18n-ignore numeric range predicate, not visible UI copy
|
|
118
|
+
);
|
|
114
119
|
|
|
115
120
|
return (
|
|
116
121
|
<div
|
|
@@ -133,6 +138,16 @@ export function TrimHandles({
|
|
|
133
138
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full bg-foreground text-background text-[10px] px-1.5 py-0.5 rounded font-mono whitespace-nowrap">
|
|
134
139
|
{formatMs(value.endMs - value.startMs)}
|
|
135
140
|
</div>
|
|
141
|
+
{visibleSplitPoints.map((splitMs) => (
|
|
142
|
+
<div
|
|
143
|
+
key={splitMs}
|
|
144
|
+
className="absolute top-0 h-full w-0.5 -translate-x-1/2 bg-rose-500/95"
|
|
145
|
+
style={{
|
|
146
|
+
left: `${((splitMs - value.startMs) / Math.max(value.endMs - value.startMs, 1)) * 100}%`,
|
|
147
|
+
}}
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
/>
|
|
150
|
+
))}
|
|
136
151
|
</div>
|
|
137
152
|
|
|
138
153
|
{/* Left handle */}
|
|
@@ -18,6 +18,8 @@ export interface WaveformProps {
|
|
|
18
18
|
durationMs: number;
|
|
19
19
|
/** Excluded ranges (original time) — drawn as striped overlays. */
|
|
20
20
|
excludedRanges?: Array<{ startMs: number; endMs: number }>;
|
|
21
|
+
/** Split markers (original time) — drawn over the active selection. */
|
|
22
|
+
splitPoints?: number[];
|
|
21
23
|
/** Optional selection range (original time) highlighted in brand color. */
|
|
22
24
|
selectionRange?: { startMs: number; endMs: number } | null;
|
|
23
25
|
/** Transcript-backed activity ranges used when browser audio decoding fails. */
|
|
@@ -87,6 +89,7 @@ export function Waveform({
|
|
|
87
89
|
playheadMs,
|
|
88
90
|
durationMs,
|
|
89
91
|
excludedRanges,
|
|
92
|
+
splitPoints = [],
|
|
90
93
|
selectionRange,
|
|
91
94
|
activityRanges = [],
|
|
92
95
|
onSeek,
|
|
@@ -220,19 +223,28 @@ export function Waveform({
|
|
|
220
223
|
|
|
221
224
|
// Selection overlay
|
|
222
225
|
if (selectionRange) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const xEnd =
|
|
228
|
-
(Math.max(selectionRange.startMs, selectionRange.endMs) /
|
|
229
|
-
Math.max(durationMs, 1)) *
|
|
230
|
-
totalWidth;
|
|
226
|
+
const startMs = Math.min(selectionRange.startMs, selectionRange.endMs);
|
|
227
|
+
const endMs = Math.max(selectionRange.startMs, selectionRange.endMs);
|
|
228
|
+
const xStart = (startMs / Math.max(durationMs, 1)) * totalWidth;
|
|
229
|
+
const xEnd = (endMs / Math.max(durationMs, 1)) * totalWidth;
|
|
231
230
|
ctx.fillStyle = getBrandColorAlpha(0.28);
|
|
232
231
|
ctx.fillRect(xStart, 0, xEnd - xStart, height);
|
|
233
232
|
ctx.strokeStyle = getBrandColor();
|
|
234
233
|
ctx.lineWidth = 1;
|
|
235
234
|
ctx.strokeRect(xStart + 0.5, 0.5, xEnd - xStart - 1, height - 1);
|
|
235
|
+
|
|
236
|
+
// Keep split markers visible on the selected track as well as on the
|
|
237
|
+
// ruler so a split is visibly actionable within the selection.
|
|
238
|
+
for (const splitMs of splitPoints) {
|
|
239
|
+
if (splitMs <= startMs || splitMs >= endMs) continue;
|
|
240
|
+
const splitX = (splitMs / Math.max(durationMs, 1)) * totalWidth;
|
|
241
|
+
ctx.strokeStyle = "rgba(244, 63, 94, 0.95)";
|
|
242
|
+
ctx.lineWidth = 2;
|
|
243
|
+
ctx.beginPath();
|
|
244
|
+
ctx.moveTo(splitX, 0);
|
|
245
|
+
ctx.lineTo(splitX, height);
|
|
246
|
+
ctx.stroke();
|
|
247
|
+
}
|
|
236
248
|
}
|
|
237
249
|
}, [
|
|
238
250
|
peaks,
|
|
@@ -240,6 +252,7 @@ export function Waveform({
|
|
|
240
252
|
height,
|
|
241
253
|
excludedRanges,
|
|
242
254
|
selectionRange,
|
|
255
|
+
splitPoints,
|
|
243
256
|
durationMs,
|
|
244
257
|
activityRanges,
|
|
245
258
|
]);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IconFolderPlus,
|
|
7
7
|
IconTrash,
|
|
8
8
|
IconEdit,
|
|
9
|
+
IconDots,
|
|
9
10
|
} from "@tabler/icons-react";
|
|
10
11
|
import { useMemo, useState } from "react";
|
|
11
12
|
import { NavLink } from "react-router";
|
|
@@ -33,6 +34,13 @@ import {
|
|
|
33
34
|
ContextMenuSeparator,
|
|
34
35
|
ContextMenuTrigger,
|
|
35
36
|
} from "@/components/ui/context-menu";
|
|
37
|
+
import {
|
|
38
|
+
DropdownMenu,
|
|
39
|
+
DropdownMenuContent,
|
|
40
|
+
DropdownMenuItem,
|
|
41
|
+
DropdownMenuSeparator,
|
|
42
|
+
DropdownMenuTrigger,
|
|
43
|
+
} from "@/components/ui/dropdown-menu";
|
|
36
44
|
import {
|
|
37
45
|
useCreateFolder,
|
|
38
46
|
useDeleteFolder,
|
|
@@ -186,6 +194,50 @@ function FolderItem({
|
|
|
186
194
|
{node.name}
|
|
187
195
|
</span>
|
|
188
196
|
</NavLink>
|
|
197
|
+
<DropdownMenu>
|
|
198
|
+
<DropdownMenuTrigger asChild>
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
aria-label={`${node.name}: ${t("root.commandActions")}`}
|
|
202
|
+
title={`${node.name}: ${t("root.commandActions")}`}
|
|
203
|
+
className="rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100"
|
|
204
|
+
>
|
|
205
|
+
<IconDots className="h-3.5 w-3.5" />
|
|
206
|
+
</button>
|
|
207
|
+
</DropdownMenuTrigger>
|
|
208
|
+
<DropdownMenuContent align="start" side="right">
|
|
209
|
+
<DropdownMenuItem
|
|
210
|
+
onSelect={() => {
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
setRenameValue(node.name);
|
|
213
|
+
setRenameOpen(true);
|
|
214
|
+
}, 0);
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<IconEdit className="h-3.5 w-3.5 me-2" />{" "}
|
|
218
|
+
{t("folderTree.rename")}
|
|
219
|
+
</DropdownMenuItem>
|
|
220
|
+
<DropdownMenuItem
|
|
221
|
+
onSelect={() => {
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
setNewValue("");
|
|
224
|
+
setNewOpen(true);
|
|
225
|
+
}, 0);
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
<IconFolderPlus className="h-3.5 w-3.5 me-2" />{" "}
|
|
229
|
+
{t("folderTree.newSubfolder")}
|
|
230
|
+
</DropdownMenuItem>
|
|
231
|
+
<DropdownMenuSeparator />
|
|
232
|
+
<DropdownMenuItem
|
|
233
|
+
onSelect={() => setTimeout(() => setConfirmDelete(true), 0)}
|
|
234
|
+
className="text-destructive"
|
|
235
|
+
>
|
|
236
|
+
<IconTrash className="h-3.5 w-3.5 me-2" />{" "}
|
|
237
|
+
{t("folderTree.delete")}
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
</DropdownMenuContent>
|
|
240
|
+
</DropdownMenu>
|
|
189
241
|
</div>
|
|
190
242
|
</ContextMenuTrigger>
|
|
191
243
|
<ContextMenuContent>
|
|
@@ -361,6 +361,7 @@ function CtaEditor({
|
|
|
361
361
|
// editing this card, so an agent edit to the CTA shows up live. `editing`
|
|
362
362
|
// flips true while focus is anywhere inside the card.
|
|
363
363
|
const editing = useRef(false);
|
|
364
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
364
365
|
const [label, setLabel] = useReconciledState(cta.label, {
|
|
365
366
|
active: editing.current,
|
|
366
367
|
});
|
|
@@ -387,52 +388,91 @@ function CtaEditor({
|
|
|
387
388
|
}
|
|
388
389
|
}}
|
|
389
390
|
>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
391
|
+
{collapsed ? (
|
|
392
|
+
<div className="flex items-center gap-2">
|
|
393
|
+
<div className="min-w-0 flex-1">
|
|
394
|
+
<p className="truncate text-sm font-medium">{cta.label}</p>
|
|
395
|
+
<p className="text-xs text-muted-foreground">
|
|
396
|
+
{cta.placement === "throughout"
|
|
397
|
+
? t("playerSettings.placementThroughout")
|
|
398
|
+
: t("playerSettings.placementEnd")}
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
<Button
|
|
402
|
+
type="button"
|
|
403
|
+
size="sm"
|
|
404
|
+
variant="ghost"
|
|
405
|
+
title={t("recordingPage.edit")}
|
|
406
|
+
onClick={() => setCollapsed(false)}
|
|
407
|
+
>
|
|
408
|
+
{t("recordingPage.edit")}
|
|
409
|
+
</Button>
|
|
410
|
+
<Button type="button" size="sm" variant="ghost" onClick={onDelete}>
|
|
411
|
+
{t("playerSettings.delete")}
|
|
412
|
+
</Button>
|
|
413
|
+
</div>
|
|
414
|
+
) : (
|
|
415
|
+
<>
|
|
416
|
+
<Input
|
|
417
|
+
value={label}
|
|
418
|
+
onChange={(e) => setLabel(e.target.value)}
|
|
419
|
+
placeholder={t("playerSettings.buttonLabelPlaceholder")}
|
|
420
|
+
/>
|
|
421
|
+
<Input
|
|
422
|
+
value={url}
|
|
423
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
424
|
+
placeholder="https://…"
|
|
425
|
+
/>
|
|
426
|
+
<div className="flex gap-2">
|
|
427
|
+
<input
|
|
428
|
+
type="color"
|
|
429
|
+
value={color}
|
|
430
|
+
onChange={(e) => setColor(e.target.value)}
|
|
431
|
+
className="h-9 w-12 rounded cursor-pointer border border-border"
|
|
432
|
+
/>
|
|
433
|
+
<Select
|
|
434
|
+
value={placement}
|
|
435
|
+
onValueChange={(v) => setPlacement(v as "end" | "throughout")}
|
|
436
|
+
>
|
|
437
|
+
<SelectTrigger className="flex-1">
|
|
438
|
+
<SelectValue />
|
|
439
|
+
</SelectTrigger>
|
|
440
|
+
<SelectContent>
|
|
441
|
+
<SelectItem value="throughout">
|
|
442
|
+
{t("playerSettings.placementThroughout")}
|
|
443
|
+
</SelectItem>
|
|
444
|
+
<SelectItem value="end">
|
|
445
|
+
{t("playerSettings.placementEnd")}
|
|
446
|
+
</SelectItem>
|
|
447
|
+
</SelectContent>
|
|
448
|
+
</Select>
|
|
449
|
+
</div>
|
|
450
|
+
<div className="flex gap-2">
|
|
451
|
+
<Button
|
|
452
|
+
type="button"
|
|
453
|
+
size="sm"
|
|
454
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
455
|
+
onClick={() => {
|
|
456
|
+
onSave({ label, url, color, placement });
|
|
457
|
+
setCollapsed(true);
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{t("common.save")}
|
|
461
|
+
</Button>
|
|
462
|
+
<Button
|
|
463
|
+
type="button"
|
|
464
|
+
size="sm"
|
|
465
|
+
variant="ghost"
|
|
466
|
+
onClick={() => setCollapsed(true)}
|
|
467
|
+
>
|
|
468
|
+
{t("recordingPage.done")}
|
|
469
|
+
</Button>
|
|
470
|
+
<Button type="button" size="sm" variant="ghost" onClick={onDelete}>
|
|
471
|
+
{t("playerSettings.delete")}
|
|
472
|
+
</Button>
|
|
473
|
+
</div>
|
|
474
|
+
</>
|
|
475
|
+
)}
|
|
436
476
|
</div>
|
|
437
477
|
);
|
|
438
478
|
}
|
|
@@ -380,49 +380,55 @@ function LinkTab({
|
|
|
380
380
|
)}
|
|
381
381
|
|
|
382
382
|
<CopyField
|
|
383
|
-
label={
|
|
383
|
+
label={
|
|
384
|
+
isPublic
|
|
385
|
+
? t("shareDialog.shareLink")
|
|
386
|
+
: t("shareDialog.shareWithHumans")
|
|
387
|
+
}
|
|
384
388
|
value={shareUrl}
|
|
385
389
|
disabled={
|
|
386
390
|
visibilityPending || !sharesLoaded || (!isPublic && canManage)
|
|
387
391
|
}
|
|
388
392
|
/>
|
|
389
393
|
|
|
390
|
-
|
|
391
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
394
|
+
{!isPublic ? (
|
|
395
|
+
<div className="space-y-2">
|
|
396
|
+
<CopyField
|
|
397
|
+
label={t("shareDialog.shareWithAgents")}
|
|
398
|
+
value={agentLink}
|
|
399
|
+
disabled={agentShareDisabled}
|
|
400
|
+
/>
|
|
401
|
+
{sharesLoaded ? (
|
|
402
|
+
<>
|
|
403
|
+
<p className="text-xs text-muted-foreground">
|
|
404
|
+
{t("shareDialog.agentTokenDescription")}
|
|
405
|
+
</p>
|
|
406
|
+
{agentLinkError ? (
|
|
407
|
+
<div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
|
|
408
|
+
<p className="text-xs text-muted-foreground">
|
|
409
|
+
{t("shareDialog.agentLinkUnavailable")}
|
|
410
|
+
</p>
|
|
411
|
+
<Button
|
|
412
|
+
type="button"
|
|
413
|
+
variant="ghost"
|
|
414
|
+
size="sm"
|
|
415
|
+
className="h-7"
|
|
416
|
+
onClick={() => void loadAgentContextUrl()}
|
|
417
|
+
disabled={createAgentLink.isPending}
|
|
418
|
+
>
|
|
419
|
+
{t("shareDialog.retryAgentLink")}
|
|
420
|
+
</Button>
|
|
421
|
+
</div>
|
|
422
|
+
) : null}
|
|
423
|
+
<CopyField
|
|
424
|
+
label={t("shareDialog.copyAgentPrompt")}
|
|
425
|
+
value={agentPrompt}
|
|
426
|
+
disabled={agentShareDisabled}
|
|
427
|
+
/>
|
|
428
|
+
</>
|
|
429
|
+
) : null}
|
|
430
|
+
</div>
|
|
431
|
+
) : null}
|
|
426
432
|
|
|
427
433
|
{sharesLoaded && !isPublic && canManage ? (
|
|
428
434
|
<MakePublicCard
|
|
@@ -592,6 +592,14 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
592
592
|
pauseVideo();
|
|
593
593
|
return;
|
|
594
594
|
}
|
|
595
|
+
if (v.ended) {
|
|
596
|
+
try {
|
|
597
|
+
v.currentTime = 0;
|
|
598
|
+
setCurrentMs(0);
|
|
599
|
+
} catch {
|
|
600
|
+
// Let the normal play attempt report a media error if the seek fails.
|
|
601
|
+
}
|
|
602
|
+
}
|
|
595
603
|
requestPlay();
|
|
596
604
|
}, [isPlaying, pauseVideo, requestPlay]);
|
|
597
605
|
|
|
@@ -1423,6 +1431,21 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1423
1431
|
</div>
|
|
1424
1432
|
)}
|
|
1425
1433
|
|
|
1434
|
+
{thumbnailUrl &&
|
|
1435
|
+
!autoPlay &&
|
|
1436
|
+
!hasPlaybackStarted &&
|
|
1437
|
+
(!startMs || startMs <= 0) ? (
|
|
1438
|
+
<img
|
|
1439
|
+
src={resolveLocalUrl(thumbnailUrl)}
|
|
1440
|
+
alt=""
|
|
1441
|
+
aria-hidden="true"
|
|
1442
|
+
className={cn(
|
|
1443
|
+
"pointer-events-none absolute inset-0 z-[1] h-full w-full",
|
|
1444
|
+
cover ? "object-cover" : "object-contain",
|
|
1445
|
+
)}
|
|
1446
|
+
/>
|
|
1447
|
+
) : null}
|
|
1448
|
+
|
|
1426
1449
|
{centerOverlayMode ? (
|
|
1427
1450
|
<CenterPlaybackOverlay
|
|
1428
1451
|
mode={centerOverlayMode}
|
|
@@ -1462,7 +1485,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1462
1485
|
|
|
1463
1486
|
{/* Floating CTA (throughout placement) */}
|
|
1464
1487
|
{showThroughoutCta ? (
|
|
1465
|
-
<div data-player-ui className="absolute bottom-16 right-4 z-
|
|
1488
|
+
<div data-player-ui className="absolute bottom-16 right-4 z-30">
|
|
1466
1489
|
<CtaButton
|
|
1467
1490
|
cta={cta!}
|
|
1468
1491
|
onClick={() => onCtaClick?.(cta!.id)}
|
|
@@ -1484,6 +1507,27 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1484
1507
|
onClick={() => onCtaClick?.(cta!.id)}
|
|
1485
1508
|
large
|
|
1486
1509
|
/>
|
|
1510
|
+
<button
|
|
1511
|
+
type="button"
|
|
1512
|
+
data-player-ui
|
|
1513
|
+
aria-label={t("videoPlayer.playClip")}
|
|
1514
|
+
onClick={(e) => {
|
|
1515
|
+
e.stopPropagation();
|
|
1516
|
+
const v = videoRef.current;
|
|
1517
|
+
if (!v) return;
|
|
1518
|
+
try {
|
|
1519
|
+
v.currentTime = 0;
|
|
1520
|
+
setCurrentMs(0);
|
|
1521
|
+
} catch {
|
|
1522
|
+
// The regular play path will surface any media error.
|
|
1523
|
+
}
|
|
1524
|
+
requestPlay();
|
|
1525
|
+
}}
|
|
1526
|
+
className="pointer-events-auto inline-flex items-center gap-2 rounded-md border border-white/30 bg-white/10 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
|
1527
|
+
>
|
|
1528
|
+
<IconPlayerPlay className="h-4 w-4 fill-current" />
|
|
1529
|
+
{t("videoPlayer.playClip")}
|
|
1530
|
+
</button>
|
|
1487
1531
|
</div>
|
|
1488
1532
|
</div>
|
|
1489
1533
|
) : null}
|