mulmoclaude 0.5.2 → 0.6.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/Dockerfile.sandbox +100 -0
- package/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- package/bin/prepare-dist.js +18 -2
- package/client/assets/chunk-CernVdwh.js +1 -0
- package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
- package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
- package/client/assets/index-BbgSjFQ8.js +4968 -0
- package/client/assets/index-ECD0lgIv.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
- package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
- package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
- package/client/assets/vue-1e_vz2LW.js +1 -0
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
- package/client/index.html +33 -2
- package/package.json +20 -18
- package/sandbox-entrypoint.sh +106 -0
- package/server/accounting/accountNormalize.ts +32 -0
- package/server/accounting/defaultAccounts.ts +87 -0
- package/server/accounting/eventPublisher.ts +51 -0
- package/server/accounting/journal.ts +252 -0
- package/server/accounting/openingBalances.ts +114 -0
- package/server/accounting/report.ts +237 -0
- package/server/accounting/service.ts +718 -0
- package/server/accounting/snapshotCache.ts +333 -0
- package/server/accounting/timeSeries.ts +265 -0
- package/server/accounting/types.ts +148 -0
- package/server/agent/activeTools.ts +128 -0
- package/server/agent/attachmentConverter.ts +10 -5
- package/server/agent/backend/claude-code.ts +8 -2
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/config.ts +101 -31
- package/server/agent/index.ts +45 -33
- package/server/agent/mcp-server.ts +146 -69
- package/server/agent/mcp-tools/index.ts +1 -5
- package/server/agent/mcp-tools/notify.ts +2 -22
- package/server/agent/mcp-tools/x.ts +0 -4
- package/server/agent/mcpHealth.ts +168 -0
- package/server/agent/plugin-names.ts +20 -77
- package/server/agent/prompt.ts +259 -51
- package/server/agent/resumeFailover.ts +1 -1
- package/server/agent/stream.ts +0 -1
- package/server/api/auth/bearerAuth.ts +5 -5
- package/server/api/csrfGuard.ts +1 -1
- package/server/api/routes/accounting.ts +366 -0
- package/server/api/routes/agent.ts +509 -46
- package/server/api/routes/attachment.ts +104 -0
- package/server/api/routes/chart.ts +2 -1
- package/server/api/routes/config.ts +12 -12
- package/server/api/routes/files.ts +105 -48
- package/server/api/routes/image.ts +70 -25
- package/server/api/routes/journal.ts +35 -0
- package/server/api/routes/mulmo-script.ts +358 -118
- package/server/api/routes/mulmoScriptValidate.ts +1 -1
- package/server/api/routes/news.ts +1 -1
- package/server/api/routes/notifications.ts +92 -22
- package/server/api/routes/notifier.ts +98 -0
- package/server/api/routes/pdf.ts +188 -48
- package/server/api/routes/plugins.ts +34 -14
- package/server/api/routes/presentHtml.ts +58 -3
- package/server/api/routes/roles.ts +1 -8
- package/server/api/routes/runtime-plugin.ts +224 -0
- package/server/api/routes/scheduler.ts +7 -5
- package/server/api/routes/schedulerHandlers.ts +1 -1
- package/server/api/routes/schedulerTasks.ts +8 -7
- package/server/api/routes/sessions.ts +234 -121
- package/server/api/routes/skills.ts +56 -51
- package/server/api/routes/sources.ts +52 -45
- package/server/api/routes/translation.ts +44 -0
- package/server/api/routes/wiki/frontmatter.ts +13 -65
- package/server/api/routes/wiki/history.ts +261 -0
- package/server/api/routes/wiki/pageIndex.ts +1 -1
- package/server/api/routes/wiki.ts +50 -26
- package/server/events/file-change.ts +83 -0
- package/server/events/notifications.ts +247 -91
- package/server/events/pub-sub/index.ts +1 -1
- package/server/events/relay-client.ts +5 -5
- package/server/events/scheduler-adapter.ts +2 -2
- package/server/events/session-store/index.ts +110 -22
- package/server/events/task-manager/index.ts +10 -9
- package/server/index.ts +509 -33
- package/server/notifier/engine.ts +419 -0
- package/server/notifier/legacy-adapters.ts +76 -0
- package/server/notifier/runtime-api.ts +74 -0
- package/server/notifier/store.ts +70 -0
- package/server/notifier/types.ts +121 -0
- package/server/plugins/dev-loader.ts +171 -0
- package/server/plugins/dev-watcher.ts +150 -0
- package/server/plugins/diagnostics.ts +188 -0
- package/server/plugins/preset-list.ts +52 -0
- package/server/plugins/preset-loader.ts +112 -0
- package/server/plugins/runtime-chat-api.ts +38 -0
- package/server/plugins/runtime-loader.ts +430 -0
- package/server/plugins/runtime-registry.ts +112 -0
- package/server/plugins/runtime-tasks-api.ts +50 -0
- package/server/plugins/runtime.ts +378 -0
- package/server/services/translation/cache.ts +72 -0
- package/server/services/translation/index.ts +106 -0
- package/server/services/translation/llm.ts +140 -0
- package/server/services/translation/types.ts +35 -0
- package/server/system/credentials.ts +13 -2
- package/server/system/env.ts +6 -1
- package/server/system/logger/formatters.ts +46 -4
- package/server/system/logger/index.ts +4 -4
- package/server/system/logger/sinks.ts +26 -5
- package/server/system/logger/types.ts +2 -2
- package/server/utils/dev-plugin-args.d.mts +11 -0
- package/server/utils/dev-plugin-args.mjs +43 -0
- package/server/utils/errors.ts +13 -4
- package/server/utils/files/accounting-io.ts +295 -0
- package/server/utils/files/atomic.ts +17 -49
- package/server/utils/files/attachment-store.ts +182 -0
- package/server/utils/files/html-io.ts +1 -7
- package/server/utils/files/html-store.ts +19 -0
- package/server/utils/files/image-store.ts +20 -22
- package/server/utils/files/index.ts +5 -15
- package/server/utils/files/journal-io.ts +7 -35
- package/server/utils/files/json.ts +2 -29
- package/server/utils/files/markdown-image-fill.ts +6 -37
- package/server/utils/files/markdown-store.ts +6 -21
- package/server/utils/files/naming.ts +3 -39
- package/server/utils/files/plugins-io.ts +100 -0
- package/server/utils/files/reference-dirs-io.ts +1 -9
- package/server/utils/files/roles-io.ts +2 -10
- package/server/utils/files/safe.ts +17 -19
- package/server/utils/files/scheduler-io.ts +1 -7
- package/server/utils/files/scheduler-overrides-io.ts +3 -12
- package/server/utils/files/session-io.ts +21 -30
- package/server/utils/files/spreadsheet-store.ts +9 -22
- package/server/utils/files/translation-io.ts +46 -0
- package/server/utils/files/user-tasks-io.ts +1 -7
- package/server/utils/files/workspace-io.ts +3 -79
- package/server/utils/gemini.ts +33 -11
- package/server/utils/html/htmlArtifactSplicer.ts +41 -0
- package/server/utils/markdown/frontmatter.ts +112 -0
- package/server/utils/regex.ts +56 -0
- package/server/utils/router.ts +41 -0
- package/server/utils/slug.ts +5 -3
- package/server/utils/time.ts +12 -0
- package/server/workspace/chat-index/indexer.ts +15 -2
- package/server/workspace/chat-index/summarizer.ts +1 -1
- package/server/workspace/custom-dirs.ts +1 -1
- package/server/workspace/helps/gemini.md +1 -1
- package/server/workspace/helps/guide.md +61 -0
- package/server/workspace/helps/index.md +4 -0
- package/server/workspace/helps/presenthtml.md +80 -0
- package/server/workspace/helps/sandbox.md +7 -0
- package/server/workspace/helps/storyteller.md +101 -0
- package/server/workspace/helps/telegram.md +1 -0
- package/server/workspace/helps/wiki.md +9 -7
- package/server/workspace/journal/archivist-cli.ts +7 -33
- package/server/workspace/journal/archivist-schemas.ts +5 -43
- package/server/workspace/journal/dailyPass.ts +34 -187
- package/server/workspace/journal/diff.ts +3 -28
- package/server/workspace/journal/index.ts +10 -81
- package/server/workspace/journal/indexFile.ts +3 -24
- package/server/workspace/journal/latestDaily.ts +51 -0
- package/server/workspace/journal/memoryExtractor.ts +4 -20
- package/server/workspace/journal/optimizationPass.ts +4 -21
- package/server/workspace/journal/paths.ts +4 -23
- package/server/workspace/journal/state.ts +6 -29
- package/server/workspace/memory/io.ts +213 -0
- package/server/workspace/memory/llm-classifier.ts +158 -0
- package/server/workspace/memory/migrate.ts +263 -0
- package/server/workspace/memory/run.ts +84 -0
- package/server/workspace/memory/topic-cluster.ts +218 -0
- package/server/workspace/memory/topic-detect.ts +67 -0
- package/server/workspace/memory/topic-index-hook.ts +128 -0
- package/server/workspace/memory/topic-io.ts +180 -0
- package/server/workspace/memory/topic-migrate.ts +248 -0
- package/server/workspace/memory/topic-run.ts +172 -0
- package/server/workspace/memory/topic-swap.ts +135 -0
- package/server/workspace/memory/topic-types.ts +142 -0
- package/server/workspace/memory/types.ts +83 -0
- package/server/workspace/news/reader.ts +4 -5
- package/server/workspace/paths.ts +124 -47
- package/server/workspace/roles.ts +2 -11
- package/server/workspace/skills/parser.ts +38 -55
- package/server/workspace/skills/user-tasks.ts +1 -2
- package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
- package/server/workspace/skills-preset.ts +196 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
- package/server/workspace/sources/fetchers/index.ts +1 -1
- package/server/workspace/sources/fetchers/rssParser.ts +1 -1
- package/server/workspace/sources/pipeline/index.ts +2 -2
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/write.ts +2 -2
- package/server/workspace/sources/registry.ts +39 -61
- package/server/workspace/sources/robots.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +2 -1
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +6 -1
- package/server/workspace/wiki-backlinks/index.ts +19 -7
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
- package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
- package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
- package/server/workspace/wiki-history/provision.ts +181 -0
- package/server/workspace/wiki-pages/io.ts +217 -0
- package/server/workspace/wiki-pages/snapshot.ts +380 -0
- package/server/workspace/workspace.ts +75 -13
- package/src/App.vue +115 -40
- package/src/_runtime/protocol-vue.ts +21 -0
- package/src/_runtime/vue.ts +22 -0
- package/src/components/ChatInput.vue +14 -10
- package/src/components/CopyChatButton.vue +76 -0
- package/src/components/FileContentRenderer.vue +67 -14
- package/src/components/FileTree.vue +2 -2
- package/src/components/FilesView.vue +17 -1
- package/src/components/NewsView.vue +16 -2
- package/src/components/NotificationBell.vue +320 -93
- package/src/components/PageChatComposer.vue +5 -4
- package/src/components/PluginLauncher.vue +42 -6
- package/src/components/PluginScopedRoot.vue +87 -0
- package/src/components/RoleSelector.vue +12 -1
- package/src/components/RolesView.vue +562 -0
- package/src/components/SentAttachmentChip.vue +102 -0
- package/src/components/SessionHistoryPanel.vue +109 -20
- package/src/components/SessionRoleIcon.vue +7 -4
- package/src/components/SessionSidebar.vue +20 -7
- package/src/components/SessionTabBar.vue +1 -1
- package/src/components/SettingsMcpTab.vue +4 -4
- package/src/components/SettingsModal.vue +2 -0
- package/src/components/SidebarHeader.vue +16 -5
- package/src/components/SourcesManager.vue +23 -9
- package/src/components/SourcesView.vue +1 -1
- package/src/components/StackView.vue +102 -6
- package/src/components/SuggestionsPanel.vue +105 -16
- package/src/components/SystemFileBanner.vue +1 -1
- package/src/components/TodoExplorer.vue +4 -5
- package/src/components/todo/TodoAddDialog.vue +2 -3
- package/src/components/todo/TodoEditDialog.vue +1 -2
- package/src/components/todo/TodoEditPanel.vue +2 -3
- package/src/components/todo/TodoKanbanView.vue +8 -5
- package/src/components/todo/TodoListView.vue +3 -5
- package/src/components/todo/TodoTableView.vue +7 -5
- package/src/composables/useAccountingChannel.ts +58 -0
- package/src/composables/useActiveSession.ts +4 -25
- package/src/composables/useAppApi.ts +6 -44
- package/src/composables/useClipboardCopy.ts +3 -20
- package/src/composables/useContentDisplay.ts +33 -2
- package/src/composables/useDevPluginReload.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +5 -31
- package/src/composables/useEventListeners.ts +0 -20
- package/src/composables/useExpandedDirs.ts +4 -15
- package/src/composables/useFaviconState.ts +12 -46
- package/src/composables/useFileChange.ts +53 -0
- package/src/composables/useFreshPluginData.ts +6 -43
- package/src/composables/useHealth.ts +14 -43
- package/src/composables/useImageErrorRepair.ts +104 -0
- package/src/composables/useLatestDaily.ts +40 -0
- package/src/composables/useMarkdownDoc.ts +39 -0
- package/src/composables/useMarkdownLinkHandler.ts +1 -1
- package/src/composables/useMcpTools.ts +3 -16
- package/src/composables/useNotifications.ts +138 -112
- package/src/composables/usePdfDownload.ts +17 -3
- package/src/composables/usePendingCalls.ts +8 -26
- package/src/composables/usePluginErrorBoundary.ts +68 -0
- package/src/composables/usePubSub.ts +9 -17
- package/src/composables/useRunElapsed.ts +5 -22
- package/src/composables/useSandboxStatus.ts +4 -20
- package/src/composables/useSessionDerived.ts +7 -15
- package/src/composables/useSessionHistory.ts +70 -29
- package/src/composables/useSessionSync.ts +25 -3
- package/src/composables/useSkillsList.ts +59 -0
- package/src/composables/useTranslatedQueries.ts +109 -0
- package/src/config/apiRoutes.ts +181 -80
- package/src/config/historyFilters.ts +5 -3
- package/src/config/hostEvents.ts +17 -0
- package/src/config/mcpCatalog.ts +277 -5
- package/src/config/pubsubChannels.ts +134 -12
- package/src/config/roles.ts +212 -147
- package/src/config/systemFileDescriptors.ts +5 -5
- package/src/config/toolNames.ts +52 -30
- package/src/config/workspacePaths.ts +26 -2
- package/src/lang/de.ts +483 -27
- package/src/lang/en.ts +448 -27
- package/src/lang/es.ts +474 -27
- package/src/lang/fr.ts +476 -27
- package/src/lang/ja.ts +465 -27
- package/src/lang/ko.ts +466 -27
- package/src/lang/pt-BR.ts +473 -27
- package/src/lang/zh.ts +463 -27
- package/src/lib/vue-i18n.ts +1 -1
- package/src/lib/wiki-page/slug.ts +66 -0
- package/src/main.ts +85 -0
- package/src/plugins/_extras.ts +58 -0
- package/src/plugins/_generated/metas.ts +42 -0
- package/src/plugins/_generated/registrations.ts +44 -0
- package/src/plugins/_generated/server-bindings.ts +47 -0
- package/src/plugins/accounting/Preview.vue +106 -0
- package/src/plugins/accounting/View.vue +632 -0
- package/src/plugins/accounting/actions.ts +34 -0
- package/src/plugins/accounting/api.ts +301 -0
- package/src/plugins/accounting/components/AccountEditor.vue +250 -0
- package/src/plugins/accounting/components/AccountRow.vue +50 -0
- package/src/plugins/accounting/components/AccountsList.vue +102 -0
- package/src/plugins/accounting/components/AccountsModal.vue +300 -0
- package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
- package/src/plugins/accounting/components/BookSettings.vue +284 -0
- package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
- package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
- package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
- package/src/plugins/accounting/components/JournalList.vue +553 -0
- package/src/plugins/accounting/components/Ledger.vue +206 -0
- package/src/plugins/accounting/components/NewBookForm.vue +211 -0
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
- package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
- package/src/plugins/accounting/components/accountDraft.ts +13 -0
- package/src/plugins/accounting/components/accountNumbering.ts +103 -0
- package/src/plugins/accounting/components/accountValidation.ts +75 -0
- package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
- package/src/plugins/accounting/countries.ts +158 -0
- package/src/plugins/accounting/currencies.ts +64 -0
- package/src/plugins/accounting/dates.ts +51 -0
- package/src/plugins/accounting/definition.ts +199 -0
- package/src/plugins/accounting/fiscalYear.ts +136 -0
- package/src/plugins/accounting/index.ts +49 -0
- package/src/plugins/accounting/meta.ts +91 -0
- package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
- package/src/plugins/api.ts +125 -0
- package/src/plugins/canvas/View.vue +38 -28
- package/src/plugins/canvas/definition.ts +10 -8
- package/src/plugins/canvas/index.ts +15 -8
- package/src/plugins/canvas/meta.ts +12 -0
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +2 -2
- package/src/plugins/chart/definition.ts +12 -2
- package/src/plugins/chart/index.ts +15 -7
- package/src/plugins/chart/meta.ts +18 -0
- package/src/plugins/editImages/definition.ts +44 -0
- package/src/plugins/editImages/index.ts +43 -0
- package/src/plugins/editImages/meta.ts +5 -0
- package/src/plugins/generateImage/View.vue +3 -1
- package/src/plugins/generateImage/definition.ts +2 -0
- package/src/plugins/generateImage/index.ts +13 -5
- package/src/plugins/generateImage/meta.ts +5 -0
- package/src/plugins/index.ts +35 -0
- package/src/plugins/manageRoles/Preview.vue +7 -4
- package/src/plugins/manageRoles/View.vue +12 -8
- package/src/plugins/manageRoles/definition.ts +6 -0
- package/src/plugins/manageRoles/index.ts +7 -6
- package/src/plugins/manageSkills/View.vue +11 -7
- package/src/plugins/manageSkills/definition.ts +4 -1
- package/src/plugins/manageSkills/index.ts +14 -7
- package/src/plugins/manageSkills/meta.ts +21 -0
- package/src/plugins/manageSource/definition.ts +4 -1
- package/src/plugins/manageSource/index.ts +15 -7
- package/src/plugins/manageSource/meta.ts +21 -0
- package/src/plugins/markdown/Preview.vue +10 -8
- package/src/plugins/markdown/View.vue +84 -17
- package/src/plugins/markdown/definition.ts +7 -1
- package/src/plugins/markdown/index.ts +15 -8
- package/src/plugins/markdown/meta.ts +16 -0
- package/src/plugins/meta-types.ts +97 -0
- package/src/plugins/metas.ts +224 -0
- package/src/plugins/presentForm/Preview.vue +4 -15
- package/src/plugins/presentForm/View.vue +35 -78
- package/src/plugins/presentForm/definition.ts +7 -6
- package/src/plugins/presentForm/index.ts +12 -5
- package/src/plugins/presentForm/meta.ts +11 -0
- package/src/plugins/presentForm/plugin.ts +8 -9
- package/src/plugins/presentForm/types.ts +0 -24
- package/src/plugins/presentHtml/Preview.vue +1 -8
- package/src/plugins/presentHtml/View.vue +401 -30
- package/src/plugins/presentHtml/definition.ts +8 -5
- package/src/plugins/presentHtml/index.ts +15 -8
- package/src/plugins/presentHtml/meta.ts +14 -0
- package/src/plugins/presentMulmoScript/View.vue +327 -107
- package/src/plugins/presentMulmoScript/definition.ts +34 -7
- package/src/plugins/presentMulmoScript/helpers.ts +4 -5
- package/src/plugins/presentMulmoScript/index.ts +20 -7
- package/src/plugins/presentMulmoScript/meta.ts +52 -0
- package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
- package/src/plugins/scheduler/Preview.vue +5 -2
- package/src/plugins/scheduler/TasksTab.vue +16 -36
- package/src/plugins/scheduler/View.vue +22 -54
- package/src/plugins/scheduler/automationsDefinition.ts +14 -9
- package/src/plugins/scheduler/automationsMeta.ts +5 -0
- package/src/plugins/scheduler/calendarDefinition.ts +4 -7
- package/src/plugins/scheduler/calendarMeta.ts +28 -0
- package/src/plugins/scheduler/formatSchedule.ts +6 -24
- package/src/plugins/scheduler/index.ts +26 -52
- package/src/plugins/scope.ts +57 -0
- package/src/plugins/server-bindings-types.ts +38 -0
- package/src/plugins/server.ts +32 -0
- package/src/plugins/skill/Preview.vue +25 -0
- package/src/plugins/skill/View.vue +125 -0
- package/src/plugins/skill/definition.ts +23 -0
- package/src/plugins/skill/index.ts +36 -0
- package/src/plugins/skill/plugin.ts +31 -0
- package/src/plugins/skill/types.ts +21 -0
- package/src/plugins/spreadsheet/Preview.vue +1 -3
- package/src/plugins/spreadsheet/View.vue +29 -49
- package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
- package/src/plugins/spreadsheet/definition.ts +5 -2
- package/src/plugins/spreadsheet/index.ts +15 -8
- package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
- package/src/plugins/spreadsheet/meta.ts +14 -0
- package/src/plugins/textResponse/Preview.vue +9 -1
- package/src/plugins/textResponse/View.vue +59 -8
- package/src/plugins/textResponse/index.ts +11 -3
- package/src/plugins/textResponse/plugin.ts +8 -10
- package/src/plugins/textResponse/types.ts +28 -0
- package/src/plugins/wiki/Preview.vue +6 -4
- package/src/plugins/wiki/View.vue +463 -254
- package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
- package/src/plugins/wiki/helpers.ts +17 -0
- package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
- package/src/plugins/wiki/history/HistoryTab.vue +167 -0
- package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
- package/src/plugins/wiki/history/api.ts +52 -0
- package/src/plugins/wiki/history/diff.ts +145 -0
- package/src/plugins/wiki/index.ts +42 -32
- package/src/plugins/wiki/meta.ts +10 -0
- package/src/plugins/wiki/pageEditLoader.ts +53 -0
- package/src/plugins/wiki/route.ts +8 -0
- package/src/router/guards.ts +2 -1
- package/src/router/index.ts +19 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/tools/index.ts +50 -51
- package/src/tools/runtimeLoader.ts +141 -0
- package/src/tools/types.ts +44 -1
- package/src/types/notification.ts +23 -0
- package/src/types/pastedFile.ts +10 -0
- package/src/types/session.ts +61 -3
- package/src/types/sse.ts +21 -6
- package/src/utils/agent/eventDispatch.ts +12 -9
- package/src/utils/agent/pastedAttachment.ts +35 -0
- package/src/utils/agent/request.ts +32 -3
- package/src/utils/agent/toolCalls.ts +7 -1
- package/src/utils/api.ts +1 -1
- package/src/utils/chat/exportMarkdown.ts +243 -0
- package/src/utils/errors.ts +10 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/filesPreview/todoPreview.ts +13 -2
- package/src/utils/format/date.ts +1 -3
- package/src/utils/format/jsonSyntax.ts +5 -0
- package/src/utils/html/iframeHeightReporterScript.ts +62 -0
- package/src/utils/html/previewCsp.ts +29 -2
- package/src/utils/image/htmlSrcAttrs.ts +122 -0
- package/src/utils/image/imageRepairInlineScript.ts +115 -0
- package/src/utils/image/resolve.ts +17 -3
- package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
- package/src/utils/markdown/frontmatter.ts +125 -0
- package/src/utils/markdown/taskList.ts +7 -2
- package/src/utils/plugin/runtime.ts +132 -0
- package/src/utils/session/mergeSessions.ts +40 -37
- package/src/utils/session/sessionEntries.ts +74 -18
- package/src/utils/session/sessionHelpers.ts +54 -10
- package/src/utils/tools/result.ts +76 -14
- package/src/vite-env.d.ts +6 -0
- package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
- package/client/assets/index-CY-WpQUm.css +0 -2
- package/client/assets/index-DbTz2Mfs.js +0 -4911
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/server/api/routes/html.ts +0 -114
- package/server/api/routes/todos.ts +0 -293
- package/server/api/routes/todosColumnsHandlers.ts +0 -333
- package/server/api/routes/todosHandlers.ts +0 -274
- package/server/api/routes/todosItemsHandlers.ts +0 -386
- package/server/utils/files/todos-io.ts +0 -29
- package/src/components/NotificationToast.vue +0 -75
- package/src/plugins/editImage/definition.ts +0 -27
- package/src/plugins/editImage/index.ts +0 -37
- package/src/plugins/presentHtml/helpers.ts +0 -72
- package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
- package/src/plugins/scheduler/legacyShape.ts +0 -34
- package/src/plugins/todo/Preview.vue +0 -68
- package/src/plugins/todo/View.vue +0 -378
- package/src/plugins/todo/composables/useTodos.ts +0 -179
- package/src/plugins/todo/definition.ts +0 -45
- package/src/plugins/todo/index.ts +0 -62
- package/src/plugins/todo/labels.ts +0 -163
- package/src/plugins/todo/priority.ts +0 -98
- package/src/plugins/todo/viewModes.ts +0 -19
- package/src/plugins/wiki/definition.ts +0 -25
- package/src/tools/legacyPluginNames.ts +0 -13
- package/src/utils/format/frontmatter.ts +0 -80
- package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
- package/src/utils/notification/dispatch.ts +0 -58
- /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
- /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
- /package/src/plugins/{editImage → editImages}/View.vue +0 -0
- /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
|
@@ -15,30 +15,74 @@
|
|
|
15
15
|
<span v-if="filePath" class="truncate">{{ filePath }}</span>
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
18
|
-
<div class="ml-4 shrink-0 flex gap-2">
|
|
19
|
-
<!--
|
|
20
|
-
|
|
18
|
+
<div class="ml-4 shrink-0 flex items-center gap-2">
|
|
19
|
+
<!-- Play presentation: opens the lightbox at beat 0 and starts
|
|
20
|
+
audio. Same gating as Download Movie — only when a movie has
|
|
21
|
+
been generated, which is our proxy for "every beat has both
|
|
22
|
+
an image and audio on disk". Green outline + green icon
|
|
23
|
+
share the visual idiom with the (filled) Download button so
|
|
24
|
+
both completed-artifact actions read as the same family.
|
|
25
|
+
`isPlayReady` ensures we don't open the lightbox before the
|
|
26
|
+
first beat's image (and audio, if it has text) finish their
|
|
27
|
+
async load — moviePath can be set while loadExistingBeatImage
|
|
28
|
+
is still in flight. -->
|
|
29
|
+
<button
|
|
30
|
+
v-if="moviePath && !movieGenerating"
|
|
31
|
+
class="h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
32
|
+
:disabled="!isPlayReady"
|
|
33
|
+
:title="t('pluginMulmoScript.playPresentation')"
|
|
34
|
+
:aria-label="t('pluginMulmoScript.playPresentation')"
|
|
35
|
+
@click="playPresentation"
|
|
36
|
+
>
|
|
37
|
+
<span class="material-icons text-base">play_arrow</span>
|
|
38
|
+
</button>
|
|
39
|
+
<!-- Download Movie: bearer-authenticated blob fetch, then a
|
|
40
|
+
synthetic <a download> click. The natural <a href download>
|
|
41
|
+
approach can't attach the Authorization header, which would
|
|
42
|
+
have forced a bearer-auth exemption on the route — the
|
|
43
|
+
reviewer's P1 was that any sibling process could then read
|
|
44
|
+
a caller-controlled movie path. Going through apiFetchRaw
|
|
45
|
+
(auto-attaches bearer) keeps the auth boundary intact. -->
|
|
46
|
+
<button
|
|
21
47
|
v-if="moviePath && !movieGenerating"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
class="h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
|
|
49
|
+
:disabled="movieDownloading"
|
|
50
|
+
data-testid="mulmo-script-download-movie-button"
|
|
51
|
+
@click="downloadMovie"
|
|
25
52
|
>
|
|
26
|
-
<span class="material-icons text-
|
|
53
|
+
<span class="material-icons text-base">download</span>
|
|
27
54
|
<span>{{ t("pluginMulmoScript.movie") }}</span>
|
|
28
|
-
</
|
|
29
|
-
<!--
|
|
55
|
+
</button>
|
|
56
|
+
<!-- Regenerate Movie (icon-only): collapses to a square once a
|
|
57
|
+
movie exists — the adjacent Download / Play already make
|
|
58
|
+
the subject clear, so the "Movie" label only adds noise. -->
|
|
59
|
+
<button
|
|
60
|
+
v-if="moviePath && !movieGenerating"
|
|
61
|
+
class="h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors"
|
|
62
|
+
:title="t('pluginMulmoScript.regenerateMovie')"
|
|
63
|
+
:aria-label="t('pluginMulmoScript.regenerateMovie')"
|
|
64
|
+
data-testid="mulmo-script-regenerate-movie-button"
|
|
65
|
+
@click="generateMovie"
|
|
66
|
+
>
|
|
67
|
+
<span class="material-icons text-base">refresh</span>
|
|
68
|
+
</button>
|
|
69
|
+
<!-- Generate Movie (pill): no movie yet, or one is currently
|
|
70
|
+
generating. Keeps the label so first-time users know what
|
|
71
|
+
they're triggering. -->
|
|
30
72
|
<button
|
|
31
|
-
|
|
73
|
+
v-else
|
|
74
|
+
class="h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
32
75
|
:disabled="movieGenerating"
|
|
76
|
+
data-testid="mulmo-script-generate-movie-button"
|
|
33
77
|
@click="generateMovie"
|
|
34
78
|
>
|
|
35
|
-
<svg v-if="movieGenerating" class="animate-spin w-
|
|
79
|
+
<svg v-if="movieGenerating" class="animate-spin w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="none">
|
|
36
80
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
37
81
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
|
38
82
|
</svg>
|
|
39
83
|
<span v-if="movieGenerating">{{ t("pluginMulmoScript.generating") }}</span>
|
|
40
84
|
<template v-else>
|
|
41
|
-
<span class="material-icons text-sm
|
|
85
|
+
<span class="material-icons text-sm">refresh</span>
|
|
42
86
|
<span>{{ t("pluginMulmoScript.movie") }}</span>
|
|
43
87
|
</template>
|
|
44
88
|
</button>
|
|
@@ -216,7 +260,9 @@
|
|
|
216
260
|
{{ playingAudio?.index === index ? t("pluginMulmoScript.stop") : t("pluginMulmoScript.play") }}
|
|
217
261
|
</button>
|
|
218
262
|
<template v-else-if="audioErrors[index]">
|
|
219
|
-
<span class="text-xs text-red-400" :title="audioErrors[index]">
|
|
263
|
+
<span class="text-xs text-red-400 truncate min-w-0 max-w-[20rem]" :title="audioErrors[index]">
|
|
264
|
+
{{ t("pluginMulmoScript.errPrefix") }} {{ audioErrors[index] }}
|
|
265
|
+
</span>
|
|
220
266
|
<button
|
|
221
267
|
v-if="effectiveBeat(index).text"
|
|
222
268
|
class="text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50"
|
|
@@ -234,7 +280,12 @@
|
|
|
234
280
|
{{ t("pluginMulmoScript.generateAudio") }}
|
|
235
281
|
</button>
|
|
236
282
|
</div>
|
|
237
|
-
<button
|
|
283
|
+
<button
|
|
284
|
+
class="text-gray-400 hover:text-gray-600"
|
|
285
|
+
:title="sourceOpen[index] ? 'Hide source' : 'Show source'"
|
|
286
|
+
:data-testid="`mulmo-script-beat-source-toggle-${index}`"
|
|
287
|
+
@click="toggleSource(index)"
|
|
288
|
+
>
|
|
238
289
|
<svg
|
|
239
290
|
xmlns="http://www.w3.org/2000/svg"
|
|
240
291
|
class="w-3.5 h-3.5"
|
|
@@ -261,6 +312,7 @@
|
|
|
261
312
|
:class="isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'"
|
|
262
313
|
rows="8"
|
|
263
314
|
spellcheck="false"
|
|
315
|
+
:data-testid="`mulmo-script-beat-source-textarea-${index}`"
|
|
264
316
|
/>
|
|
265
317
|
<div class="flex items-center justify-end gap-2 px-2 pb-2">
|
|
266
318
|
<span v-if="beatSaveErrors[index]" class="text-xs text-red-600" role="alert">{{
|
|
@@ -276,6 +328,7 @@
|
|
|
276
328
|
: 'border-gray-200 text-gray-300 cursor-not-allowed'
|
|
277
329
|
"
|
|
278
330
|
:disabled="!isValidBeat(index) || !!beatSaving[index]"
|
|
331
|
+
:data-testid="`mulmo-script-beat-update-button-${index}`"
|
|
279
332
|
@click="updateBeat(index)"
|
|
280
333
|
>
|
|
281
334
|
{{ beatSaving[index] ? t("pluginMulmoScript.saving") : t("pluginMulmoScript.update") }}
|
|
@@ -308,42 +361,74 @@
|
|
|
308
361
|
</div>
|
|
309
362
|
|
|
310
363
|
<!-- Lightbox -->
|
|
311
|
-
<div v-if="lightbox" class="fixed inset-0 z-50
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
364
|
+
<div v-if="lightbox" class="fixed inset-0 z-50 bg-black/80 overflow-y-auto" @click="closeLightbox">
|
|
365
|
+
<button class="fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none" :title="t('common.close')" @click.stop="closeLightbox">
|
|
366
|
+
✕
|
|
367
|
+
</button>
|
|
368
|
+
<div class="flex flex-col items-center gap-4 pt-4 pb-8" @click.stop>
|
|
369
|
+
<div class="flex items-center gap-4">
|
|
370
|
+
<button
|
|
371
|
+
v-if="!lightbox.isCharacter"
|
|
372
|
+
class="text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none"
|
|
373
|
+
:disabled="!hasPrev"
|
|
374
|
+
@click="lightboxMove(-1)"
|
|
375
|
+
>
|
|
376
|
+
‹
|
|
377
|
+
</button>
|
|
378
|
+
<div class="flex flex-col items-center">
|
|
379
|
+
<img :src="lightbox.src" class="max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl" />
|
|
380
|
+
<div v-if="!lightbox.isCharacter && beats.length > 1" class="relative w-full h-1">
|
|
381
|
+
<div class="flex gap-1 h-full">
|
|
382
|
+
<div
|
|
383
|
+
v-for="i in beats.length"
|
|
384
|
+
:key="i - 1"
|
|
385
|
+
class="group flex-1 cursor-pointer relative transition-colors"
|
|
386
|
+
:class="
|
|
387
|
+
i - 1 === lightbox.index
|
|
388
|
+
? 'bg-white/80 hover:bg-white'
|
|
389
|
+
: i - 1 < lightbox.index
|
|
390
|
+
? 'bg-white/40 hover:bg-white/60'
|
|
391
|
+
: 'bg-white/20 hover:bg-white/40'
|
|
392
|
+
"
|
|
393
|
+
@click="jumpToBeat(i - 1)"
|
|
394
|
+
>
|
|
395
|
+
<span class="absolute -inset-y-3 inset-x-0" />
|
|
396
|
+
<div
|
|
397
|
+
v-if="beatTooltip(i - 1)"
|
|
398
|
+
class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity"
|
|
399
|
+
>
|
|
400
|
+
{{ beatTooltip(i - 1) }}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
<div
|
|
405
|
+
v-if="playingAudio && playingAudio.index === lightbox.index"
|
|
406
|
+
class="absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none"
|
|
407
|
+
:style="{ left: `${((lightbox.index + audioProgress) / beats.length) * 100}%` }"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
337
410
|
</div>
|
|
411
|
+
<button
|
|
412
|
+
v-if="!lightbox.isCharacter"
|
|
413
|
+
class="text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none"
|
|
414
|
+
:disabled="!hasNext"
|
|
415
|
+
@click="lightboxMove(1)"
|
|
416
|
+
>
|
|
417
|
+
›
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
<div v-if="lightbox.text || beatAudios[lightbox.index]" class="relative w-screen flex justify-center px-16">
|
|
421
|
+
<p v-if="lightbox.text" class="max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]">
|
|
422
|
+
{{ lightbox.text }}
|
|
423
|
+
</p>
|
|
424
|
+
<button
|
|
425
|
+
v-if="beatAudios[lightbox.index]"
|
|
426
|
+
class="absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20"
|
|
427
|
+
@click="playAudio(lightbox.index)"
|
|
428
|
+
>
|
|
429
|
+
{{ playingAudio?.index === lightbox.index ? t("pluginMulmoScript.stop") : t("pluginMulmoScript.play") }}
|
|
430
|
+
</button>
|
|
338
431
|
</div>
|
|
339
|
-
<button
|
|
340
|
-
v-if="!lightbox.isCharacter"
|
|
341
|
-
class="text-white/60 hover:text-white disabled:opacity-20 text-4xl leading-none"
|
|
342
|
-
:disabled="!hasNext"
|
|
343
|
-
@click="lightboxMove(1)"
|
|
344
|
-
>
|
|
345
|
-
›
|
|
346
|
-
</button>
|
|
347
432
|
</div>
|
|
348
433
|
</div>
|
|
349
434
|
</div>
|
|
@@ -353,18 +438,22 @@
|
|
|
353
438
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
|
354
439
|
import { useI18n } from "vue-i18n";
|
|
355
440
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
356
|
-
|
|
357
|
-
const { t } = useI18n();
|
|
358
441
|
import type { MulmoScriptData } from "./index";
|
|
359
442
|
import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
|
|
360
443
|
import { extractErrorMessage, getMissingCharacterKeys, shouldAutoRenderBeat, streamMovieEvents, validateBeatJSON } from "./helpers";
|
|
361
444
|
import { apiGet, apiPost, apiFetchRaw } from "../../utils/api";
|
|
362
|
-
import {
|
|
445
|
+
import { pluginEndpoints } from "../api";
|
|
446
|
+
import type { MulmoScriptEndpoints } from "./definition";
|
|
363
447
|
import { errorMessage } from "../../utils/errors";
|
|
364
448
|
import { useClipboardCopy } from "../../composables/useClipboardCopy";
|
|
365
449
|
import { useActiveSession } from "../../composables/useActiveSession";
|
|
366
450
|
import { GENERATION_KINDS, type PendingGeneration } from "../../types/events";
|
|
367
451
|
|
|
452
|
+
const endpoints = pluginEndpoints<MulmoScriptEndpoints>("mulmoScript");
|
|
453
|
+
const filesEndpoints = pluginEndpoints<{ content: string }>("files");
|
|
454
|
+
|
|
455
|
+
const { t } = useI18n();
|
|
456
|
+
|
|
368
457
|
interface Beat {
|
|
369
458
|
speaker?: string;
|
|
370
459
|
text?: string;
|
|
@@ -401,10 +490,6 @@ const script = computed<MulmoScript>(() => data.value?.script ?? {});
|
|
|
401
490
|
const filePath = computed(() => data.value?.filePath ?? "");
|
|
402
491
|
const beats = computed<Beat[]>(() => script.value.beats ?? []);
|
|
403
492
|
|
|
404
|
-
// Exposed to the template so the `<a :href="...">` download button
|
|
405
|
-
// can compose a query-string URL without inlining the API path.
|
|
406
|
-
const downloadMovieBase = API_ROUTES.mulmoScript.downloadMovie;
|
|
407
|
-
|
|
408
493
|
// Per-beat render state
|
|
409
494
|
type RenderState = "idle" | "rendering" | "done" | "error";
|
|
410
495
|
const renderState = reactive<Record<number, RenderState>>({});
|
|
@@ -416,16 +501,21 @@ const sourceText = reactive<Record<number, string>>({});
|
|
|
416
501
|
// the Update button. Cleared on next successful save or editor close.
|
|
417
502
|
// Store raw error + kind tag so the template picks a localized key,
|
|
418
503
|
// instead of pre-composing an English-prefixed string here.
|
|
419
|
-
|
|
504
|
+
interface BeatSaveError {
|
|
505
|
+
kind: "invalidJson" | "saveFailed";
|
|
506
|
+
error: string;
|
|
507
|
+
}
|
|
420
508
|
const beatSaveErrors = reactive<Record<number, BeatSaveError>>({});
|
|
421
509
|
const beatSaving = reactive<Record<number, boolean>>({});
|
|
422
510
|
const localOverrides = reactive<Record<number, Beat>>({});
|
|
423
511
|
const movieGenerating = ref(false);
|
|
512
|
+
const movieDownloading = ref(false);
|
|
424
513
|
const moviePath = ref<string | null>(null);
|
|
425
514
|
const beatAudios = reactive<Record<number, string>>({});
|
|
426
515
|
const audioState = reactive<Record<number, "generating" | "done" | "error">>({});
|
|
427
516
|
const audioErrors = reactive<Record<number, string>>({});
|
|
428
517
|
const playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);
|
|
518
|
+
const audioProgress = ref(0);
|
|
429
519
|
const beatListEl = ref<HTMLElement | null>(null);
|
|
430
520
|
const lightbox = ref<{
|
|
431
521
|
src: string;
|
|
@@ -474,11 +564,15 @@ function characterPrompt(key: string): string {
|
|
|
474
564
|
return (script.value.imageParams?.images?.[key]?.prompt as string) ?? "";
|
|
475
565
|
}
|
|
476
566
|
|
|
567
|
+
function stopPlayingAudio() {
|
|
568
|
+
if (!playingAudio.value) return;
|
|
569
|
+
playingAudio.value.audio.pause();
|
|
570
|
+
playingAudio.value = null;
|
|
571
|
+
audioProgress.value = 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
477
574
|
function openLightbox(index: number) {
|
|
478
|
-
|
|
479
|
-
playingAudio.value.audio.pause();
|
|
480
|
-
playingAudio.value = null;
|
|
481
|
-
}
|
|
575
|
+
stopPlayingAudio();
|
|
482
576
|
lightbox.value = {
|
|
483
577
|
src: renderedImages[index],
|
|
484
578
|
text: effectiveBeat(index).text,
|
|
@@ -486,6 +580,44 @@ function openLightbox(index: number) {
|
|
|
486
580
|
};
|
|
487
581
|
}
|
|
488
582
|
|
|
583
|
+
// Backdrop click handler. Stops any in-flight narration so the audio
|
|
584
|
+
// doesn't keep playing after the lightbox is dismissed — without this,
|
|
585
|
+
// the HTMLAudioElement created by playAudio() outlives the modal and
|
|
586
|
+
// the user hears disembodied narration with no UI to stop it.
|
|
587
|
+
function closeLightbox() {
|
|
588
|
+
stopPlayingAudio();
|
|
589
|
+
lightbox.value = null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// "Play presentation" toolbar action. Opens the lightbox at beat 0 and
|
|
593
|
+
// kicks off its narration audio; the existing on-ended hook then chains
|
|
594
|
+
// through the rest of the deck (lightboxMove(1) → playAudio if the next
|
|
595
|
+
// beat has audio), so one click runs the whole presentation. Only wired
|
|
596
|
+
// to the toolbar button when moviePath is set, which is our proxy for
|
|
597
|
+
// "every beat has both image and audio on disk".
|
|
598
|
+
//
|
|
599
|
+
// `moviePath` arrives synchronously from /movie-status, but the per-beat
|
|
600
|
+
// image and audio data URIs are populated asynchronously by
|
|
601
|
+
// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().
|
|
602
|
+
// The Play button can therefore become visible before beat 0's assets
|
|
603
|
+
// hydrate — `isPlayReady` gates the click so the lightbox never opens
|
|
604
|
+
// with an undefined src or silent narration on a beat that does have
|
|
605
|
+
// text.
|
|
606
|
+
const isPlayReady = computed<boolean>(() => {
|
|
607
|
+
if (beats.value.length === 0) return false;
|
|
608
|
+
if (!renderedImages[0]) return false;
|
|
609
|
+
// Audio is only required when the beat has text (the source of TTS).
|
|
610
|
+
// Beats without text are valid; they just play silently.
|
|
611
|
+
if (effectiveBeat(0).text && !beatAudios[0]) return false;
|
|
612
|
+
return true;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
function playPresentation() {
|
|
616
|
+
if (!isPlayReady.value) return;
|
|
617
|
+
openLightbox(0);
|
|
618
|
+
if (beatAudios[0]) playAudio(0);
|
|
619
|
+
}
|
|
620
|
+
|
|
489
621
|
const hasPrev = computed(() => {
|
|
490
622
|
if (!lightbox.value) return false;
|
|
491
623
|
for (let i = lightbox.value.index - 1; i >= 0; i--) {
|
|
@@ -502,13 +634,39 @@ const hasNext = computed(() => {
|
|
|
502
634
|
return false;
|
|
503
635
|
});
|
|
504
636
|
|
|
637
|
+
function jumpToBeat(index: number) {
|
|
638
|
+
if (!lightbox.value) return;
|
|
639
|
+
if (index === lightbox.value.index) return;
|
|
640
|
+
if (!renderedImages[index]) return;
|
|
641
|
+
const wasPlaying = playingAudio.value !== null;
|
|
642
|
+
openLightbox(index);
|
|
643
|
+
if (wasPlaying && beatAudios[index]) {
|
|
644
|
+
playAudio(index);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function beatTooltip(index: number): string {
|
|
649
|
+
const text = effectiveBeat(index).text ?? "";
|
|
650
|
+
return text.length > 80 ? `${text.slice(0, 80)}…` : text;
|
|
651
|
+
}
|
|
652
|
+
|
|
505
653
|
function lightboxMove(delta: number) {
|
|
506
654
|
if (!lightbox.value) return;
|
|
507
655
|
const total = beats.value.length;
|
|
656
|
+
// If audio was playing when the user clicked the arrow, carry the
|
|
657
|
+
// playback over to the next beat that has audio. openLightbox()
|
|
658
|
+
// unconditionally stops any active audio, so we capture the flag
|
|
659
|
+
// BEFORE that and replay AFTER. The on-ended auto-advance path
|
|
660
|
+
// already nulls playingAudio before calling lightboxMove, so this
|
|
661
|
+
// branch won't double-fire there.
|
|
662
|
+
const wasPlaying = playingAudio.value !== null;
|
|
508
663
|
let i = lightbox.value.index + delta;
|
|
509
664
|
while (i >= 0 && i < total) {
|
|
510
665
|
if (renderedImages[i]) {
|
|
511
666
|
openLightbox(i);
|
|
667
|
+
if (wasPlaying && beatAudios[i]) {
|
|
668
|
+
playAudio(i);
|
|
669
|
+
}
|
|
512
670
|
return;
|
|
513
671
|
}
|
|
514
672
|
i += delta;
|
|
@@ -545,7 +703,7 @@ async function onSourceToggle(open: boolean) {
|
|
|
545
703
|
let text = scriptSourceText.value;
|
|
546
704
|
// Read the current file from disk so beat-level edits are reflected
|
|
547
705
|
if (filePath.value) {
|
|
548
|
-
const response = await apiGet<{ content?: string }>(
|
|
706
|
+
const response = await apiGet<{ content?: string }>(filesEndpoints.content, { path: filePath.value });
|
|
549
707
|
if (response.ok && response.data.content) {
|
|
550
708
|
text = response.data.content;
|
|
551
709
|
}
|
|
@@ -568,7 +726,7 @@ async function applySource() {
|
|
|
568
726
|
alert(extractErrorMessage(err));
|
|
569
727
|
return;
|
|
570
728
|
}
|
|
571
|
-
const response = await apiPost<unknown>(
|
|
729
|
+
const response = await apiPost<unknown>(endpoints.updateScript.url, {
|
|
572
730
|
filePath: filePath.value,
|
|
573
731
|
script: parsed,
|
|
574
732
|
});
|
|
@@ -602,7 +760,7 @@ function effectiveBeat(index: number): Beat {
|
|
|
602
760
|
function toggleSource(index: number) {
|
|
603
761
|
if (!sourceOpen[index]) {
|
|
604
762
|
sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);
|
|
605
|
-
|
|
763
|
+
Reflect.deleteProperty(beatSaveErrors, index);
|
|
606
764
|
}
|
|
607
765
|
sourceOpen[index] = !sourceOpen[index];
|
|
608
766
|
}
|
|
@@ -621,14 +779,14 @@ async function updateBeat(index: number) {
|
|
|
621
779
|
}
|
|
622
780
|
const prevImage = JSON.stringify(effectiveBeat(index).image);
|
|
623
781
|
|
|
624
|
-
|
|
782
|
+
Reflect.deleteProperty(beatSaveErrors, index);
|
|
625
783
|
beatSaving[index] = true;
|
|
626
|
-
const response = await apiPost<unknown>(
|
|
784
|
+
const response = await apiPost<unknown>(endpoints.updateBeat.url, {
|
|
627
785
|
filePath: filePath.value,
|
|
628
786
|
beatIndex: index,
|
|
629
787
|
beat,
|
|
630
788
|
});
|
|
631
|
-
|
|
789
|
+
Reflect.deleteProperty(beatSaving, index);
|
|
632
790
|
if (!response.ok) {
|
|
633
791
|
beatSaveErrors[index] = { kind: "saveFailed", error: response.error };
|
|
634
792
|
return;
|
|
@@ -638,14 +796,14 @@ async function updateBeat(index: number) {
|
|
|
638
796
|
sourceOpen[index] = false;
|
|
639
797
|
|
|
640
798
|
if (JSON.stringify(beat.image) !== prevImage) {
|
|
641
|
-
|
|
799
|
+
Reflect.deleteProperty(renderedImages, index);
|
|
642
800
|
renderBeat(index);
|
|
643
801
|
}
|
|
644
802
|
}
|
|
645
803
|
|
|
646
804
|
async function renderBeat(index: number) {
|
|
647
805
|
renderState[index] = "rendering";
|
|
648
|
-
const response = await apiPost<{ image?: string; error?: string }>(
|
|
806
|
+
const response = await apiPost<{ image?: string; error?: string }>(endpoints.renderBeat.url, {
|
|
649
807
|
filePath: filePath.value,
|
|
650
808
|
beatIndex: index,
|
|
651
809
|
chatSessionId: chatSessionId.value,
|
|
@@ -666,9 +824,9 @@ async function renderBeat(index: number) {
|
|
|
666
824
|
}
|
|
667
825
|
|
|
668
826
|
async function regenerateBeat(index: number) {
|
|
669
|
-
|
|
827
|
+
Reflect.deleteProperty(renderedImages, index);
|
|
670
828
|
renderState[index] = "rendering";
|
|
671
|
-
const response = await apiPost<{ image?: string; error?: string }>(
|
|
829
|
+
const response = await apiPost<{ image?: string; error?: string }>(endpoints.renderBeat.url, {
|
|
672
830
|
filePath: filePath.value,
|
|
673
831
|
beatIndex: index,
|
|
674
832
|
force: true,
|
|
@@ -689,7 +847,7 @@ async function regenerateBeat(index: number) {
|
|
|
689
847
|
}
|
|
690
848
|
|
|
691
849
|
async function loadExistingBeatImage(index: number) {
|
|
692
|
-
const response = await apiGet<{ image?: string }>(
|
|
850
|
+
const response = await apiGet<{ image?: string }>(endpoints.beatImage.url, { filePath: filePath.value, beatIndex: String(index) });
|
|
693
851
|
// silently ignore errors — image simply hasn't been generated yet
|
|
694
852
|
if (response.ok && response.data.image) {
|
|
695
853
|
renderedImages[index] = response.data.image;
|
|
@@ -698,7 +856,7 @@ async function loadExistingBeatImage(index: number) {
|
|
|
698
856
|
}
|
|
699
857
|
|
|
700
858
|
async function loadExistingBeatAudio(index: number) {
|
|
701
|
-
const response = await apiGet<{ audio?: string }>(
|
|
859
|
+
const response = await apiGet<{ audio?: string }>(endpoints.beatAudio.url, { filePath: filePath.value, beatIndex: String(index) });
|
|
702
860
|
// silently ignore errors
|
|
703
861
|
if (response.ok && response.data.audio) {
|
|
704
862
|
beatAudios[index] = response.data.audio;
|
|
@@ -708,8 +866,8 @@ async function loadExistingBeatAudio(index: number) {
|
|
|
708
866
|
|
|
709
867
|
async function generateAudio(index: number) {
|
|
710
868
|
audioState[index] = "generating";
|
|
711
|
-
|
|
712
|
-
const response = await apiPost<{ audio?: string; error?: string }>(
|
|
869
|
+
Reflect.deleteProperty(audioErrors, index);
|
|
870
|
+
const response = await apiPost<{ audio?: string; error?: string }>(endpoints.generateBeatAudio.url, {
|
|
713
871
|
filePath: filePath.value,
|
|
714
872
|
beatIndex: index,
|
|
715
873
|
chatSessionId: chatSessionId.value,
|
|
@@ -739,9 +897,15 @@ function playAudio(index: number) {
|
|
|
739
897
|
if (!src) return;
|
|
740
898
|
const audio = new Audio(src);
|
|
741
899
|
playingAudio.value = { index, audio };
|
|
900
|
+
audioProgress.value = 0;
|
|
901
|
+
audio.addEventListener("timeupdate", () => {
|
|
902
|
+
if (playingAudio.value?.index !== index) return;
|
|
903
|
+
if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;
|
|
904
|
+
});
|
|
742
905
|
audio.addEventListener("ended", () => {
|
|
743
906
|
if (playingAudio.value?.index !== index) return;
|
|
744
907
|
playingAudio.value = null;
|
|
908
|
+
audioProgress.value = 0;
|
|
745
909
|
if (lightbox.value?.index === index) {
|
|
746
910
|
lightboxMove(1);
|
|
747
911
|
const nextIndex = lightbox.value?.index;
|
|
@@ -770,7 +934,7 @@ async function onBeatDrop(event: DragEvent, index: number) {
|
|
|
770
934
|
if (!file || !file.type.startsWith("image/")) return;
|
|
771
935
|
|
|
772
936
|
renderState[index] = "rendering";
|
|
773
|
-
|
|
937
|
+
Reflect.deleteProperty(renderErrors, index);
|
|
774
938
|
let imageData: string;
|
|
775
939
|
try {
|
|
776
940
|
imageData = await new Promise<string>((resolve, reject) => {
|
|
@@ -784,7 +948,7 @@ async function onBeatDrop(event: DragEvent, index: number) {
|
|
|
784
948
|
renderState[index] = "error";
|
|
785
949
|
return;
|
|
786
950
|
}
|
|
787
|
-
const response = await apiPost<{ image?: string; error?: string }>(
|
|
951
|
+
const response = await apiPost<{ image?: string; error?: string }>(endpoints.uploadBeatImage.url, {
|
|
788
952
|
filePath: filePath.value,
|
|
789
953
|
beatIndex: index,
|
|
790
954
|
imageData,
|
|
@@ -820,7 +984,7 @@ async function onCharDrop(event: DragEvent, key: string) {
|
|
|
820
984
|
if (!file || !file.type.startsWith("image/")) return;
|
|
821
985
|
|
|
822
986
|
charRenderState[key] = "rendering";
|
|
823
|
-
|
|
987
|
+
Reflect.deleteProperty(charErrors, key);
|
|
824
988
|
let imageData: string;
|
|
825
989
|
try {
|
|
826
990
|
imageData = await new Promise<string>((resolve, reject) => {
|
|
@@ -834,7 +998,7 @@ async function onCharDrop(event: DragEvent, key: string) {
|
|
|
834
998
|
charRenderState[key] = "error";
|
|
835
999
|
return;
|
|
836
1000
|
}
|
|
837
|
-
const response = await apiPost<{ image?: string; error?: string }>(
|
|
1001
|
+
const response = await apiPost<{ image?: string; error?: string }>(endpoints.uploadCharacterImage.url, { filePath: filePath.value, key, imageData });
|
|
838
1002
|
if (!response.ok) {
|
|
839
1003
|
charErrors[key] = response.error || "Upload failed";
|
|
840
1004
|
charRenderState[key] = "error";
|
|
@@ -863,7 +1027,7 @@ function openCharacterLightbox(key: string) {
|
|
|
863
1027
|
}
|
|
864
1028
|
|
|
865
1029
|
async function loadExistingCharacterImage(key: string) {
|
|
866
|
-
const response = await apiGet<{ image?: string }>(
|
|
1030
|
+
const response = await apiGet<{ image?: string }>(endpoints.characterImage.url, { filePath: filePath.value, key });
|
|
867
1031
|
// silently ignore errors
|
|
868
1032
|
if (response.ok && response.data.image) {
|
|
869
1033
|
charImages[key] = response.data.image;
|
|
@@ -877,8 +1041,8 @@ function refreshMissingCharacterImages() {
|
|
|
877
1041
|
|
|
878
1042
|
async function renderCharacter(key: string, force: boolean) {
|
|
879
1043
|
charRenderState[key] = "rendering";
|
|
880
|
-
|
|
881
|
-
const response = await apiPost<{ image?: string; error?: string }>(
|
|
1044
|
+
Reflect.deleteProperty(charErrors, key);
|
|
1045
|
+
const response = await apiPost<{ image?: string; error?: string }>(endpoints.renderCharacter.url, {
|
|
882
1046
|
filePath: filePath.value,
|
|
883
1047
|
key,
|
|
884
1048
|
force,
|
|
@@ -902,43 +1066,65 @@ async function generateAllCharacters() {
|
|
|
902
1066
|
await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== "rendering").map((key) => renderCharacter(key, false)));
|
|
903
1067
|
}
|
|
904
1068
|
|
|
1069
|
+
// Probe the server for an existing beat PNG before triggering any
|
|
1070
|
+
// generation. Only auto-renders when the disk is empty AND the beat
|
|
1071
|
+
// is a deterministic type — imagePrompt beats are left empty so the
|
|
1072
|
+
// user clicks Generate explicitly (avoids surprise paid text2image
|
|
1073
|
+
// calls on every page refresh).
|
|
1074
|
+
async function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {
|
|
1075
|
+
await loadExistingBeatImage(index);
|
|
1076
|
+
if (renderedImages[index]) return;
|
|
1077
|
+
if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {
|
|
1078
|
+
await renderBeat(index);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
905
1082
|
async function initializeScript() {
|
|
906
1083
|
// Reset scroll position so new results start at the top
|
|
907
1084
|
if (beatListEl.value) beatListEl.value.scrollTop = 0;
|
|
908
1085
|
// Reset per-script state
|
|
909
|
-
Object.keys(renderState).forEach((key) =>
|
|
910
|
-
Object.keys(renderedImages).forEach((key) =>
|
|
911
|
-
Object.keys(renderErrors).forEach((key) =>
|
|
912
|
-
Object.keys(sourceOpen).forEach((key) =>
|
|
913
|
-
Object.keys(sourceText).forEach((key) =>
|
|
914
|
-
Object.keys(beatSaveErrors).forEach((key) =>
|
|
915
|
-
Object.keys(beatSaving).forEach((key) =>
|
|
916
|
-
Object.keys(localOverrides).forEach((key) =>
|
|
917
|
-
Object.keys(beatAudios).forEach((key) =>
|
|
918
|
-
Object.keys(audioState).forEach((key) =>
|
|
919
|
-
Object.keys(audioErrors).forEach((key) =>
|
|
920
|
-
Object.keys(charRenderState).forEach((key) =>
|
|
921
|
-
Object.keys(charImages).forEach((key) =>
|
|
922
|
-
Object.keys(charErrors).forEach((key) =>
|
|
923
|
-
Object.keys(beatDragOver).forEach((key) =>
|
|
1086
|
+
Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));
|
|
1087
|
+
Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));
|
|
1088
|
+
Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));
|
|
1089
|
+
Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));
|
|
1090
|
+
Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));
|
|
1091
|
+
Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));
|
|
1092
|
+
Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));
|
|
1093
|
+
Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));
|
|
1094
|
+
Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));
|
|
1095
|
+
Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));
|
|
1096
|
+
Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));
|
|
1097
|
+
Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));
|
|
1098
|
+
Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));
|
|
1099
|
+
Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));
|
|
1100
|
+
Object.keys(beatDragOver).forEach((key) => Reflect.deleteProperty(beatDragOver, key));
|
|
924
1101
|
moviePath.value = null;
|
|
925
1102
|
if (sourceDetails.value) sourceDetails.value.open = false;
|
|
926
1103
|
|
|
1104
|
+
// Mount-time policy: prefer the existing PNG on the server. Every
|
|
1105
|
+
// beat — deterministic AND imagePrompt — first probes /beat-image,
|
|
1106
|
+
// and we only fall through to renderBeat() when the disk has nothing
|
|
1107
|
+
// yet AND the type is safe to auto-render (deterministic content,
|
|
1108
|
+
// no characters waiting). Without this probe a refresh would re-fire
|
|
1109
|
+
// generateBeatImage for every beat, and for imagePrompt beats that
|
|
1110
|
+
// means a paid text2image call against an image we already have.
|
|
1111
|
+
//
|
|
1112
|
+
// Stale-after-edit: if the user edits the script source the on-disk
|
|
1113
|
+
// PNG is no longer in sync with the new content, but we don't try to
|
|
1114
|
+
// detect that here — the per-beat ↺ button is one click away and a
|
|
1115
|
+
// page refresh re-runs this same probe, so the user can opt back into
|
|
1116
|
+
// a fresh render whenever they need to.
|
|
927
1117
|
const AUTO_RENDER_TYPES = ["textSlide", "markdown", "chart", "mermaid", "html_tailwind"] as const;
|
|
928
1118
|
const hasCharacters = characterKeys.value.length > 0;
|
|
929
1119
|
beats.value.forEach((beat, index) => {
|
|
930
|
-
|
|
931
|
-
renderBeat(index);
|
|
932
|
-
} else if (beat.imagePrompt) {
|
|
933
|
-
loadExistingBeatImage(index);
|
|
934
|
-
}
|
|
1120
|
+
void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);
|
|
935
1121
|
if (beat.text) loadExistingBeatAudio(index);
|
|
936
1122
|
});
|
|
937
1123
|
|
|
938
1124
|
characterKeys.value.forEach((key) => loadExistingCharacterImage(key));
|
|
939
1125
|
|
|
940
1126
|
if (filePath.value) {
|
|
941
|
-
const response = await apiGet<{ moviePath?: string }>(
|
|
1127
|
+
const response = await apiGet<{ moviePath?: string }>(endpoints.movieStatus.url, { filePath: filePath.value });
|
|
942
1128
|
if (response.ok && response.data.moviePath) {
|
|
943
1129
|
moviePath.value = response.data.moviePath;
|
|
944
1130
|
}
|
|
@@ -994,15 +1180,15 @@ async function reflectGenerationFinish(entry: PendingGeneration): Promise<void>
|
|
|
994
1180
|
if (entry.kind === GENERATION_KINDS.beatImage) {
|
|
995
1181
|
const idx = Number(entry.key);
|
|
996
1182
|
await loadExistingBeatImage(idx);
|
|
997
|
-
if (renderState[idx] === "rendering")
|
|
1183
|
+
if (renderState[idx] === "rendering") Reflect.deleteProperty(renderState, idx);
|
|
998
1184
|
} else if (entry.kind === GENERATION_KINDS.beatAudio) {
|
|
999
1185
|
const idx = Number(entry.key);
|
|
1000
1186
|
await loadExistingBeatAudio(idx);
|
|
1001
|
-
if (audioState[idx] === "generating")
|
|
1187
|
+
if (audioState[idx] === "generating") Reflect.deleteProperty(audioState, idx);
|
|
1002
1188
|
} else if (entry.kind === GENERATION_KINDS.characterImage) {
|
|
1003
1189
|
await loadExistingCharacterImage(entry.key);
|
|
1004
1190
|
if (charRenderState[entry.key] === "rendering") {
|
|
1005
|
-
|
|
1191
|
+
Reflect.deleteProperty(charRenderState, entry.key);
|
|
1006
1192
|
}
|
|
1007
1193
|
} else if (entry.kind === GENERATION_KINDS.movie) {
|
|
1008
1194
|
movieGenerating.value = false;
|
|
@@ -1012,7 +1198,7 @@ async function reflectGenerationFinish(entry: PendingGeneration): Promise<void>
|
|
|
1012
1198
|
|
|
1013
1199
|
async function refreshMoviePath(): Promise<void> {
|
|
1014
1200
|
if (!filePath.value) return;
|
|
1015
|
-
const response = await apiGet<{ moviePath?: string }>(
|
|
1201
|
+
const response = await apiGet<{ moviePath?: string }>(endpoints.movieStatus.url, { filePath: filePath.value });
|
|
1016
1202
|
if (response.ok && response.data.moviePath) {
|
|
1017
1203
|
moviePath.value = response.data.moviePath;
|
|
1018
1204
|
}
|
|
@@ -1021,7 +1207,7 @@ async function refreshMoviePath(): Promise<void> {
|
|
|
1021
1207
|
async function generateMovie() {
|
|
1022
1208
|
movieGenerating.value = true;
|
|
1023
1209
|
try {
|
|
1024
|
-
const res = await apiFetchRaw(
|
|
1210
|
+
const res = await apiFetchRaw(endpoints.generateMovie.url, {
|
|
1025
1211
|
method: "POST",
|
|
1026
1212
|
body: JSON.stringify({
|
|
1027
1213
|
filePath: filePath.value,
|
|
@@ -1046,6 +1232,40 @@ async function generateMovie() {
|
|
|
1046
1232
|
movieGenerating.value = false;
|
|
1047
1233
|
}
|
|
1048
1234
|
}
|
|
1235
|
+
|
|
1236
|
+
// Bearer-authenticated movie download. apiFetchRaw auto-attaches the
|
|
1237
|
+
// Authorization header (which a plain `<a href download>` cannot), so
|
|
1238
|
+
// the route stays behind the standard /api/* bearer guard. The blob
|
|
1239
|
+
// is hooked to a synthetic anchor whose `download` attribute carries
|
|
1240
|
+
// the filename — the browser still surfaces a native save dialog.
|
|
1241
|
+
async function downloadMovie() {
|
|
1242
|
+
if (!moviePath.value || movieDownloading.value) return;
|
|
1243
|
+
movieDownloading.value = true;
|
|
1244
|
+
let objectUrl: string | null = null;
|
|
1245
|
+
try {
|
|
1246
|
+
const res = await apiFetchRaw(endpoints.downloadMovie.url, {
|
|
1247
|
+
method: "GET",
|
|
1248
|
+
query: { moviePath: moviePath.value },
|
|
1249
|
+
});
|
|
1250
|
+
if (!res.ok) {
|
|
1251
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1252
|
+
}
|
|
1253
|
+
const blob = await res.blob();
|
|
1254
|
+
objectUrl = URL.createObjectURL(blob);
|
|
1255
|
+
const filename = moviePath.value.split("/").pop() ?? "movie.mp4";
|
|
1256
|
+
const anchor = document.createElement("a");
|
|
1257
|
+
anchor.href = objectUrl;
|
|
1258
|
+
anchor.download = filename;
|
|
1259
|
+
document.body.appendChild(anchor);
|
|
1260
|
+
anchor.click();
|
|
1261
|
+
anchor.remove();
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
alert(extractErrorMessage(err));
|
|
1264
|
+
} finally {
|
|
1265
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1266
|
+
movieDownloading.value = false;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1049
1269
|
</script>
|
|
1050
1270
|
|
|
1051
1271
|
<style scoped>
|