@agent-native/core 0.155.0 → 0.157.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/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
- package/corpus/templates/assets/app/i18n-data.ts +21 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +17 -13
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +102 -59
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +15 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +16 -2
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +42 -2
- package/corpus/templates/slides/app/global.css +7 -40
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +130 -45
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +30 -0
- package/dist/agent/production-agent.js +100 -8
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/AgentPanel.js +12 -9
- package/dist/client/AssistantChat.d.ts +15 -1
- package/dist/client/AssistantChat.js +110 -43
- package/dist/client/MultiTabAssistantChat.js +44 -27
- package/dist/client/chat/markdown-renderer.js +4 -2
- package/dist/client/chat/message-components.d.ts +2 -2
- package/dist/client/chat/message-components.js +69 -35
- package/dist/client/chat/run-recovery.d.ts +4 -1
- package/dist/client/chat/run-recovery.js +62 -30
- package/dist/client/chat/tool-call-display.d.ts +12 -3
- package/dist/client/chat/tool-call-display.js +79 -40
- package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
- package/dist/client/chat/widgets/DataChartWidget.js +8 -2
- package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
- package/dist/client/chat/widgets/DataTableWidget.js +23 -9
- package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
- package/dist/client/components/ui/message-scroller.d.ts +1 -1
- package/dist/client/components/ui/message-scroller.js +7 -3
- package/dist/client/composer/runtime-adapters.js +3 -2
- package/dist/client/context-xray/ContextMeter.js +5 -1
- package/dist/client/context-xray/ContextXRayPanel.js +5 -5
- package/dist/client/error-format.d.ts +4 -0
- package/dist/client/error-format.js +93 -5
- package/dist/client/i18n.js +83 -17
- package/dist/client/observability/ThumbsFeedback.js +10 -8
- package/dist/client/sharing/ShareButton.js +160 -54
- package/dist/coding-tools/run-code.js +2 -1
- package/dist/coding-tools/sandbox/background.d.ts +1 -0
- package/dist/coding-tools/sandbox/background.js +8 -1
- package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
- package/dist/coding-tools/sandbox/executions-store.js +41 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.d.ts +3 -0
- package/dist/localization/core-messages/ar-SA.js +541 -0
- package/dist/localization/core-messages/de-DE.d.ts +3 -0
- package/dist/localization/core-messages/de-DE.js +513 -0
- package/dist/localization/core-messages/en-US.d.ts +513 -0
- package/dist/localization/core-messages/en-US.js +513 -0
- package/dist/localization/core-messages/es-ES.d.ts +3 -0
- package/dist/localization/core-messages/es-ES.js +520 -0
- package/dist/localization/core-messages/fr-FR.d.ts +3 -0
- package/dist/localization/core-messages/fr-FR.js +520 -0
- package/dist/localization/core-messages/hi-IN.d.ts +3 -0
- package/dist/localization/core-messages/hi-IN.js +513 -0
- package/dist/localization/core-messages/ja-JP.d.ts +3 -0
- package/dist/localization/core-messages/ja-JP.js +506 -0
- package/dist/localization/core-messages/ko-KR.d.ts +3 -0
- package/dist/localization/core-messages/ko-KR.js +506 -0
- package/dist/localization/core-messages/pt-BR.d.ts +3 -0
- package/dist/localization/core-messages/pt-BR.js +520 -0
- package/dist/localization/core-messages/zh-CN.d.ts +3 -0
- package/dist/localization/core-messages/zh-CN.js +506 -0
- package/dist/localization/core-messages/zh-TW.d.ts +3 -0
- package/dist/localization/core-messages/zh-TW.js +506 -0
- package/dist/localization/core-messages.d.ts +524 -0
- package/dist/localization/core-messages.js +124 -0
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +2 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/plugin-options.d.ts +7 -2
- package/dist/server/agent-chat-plugin.d.ts +10 -0
- package/dist/server/agent-chat-plugin.js +144 -41
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +7 -1
- package/dist/server/agent-run-context.js +9 -2
- package/dist/server/agent-teams-run-queue.d.ts +3 -0
- package/dist/server/agent-teams.js +38 -9
- package/dist/server/index.d.ts +1 -1
- package/dist/server/request-context.d.ts +7 -0
- package/dist/server/request-context.js +15 -0
- package/docs/content/agent-surfaces.mdx +30 -0
- package/package.json +2 -2
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { appBasePath } from "@agent-native/core/client/api-path";
|
|
2
2
|
import { PromptComposer } from "@agent-native/core/client/composer";
|
|
3
3
|
import { useT } from "@agent-native/core/client/i18n";
|
|
4
|
-
import { IconCopy, IconSquarePlus } from "@tabler/icons-react";
|
|
5
|
-
import {
|
|
4
|
+
import { IconCopy, IconSquarePlus, IconX } from "@tabler/icons-react";
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
6
12
|
import { createPortal } from "react-dom";
|
|
7
13
|
import { toast } from "sonner";
|
|
8
14
|
|
|
@@ -66,6 +72,8 @@ export function AddSlidePopover({
|
|
|
66
72
|
agentSubmit,
|
|
67
73
|
onDuplicateCurrent,
|
|
68
74
|
onAddEmpty,
|
|
75
|
+
placement = "below",
|
|
76
|
+
targetSlideId,
|
|
69
77
|
}: {
|
|
70
78
|
open: boolean;
|
|
71
79
|
onOpenChange: (open: boolean) => void;
|
|
@@ -78,11 +86,40 @@ export function AddSlidePopover({
|
|
|
78
86
|
agentSubmit: (message: string, context: string) => void;
|
|
79
87
|
onDuplicateCurrent?: () => void;
|
|
80
88
|
onAddEmpty?: () => void;
|
|
89
|
+
/** "below" anchors under the trigger button; "right" sits beside a slide thumbnail. */
|
|
90
|
+
placement?: "below" | "right";
|
|
91
|
+
/** Id of a blank slide already inserted — the agent fills it in instead of
|
|
92
|
+
* inserting another one. Used when this popover follows a "New slide"
|
|
93
|
+
* click that already created the placeholder. */
|
|
94
|
+
targetSlideId?: string;
|
|
81
95
|
}) {
|
|
82
96
|
const t = useT();
|
|
83
97
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
84
98
|
const [promptText, setPromptText] = useState("");
|
|
85
99
|
const [googleDocContext, setGoogleDocContext] = useState("");
|
|
100
|
+
// Estimate before the panel has painted so the first frame doesn't hang
|
|
101
|
+
// off the bottom of the viewport; corrected once the real height is known.
|
|
102
|
+
const [panelHeight, setPanelHeight] = useState(320);
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
if (!open || !panelRef.current) return;
|
|
106
|
+
setPanelHeight(panelRef.current.getBoundingClientRect().height);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Content can grow after the first paint (Google Doc hint, file chips,
|
|
110
|
+
// an auto-growing textarea) without necessarily triggering a React
|
|
111
|
+
// re-render. Watch the panel directly so it keeps clamping to the
|
|
112
|
+
// viewport as it resizes, not just on the frame it first opens.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!open || !panelRef.current) return;
|
|
115
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
116
|
+
setPanelHeight(
|
|
117
|
+
entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
observer.observe(panelRef.current);
|
|
121
|
+
return () => observer.disconnect();
|
|
122
|
+
}, [open]);
|
|
86
123
|
|
|
87
124
|
useEffect(() => {
|
|
88
125
|
if (!open) return;
|
|
@@ -141,25 +178,39 @@ export function AddSlidePopover({
|
|
|
141
178
|
const googleDocSourceForContext =
|
|
142
179
|
truncateSourceForContext(googleDocContext);
|
|
143
180
|
const fileContext = describeUploadedFilesForAgent(uploaded, deckId);
|
|
144
|
-
const context =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
const context = targetSlideId
|
|
182
|
+
? [
|
|
183
|
+
`Fill in slide ${activeSlideIndex + 1} of ${slideCount} (id: ${targetSlideId}) in deck "${deckTitle}" (id: ${deckId}).`,
|
|
184
|
+
"This slide already exists as a blank placeholder that the user just inserted — update it with `update-slide`, do not call `add-slide` for it.",
|
|
185
|
+
"The visible user message above contains the user's request and/or pasted source material for this slide. Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
|
|
186
|
+
googleDocSourceForContext.text,
|
|
187
|
+
googleDocSourceForContext.truncated
|
|
188
|
+
? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
|
|
189
|
+
: "",
|
|
190
|
+
fileContext,
|
|
191
|
+
"",
|
|
192
|
+
"Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep the slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
|
|
193
|
+
"If the user asked for more than one slide's worth of content, update this slide with the first one, then call `add-slide` for the rest, positioned starting right after this slide.",
|
|
194
|
+
].join("\n")
|
|
195
|
+
: [
|
|
196
|
+
`Add a new slide to deck "${deckTitle}" (id: ${deckId}).`,
|
|
197
|
+
`Insert after slide ${activeSlideIndex + 1} of ${slideCount} (active slide id: ${activeSlideId}).`,
|
|
198
|
+
"The visible user message above contains the user's request and/or pasted source material for the new slide(s). Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
|
|
199
|
+
googleDocSourceForContext.text,
|
|
200
|
+
googleDocSourceForContext.truncated
|
|
201
|
+
? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
|
|
202
|
+
: "",
|
|
203
|
+
fileContext,
|
|
204
|
+
"",
|
|
205
|
+
"Create the slide content and insert it at the correct position using `add-slide` with --deckId=" +
|
|
206
|
+
deckId +
|
|
207
|
+
".",
|
|
208
|
+
"Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep each slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
|
|
209
|
+
"If the user asked for multiple slides, call `add-slide` once per slide. Use positions starting at " +
|
|
210
|
+
(activeSlideIndex + 1) +
|
|
211
|
+
" so the new slides land after the active slide in order.",
|
|
212
|
+
"For larger requests, keep adding slides sequentially: wait for each add-slide result, then call add-slide for the next slide. Start slide 1 immediately; do not wait to design the entire sequence before adding it.",
|
|
213
|
+
].join("\n");
|
|
163
214
|
|
|
164
215
|
agentSubmit(addSlideAgentMessage(trimmedText), context);
|
|
165
216
|
onOpenChange(false);
|
|
@@ -173,6 +224,7 @@ export function AddSlidePopover({
|
|
|
173
224
|
googleDocContext,
|
|
174
225
|
onOpenChange,
|
|
175
226
|
slideCount,
|
|
227
|
+
targetSlideId,
|
|
176
228
|
],
|
|
177
229
|
);
|
|
178
230
|
|
|
@@ -187,23 +239,40 @@ export function AddSlidePopover({
|
|
|
187
239
|
|
|
188
240
|
const rect = anchorRef.current.getBoundingClientRect();
|
|
189
241
|
const panelWidth = Math.min(420, window.innerWidth - 24);
|
|
190
|
-
const left =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
const left =
|
|
243
|
+
placement === "right"
|
|
244
|
+
? Math.min(rect.right + 8, window.innerWidth - panelWidth - 12)
|
|
245
|
+
: Math.max(12, Math.min(rect.left, window.innerWidth - panelWidth - 12));
|
|
246
|
+
const top =
|
|
247
|
+
placement === "right"
|
|
248
|
+
? Math.max(12, Math.min(rect.top, window.innerHeight - panelHeight - 12))
|
|
249
|
+
: rect.bottom + 8;
|
|
194
250
|
|
|
195
251
|
return createPortal(
|
|
196
252
|
<div
|
|
197
253
|
ref={panelRef}
|
|
254
|
+
data-add-slide-popover
|
|
198
255
|
className="fixed w-[min(420px,calc(100vw-24px))] rounded-xl border border-border bg-popover shadow-2xl shadow-black/60 z-[200] p-3"
|
|
199
256
|
style={{
|
|
200
|
-
top
|
|
257
|
+
top,
|
|
201
258
|
left,
|
|
202
259
|
}}
|
|
203
260
|
>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
261
|
+
<div className="flex items-center justify-between px-1 pb-2">
|
|
262
|
+
<p className="text-sm font-medium text-foreground/90">
|
|
263
|
+
{targetSlideId
|
|
264
|
+
? t("editorSidebar.describeThisSlide")
|
|
265
|
+
: t("editorSidebar.addSlides")}
|
|
266
|
+
</p>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => onOpenChange(false)}
|
|
270
|
+
aria-label={t("editorSidebar.closeAddSlides")}
|
|
271
|
+
className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground/70"
|
|
272
|
+
>
|
|
273
|
+
<IconX className="size-3.5" />
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
207
276
|
{(onAddEmpty || (onDuplicateCurrent && slideCount > 0)) && (
|
|
208
277
|
<>
|
|
209
278
|
{onAddEmpty && (
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
import {
|
|
3
|
-
IconLoader2,
|
|
4
3
|
IconPlus,
|
|
5
4
|
IconTextSize,
|
|
6
5
|
IconTransitionRight,
|
|
7
6
|
} from "@tabler/icons-react";
|
|
8
|
-
import { useEffect, useRef, useState } from "react";
|
|
9
7
|
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
10
9
|
import {
|
|
11
10
|
DropdownMenu,
|
|
12
11
|
DropdownMenuContent,
|
|
@@ -19,11 +18,8 @@ import {
|
|
|
19
18
|
TooltipTrigger,
|
|
20
19
|
} from "@/components/ui/tooltip";
|
|
21
20
|
import type { Slide } from "@/context/DeckContext";
|
|
22
|
-
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
23
21
|
import { cn } from "@/lib/utils";
|
|
24
22
|
|
|
25
|
-
import { AddSlidePopover } from "./AddSlidePopover";
|
|
26
|
-
|
|
27
23
|
const BUTTON_CLASS =
|
|
28
24
|
"inline-flex size-7 flex-shrink-0 items-center justify-center rounded-md transition-colors";
|
|
29
25
|
const IDLE_CLASS =
|
|
@@ -41,121 +37,79 @@ const TRANSITIONS: { value: SlideTransition; labelKey: string }[] = [
|
|
|
41
37
|
];
|
|
42
38
|
|
|
43
39
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
* Selection-independent actions pinned to the head of the contextual
|
|
41
|
+
* toolbar: add-slide, add-text-box, and the slide transition picker.
|
|
42
|
+
* Rendered both as the `leading` slot of the element-controls row and as a
|
|
43
|
+
* fallback directly in the deck toolbar for when that row is hidden (narrow
|
|
44
|
+
* viewports) or never mounts (no current slide, e.g. an empty deck).
|
|
47
45
|
*/
|
|
48
46
|
export function EditorActionCluster({
|
|
49
|
-
deckId,
|
|
50
|
-
deckTitle,
|
|
51
|
-
currentSlideId,
|
|
52
|
-
slideCount,
|
|
53
|
-
currentSlideIndex,
|
|
54
|
-
addSlideGenerating = false,
|
|
55
|
-
onAddSlideGeneratingChange,
|
|
56
|
-
onAddEmptySlide,
|
|
57
|
-
onDuplicateCurrentSlide,
|
|
58
47
|
textBoxMode,
|
|
59
48
|
onToggleTextBoxMode,
|
|
49
|
+
onAddEmptySlide,
|
|
50
|
+
addSlideGenerating,
|
|
51
|
+
currentSlideId,
|
|
60
52
|
slideTransition,
|
|
61
53
|
onChangeSlideTransition,
|
|
62
54
|
className,
|
|
63
55
|
}: {
|
|
64
|
-
deckId: string;
|
|
65
|
-
deckTitle: string;
|
|
66
|
-
currentSlideId?: string;
|
|
67
|
-
slideCount: number;
|
|
68
|
-
currentSlideIndex: number;
|
|
69
|
-
addSlideGenerating?: boolean;
|
|
70
|
-
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
71
|
-
onAddEmptySlide?: () => void;
|
|
72
|
-
onDuplicateCurrentSlide?: () => void;
|
|
73
56
|
textBoxMode?: boolean;
|
|
74
57
|
onToggleTextBoxMode?: () => void;
|
|
58
|
+
onAddEmptySlide?: () => void;
|
|
59
|
+
addSlideGenerating?: boolean;
|
|
60
|
+
currentSlideId?: string;
|
|
75
61
|
slideTransition?: Slide["transition"];
|
|
76
62
|
onChangeSlideTransition?: (transition: SlideTransition) => void;
|
|
77
63
|
className?: string;
|
|
78
64
|
}) {
|
|
79
65
|
const t = useT();
|
|
80
|
-
const { generating, submit: agentSubmit } = useAgentGenerating();
|
|
81
|
-
const [addSlideOpen, setAddSlideOpen] = useState(false);
|
|
82
|
-
const addSlideRef = useRef<HTMLButtonElement>(null);
|
|
83
66
|
// "none" is a legacy alias the presentation view already treats as instant.
|
|
84
67
|
const activeTransition: SlideTransition =
|
|
85
68
|
!slideTransition || slideTransition === "none"
|
|
86
69
|
? "instant"
|
|
87
70
|
: slideTransition;
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
if (!generating) onAddSlideGeneratingChange?.(false);
|
|
91
|
-
}, [generating, onAddSlideGeneratingChange]);
|
|
72
|
+
if (!onToggleTextBoxMode && !onAddEmptySlide) return null;
|
|
92
73
|
|
|
93
74
|
return (
|
|
94
75
|
<div className={cn("flex items-center gap-1", className)}>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
ref={addSlideRef}
|
|
76
|
+
{onAddEmptySlide && (
|
|
77
|
+
<>
|
|
78
|
+
<Button
|
|
99
79
|
type="button"
|
|
100
|
-
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="sm"
|
|
82
|
+
className="h-7 shrink-0 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
83
|
+
onClick={onAddEmptySlide}
|
|
101
84
|
disabled={addSlideGenerating}
|
|
102
|
-
className={cn(
|
|
103
|
-
BUTTON_CLASS,
|
|
104
|
-
addSlideOpen ? ACTIVE_CLASS : IDLE_CLASS,
|
|
105
|
-
)}
|
|
106
|
-
aria-label={t("editorSidebar.addSlides")}
|
|
107
85
|
>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)}
|
|
113
|
-
</button>
|
|
114
|
-
</TooltipTrigger>
|
|
115
|
-
<TooltipContent>{t("editorSidebar.addSlides")}</TooltipContent>
|
|
116
|
-
</Tooltip>
|
|
117
|
-
<AddSlidePopover
|
|
118
|
-
open={addSlideOpen}
|
|
119
|
-
onOpenChange={setAddSlideOpen}
|
|
120
|
-
anchorRef={addSlideRef}
|
|
121
|
-
deckId={deckId}
|
|
122
|
-
deckTitle={deckTitle}
|
|
123
|
-
activeSlideId={currentSlideId ?? ""}
|
|
124
|
-
slideCount={slideCount}
|
|
125
|
-
activeSlideIndex={currentSlideIndex}
|
|
126
|
-
agentSubmit={(message, context) => {
|
|
127
|
-
onAddSlideGeneratingChange?.(true);
|
|
128
|
-
agentSubmit(message, context);
|
|
129
|
-
}}
|
|
130
|
-
onDuplicateCurrent={onDuplicateCurrentSlide}
|
|
131
|
-
onAddEmpty={onAddEmptySlide}
|
|
132
|
-
/>
|
|
133
|
-
|
|
134
|
-
{onToggleTextBoxMode && (
|
|
135
|
-
<>
|
|
136
|
-
<div className={DIVIDER_CLASS} />
|
|
137
|
-
<Tooltip>
|
|
138
|
-
<TooltipTrigger asChild>
|
|
139
|
-
<button
|
|
140
|
-
type="button"
|
|
141
|
-
onClick={onToggleTextBoxMode}
|
|
142
|
-
data-toolbar-textbox-button
|
|
143
|
-
aria-label={t("editorToolbar.addTextBox")}
|
|
144
|
-
aria-pressed={textBoxMode}
|
|
145
|
-
aria-keyshortcuts="T"
|
|
146
|
-
className={cn(
|
|
147
|
-
BUTTON_CLASS,
|
|
148
|
-
textBoxMode ? ACTIVE_CLASS : IDLE_CLASS,
|
|
149
|
-
)}
|
|
150
|
-
>
|
|
151
|
-
<IconTextSize className="size-4" />
|
|
152
|
-
</button>
|
|
153
|
-
</TooltipTrigger>
|
|
154
|
-
<TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
|
|
155
|
-
</Tooltip>
|
|
86
|
+
<IconPlus className="size-3.5" />
|
|
87
|
+
{t("editorSidebar.newSlide")}
|
|
88
|
+
</Button>
|
|
89
|
+
{onToggleTextBoxMode && <div className={DIVIDER_CLASS} />}
|
|
156
90
|
</>
|
|
157
91
|
)}
|
|
158
|
-
|
|
92
|
+
{onToggleTextBoxMode && (
|
|
93
|
+
<Tooltip>
|
|
94
|
+
<TooltipTrigger asChild>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onToggleTextBoxMode}
|
|
98
|
+
data-toolbar-textbox-button
|
|
99
|
+
aria-label={t("editorToolbar.addTextBox")}
|
|
100
|
+
aria-pressed={textBoxMode}
|
|
101
|
+
aria-keyshortcuts="T"
|
|
102
|
+
className={cn(
|
|
103
|
+
BUTTON_CLASS,
|
|
104
|
+
textBoxMode ? ACTIVE_CLASS : IDLE_CLASS,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<IconTextSize className="size-4" />
|
|
108
|
+
</button>
|
|
109
|
+
</TooltipTrigger>
|
|
110
|
+
<TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
)}
|
|
159
113
|
{onChangeSlideTransition && currentSlideId && (
|
|
160
114
|
<>
|
|
161
115
|
<div className={DIVIDER_CLASS} />
|
|
@@ -18,9 +18,11 @@ import { CSS } from "@dnd-kit/utilities";
|
|
|
18
18
|
import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
|
|
19
19
|
import { hashSlideContent, type DeckFitState } from "@shared/slide-fit";
|
|
20
20
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { toast } from "sonner";
|
|
21
22
|
|
|
22
23
|
import SlideRenderer from "@/components/deck/SlideRenderer";
|
|
23
24
|
import type { SlideOverflowInfo } from "@/components/deck/SlideRenderer";
|
|
25
|
+
import { AddSlidePopover } from "@/components/editor/AddSlidePopover";
|
|
24
26
|
import { AiEditingMarker } from "@/components/editor/AiEditingMarker";
|
|
25
27
|
import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
|
|
26
28
|
import {
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
TooltipContent,
|
|
29
31
|
TooltipTrigger,
|
|
30
32
|
} from "@/components/ui/tooltip";
|
|
31
|
-
import type
|
|
33
|
+
import { defaultSlideContent, type Slide } from "@/context/DeckContext";
|
|
32
34
|
import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
|
|
33
35
|
import { TAB_ID } from "@/lib/tab-id";
|
|
34
36
|
|
|
@@ -39,6 +41,7 @@ interface EditorSidebarProps {
|
|
|
39
41
|
slides: Slide[];
|
|
40
42
|
activeSlideId: string;
|
|
41
43
|
deckId: string;
|
|
44
|
+
deckTitle: string;
|
|
42
45
|
onSelectSlide: (id: string) => void;
|
|
43
46
|
/** Viewer-role decks get thumbnails only: no add, duplicate, or delete. */
|
|
44
47
|
readOnly?: boolean;
|
|
@@ -54,6 +57,27 @@ interface EditorSidebarProps {
|
|
|
54
57
|
generatingSlide?: { index: number };
|
|
55
58
|
generatingSlideSelected?: boolean;
|
|
56
59
|
onSelectGeneratingSlide?: () => void;
|
|
60
|
+
/** The slide just inserted via the toolbar's New Slide button — the rail
|
|
61
|
+
* anchors the "describe this slide" popover to that slide's thumbnail
|
|
62
|
+
* once it mounts. Owned by the parent since the button that sets it now
|
|
63
|
+
* lives in the toolbar, outside this component. */
|
|
64
|
+
describeSlideId: string | null;
|
|
65
|
+
/** Clears `describeSlideId` in the parent when the popover closes. */
|
|
66
|
+
onCloseDescribe: () => void;
|
|
67
|
+
/** Reports add-slide generation state up so the toolbar's New Slide button
|
|
68
|
+
* can disable itself while a request is in flight. */
|
|
69
|
+
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
70
|
+
/** Resolves once a just-inserted blank slide has actually reached the
|
|
71
|
+
* server, so the agent's update-slide request can't race the add-slide
|
|
72
|
+
* persistence. */
|
|
73
|
+
onAwaitAddSlidePersisted?: () => Promise<void>;
|
|
74
|
+
/** Removes a blank placeholder slide whose persistence ultimately failed,
|
|
75
|
+
* so a flaky save doesn't leave a stray empty slide in the deck. */
|
|
76
|
+
onRemoveFailedSlide?: (slideId: string) => void;
|
|
77
|
+
/** Submits the add-slide agent request. Owned by the parent (rather than
|
|
78
|
+
* this component's own useAgentGenerating() call) so the run stays
|
|
79
|
+
* correctly scoped and trackable across a sidebar remount. */
|
|
80
|
+
addSlideAgentSubmit: (message: string, context: string) => void;
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
const DECK_FIT_STATE_KEYS = [
|
|
@@ -312,6 +336,7 @@ export default function EditorSidebar({
|
|
|
312
336
|
slides,
|
|
313
337
|
activeSlideId,
|
|
314
338
|
deckId,
|
|
339
|
+
deckTitle,
|
|
315
340
|
onSelectSlide,
|
|
316
341
|
readOnly = false,
|
|
317
342
|
slidePresence,
|
|
@@ -321,7 +346,16 @@ export default function EditorSidebar({
|
|
|
321
346
|
generatingSlide,
|
|
322
347
|
generatingSlideSelected = false,
|
|
323
348
|
onSelectGeneratingSlide,
|
|
349
|
+
describeSlideId,
|
|
350
|
+
onCloseDescribe,
|
|
351
|
+
onAddSlideGeneratingChange,
|
|
352
|
+
onAwaitAddSlidePersisted,
|
|
353
|
+
onRemoveFailedSlide,
|
|
354
|
+
addSlideAgentSubmit,
|
|
324
355
|
}: EditorSidebarProps) {
|
|
356
|
+
const t = useT();
|
|
357
|
+
const [describeAnchorEl, setDescribeAnchorEl] =
|
|
358
|
+
useState<HTMLButtonElement | null>(null);
|
|
325
359
|
const [thumbnailListScrolled, setThumbnailListScrolled] = useState(false);
|
|
326
360
|
const slideButtonRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
327
361
|
const measurementsRef = useRef(
|
|
@@ -331,6 +365,7 @@ export default function EditorSidebar({
|
|
|
331
365
|
>(),
|
|
332
366
|
);
|
|
333
367
|
const writeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
368
|
+
|
|
334
369
|
const aiEditedSlideIds = new Set(
|
|
335
370
|
(recentEdits ?? [])
|
|
336
371
|
.filter((edit) => edit.isAgent)
|
|
@@ -398,6 +433,7 @@ export default function EditorSidebar({
|
|
|
398
433
|
}, [deckId, aspectRatio]);
|
|
399
434
|
|
|
400
435
|
useEffect(() => {
|
|
436
|
+
setDescribeAnchorEl(null);
|
|
401
437
|
setThumbnailListScrolled(false);
|
|
402
438
|
}, [deckId]);
|
|
403
439
|
|
|
@@ -421,10 +457,22 @@ export default function EditorSidebar({
|
|
|
421
457
|
} else {
|
|
422
458
|
slideButtonRefs.current.delete(slideId);
|
|
423
459
|
}
|
|
460
|
+
// The new-slide prompt anchors to the just-created slide's thumbnail,
|
|
461
|
+
// which doesn't exist yet at click time — pick it up as soon as it mounts.
|
|
462
|
+
// Center it in the scroll area first so the prompt has room on-screen
|
|
463
|
+
// instead of opening off the bottom edge when the new slide lands there.
|
|
464
|
+
if (node && slideId === describeSlideId) {
|
|
465
|
+
node.scrollIntoView({ block: "center" });
|
|
466
|
+
setDescribeAnchorEl(node);
|
|
467
|
+
}
|
|
424
468
|
},
|
|
425
|
-
[],
|
|
469
|
+
[describeSlideId],
|
|
426
470
|
);
|
|
427
471
|
|
|
472
|
+
const describeSlideIndex = describeSlideId
|
|
473
|
+
? slides.findIndex((s) => s.id === describeSlideId)
|
|
474
|
+
: -1;
|
|
475
|
+
|
|
428
476
|
// Arrow key navigation for slides
|
|
429
477
|
useEffect(() => {
|
|
430
478
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -518,6 +566,50 @@ export default function EditorSidebar({
|
|
|
518
566
|
)}
|
|
519
567
|
</div>
|
|
520
568
|
</div>
|
|
569
|
+
{describeSlideId && describeSlideIndex !== -1 && describeAnchorEl && (
|
|
570
|
+
<AddSlidePopover
|
|
571
|
+
open
|
|
572
|
+
onOpenChange={(open) => {
|
|
573
|
+
if (!open) {
|
|
574
|
+
onCloseDescribe();
|
|
575
|
+
setDescribeAnchorEl(null);
|
|
576
|
+
}
|
|
577
|
+
}}
|
|
578
|
+
anchorRef={{ current: describeAnchorEl }}
|
|
579
|
+
placement="right"
|
|
580
|
+
deckId={deckId}
|
|
581
|
+
deckTitle={deckTitle}
|
|
582
|
+
activeSlideId={describeSlideId}
|
|
583
|
+
activeSlideIndex={describeSlideIndex}
|
|
584
|
+
slideCount={slides.length}
|
|
585
|
+
targetSlideId={describeSlideId}
|
|
586
|
+
agentSubmit={async (message, context) => {
|
|
587
|
+
onAddSlideGeneratingChange?.(true);
|
|
588
|
+
try {
|
|
589
|
+
await onAwaitAddSlidePersisted?.();
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error("Failed to persist new slide:", error);
|
|
592
|
+
onAddSlideGeneratingChange?.(false);
|
|
593
|
+
// The popover already closed (AddSlidePopover doesn't wait on
|
|
594
|
+
// this async callback), so the typed prompt is gone either
|
|
595
|
+
// way. Only remove the placeholder if it's still untouched —
|
|
596
|
+
// the save retries take long enough that the user could have
|
|
597
|
+
// started editing it directly on the canvas in the meantime,
|
|
598
|
+
// and deleting it would destroy that work.
|
|
599
|
+
const current = slides.find((s) => s.id === describeSlideId);
|
|
600
|
+
if (
|
|
601
|
+
current?.content === defaultSlideContent.blank &&
|
|
602
|
+
!current.notes
|
|
603
|
+
) {
|
|
604
|
+
onRemoveFailedSlide?.(describeSlideId);
|
|
605
|
+
}
|
|
606
|
+
toast.error(t("editorSidebar.newSlideSaveFailed"));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
addSlideAgentSubmit(message, context);
|
|
610
|
+
}}
|
|
611
|
+
/>
|
|
612
|
+
)}
|
|
521
613
|
</div>
|
|
522
614
|
);
|
|
523
615
|
}
|
|
@@ -76,7 +76,6 @@ interface EditorToolbarProps {
|
|
|
76
76
|
/** Whether the user may create and manage comments without editing slides. */
|
|
77
77
|
canComment?: boolean;
|
|
78
78
|
onTitleChange: (title: string) => void;
|
|
79
|
-
slideCount: number;
|
|
80
79
|
currentSlideIndex: number;
|
|
81
80
|
sidebarOpen: boolean;
|
|
82
81
|
onToggleSidebar: () => void;
|
|
@@ -132,16 +131,13 @@ interface EditorToolbarProps {
|
|
|
132
131
|
onExportPptx?: () => Promise<void> | void;
|
|
133
132
|
/** Create the deck in the user's Google Drive as native Google Slides */
|
|
134
133
|
onExportGoogleSlides?: () => Promise<GoogleSlidesExportResult>;
|
|
135
|
-
/**
|
|
134
|
+
/** Inserts a blank slide directly below the active slide. Threaded through
|
|
135
|
+
* to the fallback action cluster below so an empty deck (no current
|
|
136
|
+
* slide, so the primary element-controls toolbar never mounts) still has
|
|
137
|
+
* a way to add its first slide. */
|
|
136
138
|
onAddEmptySlide?: () => void;
|
|
137
|
-
/**
|
|
138
|
-
onDuplicateCurrentSlide?: () => void;
|
|
139
|
-
/** Id of the current slide, so an agent add-slide lands in the right place */
|
|
140
|
-
currentSlideId?: string;
|
|
141
|
-
/** True while an agent add-slide request is in flight */
|
|
139
|
+
/** True while an agent add-slide request is in flight. */
|
|
142
140
|
addSlideGenerating?: boolean;
|
|
143
|
-
/** Called when an agent add-slide request is submitted */
|
|
144
|
-
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
const TOOLBAR_ICON_BUTTON_CLASS =
|
|
@@ -152,7 +148,6 @@ export default function EditorToolbar({
|
|
|
152
148
|
deckId,
|
|
153
149
|
deckTitle,
|
|
154
150
|
onTitleChange,
|
|
155
|
-
slideCount,
|
|
156
151
|
currentSlideIndex,
|
|
157
152
|
sidebarOpen,
|
|
158
153
|
onToggleSidebar,
|
|
@@ -185,10 +180,7 @@ export default function EditorToolbar({
|
|
|
185
180
|
onExportPptx,
|
|
186
181
|
onExportGoogleSlides,
|
|
187
182
|
onAddEmptySlide,
|
|
188
|
-
|
|
189
|
-
currentSlideId,
|
|
190
|
-
addSlideGenerating = false,
|
|
191
|
-
onAddSlideGeneratingChange,
|
|
183
|
+
addSlideGenerating,
|
|
192
184
|
canEdit = true,
|
|
193
185
|
canComment = canEdit,
|
|
194
186
|
}: EditorToolbarProps) {
|
|
@@ -523,7 +515,7 @@ export default function EditorToolbar({
|
|
|
523
515
|
useEffect(() => registerEditorCommands(() => editorCommandsRef.current), []);
|
|
524
516
|
|
|
525
517
|
return (
|
|
526
|
-
<div className="deck-editor-toolbar flex h-
|
|
518
|
+
<div className="deck-editor-toolbar flex h-14 shrink-0 items-center gap-1 overflow-x-auto whitespace-nowrap bg-background px-2 sm:px-3">
|
|
527
519
|
{/* Back button */}
|
|
528
520
|
<Tooltip>
|
|
529
521
|
<TooltipTrigger asChild>
|
|
@@ -554,23 +546,19 @@ export default function EditorToolbar({
|
|
|
554
546
|
<TooltipContent>{t("editorToolbar.toggleSlideList")}</TooltipContent>
|
|
555
547
|
</Tooltip>
|
|
556
548
|
|
|
557
|
-
{/*
|
|
558
|
-
* toolbar below
|
|
559
|
-
*
|
|
560
|
-
|
|
549
|
+
{/* New Slide and the text-box tool live at the head of the contextual
|
|
550
|
+
* toolbar below, which SlideEditor portals in at every viewport size
|
|
551
|
+
* (a wide inline row or a narrow standalone row) whenever there's a
|
|
552
|
+
* current slide. Render this fallback only when there isn't one — an
|
|
553
|
+
* empty deck — so those two rows never end up showing the same
|
|
554
|
+
* buttons twice. */}
|
|
555
|
+
{canEdit && !contextToolbarVisible && (
|
|
561
556
|
<EditorActionCluster
|
|
562
|
-
className={contextToolbarVisible ? "lg:hidden" : undefined}
|
|
563
|
-
deckId={deckId}
|
|
564
|
-
deckTitle={deckTitle}
|
|
565
|
-
currentSlideId={currentSlideId}
|
|
566
|
-
slideCount={slideCount}
|
|
567
|
-
currentSlideIndex={currentSlideIndex}
|
|
568
|
-
addSlideGenerating={addSlideGenerating}
|
|
569
|
-
onAddSlideGeneratingChange={onAddSlideGeneratingChange}
|
|
570
|
-
onAddEmptySlide={onAddEmptySlide}
|
|
571
|
-
onDuplicateCurrentSlide={onDuplicateCurrentSlide}
|
|
572
557
|
textBoxMode={textBoxMode}
|
|
573
558
|
onToggleTextBoxMode={onToggleTextBoxMode}
|
|
559
|
+
onAddEmptySlide={onAddEmptySlide}
|
|
560
|
+
addSlideGenerating={addSlideGenerating}
|
|
561
|
+
currentSlideId={currentSlide?.id}
|
|
574
562
|
slideTransition={currentSlide?.transition}
|
|
575
563
|
onChangeSlideTransition={onChangeSlideTransition}
|
|
576
564
|
/>
|