@ptengine/inline-editor 0.0.0-stage → 0.1.1

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 (169) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +286 -2
  3. package/dist/InlineEditor.d.ts +349 -0
  4. package/dist/InlineEditor.js +886 -0
  5. package/dist/PagePreview.d.ts +83 -0
  6. package/dist/PagePreview.js +111 -0
  7. package/dist/assets/AssetsPanel.d.ts +19 -0
  8. package/dist/assets/AssetsPanel.js +23 -0
  9. package/dist/assets/componentLibrary.d.ts +23 -0
  10. package/dist/assets/componentLibrary.js +12 -0
  11. package/dist/canvas-ui/index.d.ts +17 -0
  12. package/dist/canvas-ui/index.js +7 -0
  13. package/dist/canvas-ui/scope.d.ts +13 -0
  14. package/dist/canvas-ui/scope.js +1 -0
  15. package/dist/canvas-ui/selection/classNames.d.ts +30 -0
  16. package/dist/canvas-ui/selection/classNames.js +15 -0
  17. package/dist/canvas-ui/selection/tokens.css +204 -0
  18. package/dist/canvas-ui/tree/TreePanel.d.ts +47 -0
  19. package/dist/canvas-ui/tree/TreePanel.js +132 -0
  20. package/dist/canvas-ui/tree/classNames.d.ts +21 -0
  21. package/dist/canvas-ui/tree/classNames.js +8 -0
  22. package/dist/canvas-ui/tree/order.d.ts +38 -0
  23. package/dist/canvas-ui/tree/order.js +38 -0
  24. package/dist/canvas-ui/tree/tree.css +131 -0
  25. package/dist/chrome/EditorChrome.d.ts +83 -0
  26. package/dist/chrome/EditorChrome.js +10 -0
  27. package/dist/chrome/VersionMenu.d.ts +26 -0
  28. package/dist/chrome/VersionMenu.js +86 -0
  29. package/dist/chrome/topbarPrimitives.d.ts +69 -0
  30. package/dist/chrome/topbarPrimitives.js +39 -0
  31. package/dist/chrome/useDismissOnOutside.d.ts +23 -0
  32. package/dist/chrome/useDismissOnOutside.js +27 -0
  33. package/dist/dialogs/ClickActionDialog.d.ts +24 -0
  34. package/dist/dialogs/ClickActionDialog.js +35 -0
  35. package/dist/dialogs/HtmlEditDialog.d.ts +23 -0
  36. package/dist/dialogs/HtmlEditDialog.js +23 -0
  37. package/dist/dialogs/ImageEditDialog.d.ts +30 -0
  38. package/dist/dialogs/ImageEditDialog.js +57 -0
  39. package/dist/dialogs/ImageSettingDialog.d.ts +16 -0
  40. package/dist/dialogs/ImageSettingDialog.js +17 -0
  41. package/dist/dialogs/InsertElementDialog.d.ts +23 -0
  42. package/dist/dialogs/InsertElementDialog.js +28 -0
  43. package/dist/dialogs/ReplaceVideoDialog.d.ts +18 -0
  44. package/dist/dialogs/ReplaceVideoDialog.js +16 -0
  45. package/dist/dialogs/actionTypes.d.ts +53 -0
  46. package/dist/dialogs/actionTypes.js +74 -0
  47. package/dist/dialogs/htmlValidation.d.ts +24 -0
  48. package/dist/dialogs/htmlValidation.js +37 -0
  49. package/dist/dialogs/imageEdit.d.ts +49 -0
  50. package/dist/dialogs/imageEdit.js +30 -0
  51. package/dist/index.d.ts +101 -0
  52. package/dist/index.js +53 -0
  53. package/dist/kernel/CanvasContextBar.d.ts +23 -0
  54. package/dist/kernel/CanvasContextBar.js +12 -0
  55. package/dist/kernel/CanvasFrame.d.ts +56 -0
  56. package/dist/kernel/CanvasFrame.js +53 -0
  57. package/dist/kernel/ai/aiInlineApply.d.ts +330 -0
  58. package/dist/kernel/ai/aiInlineApply.js +477 -0
  59. package/dist/kernel/ai/imageSource.d.ts +24 -0
  60. package/dist/kernel/ai/imageSource.js +19 -0
  61. package/dist/kernel/ai/insertHtmlGate.d.ts +71 -0
  62. package/dist/kernel/ai/insertHtmlGate.js +123 -0
  63. package/dist/kernel/ai/postApplyCheck.d.ts +56 -0
  64. package/dist/kernel/ai/postApplyCheck.js +74 -0
  65. package/dist/kernel/ai/replaceHtmlGate.d.ts +71 -0
  66. package/dist/kernel/ai/replaceHtmlGate.js +147 -0
  67. package/dist/kernel/blocks/BlockOverlay.d.ts +16 -0
  68. package/dist/kernel/blocks/BlockOverlay.js +66 -0
  69. package/dist/kernel/blocks/annotateAndTile.d.ts +106 -0
  70. package/dist/kernel/blocks/annotateAndTile.js +122 -0
  71. package/dist/kernel/blocks/resolveBlocks.d.ts +153 -0
  72. package/dist/kernel/blocks/resolveBlocks.js +233 -0
  73. package/dist/kernel/canvasCapabilities.d.ts +31 -0
  74. package/dist/kernel/canvasCapabilities.js +39 -0
  75. package/dist/kernel/canvasCommands.d.ts +126 -0
  76. package/dist/kernel/canvasCommands.js +146 -0
  77. package/dist/kernel/canvasErrors.d.ts +1 -0
  78. package/dist/kernel/canvasErrors.js +14 -0
  79. package/dist/kernel/captureGeometry.d.ts +35 -0
  80. package/dist/kernel/captureGeometry.js +25 -0
  81. package/dist/kernel/captureOrchestration.d.ts +48 -0
  82. package/dist/kernel/captureOrchestration.js +43 -0
  83. package/dist/kernel/captureSlices.d.ts +36 -0
  84. package/dist/kernel/captureSlices.js +20 -0
  85. package/dist/kernel/changelist/ChangeListPanel.d.ts +13 -0
  86. package/dist/kernel/changelist/ChangeListPanel.js +47 -0
  87. package/dist/kernel/changelist/ChangeListPopover.d.ts +20 -0
  88. package/dist/kernel/changelist/ChangeListPopover.js +10 -0
  89. package/dist/kernel/changelist/builders.d.ts +89 -0
  90. package/dist/kernel/changelist/builders.js +81 -0
  91. package/dist/kernel/changelist/changeSummary.d.ts +19 -0
  92. package/dist/kernel/changelist/changeSummary.js +85 -0
  93. package/dist/kernel/changelist/clipboard.d.ts +16 -0
  94. package/dist/kernel/changelist/clipboard.js +4 -0
  95. package/dist/kernel/changelist/exportForSave.d.ts +30 -0
  96. package/dist/kernel/changelist/exportForSave.js +11 -0
  97. package/dist/kernel/changelist/mergeForSave.d.ts +79 -0
  98. package/dist/kernel/changelist/mergeForSave.js +40 -0
  99. package/dist/kernel/changelist/moveObserverSafety.d.ts +70 -0
  100. package/dist/kernel/changelist/moveObserverSafety.js +39 -0
  101. package/dist/kernel/changelist/overrides.d.ts +22 -0
  102. package/dist/kernel/changelist/overrides.js +63 -0
  103. package/dist/kernel/changelist/useChangeList.d.ts +200 -0
  104. package/dist/kernel/changelist/useChangeList.js +282 -0
  105. package/dist/kernel/config.d.ts +29 -0
  106. package/dist/kernel/config.js +10 -0
  107. package/dist/kernel/dataUrlToBlob.d.ts +8 -0
  108. package/dist/kernel/dataUrlToBlob.js +17 -0
  109. package/dist/kernel/linkHref.d.ts +25 -0
  110. package/dist/kernel/linkHref.js +16 -0
  111. package/dist/kernel/mobileFit.d.ts +46 -0
  112. package/dist/kernel/mobileFit.js +44 -0
  113. package/dist/kernel/moveState.d.ts +35 -0
  114. package/dist/kernel/moveState.js +14 -0
  115. package/dist/kernel/selection/SelectionOverlay.d.ts +10 -0
  116. package/dist/kernel/selection/SelectionOverlay.js +25 -0
  117. package/dist/kernel/selection/SelectionToolbar.d.ts +30 -0
  118. package/dist/kernel/selection/SelectionToolbar.js +82 -0
  119. package/dist/kernel/selection/SelectorPathBar.d.ts +27 -0
  120. package/dist/kernel/selection/SelectorPathBar.js +55 -0
  121. package/dist/kernel/selection/buildDescendantShelf.d.ts +40 -0
  122. package/dist/kernel/selection/buildDescendantShelf.js +50 -0
  123. package/dist/kernel/selection/buildRootShelf.d.ts +26 -0
  124. package/dist/kernel/selection/buildRootShelf.js +149 -0
  125. package/dist/kernel/selection/elementAiContext.d.ts +324 -0
  126. package/dist/kernel/selection/elementAiContext.js +122 -0
  127. package/dist/kernel/selection/elementGeometryForAi.d.ts +9 -0
  128. package/dist/kernel/selection/elementGeometryForAi.js +15 -0
  129. package/dist/kernel/selection/geometry.d.ts +55 -0
  130. package/dist/kernel/selection/geometry.js +18 -0
  131. package/dist/kernel/selection/resolveAnchors.d.ts +116 -0
  132. package/dist/kernel/selection/resolveAnchors.js +173 -0
  133. package/dist/kernel/selection/shelfRow.d.ts +33 -0
  134. package/dist/kernel/selection/shelfRow.js +21 -0
  135. package/dist/kernel/selection/tagLabels.d.ts +9 -0
  136. package/dist/kernel/selection/tagLabels.js +46 -0
  137. package/dist/kernel/selection/toolbarButtons.d.ts +60 -0
  138. package/dist/kernel/selection/toolbarButtons.js +105 -0
  139. package/dist/kernel/selection/useAiSelectionContext.d.ts +26 -0
  140. package/dist/kernel/selection/useAiSelectionContext.js +26 -0
  141. package/dist/kernel/selection/useCanvasGeometry.d.ts +32 -0
  142. package/dist/kernel/selection/useCanvasGeometry.js +47 -0
  143. package/dist/kernel/selection/useSelectionRestore.d.ts +18 -0
  144. package/dist/kernel/selection/useSelectionRestore.js +25 -0
  145. package/dist/kernel/shrinkImageForUpload.d.ts +45 -0
  146. package/dist/kernel/shrinkImageForUpload.js +61 -0
  147. package/dist/kernel/useCanvasBridge.d.ts +36 -0
  148. package/dist/kernel/useCanvasBridge.js +41 -0
  149. package/dist/kernel/useCanvasGate.d.ts +22 -0
  150. package/dist/kernel/useCanvasGate.js +35 -0
  151. package/dist/kernel/useCanvasSession.d.ts +44 -0
  152. package/dist/kernel/useCanvasSession.js +182 -0
  153. package/dist/kernel/useCanvasStream.d.ts +39 -0
  154. package/dist/kernel/useCanvasStream.js +45 -0
  155. package/dist/kernel/useMobileFit.d.ts +40 -0
  156. package/dist/kernel/useMobileFit.js +139 -0
  157. package/dist/locales.d.ts +1094 -0
  158. package/dist/locales.js +545 -0
  159. package/dist/panels/StylePanel.d.ts +24 -0
  160. package/dist/panels/StylePanel.js +40 -0
  161. package/dist/panels/styleFields.d.ts +39 -0
  162. package/dist/panels/styleFields.js +91 -0
  163. package/dist/tree/LayersPanel.d.ts +30 -0
  164. package/dist/tree/LayersPanel.js +15 -0
  165. package/dist/tree/domTreeAdapter.d.ts +63 -0
  166. package/dist/tree/domTreeAdapter.js +90 -0
  167. package/dist/useInlineEditor.d.ts +161 -0
  168. package/dist/useInlineEditor.js +346 -0
  169. package/package.json +58 -3
