@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.
Files changed (87) hide show
  1. package/dist/components/ui/badge.d.ts +1 -1
  2. package/dist/components/ui/button.d.ts +2 -2
  3. package/dist/components/ui/switch.js +1 -1
  4. package/dist/components/ui/switch.js.map +1 -1
  5. package/dist/config/config.js +18 -2
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/editor/AspectRatioSelector.d.ts +13 -0
  8. package/dist/editor/AspectRatioSelector.js +71 -0
  9. package/dist/editor/AspectRatioSelector.js.map +1 -0
  10. package/dist/editor/ConfirmationDialog.js +4 -5
  11. package/dist/editor/ConfirmationDialog.js.map +1 -1
  12. package/dist/editor/PictureCropper.d.ts +1 -1
  13. package/dist/editor/PictureCropper.js +466 -113
  14. package/dist/editor/PictureCropper.js.map +1 -1
  15. package/dist/editor/Terminal.js +5 -4
  16. package/dist/editor/Terminal.js.map +1 -1
  17. package/dist/editor/ai/AiTerminal.js +20 -2
  18. package/dist/editor/ai/AiTerminal.js.map +1 -1
  19. package/dist/editor/client/EditorClient.js +14 -3
  20. package/dist/editor/client/EditorClient.js.map +1 -1
  21. package/dist/editor/client/editContext.d.ts +3 -0
  22. package/dist/editor/client/editContext.js.map +1 -1
  23. package/dist/editor/commands/componentCommands.js +13 -5
  24. package/dist/editor/commands/componentCommands.js.map +1 -1
  25. package/dist/editor/media-selector/Preview.js +1 -1
  26. package/dist/editor/media-selector/Preview.js.map +1 -1
  27. package/dist/editor/page-editor-chrome/FrameMenu.js +48 -24
  28. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  29. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
  30. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +6 -6
  31. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  32. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +26 -4
  33. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  34. package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
  35. package/dist/editor/page-viewer/EditorForm.js +16 -2
  36. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  37. package/dist/editor/page-viewer/EditorFormPopup.d.ts +11 -0
  38. package/dist/editor/page-viewer/EditorFormPopup.js +41 -0
  39. package/dist/editor/page-viewer/EditorFormPopup.js.map +1 -0
  40. package/dist/editor/page-viewer/PageViewer.js +4 -6
  41. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  42. package/dist/editor/services/contextService.d.ts +26 -0
  43. package/dist/editor/services/contextService.js +102 -0
  44. package/dist/editor/services/contextService.js.map +1 -0
  45. package/dist/editor/sidebar/Completions.d.ts +1 -0
  46. package/dist/editor/sidebar/Completions.js +54 -0
  47. package/dist/editor/sidebar/Completions.js.map +1 -0
  48. package/dist/editor/sidebar/Validation.js +3 -3
  49. package/dist/editor/sidebar/Validation.js.map +1 -1
  50. package/dist/editor/ui/PerfectTree.js +17 -3
  51. package/dist/editor/ui/PerfectTree.js.map +1 -1
  52. package/dist/editor/ui/SimpleTabs.d.ts +2 -1
  53. package/dist/editor/ui/SimpleTabs.js +2 -2
  54. package/dist/editor/ui/SimpleTabs.js.map +1 -1
  55. package/dist/revision.d.ts +2 -2
  56. package/dist/revision.js +2 -2
  57. package/dist/styles.css +134 -30
  58. package/dist/types.d.ts +1 -0
  59. package/package.json +1 -1
  60. package/src/components/ui/switch.tsx +1 -1
  61. package/src/config/config.tsx +18 -1
  62. package/src/editor/AspectRatioSelector.tsx +146 -0
  63. package/src/editor/ConfirmationDialog.tsx +36 -45
  64. package/src/editor/PictureCropper.tsx +724 -233
  65. package/src/editor/Terminal.tsx +9 -8
  66. package/src/editor/ai/AiTerminal.tsx +58 -15
  67. package/src/editor/client/EditorClient.tsx +26 -1
  68. package/src/editor/client/editContext.ts +7 -0
  69. package/src/editor/commands/componentCommands.tsx +14 -9
  70. package/src/editor/media-selector/Preview.tsx +7 -5
  71. package/src/editor/page-editor-chrome/FrameMenu.tsx +70 -15
  72. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -3
  73. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +31 -5
  74. package/src/editor/page-viewer/EditorForm.tsx +21 -1
  75. package/src/editor/page-viewer/EditorFormPopup.tsx +104 -0
  76. package/src/editor/page-viewer/PageViewer.tsx +3 -11
  77. package/src/editor/services/contextService.ts +146 -0
  78. package/src/editor/sidebar/Completions.tsx +160 -0
  79. package/src/editor/sidebar/Validation.tsx +9 -10
  80. package/src/editor/ui/PerfectTree.tsx +19 -3
  81. package/src/editor/ui/SimpleTabs.tsx +4 -1
  82. package/src/revision.ts +2 -2
  83. package/src/types.ts +1 -0
  84. package/dist/editor/menubar/BrowseHistory.d.ts +0 -6
  85. package/dist/editor/menubar/BrowseHistory.js +0 -11
  86. package/dist/editor/menubar/BrowseHistory.js.map +0 -1
  87. package/src/editor/menubar/BrowseHistory.tsx +0 -28
@@ -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
- <button
166
+ <SimpleIconButton
165
167
  onClick={() => {
166
168
  setMessages([]);
167
169
  onReset();
168
170
  }}
169
- >
170
- <i className="pi pi-trash m-1 text-sm" />
171
- </button>
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
- <Button
216
- icon={"pi pi-send"}
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 text-blue-300"
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
- <Dropdown
371
- className="text-sm"
372
- value={activeProfile}
373
- onChange={(e) => setActiveProfile(e.value)}
374
- optionLabel="name"
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: undefined,
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 (!context.event) {
172
- console.warn("Design command executed without event context");
173
- return;
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
- <img
46
- src={selectedImage?.previewUrl}
47
- className="object-contain"
48
- fetchPriority="high"
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
- <div
316
- className="cursor-pointer hover:text-gray-200"
317
- title={b.label}
318
- key={i}
319
- onClick={(ev) => {
320
- ev.stopPropagation();
321
- b.onClick(ev);
322
- }}
323
- >
324
- {typeof b.icon === "string" ? (
325
- <i className={b.icon + " cursor-pointer text-sm"} />
326
- ) : (
327
- b.icon
328
- )}
329
- </div>
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={classNames(
152
+ className={cn(
148
153
  isHovering
149
- ? "z-30 h-20 w-20 shadow-xl shadow-black shadow-blue-500/50"
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 sentence. ONLY provide the completion (do not repeat my text): ${contentUpToCursor}`,
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("content");
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
+ });