@parhelia/core 0.1.12390 → 0.1.12397

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 (79) hide show
  1. package/dist/editor/Editor.js +32 -17
  2. package/dist/editor/Editor.js.map +1 -1
  3. package/dist/editor/PictureCropper.js +9 -4
  4. package/dist/editor/PictureCropper.js.map +1 -1
  5. package/dist/editor/PictureEditor.js +12 -13
  6. package/dist/editor/PictureEditor.js.map +1 -1
  7. package/dist/editor/SetupWizard.js +20 -2
  8. package/dist/editor/SetupWizard.js.map +1 -1
  9. package/dist/editor/ai/AgentTerminal.js +14 -3
  10. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  11. package/dist/editor/ai/dialogs/agentDialogTypes.d.ts +1 -1
  12. package/dist/editor/client/editContext.d.ts +4 -0
  13. package/dist/editor/client/editContext.js.map +1 -1
  14. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +283 -298
  15. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  16. package/dist/editor/pictureRawValue.d.ts +3 -0
  17. package/dist/editor/pictureRawValue.js +30 -0
  18. package/dist/editor/pictureRawValue.js.map +1 -0
  19. package/dist/editor/services/templateBuilderService.d.ts +7 -0
  20. package/dist/editor/services/templateBuilderService.js +7 -1
  21. package/dist/editor/services/templateBuilderService.js.map +1 -1
  22. package/dist/editor/settings/About.js +25 -19
  23. package/dist/editor/settings/About.js.map +1 -1
  24. package/dist/editor/settings/panels/AgentProfileEditorPanel.d.ts +14 -0
  25. package/dist/editor/settings/panels/AgentProfileEditorPanel.js +7 -0
  26. package/dist/editor/settings/panels/AgentProfileEditorPanel.js.map +1 -0
  27. package/dist/editor/settings/panels/AgentsPanel.js +2 -2
  28. package/dist/editor/settings/panels/AgentsPanel.js.map +1 -1
  29. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +146 -8
  30. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  31. package/dist/editor/setup-wizard/steps/CompleteStep.d.ts +2 -1
  32. package/dist/editor/setup-wizard/steps/CompleteStep.js +2 -1
  33. package/dist/editor/setup-wizard/steps/CompleteStep.js.map +1 -1
  34. package/dist/editor/setup-wizard/steps/LicenseActivationStep.d.ts +9 -0
  35. package/dist/editor/setup-wizard/steps/LicenseActivationStep.js +160 -0
  36. package/dist/editor/setup-wizard/steps/LicenseActivationStep.js.map +1 -0
  37. package/dist/editor/setup-wizard/steps/LicenseEmailStep.d.ts +10 -0
  38. package/dist/editor/setup-wizard/steps/LicenseEmailStep.js +101 -0
  39. package/dist/editor/setup-wizard/steps/LicenseEmailStep.js.map +1 -0
  40. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +422 -65
  41. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js.map +1 -1
  42. package/dist/licensing/EmailEntry.js +1 -1
  43. package/dist/licensing/EmailEntry.js.map +1 -1
  44. package/dist/licensing/LicenseActivationForm.js +1 -1
  45. package/dist/licensing/LicenseActivationForm.js.map +1 -1
  46. package/dist/licensing/LicenseCodeEntry.js +2 -2
  47. package/dist/licensing/LicenseCodeEntry.js.map +1 -1
  48. package/dist/licensing/LicenseContext.js +18 -9
  49. package/dist/licensing/LicenseContext.js.map +1 -1
  50. package/dist/licensing/LicenseOverlay.js +2 -1
  51. package/dist/licensing/LicenseOverlay.js.map +1 -1
  52. package/dist/licensing/licenseService.d.ts +10 -0
  53. package/dist/licensing/licenseService.js +28 -0
  54. package/dist/licensing/licenseService.js.map +1 -1
  55. package/dist/revision.d.ts +2 -2
  56. package/dist/revision.js +2 -2
  57. package/dist/task-board/TaskBoardWorkspace.js +3 -2
  58. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  59. package/dist/task-board/components/ProjectDashboard.d.ts +4 -0
  60. package/dist/task-board/components/ProjectDashboard.js +1 -1
  61. package/dist/task-board/components/ProjectDashboard.js.map +1 -1
  62. package/dist/task-board/components/ProjectListContent.d.ts +1 -1
  63. package/dist/task-board/components/ProjectListContent.js +4 -1
  64. package/dist/task-board/components/ProjectListContent.js.map +1 -1
  65. package/dist/task-board/components/ProjectOverviewContent.d.ts +17 -0
  66. package/dist/task-board/components/ProjectOverviewContent.js +134 -0
  67. package/dist/task-board/components/ProjectOverviewContent.js.map +1 -0
  68. package/dist/task-board/components/ProjectSelector.d.ts +1 -1
  69. package/dist/task-board/components/ProjectSelector.js +1 -1
  70. package/dist/task-board/components/ProjectSelector.js.map +1 -1
  71. package/dist/task-board/components/TaskDetailPanel.js +59 -9
  72. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  73. package/dist/task-board/services/taskService.d.ts +4 -1
  74. package/dist/task-board/services/taskService.js +3 -0
  75. package/dist/task-board/services/taskService.js.map +1 -1
  76. package/dist/task-board/taskBoardNavStore.d.ts +3 -1
  77. package/dist/task-board/taskBoardNavStore.js.map +1 -1
  78. package/dist/task-board/types.d.ts +30 -0
  79. package/package.json +1 -1
