@agent-native/core 0.149.6 → 0.151.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 +1 -1
- package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
- package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +32 -176
- package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
- package/corpus/templates/clips/desktop/src/styles.css +1 -166
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
- package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
- package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
- package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
- package/corpus/templates/clips/server/plugins/auth.ts +3 -0
- package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
- package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
- package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/dispatch/app/global.css +2 -2
- package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
- package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
- package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
- package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
- package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
- package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
- package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
- package/corpus/templates/forms/shared/page-url.ts +54 -0
- package/corpus/templates/forms/shared/types.ts +4 -4
- package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/slides/actions/add-slide.ts +0 -27
- package/corpus/templates/slides/actions/import-file.ts +28 -9
- package/corpus/templates/slides/actions/view-screen.ts +4 -4
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
- package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
- package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
- package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
- package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
- package/corpus/templates/slides/app/global.css +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
- package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
- package/corpus/templates/slides/app/pages/Index.tsx +28 -3
- package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
- package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
- package/corpus/templates/slides/shared/deck-title.ts +56 -0
- package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/agent/run-store.js +6 -1
- package/dist/agent/thread-data-builder.js +19 -44
- package/dist/agent/types.d.ts +13 -0
- package/dist/catalog.json +1 -0
- package/dist/cli/create.js +1 -1
- package/dist/client/AssistantChat.js +58 -10
- package/dist/client/FileStorageSetupCard.d.ts +6 -0
- package/dist/client/FileStorageSetupCard.js +18 -0
- package/dist/client/NewWorkspaceAppFlow.js +10 -0
- package/dist/client/agent-chat-adapter.js +87 -11
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/agent-chat.js +8 -3
- package/dist/client/chat/attachment-adapters.js +10 -0
- package/dist/client/chat/message-components.d.ts +4 -1
- package/dist/client/chat/message-components.js +37 -13
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +6 -6
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/chat-first/types.d.ts +1 -0
- package/dist/client/extensions/EmbeddedExtension.js +1 -1
- package/dist/client/extensions/ExtensionViewer.js +1 -1
- package/dist/client/extensions/InlineExtensionFrame.js +1 -1
- package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
- package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
- package/dist/client/settings/FileStorageSettingsForm.js +113 -0
- package/dist/client/settings/SettingsPanel.js +2 -1
- package/dist/client/sse-event-processor.js +21 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.js +2 -2
- package/dist/extensions/html-shell.js +3 -1
- package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
- package/dist/file-upload/pre-upload-attachments.js +68 -24
- package/dist/file-upload/registry.d.ts +4 -5
- package/dist/file-upload/registry.js +7 -14
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +168 -0
- package/dist/file-upload/types.d.ts +3 -3
- package/dist/file-upload/types.js +2 -2
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.js +23 -1
- package/dist/onboarding/default-steps.js +69 -0
- package/dist/onboarding/types.d.ts +2 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat/browser-team-tools.js +20 -0
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +45 -21
- package/dist/server/better-auth-instance.d.ts +2 -0
- package/dist/server/better-auth-instance.js +2 -2
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.js +26 -16
- package/dist/server/identity-sso-store.d.ts +3 -0
- package/dist/server/identity-sso-store.js +45 -0
- package/dist/server/identity-sso.d.ts +20 -10
- package/dist/server/identity-sso.js +79 -31
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
- package/docs/content/authentication.mdx +27 -0
- package/docs/content/cross-app-sso.mdx +38 -8
- package/docs/content/extensions.mdx +15 -15
- package/docs/content/file-uploads.mdx +10 -12
- package/docs/content/generative-ui.mdx +6 -1
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
|
@@ -4,6 +4,14 @@ interface ReplaceOptions {
|
|
|
4
4
|
alt?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface SlideImageDropPosition {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DROPPED_IMAGE_WIDTH = 320;
|
|
13
|
+
const DROPPED_IMAGE_HEIGHT = 180;
|
|
14
|
+
|
|
7
15
|
interface PlaceholderTarget {
|
|
8
16
|
index: number | null;
|
|
9
17
|
label: string;
|
|
@@ -53,6 +61,13 @@ function cleanAlt(value: string | undefined): string {
|
|
|
53
61
|
return (value || "Uploaded image").replace(/\s+/g, " ").trim();
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
function createSlideObjectId(): string {
|
|
65
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
66
|
+
return crypto.randomUUID();
|
|
67
|
+
}
|
|
68
|
+
return `slide-object-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
function hasStyleProperty(style: string, property: string): boolean {
|
|
57
72
|
return new RegExp(`(?:^|;)\\s*${property}\\s*:`, "i").test(style);
|
|
58
73
|
}
|
|
@@ -188,6 +203,52 @@ export function insertImageIntoSlideHtml(
|
|
|
188
203
|
return serializeFragment(doc);
|
|
189
204
|
}
|
|
190
205
|
|
|
206
|
+
/** Insert a desktop drop as a durable, independently movable canvas object. */
|
|
207
|
+
export function insertDroppedImageIntoSlideHtml(
|
|
208
|
+
content: string,
|
|
209
|
+
newSrc: string,
|
|
210
|
+
options: ReplaceOptions & { position?: SlideImageDropPosition } = {},
|
|
211
|
+
): string {
|
|
212
|
+
const doc = parseFragment(content);
|
|
213
|
+
const img = doc.createElement("img");
|
|
214
|
+
const position = options.position ?? {
|
|
215
|
+
x: 640,
|
|
216
|
+
y: 360,
|
|
217
|
+
};
|
|
218
|
+
const left = Math.max(0, Math.round(position.x - DROPPED_IMAGE_WIDTH / 2));
|
|
219
|
+
const top = Math.max(0, Math.round(position.y - DROPPED_IMAGE_HEIGHT / 2));
|
|
220
|
+
|
|
221
|
+
img.setAttribute("src", newSrc);
|
|
222
|
+
img.setAttribute("alt", cleanAlt(options.alt));
|
|
223
|
+
img.setAttribute("data-slide-object-id", createSlideObjectId());
|
|
224
|
+
img.className = "fmd-img-uploaded";
|
|
225
|
+
img.setAttribute(
|
|
226
|
+
"style",
|
|
227
|
+
`position: absolute; left: ${left}px; top: ${top}px; width: ${DROPPED_IMAGE_WIDTH}px; height: ${DROPPED_IMAGE_HEIGHT}px; max-width: none; max-height: none; margin: 0; border-radius: 8px; object-fit: contain; box-sizing: border-box; z-index: 1;`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const slideRoot = doc.body.querySelector<HTMLElement>(".fmd-slide");
|
|
231
|
+
if (slideRoot) {
|
|
232
|
+
if (!hasStyleProperty(slideRoot.getAttribute("style") ?? "", "position")) {
|
|
233
|
+
slideRoot.setAttribute(
|
|
234
|
+
"style",
|
|
235
|
+
`${(slideRoot.getAttribute("style") ?? "").trim().replace(/;+\s*$/, "")}; position: relative;`.replace(
|
|
236
|
+
/^;\s*/,
|
|
237
|
+
"",
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
slideRoot.appendChild(img);
|
|
242
|
+
} else {
|
|
243
|
+
// Markdown-backed slides keep their source text. Appending a raw image tag
|
|
244
|
+
// lets ReactMarkdown preserve the text while the slide canvas supplies the
|
|
245
|
+
// positioned containing block at render time.
|
|
246
|
+
doc.body.append(doc.createTextNode("\n\n"), img);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return serializeFragment(doc);
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
export function replaceImageTargetInSlideHtml(
|
|
192
253
|
content: string,
|
|
193
254
|
oldSrc: string,
|
|
@@ -36,10 +36,7 @@ import EditorSidebar from "@/components/editor/EditorSidebar";
|
|
|
36
36
|
import EditorToolbar from "@/components/editor/EditorToolbar";
|
|
37
37
|
import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
|
|
38
38
|
import HistoryPanel from "@/components/editor/HistoryPanel";
|
|
39
|
-
import ImageDropPromptPopover from "@/components/editor/ImageDropPromptPopover";
|
|
40
39
|
import ImageGenPanel from "@/components/editor/ImageGenPanel";
|
|
41
|
-
import ImageSearchPanel from "@/components/editor/ImageSearchPanel";
|
|
42
|
-
import LogoSearchPanel from "@/components/editor/LogoSearchPanel";
|
|
43
40
|
import { QuestionFlow } from "@/components/editor/QuestionFlow";
|
|
44
41
|
import SlideEditor from "@/components/editor/SlideEditor";
|
|
45
42
|
import { TweaksPanel } from "@/components/editor/TweaksPanel";
|
|
@@ -49,7 +46,6 @@ import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
|
49
46
|
import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
|
|
50
47
|
import { useDeckPresence } from "@/hooks/use-deck-presence";
|
|
51
48
|
import { useDeckRole } from "@/hooks/use-deck-role";
|
|
52
|
-
import { useGeneratingSlidePreview } from "@/hooks/use-generating-slide-preview";
|
|
53
49
|
import {
|
|
54
50
|
useSlideComments,
|
|
55
51
|
type CommentThread,
|
|
@@ -71,7 +67,7 @@ import {
|
|
|
71
67
|
import { isMissingUploadProviderError } from "@/lib/image-drop-to-agent";
|
|
72
68
|
import { imageFileLooksSupported } from "@/lib/slide-image-replacement";
|
|
73
69
|
import {
|
|
74
|
-
|
|
70
|
+
insertDroppedImageIntoSlideHtml,
|
|
75
71
|
replaceImageTargetInSlideHtml,
|
|
76
72
|
} from "@/lib/slide-image-replacement";
|
|
77
73
|
import { TAB_ID } from "@/lib/tab-id";
|
|
@@ -203,8 +199,6 @@ export default function DeckEditor() {
|
|
|
203
199
|
// Dialog/popover states
|
|
204
200
|
const [imageGenOpen, setImageGenOpen] = useState(false);
|
|
205
201
|
const [assetLibraryOpen, setAssetLibraryOpen] = useState(false);
|
|
206
|
-
const [imageSearchOpen, setImageSearchOpen] = useState(false);
|
|
207
|
-
const [logoSearchOpen, setLogoSearchOpen] = useState(false);
|
|
208
202
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
209
203
|
const historyButtonRef = useRef<HTMLButtonElement>(null);
|
|
210
204
|
const [sidePanel, setSidePanel] = useState<EditorSidePanel>(null);
|
|
@@ -246,19 +240,6 @@ export default function DeckEditor() {
|
|
|
246
240
|
// Hidden file input for direct upload
|
|
247
241
|
const uploadInputRef = useRef<HTMLInputElement>(null);
|
|
248
242
|
|
|
249
|
-
// Drop-to-prompt popover state. Opens when a user drops an image somewhere
|
|
250
|
-
// other than an existing image/placeholder on the slide — so we ask "what
|
|
251
|
-
// should we do with this?" and hand the image off to the agent chat instead
|
|
252
|
-
// of guessing (or worse, letting the browser navigate to the file).
|
|
253
|
-
const [imageDropPopover, setImageDropPopover] = useState<{
|
|
254
|
-
open: boolean;
|
|
255
|
-
file: File | null;
|
|
256
|
-
position: { x: number; y: number } | null;
|
|
257
|
-
}>({ open: false, file: null, position: null });
|
|
258
|
-
const closeImageDropPopover = useCallback(() => {
|
|
259
|
-
setImageDropPopover({ open: false, file: null, position: null });
|
|
260
|
-
}, []);
|
|
261
|
-
|
|
262
243
|
const deck = getDeck(id || "");
|
|
263
244
|
const fitDims = getAspectRatioDims(deck?.aspectRatio);
|
|
264
245
|
const currentDeckAccessKey = deckAccessCheckKey(id, org?.orgId);
|
|
@@ -267,11 +248,6 @@ export default function DeckEditor() {
|
|
|
267
248
|
((org?.pendingInvitations?.length ?? 0) > 0 ||
|
|
268
249
|
(org?.domainMatches?.length ?? 0) > 0);
|
|
269
250
|
const slideCount = deck?.slides.length ?? 0;
|
|
270
|
-
const streamedGeneratingSlideContent = useGeneratingSlidePreview({
|
|
271
|
-
deckId: id ?? "",
|
|
272
|
-
slideCount,
|
|
273
|
-
generating,
|
|
274
|
-
});
|
|
275
251
|
// Mirror Google Slides: viewers see the editor shell with edit affordances
|
|
276
252
|
// disabled (rather than a separate "viewer" route). Owners/Editors/Admins
|
|
277
253
|
// get the full editor. Only assume edit access while the role is still
|
|
@@ -365,26 +341,17 @@ export default function DeckEditor() {
|
|
|
365
341
|
);
|
|
366
342
|
const slideWasAdded = currentSlideIds.length > previousSlideIds.length;
|
|
367
343
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
) {
|
|
344
|
+
previousSlideIdsRef.current = currentSlideIds;
|
|
345
|
+
if (!slideWasAdded) return;
|
|
346
|
+
|
|
347
|
+
// Keep the user's current slide stable while AI appends slides. The only
|
|
348
|
+
// exception is an explicit click on the synthetic generating-slide row,
|
|
349
|
+
// which opts the user into following that one generated slide.
|
|
350
|
+
if (addedSlide && generatingSlideSelected) {
|
|
376
351
|
setActiveSlideId(addedSlide.id);
|
|
377
352
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (slideWasAdded) setGeneratingSlideSelected(false);
|
|
381
|
-
}, [
|
|
382
|
-
addSlideGenerating,
|
|
383
|
-
deck,
|
|
384
|
-
generating,
|
|
385
|
-
generatingSlideSelected,
|
|
386
|
-
isNewDeckGenerating,
|
|
387
|
-
]);
|
|
353
|
+
setGeneratingSlideSelected(false);
|
|
354
|
+
}, [deck, generatingSlideSelected]);
|
|
388
355
|
|
|
389
356
|
useEffect(() => {
|
|
390
357
|
if (
|
|
@@ -541,22 +508,43 @@ export default function DeckEditor() {
|
|
|
541
508
|
position?: { x: number; y: number },
|
|
542
509
|
) => {
|
|
543
510
|
if (!id || !currentSlideRef.current) return;
|
|
544
|
-
|
|
545
|
-
// canvas, or the editor chrome), defer to the user: open the popover so
|
|
546
|
-
// they can tell the agent what to do with the image. The agent can then
|
|
547
|
-
// decide which slide / placeholder / element to update, generate a
|
|
548
|
-
// matching layout, or add a new slide.
|
|
511
|
+
const targetSlideId = currentSlideRef.current.id;
|
|
549
512
|
if (!replaceSrc) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
513
|
+
try {
|
|
514
|
+
const newUrl = await uploadImageAsset(file);
|
|
515
|
+
const targetSlide =
|
|
516
|
+
currentSlideRef.current?.id === targetSlideId
|
|
517
|
+
? currentSlideRef.current
|
|
518
|
+
: getDeck(id)?.slides.find((slide) => slide.id === targetSlideId);
|
|
519
|
+
if (!targetSlide) return;
|
|
520
|
+
const updatedContent = insertDroppedImageIntoSlideHtml(
|
|
521
|
+
targetSlide.content,
|
|
522
|
+
newUrl,
|
|
523
|
+
{ alt: file.name, position },
|
|
524
|
+
);
|
|
525
|
+
if (updatedContent !== targetSlide.content) {
|
|
526
|
+
updateSlide(id, targetSlide.id, { content: updatedContent });
|
|
527
|
+
}
|
|
528
|
+
toast.success(t("deckEditor.imageAdded"), {
|
|
529
|
+
description: file.name,
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
toast.error(t("deckEditor.imageUploadFailed"), {
|
|
533
|
+
description:
|
|
534
|
+
error instanceof Error
|
|
535
|
+
? error.message
|
|
536
|
+
: t("deckEditor.imageUploadError"),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
555
539
|
return;
|
|
556
540
|
}
|
|
557
|
-
const targetSlide = currentSlideRef.current;
|
|
558
541
|
try {
|
|
559
542
|
const newUrl = await uploadImageAsset(file);
|
|
543
|
+
const targetSlide =
|
|
544
|
+
currentSlideRef.current?.id === targetSlideId
|
|
545
|
+
? currentSlideRef.current
|
|
546
|
+
: getDeck(id)?.slides.find((slide) => slide.id === targetSlideId);
|
|
547
|
+
if (!targetSlide) return;
|
|
560
548
|
const updatedContent = replaceImageTargetInSlideHtml(
|
|
561
549
|
targetSlide.content,
|
|
562
550
|
replaceSrc,
|
|
@@ -578,7 +566,7 @@ export default function DeckEditor() {
|
|
|
578
566
|
});
|
|
579
567
|
}
|
|
580
568
|
},
|
|
581
|
-
[id, updateSlide, uploadImageAsset],
|
|
569
|
+
[getDeck, id, t, updateSlide, uploadImageAsset],
|
|
582
570
|
);
|
|
583
571
|
|
|
584
572
|
// Drag an already-hosted image (e.g. dragged out of a generated-image
|
|
@@ -586,17 +574,18 @@ export default function DeckEditor() {
|
|
|
586
574
|
// uploadAndApplyImage there's nothing to upload — the URL is already a
|
|
587
575
|
// live asset — so this just swaps it into the target image/placeholder.
|
|
588
576
|
const dropImageUrlOnSlide = useCallback(
|
|
589
|
-
(
|
|
577
|
+
(
|
|
578
|
+
replaceSrc: string | null,
|
|
579
|
+
url: string,
|
|
580
|
+
position?: { x: number; y: number },
|
|
581
|
+
) => {
|
|
590
582
|
if (!id || !currentSlideRef.current) return;
|
|
591
583
|
if (!replaceSrc) {
|
|
592
|
-
// No existing image/placeholder under the drop point (e.g. an empty
|
|
593
|
-
// slide) — the URL is already a known, hosted asset (unlike an
|
|
594
|
-
// arbitrary local file), so just add it to the slide directly
|
|
595
|
-
// instead of routing through the agent-prompt popover.
|
|
596
584
|
const targetSlide = currentSlideRef.current;
|
|
597
|
-
const updatedContent =
|
|
585
|
+
const updatedContent = insertDroppedImageIntoSlideHtml(
|
|
598
586
|
targetSlide.content,
|
|
599
587
|
url,
|
|
588
|
+
{ position },
|
|
600
589
|
);
|
|
601
590
|
if (updatedContent !== targetSlide.content) {
|
|
602
591
|
updateSlide(id, targetSlide.id, { content: updatedContent });
|
|
@@ -981,10 +970,9 @@ export default function DeckEditor() {
|
|
|
981
970
|
|
|
982
971
|
// Editor-wide drag-and-drop catch-all. SlideEditor's own drop handler runs
|
|
983
972
|
// first for drops landing on a slide (it calls stopPropagation), so this
|
|
984
|
-
// only fires for drops that landed in the surrounding chrome
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
// surprises users who expect drop-to-attach behavior everywhere.
|
|
973
|
+
// only fires for drops that landed in the surrounding chrome. Prevent the
|
|
974
|
+
// browser from navigating to the dropped file, and add it to the active
|
|
975
|
+
// slide at the default canvas position.
|
|
988
976
|
const editorDragOver = (e: React.DragEvent) => {
|
|
989
977
|
if (!Array.from(e.dataTransfer?.types ?? []).includes("Files")) return;
|
|
990
978
|
e.preventDefault();
|
|
@@ -996,15 +984,8 @@ export default function DeckEditor() {
|
|
|
996
984
|
if (!file) return;
|
|
997
985
|
e.preventDefault();
|
|
998
986
|
e.stopPropagation();
|
|
999
|
-
|
|
1000
|
-
open: true,
|
|
1001
|
-
file,
|
|
1002
|
-
position: { x: e.clientX, y: e.clientY },
|
|
1003
|
-
});
|
|
987
|
+
void uploadAndApplyImage(null, file);
|
|
1004
988
|
};
|
|
1005
|
-
const contextHintForDrop = currentSlide
|
|
1006
|
-
? `Current slide: ${currentSlide.id} (index ${currentIndex >= 0 ? currentIndex : 0}). Deck: ${id}.`
|
|
1007
|
-
: `Deck: ${id}.`;
|
|
1008
989
|
|
|
1009
990
|
const handleAddEmptySlide = () => {
|
|
1010
991
|
const activeIdx = deck.slides.findIndex((s) => s.id === activeSlideId);
|
|
@@ -1034,7 +1015,7 @@ export default function DeckEditor() {
|
|
|
1034
1015
|
setReplaceImageSrc(null);
|
|
1035
1016
|
setAssetLibraryOpen(true);
|
|
1036
1017
|
}}
|
|
1037
|
-
onShowHistory={() => setHistoryOpen(!
|
|
1018
|
+
onShowHistory={() => setHistoryOpen((open) => !open)}
|
|
1038
1019
|
historyButtonRef={historyButtonRef}
|
|
1039
1020
|
currentSlide={currentSlide}
|
|
1040
1021
|
onWideContextToolbarSlotChange={setWideContextToolbarSlot}
|
|
@@ -1159,7 +1140,6 @@ export default function DeckEditor() {
|
|
|
1159
1140
|
generatingSlideVisible
|
|
1160
1141
|
? {
|
|
1161
1142
|
index: deck.slides.length,
|
|
1162
|
-
content: streamedGeneratingSlideContent,
|
|
1163
1143
|
}
|
|
1164
1144
|
: undefined
|
|
1165
1145
|
}
|
|
@@ -1191,7 +1171,6 @@ export default function DeckEditor() {
|
|
|
1191
1171
|
<div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
|
|
1192
1172
|
<div className="m-auto w-full max-w-6xl">
|
|
1193
1173
|
<GeneratingSlidePreview
|
|
1194
|
-
content={streamedGeneratingSlideContent}
|
|
1195
1174
|
aspectRatio={deck.aspectRatio}
|
|
1196
1175
|
designSystem={designSystem}
|
|
1197
1176
|
thumbnail={false}
|
|
@@ -1207,7 +1186,6 @@ export default function DeckEditor() {
|
|
|
1207
1186
|
<div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
|
|
1208
1187
|
<div className="m-auto w-full max-w-6xl">
|
|
1209
1188
|
<GeneratingSlidePreview
|
|
1210
|
-
content={streamedGeneratingSlideContent}
|
|
1211
1189
|
aspectRatio={deck.aspectRatio}
|
|
1212
1190
|
designSystem={designSystem}
|
|
1213
1191
|
thumbnail={false}
|
|
@@ -1260,14 +1238,6 @@ export default function DeckEditor() {
|
|
|
1260
1238
|
setReplaceImageSrc(src);
|
|
1261
1239
|
uploadInputRef.current?.click();
|
|
1262
1240
|
}}
|
|
1263
|
-
onSearchImage={(src) => {
|
|
1264
|
-
setReplaceImageSrc(src);
|
|
1265
|
-
setImageSearchOpen(true);
|
|
1266
|
-
}}
|
|
1267
|
-
onLogoSearch={(src) => {
|
|
1268
|
-
setReplaceImageSrc(src);
|
|
1269
|
-
setLogoSearchOpen(true);
|
|
1270
|
-
}}
|
|
1271
1241
|
onDropImage={uploadAndApplyImage}
|
|
1272
1242
|
onDropImageUrl={dropImageUrlOnSlide}
|
|
1273
1243
|
onToggleObjectFit={toggleObjectFit}
|
|
@@ -1388,30 +1358,6 @@ export default function DeckEditor() {
|
|
|
1388
1358
|
: undefined
|
|
1389
1359
|
}
|
|
1390
1360
|
/>
|
|
1391
|
-
<ImageSearchPanel
|
|
1392
|
-
open={imageSearchOpen}
|
|
1393
|
-
onOpenChange={setImageSearchOpen}
|
|
1394
|
-
onSelectImage={
|
|
1395
|
-
replaceImageSrc
|
|
1396
|
-
? (newUrl) => {
|
|
1397
|
-
replaceImageInSlide(replaceImageSrc, newUrl);
|
|
1398
|
-
setReplaceImageSrc(null);
|
|
1399
|
-
}
|
|
1400
|
-
: undefined
|
|
1401
|
-
}
|
|
1402
|
-
/>
|
|
1403
|
-
<LogoSearchPanel
|
|
1404
|
-
open={logoSearchOpen}
|
|
1405
|
-
onOpenChange={setLogoSearchOpen}
|
|
1406
|
-
onSelectLogo={
|
|
1407
|
-
replaceImageSrc
|
|
1408
|
-
? (newUrl) => {
|
|
1409
|
-
replaceImageInSlide(replaceImageSrc, newUrl);
|
|
1410
|
-
setReplaceImageSrc(null);
|
|
1411
|
-
}
|
|
1412
|
-
: undefined
|
|
1413
|
-
}
|
|
1414
|
-
/>
|
|
1415
1361
|
<HistoryPanel
|
|
1416
1362
|
deckId={id}
|
|
1417
1363
|
open={historyOpen}
|
|
@@ -1419,13 +1365,6 @@ export default function DeckEditor() {
|
|
|
1419
1365
|
canRestore={canEdit}
|
|
1420
1366
|
anchorRef={historyButtonRef}
|
|
1421
1367
|
/>
|
|
1422
|
-
<ImageDropPromptPopover
|
|
1423
|
-
open={imageDropPopover.open}
|
|
1424
|
-
file={imageDropPopover.file}
|
|
1425
|
-
position={imageDropPopover.position}
|
|
1426
|
-
contextHint={contextHintForDrop}
|
|
1427
|
-
onClose={closeImageDropPopover}
|
|
1428
|
-
/>
|
|
1429
1368
|
</div>
|
|
1430
1369
|
);
|
|
1431
1370
|
}
|
|
@@ -26,6 +26,7 @@ import { useLocation, useNavigate, useSearchParams } from "react-router";
|
|
|
26
26
|
import { toast } from "sonner";
|
|
27
27
|
|
|
28
28
|
import DeckCard from "@/components/deck/DeckCard";
|
|
29
|
+
import { DeckEditorSkeleton } from "@/components/editor/DeckEditorSkeleton";
|
|
29
30
|
import {
|
|
30
31
|
NewDeckReferenceStep,
|
|
31
32
|
type ImportedReference,
|
|
@@ -271,6 +272,7 @@ export default function Index() {
|
|
|
271
272
|
} | null>(null);
|
|
272
273
|
const [showNewDeckReferenceStep, setShowNewDeckReferenceStep] =
|
|
273
274
|
useState(false);
|
|
275
|
+
const [isStartingNewDeck, setIsStartingNewDeck] = useState(false);
|
|
274
276
|
const [recentReferences, setRecentReferences] = useState<RecentReference[]>(
|
|
275
277
|
[],
|
|
276
278
|
);
|
|
@@ -544,11 +546,15 @@ export default function Index() {
|
|
|
544
546
|
: undefined;
|
|
545
547
|
let deck: ReturnType<typeof createDeck> | undefined;
|
|
546
548
|
flushSync(() => {
|
|
549
|
+
setIsStartingNewDeck(true);
|
|
547
550
|
deck = createDeck(undefined, {
|
|
548
551
|
designSystemId: selectedDesignSystem?.id ?? null,
|
|
549
552
|
});
|
|
550
553
|
});
|
|
551
|
-
if (!deck)
|
|
554
|
+
if (!deck) {
|
|
555
|
+
setIsStartingNewDeck(false);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
552
558
|
navigate(`/deck/${deck.id}`);
|
|
553
559
|
};
|
|
554
560
|
|
|
@@ -588,12 +594,20 @@ export default function Index() {
|
|
|
588
594
|
: undefined;
|
|
589
595
|
let deck: ReturnType<typeof createDeck> | undefined;
|
|
590
596
|
flushSync(() => {
|
|
597
|
+
// Commit the destination-shaped shell before navigating. React Router
|
|
598
|
+
// keeps the current outlet mounted while a cold route chunk loads, and
|
|
599
|
+
// showing the deck grid during that handoff makes the route indicator
|
|
600
|
+
// look like a second app.
|
|
601
|
+
setIsStartingNewDeck(true);
|
|
591
602
|
deck = createDeck(undefined, {
|
|
592
603
|
noDefaultSlides: true,
|
|
593
604
|
designSystemId: selectedDesignSystem?.id ?? null,
|
|
594
605
|
});
|
|
595
606
|
});
|
|
596
|
-
if (!deck)
|
|
607
|
+
if (!deck) {
|
|
608
|
+
setIsStartingNewDeck(false);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
597
611
|
const deckId = deck.id;
|
|
598
612
|
setNewDeckPromptOpen(false);
|
|
599
613
|
|
|
@@ -735,7 +749,7 @@ export default function Index() {
|
|
|
735
749
|
"Start a `manage-progress` run so progress appears in the app header. Add the first slide as soon as it is ready, then continue one slide at a time so the editor visibly fills in.",
|
|
736
750
|
"After reading any requested or attached reference material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Never use the deck id, run id, file id, or another opaque alphanumeric token as the title. Call `patch-deck` with `deckId: \"" +
|
|
737
751
|
deckId +
|
|
738
|
-
'\"` and `operations: [{ "op": "patch-deck-fields", "fields": { "title": "<generated title>" } }]`. Include only `title` in `fields`; omit all other optional fields. Never leave a generated deck named "Untitled Deck" or another placeholder.',
|
|
752
|
+
'\"` and `operations: [{ "op": "patch-deck-fields", "fields": { "title": "<generated title>" } }]`. Include only `title` in `fields`; omit all other optional fields. Never leave a generated deck named "Untitled Deck" or another placeholder, and do not reuse the uploaded filename or a generic label like "Untitled scene" when the content can describe the deck better.',
|
|
739
753
|
"If the user asks for a standalone visual, diagram, hero, one-pager, poster, or a couple of visuals, create only the requested one/few polished visual slides. Do not pad the result into a full presentation.",
|
|
740
754
|
"If the request is for a presentation or deck and does not explicitly ask for one slide, infer a coherent multi-slide outline from the scope and keep adding slides until that outline is complete. Do not stop after the first slide just because the prompt has few explicit instructions.",
|
|
741
755
|
"Add slides ONE AT A TIME using the `add-slide` action with --deckId=" +
|
|
@@ -1290,6 +1304,17 @@ export default function Index() {
|
|
|
1290
1304
|
),
|
|
1291
1305
|
);
|
|
1292
1306
|
|
|
1307
|
+
if (isStartingNewDeck) {
|
|
1308
|
+
return (
|
|
1309
|
+
<div
|
|
1310
|
+
className="fixed inset-0 z-[300] min-h-screen bg-background"
|
|
1311
|
+
data-testid="new-deck-loading"
|
|
1312
|
+
>
|
|
1313
|
+
<DeckEditorSkeleton label={t("deckEditor.lookingForDeck")} />
|
|
1314
|
+
</div>
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1293
1318
|
return (
|
|
1294
1319
|
<main className="min-w-0 flex-1 overflow-y-auto px-4 pb-6 pt-0 sm:px-6 sm:pb-10">
|
|
1295
1320
|
{loading ? (
|
|
@@ -158,11 +158,15 @@ function stripForwardedAttachmentData(
|
|
|
158
158
|
): AgentChatAttachment {
|
|
159
159
|
const next = { ...attachment };
|
|
160
160
|
// Keep visual data for the current model turn so uploaded screenshots remain
|
|
161
|
-
// available for vision analysis
|
|
161
|
+
// available for vision analysis. Keep non-visual bytes until core's shared
|
|
162
|
+
// pre-upload boundary has created the durable public object-storage URL;
|
|
163
|
+
// `slidesUploadPath` is a private import handle, not a chat attachment URL.
|
|
162
164
|
const inlineImage = isVisualAttachment(attachment)
|
|
163
165
|
? decodeDataUrl(attachment.data)
|
|
164
166
|
: null;
|
|
165
|
-
if (!inlineImage
|
|
167
|
+
if (!inlineImage && (saved.url || !attachment.data)) {
|
|
168
|
+
delete next.data;
|
|
169
|
+
} else if (inlineImage && inlineImage.bytes.length > MAX_INLINE_IMAGE_BYTES) {
|
|
166
170
|
delete next.data;
|
|
167
171
|
}
|
|
168
172
|
(next as any).slidesUploadPath = saved.path;
|
|
@@ -11,6 +11,14 @@ const GENERATED_TITLE_PLACEHOLDERS = new Set([
|
|
|
11
11
|
"your name",
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
|
+
const IMPORTED_TITLE_PLACEHOLDERS = new Set([
|
|
15
|
+
"untitled deck",
|
|
16
|
+
"untitled file",
|
|
17
|
+
"untitled presentation",
|
|
18
|
+
"untitled scene",
|
|
19
|
+
"untitled slide",
|
|
20
|
+
]);
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Generated deck ids should never become user-facing titles. Keep this
|
|
16
24
|
* deliberately narrow so normal titles with spaces and punctuation remain
|
|
@@ -63,6 +71,15 @@ function plainText(value: string): string {
|
|
|
63
71
|
.trim();
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
function plainTextLines(value: string): string[] {
|
|
75
|
+
return decodeHtmlEntities(
|
|
76
|
+
value.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/gi, "\n"),
|
|
77
|
+
)
|
|
78
|
+
.split(/\r?\n/)
|
|
79
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
function usableCandidate(value: string): string | null {
|
|
67
84
|
const candidate = plainText(value).replace(/^[•●▪‣\-\s]+/, "");
|
|
68
85
|
if (!candidate || candidate.length > 140) return null;
|
|
@@ -101,6 +118,17 @@ export function deriveDeckTitleFromSlideContent(
|
|
|
101
118
|
if (text) candidates.push({ score: fontSize, text });
|
|
102
119
|
}
|
|
103
120
|
|
|
121
|
+
const fallbackLines = plainTextLines(content);
|
|
122
|
+
if (fallbackLines.length > 1) {
|
|
123
|
+
for (const line of fallbackLines) {
|
|
124
|
+
const text = usableCandidate(line);
|
|
125
|
+
if (text) {
|
|
126
|
+
candidates.push({ score: 1, text });
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
return candidates.sort((a, b) => b.score - a.score)[0]?.text ?? null;
|
|
105
133
|
}
|
|
106
134
|
|
|
@@ -126,6 +154,34 @@ export function repairGeneratedDeckTitle(
|
|
|
126
154
|
);
|
|
127
155
|
}
|
|
128
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Imported files often fall back to filenames when they do not have a
|
|
159
|
+
* meaningful title in their own metadata. Prefer the first slide's content
|
|
160
|
+
* when it can produce a real deck title; otherwise keep a human-readable
|
|
161
|
+
* fallback instead of a source filename placeholder.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveImportedDeckTitle(
|
|
164
|
+
requestedTitle: unknown,
|
|
165
|
+
firstSlideContent: unknown,
|
|
166
|
+
): string {
|
|
167
|
+
if (typeof requestedTitle === "string") {
|
|
168
|
+
const title = requestedTitle.trim();
|
|
169
|
+
const opaqueTitle = isOpaqueDeckTitle(requestedTitle);
|
|
170
|
+
if (
|
|
171
|
+
title &&
|
|
172
|
+
!opaqueTitle &&
|
|
173
|
+
!IMPORTED_TITLE_PLACEHOLDERS.has(title.toLowerCase())
|
|
174
|
+
) {
|
|
175
|
+
return title;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const derivedTitle = deriveDeckTitleFromSlideContent(firstSlideContent);
|
|
180
|
+
if (derivedTitle) return derivedTitle;
|
|
181
|
+
|
|
182
|
+
return "Imported File";
|
|
183
|
+
}
|
|
184
|
+
|
|
129
185
|
export function assertHumanReadableDeckTitle(title: string): void {
|
|
130
186
|
if (isOpaqueDeckTitle(title)) {
|
|
131
187
|
throw new Error(
|