@@ -0,0 +1,886 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { CANVAS_UI_SCOPE } from './canvas-ui/index.js';
5
+ import { EditorChrome } from './chrome/EditorChrome.js';
6
+ import { CanvasFrame, TARGET_VIEWPORT_WIDTH, computeZoom } from './kernel/CanvasFrame.js';
7
+ import { CanvasContextBar } from './kernel/CanvasContextBar.js';
8
+ import { SelectionOverlay } from './kernel/selection/SelectionOverlay.js';
9
+ import { BlockOverlay } from './kernel/blocks/BlockOverlay.js';
10
+ import { dataUrlToBlob } from './kernel/dataUrlToBlob.js';
11
+ import { SelectionToolbar } from './kernel/selection/SelectionToolbar.js';
12
+ import { ChangeListPanel } from './kernel/changelist/ChangeListPanel.js';
13
+ import { ChangeListPopover } from './kernel/changelist/ChangeListPopover.js';
14
+ import { frameRectToProductRect, measureFrameContext } from './kernel/selection/geometry.js';
15
+ import { buildElementAiContext, extractPageLinks, linksFragment, ROOT_SHELF_LIMIT, SHELF_TREE_MAX_DEPTH, SHELF_TREE_MAX_NODES } from './kernel/selection/elementAiContext.js';
16
+ import { getAncestorChain, getDomTree, getElementCapabilities, getElementImages, getOuterHtml, resolveCanvasSelector } from './kernel/canvasCommands.js';
17
+ import { useAiSelectionContext } from './kernel/selection/useAiSelectionContext.js';
18
+ import { buildDescendantShelf } from './kernel/selection/buildDescendantShelf.js';
19
+ import { buildRootShelf } from './kernel/selection/buildRootShelf.js';
20
+ import { backgroundImageAncestorFor, blockContainerFor, resolveInlinePlanAnchors, withImageRows } from './kernel/selection/resolveAnchors.js';
21
+ import { elementGeometryForAi } from './kernel/selection/elementGeometryForAi.js';
22
+ import { applyAiChanges } from './kernel/ai/aiInlineApply.js';
23
+ import { APPLY_CHECK_SETTLE_MS } from './kernel/ai/postApplyCheck.js';
24
+ import { deleteElement, editHtml, editStyle, insertElement, replaceImage, replaceVideo, setClickAction, setImageAlt } from './kernel/changelist/useChangeList.js';
25
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@ptengine/design-components';
26
+ import { pasteModes } from './kernel/selection/toolbarButtons.js';
27
+ import { ClickActionDialog } from './dialogs/ClickActionDialog.js';
28
+ import { ImageEditDialog } from './dialogs/ImageEditDialog.js';
29
+ import { resolveImageBranch } from './dialogs/imageEdit.js';
30
+ import { ImageSettingDialog } from './dialogs/ImageSettingDialog.js';
31
+ import { InsertElementDialog } from './dialogs/InsertElementDialog.js';
32
+ import { ReplaceVideoDialog } from './dialogs/ReplaceVideoDialog.js';
33
+ import { StylePanel } from './panels/StylePanel.js';
34
+ import { HtmlEditDialog } from './dialogs/HtmlEditDialog.js';
35
+ import { setCode } from './kernel/canvasCommands.js';
36
+ import { describeCanvasError } from './kernel/canvasErrors.js';
37
+ import { LayersPanel } from './tree/LayersPanel.js';
38
+ import { AssetsPanel } from './assets/AssetsPanel.js';
39
+ import { componentInsertPayload } from './assets/componentLibrary.js';
40
+ import { moveModeFromDropPos } from './tree/domTreeAdapter.js';
41
+ import { useInlineEditor } from './useInlineEditor.js';
42
+ import { shrinkImageForUpload } from './kernel/shrinkImageForUpload.js';
43
+ const nodeSelectorOf = (n) => n.selector;
44
+ function imageTargetOf(caps) {
45
+ if (!caps)
46
+ return undefined;
47
+ const branch = resolveImageBranch(caps);
48
+ if (branch === 'img-src')
49
+ return 'img';
50
+ return branch === 'background' ? 'background' : undefined;
51
+ }
52
+ const AI_ELEMENT_IMAGE_LIMIT = 4;
53
+ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedImageOrigins, allowedImageUrls, allowedLinkUrls, viewportWidthOverride, blockOverlay }) {
54
+ const { t } = useTranslation();
55
+ const canvasAreaRef = useRef(null);
56
+ const canvasIframeRef = useRef(null);
57
+ const insertedRemovedRef = useRef(null);
58
+ const insertedRestoredRef = useRef(null);
59
+ const editor = useInlineEditor({
60
+ onInsertedRemoved: selectors => insertedRemovedRef.current?.(selectors),
61
+ onInsertedRestored: selectors => insertedRestoredRef.current?.(selectors),
62
+ url,
63
+ device,
64
+ iframeRef: canvasIframeRef,
65
+ renderNodeIcon
66
+ });
67
+ const { selection, tree, clipboard, move, capturing, textEditing, geometry, changeList, commands, frameProps, domReady } = editor;
68
+ const selectAndReveal = useCallback(async (selector) => {
69
+ await commands.select(selector);
70
+ await commands.reveal(selector);
71
+ }, [commands]);
72
+ const [commandError, setCommandError] = useState(null);
73
+ const [aiApplyNote, setAiApplyNote] = useState(null);
74
+ const [shot, setShot] = useState(null);
75
+ const [shotUrl, setShotUrl] = useState(null);
76
+ const [panelWidth, setPanelWidth] = useState(0);
77
+ const [showChangeList, setShowChangeList] = useState(false);
78
+ const [stylePanelOpen, setStylePanelOpen] = useState(false);
79
+ const [htmlDialog, setHtmlDialog] = useState({ open: false, seed: '' });
80
+ const [imageDialog, setImageDialog] = useState({ open: false, seed: '' });
81
+ const [imageSettingDialog, setImageSettingDialog] = useState({ open: false, seed: '' });
82
+ const [videoDialog, setVideoDialog] = useState({ open: false, seed: '' });
83
+ const [insertDialog, setInsertDialog] = useState(false);
84
+ const [pasteDialog, setPasteDialog] = useState(false);
85
+ const [showLeftPanel, setShowLeftPanel] = useState(true);
86
+ const [leftTab, setLeftTab] = useState('layers');
87
+ const [actionDialog, setActionDialog] = useState({ open: false, seed: null });
88
+ const pageOrigin = useMemo(() => {
89
+ try {
90
+ return new URL(url).origin;
91
+ }
92
+ catch {
93
+ return '';
94
+ }
95
+ }, [url]);
96
+ const imageOrigins = useMemo(() => Array.from(new Set([pageOrigin, ...(allowedImageOrigins ?? [])].filter(Boolean))), [pageOrigin, allowedImageOrigins]);
97
+ const changeListRef = useRef(changeList);
98
+ changeListRef.current = changeList;
99
+ const versionChangesRef = useRef(versionChanges);
100
+ versionChangesRef.current = versionChanges;
101
+ const versionCallbacksRef = useRef({ onVersionChange, onVersionLeave, onVersionEnter });
102
+ versionCallbacksRef.current = { onVersionChange, onVersionLeave, onVersionEnter };
103
+ const resolveChangeIdRef = useRef(resolveChangeId);
104
+ resolveChangeIdRef.current = resolveChangeId;
105
+ const hasVersions = !!versions?.length;
106
+ const hasVersionsRef = useRef(hasVersions);
107
+ hasVersionsRef.current = hasVersions;
108
+ const loadedVersionRef = useRef(activeVersionId);
109
+ const [loadedVersion, setLoadedVersion] = useState(activeVersionId);
110
+ const leftVersionIdsRef = useRef(new Map());
111
+ const [switching, setSwitching] = useState(false);
112
+ const switchingRef = useRef(false);
113
+ const queueRef = useRef(Promise.resolve());
114
+ const enqueue = useCallback((task) => {
115
+ const run = queueRef.current.then(task, task);
116
+ queueRef.current = run.then(() => undefined, () => undefined);
117
+ return run;
118
+ }, []);
119
+ const prevIdsFor = useCallback((versionId, next) => {
120
+ const remembered = versionId ? leftVersionIdsRef.current.get(versionId) : undefined;
121
+ if (remembered?.length === next.length)
122
+ return remembered;
123
+ const live = changeListRef.current.peekList().map(r => r.changeId);
124
+ if (live.length === next.length && live.length > 0)
125
+ return live;
126
+ const fromChanges = next.map(c => c.id ?? '');
127
+ return fromChanges.length > 0 && fromChanges.every(Boolean) ? fromChanges : undefined;
128
+ }, []);
129
+ const switchToVersion = useCallback(async (nextId) => {
130
+ const prevId = loadedVersionRef.current;
131
+ if (!hasVersionsRef.current || nextId === prevId)
132
+ return;
133
+ setSwitching(true);
134
+ switchingRef.current = true;
135
+ try {
136
+ const live = changeListRef.current.peekList();
137
+ if (prevId) {
138
+ versionCallbacksRef.current.onVersionLeave?.(prevId, live);
139
+ leftVersionIdsRef.current.set(prevId, live.map(r => r.changeId));
140
+ }
141
+ const next = versionChangesRef.current?.[nextId] ?? [];
142
+ const { idMap } = await changeListRef.current.replaceAll(next, prevIdsFor(nextId, next));
143
+ loadedVersionRef.current = nextId;
144
+ setLoadedVersion(nextId);
145
+ versionCallbacksRef.current.onVersionEnter?.(nextId, idMap);
146
+ }
147
+ finally {
148
+ setSwitching(false);
149
+ switchingRef.current = false;
150
+ }
151
+ }, [prevIdsFor]);
152
+ const rejectWhileSwitching = useCallback((run) => {
153
+ if (!switchingRef.current)
154
+ return run();
155
+ return Promise.reject(new Error('VERSION_SWITCHING'));
156
+ }, []);
157
+ const writes = useMemo(() => ({
158
+ addStyle: input => rejectWhileSwitching(() => changeListRef.current.addStyle(input)),
159
+ addContent: input => rejectWhileSwitching(() => changeListRef.current.addContent(input)),
160
+ addAttribute: input => rejectWhileSwitching(() => changeListRef.current.addAttribute(input)),
161
+ addAction: input => rejectWhileSwitching(() => changeListRef.current.addAction(input)),
162
+ addMove: input => rejectWhileSwitching(() => changeListRef.current.addMove(input)),
163
+ revert: id => rejectWhileSwitching(() => changeListRef.current.revert(id)),
164
+ revertAll: () => rejectWhileSwitching(() => changeListRef.current.revertAll())
165
+ }), [rejectWhileSwitching]);
166
+ const uiWrite = useCallback((run) => {
167
+ if (switchingRef.current) {
168
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
169
+ return;
170
+ }
171
+ void run().catch(e => setCommandError(describeCanvasError(e)));
172
+ }, [t]);
173
+ useEffect(() => {
174
+ if (!hasVersions || !activeVersionId)
175
+ return;
176
+ if (activeVersionId === loadedVersionRef.current)
177
+ return;
178
+ void enqueue(() => switchToVersion(activeVersionId)).catch(e => {
179
+ setCommandError(describeCanvasError(e));
180
+ const back = loadedVersionRef.current;
181
+ if (back)
182
+ versionCallbacksRef.current.onVersionChange?.(back);
183
+ });
184
+ }, [activeVersionId, hasVersions, enqueue, switchToVersion]);
185
+ useEffect(() => {
186
+ const el = canvasAreaRef.current;
187
+ if (!el)
188
+ return;
189
+ const ro = new ResizeObserver(entries => {
190
+ const w = entries[0]?.contentRect.width;
191
+ if (w)
192
+ setPanelWidth(w);
193
+ });
194
+ ro.observe(el);
195
+ return () => ro.disconnect();
196
+ }, []);
197
+ const sessionIdForAssets = frameProps?.sessionId;
198
+ useEffect(() => {
199
+ injectedAssetsRef.current = [];
200
+ }, [sessionIdForAssets]);
201
+ const hydratedSessionRef = useRef(null);
202
+ const sessionIdForHydrate = frameProps?.sessionId;
203
+ const bootChanges = hasVersions && activeVersionId && versionChanges?.[activeVersionId]
204
+ ? versionChanges[activeVersionId]
205
+ : initialChanges;
206
+ useEffect(() => {
207
+ if (!domReady || !sessionIdForHydrate)
208
+ return;
209
+ if (hydratedSessionRef.current === sessionIdForHydrate)
210
+ return;
211
+ hydratedSessionRef.current = sessionIdForHydrate;
212
+ if (!bootChanges?.length)
213
+ return;
214
+ const versionId = hasVersions ? activeVersionId : undefined;
215
+ void enqueue(async () => {
216
+ const { outcome, idMap } = await changeListRef.current.replaceAll(bootChanges, prevIdsFor(versionId, bootChanges));
217
+ if (versionId) {
218
+ loadedVersionRef.current = versionId;
219
+ setLoadedVersion(versionId);
220
+ versionCallbacksRef.current.onVersionEnter?.(versionId, idMap);
221
+ }
222
+ onHydrated?.(outcome);
223
+ }).catch(e => setCommandError(describeCanvasError(e)));
224
+ }, [domReady, sessionIdForHydrate, bootChanges]);
225
+ const commandsRef = useRef(commands);
226
+ commandsRef.current = commands;
227
+ const sessionReady = domReady && !editor.mobileFit.pending;
228
+ const readySessionRef = useRef(null);
229
+ readySessionRef.current = sessionReady && sessionIdForHydrate ? sessionIdForHydrate : null;
230
+ const fitScaleRef = useRef(1);
231
+ fitScaleRef.current = editor.mobileFit.scale;
232
+ const sessionReadyRef = useRef(null);
233
+ useEffect(() => {
234
+ if (!onSessionReady || !sessionReady || !sessionIdForHydrate)
235
+ return;
236
+ if (sessionReadyRef.current === sessionIdForHydrate)
237
+ return;
238
+ sessionReadyRef.current = sessionIdForHydrate;
239
+ onSessionReady(sessionIdForHydrate);
240
+ }, [onSessionReady, sessionReady, sessionIdForHydrate]);
241
+ const editorHandleSessionRef = useRef(null);
242
+ useEffect(() => {
243
+ if (!onEditorReady || !sessionReady || !sessionIdForHydrate)
244
+ return;
245
+ if (editorHandleSessionRef.current === sessionIdForHydrate)
246
+ return;
247
+ editorHandleSessionRef.current = sessionIdForHydrate;
248
+ onEditorReady({
249
+ clearSelection: () => commandsRef.current.select(''),
250
+ peekList: () => changeListRef.current.peekList(),
251
+ hasUnsavedChanges: () => changeListRef.current.peekList().length > 0,
252
+ capturePage: async () => {
253
+ const outcome = await commandsRef.current.captureSelectedElement('body');
254
+ return dataUrlToBlob(outcome.dataUrl);
255
+ },
256
+ scrollToSelector: selector => commandsRef.current.reveal(selector),
257
+ getSessionId: () => readySessionRef.current,
258
+ getMobileFitScale: () => fitScaleRef.current
259
+ });
260
+ }, [onEditorReady, sessionReady, sessionIdForHydrate]);
261
+ const selectedSelector = selection?.selector;
262
+ useEffect(() => {
263
+ setStylePanelOpen(false);
264
+ }, [selectedSelector]);
265
+ const overridePc = viewportWidthOverride?.pc;
266
+ const overrideMobile = viewportWidthOverride?.mobile;
267
+ const targetViewportWidth = useMemo(() => ({
268
+ pc: overridePc ?? TARGET_VIEWPORT_WIDTH.pc,
269
+ mobile: overrideMobile ?? TARGET_VIEWPORT_WIDTH.mobile
270
+ }), [overridePc, overrideMobile]);
271
+ const zoom = computeZoom(panelWidth, targetViewportWidth[device]);
272
+ const iframeRect = canvasIframeRef.current?.getBoundingClientRect();
273
+ const overlayContext = useMemo(() => measureFrameContext(canvasIframeRef.current, zoom), [iframeRect?.left, iframeRect?.top, zoom]);
274
+ const canvasAreaRect = canvasAreaRef.current?.getBoundingClientRect();
275
+ const area = {
276
+ x: canvasAreaRect?.left ?? 0,
277
+ y: canvasAreaRect?.top ?? 0,
278
+ width: canvasAreaRect?.width ?? 0,
279
+ height: canvasAreaRect?.height ?? 0
280
+ };
281
+ const selectedRect = geometry.latest?.selectedViewportRect
282
+ ? frameRectToProductRect(geometry.latest.selectedViewportRect, overlayContext)
283
+ : null;
284
+ const depsOf = useCallback((self) => ({
285
+ identifier: self.selector,
286
+ selector: self.selector,
287
+ ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector),
288
+ addStyle: writes.addStyle,
289
+ addContent: writes.addContent,
290
+ addAttribute: writes.addAttribute,
291
+ addAction: writes.addAction
292
+ }), [writes, selection]);
293
+ const injectedAssetsRef = useRef([]);
294
+ const insertComponent = useCallback(async (component) => {
295
+ const target = selection?.chain[0];
296
+ const sessionId = frameProps?.sessionId;
297
+ if (!target || !sessionId)
298
+ return;
299
+ if (switchingRef.current) {
300
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
301
+ return;
302
+ }
303
+ setCommandError(null);
304
+ try {
305
+ const { html, codeList } = componentInsertPayload(component);
306
+ if (codeList.length > 0) {
307
+ injectedAssetsRef.current = [...injectedAssetsRef.current, ...codeList];
308
+ await setCode(sessionId, injectedAssetsRef.current);
309
+ }
310
+ await writes.addContent({
311
+ identifier: target.selector,
312
+ selector: target.selector,
313
+ html,
314
+ mode: 'append'
315
+ });
316
+ if (component.js) {
317
+ setCommandError('组件已插入。它带有 JS,画布里不会执行(客户页没有 SDK 运行时),发布后才生效');
318
+ }
319
+ }
320
+ catch (error) {
321
+ setCommandError(describeCanvasError(error));
322
+ }
323
+ }, [writes, frameProps?.sessionId, selection, t]);
324
+ const captureAiShot = useCallback(async (selector) => {
325
+ if (!uploadImage)
326
+ return { error: 'host did not provide uploadImage' };
327
+ try {
328
+ const outcome = await commands.captureSelectedElement(selector);
329
+ const raw = await (await fetch(outcome.dataUrl)).blob();
330
+ const blob = await shrinkImageForUpload(raw);
331
+ const url = await uploadImage(blob);
332
+ return url ? { url } : { error: 'upload returned no url' };
333
+ }
334
+ catch (error) {
335
+ setCommandError(describeCanvasError(error));
336
+ return { error: error instanceof Error ? error.message : String(error) };
337
+ }
338
+ }, [uploadImage, commands]);
339
+ const captureAiScreenshot = useCallback(async (selector) => {
340
+ const shot = await captureAiShot(selector);
341
+ return 'url' in shot ? shot.url : null;
342
+ }, [captureAiShot]);
343
+ const descendantShelfRef = useRef([]);
344
+ const geometryRef = useRef(null);
345
+ geometryRef.current = geometry.latest;
346
+ const [elementImages, setElementImages] = useState(null);
347
+ const buildAiContext = useCallback((self, outerHtml) => buildElementAiContext({
348
+ selector: self.selector,
349
+ tag: self.tag,
350
+ imageTarget: imageTargetOf(selection?.capabilities),
351
+ images: elementImages?.selector === self.selector ? elementImages.images : undefined,
352
+ ancestors: (selection?.chain ?? []).map(n => n.selector).slice().reverse(),
353
+ outerHtml,
354
+ descendants: descendantShelfRef.current,
355
+ hasChildren: (selection?.capabilities?.childElementCount ?? 0) > 0,
356
+ geometry: elementGeometryForAi(geometryRef.current, device)
357
+ }), [selection, elementImages]);
358
+ const sessionIdForAiContext = frameProps?.sessionId;
359
+ const readOuterHtmlForAi = useCallback((selector) => getOuterHtml(sessionIdForAiContext, selector), [sessionIdForAiContext]);
360
+ const captureForAi = useCallback((selector) => () => captureAiScreenshot(selector), [captureAiScreenshot]);
361
+ useEffect(() => {
362
+ const picked = selection?.chain[0];
363
+ if (!trackAiSelection || !picked || !sessionIdForAiContext) {
364
+ descendantShelfRef.current = [];
365
+ return;
366
+ }
367
+ let stale = false;
368
+ void getDomTree(sessionIdForAiContext, {
369
+ rootSelector: picked.selector,
370
+ maxDepth: SHELF_TREE_MAX_DEPTH,
371
+ maxChildren: 20,
372
+ maxNodes: SHELF_TREE_MAX_NODES,
373
+ visibleOnly: true,
374
+ includeRect: true
375
+ })
376
+ .then(root => {
377
+ if (!stale)
378
+ descendantShelfRef.current = buildDescendantShelf(root);
379
+ })
380
+ .catch(() => {
381
+ descendantShelfRef.current = [];
382
+ });
383
+ return () => {
384
+ stale = true;
385
+ };
386
+ }, [trackAiSelection, selection, sessionIdForAiContext]);
387
+ useEffect(() => {
388
+ const picked = selection?.chain[0];
389
+ if (!trackAiSelection || !picked || !sessionIdForAiContext)
390
+ return;
391
+ let stale = false;
392
+ void getElementImages(sessionIdForAiContext, picked.selector, AI_ELEMENT_IMAGE_LIMIT)
393
+ .then(({ images }) => {
394
+ if (stale || !images.length)
395
+ return;
396
+ setElementImages({ selector: picked.selector, images });
397
+ })
398
+ .catch(() => {
399
+ });
400
+ return () => {
401
+ stale = true;
402
+ };
403
+ }, [trackAiSelection, selection, sessionIdForAiContext]);
404
+ const pageContextSessionRef = useRef(null);
405
+ const rootShelfRef = useRef([]);
406
+ const aiInsertedRef = useRef([]);
407
+ const onAiInsertedRemovedRef = useRef(onAiInsertedRemoved);
408
+ onAiInsertedRemovedRef.current = onAiInsertedRemoved;
409
+ insertedRemovedRef.current = selectors => {
410
+ const gone = new Set(selectors);
411
+ aiInsertedRef.current = aiInsertedRef.current.filter(s => !gone.has(s));
412
+ onAiInsertedRemovedRef.current?.(selectors);
413
+ };
414
+ const onAiInsertedRestoredRef = useRef(onAiInsertedRestored);
415
+ onAiInsertedRestoredRef.current = onAiInsertedRestored;
416
+ insertedRestoredRef.current = selectors => {
417
+ for (const sel of selectors)
418
+ if (!aiInsertedRef.current.includes(sel))
419
+ aiInsertedRef.current.push(sel);
420
+ onAiInsertedRestoredRef.current?.(selectors);
421
+ };
422
+ const rootShelfAncestorsRef = useRef(new Map());
423
+ const anchorsResultRef = useRef(null);
424
+ const anchorsRef = useRef(anchors);
425
+ useEffect(() => {
426
+ anchorsRef.current = anchors;
427
+ }, [anchors]);
428
+ useEffect(() => {
429
+ rootShelfRef.current = [];
430
+ rootShelfAncestorsRef.current = new Map();
431
+ aiInsertedRef.current = [];
432
+ anchorsResultRef.current = null;
433
+ pageContextSessionRef.current = null;
434
+ leftVersionIdsRef.current = new Map();
435
+ }, [sessionIdForAiContext]);
436
+ useEffect(() => {
437
+ if (!onPageContextReady || !domReady || !sessionIdForAiContext)
438
+ return;
439
+ if (pageContextSessionRef.current === sessionIdForAiContext)
440
+ return;
441
+ pageContextSessionRef.current = sessionIdForAiContext;
442
+ let stale = false;
443
+ void (async () => {
444
+ const root = await getDomTree(sessionIdForAiContext, {
445
+ rootSelector: 'body',
446
+ maxDepth: 16,
447
+ maxChildren: 40,
448
+ maxNodes: 1500,
449
+ visibleOnly: true,
450
+ includeRect: true
451
+ });
452
+ const { html } = await getOuterHtml(sessionIdForAiContext, 'body');
453
+ if (stale)
454
+ return;
455
+ const { shelf, ancestorsOf } = buildRootShelf(root, {
456
+ limit: ROOT_SHELF_LIMIT,
457
+ viewportWidth: geometryRef.current?.viewportWidth
458
+ });
459
+ const planAnchors = anchorsRef.current ?? [];
460
+ const anchorOut = planAnchors.length
461
+ ? await resolveInlinePlanAnchors({
462
+ anchors: planAnchors,
463
+ rootTree: root,
464
+ resolveSelector: async (selector) => {
465
+ const r = await resolveCanvasSelector(sessionIdForAiContext, selector);
466
+ return { matched: r.matched, matchCount: r.matchCount };
467
+ },
468
+ getSubtree: (rootSelector) => getDomTree(sessionIdForAiContext, {
469
+ rootSelector,
470
+ maxDepth: SHELF_TREE_MAX_DEPTH,
471
+ maxChildren: 20,
472
+ maxNodes: SHELF_TREE_MAX_NODES,
473
+ visibleOnly: true,
474
+ includeRect: true
475
+ })
476
+ })
477
+ : null;
478
+ if (stale)
479
+ return;
480
+ const anchorRows = anchorOut?.shelf ?? [];
481
+ const anchorSelectors = new Set(anchorRows.map(r => r.selector));
482
+ const mergedShelf = [
483
+ ...anchorRows,
484
+ ...shelf.filter(row => !anchorSelectors.has(row.selector))
485
+ ];
486
+ const mergedAncestors = new Map(ancestorsOf);
487
+ for (const [selector, chain] of anchorOut?.ancestorsOf ?? []) {
488
+ mergedAncestors.set(selector, chain);
489
+ }
490
+ rootShelfRef.current = mergedShelf;
491
+ rootShelfAncestorsRef.current = mergedAncestors;
492
+ anchorsResultRef.current = anchorOut?.anchors ?? null;
493
+ const resolvedAnchors = (anchorOut?.anchors ?? []).filter(a => a.resolved);
494
+ const anchorBlocks = [];
495
+ for (const anchor of resolvedAnchors) {
496
+ const selector = blockContainerFor(anchor, mergedShelf, mergedAncestors);
497
+ if (!anchorBlocks.some(b => b.selector === selector))
498
+ anchorBlocks.push({ anchor, selector });
499
+ }
500
+ const fragments = [];
501
+ const images = [];
502
+ for (const { anchor, selector } of anchorBlocks) {
503
+ try {
504
+ const { html: blockHtml } = await getOuterHtml(sessionIdForAiContext, selector);
505
+ fragments.push({ label: anchor.blockId ?? anchor.label, selector, html: blockHtml });
506
+ }
507
+ catch {
508
+ }
509
+ const backgroundUp = anchor.blockSelectors?.length
510
+ ? null
511
+ : backgroundImageAncestorFor(anchor.selector, mergedShelf, mergedAncestors);
512
+ const imageRoots = Array.from(new Set([...(anchor.blockSelectors ?? []), selector, ...(backgroundUp ? [backgroundUp] : [])])).slice(0, 3);
513
+ const seenUrls = new Set(images.map(i => i.url));
514
+ for (const root of imageRoots) {
515
+ try {
516
+ const { images: found } = await getElementImages(sessionIdForAiContext, root);
517
+ for (const im of found) {
518
+ if (seenUrls.has(im.url))
519
+ continue;
520
+ seenUrls.add(im.url);
521
+ images.push({
522
+ ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
523
+ selector: im.selector,
524
+ kind: im.kind,
525
+ url: im.url
526
+ });
527
+ }
528
+ }
529
+ catch {
530
+ }
531
+ }
532
+ }
533
+ if (stale)
534
+ return;
535
+ const onShelf = new Set(mergedShelf.map(r => r.selector));
536
+ const imageAncestors = new Map();
537
+ for (const selector of new Set(images.map(im => im.selector))) {
538
+ if (onShelf.has(selector))
539
+ continue;
540
+ try {
541
+ const { chain } = await getAncestorChain(sessionIdForAiContext, selector);
542
+ if (chain.length)
543
+ imageAncestors.set(selector, chain.slice(1).map(c => c.selector));
544
+ }
545
+ catch {
546
+ }
547
+ }
548
+ if (stale)
549
+ return;
550
+ const { shelf: pageShelf, added: imageRowCount } = withImageRows(mergedShelf, anchorRows.length, images.filter(im => onShelf.has(im.selector) || imageAncestors.has(im.selector)));
551
+ for (const [selector, chain] of imageAncestors)
552
+ mergedAncestors.set(selector, chain);
553
+ rootShelfRef.current = pageShelf;
554
+ rootShelfAncestorsRef.current = mergedAncestors;
555
+ if (fragments.length) {
556
+ const links = linksFragment(extractPageLinks(html));
557
+ if (links)
558
+ fragments.push(links);
559
+ }
560
+ const captureAnchors = anchorBlocks.length
561
+ ? async () => {
562
+ const shots = [];
563
+ for (const { anchor, selector } of anchorBlocks) {
564
+ const shotSelector = anchor.blockSelectors?.[0] ?? selector;
565
+ const shot = await captureAiShot(shotSelector);
566
+ const base = {
567
+ ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
568
+ label: anchor.label,
569
+ selector: shotSelector
570
+ };
571
+ shots.push('url' in shot ? { ...base, url: shot.url } : { ...base, error: shot.error });
572
+ }
573
+ return shots;
574
+ }
575
+ : undefined;
576
+ const frame = geometryRef.current;
577
+ onPageContextReady(buildElementAiContext({
578
+ selector: 'body',
579
+ tag: 'body',
580
+ ancestors: [],
581
+ outerHtml: html,
582
+ fragments,
583
+ images,
584
+ descendants: pageShelf,
585
+ descendantLimit: ROOT_SHELF_LIMIT + anchorRows.length + imageRowCount,
586
+ ...(anchorsResultRef.current ? { anchors: anchorsResultRef.current } : {}),
587
+ hasChildren: true,
588
+ ...(frame
589
+ ? {
590
+ geometry: {
591
+ width: Math.round(frame.viewportWidth),
592
+ height: Math.round(frame.viewportHeight),
593
+ topInPage: 0,
594
+ viewportWidth: Math.round(frame.viewportWidth),
595
+ viewportHeight: Math.round(frame.viewportHeight),
596
+ device
597
+ }
598
+ }
599
+ : {})
600
+ }), () => captureAiScreenshot('body'), captureAnchors);
601
+ })().catch(error => {
602
+ if (!stale)
603
+ setCommandError(describeCanvasError(error));
604
+ });
605
+ return () => {
606
+ stale = true;
607
+ };
608
+ }, [
609
+ onPageContextReady,
610
+ domReady,
611
+ sessionIdForAiContext,
612
+ device,
613
+ captureAiScreenshot,
614
+ captureAiShot
615
+ ]);
616
+ useAiSelectionContext({
617
+ enabled: trackAiSelection,
618
+ picked: selection?.chain[0],
619
+ sessionId: sessionIdForAiContext,
620
+ readOuterHtml: readOuterHtmlForAi,
621
+ selectorOf: nodeSelectorOf,
622
+ buildContext: buildAiContext,
623
+ captureFor: captureForAi,
624
+ onChange: onAiContextChange
625
+ });
626
+ useEffect(() => {
627
+ if (!onAiApplyReady)
628
+ return;
629
+ const picked = selection?.chain[0];
630
+ const pageMode = !!onPageContextReady;
631
+ if (!sessionIdForAiContext || (!picked && !pageMode)) {
632
+ onAiApplyReady(null);
633
+ return;
634
+ }
635
+ const scope = picked
636
+ ? {
637
+ selector: picked.selector,
638
+ identifier: picked.selector,
639
+ descendants: () => [
640
+ ...descendantShelfRef.current.map(d => d.selector),
641
+ ...aiInsertedRef.current
642
+ ],
643
+ ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector)
644
+ }
645
+ : {
646
+ selector: 'body',
647
+ identifier: 'body',
648
+ descendants: () => [...rootShelfRef.current.map(d => d.selector), ...aiInsertedRef.current],
649
+ ancestors: [],
650
+ ancestorsOf: (selector) => rootShelfAncestorsRef.current.get(selector)
651
+ };
652
+ onAiApplyReady({
653
+ activeVersionId: loadedVersion,
654
+ apply: async (changes, opts) => enqueue(async () => {
655
+ const target = opts?.versionId;
656
+ const rejectAll = (detail) => ({
657
+ applied: [],
658
+ rejected: changes.map((c, index) => ({
659
+ index,
660
+ selector: typeof c?.selector === 'string' ? c.selector : '',
661
+ reason: 'VERSION_SWITCH_FAILED',
662
+ detail
663
+ })),
664
+ versionId: loadedVersionRef.current ?? ''
665
+ });
666
+ if (target && target !== loadedVersionRef.current) {
667
+ if (!hasVersionsRef.current) {
668
+ return rejectAll('host did not pass `versions`');
669
+ }
670
+ try {
671
+ await switchToVersion(target);
672
+ versionCallbacksRef.current.onVersionChange?.(target);
673
+ }
674
+ catch (e) {
675
+ return rejectAll(describeCanvasError(e));
676
+ }
677
+ }
678
+ const outcome = await applyAiChanges(changes, {
679
+ scope: opts?.grant ? { ...scope, grant: opts.grant } : scope,
680
+ resolve: async (selector) => {
681
+ const r = await resolveCanvasSelector(sessionIdForAiContext, selector);
682
+ return { matched: r.matched, matchCount: r.matchCount, viewportRect: r.viewportRect };
683
+ },
684
+ insertedSelectorsOf: (changeId) => changeListRef.current.peekList().find(r => r.changeId === changeId)
685
+ ?.insertedSelectors ?? [],
686
+ settle: () => new Promise(r => setTimeout(r, APPLY_CHECK_SETTLE_MS)),
687
+ viewportWidth: () => geometryRef.current?.viewportWidth,
688
+ getCapabilities: (selector) => getElementCapabilities(sessionIdForAiContext, selector),
689
+ addStyle: changeList.addStyle,
690
+ addContent: changeList.addContent,
691
+ addMove: changeList.addMove,
692
+ addAttribute: changeList.addAttribute,
693
+ addAction: changeList.addAction,
694
+ allowedImageOrigins: imageOrigins,
695
+ allowedImageUrls: allowedImageUrls ?? [],
696
+ allowedLinkUrls: allowedLinkUrls ?? [],
697
+ pageOrigin
698
+ });
699
+ for (const a of outcome.applied) {
700
+ for (const sel of a.insertedSelectors ?? []) {
701
+ if (!aiInsertedRef.current.includes(sel))
702
+ aiInsertedRef.current.push(sel);
703
+ }
704
+ }
705
+ const skipped = outcome.rejected
706
+ .map(r => `${r.reason}${r.detail ? `(${r.detail})` : ''}`)
707
+ .join(';');
708
+ setAiApplyNote(outcome.applied.length
709
+ ? t('inlineCanvas.ai.applyNote', 'AI 改动:{{ok}} 条已应用{{skip}}', {
710
+ ok: outcome.applied.length,
711
+ skip: skipped
712
+ ? `,${outcome.rejected.length} 条被跳过:${skipped}`
713
+ : ''
714
+ })
715
+ : t('inlineCanvas.ai.applyNoneNote', 'AI 改动全部被跳过:{{skip}}', {
716
+ skip: skipped
717
+ }));
718
+ return { ...outcome, versionId: loadedVersionRef.current ?? '' };
719
+ }),
720
+ revert: async (changeIds) => {
721
+ const mapped = changeIds.map(id => resolveChangeIdRef.current?.(id) ?? id);
722
+ const live = new Set(changeListRef.current.peekList().map(r => r.changeId));
723
+ if (mapped.some(id => !live.has(id))) {
724
+ return { ok: false, reason: 'VERSION_NOT_ACTIVE' };
725
+ }
726
+ for (const id of [...mapped].reverse()) {
727
+ await changeListRef.current.revert(id);
728
+ }
729
+ return { ok: true };
730
+ }
731
+ });
732
+ return () => onAiApplyReady(null);
733
+ }, [
734
+ onAiApplyReady,
735
+ onPageContextReady,
736
+ selection,
737
+ sessionIdForAiContext,
738
+ changeList.addStyle,
739
+ changeList.addContent,
740
+ changeList.addMove,
741
+ changeList.addAttribute,
742
+ changeList.addAction,
743
+ imageOrigins,
744
+ allowedImageUrls,
745
+ allowedLinkUrls,
746
+ pageOrigin,
747
+ enqueue,
748
+ switchToVersion,
749
+ loadedVersion,
750
+ t
751
+ ]);
752
+ const onToolbarAction = useCallback((buttonId) => {
753
+ const self = selection?.chain[0];
754
+ const sessionId = frameProps?.sessionId;
755
+ if (!self || !sessionId)
756
+ return;
757
+ if (switchingRef.current) {
758
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
759
+ return;
760
+ }
761
+ if (buttonId === 'delete') {
762
+ uiWrite(() => deleteElement(depsOf(self)));
763
+ return;
764
+ }
765
+ if (buttonId === 'copy') {
766
+ setCommandError(null);
767
+ commands
768
+ .copyElement(self.selector)
769
+ .catch(error => setCommandError(describeCanvasError(error)));
770
+ return;
771
+ }
772
+ if (buttonId === 'paste') {
773
+ if (!clipboard) {
774
+ setCommandError(t('inlineCanvas.clipboard.nothingToPaste'));
775
+ return;
776
+ }
777
+ setPasteDialog(true);
778
+ return;
779
+ }
780
+ if (buttonId === 'move') {
781
+ setCommandError(null);
782
+ commands.startMove(self.selector);
783
+ return;
784
+ }
785
+ if (buttonId === 'replace-image') {
786
+ setImageDialog({ open: true, seed: '' });
787
+ return;
788
+ }
789
+ if (buttonId === 'replace-video') {
790
+ setVideoDialog({ open: true, seed: '' });
791
+ return;
792
+ }
793
+ if (buttonId === 'insert') {
794
+ setInsertDialog(true);
795
+ return;
796
+ }
797
+ if (buttonId === 'image-setting') {
798
+ setImageSettingDialog({ open: true, seed: '' });
799
+ return;
800
+ }
801
+ if (buttonId === 'click-action') {
802
+ setActionDialog({ open: true, seed: null });
803
+ return;
804
+ }
805
+ if (buttonId === 'edit-style') {
806
+ setStylePanelOpen(true);
807
+ return;
808
+ }
809
+ if (buttonId === 'edit-html') {
810
+ void getOuterHtml(sessionId, self.selector).then(r => setHtmlDialog({ open: true, seed: r.html }));
811
+ return;
812
+ }
813
+ if (buttonId === 'ask-ai') {
814
+ if (!onAskAi)
815
+ return;
816
+ setCommandError(null);
817
+ void getOuterHtml(sessionId, self.selector)
818
+ .then(r => onAskAi(buildAiContext(self, r.html), () => captureAiScreenshot(self.selector)))
819
+ .catch(error => setCommandError(describeCanvasError(error)));
820
+ return;
821
+ }
822
+ if (buttonId === 'screenshot') {
823
+ setCommandError(null);
824
+ setShot(null);
825
+ setShotUrl(null);
826
+ commands
827
+ .captureSelectedElement(self.selector)
828
+ .then(async (outcome) => {
829
+ setShot(outcome);
830
+ if (!uploadImage)
831
+ return;
832
+ const blob = await (await fetch(outcome.dataUrl)).blob();
833
+ setShotUrl(await uploadImage(blob));
834
+ })
835
+ .catch(error => setCommandError(describeCanvasError(error)));
836
+ return;
837
+ }
838
+ if (buttonId === 'edit-text') {
839
+ canvasIframeRef.current?.focus();
840
+ setCommandError(null);
841
+ commands
842
+ .startTextEdit(self.selector)
843
+ .catch(error => setCommandError(describeCanvasError(error)));
844
+ }
845
+ }, [clipboard, commands, depsOf, frameProps?.sessionId, selection, t, uploadImage]);
846
+ const self = selection?.chain[0];
847
+ return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsx(EditorChrome, { variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, changeCount: changeList.list.length, onExit: onExit, onSave: () => {
848
+ if (switchingRef.current) {
849
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
850
+ return;
851
+ }
852
+ onSave?.(editor);
853
+ }, onAddVersion: onAddVersion, onRenameVersion: onRenameVersion, onPreview: onPreview, onToggleChangeList: () => setShowChangeList(v => !v), changeListOpen: showChangeList, onToggleAi: onToggleAi, aiOpen: aiOpen }), _jsxs("div", { className: "relative flex min-h-0 flex-1", children: [_jsxs(ResizablePanelGroup, { direction: "horizontal", autoSaveId: "inline-editor-panes-v2", className: "flex min-h-0 w-full flex-1", children: [showLeftPanel && (_jsxs(_Fragment, { children: [_jsx(ResizablePanel, { id: "left", order: 1, defaultSize: 18, minSize: 12, maxSize: 40, className: "flex min-h-0 flex-col", children: _jsxs("aside", { "data-testid": "inline-editor-left-panel", className: "inline-editor-left flex h-full min-h-0 flex-col bg-white", children: [!!libraryComponents && (_jsx("div", { className: "flex h-9 shrink-0 items-center gap-1 border-b\n border-[#EBECF0] px-2", children: [
854
+ ['layers', '图层'],
855
+ ['assets', '组件']
856
+ ]
857
+ .map(([tab, label]) => (_jsx("button", { type: "button", "data-testid": `left-tab-${tab}`, "aria-pressed": leftTab === tab, onClick: () => setLeftTab(tab), className: `rounded px-2 py-1 text-[12px] ${leftTab === tab
858
+ ? 'bg-[#F4F5F7] font-semibold text-[#091E42]'
859
+ : 'text-[#6B778C]'}`, children: label }, tab))) })), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: leftTab === 'assets' && libraryComponents ? (_jsx(AssetsPanel, { components: libraryComponents, loading: libraryLoading, canInsert: !!selection?.chain[0] && !switching, onInsert: component => void insertComponent(component) })) : (_jsx(LayersPanel, { tree: tree, selectedSelector: selection?.selector ?? null, onSelect: s => void selectAndReveal(s), onHover: s => void commands.hover(s), onExpand: id => void commands.expandTreeNode(id), onMove: (dragSelector, targetSelector, pos) => uiWrite(() => writes.addMove({
860
+ identifier: dragSelector,
861
+ selector: dragSelector,
862
+ targetIdentifier: targetSelector,
863
+ targetSelector,
864
+ mode: moveModeFromDropPos(pos)
865
+ })) })) })] }) }), _jsx(ResizableHandle, { withHandle: true })] })), _jsx(ResizablePanel, { id: "canvas", order: 2, minSize: 30, children: _jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col", children: [_jsx(CanvasContextBar, { url: url, device: device, onDeviceChange: onDeviceChange }), _jsx("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: frameProps && (_jsx(CanvasFrame, { url: frameProps.url, sessionId: frameProps.sessionId, device: frameProps.device, panelWidth: panelWidth, targetViewportWidth: targetViewportWidth[frameProps.device], onFrameLoad: () => { }, domReady: domReady, iframeRef: canvasIframeRef }, frameProps.sessionId)) })] }) })] }), !capturing && shot && (_jsxs("section", { "data-testid": "inline-editor-shot", className: "absolute bottom-4 right-4 z-30 w-[260px] rounded-lg border\n border-[#DFE1E6] bg-white p-3\n shadow-[0_8px_24px_rgba(9,30,66,0.14),0_2px_8px_rgba(9,30,66,0.08)]", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("span", { className: "text-[13px] font-semibold text-[#091E42]", children: "\u622A\u56FE" }), _jsx("button", { type: "button", className: "text-[11px] text-[#6B778C]", onClick: () => {
866
+ setShot(null);
867
+ setShotUrl(null);
868
+ }, children: "\u5173\u95ED" })] }), _jsx("img", { src: shot.dataUrl, alt: "", className: "max-h-40 w-full rounded border border-[#EBECF0] object-contain" }), shot.truncated && (_jsx("p", { "data-testid": "inline-editor-shot-truncated", className: "mt-1.5 text-[11px] leading-4 text-[#DE350B]", children: "\u5143\u7D20\u8D85\u51FA\u53EF\u89C6\u533A\uFF0C\u53EA\u622A\u5230\u53EF\u89C1\u90E8\u5206" })), shot.scale < 1 && (_jsxs("p", { "data-testid": "inline-editor-shot-scale", className: "mt-1.5 text-[11px] leading-4 text-[#6B778C]", children: ["\u5F53\u524D\u7A97\u53E3\u8F83\u7A84\uFF0C\u622A\u56FE\u5206\u8FA8\u7387\u7EA6\u4E3A\u539F\u56FE\u7684", ` ${Math.round(shot.scale * 100)}%`] })), shotUrl && (_jsx("a", { "data-testid": "inline-editor-shot-url", href: shotUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1.5 block truncate text-[11px] text-[#0052CC] underline", children: shotUrl }))] })), _jsx(ChangeListPopover, { open: showChangeList && !capturing, onClose: () => setShowChangeList(false), children: _jsx(ChangeListPanel, { list: changeList.list, onRevert: id => uiWrite(() => writes.revert(id)), onRevertAll: () => uiWrite(() => writes.revertAll()), lastError: changeList.lastError, onHover: s => void commands.hover(s), onSelect: s => void selectAndReveal(s) }) })] }), !capturing && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsx("div", { "data-testid": "inline-editor-command-error", className: "fixed z-[2147483003] -translate-x-1/2 rounded bg-[#DE350B] px-2.5\n py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setCommandError(null), children: commandError })), !capturing && aiApplyNote && (_jsx("div", { "data-testid": "inline-editor-ai-apply-note", className: "fixed z-[2147483003] max-w-[520px] -translate-x-1/2 rounded\n bg-[#253858] px-2.5 py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setAiApplyNote(null), children: aiApplyNote })), !capturing && move && (_jsxs("div", { "data-testid": "inline-editor-move-bar", className: "fixed z-[2147483003] flex -translate-x-1/2 items-center gap-2\n rounded bg-[#091E42] px-2.5 py-1.5 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [move.target === null ? (_jsx(_Fragment, { children: _jsx("span", { children: move.rejected === 'same-element'
869
+ ? '不能移到它自己里面,请另选一个目标'
870
+ : t('inlineCanvas.move.pickTarget') }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: t('inlineCanvas.move.position') }), ['top', 'bottom', 'before', 'after'].map(pos => (_jsx("button", { type: "button", "data-testid": `move-position-${pos}`, className: "rounded bg-white/15 px-1.5 py-0.5 hover:bg-white/25", onClick: () => void commands
871
+ .confirmMove(pos)
872
+ .catch(e => setCommandError(describeCanvasError(e))), children: t(`inlineCanvas.move.${pos}`) }, pos)))] })), _jsx("button", { type: "button", "data-testid": "move-cancel", className: "ml-1 text-white/70 underline", onClick: () => commands.cancelMove(), children: t('inlineCanvas.move.cancel') })] })), !capturing && textEditing !== null && (_jsx("div", { "data-testid": "inline-editor-text-editing-hint", className: "pointer-events-none fixed z-[2147483003] -translate-x-1/2 rounded\n bg-[#091E42] px-2.5 py-1 text-[11px] leading-4 text-white shadow", style: {
873
+ left: `${area.x + area.width / 2}px`,
874
+ top: `${area.y + 8}px`
875
+ }, children: "\u6B63\u5728\u7F16\u8F91\u6587\u5B57 \u00B7 \u56DE\u8F66\u63D0\u4EA4 \u00B7 Esc \u53D6\u6D88" })), !capturing && !switching && (_jsx(SelectionToolbar, { selectedRect: selectedRect, area: area, capabilities: selection?.capabilities ?? null, isScrolling: geometry.isScrolling, onAction: onToolbarAction, host: { askAi: !!onAskAi } })), stylePanelOpen && !switching && selectedRect && self && (_jsx("div", { className: "fixed z-[2147483002]", style: {
876
+ left: `${Math.min(selectedRect.x + selectedRect.width + 12, area.x + area.width - 332)}px`,
877
+ top: `${Math.max(area.y + 8, selectedRect.y)}px`
878
+ }, children: _jsx(StylePanel, { tag: self.tag, capabilities: selection?.capabilities ?? null, declaredStyles: selection?.declaredStyles, onClose: () => setStylePanelOpen(false), onApply: (property, value) => uiWrite(() => editStyle(depsOf(self), property, value)) }) })), _jsx(Dialog, { open: pasteDialog, onOpenChange: setPasteDialog, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "paste-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.clipboard.pastePosition') }) }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: (selection?.capabilities ? pasteModes(selection.capabilities) : []).map(mode => (_jsx("button", { type: "button", "data-testid": `paste-mode-${mode}`, className: "rounded border border-[#DFE1E6] px-2 py-1 text-[12px]\n text-[#344563] hover:border-[#00A92E]", onClick: () => {
879
+ if (!self)
880
+ return;
881
+ setPasteDialog(false);
882
+ void commands
883
+ .pasteInto(self.selector, mode)
884
+ .catch(e => setCommandError(describeCanvasError(e)));
885
+ }, children: t(`inlineCanvas.clipboard.${mode}`) }, mode))) })] }) }), _jsx(ImageEditDialog, { open: imageDialog.open, capabilities: selection?.capabilities ?? null, seed: imageDialog.seed, uploadImage: uploadImage, onOpenChange: open => setImageDialog(prev => ({ ...prev, open })), onApply: (input) => self && uiWrite(() => replaceImage(depsOf(self), input)) }), _jsx(ReplaceVideoDialog, { open: videoDialog.open, seed: videoDialog.seed, onOpenChange: open => setVideoDialog(prev => ({ ...prev, open })), onApply: url => self && uiWrite(() => replaceVideo(depsOf(self), url)) }), _jsx(InsertElementDialog, { open: insertDialog, capabilities: selection?.capabilities ?? null, onOpenChange: setInsertDialog, onApply: input => self && uiWrite(() => insertElement(depsOf(self), input)) }), _jsx(ImageSettingDialog, { open: imageSettingDialog.open, seed: imageSettingDialog.seed, onOpenChange: open => setImageSettingDialog(prev => ({ ...prev, open })), onApply: alt => self && uiWrite(() => setImageAlt(depsOf(self), alt)) }), _jsx(ClickActionDialog, { open: actionDialog.open, seed: actionDialog.seed, onOpenChange: open => setActionDialog(prev => ({ ...prev, open })), onApply: action => self && uiWrite(() => setClickAction(depsOf(self), action)) }), _jsx(HtmlEditDialog, { open: htmlDialog.open, seed: htmlDialog.seed, onOpenChange: open => setHtmlDialog(prev => ({ ...prev, open })), onApply: html => self && uiWrite(() => editHtml(depsOf(self), html)) })] }));
886
+ }