@@ -1,17 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  import { useEffect, useCallback, useState, useMemo, useRef, } from "react";
2
3
  import { useDebouncedCallback } from "use-debounce";
3
4
  import { useEditContext, useFieldsEditContext } from "../client/editContext";
4
5
  import { generatePageContext, getCachedContext, } from "../services/contextService";
6
+ import { WandSparkles } from "lucide-react";
7
+ import { createRoot } from "react-dom/client";
8
+ function InlineCompletionHint({ hintText, isMobile, onAccept, positionStyle, }) {
9
+ return (_jsxs("div", { className: "shadow-[0_4px_14px_rgba(15,23,42,0.14),0_0_0_1px_rgba(15,23,42,0.06)]pointer-events-auto fixed z-95 inline-flex max-w-none cursor-pointer items-center gap-2 rounded-md border border-slate-400 bg-slate-100 px-2.5 py-2 text-xs leading-snug font-medium whitespace-nowrap text-slate-900", style: positionStyle, onClick: isMobile ? onAccept : undefined, role: isMobile ? "button" : undefined, children: [_jsx(WandSparkles, { className: "size-3.5 shrink-0 text-violet-600", strokeWidth: 2, "aria-hidden": true }), _jsx("span", { children: hintText })] }));
10
+ }
5
11
  export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatingRef, }) {
6
12
  const editContext = useEditContext();
7
13
  const fieldsContext = useFieldsEditContext();
8
14
  const [currentCompletion, setCurrentCompletion] = useState(null);
9
- const [isLoading, setIsLoading] = useState(false);
15
+ const [, setIsLoading] = useState(false);
10
16
  const abortControllerRef = useRef(null);
11
17
  const loadingAnimationRef = useRef(null);
18
+ const applyCompletionRef = useRef(null);
19
+ const hintReactRootRef = useRef(null);
12
20
  // Clean up hint element on unmount
13
21
  useEffect(() => {
14
22
  return () => {
23
+ hintReactRootRef.current?.unmount();
24
+ hintReactRootRef.current = null;
15
25
  const hintElement = document.getElementById(`${cursorSpanId}-hint`);
16
26
  if (hintElement) {
17
27
  hintElement.remove();
@@ -23,7 +33,7 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
23
33
  }, [cursorSpanId]);
24
34
  const lastCaretPosRef = useRef(null);
25
35
  // Simple function to track caret position without inserting spans
26
- const positionCursorSpan = () => {
36
+ const positionCursorSpan = useCallback(() => {
27
37
  if (isUpdatingRef.current)
28
38
  return;
29
39
  isUpdatingRef.current = true;
@@ -97,55 +107,14 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
97
107
  isUpdatingRef.current = false;
98
108
  }, 10);
99
109
  }
100
- };
101
- useEffect(() => {
102
- // Handle keydown events - especially for cursor movement
103
- const keyHandler = (e) => {
104
- if (e.key === "ArrowLeft" ||
105
- e.key === "ArrowRight" ||
106
- e.key === "ArrowUp" ||
107
- e.key === "ArrowDown" ||
108
- e.key === " " ||
109
- e.key === "Tab" ||
110
- e.key === "End" ||
111
- e.key === "Backspace" ||
112
- e.key === "Delete" // Add Delete key handling
113
- ) {
114
- // Clear the completion when arrow keys are used
115
- setCurrentCompletion(null);
116
- clearCursorSpan();
117
- // Let the browser handle the cursor movement/deletion
118
- // Then update our cursor span after a small delay
119
- setTimeout(() => {
120
- if (!isUpdatingRef.current) {
121
- positionCursorSpan();
122
- }
123
- }, 10);
124
- // Special handling for right arrow and delete which seems to have issues
125
- if (e.key === "ArrowRight" || e.key === "Delete") {
126
- // Make sure the cursor span doesn't block the movement/deletion
127
- const cursorSpan = pageViewContext.editorIframe?.contentWindow?.document.getElementById(cursorSpanId);
128
- if (cursorSpan) {
129
- // Temporarily make it display none so it doesn't interfere with selection
130
- const originalDisplay = cursorSpan.style.display;
131
- cursorSpan.style.display = "none";
132
- // Restore after the browser has processed the movement/deletion
133
- setTimeout(() => {
134
- if (cursorSpan.parentNode) {
135
- cursorSpan.style.display = originalDisplay;
136
- }
137
- }, 0);
138
- }
139
- }
140
- }
141
- };
142
- pageViewContext.editorIframe?.contentWindow?.document.addEventListener("keydown", keyHandler);
143
- return () => {
144
- pageViewContext.editorIframe?.contentWindow?.document.removeEventListener("keydown", keyHandler);
145
- };
146
- }, [currentCompletion, pageViewContext.page]);
110
+ }, [
111
+ pageViewContext.editorIframe?.contentWindow,
112
+ fieldsContext?.inlineEditingFieldElement,
113
+ cursorSpanId,
114
+ isUpdatingRef,
115
+ ]);
147
116
  // Extracts the text up to the cursor position in the editable element
