@agent-native/core 0.137.0 → 0.137.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
- package/corpus/templates/analytics/app/i18n-data.ts +38 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
- package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
- package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +258 -0
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +130 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +25 -337
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +73 -53
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +804 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +101 -45
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +7 -4
- package/corpus/templates/slides/app/components/editor/bullet-editing.ts +11 -3
- package/corpus/templates/slides/app/components/editor/commit-active-edit.ts +21 -0
- package/corpus/templates/slides/app/components/editor/list-editing.ts +219 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +0 -3
- package/corpus/templates/slides/app/components/editor/slide-style.ts +199 -0
- package/corpus/templates/slides/app/global.css +14 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -1
- package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +51 -22
- package/corpus/templates/slides/app/pages/Index.tsx +236 -181
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-moved-to-the-toolbar-and-the-slide-rail-is-now-mor.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-undo-redo-and-the-text-tool-now-lead-the-slide-too.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-slide-styling-now-appears-in-a-contextual-toolbar-above-the-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-01-the-slide-style-side-panel-is-retired-all-styling-now-lives-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-03-the-slide-toolbar-is-denser-swatch-only-colors-dropdowns-for.md +10 -0
- package/corpus/templates/slides/changelog/2026-08-04-bullet-and-numbered-list-buttons-in-the-slide-toolbar-conver.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-04-italic-and-underline-are-now-one-click-away-in-the-slide-too.md +6 -0
- package/dist/client/DefaultSpinner.d.ts +0 -12
- package/dist/client/DefaultSpinner.js +22 -2
- package/dist/client/onboarding/use-onboarding.js +3 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- package/package.json +2 -2
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +0 -763
|
@@ -27,14 +27,14 @@ import {
|
|
|
27
27
|
IconAdjustments,
|
|
28
28
|
IconPencilPlus,
|
|
29
29
|
IconPin,
|
|
30
|
-
IconLetterT,
|
|
31
30
|
IconTool,
|
|
32
31
|
IconDownload,
|
|
33
32
|
IconSun,
|
|
34
33
|
IconMoon,
|
|
35
34
|
IconDotsVertical,
|
|
36
|
-
IconPalette,
|
|
37
35
|
IconLoader2,
|
|
36
|
+
IconArrowBackUp,
|
|
37
|
+
IconArrowForwardUp,
|
|
38
38
|
} from "@tabler/icons-react";
|
|
39
39
|
import { useTheme } from "next-themes";
|
|
40
40
|
import { useState, useRef, useEffect } from "react";
|
|
@@ -57,7 +57,11 @@ import {
|
|
|
57
57
|
} from "@/components/ui/tooltip";
|
|
58
58
|
import { SaveStatusIndicator } from "@/components/visual-editor";
|
|
59
59
|
import type { Deck, Slide, SlideLayout } from "@/context/DeckContext";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
defaultSlideContent,
|
|
62
|
+
useDecks,
|
|
63
|
+
useSaveState,
|
|
64
|
+
} from "@/context/DeckContext";
|
|
61
65
|
import {
|
|
62
66
|
ASPECT_RATIO_VALUES,
|
|
63
67
|
type AspectRatio,
|
|
@@ -65,7 +69,10 @@ import {
|
|
|
65
69
|
} from "@/lib/aspect-ratios";
|
|
66
70
|
import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
|
|
67
71
|
import { parseUploadResponse } from "@/lib/upload-response";
|
|
72
|
+
import { shortcutLabel } from "@/lib/utils";
|
|
68
73
|
|
|
74
|
+
import { commitActiveEditThenRun } from "./commit-active-edit";
|
|
75
|
+
import { EditorActionCluster } from "./EditorActionCluster";
|
|
69
76
|
import { ExportMenu } from "./ExportMenu";
|
|
70
77
|
interface EditorToolbarProps {
|
|
71
78
|
deck: Deck;
|
|
@@ -99,10 +106,6 @@ interface EditorToolbarProps {
|
|
|
99
106
|
commentsOpen?: boolean;
|
|
100
107
|
/** Toggle the comments panel */
|
|
101
108
|
onToggleComments?: () => void;
|
|
102
|
-
/** Whether the style panel is open */
|
|
103
|
-
styleOpen?: boolean;
|
|
104
|
-
/** Toggle the style panel */
|
|
105
|
-
onToggleStyle?: () => void;
|
|
106
109
|
/** Number of unresolved comments on the current slide */
|
|
107
110
|
unresolvedCommentCount?: number;
|
|
108
111
|
/** Current user email for avatar display */
|
|
@@ -139,6 +142,16 @@ interface EditorToolbarProps {
|
|
|
139
142
|
aspectRatio?: AspectRatio;
|
|
140
143
|
/** Change the deck's aspect ratio */
|
|
141
144
|
onSetAspectRatio?: (ratio: AspectRatio) => void;
|
|
145
|
+
/** Insert a blank slide after the current one */
|
|
146
|
+
onAddEmptySlide?: () => void;
|
|
147
|
+
/** Duplicate the current slide */
|
|
148
|
+
onDuplicateCurrentSlide?: () => void;
|
|
149
|
+
/** Id of the current slide, so an agent add-slide lands in the right place */
|
|
150
|
+
currentSlideId?: string;
|
|
151
|
+
/** True while an agent add-slide request is in flight */
|
|
152
|
+
addSlideGenerating?: boolean;
|
|
153
|
+
/** Called when an agent add-slide request is submitted */
|
|
154
|
+
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
const slideLayoutOptions: { value: SlideLayout; labelKey: string }[] = [
|
|
@@ -249,8 +262,6 @@ export default function EditorToolbar({
|
|
|
249
262
|
agentActive,
|
|
250
263
|
commentsOpen,
|
|
251
264
|
onToggleComments,
|
|
252
|
-
styleOpen,
|
|
253
|
-
onToggleStyle,
|
|
254
265
|
unresolvedCommentCount = 0,
|
|
255
266
|
currentUserEmail,
|
|
256
267
|
animationsOpen,
|
|
@@ -269,6 +280,11 @@ export default function EditorToolbar({
|
|
|
269
280
|
onExportGoogleSlides,
|
|
270
281
|
aspectRatio,
|
|
271
282
|
onSetAspectRatio,
|
|
283
|
+
onAddEmptySlide,
|
|
284
|
+
onDuplicateCurrentSlide,
|
|
285
|
+
currentSlideId,
|
|
286
|
+
addSlideGenerating = false,
|
|
287
|
+
onAddSlideGeneratingChange,
|
|
272
288
|
canEdit = true,
|
|
273
289
|
}: EditorToolbarProps) {
|
|
274
290
|
const t = useT();
|
|
@@ -317,6 +333,11 @@ export default function EditorToolbar({
|
|
|
317
333
|
setLayoutOpen(false);
|
|
318
334
|
};
|
|
319
335
|
const [toolsOpen, setToolsOpen] = useState(false);
|
|
336
|
+
// The contextual toolbar hosts the action cluster whenever it is on screen.
|
|
337
|
+
// That row rides on SlideEditor, which only mounts for a real slide, so an
|
|
338
|
+
// empty deck must keep this fallback or it has no way to add one.
|
|
339
|
+
const contextToolbarVisible = canEdit && Boolean(currentSlide);
|
|
340
|
+
const { undo, redo, canUndo, canRedo } = useDecks();
|
|
320
341
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
321
342
|
const [importing, setImporting] = useState(false);
|
|
322
343
|
const { setTheme, resolvedTheme } = useTheme();
|
|
@@ -442,6 +463,26 @@ export default function EditorToolbar({
|
|
|
442
463
|
<TooltipContent>{t("editorToolbar.toggleSlideList")}</TooltipContent>
|
|
443
464
|
</Tooltip>
|
|
444
465
|
|
|
466
|
+
{/* Add slide and the text-box tool live at the head of the contextual
|
|
467
|
+
* toolbar below. That row is desktop-only and needs a slide to mount on,
|
|
468
|
+
* so keep a fallback here for narrow screens and empty decks. */}
|
|
469
|
+
{canEdit && (
|
|
470
|
+
<EditorActionCluster
|
|
471
|
+
className={contextToolbarVisible ? "lg:hidden" : undefined}
|
|
472
|
+
deckId={deckId}
|
|
473
|
+
deckTitle={deckTitle}
|
|
474
|
+
currentSlideId={currentSlideId}
|
|
475
|
+
slideCount={slideCount}
|
|
476
|
+
currentSlideIndex={currentSlideIndex}
|
|
477
|
+
addSlideGenerating={addSlideGenerating}
|
|
478
|
+
onAddSlideGeneratingChange={onAddSlideGeneratingChange}
|
|
479
|
+
onAddEmptySlide={onAddEmptySlide}
|
|
480
|
+
onDuplicateCurrentSlide={onDuplicateCurrentSlide}
|
|
481
|
+
textBoxMode={textBoxMode}
|
|
482
|
+
onToggleTextBoxMode={onToggleTextBoxMode}
|
|
483
|
+
/>
|
|
484
|
+
)}
|
|
485
|
+
|
|
445
486
|
{/* Deck title */}
|
|
446
487
|
<input
|
|
447
488
|
type="text"
|
|
@@ -745,29 +786,6 @@ graph TD
|
|
|
745
786
|
</>
|
|
746
787
|
)}
|
|
747
788
|
|
|
748
|
-
{canEdit && onToggleTextBoxMode && (
|
|
749
|
-
<Tooltip>
|
|
750
|
-
<TooltipTrigger asChild>
|
|
751
|
-
<button
|
|
752
|
-
type="button"
|
|
753
|
-
onClick={onToggleTextBoxMode}
|
|
754
|
-
data-toolbar-textbox-button
|
|
755
|
-
aria-label={t("editorToolbar.addTextBox")}
|
|
756
|
-
aria-pressed={textBoxMode}
|
|
757
|
-
aria-keyshortcuts="T"
|
|
758
|
-
className={`${TOOLBAR_ICON_BUTTON_CLASS} ${
|
|
759
|
-
textBoxMode
|
|
760
|
-
? "bg-accent text-foreground"
|
|
761
|
-
: "text-muted-foreground hover:bg-accent hover:text-foreground/70"
|
|
762
|
-
}`}
|
|
763
|
-
>
|
|
764
|
-
<IconLetterT className="size-4" />
|
|
765
|
-
</button>
|
|
766
|
-
</TooltipTrigger>
|
|
767
|
-
<TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
|
|
768
|
-
</Tooltip>
|
|
769
|
-
)}
|
|
770
|
-
|
|
771
789
|
{/* Slide tools palette — animations, tweaks, draw, comment-pin all live
|
|
772
790
|
* inside one popover so the toolbar doesn't drown in icons. Hidden in
|
|
773
791
|
* view-only mode since none of these affordances apply. */}
|
|
@@ -879,27 +897,6 @@ graph TD
|
|
|
879
897
|
className="flex-shrink-0 mr-0.5"
|
|
880
898
|
/>
|
|
881
899
|
|
|
882
|
-
{/* Style toggle */}
|
|
883
|
-
{canEdit && currentSlide && onToggleStyle && (
|
|
884
|
-
<Tooltip>
|
|
885
|
-
<TooltipTrigger asChild>
|
|
886
|
-
<button
|
|
887
|
-
onClick={onToggleStyle}
|
|
888
|
-
data-slide-style-trigger="true"
|
|
889
|
-
className={`${TOOLBAR_ICON_BUTTON_CLASS} relative ${
|
|
890
|
-
styleOpen
|
|
891
|
-
? "bg-accent text-foreground"
|
|
892
|
-
: "text-muted-foreground hover:bg-accent hover:text-foreground/70"
|
|
893
|
-
}`}
|
|
894
|
-
aria-label={t("styleInspector.title")}
|
|
895
|
-
>
|
|
896
|
-
<IconPalette className="size-4" />
|
|
897
|
-
</button>
|
|
898
|
-
</TooltipTrigger>
|
|
899
|
-
<TooltipContent>{t("styleInspector.title")}</TooltipContent>
|
|
900
|
-
</Tooltip>
|
|
901
|
-
)}
|
|
902
|
-
|
|
903
900
|
{/* Comments toggle */}
|
|
904
901
|
{onToggleComments && (
|
|
905
902
|
<Tooltip>
|
|
@@ -1008,6 +1005,29 @@ graph TD
|
|
|
1008
1005
|
<TooltipContent>{t("editorToolbar.more")}</TooltipContent>
|
|
1009
1006
|
</Tooltip>
|
|
1010
1007
|
<DropdownMenuContent align="end" className="w-48">
|
|
1008
|
+
{canEdit && (
|
|
1009
|
+
<>
|
|
1010
|
+
<DropdownMenuItem
|
|
1011
|
+
disabled={!canUndo}
|
|
1012
|
+
onSelect={() => commitActiveEditThenRun(undo)}
|
|
1013
|
+
>
|
|
1014
|
+
<IconArrowBackUp className="w-4 h-4 mr-2" />
|
|
1015
|
+
{t("editorToolbar.undoWithShortcut", {
|
|
1016
|
+
shortcut: shortcutLabel("Cmd+Z"),
|
|
1017
|
+
})}
|
|
1018
|
+
</DropdownMenuItem>
|
|
1019
|
+
<DropdownMenuItem
|
|
1020
|
+
disabled={!canRedo}
|
|
1021
|
+
onSelect={() => commitActiveEditThenRun(redo)}
|
|
1022
|
+
>
|
|
1023
|
+
<IconArrowForwardUp className="w-4 h-4 mr-2" />
|
|
1024
|
+
{t("editorToolbar.redoWithShortcut", {
|
|
1025
|
+
shortcut: shortcutLabel("Cmd+Shift+Z"),
|
|
1026
|
+
})}
|
|
1027
|
+
</DropdownMenuItem>
|
|
1028
|
+
<DropdownMenuSeparator />
|
|
1029
|
+
</>
|
|
1030
|
+
)}
|
|
1011
1031
|
<DropdownMenuItem
|
|
1012
1032
|
disabled={importing}
|
|
1013
1033
|
onSelect={() => fileInputRef.current?.click()}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconArrowRight,
|
|
3
|
+
IconPalette,
|
|
4
|
+
IconPresentation,
|
|
5
|
+
IconUpload,
|
|
6
|
+
} from "@tabler/icons-react";
|
|
7
|
+
import type { ChangeEvent, ReactNode } from "react";
|
|
8
|
+
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
} from "@/components/ui/dialog";
|
|
17
|
+
import {
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectGroup,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectLabel,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select";
|
|
26
|
+
import type { Deck } from "@/context/DeckContext";
|
|
27
|
+
import type { RecentReference } from "@/lib/recent-references";
|
|
28
|
+
import { cn } from "@/lib/utils";
|
|
29
|
+
|
|
30
|
+
export interface NewDeckReferenceSelection {
|
|
31
|
+
designSystemId?: string | null;
|
|
32
|
+
referenceDeckId?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DesignSystemOption {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
isDefault?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface NewDeckReferenceStepProps {
|
|
42
|
+
open: boolean;
|
|
43
|
+
designSystems: DesignSystemOption[];
|
|
44
|
+
decks: Deck[];
|
|
45
|
+
defaultDesignSystemId: string | null;
|
|
46
|
+
defaultReferenceDeckId: string | null;
|
|
47
|
+
recentReferences: RecentReference[];
|
|
48
|
+
onSelect: (selection: NewDeckReferenceSelection) => void;
|
|
49
|
+
onImport: (files: File[]) => Promise<void>;
|
|
50
|
+
onSkip: () => void;
|
|
51
|
+
onOpenChange: (open: boolean) => void;
|
|
52
|
+
importing?: boolean;
|
|
53
|
+
title: string;
|
|
54
|
+
designSystemLabel: string;
|
|
55
|
+
referenceDeckLabel: string;
|
|
56
|
+
chooseDeckLabel: string;
|
|
57
|
+
importFileLabel: string;
|
|
58
|
+
importingLabel: string;
|
|
59
|
+
skipLabel: string;
|
|
60
|
+
defaultSuffix: string;
|
|
61
|
+
starredLabel: string;
|
|
62
|
+
otherDecksLabel: string;
|
|
63
|
+
description: string;
|
|
64
|
+
promptNote?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function NewDeckReferenceStep({
|
|
68
|
+
open,
|
|
69
|
+
designSystems,
|
|
70
|
+
decks,
|
|
71
|
+
defaultDesignSystemId,
|
|
72
|
+
defaultReferenceDeckId,
|
|
73
|
+
recentReferences,
|
|
74
|
+
onSelect,
|
|
75
|
+
onImport,
|
|
76
|
+
onSkip,
|
|
77
|
+
onOpenChange,
|
|
78
|
+
importing = false,
|
|
79
|
+
title,
|
|
80
|
+
designSystemLabel,
|
|
81
|
+
referenceDeckLabel,
|
|
82
|
+
chooseDeckLabel,
|
|
83
|
+
importFileLabel,
|
|
84
|
+
importingLabel,
|
|
85
|
+
skipLabel,
|
|
86
|
+
defaultSuffix,
|
|
87
|
+
starredLabel,
|
|
88
|
+
otherDecksLabel,
|
|
89
|
+
description,
|
|
90
|
+
promptNote,
|
|
91
|
+
}: NewDeckReferenceStepProps) {
|
|
92
|
+
const designSystemById = new Map(
|
|
93
|
+
designSystems.map((designSystem) => [designSystem.id, designSystem]),
|
|
94
|
+
);
|
|
95
|
+
const deckById = new Map(decks.map((deck) => [deck.id, deck]));
|
|
96
|
+
const recentOptions = recentReferences
|
|
97
|
+
.map((reference) => {
|
|
98
|
+
const item =
|
|
99
|
+
reference.kind === "design-system"
|
|
100
|
+
? designSystemById.get(reference.id)
|
|
101
|
+
: deckById.get(reference.id);
|
|
102
|
+
return item ? { reference, item } : null;
|
|
103
|
+
})
|
|
104
|
+
.filter(
|
|
105
|
+
(
|
|
106
|
+
value,
|
|
107
|
+
): value is {
|
|
108
|
+
reference: RecentReference;
|
|
109
|
+
item: DesignSystemOption | Deck;
|
|
110
|
+
} => value !== null,
|
|
111
|
+
);
|
|
112
|
+
const defaultReferenceKeys = new Set(
|
|
113
|
+
[
|
|
114
|
+
defaultDesignSystemId ? `design-system:${defaultDesignSystemId}` : null,
|
|
115
|
+
defaultReferenceDeckId ? `deck:${defaultReferenceDeckId}` : null,
|
|
116
|
+
].filter((value): value is string => value !== null),
|
|
117
|
+
);
|
|
118
|
+
const visibleRecentOptions = recentOptions.filter(
|
|
119
|
+
({ reference }) =>
|
|
120
|
+
!defaultReferenceKeys.has(`${reference.kind}:${reference.id}`),
|
|
121
|
+
);
|
|
122
|
+
const starredDecks = decks.filter((deck) => deck.starred);
|
|
123
|
+
const otherDecks = decks.filter((deck) => !deck.starred);
|
|
124
|
+
|
|
125
|
+
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
126
|
+
const files = Array.from(event.target.files ?? []);
|
|
127
|
+
event.target.value = "";
|
|
128
|
+
if (files.length === 0) return;
|
|
129
|
+
await onImport(files);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
134
|
+
<DialogContent className="max-h-[min(720px,calc(100vh-32px))] overflow-y-auto sm:max-w-2xl">
|
|
135
|
+
<DialogHeader>
|
|
136
|
+
<DialogTitle>{title}</DialogTitle>
|
|
137
|
+
<DialogDescription>{description}</DialogDescription>
|
|
138
|
+
</DialogHeader>
|
|
139
|
+
|
|
140
|
+
<div className="grid gap-4">
|
|
141
|
+
{(defaultDesignSystemId || defaultReferenceDeckId) && (
|
|
142
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
143
|
+
{defaultDesignSystemId &&
|
|
144
|
+
designSystemById.has(defaultDesignSystemId) && (
|
|
145
|
+
<ReferenceCard
|
|
146
|
+
icon={<IconPalette className="size-4" />}
|
|
147
|
+
label={
|
|
148
|
+
designSystemById.get(defaultDesignSystemId)?.title ??
|
|
149
|
+
designSystemLabel
|
|
150
|
+
}
|
|
151
|
+
meta={`${designSystemLabel}${defaultSuffix}`}
|
|
152
|
+
onClick={() =>
|
|
153
|
+
onSelect({
|
|
154
|
+
designSystemId: defaultDesignSystemId,
|
|
155
|
+
referenceDeckId: null,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
{defaultReferenceDeckId &&
|
|
161
|
+
deckById.has(defaultReferenceDeckId) && (
|
|
162
|
+
<ReferenceCard
|
|
163
|
+
icon={<IconPresentation className="size-4" />}
|
|
164
|
+
label={deckById.get(defaultReferenceDeckId)?.title ?? ""}
|
|
165
|
+
meta={`${referenceDeckLabel}${defaultSuffix}`}
|
|
166
|
+
onClick={() =>
|
|
167
|
+
onSelect({
|
|
168
|
+
designSystemId: null,
|
|
169
|
+
referenceDeckId: defaultReferenceDeckId,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{visibleRecentOptions.length > 0 && (
|
|
178
|
+
<div className="grid gap-2">
|
|
179
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
180
|
+
{visibleRecentOptions.map(({ reference, item }) => (
|
|
181
|
+
<ReferenceCard
|
|
182
|
+
key={`${reference.kind}:${reference.id}`}
|
|
183
|
+
icon={
|
|
184
|
+
reference.kind === "design-system" ? (
|
|
185
|
+
<IconPalette className="size-4" />
|
|
186
|
+
) : (
|
|
187
|
+
<IconPresentation className="size-4" />
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
label={item.title}
|
|
191
|
+
meta={
|
|
192
|
+
reference.kind === "design-system"
|
|
193
|
+
? designSystemLabel
|
|
194
|
+
: referenceDeckLabel
|
|
195
|
+
}
|
|
196
|
+
onClick={() =>
|
|
197
|
+
onSelect(
|
|
198
|
+
reference.kind === "design-system"
|
|
199
|
+
? {
|
|
200
|
+
designSystemId: reference.id,
|
|
201
|
+
referenceDeckId: null,
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
designSystemId: null,
|
|
205
|
+
referenceDeckId: reference.id,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{decks.length > 0 && (
|
|
216
|
+
<div className="grid gap-2">
|
|
217
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
218
|
+
{referenceDeckLabel}
|
|
219
|
+
</span>
|
|
220
|
+
<Select
|
|
221
|
+
onValueChange={(value) =>
|
|
222
|
+
onSelect({ designSystemId: null, referenceDeckId: value })
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<SelectTrigger className="w-full">
|
|
226
|
+
<SelectValue placeholder={chooseDeckLabel} />
|
|
227
|
+
</SelectTrigger>
|
|
228
|
+
<SelectContent>
|
|
229
|
+
{starredDecks.length > 0 && (
|
|
230
|
+
<SelectGroup>
|
|
231
|
+
<SelectLabel>{starredLabel}</SelectLabel>
|
|
232
|
+
{starredDecks.map((deck) => (
|
|
233
|
+
<SelectItem key={deck.id} value={deck.id}>
|
|
234
|
+
{deck.title}
|
|
235
|
+
</SelectItem>
|
|
236
|
+
))}
|
|
237
|
+
</SelectGroup>
|
|
238
|
+
)}
|
|
239
|
+
{otherDecks.length > 0 && (
|
|
240
|
+
<SelectGroup>
|
|
241
|
+
<SelectLabel>{otherDecksLabel}</SelectLabel>
|
|
242
|
+
{otherDecks.map((deck) => (
|
|
243
|
+
<SelectItem key={deck.id} value={deck.id}>
|
|
244
|
+
{deck.title}
|
|
245
|
+
</SelectItem>
|
|
246
|
+
))}
|
|
247
|
+
</SelectGroup>
|
|
248
|
+
)}
|
|
249
|
+
</SelectContent>
|
|
250
|
+
</Select>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
<div className="flex flex-wrap items-center gap-2 border-t border-border pt-3">
|
|
255
|
+
<label
|
|
256
|
+
className={cn(
|
|
257
|
+
"inline-flex cursor-pointer items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-accent",
|
|
258
|
+
importing && "pointer-events-none opacity-60",
|
|
259
|
+
)}
|
|
260
|
+
>
|
|
261
|
+
<IconUpload className="size-4" />
|
|
262
|
+
{importing ? importingLabel : importFileLabel}
|
|
263
|
+
<input
|
|
264
|
+
type="file"
|
|
265
|
+
className="sr-only"
|
|
266
|
+
accept=".pptx,.pdf,.docx"
|
|
267
|
+
multiple
|
|
268
|
+
disabled={importing}
|
|
269
|
+
onChange={(event) => void handleImport(event)}
|
|
270
|
+
/>
|
|
271
|
+
</label>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{promptNote && (
|
|
276
|
+
<p className="truncate border-t border-border pt-3 text-xs text-muted-foreground">
|
|
277
|
+
{promptNote}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<div className="flex items-center justify-between border-t border-border pt-3">
|
|
282
|
+
<Button type="button" variant="ghost" onClick={onSkip}>
|
|
283
|
+
{skipLabel}
|
|
284
|
+
</Button>
|
|
285
|
+
<IconArrowRight className="size-4 text-muted-foreground" />
|
|
286
|
+
</div>
|
|
287
|
+
</DialogContent>
|
|
288
|
+
</Dialog>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function ReferenceCard({
|
|
293
|
+
icon,
|
|
294
|
+
label,
|
|
295
|
+
meta,
|
|
296
|
+
onClick,
|
|
297
|
+
}: {
|
|
298
|
+
icon: ReactNode;
|
|
299
|
+
label: string;
|
|
300
|
+
meta: string;
|
|
301
|
+
onClick: () => void;
|
|
302
|
+
}) {
|
|
303
|
+
return (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={onClick}
|
|
307
|
+
className="flex min-w-0 items-center gap-3 rounded-lg border border-border bg-card px-3 py-3 text-start transition-colors hover:border-foreground/30 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
308
|
+
>
|
|
309
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent text-muted-foreground">
|
|
310
|
+
{icon}
|
|
311
|
+
</span>
|
|
312
|
+
<span className="min-w-0">
|
|
313
|
+
<span className="block truncate text-sm font-medium">{label}</span>
|
|
314
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
315
|
+
{meta}
|
|
316
|
+
</span>
|
|
317
|
+
</span>
|
|
318
|
+
<IconArrowRight className="ms-auto size-4 shrink-0 text-muted-foreground" />
|
|
319
|
+
</button>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -16,6 +16,37 @@ export interface UploadedFile {
|
|
|
16
16
|
size: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export async function uploadPromptFiles(
|
|
20
|
+
files: File[],
|
|
21
|
+
): Promise<UploadedFile[]> {
|
|
22
|
+
if (files.length === 0) return [];
|
|
23
|
+
const formData = new FormData();
|
|
24
|
+
files.forEach((file) => formData.append("files", file));
|
|
25
|
+
const response = await fetch(`${appBasePath()}/api/uploads`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: formData,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
let message = "Upload failed";
|
|
31
|
+
try {
|
|
32
|
+
const data: unknown = await response.json();
|
|
33
|
+
if (
|
|
34
|
+
data &&
|
|
35
|
+
typeof data === "object" &&
|
|
36
|
+
"error" in data &&
|
|
37
|
+
typeof data.error === "string" &&
|
|
38
|
+
data.error.trim()
|
|
39
|
+
) {
|
|
40
|
+
message = data.error;
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Upload failed (${response.status})`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw new Error(message);
|
|
46
|
+
}
|
|
47
|
+
return (await response.json()) as UploadedFile[];
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
/**
|
|
20
51
|
* Radix popovers portal to `document.body`, so a mousedown inside the model
|
|
21
52
|
* picker or attachment menu reads as "outside" any panel that hosts a composer.
|
|
@@ -137,17 +168,7 @@ export default function PromptPopover({
|
|
|
137
168
|
if (files.length === 0) return [];
|
|
138
169
|
setUploading(true);
|
|
139
170
|
try {
|
|
140
|
-
|
|
141
|
-
files.forEach((f) => formData.append("files", f));
|
|
142
|
-
const res = await fetch(`${appBasePath()}/api/uploads`, {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: formData,
|
|
145
|
-
});
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const data = await res.json().catch(() => null);
|
|
148
|
-
throw new Error(data?.error || "Upload failed");
|
|
149
|
-
}
|
|
150
|
-
return (await res.json()) as UploadedFile[];
|
|
171
|
+
return await uploadPromptFiles(files);
|
|
151
172
|
} finally {
|
|
152
173
|
setUploading(false);
|
|
153
174
|
}
|