@agent-native/core 0.126.0 → 0.127.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +83 -0
- package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
- package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/index.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +6 -1
- package/corpus/core/src/agent/run-manager.ts +37 -0
- package/corpus/core/src/agent/run-store.ts +18 -0
- package/corpus/core/src/cli/template-sync.ts +96 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
- package/corpus/core/src/client/agent-chat.ts +19 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
- package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
- package/corpus/core/src/client/use-chat-models.ts +54 -37
- package/corpus/core/src/localization/default-messages.ts +11 -0
- package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
- package/corpus/core/src/mcp/builtin-tools.ts +146 -34
- package/corpus/core/src/server/agent-capabilities.ts +8 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
- package/corpus/core/src/server/agent-discovery.ts +1 -1
- package/corpus/core/src/server/agent-peer-probe.ts +133 -0
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/server/core-routes-plugin.ts +67 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
- package/corpus/core/src/templates/chat/_gitignore +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
- package/corpus/templates/design/actions/create-file.ts +15 -0
- package/corpus/templates/design/actions/generate-design.ts +40 -0
- package/corpus/templates/design/actions/present-design-variants.ts +16 -0
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
- package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +608 -18
- package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
- package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
- package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
- package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +11 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +34 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +8 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +16 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/template-sync.d.ts.map +1 -1
- package/dist/cli/template-sync.js +88 -0
- package/dist/cli/template-sync.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +60 -31
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +16 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/AgentsSection.d.ts.map +1 -1
- package/dist/client/settings/AgentsSection.js +250 -30
- package/dist/client/settings/AgentsSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +16 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +46 -27
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +9 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +9 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/ask-app-task-handle.d.ts +25 -0
- package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
- package/dist/mcp/ask-app-task-handle.js +136 -0
- package/dist/mcp/ask-app-task-handle.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +3 -3
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -38
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-capabilities.d.ts +7 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +1 -0
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +7 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +16 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts +1 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +1 -1
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/agent-peer-probe.d.ts +37 -0
- package/dist/server/agent-peer-probe.d.ts.map +1 -0
- package/dist/server/agent-peer-probe.js +90 -0
- package/dist/server/agent-peer-probe.js.map +1 -0
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +53 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -0
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +4 -0
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/docs/content/external-agents-catalog.mdx +14 -11
- package/docs/content/mcp-protocol.mdx +9 -4
- package/package.json +2 -2
- package/src/a2a/index.ts +2 -0
- package/src/agent/production-agent.ts +6 -1
- package/src/agent/run-manager.ts +37 -0
- package/src/agent/run-store.ts +18 -0
- package/src/cli/template-sync.ts +96 -0
- package/src/client/MultiTabAssistantChat.tsx +75 -43
- package/src/client/agent-chat.ts +19 -1
- package/src/client/chat/tool-call-display.tsx +25 -0
- package/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/src/client/settings/AgentsSection.tsx +565 -71
- package/src/client/settings/SecretsSection.tsx +71 -36
- package/src/client/use-chat-models.ts +54 -37
- package/src/localization/default-messages.ts +11 -0
- package/src/mcp/ask-app-task-handle.ts +183 -0
- package/src/mcp/builtin-tools.ts +146 -34
- package/src/server/agent-capabilities.ts +8 -1
- package/src/server/agent-chat-plugin.ts +17 -0
- package/src/server/agent-discovery.ts +1 -1
- package/src/server/agent-peer-probe.ts +133 -0
- package/src/server/builder-design-systems.ts +1 -1
- package/src/server/core-routes-plugin.ts +67 -0
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/templates/chat/_gitignore +2 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
|
@@ -181,9 +181,7 @@ export default function DeckCard({
|
|
|
181
181
|
Rename
|
|
182
182
|
</DropdownMenuItem>
|
|
183
183
|
<DropdownMenuItem
|
|
184
|
-
|
|
185
|
-
e.preventDefault();
|
|
186
|
-
e.stopPropagation();
|
|
184
|
+
onSelect={() => {
|
|
187
185
|
if (isDuplicating) return;
|
|
188
186
|
onDuplicate(deck.id);
|
|
189
187
|
}}
|
|
@@ -204,9 +202,7 @@ export default function DeckCard({
|
|
|
204
202
|
</DropdownMenuItem>
|
|
205
203
|
<DropdownMenuSeparator />
|
|
206
204
|
<DropdownMenuItem
|
|
207
|
-
|
|
208
|
-
e.preventDefault();
|
|
209
|
-
e.stopPropagation();
|
|
205
|
+
onSelect={() => {
|
|
210
206
|
onDelete(deck.id);
|
|
211
207
|
}}
|
|
212
208
|
className="text-red-400 focus:text-red-400"
|
|
@@ -28,7 +28,10 @@ import { toast } from "sonner";
|
|
|
28
28
|
|
|
29
29
|
import SlideRenderer from "@/components/deck/SlideRenderer";
|
|
30
30
|
import { GoogleDocImportHint } from "@/components/editor/GoogleDocImportHint";
|
|
31
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
isInsidePortaledLayer,
|
|
33
|
+
type UploadedFile,
|
|
34
|
+
} from "@/components/editor/PromptDialog";
|
|
32
35
|
import {
|
|
33
36
|
Tooltip,
|
|
34
37
|
TooltipContent,
|
|
@@ -366,6 +369,7 @@ function AddSlidePopover({
|
|
|
366
369
|
useEffect(() => {
|
|
367
370
|
if (!open) return;
|
|
368
371
|
const handleClick = (e: MouseEvent) => {
|
|
372
|
+
if (isInsidePortaledLayer(e.target)) return;
|
|
369
373
|
if (
|
|
370
374
|
panelRef.current &&
|
|
371
375
|
!panelRef.current.contains(e.target as Node) &&
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
type AspectRatio,
|
|
65
65
|
DEFAULT_ASPECT_RATIO,
|
|
66
66
|
} from "@/lib/aspect-ratios";
|
|
67
|
+
import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
|
|
67
68
|
import { parseUploadResponse } from "@/lib/upload-response";
|
|
68
69
|
import { shortcutLabel } from "@/lib/utils";
|
|
69
70
|
|
|
@@ -134,6 +135,8 @@ interface EditorToolbarProps {
|
|
|
134
135
|
onExportPdf?: () => void;
|
|
135
136
|
/** Export the deck as PPTX */
|
|
136
137
|
onExportPptx?: () => Promise<void> | void;
|
|
138
|
+
/** Create the deck in the user's Google Drive as native Google Slides */
|
|
139
|
+
onExportGoogleSlides?: () => Promise<GoogleSlidesExportResult>;
|
|
137
140
|
/** Active deck aspect ratio (defaults to 16:9 when omitted) */
|
|
138
141
|
aspectRatio?: AspectRatio;
|
|
139
142
|
/** Change the deck's aspect ratio */
|
|
@@ -267,6 +270,7 @@ export default function EditorToolbar({
|
|
|
267
270
|
onDuplicateDeck,
|
|
268
271
|
onExportPdf,
|
|
269
272
|
onExportPptx,
|
|
273
|
+
onExportGoogleSlides,
|
|
270
274
|
aspectRatio,
|
|
271
275
|
onSetAspectRatio,
|
|
272
276
|
designSystemTitle,
|
|
@@ -998,6 +1002,7 @@ graph TD
|
|
|
998
1002
|
onDuplicate={onDuplicateDeck ?? (() => {})}
|
|
999
1003
|
onExportPdf={onExportPdf ?? (() => {})}
|
|
1000
1004
|
onExportPptx={onExportPptx ?? (() => {})}
|
|
1005
|
+
onExportGoogleSlides={onExportGoogleSlides}
|
|
1001
1006
|
/>
|
|
1002
1007
|
</div>
|
|
1003
1008
|
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
DropdownMenuSeparator,
|
|
19
19
|
DropdownMenuTrigger,
|
|
20
20
|
} from "@/components/ui/dropdown-menu";
|
|
21
|
+
import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
|
|
22
|
+
|
|
23
|
+
/** Google Slides' File → Import dialog, primed to ask for a file. */
|
|
24
|
+
const GOOGLE_SLIDES_IMPORT_URL =
|
|
25
|
+
"https://docs.google.com/presentation/u/0/?usp=import";
|
|
21
26
|
|
|
22
27
|
interface ExportMenuProps {
|
|
23
28
|
deckId: string;
|
|
@@ -25,6 +30,7 @@ interface ExportMenuProps {
|
|
|
25
30
|
onDuplicate: () => void;
|
|
26
31
|
onExportPdf: () => void;
|
|
27
32
|
onExportPptx: () => Promise<void> | void;
|
|
33
|
+
onExportGoogleSlides?: () => Promise<GoogleSlidesExportResult>;
|
|
28
34
|
onShareLink?: () => void;
|
|
29
35
|
onShareTeam?: () => void;
|
|
30
36
|
}
|
|
@@ -35,6 +41,7 @@ export function ExportMenu({
|
|
|
35
41
|
onDuplicate,
|
|
36
42
|
onExportPdf,
|
|
37
43
|
onExportPptx,
|
|
44
|
+
onExportGoogleSlides,
|
|
38
45
|
onShareLink,
|
|
39
46
|
onShareTeam,
|
|
40
47
|
}: ExportMenuProps) {
|
|
@@ -84,12 +91,26 @@ export function ExportMenu({
|
|
|
84
91
|
};
|
|
85
92
|
|
|
86
93
|
const handleExportGoogleSlides = async () => {
|
|
94
|
+
if (!onExportGoogleSlides) return;
|
|
95
|
+
// Opened up-front: browsers only honour window.open() inside the click
|
|
96
|
+
// gesture, and building the PPTX is async.
|
|
97
|
+
const target = window.open("", "_blank");
|
|
87
98
|
try {
|
|
88
|
-
await
|
|
99
|
+
const result = await onExportGoogleSlides();
|
|
100
|
+
if (result.url !== null) {
|
|
101
|
+
if (target) target.location.href = result.url;
|
|
102
|
+
toast.success(t("editorExport.googleSlidesCreated"), {
|
|
103
|
+
description: t("editorExport.googleSlidesCreatedHint"),
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.warn("Google Slides upload unavailable:", result.reason);
|
|
108
|
+
if (target) target.location.href = GOOGLE_SLIDES_IMPORT_URL;
|
|
89
109
|
toast.success(t("editorExport.googleSlidesDownloaded"), {
|
|
90
110
|
description: t("editorExport.googleSlidesImportHint"),
|
|
91
111
|
});
|
|
92
112
|
} catch (err) {
|
|
113
|
+
target?.close();
|
|
93
114
|
console.error("Export failed:", err);
|
|
94
115
|
toast.error(t("editorExport.exportFailed"), {
|
|
95
116
|
description:
|
|
@@ -166,13 +187,15 @@ export function ExportMenu({
|
|
|
166
187
|
<IconDownload className="w-4 h-4 mr-2" />
|
|
167
188
|
{t("editorExport.exportPptx")}
|
|
168
189
|
</DropdownMenuItem>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
{onExportGoogleSlides && (
|
|
191
|
+
<DropdownMenuItem
|
|
192
|
+
onClick={handleExportGoogleSlides}
|
|
193
|
+
className="cursor-pointer"
|
|
194
|
+
>
|
|
195
|
+
<IconBrandGoogle className="w-4 h-4 mr-2" />
|
|
196
|
+
{t("editorExport.openInGoogleSlides")}
|
|
197
|
+
</DropdownMenuItem>
|
|
198
|
+
)}
|
|
176
199
|
<DropdownMenuSeparator />
|
|
177
200
|
<DropdownMenuItem onClick={onDuplicate} className="cursor-pointer">
|
|
178
201
|
<IconCopy className="w-4 h-4 mr-2" />
|
|
@@ -16,6 +16,20 @@ export interface UploadedFile {
|
|
|
16
16
|
size: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Radix popovers portal to `document.body`, so a mousedown inside the model
|
|
21
|
+
* picker or attachment menu reads as "outside" any panel that hosts a composer.
|
|
22
|
+
* Closing on it unmounts the popover before its own click fires, which looks
|
|
23
|
+
* exactly like a dead button.
|
|
24
|
+
*/
|
|
25
|
+
export function isInsidePortaledLayer(target: EventTarget | null): boolean {
|
|
26
|
+
return Boolean(
|
|
27
|
+
(target as Element | null)?.closest?.(
|
|
28
|
+
"[data-radix-popper-content-wrapper]",
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
interface PromptPopoverProps {
|
|
20
34
|
open: boolean;
|
|
21
35
|
onOpenChange: (open: boolean) => void;
|
|
@@ -98,6 +112,7 @@ export default function PromptPopover({
|
|
|
98
112
|
useEffect(() => {
|
|
99
113
|
if (!open) return;
|
|
100
114
|
const handleClick = (e: MouseEvent) => {
|
|
115
|
+
if (isInsidePortaledLayer(e.target)) return;
|
|
101
116
|
if (
|
|
102
117
|
panelRef.current &&
|
|
103
118
|
!panelRef.current.contains(e.target as Node) &&
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
IconChevronLeft,
|
|
4
4
|
IconChevronRight,
|
|
5
5
|
IconMaximize,
|
|
6
|
+
IconNotes,
|
|
6
7
|
IconX,
|
|
7
8
|
} from "@tabler/icons-react";
|
|
8
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
resolveSlideAnimationElement,
|
|
21
22
|
} from "@/lib/slide-animation-elements";
|
|
22
23
|
|
|
24
|
+
import { openPresentChannel, type PresentMessage } from "./present-channel";
|
|
25
|
+
|
|
23
26
|
interface PresentationViewProps {
|
|
24
27
|
slides: Slide[];
|
|
25
28
|
deckId: string;
|
|
@@ -271,6 +274,58 @@ export default function PresentationView({
|
|
|
271
274
|
}
|
|
272
275
|
}, [navigate, deckId, isShared]);
|
|
273
276
|
|
|
277
|
+
// Presenter window: it owns no navigation state of its own, it just sends
|
|
278
|
+
// commands and mirrors whatever we echo back — so build steps stay
|
|
279
|
+
// authoritative here.
|
|
280
|
+
const channelRef = useRef<BroadcastChannel | null>(null);
|
|
281
|
+
const goNextRef = useRef(goNext);
|
|
282
|
+
const goPrevRef = useRef(goPrev);
|
|
283
|
+
goNextRef.current = goNext;
|
|
284
|
+
goPrevRef.current = goPrev;
|
|
285
|
+
|
|
286
|
+
const broadcastState = useCallback(() => {
|
|
287
|
+
channelRef.current?.postMessage({
|
|
288
|
+
type: "state",
|
|
289
|
+
index: currentIndex,
|
|
290
|
+
} satisfies PresentMessage);
|
|
291
|
+
}, [currentIndex]);
|
|
292
|
+
const broadcastStateRef = useRef(broadcastState);
|
|
293
|
+
broadcastStateRef.current = broadcastState;
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const channel = openPresentChannel(deckId);
|
|
297
|
+
channelRef.current = channel;
|
|
298
|
+
if (!channel) return;
|
|
299
|
+
channel.onmessage = (event: MessageEvent<PresentMessage>) => {
|
|
300
|
+
const message = event.data;
|
|
301
|
+
if (message?.type === "hello") {
|
|
302
|
+
broadcastStateRef.current();
|
|
303
|
+
} else if (message?.type === "command") {
|
|
304
|
+
if (message.command === "next") goNextRef.current();
|
|
305
|
+
else goPrevRef.current();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
return () => {
|
|
309
|
+
channel.close();
|
|
310
|
+
channelRef.current = null;
|
|
311
|
+
};
|
|
312
|
+
}, [deckId]);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
broadcastState();
|
|
316
|
+
}, [broadcastState]);
|
|
317
|
+
|
|
318
|
+
const openPresenterWindow = useCallback(() => {
|
|
319
|
+
const url = new URL(window.location.href);
|
|
320
|
+
url.searchParams.set("presenter", "1");
|
|
321
|
+
url.searchParams.set("slide", String(currentIndex + 1));
|
|
322
|
+
window.open(
|
|
323
|
+
url.toString(),
|
|
324
|
+
`slides-presenter-${deckId}`,
|
|
325
|
+
"width=1200,height=760",
|
|
326
|
+
);
|
|
327
|
+
}, [currentIndex, deckId]);
|
|
328
|
+
|
|
274
329
|
useEffect(() => {
|
|
275
330
|
const handleKey = (e: KeyboardEvent) => {
|
|
276
331
|
switch (e.key) {
|
|
@@ -294,6 +349,11 @@ export default function PresentationView({
|
|
|
294
349
|
document.exitFullscreen().catch(() => {});
|
|
295
350
|
}
|
|
296
351
|
break;
|
|
352
|
+
case "s":
|
|
353
|
+
case "S":
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
openPresenterWindow();
|
|
356
|
+
break;
|
|
297
357
|
case "Escape":
|
|
298
358
|
exit();
|
|
299
359
|
break;
|
|
@@ -301,7 +361,7 @@ export default function PresentationView({
|
|
|
301
361
|
};
|
|
302
362
|
window.addEventListener("keydown", handleKey);
|
|
303
363
|
return () => window.removeEventListener("keydown", handleKey);
|
|
304
|
-
}, [goNext, goPrev, exit]);
|
|
364
|
+
}, [goNext, goPrev, exit, openPresenterWindow]);
|
|
305
365
|
|
|
306
366
|
// Try to enter fullscreen. Browsers require a user gesture; the click that
|
|
307
367
|
// navigated to /present often counts, but Safari/Firefox sometimes block
|
|
@@ -501,13 +561,23 @@ export default function PresentationView({
|
|
|
501
561
|
</button>
|
|
502
562
|
</div>
|
|
503
563
|
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
564
|
+
<div className="flex items-center gap-2">
|
|
565
|
+
<button
|
|
566
|
+
onClick={openPresenterWindow}
|
|
567
|
+
className="p-3 sm:p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors cursor-pointer"
|
|
568
|
+
aria-label={t("presentation.presenterView")}
|
|
569
|
+
title={t("presentation.presenterView")}
|
|
570
|
+
>
|
|
571
|
+
<IconNotes className="w-5 h-5 sm:w-4 sm:h-4 text-white" />
|
|
572
|
+
</button>
|
|
573
|
+
<button
|
|
574
|
+
onClick={exit}
|
|
575
|
+
className="p-3 sm:p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
|
|
576
|
+
aria-label={t("presentation.exitPresentation")}
|
|
577
|
+
>
|
|
578
|
+
<IconX className="w-5 h-5 sm:w-4 sm:h-4 text-white" />
|
|
579
|
+
</button>
|
|
580
|
+
</div>
|
|
511
581
|
</div>
|
|
512
582
|
|
|
513
583
|
{/* Progress bar */}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconChevronLeft, IconChevronRight, IconX } from "@tabler/icons-react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import SlideRenderer from "@/components/deck/SlideRenderer";
|
|
6
|
+
import type { Slide } from "@/context/DeckContext";
|
|
7
|
+
import type { AspectRatio } from "@/lib/aspect-ratios";
|
|
8
|
+
|
|
9
|
+
import { openPresentChannel, type PresentMessage } from "./present-channel";
|
|
10
|
+
|
|
11
|
+
interface PresenterViewProps {
|
|
12
|
+
slides: Slide[];
|
|
13
|
+
deckId: string;
|
|
14
|
+
startIndex?: number;
|
|
15
|
+
aspectRatio?: AspectRatio;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatElapsed(seconds: number) {
|
|
19
|
+
const mins = Math.floor(seconds / 60);
|
|
20
|
+
const secs = seconds % 60;
|
|
21
|
+
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function PresenterView({
|
|
25
|
+
slides,
|
|
26
|
+
deckId,
|
|
27
|
+
startIndex = 0,
|
|
28
|
+
aspectRatio,
|
|
29
|
+
}: PresenterViewProps) {
|
|
30
|
+
const t = useT();
|
|
31
|
+
const [index, setIndex] = useState(startIndex);
|
|
32
|
+
const [elapsed, setElapsed] = useState(0);
|
|
33
|
+
const channelRef = useRef<BroadcastChannel | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const channel = openPresentChannel(deckId);
|
|
37
|
+
channelRef.current = channel;
|
|
38
|
+
if (!channel) return;
|
|
39
|
+
channel.onmessage = (event: MessageEvent<PresentMessage>) => {
|
|
40
|
+
if (event.data?.type === "state") setIndex(event.data.index);
|
|
41
|
+
};
|
|
42
|
+
channel.postMessage({ type: "hello" } satisfies PresentMessage);
|
|
43
|
+
return () => {
|
|
44
|
+
channel.close();
|
|
45
|
+
channelRef.current = null;
|
|
46
|
+
};
|
|
47
|
+
}, [deckId]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const timer = window.setInterval(() => setElapsed((s) => s + 1), 1000);
|
|
51
|
+
return () => window.clearInterval(timer);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const send = useCallback((message: PresentMessage) => {
|
|
55
|
+
channelRef.current?.postMessage(message);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const goNext = useCallback(
|
|
59
|
+
() => send({ type: "command", command: "next" }),
|
|
60
|
+
[send],
|
|
61
|
+
);
|
|
62
|
+
const goPrev = useCallback(
|
|
63
|
+
() => send({ type: "command", command: "prev" }),
|
|
64
|
+
[send],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
69
|
+
switch (e.key) {
|
|
70
|
+
case "ArrowRight":
|
|
71
|
+
case "ArrowDown":
|
|
72
|
+
case " ":
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
goNext();
|
|
75
|
+
break;
|
|
76
|
+
case "ArrowLeft":
|
|
77
|
+
case "ArrowUp":
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
goPrev();
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
window.addEventListener("keydown", handleKey);
|
|
84
|
+
return () => window.removeEventListener("keydown", handleKey);
|
|
85
|
+
}, [goNext, goPrev]);
|
|
86
|
+
|
|
87
|
+
const safeSlides = useMemo(
|
|
88
|
+
() => (Array.isArray(slides) ? slides.filter(Boolean) : []),
|
|
89
|
+
[slides],
|
|
90
|
+
);
|
|
91
|
+
const current = safeSlides[index];
|
|
92
|
+
const next = safeSlides[index + 1];
|
|
93
|
+
const notes = current?.notes?.trim();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="fixed inset-0 flex flex-col bg-[hsl(240,6%,6%)] text-white">
|
|
97
|
+
<header className="flex items-center justify-between gap-4 border-b border-white/10 px-5 py-3">
|
|
98
|
+
<span className="font-mono text-sm text-white/50">
|
|
99
|
+
{index + 1} / {safeSlides.length}
|
|
100
|
+
</span>
|
|
101
|
+
<span className="font-mono text-2xl tabular-nums text-white/80">
|
|
102
|
+
{formatElapsed(elapsed)}
|
|
103
|
+
</span>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<button
|
|
106
|
+
onClick={goPrev}
|
|
107
|
+
disabled={index === 0}
|
|
108
|
+
className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-30"
|
|
109
|
+
aria-label={t("presentation.previousSlide")}
|
|
110
|
+
>
|
|
111
|
+
<IconChevronLeft className="h-4 w-4" />
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
onClick={goNext}
|
|
115
|
+
disabled={index >= safeSlides.length - 1}
|
|
116
|
+
className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-30"
|
|
117
|
+
aria-label={t("presentation.nextSlide")}
|
|
118
|
+
>
|
|
119
|
+
<IconChevronRight className="h-4 w-4" />
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => window.close()}
|
|
123
|
+
className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20"
|
|
124
|
+
aria-label={t("presentation.closePresenterView")}
|
|
125
|
+
>
|
|
126
|
+
<IconX className="h-4 w-4" />
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</header>
|
|
130
|
+
|
|
131
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4 p-4">
|
|
132
|
+
<div className="flex gap-4">
|
|
133
|
+
<div className="w-2/3 overflow-hidden rounded-lg bg-black">
|
|
134
|
+
{current && (
|
|
135
|
+
<SlideRenderer
|
|
136
|
+
slide={current}
|
|
137
|
+
thumbnail
|
|
138
|
+
aspectRatio={aspectRatio}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex w-1/3 flex-col gap-2">
|
|
143
|
+
<div className="font-mono text-[11px] uppercase tracking-widest text-white/40">
|
|
144
|
+
{t("presentation.upNext")}
|
|
145
|
+
</div>
|
|
146
|
+
{next ? (
|
|
147
|
+
<div className="overflow-hidden rounded-lg bg-black">
|
|
148
|
+
<SlideRenderer
|
|
149
|
+
slide={next}
|
|
150
|
+
thumbnail
|
|
151
|
+
aspectRatio={aspectRatio}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<div className="rounded-lg bg-white/[0.04] p-4 text-sm text-white/40">
|
|
156
|
+
{t("presentation.endOfDeck")}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="min-h-0 flex-1 overflow-y-auto rounded-lg bg-white/[0.04] p-5">
|
|
163
|
+
<div className="mb-2 font-mono text-[11px] uppercase tracking-widest text-white/40">
|
|
164
|
+
{t("presentation.speakerNotes")}
|
|
165
|
+
</div>
|
|
166
|
+
{notes ? (
|
|
167
|
+
<p className="whitespace-pre-wrap text-lg leading-relaxed text-white/90">
|
|
168
|
+
{notes}
|
|
169
|
+
</p>
|
|
170
|
+
) : (
|
|
171
|
+
<p className="text-sm text-white/40">
|
|
172
|
+
{t("presentation.noNotesForSlide")}
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-window channel between the fullscreen presentation and the presenter
|
|
3
|
+
* window. The presenter window never advances on its own — it sends commands
|
|
4
|
+
* and renders whatever state the presentation echoes back, so build steps
|
|
5
|
+
* (which move the step, not the slide) stay authoritative in one place.
|
|
6
|
+
*/
|
|
7
|
+
export type PresentMessage =
|
|
8
|
+
| { type: "state"; index: number }
|
|
9
|
+
| { type: "command"; command: "next" | "prev" }
|
|
10
|
+
| { type: "hello" };
|
|
11
|
+
|
|
12
|
+
export function openPresentChannel(deckId: string): BroadcastChannel | null {
|
|
13
|
+
if (typeof BroadcastChannel === "undefined") return null;
|
|
14
|
+
return new BroadcastChannel(`slides-present:${deckId}`);
|
|
15
|
+
}
|
|
@@ -256,7 +256,10 @@ const messages = {
|
|
|
256
256
|
downloadHtml: "Download as HTML",
|
|
257
257
|
exportPdf: "Export as PDF",
|
|
258
258
|
exportPptx: "Export as PPTX",
|
|
259
|
-
|
|
259
|
+
openInGoogleSlides: "Open in Google Slides",
|
|
260
|
+
googleSlidesCreated: "Opened in Google Slides",
|
|
261
|
+
googleSlidesCreatedHint:
|
|
262
|
+
"A copy of this deck was created in your Google Drive.",
|
|
260
263
|
duplicateDeck: "Duplicate deck",
|
|
261
264
|
},
|
|
262
265
|
share: {
|
|
@@ -467,6 +470,12 @@ const messages = {
|
|
|
467
470
|
slides: "Slides",
|
|
468
471
|
},
|
|
469
472
|
presentation: {
|
|
473
|
+
presenterView: "Presenter view",
|
|
474
|
+
speakerNotes: "Speaker notes",
|
|
475
|
+
noNotesForSlide: "No notes for this slide",
|
|
476
|
+
upNext: "Up next",
|
|
477
|
+
endOfDeck: "End of deck",
|
|
478
|
+
closePresenterView: "Close presenter view",
|
|
470
479
|
noSlides: "No slides to present",
|
|
471
480
|
previousSlide: "Previous slide",
|
|
472
481
|
nextSlide: "Next slide",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { appBasePath } from "@agent-native/core/client/api-path";
|
|
2
|
+
|
|
3
|
+
import type { AspectRatio } from "./aspect-ratios";
|
|
4
|
+
import { buildDeckPptxBlob } from "./export-pptx-client";
|
|
5
|
+
|
|
6
|
+
interface GoogleSlidesExportSlide {
|
|
7
|
+
id: string;
|
|
8
|
+
notes?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type GoogleSlidesExportResult =
|
|
12
|
+
| { url: string }
|
|
13
|
+
/** Drive was unavailable, so the PPTX was downloaded for a manual import. */
|
|
14
|
+
| { url: null; downloaded: true; reason: string };
|
|
15
|
+
|
|
16
|
+
function triggerBlobDownload(blob: Blob, filename: string) {
|
|
17
|
+
const url = URL.createObjectURL(blob);
|
|
18
|
+
const a = document.createElement("a");
|
|
19
|
+
a.href = url;
|
|
20
|
+
a.download = filename;
|
|
21
|
+
a.rel = "noopener";
|
|
22
|
+
document.body.appendChild(a);
|
|
23
|
+
a.click();
|
|
24
|
+
a.remove();
|
|
25
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a native Google Slides deck in the user's Drive when their Google
|
|
30
|
+
* account is connected. Without a connection — or if Drive rejects the upload —
|
|
31
|
+
* the PPTX is downloaded instead so the user can import it by hand, and the
|
|
32
|
+
* reason is reported rather than swallowed.
|
|
33
|
+
*/
|
|
34
|
+
export async function exportDeckToGoogleSlides(
|
|
35
|
+
deckTitle: string,
|
|
36
|
+
slides: GoogleSlidesExportSlide[],
|
|
37
|
+
aspectRatio?: AspectRatio,
|
|
38
|
+
): Promise<GoogleSlidesExportResult> {
|
|
39
|
+
const { blob, filename } = await buildDeckPptxBlob(
|
|
40
|
+
deckTitle,
|
|
41
|
+
slides,
|
|
42
|
+
aspectRatio,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const form = new FormData();
|
|
46
|
+
form.append("file", blob, filename);
|
|
47
|
+
form.append("title", deckTitle);
|
|
48
|
+
|
|
49
|
+
const res = await fetch(`${appBasePath()}/api/exports/google-slides`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: form,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const payload = (await res.json().catch(() => null)) as {
|
|
55
|
+
url?: string;
|
|
56
|
+
error?: string;
|
|
57
|
+
} | null;
|
|
58
|
+
|
|
59
|
+
if (res.ok && payload?.url) return { url: payload.url };
|
|
60
|
+
|
|
61
|
+
triggerBlobDownload(blob, filename);
|
|
62
|
+
return {
|
|
63
|
+
url: null,
|
|
64
|
+
downloaded: true,
|
|
65
|
+
reason: payload?.error ?? `HTTP ${res.status}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -313,11 +313,11 @@ function widenNoWrapTextElements(root: HTMLElement) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
export async function
|
|
316
|
+
export async function buildDeckPptxBlob(
|
|
317
317
|
deckTitle: string,
|
|
318
318
|
slides: PptxExportSlide[],
|
|
319
319
|
aspectRatio?: AspectRatio,
|
|
320
|
-
): Promise<
|
|
320
|
+
): Promise<{ blob: Blob; filename: string }> {
|
|
321
321
|
const { exportToPptx } = await importExportModule(
|
|
322
322
|
() => import("dom-to-pptx"),
|
|
323
323
|
);
|
|
@@ -359,10 +359,23 @@ export async function exportDeckAsPptx(
|
|
|
359
359
|
);
|
|
360
360
|
|
|
361
361
|
const blob = await addSpeakerNotesToPptxBlob(initialBlob, slides);
|
|
362
|
-
|
|
362
|
+
return { blob, filename: safePptxName(deckTitle) };
|
|
363
363
|
} finally {
|
|
364
364
|
for (const clone of exportClones) {
|
|
365
365
|
clone.cleanup();
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
+
|
|
370
|
+
export async function exportDeckAsPptx(
|
|
371
|
+
deckTitle: string,
|
|
372
|
+
slides: PptxExportSlide[],
|
|
373
|
+
aspectRatio?: AspectRatio,
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
const { blob, filename } = await buildDeckPptxBlob(
|
|
376
|
+
deckTitle,
|
|
377
|
+
slides,
|
|
378
|
+
aspectRatio,
|
|
379
|
+
);
|
|
380
|
+
triggerBlobDownload(blob, filename);
|
|
381
|
+
}
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
} from "@/hooks/use-slide-comments";
|
|
63
63
|
import type { AspectRatio } from "@/lib/aspect-ratios";
|
|
64
64
|
import { getPreset } from "@/lib/design-systems";
|
|
65
|
+
import { exportDeckToGoogleSlides } from "@/lib/export-google-slides-client";
|
|
65
66
|
import { exportDeckAsPdf } from "@/lib/export-pdf-client";
|
|
66
67
|
import { exportDeckAsPptx } from "@/lib/export-pptx-client";
|
|
67
68
|
import {
|
|
@@ -854,6 +855,16 @@ export default function DeckEditor() {
|
|
|
854
855
|
}
|
|
855
856
|
await exportDeckAsPptx(deck.title, slides, deck.aspectRatio);
|
|
856
857
|
}}
|
|
858
|
+
onExportGoogleSlides={async () => {
|
|
859
|
+
const slides = deck.slides.map((s) => ({
|
|
860
|
+
id: s.id,
|
|
861
|
+
notes: s.notes,
|
|
862
|
+
}));
|
|
863
|
+
if (slides.length === 0) {
|
|
864
|
+
throw new Error(t("deckEditor.deckHasNoSlides"));
|
|
865
|
+
}
|
|
866
|
+
return exportDeckToGoogleSlides(deck.title, slides, deck.aspectRatio);
|
|
867
|
+
}}
|
|
857
868
|
aspectRatio={deck.aspectRatio}
|
|
858
869
|
designSystemTitle={designSystemTitle}
|
|
859
870
|
onSetAspectRatio={(ratio: AspectRatio) => {
|