148
- const getContentUpToCursor = (element) => {
117
+ const getContentUpToCursor = useCallback((element) => {
149
118
  const iframeWindow = pageViewContext.editorIframe?.contentWindow;
150
119
  const selection = iframeWindow?.getSelection();
151
120
  if (!element || !selection || selection.rangeCount === 0)
@@ -168,19 +137,17 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
168
137
  contentUpToCursor = tempRange.toString();
169
138
  }
170
139
  return contentUpToCursor;
171
- };
140
+ }, [pageViewContext.editorIframe?.contentWindow]);
172
141
  // Loading animation with three dots changing color
173
- const startLoadingAnimation = () => {
142
+ const startLoadingAnimation = useCallback(() => {
174
143
  const doc = pageViewContext.editorIframe?.contentWindow?.document;
175
144
  const span = doc?.getElementById(cursorSpanId);
176
145
  if (!doc || !span)
177
146
  return;
178
- // Create dots container
179
147
  span.innerHTML = "";
180
148
  span.style.display = "inline-flex";
181
149
  span.style.gap = "4px";
182
150
  span.style.alignItems = "center";
183
- // Create three dots
184
151
  for (let i = 0; i < 3; i++) {
185
152
  const dot = doc.createElement("span");
186
153
  dot.textContent = "•";
@@ -189,7 +156,6 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
189
156
  dot.style.fontSize = "16px";
190
157
  span.appendChild(dot);
191
158
  }
192
- // Animate dots
193
159
  let step = 0;
194
160
  const animate = () => {
195
161
  const dots = span.querySelectorAll("span");
@@ -203,7 +169,6 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
203
169
  });
204
170
  step++;
205
171
  loadingAnimationRef.current = requestAnimationFrame(() => {
206
- // Slow down animation by only updating every 15 frames (~250ms at 60fps)
207
172
  if (step % 15 === 0) {
208
173
  animate();
209
174
  }
@@ -213,34 +178,27 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
213
178
  });
214
179
  };
215
180
  animate();
216
- };
217
- const stopLoadingAnimation = () => {
181
+ }, [pageViewContext.editorIframe?.contentWindow?.document, cursorSpanId]);
182
+ const stopLoadingAnimation = useCallback(() => {
218
183
  if (loadingAnimationRef.current) {
219
184
  cancelAnimationFrame(loadingAnimationRef.current);
220
185
  loadingAnimationRef.current = null;
221
186
  }
222
- };
223
- const getCompletion = async (element, isManualTrigger = false) => {
187
+ }, []);
188
+ const getCompletion = useCallback(async (element, isManualTrigger = false) => {
224
189
  const contentUpToCursor = getContentUpToCursor(element);
225
190
  if (!contentUpToCursor?.trim())
226
191
  return null;
227
- // Abort any in-flight request
228
192
  if (abortControllerRef.current) {
229
193
  abortControllerRef.current.abort();
230
194
  }
231
- // Create a new abort controller for this request
232
195
  abortControllerRef.current = new AbortController();
233
- const signal = abortControllerRef.current.signal;
234
- // Get field attributes
235
196
  const fieldId = element.getAttribute("data-fieldid");
236
- const fieldName = element.getAttribute("data-fieldname");
237
197
  const itemId = element.getAttribute("data-itemid");
238
198
  const language = element.getAttribute("data-language");
239
199
  const version = element.getAttribute("data-version");
240
200
  if (!fieldId || !itemId || !language || !version)
241
201
  return null;
242
- // Only trigger completion after a space for automatic completions
243
- // Manual triggers (Ctrl+Space) can work anywhere
244
202
  if (!isManualTrigger) {
245
203
  const lastChar = contentUpToCursor.slice(-1);
246
204
  if (lastChar !== " " && lastChar !== "\u00A0") {
@@ -249,7 +207,6 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
249
207
  }
250
208
  if (!editContext)
251
209
  return null;
252
- // Get page context for better completions
253
210
  let pageContext = getCachedContext(editContext);
254
211
  if (!pageContext) {
255
212
  try {
@@ -257,22 +214,20 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
257
214
  }
258
215
  catch (error) {
259
216
  console.warn("Failed to generate page context:", error);
260
- // Continue without context
261
217
  }
262
218
  }
263
219
  let contextString = "";
264
220
  if (pageContext) {
265
221
  contextString = `Page Name: ${pageContext.pageTitle} (${pageContext.pageType})\n PageSummary: ${pageContext.abstract}`;
266
222
  }
267
- // Show loading indicator
268
223
  setIsLoading(true);
269
224
  startLoadingAnimation();
270
225
  try {
271
226
  const endpoint = `/parhelia/agent/GetTextCompletion`;
272
227
  const response = await fetch(endpoint, {
273
- method: 'POST',
228
+ method: "POST",
274
229
  headers: {
275
- 'Content-Type': 'application/x-www-form-urlencoded',
230
+ "Content-Type": "application/x-www-form-urlencoded",
276
231
  },
277
232
  body: new URLSearchParams({
278
233
  textToComplete: contentUpToCursor,
@@ -283,18 +238,22 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
283
238
  return data.completion || null;
284
239
  }
285
240
  catch (error) {
286
- // Ignore AbortError as it's expected when cancelling
287
241
  if (error instanceof Error && error.name !== "AbortError") {
288
242
  console.error("Error getting completion:", error);
289
243
  }
290
244
  return null;
291
245
  }
292
246
  finally {
293
- // Hide loading indicator
294
247
  setIsLoading(false);
295
248
  stopLoadingAnimation();
296
249
  }
297
- };
250
+ }, [
251
+ getContentUpToCursor,
252
+ editContext,
253
+ pageViewContext,
254
+ startLoadingAnimation,
255
+ stopLoadingAnimation,
256
+ ]);
298
257
  // Debounced AI call: recompute the sentence, call getCompletion, and extract only the "tail" for the ghost text
299
258
  const getCompletionDebounced = useDebouncedCallback(async (isManualTrigger = false) => {
300
259
  const el = fieldsContext?.inlineEditingFieldElement;
@@ -317,50 +276,17 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
317
276
  setCurrentCompletion(suggestion);
318
277
  updateCursorSpan(suggestion.substring(sentence.length));
319
278
  }, 250);
320
- // Manual completion trigger (non-debounced for immediate response)
321
- const getCompletionManual = async () => {
322
- const el = fieldsContext?.inlineEditingFieldElement;
323
- if (!el)
324
- return;
325
- // 1) Recompute the exact sentence at this moment
326
- const full = getContentUpToCursor(el) || "";
327
- const sentence = full.split(/[.?!]\s*/).pop() || "";
328
- // 2) Ask AI for a completion
329
- const rawSuggestion = await getCompletion(el, true);
330
- if (!rawSuggestion) {
331
- setCurrentCompletion(null);
332
- clearCursorSpan();
333
- return;
334
- }
335
- // 3) Strip off the already-typed sentence to leave just the "completion tail"
336
- const suggestion = rawSuggestion.startsWith(sentence)
337
- ? rawSuggestion
338
- : sentence + rawSuggestion;
339
- setCurrentCompletion(suggestion);
340
- updateCursorSpan(suggestion.substring(sentence.length));
341
- };
342
279
  // Inserts or clears the ghost text inside the cursor span
343
- const updateCursorSpan = (text) => {
280
+ const updateCursorSpan = useCallback((text) => {
344
281
  const doc = pageViewContext.editorIframe?.contentWindow?.document;
345
282
  const span = doc?.getElementById(cursorSpanId);
346
283
  if (!doc || !span)
347
284
  return;
348
285
  // Update the completion text
349
286
  if (text) {
350
- // // Create a temporary span to measure the text width
351
- // const measureSpan = doc.createElement("span");
352
- // measureSpan.style.visibility = "hidden";
353
- // measureSpan.style.position = "absolute";
354
- // measureSpan.style.whiteSpace = "pre"; // Preserve whitespace
355
- // measureSpan.style.font = window.getComputedStyle(span).font; // Match the font
356
- // measureSpan.textContent = text;
357
- // doc.body.appendChild(measureSpan);
358
- // const textWidth = measureSpan.getBoundingClientRect().width;
359
- // measureSpan.remove();
360
287
  span.textContent = text;
361
288
  span.style.color = "#888";
362
289
  span.style.fontStyle = "italic";
363
- //span.innerHTML = "hello";
364
290
  // Create or update hint element in the main document
365
291
  let hintElement = document.getElementById(`${cursorSpanId}-hint`);
366
292
  if (!hintElement) {
@@ -368,68 +294,264 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
368
294
  hintElement.id = `${cursorSpanId}-hint`;
369
295
  document.body.appendChild(hintElement);
370
296
  }
371
- hintElement.textContent = "Press Tab to accept ⇥";
372
- // Apply styles that match Tailwind's utility classes
373
- Object.assign(hintElement.style, {
374
- position: "fixed",
375
- fontSize: "12px",
376
- fontWeight: "normal",
377
- color: "rgb(75, 85, 99)",
378
- backgroundColor: "white",
379
- padding: "0.5rem",
380
- marginLeft: "0.25rem",
381
- marginRight: "0.25rem",
382
- borderRadius: "0.25rem",
383
- border: "1px solid rgb(229, 231, 235)",
384
- fontStyle: "normal",
385
- display: "block",
386
- lineHeight: "1.4",
387
- zIndex: "95",
388
- pointerEvents: "none",
389
- boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
390
- });
391
- // Position the hint element relative to the iframe and cursor span
297
+ if (!hintReactRootRef.current) {
298
+ hintElement.replaceChildren();
299
+ hintReactRootRef.current = createRoot(hintElement);
300
+ }
301
+ const hintLabel = editContext?.isMobile
302
+ ? "Tap to accept"
303
+ : "Press Tab to accept ⇥";
304
+ const isMobile = editContext?.isMobile ?? false;
392
305
  const iframeRect = pageViewContext.editorIframe?.getBoundingClientRect();
393
306
  const spanRect = span.getBoundingClientRect();
307
+ let positionStyle = {
308
+ marginLeft: "0.25rem",
309
+ marginRight: "0.25rem",
310
+ };
394
311
  if (iframeRect) {
395
- // Create a range for the last character to get proper position
396
- const textLength = span.textContent?.length || 0;
397
- if (textLength > 0 && span.firstChild) {
398
- const range = doc.createRange();
399
- range.setStart(span.firstChild, Math.max(0, textLength - 1));
400
- range.setEnd(span.firstChild, textLength);
401
- const rangeRect = range.getBoundingClientRect();
402
- // Position at the end of the actual text
403
- const absoluteLeft = iframeRect.left + rangeRect.right;
404
- const absoluteTop = iframeRect.top + rangeRect.top;
405
- hintElement.style.left = `${absoluteLeft}px`;
406
- hintElement.style.top = `${absoluteTop}px`;
312
+ if (isMobile) {
313
+ const centerX = (spanRect.left + spanRect.right) / 2;
314
+ const gap = 6;
315
+ positionStyle = {
316
+ ...positionStyle,
317
+ left: centerX,
318
+ top: spanRect.top - gap,
319
+ transform: "translate(-50%, -100%)",
320
+ };
407
321
  }
408
322
  else {
409
- // Fallback to the old positioning if there's no text
410
- const absoluteLeft = iframeRect.left + spanRect.right;
411
- const absoluteTop = iframeRect.top + spanRect.top;
412
- hintElement.style.left = `${absoluteLeft}px`;
413
- hintElement.style.top = `${absoluteTop}px`;
323
+ const textLength = span.textContent?.length || 0;
324
+ let left;
325
+ let top;
326
+ if (textLength > 0 && span.firstChild) {
327
+ const range = doc.createRange();
328
+ range.setStart(span.firstChild, Math.max(0, textLength - 1));
329
+ range.setEnd(span.firstChild, textLength);
330
+ const rangeRect = range.getBoundingClientRect();
331
+ left = iframeRect.left + rangeRect.right;
332
+ top = iframeRect.top + rangeRect.top;
333
+ }
334
+ else {
335
+ left = iframeRect.left + spanRect.right;
336
+ top = iframeRect.top + spanRect.top;
337
+ }
338
+ positionStyle = {
339
+ ...positionStyle,
340
+ left,
341
+ top,
342
+ transform: undefined,
343
+ };
414
344
  }
415
345
  }
346
+ hintReactRootRef.current.render(_jsx(InlineCompletionHint, { hintText: hintLabel, isMobile: isMobile, onAccept: () => applyCompletionRef.current?.(), positionStyle: positionStyle }));
416
347
  }
417
348
  else {
418
349
  span.textContent = "";
419
350
  // Remove hint element if it exists
420
351
  const hintElement = document.getElementById(`${cursorSpanId}-hint`);
421
352
  if (hintElement) {
353
+ hintReactRootRef.current?.unmount();
354
+ hintReactRootRef.current = null;
422
355
  hintElement.remove();
423
356
  }
424
357
  }
425
- };
426
- const clearCursorSpan = () => {
358
+ }, [pageViewContext.editorIframe, cursorSpanId, editContext?.isMobile]);
359
+ const clearCursorSpan = useCallback(() => {
427
360
  const doc = pageViewContext.editorIframe?.contentWindow?.document;
428
361
  if (!doc)
429
362
  return;
430
363
  // Clear the completion text
431
364
  updateCursorSpan("");
432
- };
365
+ }, [pageViewContext.editorIframe?.contentWindow?.document, updateCursorSpan]);
366
+ // Handle keydown events for cursor movement (arrow keys, etc.)
367
+ useEffect(() => {
368
+ const keyHandler = (e) => {
369
+ if (e.key === "ArrowLeft" ||
370
+ e.key === "ArrowRight" ||
371
+ e.key === "ArrowUp" ||
372
+ e.key === "ArrowDown" ||
373
+ e.key === " " ||
374
+ e.key === "Tab" ||
375
+ e.key === "End" ||
376
+ e.key === "Backspace" ||
377
+ e.key === "Delete") {
378
+ setCurrentCompletion(null);
379
+ clearCursorSpan();
380
+ setTimeout(() => {
381
+ if (!isUpdatingRef.current) {
382
+ positionCursorSpan();
383
+ }
384
+ }, 10);
385
+ if (e.key === "ArrowRight" || e.key === "Delete") {
386
+ const cursorSpan = pageViewContext.editorIframe?.contentWindow?.document.getElementById(cursorSpanId);
387
+ if (cursorSpan) {
388
+ const originalDisplay = cursorSpan.style.display;
389
+ cursorSpan.style.display = "none";
390
+ setTimeout(() => {
391
+ if (cursorSpan.parentNode) {
392
+ cursorSpan.style.display = originalDisplay;
393
+ }
394
+ }, 0);
395
+ }
396
+ }
397
+ }
398
+ };
399
+ const doc = pageViewContext.editorIframe?.contentWindow?.document;
400
+ if (!doc)
401
+ return;
402
+ doc.addEventListener("keydown", keyHandler);
403
+ return () => doc.removeEventListener("keydown", keyHandler);
404
+ }, [
405
+ clearCursorSpan,
406
+ cursorSpanId,
407
+ isUpdatingRef,
408
+ pageViewContext.editorIframe?.contentWindow?.document,
409
+ pageViewContext.page,
410
+ positionCursorSpan,
411
+ ]);
412
+ // Function to apply the completion (must be before handleInput)
413
+ const applyCompletion = useCallback(() => {
414
+ const iframeWindow = pageViewContext.editorIframe?.contentWindow;
415
+ const iframeDocument = iframeWindow?.document;
416
+ if (!iframeWindow ||
417
+ !iframeDocument ||
418
+ !fieldsContext?.inlineEditingFieldElement)
419
+ return;
420
+ const cursorSpan = iframeDocument.getElementById(cursorSpanId);
421
+ if (!cursorSpan)
422
+ return;
423
+ const completionToApply = cursorSpan.textContent || "";
424
+ if (!completionToApply)
425
+ return;
426
+ const element = fieldsContext.inlineEditingFieldElement;
427
+ const fieldId = element.getAttribute("data-fieldid");
428
+ const fieldName = element.getAttribute("data-fieldname");
429
+ const itemId = element.getAttribute("data-itemid");
430
+ const language = element.getAttribute("data-language");
431
+ const versionStr = element.getAttribute("data-version");
432
+ const isRichText = element.getAttribute("data-is-richtext") === "true";
433
+ const version = versionStr ? parseInt(versionStr, 10) : undefined;
434
+ if (!fieldId || !itemId || !language || !version)
435
+ return;
436
+ const selection = iframeWindow.getSelection();
437
+ if (!selection || selection.rangeCount === 0)
438
+ return;
439
+ const range = selection.getRangeAt(0);
440
+ const tempRange = document.createRange();
441
+ tempRange.selectNodeContents(element);
442
+ tempRange.setEnd(range.startContainer, range.startOffset);
443
+ const textUpToCursor = tempRange.toString();
444
+ const wordBoundaryRegex = /[\s.,;:!?"'()[\]{}<>/|=+\-*&^%$#@~`](?=[^\s.,;:!?"'()[\]{}<>/|=+\-*&^%$#@~`]*$)/;
445
+ const match = textUpToCursor.match(wordBoundaryRegex);
446
+ const lastWordBoundaryIndex = match && match.index !== undefined ? match.index + 1 : 0;
447
+ const currentPartialWord = textUpToCursor
448
+ .substring(lastWordBoundaryIndex)
449
+ .trim();
450
+ const isOverlapping = currentPartialWord.length > 0 &&
451
+ completionToApply
452
+ .toLowerCase()
453
+ .startsWith(currentPartialWord.toLowerCase());
454
+ if (isOverlapping) {
455
+ const wordRange = document.createRange();
456
+ const startContainer = range.startContainer;
457
+ const startOffset = range.startOffset - currentPartialWord.length;
458
+ if (startOffset >= 0 && startContainer.nodeType === Node.TEXT_NODE) {
459
+ wordRange.setStart(startContainer, startOffset);
460
+ wordRange.setEnd(range.startContainer, range.startOffset);
461
+ wordRange.deleteContents();
462
+ }
463
+ else {
464
+ if (textUpToCursor.length > 0 && !textUpToCursor.endsWith(" ")) {
465
+ const spaceNode = document.createTextNode(" ");
466
+ range.insertNode(spaceNode);
467
+ range.setStartAfter(spaceNode);
468
+ range.setEndAfter(spaceNode);
469
+ }
470
+ }
471
+ }
472
+ else {
473
+ if (textUpToCursor.length > 0 &&
474
+ !textUpToCursor.endsWith(" ") &&
475
+ !textUpToCursor.endsWith("\n") &&
476
+ !/[.!?\-—:;({[\s]$/.test(textUpToCursor)) {
477
+ const spaceNode = document.createTextNode(" ");
478
+ range.insertNode(spaceNode);
479
+ range.setStartAfter(spaceNode);
480
+ range.setEndAfter(spaceNode);
481
+ }
482
+ }
483
+ const textNode = document.createTextNode(completionToApply);
484
+ range.insertNode(textNode);
485
+ range.setStartAfter(textNode);
486
+ range.setEndAfter(textNode);
487
+ selection.removeAllRanges();
488
+ selection.addRange(range);
489
+ setCurrentCompletion(null);
490
+ clearCursorSpan();
491
+ setTimeout(() => {
492
+ let valueToSave;
493
+ if (isRichText) {
494
+ const clone = element.cloneNode(true);
495
+ const cursorElem = clone.querySelector(`#${cursorSpanId}`);
496
+ if (cursorElem)
497
+ cursorElem.parentNode?.removeChild(cursorElem);
498
+ const ownerDoc = clone.ownerDocument || document;
499
+ const walker = ownerDoc.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
500
+ const toClean = [];
501
+ while (walker.nextNode()) {
502
+ const tn = walker.currentNode;
503
+ if (tn.nodeValue && tn.nodeValue.includes("\u200B"))
504
+ toClean.push(tn);
505
+ }
506
+ toClean.forEach((tn) => (tn.nodeValue = tn.nodeValue?.replaceAll("\u200B", "") || ""));
507
+ valueToSave = clone.innerHTML;
508
+ }
509
+ else {
510
+ valueToSave = (element.innerText || "").replaceAll("\u200B", "");
511
+ }
512
+ editContext?.operations.editField({
513
+ field: {
514
+ fieldId,
515
+ fieldName: fieldName ?? undefined,
516
+ item: { id: itemId, language, version },
517
+ },
518
+ refresh: "none",
519
+ value: valueToSave,
520
+ });
521
+ }, 0);
522
+ }, [
523
+ pageViewContext.editorIframe?.contentWindow,
524
+ fieldsContext?.inlineEditingFieldElement,
525
+ cursorSpanId,
526
+ clearCursorSpan,
527
+ editContext?.operations,
528
+ ]);
529
+ applyCompletionRef.current = applyCompletion;
530
+ // Manual completion trigger (non-debounced)
531
+ const getCompletionManual = useCallback(async () => {
532
+ const el = fieldsContext?.inlineEditingFieldElement;
533
+ if (!el)
534
+ return;
535
+ const full = getContentUpToCursor(el) || "";
536
+ const sentence = full.split(/[.?!]\s*/).pop() || "";
537
+ const rawSuggestion = await getCompletion(el, true);
538
+ if (!rawSuggestion) {
539
+ setCurrentCompletion(null);
540
+ clearCursorSpan();
541
+ return;
542
+ }
543
+ const suggestion = rawSuggestion.startsWith(sentence)
544
+ ? rawSuggestion
545
+ : sentence + rawSuggestion;
546
+ setCurrentCompletion(suggestion);
547
+ updateCursorSpan(suggestion.substring(sentence.length));
548
+ }, [
549
+ fieldsContext?.inlineEditingFieldElement,
550
+ getContentUpToCursor,
551
+ getCompletion,
552
+ clearCursorSpan,
553
+ updateCursorSpan,
554
+ ]);
433
555
  // On every input: either reuse the existing suggestion or fire a new one
434
556
  const handleInput = useCallback((e) => {
435
557
  const el = fieldsContext?.inlineEditingFieldElement;
@@ -518,9 +640,15 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
518
640
  }
519
641
  }, [
520
642
  fieldsContext?.inlineEditingFieldElement,
643
+ getContentUpToCursor,
521
644
  currentCompletion,
522
- getCompletionDebounced,
645
+ clearCursorSpan,
523
646
  getCompletionManual,
647
+ pageViewContext.editorIframe?.contentWindow,
648
+ cursorSpanId,
649
+ applyCompletion,
650
+ updateCursorSpan,
651
+ getCompletionDebounced,
524
652
  ]);
525
653
  // Wire up the input listener
526
654
  useEffect(() => {
@@ -558,6 +686,9 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
558
686
  }, [
559
687
  fieldsContext?.inlineEditingFieldElement,
560
688
  editContext?.enableCompletions,
689
+ isUpdatingRef,
690
+ positionCursorSpan,
691
+ clearCursorSpan,
561
692
  ]);
562
693
  // Clean up abort controller on unmount
563
694
  useEffect(() => {
@@ -572,157 +703,11 @@ export function useInlineAiCompletion({ pageViewContext, cursorSpanId, isUpdatin
572
703
  }
573
704
  };
574
705
  }, []);
575
- // Function to apply the completion
576
- const applyCompletion = () => {
577
- // Get the cursor span to read the most up-to-date completion
578
- const iframeWindow = pageViewContext.editorIframe?.contentWindow;
579
- const iframeDocument = iframeWindow?.document;
580
- if (!iframeWindow ||
581
- !iframeDocument ||
582
- !fieldsContext?.inlineEditingFieldElement)
583
- return;
584
- const cursorSpan = iframeDocument.getElementById(cursorSpanId);
585
- if (!cursorSpan)
586
- return;
587
- // Get the completion text directly from the cursor span, which should be the most current
588
- const completionToApply = cursorSpan.textContent || "";
589
- if (!completionToApply)
590
- return;
591
- const element = fieldsContext?.inlineEditingFieldElement;
592
- // Get field attributes for saving
593
- const fieldId = element.getAttribute("data-fieldid");
594
- const fieldName = element.getAttribute("data-fieldname");
595
- const itemId = element.getAttribute("data-itemid");
596
- const language = element.getAttribute("data-language");
597
- const versionStr = element.getAttribute("data-version");
598
- const isRichText = element.getAttribute("data-is-richtext") === "true";
599
- const version = versionStr ? parseInt(versionStr, 10) : undefined;
600
- if (!fieldId || !itemId || !language || !version)
601
- return;
602
- // Get the current selection position
603
- const selection = iframeWindow.getSelection();
604
- if (!selection || selection.rangeCount === 0)
605
- return;
606
- // Get the text up to the cursor to analyze current word
607
- const range = selection.getRangeAt(0);
608
- const tempRange = document.createRange();
609
- tempRange.selectNodeContents(element);
610
- tempRange.setEnd(range.startContainer, range.startOffset);
611
- const textUpToCursor = tempRange.toString();
612
- // Get the current partial word by finding text from the last word boundary to cursor
613
- // Look for last word boundary (space, punctuation, etc.)
614
- const wordBoundaryRegex = /[\s.,;:!?"'()[\]{}<>\/\\|=+\-*&^%$#@~`](?=[^\s.,;:!?"'()[\]{}<>\/\\|=+\-*&^%$#@~`]*$)/;
615
- const match = textUpToCursor.match(wordBoundaryRegex);
616
- const lastWordBoundaryIndex = match && match.index !== undefined ? match.index + 1 : 0;
617
- const currentPartialWord = textUpToCursor
618
- .substring(lastWordBoundaryIndex)
619
- .trim();
620
- // Check if completion overlaps with current partial word
621
- // (e.g., if user typed "int" and completion is "integer")
622
- const isOverlapping = currentPartialWord.length > 0 &&
623
- completionToApply
624
- .toLowerCase()
625
- .startsWith(currentPartialWord.toLowerCase());
626
- console.log("Is overlapping:", isOverlapping);
627
- // If there's overlap, we need to delete the current partial word
628
- if (isOverlapping) {
629
- // Create a range to select the current partial word
630
- const wordRange = document.createRange();
631
- // Position where the current word starts
632
- let startContainer = range.startContainer;
633
- let startOffset = range.startOffset - currentPartialWord.length;
634
- // We need to handle the case where the word spans multiple text nodes
635
- // For simplicity, we'll try to handle the common case first
636
- if (startOffset >= 0 && startContainer.nodeType === Node.TEXT_NODE) {
637
- // Simple case: word is in the same text node as cursor
638
- wordRange.setStart(startContainer, startOffset);
639
- wordRange.setEnd(range.startContainer, range.startOffset);
640
- wordRange.deleteContents(); // Delete the partial word
641
- }
642
- else {
643
- // Complex case: use a simpler approach - just insert, user can delete manually if needed
644
- // This is a fallback for complex DOM structures
645
- // Add a space if we're in the middle of a sentence
646
- if (textUpToCursor.length > 0 && !textUpToCursor.endsWith(" ")) {
647
- const spaceNode = document.createTextNode(" ");
648
- range.insertNode(spaceNode);
649
- range.setStartAfter(spaceNode);
650
- range.setEndAfter(spaceNode);
651
- }
652
- }
653
- }
654
- else {
655
- // Not overlapping, add a space if we're in the middle of text
656
- // and not already at the beginning of text or after a space
657
- if (textUpToCursor.length > 0 &&
658
- !textUpToCursor.endsWith(" ") &&
659
- // Don't add space at the start of a line or after punctuation that shouldn't have a space
660
- !textUpToCursor.endsWith("\n") &&
661
- !/[.!?\-—:;({[\s]$/.test(textUpToCursor)) {
662
- const spaceNode = document.createTextNode(" ");
663
- range.insertNode(spaceNode);
664
- range.setStartAfter(spaceNode);
665
- range.setEndAfter(spaceNode);
666
- }
667
- }
668
- // Now insert the completion text
669
- const textNode = document.createTextNode(isOverlapping
670
- ? completionToApply // If overlapping, use the full completion
671
- : completionToApply);
672
- range.insertNode(textNode);
673
- // Move the cursor after the inserted text
674
- range.setStartAfter(textNode);
675
- range.setEndAfter(textNode);
676
- selection.removeAllRanges();
677
- selection.addRange(range);
678
- setCurrentCompletion(null);
679
- clearCursorSpan();
680
- // Explicitly save the field value since the MutationObserver may miss this change
681
- // when isUpdatingRef is true during cursor positioning
682
- setTimeout(() => {
683
- // Get the final value from the element (excluding cursor span content)
684
- let valueToSave;
685
- if (isRichText) {
686
- const clone = element.cloneNode(true);
687
- const cursorElem = clone.querySelector(`#${cursorSpanId}`);
688
- if (cursorElem)
689
- cursorElem.parentNode?.removeChild(cursorElem);
690
- // Clean up zero-width spaces
691
- const ownerDoc = clone.ownerDocument || document;
692
- const walker = ownerDoc.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
693
- const toClean = [];
694
- while (walker.nextNode()) {
695
- const tn = walker.currentNode;
696
- if (tn.nodeValue && tn.nodeValue.includes("\u200B"))
697
- toClean.push(tn);
698
- }
699
- toClean.forEach((tn) => (tn.nodeValue = tn.nodeValue?.replaceAll("\u200B", "") || ""));
700
- valueToSave = clone.innerHTML;
701
- }
702
- else {
703
- valueToSave = (element.innerText || "").replaceAll("\u200B", "");
704
- }
705
- // Call editField to save the value
706
- editContext?.operations.editField({
707
- field: {
708
- fieldId,
709
- fieldName: fieldName ?? undefined,
710
- item: {
711
- id: itemId,
712
- language,
713
- version,
714
- },
715
- },
716
- refresh: "none",
717
- value: valueToSave,
718
- });
719
- }, 0);
720
- };
721
706
  // Exposed manual trigger (if needed)
722
707
  return useMemo(() => () => {
723
708
  setCurrentCompletion(null);
724
709
  clearCursorSpan();
725
710
  getCompletionManual();
726
- }, [getCompletionManual]);
711
+ }, [clearCursorSpan, getCompletionManual]);
727
712
  }
728
713
  //# sourceMappingURL=useInlineAICompletion.js.map