@agent-native/core 0.102.1 → 0.103.0
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/docs/content/local-file-mode.mdx +99 -489
- package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/corpus/core/docs/content/template-content.mdx +26 -42
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/content-local.ts +86 -18
- package/corpus/core/src/cli/index.ts +2 -2
- package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
- package/corpus/core/src/cli/skills.ts +47 -13
- 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/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/core/src/local-artifacts/index.ts +65 -0
- package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
- package/corpus/templates/assets/app/i18n-data.ts +215 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -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 +34 -13
- 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 +116 -58
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
- package/corpus/templates/clips/app/components/player/video-player.tsx +50 -2
- package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
- package/corpus/templates/clips/app/i18n/ar-SA.ts +5 -2
- package/corpus/templates/clips/app/i18n/de-DE.ts +6 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -2
- package/corpus/templates/clips/app/i18n/es-ES.ts +6 -2
- package/corpus/templates/clips/app/i18n/fr-FR.ts +5 -2
- package/corpus/templates/clips/app/i18n/hi-IN.ts +5 -2
- package/corpus/templates/clips/app/i18n/ja-JP.ts +6 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +5 -2
- package/corpus/templates/clips/app/i18n/pt-BR.ts +6 -2
- package/corpus/templates/clips/app/i18n/zh-CN.ts +5 -2
- package/corpus/templates/clips/app/i18n/zh-TW.ts +5 -2
- 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 +98 -32
- 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/clips/server/lib/share-password.ts +6 -3
- package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
- package/corpus/templates/content/AGENTS.md +25 -30
- package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
- package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
- package/corpus/templates/content/actions/_content-files.ts +338 -0
- package/corpus/templates/content/actions/_content-space-access.ts +135 -0
- package/corpus/templates/content/actions/_content-spaces.ts +640 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
- package/corpus/templates/content/actions/_database-utils.ts +7 -1
- package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
- package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
- package/corpus/templates/content/actions/_property-utils.ts +9 -4
- package/corpus/templates/content/actions/add-database-item.ts +108 -65
- package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
- package/corpus/templates/content/actions/create-content-database.ts +129 -3
- package/corpus/templates/content/actions/create-document.ts +65 -55
- package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
- package/corpus/templates/content/actions/delete-content-database.ts +3 -0
- package/corpus/templates/content/actions/delete-document.ts +7 -11
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
- package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
- package/corpus/templates/content/actions/edit-document.ts +2 -28
- package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
- package/corpus/templates/content/actions/export-content-source.ts +47 -9
- package/corpus/templates/content/actions/get-document.ts +0 -14
- package/corpus/templates/content/actions/import-content-source.ts +43 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
- package/corpus/templates/content/actions/list-documents.ts +12 -9
- package/corpus/templates/content/actions/move-document.ts +3 -14
- package/corpus/templates/content/actions/pull-document.ts +0 -23
- package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
- package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
- package/corpus/templates/content/actions/search-documents.ts +0 -34
- package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
- package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
- package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
- package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
- package/corpus/templates/content/actions/update-document.ts +0 -15
- package/corpus/templates/content/actions/view-screen.ts +2 -50
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
- package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
- package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
- package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
- package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
- package/corpus/templates/content/app/i18n-data.ts +75 -0
- package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
- package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +37 -1
- package/corpus/templates/content/scripts/dev-database.mjs +7 -5
- package/corpus/templates/content/server/db/schema.ts +113 -40
- package/corpus/templates/content/server/lib/document-context.ts +15 -1
- package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
- package/corpus/templates/content/server/plugins/db.ts +153 -0
- package/corpus/templates/content/shared/api.ts +17 -3
- package/corpus/templates/content/shared/builder-mdx.ts +163 -16
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
- package/corpus/templates/design/AGENTS.md +6 -1
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
- package/corpus/templates/design/actions/list-design-templates.ts +69 -21
- package/corpus/templates/design/actions/navigate.ts +2 -2
- package/corpus/templates/design/actions/view-screen.ts +15 -2
- package/corpus/templates/design/agent-native.app-skill.json +5 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
- package/corpus/templates/design/app/i18n-data.ts +21 -2
- package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
- package/corpus/templates/design/app/pages/Index.tsx +195 -24
- package/corpus/templates/design/app/pages/Templates.tsx +144 -55
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
- package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/mail/AGENTS.md +23 -18
- package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
- package/corpus/templates/mail/actions/mark-read.ts +131 -20
- package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
- package/corpus/templates/mail/actions/move-email.ts +30 -24
- package/corpus/templates/mail/actions/send-email.ts +73 -68
- package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +218 -209
- package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
- package/corpus/templates/mail/server/lib/email-state.ts +198 -113
- package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
- package/corpus/templates/mail/server/lib/jobs.ts +69 -74
- package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +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/cli/content-local.d.ts +1 -0
- package/dist/cli/content-local.d.ts.map +1 -1
- package/dist/cli/content-local.js +75 -17
- package/dist/cli/content-local.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills-content/content-skill.d.ts +1 -1
- package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/content-skill.js +36 -25
- package/dist/cli/skills-content/content-skill.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +47 -14
- package/dist/cli/skills.js.map +1 -1
- 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/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/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/local-artifacts/index.d.ts +10 -0
- package/dist/local-artifacts/index.d.ts.map +1 -1
- package/dist/local-artifacts/index.js +39 -0
- package/dist/local-artifacts/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/local-file-mode.mdx +99 -489
- package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/docs/content/template-content.mdx +26 -42
- package/package.json +1 -1
- package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { defineAction } from "@agent-native/core";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
readAppState,
|
|
20
|
+
writeAppState,
|
|
21
|
+
} from "@agent-native/core/application-state";
|
|
19
22
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
20
23
|
import { eq } from "drizzle-orm";
|
|
21
24
|
import { z } from "zod";
|
|
@@ -51,6 +54,11 @@ Output markdown.`,
|
|
|
51
54
|
Keep it concise, warm, and professional.`,
|
|
52
55
|
} as const;
|
|
53
56
|
|
|
57
|
+
// Actions for the same recording can arrive concurrently when the player and
|
|
58
|
+
// agent both retry a request. Keep the read-check-write/enqueue sequence
|
|
59
|
+
// single-flight within this runtime so only one request can claim generation.
|
|
60
|
+
const workflowGenerationLocks = new Set<string>();
|
|
61
|
+
|
|
54
62
|
export default defineAction({
|
|
55
63
|
description:
|
|
56
64
|
"Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent writes the result to clips-workflow-<recordingId> in application_state.",
|
|
@@ -61,71 +69,106 @@ export default defineAction({
|
|
|
61
69
|
run: async (args) => {
|
|
62
70
|
await assertAccess("recording", args.recordingId, "viewer");
|
|
63
71
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const stateKey = `clips-workflow-${args.recordingId}`;
|
|
73
|
+
if (workflowGenerationLocks.has(stateKey)) {
|
|
74
|
+
return {
|
|
75
|
+
queued: false,
|
|
76
|
+
duplicate: true,
|
|
77
|
+
recordingId: args.recordingId,
|
|
78
|
+
kind: args.kind,
|
|
79
|
+
stateKey,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
workflowGenerationLocks.add(stateKey);
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
try {
|
|
85
|
+
const db = getDb();
|
|
86
|
+
const [rec] = await db
|
|
87
|
+
.select()
|
|
88
|
+
.from(schema.recordings)
|
|
89
|
+
.where(eq(schema.recordings.id, args.recordingId))
|
|
90
|
+
.limit(1);
|
|
91
|
+
if (!rec) throw new Error(`Recording not found: ${args.recordingId}`);
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
const [transcript] = await db
|
|
94
|
+
.select()
|
|
95
|
+
.from(schema.recordingTranscripts)
|
|
96
|
+
.where(eq(schema.recordingTranscripts.recordingId, args.recordingId))
|
|
97
|
+
.limit(1);
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
// a loading state immediately.
|
|
83
|
-
await writeAppState(stateKey, {
|
|
84
|
-
kind: args.kind,
|
|
85
|
-
status: "generating",
|
|
86
|
-
recordingId: args.recordingId,
|
|
87
|
-
requestedAt: new Date().toISOString(),
|
|
88
|
-
} as any);
|
|
99
|
+
const includeFullVideoInAi = await readIncludeFullVideoInAi();
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
const existing = await readAppState(stateKey).catch(() => null);
|
|
102
|
+
if (existing?.status === "generating") {
|
|
103
|
+
const requestedAt = Date.parse(String(existing.requestedAt ?? ""));
|
|
104
|
+
const isRecent =
|
|
105
|
+
!Number.isFinite(requestedAt) ||
|
|
106
|
+
Date.now() - requestedAt < 10 * 60_000;
|
|
107
|
+
if (isRecent) {
|
|
108
|
+
return {
|
|
109
|
+
queued: false,
|
|
110
|
+
duplicate: true,
|
|
111
|
+
recordingId: args.recordingId,
|
|
112
|
+
kind: existing.kind ?? args.kind,
|
|
113
|
+
stateKey,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
args.recordingId
|
|
118
|
+
// Seed the output state with a "generating" placeholder so the UI can show
|
|
119
|
+
// a loading state immediately.
|
|
120
|
+
await writeAppState(stateKey, {
|
|
121
|
+
kind: args.kind,
|
|
122
|
+
status: "generating",
|
|
123
|
+
recordingId: args.recordingId,
|
|
124
|
+
requestedAt: new Date().toISOString(),
|
|
125
|
+
} as any);
|
|
126
|
+
|
|
127
|
+
const baseMessage =
|
|
128
|
+
`Generate a ${args.kind.toUpperCase()} workflow document from recording ${args.recordingId} ` +
|
|
129
|
+
`(title: "${rec.title}"). Read the transcript from this request's context. ` +
|
|
130
|
+
`${KIND_PROMPTS[args.kind]} ` +
|
|
131
|
+
`Then write the final markdown to application_state key "${stateKey}" as ` +
|
|
132
|
+
`\`{ kind: "${args.kind}", status: "ready", content: "...", recordingId: "${args.recordingId}" }\`. ` +
|
|
133
|
+
`Finish by replying in chat with the same generated markdown so the user can read it immediately.`;
|
|
134
|
+
|
|
135
|
+
const request = {
|
|
136
|
+
kind: "generate-workflow" as const,
|
|
137
|
+
workflowKind: args.kind,
|
|
138
|
+
recordingId: args.recordingId,
|
|
139
|
+
requestedAt: new Date().toISOString(),
|
|
140
|
+
recordingTitle: rec.title,
|
|
141
|
+
recordingDescription: rec.description,
|
|
142
|
+
transcriptStatus: transcript?.status ?? "pending",
|
|
143
|
+
transcriptText: transcript?.fullText ?? "",
|
|
144
|
+
stateKey,
|
|
145
|
+
instructions: KIND_PROMPTS[args.kind],
|
|
113
146
|
includeFullVideoInAi,
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
message: withFullVideoAiInstructions(
|
|
148
|
+
baseMessage,
|
|
149
|
+
args.recordingId,
|
|
150
|
+
includeFullVideoInAi,
|
|
151
|
+
),
|
|
152
|
+
};
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
await writeAppState(
|
|
155
|
+
`clips-ai-request-${args.recordingId}`,
|
|
156
|
+
request as any,
|
|
157
|
+
);
|
|
158
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
119
159
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
console.log(
|
|
161
|
+
`Delegation queued: generate-workflow (${args.kind}) for ${args.recordingId}`,
|
|
162
|
+
);
|
|
163
|
+
return {
|
|
164
|
+
queued: true,
|
|
165
|
+
recordingId: args.recordingId,
|
|
166
|
+
kind: args.kind,
|
|
167
|
+
stateKey,
|
|
168
|
+
includeFullVideoInAi,
|
|
169
|
+
};
|
|
170
|
+
} finally {
|
|
171
|
+
workflowGenerationLocks.delete(stateKey);
|
|
172
|
+
}
|
|
130
173
|
},
|
|
131
174
|
});
|
|
@@ -20,12 +20,20 @@
|
|
|
20
20
|
import { defineAction, embedApp } from "@agent-native/core";
|
|
21
21
|
import { readAppState } from "@agent-native/core/application-state";
|
|
22
22
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
23
|
+
import {
|
|
24
|
+
getRequestOrgId,
|
|
25
|
+
getRequestUserEmail,
|
|
26
|
+
} from "@agent-native/core/server/request-context";
|
|
23
27
|
import { resolveAccess, ForbiddenError } from "@agent-native/core/sharing";
|
|
24
|
-
import { asc, eq } from "drizzle-orm";
|
|
28
|
+
import { and, asc, eq, or, sql } from "drizzle-orm";
|
|
25
29
|
import { z } from "zod";
|
|
26
30
|
|
|
27
31
|
import { getDb, schema } from "../server/db/index.js";
|
|
28
32
|
import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
|
|
33
|
+
import {
|
|
34
|
+
canOpenDirectRecordingPage,
|
|
35
|
+
isRecordingExpired,
|
|
36
|
+
} from "../server/lib/recording-page-access.js";
|
|
29
37
|
import { parseSpaceIds } from "../server/lib/recordings.js";
|
|
30
38
|
import { parseBrowserDiagnosticsRow } from "../shared/browser-diagnostics.js";
|
|
31
39
|
import {
|
|
@@ -106,6 +114,61 @@ export default defineAction({
|
|
|
106
114
|
const db = getDb();
|
|
107
115
|
const rec: any = access.resource;
|
|
108
116
|
|
|
117
|
+
if (isRecordingExpired(rec.expiresAt)) {
|
|
118
|
+
throw new ForbiddenError("Recording has expired");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let hasExplicitShare = access.role === "owner";
|
|
122
|
+
if (rec.visibility === "public" && access.role !== "owner") {
|
|
123
|
+
const userEmail = getRequestUserEmail()?.trim().toLowerCase();
|
|
124
|
+
const orgId = getRequestOrgId();
|
|
125
|
+
const principals = [];
|
|
126
|
+
if (userEmail) {
|
|
127
|
+
principals.push(
|
|
128
|
+
and(
|
|
129
|
+
eq(schema.recordingShares.principalType, "user"),
|
|
130
|
+
sql`lower(${schema.recordingShares.principalId}) = ${userEmail}`,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (orgId) {
|
|
135
|
+
principals.push(
|
|
136
|
+
and(
|
|
137
|
+
eq(schema.recordingShares.principalType, "org"),
|
|
138
|
+
eq(schema.recordingShares.principalId, orgId),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (principals.length > 0) {
|
|
143
|
+
const [share] = await db
|
|
144
|
+
.select({ id: schema.recordingShares.id })
|
|
145
|
+
.from(schema.recordingShares)
|
|
146
|
+
.where(
|
|
147
|
+
and(
|
|
148
|
+
eq(schema.recordingShares.resourceId, args.recordingId),
|
|
149
|
+
or(...principals),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
.limit(1);
|
|
153
|
+
hasExplicitShare = Boolean(share);
|
|
154
|
+
} else {
|
|
155
|
+
hasExplicitShare = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
!canOpenDirectRecordingPage({
|
|
161
|
+
role: access.role,
|
|
162
|
+
visibility: rec.visibility,
|
|
163
|
+
hasPassword: Boolean(rec.password),
|
|
164
|
+
hasExplicitShare,
|
|
165
|
+
})
|
|
166
|
+
) {
|
|
167
|
+
throw new ForbiddenError(
|
|
168
|
+
"Open this recording from its share link instead of the direct recording URL",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
109
172
|
const [transcript] = await db
|
|
110
173
|
.select()
|
|
111
174
|
.from(schema.recordingTranscripts)
|
|
@@ -22,6 +22,12 @@ export default defineAction({
|
|
|
22
22
|
"Ask the agent to regenerate this recording's description/summary based on its transcript (and the full video when Include full video is enabled).",
|
|
23
23
|
schema: z.object({
|
|
24
24
|
recordingId: z.string().describe("Recording ID"),
|
|
25
|
+
openInChat: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"When true, focus the queued generation request in the agent chat instead of keeping it hidden.",
|
|
30
|
+
),
|
|
25
31
|
}),
|
|
26
32
|
run: async (args) => {
|
|
27
33
|
await assertAccess("recording", args.recordingId, "editor");
|
|
@@ -74,6 +80,7 @@ export default defineAction({
|
|
|
74
80
|
transcriptStatus: transcript?.status ?? "pending",
|
|
75
81
|
transcriptText: transcript?.fullText ?? "",
|
|
76
82
|
includeFullVideoInAi,
|
|
83
|
+
openInChat: args.openInChat === true,
|
|
77
84
|
message: withFullVideoAiInstructions(
|
|
78
85
|
baseMessage,
|
|
79
86
|
args.recordingId,
|
|
@@ -59,7 +59,9 @@ function buildTitleContext({
|
|
|
59
59
|
}): string | undefined {
|
|
60
60
|
const parts: string[] = [];
|
|
61
61
|
if (currentTitle && !isDefaultTitle(currentTitle)) {
|
|
62
|
-
parts.push(
|
|
62
|
+
parts.push(
|
|
63
|
+
`Current title: ${currentTitle}\nWhen regenerating, do not return this title verbatim; choose a more specific alternative when the transcript supports one.`,
|
|
64
|
+
);
|
|
63
65
|
}
|
|
64
66
|
if (agentsContext) parts.push(agentsContext);
|
|
65
67
|
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
@@ -237,9 +239,14 @@ export default defineAction({
|
|
|
237
239
|
|
|
238
240
|
if (!fresh) throw new Error(`Recording not found: ${args.recordingId}`);
|
|
239
241
|
|
|
242
|
+
const titleMatchesCurrent =
|
|
243
|
+
fresh.title?.trim().toLocaleLowerCase() ===
|
|
244
|
+
generatedTitle.trim().toLocaleLowerCase();
|
|
245
|
+
|
|
240
246
|
if (
|
|
241
|
-
|
|
242
|
-
fresh.title
|
|
247
|
+
!titleMatchesCurrent &&
|
|
248
|
+
(isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
|
|
249
|
+
fresh.title === rec.title)
|
|
243
250
|
) {
|
|
244
251
|
await db
|
|
245
252
|
.update(schema.recordings)
|
|
@@ -272,12 +279,18 @@ export default defineAction({
|
|
|
272
279
|
};
|
|
273
280
|
}
|
|
274
281
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
if (!titleMatchesCurrent) {
|
|
283
|
+
return {
|
|
284
|
+
updated: false,
|
|
285
|
+
skipped: true,
|
|
286
|
+
reason: "Recording title changed before generation completed",
|
|
287
|
+
recordingId: args.recordingId,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.warn(
|
|
292
|
+
`[clips] AI title regeneration returned the existing title for ${args.recordingId}; queueing a refinement instead`,
|
|
293
|
+
);
|
|
281
294
|
}
|
|
282
295
|
} catch (err) {
|
|
283
296
|
builderCreditsPaused = isBuilderCreditsExhaustedMessage(
|
|
@@ -302,9 +315,14 @@ export default defineAction({
|
|
|
302
315
|
|
|
303
316
|
if (!fresh) throw new Error(`Recording not found: ${args.recordingId}`);
|
|
304
317
|
|
|
318
|
+
const titleMatchesCurrent =
|
|
319
|
+
fresh.title?.trim().toLocaleLowerCase() ===
|
|
320
|
+
fallbackTitle.trim().toLocaleLowerCase();
|
|
321
|
+
|
|
305
322
|
if (
|
|
306
|
-
|
|
307
|
-
fresh.title
|
|
323
|
+
!titleMatchesCurrent &&
|
|
324
|
+
(isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
|
|
325
|
+
fresh.title === rec.title)
|
|
308
326
|
) {
|
|
309
327
|
await db
|
|
310
328
|
.update(schema.recordings)
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
parseEdits,
|
|
64
64
|
getExcludedRanges,
|
|
65
65
|
formatMs,
|
|
66
|
+
skipExcludedRange,
|
|
66
67
|
type EditsJson,
|
|
67
68
|
} from "@/lib/timestamp-mapping";
|
|
68
69
|
import { cn } from "@/lib/utils";
|
|
@@ -414,13 +415,20 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
414
415
|
if (!recording?.id) return;
|
|
415
416
|
const next = readPlaybackSpeedPreference(defaultPreviewSpeed);
|
|
416
417
|
setPlaybackSpeed(next);
|
|
417
|
-
if (videoRef.current)
|
|
418
|
+
if (videoRef.current) {
|
|
419
|
+
videoRef.current.defaultPlaybackRate = next;
|
|
420
|
+
videoRef.current.playbackRate = next;
|
|
421
|
+
}
|
|
418
422
|
}, [defaultPreviewSpeed, recording?.id]);
|
|
419
423
|
|
|
420
424
|
// Keep the editor preview speed visible and in sync with the media element.
|
|
425
|
+
// `defaultPlaybackRate` is set too so a `videoUrl` source swap that resets
|
|
426
|
+
// `playbackRate` (some browsers do this on load) falls back to the chosen
|
|
427
|
+
// speed instead of 1x.
|
|
421
428
|
useEffect(() => {
|
|
422
429
|
const v = videoRef.current;
|
|
423
430
|
if (!v) return;
|
|
431
|
+
v.defaultPlaybackRate = playbackSpeed;
|
|
424
432
|
v.playbackRate = playbackSpeed;
|
|
425
433
|
}, [playbackSpeed, videoUrl]);
|
|
426
434
|
|
|
@@ -428,17 +436,25 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
428
436
|
const next = parsePlaybackSpeed(rate) ?? 1.2;
|
|
429
437
|
setPlaybackSpeed(next);
|
|
430
438
|
savePlaybackSpeedPreference(next);
|
|
431
|
-
if (videoRef.current)
|
|
439
|
+
if (videoRef.current) {
|
|
440
|
+
videoRef.current.defaultPlaybackRate = next;
|
|
441
|
+
videoRef.current.playbackRate = next;
|
|
442
|
+
}
|
|
432
443
|
}, []);
|
|
433
444
|
|
|
434
445
|
// Keep the playheadMs in sync with the element's currentTime.
|
|
435
446
|
useEffect(() => {
|
|
436
447
|
const v = videoRef.current;
|
|
437
448
|
if (!v) return;
|
|
438
|
-
const onTime = () =>
|
|
449
|
+
const onTime = () => {
|
|
450
|
+
const rawMs = v.currentTime * 1000;
|
|
451
|
+
const visibleMs = skipExcludedRange(rawMs, excludedRanges, durationMs);
|
|
452
|
+
if (visibleMs !== rawMs) v.currentTime = visibleMs / 1000;
|
|
453
|
+
setPlayheadMs(visibleMs);
|
|
454
|
+
};
|
|
439
455
|
v.addEventListener("timeupdate", onTime);
|
|
440
456
|
return () => v.removeEventListener("timeupdate", onTime);
|
|
441
|
-
}, [videoUrl]);
|
|
457
|
+
}, [durationMs, excludedRanges, videoUrl]);
|
|
442
458
|
|
|
443
459
|
// Expose the in-editor state so the agent can read "the user is editing and scrubbed to X".
|
|
444
460
|
useEffect(() => {
|
|
@@ -510,11 +526,15 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
510
526
|
[recordingId, trim],
|
|
511
527
|
);
|
|
512
528
|
|
|
513
|
-
const seek = useCallback(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
529
|
+
const seek = useCallback(
|
|
530
|
+
(ms: number) => {
|
|
531
|
+
const visibleMs = skipExcludedRange(ms, excludedRanges, durationMs);
|
|
532
|
+
const v = videoRef.current;
|
|
533
|
+
if (v) v.currentTime = visibleMs / 1000;
|
|
534
|
+
setPlayheadMs(visibleMs);
|
|
535
|
+
},
|
|
536
|
+
[durationMs, excludedRanges],
|
|
537
|
+
);
|
|
518
538
|
|
|
519
539
|
// --- keyboard shortcuts -------------------------------------------------
|
|
520
540
|
useEffect(() => {
|
|
@@ -585,8 +605,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
585
605
|
|
|
586
606
|
// Default selection window so the TrimHandles have something to render.
|
|
587
607
|
const effectiveSelection = selectionRange ?? {
|
|
588
|
-
startMs:
|
|
589
|
-
endMs:
|
|
608
|
+
startMs: 0,
|
|
609
|
+
endMs: durationMs || 1_000,
|
|
590
610
|
};
|
|
591
611
|
|
|
592
612
|
if (playerDataQuery.isLoading) {
|
|
@@ -684,7 +704,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
684
704
|
playheadMs={playheadMs}
|
|
685
705
|
durationMs={durationMs}
|
|
686
706
|
excludedRanges={excludedRanges}
|
|
687
|
-
selectionRange={
|
|
707
|
+
selectionRange={effectiveSelection}
|
|
708
|
+
splitPoints={splitPoints}
|
|
688
709
|
activityRanges={transcriptSegments}
|
|
689
710
|
onSeek={seek}
|
|
690
711
|
scrollLeft={scrollLeft}
|
|
@@ -707,6 +728,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
707
728
|
value={effectiveSelection}
|
|
708
729
|
onChange={setSelectionRange}
|
|
709
730
|
durationMs={durationMs}
|
|
731
|
+
splitPoints={splitPoints}
|
|
710
732
|
/>
|
|
711
733
|
</div>
|
|
712
734
|
</div>
|
|
@@ -727,7 +749,6 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
|
|
|
727
749
|
durationMs={durationMs}
|
|
728
750
|
playheadMs={playheadMs}
|
|
729
751
|
chapters={chapters}
|
|
730
|
-
excludedRanges={excludedRanges}
|
|
731
752
|
splitPoints={splitPoints}
|
|
732
753
|
onSeek={seek}
|
|
733
754
|
onClickChapter={(c) => seek(c.startMs)}
|
|
@@ -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")}
|