@agent-native/core 0.126.0 → 0.127.1
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 +71 -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/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/core-routes-plugin.ts +67 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- 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/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
- 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/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 +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 +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/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/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/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/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/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/core-routes-plugin.ts +67 -0
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
|
@@ -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) => {
|
|
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { useParams, Navigate, useSearchParams } from "react-router";
|
|
4
4
|
|
|
5
5
|
import PresentationView from "@/components/presentation/PresentationView";
|
|
6
|
+
import PresenterView from "@/components/presentation/PresenterView";
|
|
6
7
|
import { useDecks } from "@/context/DeckContext";
|
|
7
8
|
import type { Deck } from "@/context/DeckContext";
|
|
8
9
|
|
|
@@ -45,10 +46,14 @@ export default function Presentation() {
|
|
|
45
46
|
};
|
|
46
47
|
}, [contextDeck, id, loading]);
|
|
47
48
|
|
|
48
|
-
if (
|
|
49
|
+
if (!id) return <Navigate to="/" replace />;
|
|
50
|
+
// "Not fetched yet" is not "not found": on a cold load of this URL the deck
|
|
51
|
+
// context is empty and the fallback fetch has not run, so redirecting on a
|
|
52
|
+
// falsy deck bounced every direct/presenter/share link back to the index.
|
|
53
|
+
if (!deck && fallbackState !== "missing") {
|
|
49
54
|
return <div className="h-screen bg-black" />;
|
|
50
55
|
}
|
|
51
|
-
if (!deck
|
|
56
|
+
if (!deck) {
|
|
52
57
|
return <Navigate to="/" replace />;
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -58,8 +63,11 @@ export default function Presentation() {
|
|
|
58
63
|
? Math.max(0, parsedSlide - 1)
|
|
59
64
|
: 0;
|
|
60
65
|
|
|
66
|
+
const View =
|
|
67
|
+
searchParams.get("presenter") === "1" ? PresenterView : PresentationView;
|
|
68
|
+
|
|
61
69
|
return (
|
|
62
|
-
<
|
|
70
|
+
<View
|
|
63
71
|
slides={Array.isArray(deck.slides) ? deck.slides : []}
|
|
64
72
|
deckId={id}
|
|
65
73
|
startIndex={startSlide}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getSession, runWithRequestContext } from "@agent-native/core/server";
|
|
2
|
+
import {
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
readMultipartFormData,
|
|
5
|
+
setResponseStatus,
|
|
6
|
+
} from "h3";
|
|
7
|
+
|
|
8
|
+
import { getGoogleDocsAccessToken } from "../../../lib/google-docs-oauth.js";
|
|
9
|
+
|
|
10
|
+
const PPTX_CONTENT_TYPE =
|
|
11
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
12
|
+
const GOOGLE_SLIDES_MIME = "application/vnd.google-apps.presentation";
|
|
13
|
+
const UPLOAD_URL =
|
|
14
|
+
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Uploads a browser-generated PPTX into the user's Drive, letting Drive convert
|
|
18
|
+
* it to a native Google Slides deck. The PPTX comes from the client because the
|
|
19
|
+
* browser export renders the real slide DOM — the server-side pptxgenjs export
|
|
20
|
+
* is a lower-fidelity fallback and would ship a visibly worse deck to Google.
|
|
21
|
+
*/
|
|
22
|
+
export default defineEventHandler(async (event) => {
|
|
23
|
+
const session = await getSession(event).catch(() => null);
|
|
24
|
+
if (!session?.email) {
|
|
25
|
+
setResponseStatus(event, 401);
|
|
26
|
+
return { error: "Unauthorized" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parts = (await readMultipartFormData(event)) ?? [];
|
|
30
|
+
const file = parts.find((part) => part.name === "file");
|
|
31
|
+
const titlePart = parts.find((part) => part.name === "title");
|
|
32
|
+
const title = titlePart
|
|
33
|
+
? new TextDecoder().decode(titlePart.data).trim() || "Untitled deck"
|
|
34
|
+
: "Untitled deck";
|
|
35
|
+
|
|
36
|
+
if (!file?.data?.length) {
|
|
37
|
+
setResponseStatus(event, 400);
|
|
38
|
+
return { error: "file required" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Same request context the actions run in — Google's client credentials can
|
|
42
|
+
// be org-scoped vault secrets, and resolving them without the org reports the
|
|
43
|
+
// integration as unconfigured.
|
|
44
|
+
const account = await runWithRequestContext(
|
|
45
|
+
{ userEmail: session.email, orgId: session.orgId },
|
|
46
|
+
() => getGoogleDocsAccessToken(session.email),
|
|
47
|
+
);
|
|
48
|
+
if (!account) {
|
|
49
|
+
setResponseStatus(event, 409);
|
|
50
|
+
return {
|
|
51
|
+
error: "No connected Google account.",
|
|
52
|
+
code: "google-not-connected",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const boundary = `an-slides-${Math.random().toString(36).slice(2)}`;
|
|
57
|
+
const body = new Blob([
|
|
58
|
+
`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n`,
|
|
59
|
+
JSON.stringify({ name: title, mimeType: GOOGLE_SLIDES_MIME }),
|
|
60
|
+
`\r\n--${boundary}\r\nContent-Type: ${PPTX_CONTENT_TYPE}\r\n\r\n`,
|
|
61
|
+
new Uint8Array(file.data),
|
|
62
|
+
`\r\n--${boundary}--`,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const response = await fetch(UPLOAD_URL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${account.accessToken}`,
|
|
69
|
+
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
70
|
+
},
|
|
71
|
+
body,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = (await response.json().catch(() => null)) as {
|
|
75
|
+
id?: string;
|
|
76
|
+
webViewLink?: string;
|
|
77
|
+
error?: { message?: string };
|
|
78
|
+
} | null;
|
|
79
|
+
|
|
80
|
+
if (!response.ok || !result?.webViewLink) {
|
|
81
|
+
setResponseStatus(event, 502);
|
|
82
|
+
return {
|
|
83
|
+
error:
|
|
84
|
+
result?.error?.message ??
|
|
85
|
+
`Google Drive returned HTTP ${response.status} while creating the deck.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { url: result.webViewLink, accountEmail: account.accountEmail };
|
|
90
|
+
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @agent-native/toolkit
|
|
2
2
|
|
|
3
|
+
## 0.10.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cbc6936: Show only the connect actions in the composer model picker when no LLM provider is configured, instead of a list of unpickable "needs API key" models, and surface Builder connect failures instead of leaving the "Connect Builder.io" button looking dead when the popup is blocked.
|
|
8
|
+
|
|
3
9
|
## 0.10.8
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|