@alpaca-editor/core 1.0.3978 → 1.0.3980
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/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/config/config.js +18 -2
- package/dist/config/config.js.map +1 -1
- package/dist/editor/AspectRatioSelector.d.ts +13 -0
- package/dist/editor/AspectRatioSelector.js +71 -0
- package/dist/editor/AspectRatioSelector.js.map +1 -0
- package/dist/editor/ConfirmationDialog.js +4 -5
- package/dist/editor/ConfirmationDialog.js.map +1 -1
- package/dist/editor/PictureCropper.d.ts +1 -1
- package/dist/editor/PictureCropper.js +466 -113
- package/dist/editor/PictureCropper.js.map +1 -1
- package/dist/editor/Terminal.js +5 -4
- package/dist/editor/Terminal.js.map +1 -1
- package/dist/editor/ai/AiTerminal.js +20 -2
- package/dist/editor/ai/AiTerminal.js.map +1 -1
- package/dist/editor/client/EditorClient.js +14 -3
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +3 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +13 -5
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/media-selector/Preview.js +1 -1
- package/dist/editor/media-selector/Preview.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +48 -24
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +6 -6
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js +26 -4
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
- package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
- package/dist/editor/page-viewer/EditorForm.js +16 -2
- package/dist/editor/page-viewer/EditorForm.js.map +1 -1
- package/dist/editor/page-viewer/EditorFormPopup.d.ts +11 -0
- package/dist/editor/page-viewer/EditorFormPopup.js +41 -0
- package/dist/editor/page-viewer/EditorFormPopup.js.map +1 -0
- package/dist/editor/page-viewer/PageViewer.js +4 -6
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/services/contextService.d.ts +26 -0
- package/dist/editor/services/contextService.js +102 -0
- package/dist/editor/services/contextService.js.map +1 -0
- package/dist/editor/sidebar/Completions.d.ts +1 -0
- package/dist/editor/sidebar/Completions.js +54 -0
- package/dist/editor/sidebar/Completions.js.map +1 -0
- package/dist/editor/sidebar/Validation.js +3 -3
- package/dist/editor/sidebar/Validation.js.map +1 -1
- package/dist/editor/ui/PerfectTree.js +17 -3
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/ui/SimpleTabs.d.ts +2 -1
- package/dist/editor/ui/SimpleTabs.js +2 -2
- package/dist/editor/ui/SimpleTabs.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +134 -30
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/config/config.tsx +18 -1
- package/src/editor/AspectRatioSelector.tsx +146 -0
- package/src/editor/ConfirmationDialog.tsx +36 -45
- package/src/editor/PictureCropper.tsx +724 -233
- package/src/editor/Terminal.tsx +9 -8
- package/src/editor/ai/AiTerminal.tsx +58 -15
- package/src/editor/client/EditorClient.tsx +26 -1
- package/src/editor/client/editContext.ts +7 -0
- package/src/editor/commands/componentCommands.tsx +14 -9
- package/src/editor/media-selector/Preview.tsx +7 -5
- package/src/editor/page-editor-chrome/FrameMenu.tsx +70 -15
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -3
- package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +31 -5
- package/src/editor/page-viewer/EditorForm.tsx +21 -1
- package/src/editor/page-viewer/EditorFormPopup.tsx +104 -0
- package/src/editor/page-viewer/PageViewer.tsx +3 -11
- package/src/editor/services/contextService.ts +146 -0
- package/src/editor/sidebar/Completions.tsx +160 -0
- package/src/editor/sidebar/Validation.tsx +9 -10
- package/src/editor/ui/PerfectTree.tsx +19 -3
- package/src/editor/ui/SimpleTabs.tsx +4 -1
- package/src/revision.ts +2 -2
- package/src/types.ts +1 -0
- package/dist/editor/menubar/BrowseHistory.d.ts +0 -6
- package/dist/editor/menubar/BrowseHistory.js +0 -11
- package/dist/editor/menubar/BrowseHistory.js.map +0 -1
- package/src/editor/menubar/BrowseHistory.tsx +0 -28
package/src/editor/Terminal.tsx
CHANGED
|
@@ -8,6 +8,8 @@ import React, {
|
|
|
8
8
|
import { Button } from "primereact/button";
|
|
9
9
|
import { InputTextarea } from "primereact/inputtextarea";
|
|
10
10
|
import { classNames } from "primereact/utils";
|
|
11
|
+
import { SimpleIconButton } from "./ui/SimpleIconButton";
|
|
12
|
+
import { Trash2, Send } from "lucide-react";
|
|
11
13
|
|
|
12
14
|
type Message = {
|
|
13
15
|
text: React.ReactNode;
|
|
@@ -161,14 +163,14 @@ export const Terminal = forwardRef<
|
|
|
161
163
|
return (
|
|
162
164
|
<div className={classNames("flex h-full flex-col", className)}>
|
|
163
165
|
<div className="flex items-center justify-between gap-2 border-b border-gray-200 p-1 text-xs">
|
|
164
|
-
<
|
|
166
|
+
<SimpleIconButton
|
|
165
167
|
onClick={() => {
|
|
166
168
|
setMessages([]);
|
|
167
169
|
onReset();
|
|
168
170
|
}}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
icon={<Trash2 size={16} strokeWidth={1} />}
|
|
172
|
+
label="Clear terminal"
|
|
173
|
+
/>
|
|
172
174
|
{toolbar}
|
|
173
175
|
</div>
|
|
174
176
|
<div className="flex-1 overflow-x-hidden overflow-y-auto p-2">
|
|
@@ -212,13 +214,12 @@ export const Terminal = forwardRef<
|
|
|
212
214
|
/>
|
|
213
215
|
<div className="flex items-center justify-between py-1">
|
|
214
216
|
{statusbar}
|
|
215
|
-
<
|
|
216
|
-
icon={
|
|
217
|
-
text
|
|
218
|
-
size="small"
|
|
217
|
+
<SimpleIconButton
|
|
218
|
+
icon={<Send size={16} strokeWidth={1} />}
|
|
219
219
|
className="tour-send-button"
|
|
220
220
|
onClick={submit}
|
|
221
221
|
disabled={prompt.trim().length === 0}
|
|
222
|
+
label="Send"
|
|
222
223
|
/>
|
|
223
224
|
</div>
|
|
224
225
|
</div>
|
|
@@ -14,6 +14,7 @@ import { AiResponseMessage } from "./AiResponseMessage";
|
|
|
14
14
|
import { AiProfile, loadAiProfiles } from "../services/aiService";
|
|
15
15
|
import { EditOperation } from "../../types";
|
|
16
16
|
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
17
|
+
import { Settings } from "lucide-react";
|
|
17
18
|
|
|
18
19
|
type Response = {
|
|
19
20
|
messages: Message[];
|
|
@@ -80,6 +81,8 @@ export function AiTerminal({
|
|
|
80
81
|
const selection = editContext.selection;
|
|
81
82
|
const terminalRef = useRef<{ submit: () => void }>(null);
|
|
82
83
|
const [responseMessages, setResponseMessages] = useState<Message[]>([]);
|
|
84
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
85
|
+
const settingsRef = useRef<HTMLDivElement>(null);
|
|
83
86
|
|
|
84
87
|
useEffect(() => {
|
|
85
88
|
if (options?.initialPrompt && !initialPromptExecuted && model) {
|
|
@@ -116,6 +119,25 @@ export function AiTerminal({
|
|
|
116
119
|
messagesRef.current = responseMessages;
|
|
117
120
|
}, [responseMessages]);
|
|
118
121
|
|
|
122
|
+
// Handle click outside for settings popover
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
function handleClickOutside(event: MouseEvent) {
|
|
125
|
+
if (
|
|
126
|
+
settingsRef.current &&
|
|
127
|
+
!settingsRef.current.contains(event.target as Node)
|
|
128
|
+
) {
|
|
129
|
+
setShowSettings(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (showSettings) {
|
|
134
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
135
|
+
return () => {
|
|
136
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}, [showSettings]);
|
|
140
|
+
|
|
119
141
|
function handleResponse(
|
|
120
142
|
response: Response,
|
|
121
143
|
terminalCallback: (text: React.ReactNode, finished: boolean) => void,
|
|
@@ -328,7 +350,7 @@ export function AiTerminal({
|
|
|
328
350
|
setPrompt={setPrompt}
|
|
329
351
|
statusbar=<div className="flex flex-1 items-center justify-between gap-1">
|
|
330
352
|
<a
|
|
331
|
-
className="ml-1 flex cursor-pointer items-center gap-1 text-xs
|
|
353
|
+
className="ml-1 flex cursor-pointer items-center gap-1 text-xs"
|
|
332
354
|
onClick={() => {
|
|
333
355
|
setShowPredefined(!showPredefined);
|
|
334
356
|
}}
|
|
@@ -367,21 +389,42 @@ export function AiTerminal({
|
|
|
367
389
|
)}
|
|
368
390
|
</div>
|
|
369
391
|
toolbar=<div className="flex items-stretch gap-1">
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
options={profiles}
|
|
376
|
-
/>
|
|
377
|
-
{activeProfile && (
|
|
378
|
-
<Dropdown
|
|
379
|
-
className="text-sm"
|
|
380
|
-
value={model}
|
|
381
|
-
onChange={(e) => setModel(e.value)}
|
|
382
|
-
options={activeProfile.models}
|
|
392
|
+
<div className="relative" ref={settingsRef}>
|
|
393
|
+
<SimpleIconButton
|
|
394
|
+
icon={<Settings size={16} strokeWidth={1} />}
|
|
395
|
+
label="Settings"
|
|
396
|
+
onClick={() => setShowSettings(!showSettings)}
|
|
383
397
|
/>
|
|
384
|
-
|
|
398
|
+
{showSettings && (
|
|
399
|
+
<div className="absolute top-8 right-0 z-50 flex flex-col gap-2 rounded-lg border border-gray-200 bg-white p-3 shadow-lg">
|
|
400
|
+
<div className="flex flex-col gap-1">
|
|
401
|
+
<label className="text-xs font-medium text-gray-700">
|
|
402
|
+
Profile
|
|
403
|
+
</label>
|
|
404
|
+
<Dropdown
|
|
405
|
+
className="text-sm"
|
|
406
|
+
value={activeProfile}
|
|
407
|
+
onChange={(e) => setActiveProfile(e.value)}
|
|
408
|
+
optionLabel="name"
|
|
409
|
+
options={profiles}
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
{activeProfile && (
|
|
413
|
+
<div className="flex flex-col gap-1">
|
|
414
|
+
<label className="text-xs font-medium text-gray-700">
|
|
415
|
+
Model
|
|
416
|
+
</label>
|
|
417
|
+
<Dropdown
|
|
418
|
+
className="text-sm"
|
|
419
|
+
value={model}
|
|
420
|
+
onChange={(e) => setModel(e.value)}
|
|
421
|
+
options={activeProfile.models}
|
|
422
|
+
/>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
385
428
|
{closeButton}
|
|
386
429
|
</div>
|
|
387
430
|
commandHandler={(v, callback) => {
|
|
@@ -68,6 +68,10 @@ import { FieldEditorPopup, FieldEditorPopupRef } from "../FieldEditorPopup";
|
|
|
68
68
|
|
|
69
69
|
import { Command, CommandData } from "../commands/commands";
|
|
70
70
|
import { AiPopup, AiPopupRef } from "../ai/AiPopup";
|
|
71
|
+
import {
|
|
72
|
+
EditorFormPopup,
|
|
73
|
+
EditorFormPopupRef,
|
|
74
|
+
} from "../page-viewer/EditorFormPopup";
|
|
71
75
|
|
|
72
76
|
import { ComponentDetails } from "../services/componentDesignerService";
|
|
73
77
|
import {
|
|
@@ -219,6 +223,7 @@ export function EditorClient({
|
|
|
219
223
|
|
|
220
224
|
const aiPopupRef = React.useRef<AiPopupRef>(null);
|
|
221
225
|
const fieldEditorPopupRef = React.useRef<FieldEditorPopupRef>(null);
|
|
226
|
+
const editorFormPopupRef = React.useRef<EditorFormPopupRef>(null);
|
|
222
227
|
|
|
223
228
|
const [validationResult, setValidationResult] = useState<
|
|
224
229
|
ValidationResult[] | undefined
|
|
@@ -306,7 +311,7 @@ export function EditorClient({
|
|
|
306
311
|
language: entry.itemLanguage,
|
|
307
312
|
hasLayout: false, // This will need to be determined from the item
|
|
308
313
|
templateName: "", // This will need to be determined from the item
|
|
309
|
-
icon:
|
|
314
|
+
icon: entry.icon,
|
|
310
315
|
}),
|
|
311
316
|
);
|
|
312
317
|
});
|
|
@@ -328,6 +333,7 @@ export function EditorClient({
|
|
|
328
333
|
const [showRightSidebar, setShowRightSidebar] = useState(
|
|
329
334
|
userPreferences.showRightSidebar ?? true,
|
|
330
335
|
);
|
|
336
|
+
const [activeEditorTab, setActiveEditorTab] = useState<string | null>(null);
|
|
331
337
|
const [hideNonEditableComponents, setHideNonEditableComponents] = useState(
|
|
332
338
|
userPreferences.hideNonEditableComponents ?? false,
|
|
333
339
|
);
|
|
@@ -1970,6 +1976,13 @@ export function EditorClient({
|
|
|
1970
1976
|
setCurrentOverlay("fields");
|
|
1971
1977
|
fieldEditorPopupRef.current?.show(fields, sections, ev);
|
|
1972
1978
|
},
|
|
1979
|
+
showEditorFormPopup: (
|
|
1980
|
+
event: MouseEvent<HTMLElement>,
|
|
1981
|
+
activeTab?: string,
|
|
1982
|
+
) => {
|
|
1983
|
+
setCurrentOverlay("editor-form");
|
|
1984
|
+
editorFormPopupRef.current?.show(event, activeTab);
|
|
1985
|
+
},
|
|
1973
1986
|
inserting,
|
|
1974
1987
|
validating,
|
|
1975
1988
|
validationResult,
|
|
@@ -2117,6 +2130,8 @@ export function EditorClient({
|
|
|
2117
2130
|
setEnableCompletions,
|
|
2118
2131
|
showRightSidebar,
|
|
2119
2132
|
setShowRightSidebar: handleSetShowRightSidebar,
|
|
2133
|
+
activeEditorTab,
|
|
2134
|
+
setActiveEditorTab,
|
|
2120
2135
|
hideNonEditableComponents,
|
|
2121
2136
|
setHideNonEditableComponents: handleSetHideNonEditableComponents,
|
|
2122
2137
|
quotaInfo,
|
|
@@ -2204,6 +2219,8 @@ export function EditorClient({
|
|
|
2204
2219
|
setShowSuggestedEditsDiff,
|
|
2205
2220
|
showRightSidebar,
|
|
2206
2221
|
handleSetShowRightSidebar,
|
|
2222
|
+
activeEditorTab,
|
|
2223
|
+
setActiveEditorTab,
|
|
2207
2224
|
hideNonEditableComponents,
|
|
2208
2225
|
handleSetHideNonEditableComponents,
|
|
2209
2226
|
quotaInfo,
|
|
@@ -2256,6 +2273,10 @@ export function EditorClient({
|
|
|
2256
2273
|
<Shrink className="h-5 w-5" />
|
|
2257
2274
|
</button>
|
|
2258
2275
|
</div>
|
|
2276
|
+
<EditorFormPopup
|
|
2277
|
+
ref={editorFormPopupRef}
|
|
2278
|
+
pageViewContext={pageViewContext}
|
|
2279
|
+
/>
|
|
2259
2280
|
{showFullscreenHint && (
|
|
2260
2281
|
<div
|
|
2261
2282
|
className="fixed inset-0"
|
|
@@ -2308,6 +2329,10 @@ export function EditorClient({
|
|
|
2308
2329
|
|
|
2309
2330
|
<AiPopup ref={aiPopupRef} />
|
|
2310
2331
|
<FieldEditorPopup ref={fieldEditorPopupRef} />
|
|
2332
|
+
<EditorFormPopup
|
|
2333
|
+
ref={editorFormPopupRef}
|
|
2334
|
+
pageViewContext={pageViewContext}
|
|
2335
|
+
/>
|
|
2311
2336
|
{isTourActive && (
|
|
2312
2337
|
<Tour tourStopCallback={() => setIsTourActive(false)} />
|
|
2313
2338
|
)}
|
|
@@ -251,6 +251,10 @@ export type EditContextType = {
|
|
|
251
251
|
aiTerminalOptions?: AiTerminalOptions,
|
|
252
252
|
) => void;
|
|
253
253
|
showFieldEditorPopup: (fields: Field[], sections: string[], ev: any) => void;
|
|
254
|
+
showEditorFormPopup: (
|
|
255
|
+
event: MouseEvent<HTMLElement>,
|
|
256
|
+
activeTab?: string,
|
|
257
|
+
) => void;
|
|
254
258
|
validationResult: ValidationResult[] | undefined;
|
|
255
259
|
contentEditorItem: FullItem | undefined;
|
|
256
260
|
|
|
@@ -326,6 +330,9 @@ export type EditContextType = {
|
|
|
326
330
|
showRightSidebar: boolean;
|
|
327
331
|
setShowRightSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
|
328
332
|
|
|
333
|
+
activeEditorTab: string | null;
|
|
334
|
+
setActiveEditorTab: React.Dispatch<React.SetStateAction<string | null>>;
|
|
335
|
+
|
|
329
336
|
hideNonEditableComponents: boolean;
|
|
330
337
|
setHideNonEditableComponents: React.Dispatch<React.SetStateAction<boolean>>;
|
|
331
338
|
|
|
@@ -165,18 +165,23 @@ async function getDesignCommand(
|
|
|
165
165
|
return {
|
|
166
166
|
id: "design",
|
|
167
167
|
icon: <Palette size={defaultIconSize} />,
|
|
168
|
-
label: "Design",
|
|
168
|
+
label: "Open Design Options",
|
|
169
169
|
disabled: () => false,
|
|
170
170
|
execute: async (context: CommandContext<any>) => {
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
// Check if we're in fullscreen mode
|
|
172
|
+
if (editContext.pageView.fullscreen) {
|
|
173
|
+
// In fullscreen mode, show the EditorForm popup
|
|
174
|
+
if (!context.event) {
|
|
175
|
+
console.warn(
|
|
176
|
+
"Design command executed without event context in fullscreen mode",
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
editContext.showEditorFormPopup(context.event as any, "design");
|
|
181
|
+
} else {
|
|
182
|
+
// Switch to the design tab
|
|
183
|
+
editContext.setActiveEditorTab("design");
|
|
174
184
|
}
|
|
175
|
-
editContext.showFieldEditorPopup(
|
|
176
|
-
item.fields || [],
|
|
177
|
-
["Design", "Rendering"],
|
|
178
|
-
context.event,
|
|
179
|
-
);
|
|
180
185
|
},
|
|
181
186
|
visibilityScopes: ["editFrame", "contextMenu"],
|
|
182
187
|
};
|
|
@@ -42,11 +42,13 @@ export function Preview({ selectedImage }: { selectedImage?: Thumbnail }) {
|
|
|
42
42
|
<div>Updated by: {selectedImage.updatedBy}</div>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
<div className="flex min-h-0 flex-1 items-center justify-center">
|
|
46
|
+
<img
|
|
47
|
+
src={selectedImage?.previewUrl}
|
|
48
|
+
className="max-h-full max-w-full object-contain"
|
|
49
|
+
fetchPriority="high"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
50
52
|
</div>
|
|
51
53
|
);
|
|
52
54
|
}
|
|
@@ -7,6 +7,12 @@ import { Component } from "../pageModel";
|
|
|
7
7
|
import { PageViewContext } from "../page-viewer/pageViewContext";
|
|
8
8
|
import { ArrowUpFromDot } from "lucide-react";
|
|
9
9
|
import { cn } from "../../lib/utils";
|
|
10
|
+
import { PlaceholderDropZone } from "./PlaceholderDropZone";
|
|
11
|
+
import {
|
|
12
|
+
Tooltip,
|
|
13
|
+
TooltipContent,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "../../components/ui/tooltip";
|
|
10
16
|
|
|
11
17
|
import { ComponentCommand } from "../commands/componentCommands";
|
|
12
18
|
|
|
@@ -248,6 +254,31 @@ export function FrameMenu({
|
|
|
248
254
|
const isMultiSelected = editContext.selection.length > 1;
|
|
249
255
|
if (!componentRect) return null;
|
|
250
256
|
|
|
257
|
+
// Check if the component's placeholder can accept more components
|
|
258
|
+
const placeholder = component.parentPlaceholder;
|
|
259
|
+
const canAppend =
|
|
260
|
+
placeholder?.editable &&
|
|
261
|
+
placeholder?.insertOptions?.some(
|
|
262
|
+
(option) => !option.isHidden && !option.isInvalid,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Calculate position for the append dropzone (bottom center of component)
|
|
266
|
+
const appendPosition = canAppend
|
|
267
|
+
? {
|
|
268
|
+
x: componentRect.x + componentRect.width / 2,
|
|
269
|
+
y: componentRect.y + componentRect.height,
|
|
270
|
+
}
|
|
271
|
+
: null;
|
|
272
|
+
|
|
273
|
+
// Calculate the index after the current component
|
|
274
|
+
const appendIndex = placeholder
|
|
275
|
+
? placeholder.components.findIndex((c) => c.id === component.id) + 1
|
|
276
|
+
: 0;
|
|
277
|
+
|
|
278
|
+
// Create a dummy element for the dropzone positioning
|
|
279
|
+
const dummyElement =
|
|
280
|
+
typeof document !== "undefined" ? document.createElement("div") : null;
|
|
281
|
+
|
|
251
282
|
return (
|
|
252
283
|
<>
|
|
253
284
|
<div
|
|
@@ -312,27 +343,51 @@ export function FrameMenu({
|
|
|
312
343
|
{editContext.mode === "edit" && (
|
|
313
344
|
<div className="flex items-center gap-2.5 text-sm">
|
|
314
345
|
{buttons.map((b, i) => (
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
346
|
+
<Tooltip key={i}>
|
|
347
|
+
<TooltipTrigger asChild>
|
|
348
|
+
<div
|
|
349
|
+
className="cursor-pointer hover:text-gray-200"
|
|
350
|
+
onClick={(ev) => {
|
|
351
|
+
ev.stopPropagation();
|
|
352
|
+
b.onClick(ev);
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{typeof b.icon === "string" ? (
|
|
356
|
+
<i className={b.icon + " cursor-pointer text-sm"} />
|
|
357
|
+
) : (
|
|
358
|
+
b.icon
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
</TooltipTrigger>
|
|
362
|
+
<TooltipContent>{b.label}</TooltipContent>
|
|
363
|
+
</Tooltip>
|
|
330
364
|
))}
|
|
331
365
|
</div>
|
|
332
366
|
)}
|
|
333
367
|
</div>
|
|
334
368
|
)}
|
|
335
369
|
</div>
|
|
370
|
+
|
|
371
|
+
{/* Reuse PlaceholderDropZone for append functionality */}
|
|
372
|
+
{canAppend &&
|
|
373
|
+
!isReadonly &&
|
|
374
|
+
editContext.mode === "edit" &&
|
|
375
|
+
placeholder &&
|
|
376
|
+
appendPosition &&
|
|
377
|
+
dummyElement && (
|
|
378
|
+
<PlaceholderDropZone
|
|
379
|
+
className="bg-component-blue/40 hover:bg-component-blue rounded-md"
|
|
380
|
+
insertMenuTitle="Append"
|
|
381
|
+
placeholder={placeholder}
|
|
382
|
+
description={`Append to ${placeholder.name}`}
|
|
383
|
+
index={appendIndex}
|
|
384
|
+
parentName={component.name}
|
|
385
|
+
spotPosition={appendPosition}
|
|
386
|
+
spotPositionElement={dummyElement}
|
|
387
|
+
spotPositionAnchor="top"
|
|
388
|
+
size="small"
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
336
391
|
</>
|
|
337
392
|
);
|
|
338
393
|
}
|
|
@@ -7,6 +7,7 @@ import { MenuItem } from "primereact/menuitem";
|
|
|
7
7
|
|
|
8
8
|
import { getAbsoluteIconUrl } from "../utils";
|
|
9
9
|
import { Placeholder } from "../pageModel";
|
|
10
|
+
import { cn } from "../../lib/utils";
|
|
10
11
|
|
|
11
12
|
export function PlaceholderDropZone({
|
|
12
13
|
placeholder,
|
|
@@ -17,6 +18,8 @@ export function PlaceholderDropZone({
|
|
|
17
18
|
spotPositionElement,
|
|
18
19
|
spotPositionAnchor,
|
|
19
20
|
size,
|
|
21
|
+
className,
|
|
22
|
+
insertMenuTitle,
|
|
20
23
|
}: {
|
|
21
24
|
placeholder: Placeholder;
|
|
22
25
|
description?: string;
|
|
@@ -26,6 +29,8 @@ export function PlaceholderDropZone({
|
|
|
26
29
|
spotPositionElement: Element;
|
|
27
30
|
size: "large" | "small";
|
|
28
31
|
spotPositionAnchor: "top" | "bottom" | "left" | "right";
|
|
32
|
+
className?: string;
|
|
33
|
+
insertMenuTitle?: string;
|
|
29
34
|
}) {
|
|
30
35
|
const [isHovering, setIsHovering] = useState(false);
|
|
31
36
|
|
|
@@ -130,7 +135,7 @@ export function PlaceholderDropZone({
|
|
|
130
135
|
if (insertOptions.length > 0)
|
|
131
136
|
editContext.showContextMenu(e, [
|
|
132
137
|
{
|
|
133
|
-
label: "Insert",
|
|
138
|
+
label: insertMenuTitle || "Insert",
|
|
134
139
|
disabled: true,
|
|
135
140
|
className: "border-b border-gray-400 pb-2 pl-2",
|
|
136
141
|
template: () => {
|
|
@@ -144,11 +149,12 @@ export function PlaceholderDropZone({
|
|
|
144
149
|
className="placeholder placeholder-dropzone cursor-pointer"
|
|
145
150
|
>
|
|
146
151
|
<div
|
|
147
|
-
className={
|
|
152
|
+
className={cn(
|
|
148
153
|
isHovering
|
|
149
|
-
? "z-30 h-20 w-20 shadow-xl
|
|
154
|
+
? "z-30 h-20 w-20 shadow-xl"
|
|
150
155
|
: "z-30 h-10 w-10 hover:scale-[1.1]",
|
|
151
156
|
"test-ph-dropzone tour-placeholder-dropzone absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-blue-500 p-2 text-center text-sm text-white opacity-100 transition-all duration-300 ease-in-out",
|
|
157
|
+
className,
|
|
152
158
|
)}
|
|
153
159
|
data-testid="placeholder-dropzone-button"
|
|
154
160
|
>
|
|
@@ -10,6 +10,11 @@ import { useDebouncedCallback } from "use-debounce";
|
|
|
10
10
|
import { PageViewContext } from "../page-viewer/pageViewContext";
|
|
11
11
|
import { useEditContext } from "../client/editContext";
|
|
12
12
|
import { executePrompt } from "../services/aiService";
|
|
13
|
+
import {
|
|
14
|
+
generatePageContext,
|
|
15
|
+
getCachedContext,
|
|
16
|
+
PageContext,
|
|
17
|
+
} from "../services/contextService";
|
|
13
18
|
|
|
14
19
|
export function useInlineAiCompletion({
|
|
15
20
|
pageViewContext,
|
|
@@ -316,22 +321,43 @@ export function useInlineAiCompletion({
|
|
|
316
321
|
}
|
|
317
322
|
}
|
|
318
323
|
|
|
324
|
+
if (!editContext) return null;
|
|
325
|
+
|
|
326
|
+
// Get page context for better completions
|
|
327
|
+
let pageContext: PageContext | null = getCachedContext(editContext);
|
|
328
|
+
if (!pageContext) {
|
|
329
|
+
try {
|
|
330
|
+
pageContext = await generatePageContext(editContext, pageViewContext);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.warn("Failed to generate page context:", error);
|
|
333
|
+
// Continue without context
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Create context-aware completion prompt
|
|
338
|
+
const systemPrompt = pageContext
|
|
339
|
+
? `You are a content completion tool for a ${pageContext.pageType} page titled "${pageContext.pageTitle}".
|
|
340
|
+
|
|
341
|
+
Page context: ${pageContext.abstract}
|
|
342
|
+
|
|
343
|
+
Field being edited: ${fieldName || fieldId}
|
|
344
|
+
|
|
345
|
+
ONLY provide the completion text (what comes after the user's text), never repeat any part of what the user wrote. Your completion should flow naturally from the user's text and be relevant to the page context and field being edited.`
|
|
346
|
+
: "You are a sentence completion tool. ONLY provide the completion text (what comes after the user's text), never repeat any part of what the user wrote. Your completion should flow naturally from the user's text.";
|
|
347
|
+
|
|
319
348
|
const messages = [
|
|
320
349
|
{
|
|
321
350
|
name: "system",
|
|
322
351
|
role: "system",
|
|
323
|
-
content:
|
|
324
|
-
"You are a sentence completion tool. ONLY provide the completion text (what comes after the user's text), never repeat any part of what the user wrote. Your completion should flow naturally from the user's text.",
|
|
352
|
+
content: systemPrompt,
|
|
325
353
|
},
|
|
326
354
|
{
|
|
327
355
|
name: "user",
|
|
328
356
|
role: "user",
|
|
329
|
-
content: `Complete this
|
|
357
|
+
content: `Complete this text. ONLY provide the completion (do not repeat my text): ${contentUpToCursor}`,
|
|
330
358
|
},
|
|
331
359
|
];
|
|
332
360
|
|
|
333
|
-
if (!editContext) return null;
|
|
334
|
-
|
|
335
361
|
// Show loading indicator
|
|
336
362
|
setIsLoading(true);
|
|
337
363
|
startLoadingAnimation();
|
|
@@ -18,18 +18,38 @@ export function EditorForm({
|
|
|
18
18
|
readonly,
|
|
19
19
|
compareView,
|
|
20
20
|
onCollapse,
|
|
21
|
+
initialActiveTab,
|
|
21
22
|
}: {
|
|
22
23
|
pageViewContext?: PageViewContext;
|
|
23
24
|
readonly?: boolean;
|
|
24
25
|
compareView: boolean;
|
|
25
26
|
onCollapse?: () => void;
|
|
27
|
+
initialActiveTab?: string | null;
|
|
26
28
|
}) {
|
|
27
29
|
const editContext = useEditContext()!;
|
|
28
30
|
if (!pageViewContext) pageViewContext = editContext.pageView;
|
|
29
31
|
|
|
30
32
|
const insertMode =
|
|
31
33
|
editContext.insertMode && editContext.mode === "edit" && !compareView;
|
|
32
|
-
const [activeTabKey, setActiveTabKey] = useState(
|
|
34
|
+
const [activeTabKey, setActiveTabKey] = useState(
|
|
35
|
+
initialActiveTab || "content",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Switch to the tab requested by the EditContext
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (editContext.activeEditorTab) {
|
|
41
|
+
setActiveTabKey(editContext.activeEditorTab);
|
|
42
|
+
// Clear the request after switching
|
|
43
|
+
editContext.setActiveEditorTab(null);
|
|
44
|
+
}
|
|
45
|
+
}, [editContext.activeEditorTab, editContext.setActiveEditorTab]);
|
|
46
|
+
|
|
47
|
+
// Set initial active tab if provided
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (initialActiveTab) {
|
|
50
|
+
setActiveTabKey(initialActiveTab);
|
|
51
|
+
}
|
|
52
|
+
}, [initialActiveTab]);
|
|
33
53
|
const [item, setItem] = useState<RenderedItem>();
|
|
34
54
|
const [component, setComponent] = useState<Component>();
|
|
35
55
|
const [fullItem, setFullItem] = useState<FullItem>();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SyntheticEvent,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { EditorForm } from "./EditorForm";
|
|
11
|
+
import { PageViewContext } from "./pageViewContext";
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverAnchor,
|
|
16
|
+
} from "../../components/ui/popover";
|
|
17
|
+
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
18
|
+
import { X } from "lucide-react";
|
|
19
|
+
|
|
20
|
+
export interface EditorFormPopupRef {
|
|
21
|
+
show: (event: SyntheticEvent, activeTab?: string) => void;
|
|
22
|
+
close: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface EditorFormPopupProps {
|
|
26
|
+
pageViewContext?: PageViewContext;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const EditorFormPopup = forwardRef<
|
|
30
|
+
EditorFormPopupRef,
|
|
31
|
+
EditorFormPopupProps
|
|
32
|
+
>(({ pageViewContext }, ref) => {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
34
|
+
const [anchorPosition, setAnchorPosition] = useState({ x: 0, y: 0 });
|
|
35
|
+
const [requestedTab, setRequestedTab] = useState<string | null>(null);
|
|
36
|
+
const anchorRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
useImperativeHandle(ref, () => ({
|
|
39
|
+
show: (ev: SyntheticEvent, activeTab?: string) => {
|
|
40
|
+
setRequestedTab(activeTab || null);
|
|
41
|
+
|
|
42
|
+
// Get the position from the event to position the anchor
|
|
43
|
+
const rect = (ev.target as HTMLElement)?.getBoundingClientRect();
|
|
44
|
+
if (rect) {
|
|
45
|
+
setAnchorPosition({
|
|
46
|
+
x: rect.left + rect.width / 2,
|
|
47
|
+
y: rect.top + rect.height / 2,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setIsOpen(true);
|
|
52
|
+
},
|
|
53
|
+
close: () => {
|
|
54
|
+
setIsOpen(false);
|
|
55
|
+
setRequestedTab(null);
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
61
|
+
{/* Invisible anchor positioned at the click location */}
|
|
62
|
+
<PopoverAnchor asChild>
|
|
63
|
+
<div
|
|
64
|
+
ref={anchorRef}
|
|
65
|
+
style={{
|
|
66
|
+
position: "fixed",
|
|
67
|
+
left: anchorPosition.x,
|
|
68
|
+
top: anchorPosition.y,
|
|
69
|
+
width: 1,
|
|
70
|
+
height: 1,
|
|
71
|
+
pointerEvents: "none",
|
|
72
|
+
zIndex: -1,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
</PopoverAnchor>
|
|
76
|
+
|
|
77
|
+
<PopoverContent
|
|
78
|
+
className="h-[600px] w-[400px] p-0"
|
|
79
|
+
align="start"
|
|
80
|
+
onClick={(ev) => ev.stopPropagation()}
|
|
81
|
+
>
|
|
82
|
+
<div className="flex h-full w-full flex-col">
|
|
83
|
+
<div className="flex items-center justify-between border-b p-2">
|
|
84
|
+
<h3 className="text-sm font-medium">Editor Form</h3>
|
|
85
|
+
<SimpleIconButton
|
|
86
|
+
icon={<X strokeWidth={1} />}
|
|
87
|
+
onClick={() => setIsOpen(false)}
|
|
88
|
+
label="Close"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex-1 overflow-hidden">
|
|
92
|
+
<EditorForm
|
|
93
|
+
pageViewContext={pageViewContext}
|
|
94
|
+
readonly={false}
|
|
95
|
+
compareView={false}
|
|
96
|
+
onCollapse={() => setIsOpen(false)}
|
|
97
|
+
initialActiveTab={requestedTab}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</PopoverContent>
|
|
102
|
+
</Popover>
|
|
103
|
+
);
|
|
104
|
+
});
|