import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue" import { useRoute, useRouter } from "vue-router" import editorApi from "@/module/editor/service/editor-api" import { editorBridgeEventType } from "@/module/editor/model/editor-bridge-protocol" import { buildSelectedSelectorSnapshot, buildSelectedElementSummary, buildQuickEditActions, buildRecommendedFlowSummary, buildSelectedPreviewSummary, buildCanvasCheckSummary, buildCanvasFeedbackSummary, buildPreviewFreshness, buildCanvasCandidates, buildDraftFormGuide, buildInteractionStateSummary, buildOperatorGuide, buildWorkspaceConsistency, buildInspectorSummary, buildWorkspaceJourney, buildWorkspaceNextAction, buildWorkspaceReadiness, createChangeDraft, editorChangeTemplates, formatChangesJson, mapEditorSession, mapPreviewOperations, normalizeDraftByType, parseChangesJson, summarizeChanges, } from "@/module/editor/model/editor-workspace-model" export function useEditorWorkspacePage() { const route = useRoute() const router = useRouter() const createEmptySelectedElementMeta = () => ({ tag: "", text: "", href: "", src: "", hidden: false, source: "", }) const variantId = computed(() => String(route.params.variantId || "")) const loading = ref(false) const errorMessage = ref("") const session = ref(null) const changes = ref([]) const changesJson = ref("[]") const previewOperations = ref([]) const baseUrl = ref("") const selectedSelector = ref("") const changeDraft = ref(createChangeDraft()) const canvasBridgeState = ref("idle") const canvasBridgePageUrl = ref("") const selectedElementMeta = ref(createEmptySelectedElementMeta()) const hoveredElementMeta = ref({ selector: "", tag: "", text: "", href: "", src: "", source: "", }) const canvasEventLog = ref([]) const canvasBridgeCommand = ref(null) const canvasDocumentHtml = ref("") const draftFocusTarget = ref("") const deviceMode = ref("desktop") const historyStack = ref([]) const historyIndex = ref(-1) const insertBlockType = ref("content-card") let liveEditHistoryTimer = null const insertBlockTypeOptions = [ { value: "text-block", label: "文字區塊" }, { value: "button-block", label: "按鈕區塊" }, { value: "image-block", label: "圖片區塊" }, { value: "content-card", label: "內容卡片" }, ] const activeEditSelector = computed(() => selectedSelector.value) const snapshotEditorState = () => ({ changesJson: changesJson.value, changeDraft: JSON.parse(JSON.stringify(changeDraft.value)), previewOperations: JSON.parse(JSON.stringify(previewOperations.value)), selectedSelector: selectedSelector.value, selectedElementMeta: JSON.parse(JSON.stringify(selectedElementMeta.value || createEmptySelectedElementMeta())), canvasDocumentHtml: canvasDocumentHtml.value, }) const applySnapshot = (snapshot = {}) => { changesJson.value = snapshot.changesJson || "[]" changeDraft.value = normalizeDraftByType(snapshot.changeDraft || createChangeDraft()) previewOperations.value = Array.isArray(snapshot.previewOperations) ? snapshot.previewOperations : [] selectedSelector.value = snapshot.selectedSelector || "" selectedElementMeta.value = { ...createEmptySelectedElementMeta(), ...(snapshot.selectedElementMeta || {}), } canvasDocumentHtml.value = snapshot.canvasDocumentHtml || "" if (snapshot.canvasDocumentHtml) { canvasBridgeCommand.value = { type: editorBridgeEventType.RESTORE_CANVAS_STATE, pageHtml: snapshot.canvasDocumentHtml, selector: snapshot.selectedSelector || "", source: "editor-workspace", issuedAt: Date.now(), } } } const recordHistory = () => { const snapshot = snapshotEditorState() const nextStack = historyStack.value.slice(0, historyIndex.value + 1) const previousSnapshot = nextStack[nextStack.length - 1] if (previousSnapshot && JSON.stringify(previousSnapshot) === JSON.stringify(snapshot)) { return } nextStack.push(snapshot) historyStack.value = nextStack.slice(-20) historyIndex.value = historyStack.value.length - 1 } const invalidatePreviewOperations = () => { if (!previewOperations.value.length) { return } previewOperations.value = [] } const scheduleLiveEditHistory = () => { window.clearTimeout(liveEditHistoryTimer) liveEditHistoryTimer = window.setTimeout(() => { recordHistory() }, 280) } const canUndo = computed(() => historyIndex.value > 0) const canRedo = computed(() => historyIndex.value >= 0 && historyIndex.value < historyStack.value.length - 1) const previewOperationsPreview = computed(() => JSON.stringify(previewOperations.value, null, 2) ) const parsedChanges = computed(() => { try { return parseChangesJson(changesJson.value) } catch (error) { return [] } }) const changeSummary = computed(() => summarizeChanges(parsedChanges.value)) const canvasCandidates = computed(() => buildCanvasCandidates(parsedChanges.value) ) const inspectorSummary = computed(() => buildInspectorSummary({ selectedSelector: selectedSelector.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const interactionStateSummary = computed(() => buildInteractionStateSummary({ hoveredElementMeta: hoveredElementMeta.value, selectedSelector: selectedSelector.value, selectedElementMeta: selectedElementMeta.value, }) ) const workspaceConsistency = computed(() => buildWorkspaceConsistency({ selectedSelector: selectedSelector.value, draft: changeDraft.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const selectedSelectorSnapshot = computed(() => buildSelectedSelectorSnapshot({ selectedSelector: selectedSelector.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const selectedElementSummary = computed(() => buildSelectedElementSummary({ selectedSelector: selectedSelector.value, selectedElementMeta: selectedElementMeta.value, }) ) const quickEditActions = computed(() => buildQuickEditActions({ selectedSelector: selectedSelector.value, selectedElementSummary: selectedElementSummary.value, selectedElementMeta: selectedElementMeta.value, }) ) const draftFormGuide = computed(() => buildDraftFormGuide({ draft: changeDraft.value, selectedElementSummary: selectedElementSummary.value, selectedElementMeta: selectedElementMeta.value, }) ) const previewFreshness = computed(() => buildPreviewFreshness({ changes: parsedChanges.value, previewOperations: previewOperations.value, draft: changeDraft.value, }) ) const recommendedFlowSummary = computed(() => buildRecommendedFlowSummary({ selectedSelector: selectedSelector.value, selectedElementSummary: selectedElementSummary.value, draft: changeDraft.value, previewOperations: previewOperations.value, }) ) const selectedPreviewSummary = computed(() => buildSelectedPreviewSummary({ selectedSelector: selectedSelector.value, selectedSelectorSnapshot: selectedSelectorSnapshot.value, }) ) const canvasCheckSummary = computed(() => buildCanvasCheckSummary({ selectedSelector: selectedSelector.value, selectedElementSummary: selectedElementSummary.value, selectedPreviewSummary: selectedPreviewSummary.value, }) ) const canvasFeedbackSummary = computed(() => buildCanvasFeedbackSummary({ interactionStateSummary: interactionStateSummary.value, selectedSelector: selectedSelector.value, hoveredElementMeta: hoveredElementMeta.value, selectedElementSummary: selectedElementSummary.value, selectedPreviewSummary: selectedPreviewSummary.value, previewFreshness: previewFreshness.value, }) ) const workspaceReadiness = computed(() => buildWorkspaceReadiness({ baseUrl: baseUrl.value, session: session.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const nextAction = computed(() => buildWorkspaceNextAction({ baseUrl: baseUrl.value, session: session.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const workspaceJourney = computed(() => buildWorkspaceJourney({ baseUrl: baseUrl.value, session: session.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const operatorGuide = computed(() => buildOperatorGuide({ baseUrl: baseUrl.value, session: session.value, selectedSelector: selectedSelector.value, draft: changeDraft.value, changes: parsedChanges.value, previewOperations: previewOperations.value, }) ) const applyTemplate = (templateKey) => { const template = editorChangeTemplates.find((item) => item.key === templateKey) if (!template) { return } changesJson.value = formatChangesJson([...parsedChanges.value, template.item]) changeDraft.value = normalizeDraftByType({ ...template.item, selector_value: selectedSelector.value || template.item.selector_value || "", }) } const selectCanvasCandidate = (payload) => { const nextSelector = typeof payload === "string" ? payload : payload?.selector || "" selectedSelector.value = nextSelector changeDraft.value = normalizeDraftByType({ ...changeDraft.value, selector_value: nextSelector, }) selectedElementMeta.value = { tag: payload?.tag || "", text: payload?.text || "", href: payload?.href || "", src: payload?.src || "", hidden: Boolean(payload?.hidden), source: payload?.source || "candidate-button", } if (nextSelector) { canvasEventLog.value = [ { id: `${Date.now()}-${nextSelector}`, type: "selection", value: nextSelector, source: payload?.source || "candidate-button", }, ...canvasEventLog.value, ].slice(0, 8) } } const patchDraft = (patch = {}) => { changeDraft.value = normalizeDraftByType({ ...changeDraft.value, ...patch, payload: { ...changeDraft.value.payload, ...(patch.payload || {}), }, }) } const setDeviceMode = (mode = "desktop") => { if (!["desktop", "tablet", "mobile"].includes(mode)) { return } deviceMode.value = mode } const moveSelectedBlock = (direction = "down") => { if (!selectedSelector.value || !["up", "down"].includes(direction)) { return } sendCanvasCommand(editorBridgeEventType.MOVE_BLOCK, selectedSelector.value, { direction }) } const toggleSelectedBlockVisibility = () => { if (!selectedSelector.value) { return } sendCanvasCommand(editorBridgeEventType.TOGGLE_BLOCK_VISIBILITY, selectedSelector.value) selectedElementMeta.value = { ...selectedElementMeta.value, hidden: !selectedElementMeta.value.hidden } } const duplicateSelectedBlock = () => { if (!selectedSelector.value) { return } sendCanvasCommand(editorBridgeEventType.DUPLICATE_BLOCK, selectedSelector.value) } const deleteSelectedBlock = () => { if (!selectedSelector.value) { return } sendCanvasCommand(editorBridgeEventType.DELETE_BLOCK, selectedSelector.value) } const insertBlockNearSelection = (position = "after") => { if (!selectedSelector.value || !["before", "after"].includes(position)) { return } const selectedOption = insertBlockTypeOptions.find((option) => option.value === insertBlockType.value) || insertBlockTypeOptions[insertBlockTypeOptions.length - 1] sendCanvasCommand(editorBridgeEventType.INSERT_BLOCK, selectedSelector.value, { position, blockType: selectedOption.value, blockTypeLabel: selectedOption.label, }) } const applySelectedElementToDraft = () => { const targetSelector = activeEditSelector.value if (!targetSelector) { return } const textContent = selectedElementMeta.value.text || "" if (changeDraft.value.change_type === "replace_text" && textContent) { patchDraft({ selector_value: targetSelector, payload: { text: textContent, }, }) return } if (changeDraft.value.change_type === "set_html" && textContent) { patchDraft({ selector_value: targetSelector, payload: { html: textContent, }, }) return } patchDraft({ selector_value: targetSelector, }) } const applyRecommendedChangeType = () => { const targetSelector = activeEditSelector.value || selectedSelector.value const recommendedChangeType = selectedElementSummary.value?.recommendedChangeType || "replace_text" patchDraft({ change_type: recommendedChangeType, selector_value: targetSelector, payload: {}, }) const textContent = selectedElementMeta.value.text || "" if (recommendedChangeType === "replace_text" && textContent) { patchDraft({ payload: { text: textContent, }, }) return } if (recommendedChangeType === "set_html" && textContent) { patchDraft({ payload: { html: textContent, }, }) } } const resolveDraftFocusTarget = (changeType = "", selectedMeta = {}, preferredField = "") => { if (preferredField) { return preferredField } if (changeType === "replace_text" || changeType === "set_html") { return "content" } if (changeType === "set_attribute") { if (selectedMeta?.src) { return "attribute-value" } return "attribute-name" } if (changeType === "set_style") { return "style-property" } return "" } const queueDraftFocus = async (target = "") => { if (!target) { return } draftFocusTarget.value = target await nextTick() } const clearDraftFocusTarget = () => { draftFocusTarget.value = "" } const startDraftEditFlow = async (intent = "") => { const targetSelector = activeEditSelector.value || selectedSelector.value if (!targetSelector) { return } if (!session.value?.id) { await createSession() if (!session.value?.id) { return } } loadSelectedSelectorIntoDraft() syncSelectedSelectorToDraft() const draftMatchesSelection = changeDraft.value?.selector_value === targetSelector const selectedMeta = selectedElementMeta.value || {} const forcedIntent = typeof intent === "string" ? { changeType: intent, focusTarget: "", attributeName: "", attributeValue: "", } : { changeType: intent?.changeType || "", focusTarget: intent?.focusTarget || "", attributeName: intent?.attributeName || "", attributeValue: intent?.attributeValue || "", } const targetChangeType = forcedIntent.changeType || selectedElementSummary.value?.recommendedChangeType || "replace_text" if (forcedIntent.changeType) { patchDraft({ change_type: forcedIntent.changeType, selector_value: targetSelector, payload: {}, }) if (forcedIntent.changeType === "replace_text") { patchDraft({ payload: { text: selectedMeta.text || "新的主標題文案", }, }) } else if (forcedIntent.changeType === "set_attribute") { patchDraft({ payload: { name: forcedIntent.attributeName || (selectedMeta.src ? "src" : selectedMeta.href ? "href" : "href"), value: forcedIntent.attributeValue || selectedMeta.src || selectedMeta.href || "https://ose.tw", }, }) } else if (forcedIntent.changeType === "set_style") { patchDraft({ payload: { property: "backgroundColor", value: "#111827", }, }) } else { applyRecommendedChangeType() } } else if ( !draftMatchesSelection || !changeDraft.value?.change_type || !changeDraft.value?.payload || Object.keys(changeDraft.value.payload || {}).length === 0 ) { applyRecommendedChangeType() } scrollToSection("editor-draft-section") await queueDraftFocus( resolveDraftFocusTarget(targetChangeType, selectedMeta, forcedIntent.focusTarget) ) if (targetChangeType === "replace_text") { sendCanvasCommand("mkt-editor:start-inline-edit", targetSelector, { editKind: "replace_text", }) } if (targetChangeType === "set_attribute") { sendCanvasCommand("mkt-editor:start-inline-edit", targetSelector, { editKind: "set_attribute", attributeName: forcedIntent.attributeName || changeDraft.value?.payload?.name || (selectedMeta.src ? "src" : "href"), attributeValue: forcedIntent.attributeValue || changeDraft.value?.payload?.value || selectedMeta.src || selectedMeta.href || "", }) } if (targetChangeType === "set_style") { sendCanvasCommand("mkt-editor:start-inline-edit", targetSelector, { editKind: "set_style", styleProperty: changeDraft.value?.payload?.property || "backgroundColor", styleValue: changeDraft.value?.payload?.value || "#111827", }) } } const runRecommendedFlow = async () => { if (!selectedSelector.value) { return } const draftMatchesSelection = changeDraft.value?.selector_value === selectedSelector.value if (!draftMatchesSelection) { applyRecommendedChangeType() return } await buildPreview() } const runSelectedPreviewFlow = async () => { if (!selectedSelector.value) { return } if ((selectedSelectorSnapshot.value?.previewOperations || []).length === 0) { await buildPreview() return } scrollToSection("editor-canvas-section") } const runCanvasCheckFlow = async () => { if (!selectedSelector.value) { return } if (selectedPreviewSummary.value.status !== "ready") { await runSelectedPreviewFlow() return } focusSelectedOnCanvas() scrollToSection("editor-canvas-section") } const runQuickEditAction = async (changeType = "") => { if (!changeType) { return } await startDraftEditFlow({ changeType, focusTarget: resolveDraftFocusTarget(changeType, selectedElementMeta.value || {}), }) } const syncSelectedSelectorToDraft = () => { const targetSelector = activeEditSelector.value || selectedSelector.value if (!targetSelector) { return } patchDraft({ selector_value: targetSelector, }) } const applyDraftToChanges = () => { const nextDraft = normalizeDraftByType(changeDraft.value) changesJson.value = formatChangesJson([...parsedChanges.value, nextDraft]) recordHistory() } const replaceChangesWithDraft = () => { const nextDraft = normalizeDraftByType(changeDraft.value) changesJson.value = formatChangesJson([nextDraft]) recordHistory() } const loadSelectedSelectorIntoDraft = () => { const targetSelector = activeEditSelector.value || selectedSelector.value if (!targetSelector) { return } const matchedChange = parsedChanges.value.find( (item) => item.selector_value === targetSelector ) if (matchedChange) { changeDraft.value = normalizeDraftByType(matchedChange) return } changeDraft.value = normalizeDraftByType({ ...changeDraft.value, selector_value: targetSelector, }) } const createSession = async () => { loading.value = true errorMessage.value = "" try { const response = await editorApi.createEditorSession({ variant_id: variantId.value, base_url: baseUrl.value || null, mode: "edit", }) session.value = mapEditorSession(response) } catch (error) { errorMessage.value = error?.message || "無法建立 editor session。" } finally { loading.value = false } } const loadChanges = async () => { loading.value = true errorMessage.value = "" try { changes.value = await editorApi.getVariantChanges(variantId.value) changesJson.value = formatChangesJson(changes.value) recordHistory() } catch (error) { errorMessage.value = error?.message || "無法載入 variant changes。" } finally { loading.value = false } } const saveChanges = async () => { loading.value = true errorMessage.value = "" try { const items = parseChangesJson(changesJson.value) changes.value = await editorApi.saveVariantChanges(variantId.value, items) changesJson.value = formatChangesJson(changes.value) } catch (error) { errorMessage.value = error?.message || "無法保存 variant changes。" } finally { loading.value = false } } const buildPreview = async () => { loading.value = true errorMessage.value = "" try { const items = parseChangesJson(changesJson.value) const preview = await editorApi.buildPreview({ variant_id: variantId.value, items: items, }) previewOperations.value = mapPreviewOperations(preview) recordHistory() } catch (error) { errorMessage.value = error?.message || "無法 build preview。" } finally { loading.value = false } } const undoHistory = () => { if (!canUndo.value) { return } historyIndex.value -= 1 applySnapshot(historyStack.value[historyIndex.value]) } const redoHistory = () => { if (!canRedo.value) { return } historyIndex.value += 1 applySnapshot(historyStack.value[historyIndex.value]) } const goBack = () => { router.back() } const loadDemoPage = () => { if (typeof window === "undefined") { return } baseUrl.value = `${window.location.origin}/editor-demo.html` } const handleCanvasReady = (payload = {}) => { canvasBridgeState.value = "ready" canvasBridgePageUrl.value = payload.page_url || baseUrl.value || "" if (payload.page_html) { const shouldSeedHistory = !canvasDocumentHtml.value || historyStack.value.length === 0 || !historyStack.value[historyStack.value.length - 1]?.canvasDocumentHtml canvasDocumentHtml.value = payload.page_html if (shouldSeedHistory) { recordHistory() } } canvasEventLog.value = [ { id: `${Date.now()}-ready`, type: "ready", value: payload.page_url || baseUrl.value || "", source: "canvas-bridge", }, ...canvasEventLog.value, ].slice(0, 8) } const applySelectedMetaFromPayload = (payload = {}, fallbackMeta = selectedElementMeta.value) => ({ ...fallbackMeta, tag: payload.tag || fallbackMeta.tag || "", text: payload.text || fallbackMeta.text || "", href: payload.href || fallbackMeta.href || "", src: payload.src || fallbackMeta.src || "", hidden: Boolean(payload.hidden), source: payload.source || "canvas-bridge", }) const syncDraftSelectorAfterStructureChange = (previousSelectedSelector = "", nextSelector = "") => { if ( changeDraft.value?.selector_value && changeDraft.value.selector_value === previousSelectedSelector ) { patchDraft({ selector_value: nextSelector || "", }) } if (!nextSelector) { changeDraft.value = normalizeDraftByType({ ...changeDraft.value, selector_value: "", }) clearDraftFocusTarget() } } const handleCanvasOutbound = (payload = {}) => { if (!payload?.type) { return } if ( payload.type === editorBridgeEventType.BLOCK_VISIBILITY_CHANGE && payload.selector === selectedSelector.value ) { selectedElementMeta.value = applySelectedMetaFromPayload(payload) canvasDocumentHtml.value = payload.pageHtml || canvasDocumentHtml.value invalidatePreviewOperations() recordHistory() } if (payload.type === editorBridgeEventType.BLOCK_STRUCTURE_CHANGE) { const previousSelectedSelector = selectedSelector.value canvasDocumentHtml.value = payload.pageHtml || canvasDocumentHtml.value invalidatePreviewOperations() if (["move", "duplicate"].includes(payload.action) && payload.nextSelector) { selectedSelector.value = payload.nextSelector selectedElementMeta.value = applySelectedMetaFromPayload(payload) } if (payload.action === "delete") { selectedSelector.value = payload.nextSelector || "" if (!payload.nextSelector) { selectedElementMeta.value = createEmptySelectedElementMeta() } else { selectedElementMeta.value = applySelectedMetaFromPayload(payload) } } if (payload.action === "insert" && payload.nextSelector) { selectedSelector.value = payload.nextSelector selectedElementMeta.value = applySelectedMetaFromPayload(payload) if (payload.blockType === "button-block") { patchDraft({ change_type: "set_attribute", selector_value: payload.nextSelector, payload: { name: "href", value: payload.href || "https://ose.tw", }, }) draftFocusTarget.value = "attribute-value" sendCanvasCommand(editorBridgeEventType.START_INLINE_EDIT, payload.nextSelector, { editKind: "set_attribute", attributeName: "href", attributeValue: payload.href || "https://ose.tw", }) } else if (payload.blockType === "image-block") { patchDraft({ change_type: "set_attribute", selector_value: payload.nextSelector, payload: { name: "src", value: payload.src || "", }, }) draftFocusTarget.value = "attribute-value" sendCanvasCommand(editorBridgeEventType.START_INLINE_EDIT, payload.nextSelector, { editKind: "set_attribute", attributeName: "src", attributeValue: payload.src || "", }) } else { patchDraft({ change_type: "replace_text", selector_value: payload.nextSelector, payload: { text: payload.text || "新的區塊標題", }, }) draftFocusTarget.value = "content" if (payload.blockType === "text-block") { sendCanvasCommand(editorBridgeEventType.START_INLINE_EDIT, payload.nextSelector, { editKind: "replace_text", }) } } } syncDraftSelectorAfterStructureChange(previousSelectedSelector, payload.nextSelector || "") recordHistory() } canvasEventLog.value = [ { id: `${Date.now()}-${payload.type}`, type: payload.type, value: payload.value || "", source: payload.source || "editor-workspace", }, ...canvasEventLog.value, ].slice(0, 8) } const hydrateSelectionFromPayload = (payload = {}) => { if (payload.selector) { selectedSelector.value = payload.selector } selectedElementMeta.value = { tag: payload.tag || selectedElementMeta.value.tag || "", text: payload.text || selectedElementMeta.value.text || "", href: payload.href || selectedElementMeta.value.href || "", src: payload.src || selectedElementMeta.value.src || "", hidden: Boolean(payload.hidden), source: payload.source || selectedElementMeta.value.source || "canvas-hotspot", } } const hydrateDraftIntentFromPayload = (payload = {}) => { if (!payload.intentChangeType) { return } patchDraft({ change_type: payload.intentChangeType, selector_value: payload.selector || selectedSelector.value, payload: payload.intentChangeType === "set_attribute" ? { name: payload.intentAttributeName || "href", value: payload.intentAttributeValue || "", } : payload.intentChangeType === "set_style" ? { property: "backgroundColor", value: "#111827", } : payload.intentChangeType === "replace_text" ? { text: payload.text || selectedElementMeta.value.text || "新的主標題文案", } : {}, }) } const handleCanvasHotspotAction = async (payload = {}) => { hydrateSelectionFromPayload(payload) hydrateDraftIntentFromPayload(payload) switch (payload.action) { case "select-current": loadSelectedSelectorIntoDraft() syncSelectedSelectorToDraft() scrollToSection("editor-inspector-section") return case "inspect-current": scrollToSection("editor-inspector-section") return case "edit-current": await startDraftEditFlow() return case "edit-text-current": await startDraftEditFlow({ changeType: payload.intentChangeType || "replace_text", focusTarget: payload.intentField || "content", }) return case "edit-link-current": case "edit-image-current": await startDraftEditFlow({ changeType: payload.intentChangeType || "set_attribute", focusTarget: payload.intentField || "attribute-value", attributeName: payload.intentAttributeName || "", attributeValue: payload.intentAttributeValue || "", }) return case "edit-style-current": await startDraftEditFlow({ changeType: payload.intentChangeType || "set_style", focusTarget: payload.intentField || "style-property", }) return case "confirm-current": await runCanvasCheckFlow() return default: return } } const handleCanvasHover = (payload = {}) => { hoveredElementMeta.value = { selector: payload.selector || "", tag: payload.tag || "", text: payload.text || "", href: payload.href || "", src: payload.src || "", source: payload.source || "canvas-hover", } } const handleInlineTextChange = (payload = {}) => { if (!payload.selector) { return } selectedSelector.value = payload.selector selectedElementMeta.value = { ...selectedElementMeta.value, text: payload.text || selectedElementMeta.value.text || "", source: payload.source || "canvas-inline-edit", } canvasDocumentHtml.value = payload.pageHtml || canvasDocumentHtml.value invalidatePreviewOperations() if (changeDraft.value.change_type === "replace_text") { patchDraft({ selector_value: payload.selector, payload: { text: payload.text || "", }, }) } if (payload.source === "canvas-inline-edit-live") { scheduleLiveEditHistory() } else { recordHistory() } } const clearHoveredCandidate = () => { hoveredElementMeta.value = { selector: "", tag: "", text: "", href: "", src: "", hidden: false, source: "", } } const keepCurrentSelection = () => { clearHoveredCandidate() focusSelectedOnCanvas() scrollToSection("editor-canvas-section") } const promoteHoveredToSelected = () => { if (!hoveredElementMeta.value.selector) { return } selectCanvasCandidate({ selector: hoveredElementMeta.value.selector, tag: hoveredElementMeta.value.tag, text: hoveredElementMeta.value.text, href: hoveredElementMeta.value.href, src: hoveredElementMeta.value.src, source: "canvas-hover-promoted", }) clearHoveredCandidate() } const inspectHoveredOnCanvas = () => { if (!hoveredElementMeta.value.selector) { return } sendCanvasCommand("mkt-editor:inspect-selector", hoveredElementMeta.value.selector) scrollToSection("editor-canvas-section") } const sendCanvasCommand = (type, selector, extra = {}) => { if (!selector) { return } canvasBridgeCommand.value = { id: `${Date.now()}-${type}-${selector}`, type, selector, ...extra, source: "editor-workspace", } } const handleInlineAttributeChange = (payload = {}) => { if (!payload.selector || !payload.attributeName) { return } selectedSelector.value = payload.selector selectedElementMeta.value = { ...selectedElementMeta.value, href: payload.attributeName === "href" ? payload.attributeValue || "" : selectedElementMeta.value.href || "", src: payload.attributeName === "src" ? payload.attributeValue || "" : selectedElementMeta.value.src || "", source: payload.source || "canvas-inline-edit", } canvasDocumentHtml.value = payload.pageHtml || canvasDocumentHtml.value invalidatePreviewOperations() if (changeDraft.value.change_type === "set_attribute") { patchDraft({ selector_value: payload.selector, payload: { name: payload.attributeName, value: payload.attributeValue || "", }, }) } if (payload.source === "canvas-inline-edit-live") { scheduleLiveEditHistory() } else { recordHistory() } } const handleInlineStyleChange = (payload = {}) => { if (!payload.selector || !payload.styleProperty) { return } selectedSelector.value = payload.selector selectedElementMeta.value = { ...selectedElementMeta.value, source: payload.source || "canvas-inline-edit", } canvasDocumentHtml.value = payload.pageHtml || canvasDocumentHtml.value invalidatePreviewOperations() if (changeDraft.value.change_type === "set_style") { patchDraft({ selector_value: payload.selector, payload: { property: payload.styleProperty, value: payload.styleValue || "", }, }) } if (payload.source === "canvas-inline-edit-live") { scheduleLiveEditHistory() } else { recordHistory() } } const focusSelectedOnCanvas = () => { sendCanvasCommand("mkt-editor:focus-selector", selectedSelector.value) } const inspectSelectedOnCanvas = () => { sendCanvasCommand("mkt-editor:inspect-selector", selectedSelector.value) } const runInteractionStateAction = async () => { switch (interactionStateSummary.value.stage) { case "idle": scrollToSection("editor-canvas-section") return case "hover": promoteHoveredToSelected() return case "compare": keepCurrentSelection() return default: await runCanvasCheckFlow() } } const scrollToSection = (sectionId) => { document .getElementById(sectionId) ?.scrollIntoView({ behavior: "smooth", block: "start" }) } const runNextAction = async () => { switch (nextAction.value.key) { case "create_session": await createSession() return case "apply_template": applyTemplate("replace_text") return case "build_preview": await buildPreview() return case "canvas_next": scrollToSection("editor-canvas-section") return default: return } } onMounted(async () => { if (typeof route.query.base_url === "string" && route.query.base_url) { baseUrl.value = route.query.base_url } await loadChanges() if (canvasCandidates.value.length > 0) { selectCanvasCandidate({ ...canvasCandidates.value[0], source: "initial-candidate", }) changeDraft.value = normalizeDraftByType({ ...changeDraft.value, selector_value: selectedSelector.value, }) } if (historyStack.value.length === 0) { recordHistory() } }) onBeforeUnmount(() => { window.clearTimeout(liveEditHistoryTimer) }) return { variantId, loading, errorMessage, session, changes, changesJson, previewOperations, previewOperationsPreview, parsedChanges, changeSummary, canvasCandidates, interactionStateSummary, inspectorSummary, workspaceConsistency, selectedSelectorSnapshot, selectedElementSummary, quickEditActions, previewFreshness, recommendedFlowSummary, selectedPreviewSummary, canvasCheckSummary, canvasFeedbackSummary, selectedSelector, changeDraft, canvasBridgeState, canvasBridgePageUrl, selectedElementMeta, hoveredElementMeta, canvasEventLog, canvasBridgeCommand, workspaceReadiness, nextAction, workspaceJourney, operatorGuide, draftFormGuide, changeTemplates: editorChangeTemplates, baseUrl, selectCanvasCandidate, patchDraft, applySelectedElementToDraft, applyRecommendedChangeType, runRecommendedFlow, runSelectedPreviewFlow, runCanvasCheckFlow, runQuickEditAction, startDraftEditFlow, syncSelectedSelectorToDraft, applyDraftToChanges, replaceChangesWithDraft, loadSelectedSelectorIntoDraft, applyTemplate, createSession, loadChanges, saveChanges, buildPreview, loadDemoPage, runNextAction, scrollToSection, handleCanvasReady, handleCanvasHotspotAction, handleCanvasHover, handleInlineAttributeChange, handleInlineStyleChange, handleInlineTextChange, clearHoveredCandidate, keepCurrentSelection, handleCanvasOutbound, promoteHoveredToSelected, inspectHoveredOnCanvas, focusSelectedOnCanvas, inspectSelectedOnCanvas, runInteractionStateAction, draftFocusTarget, clearDraftFocusTarget, goBack, deviceMode, setDeviceMode, insertBlockType, insertBlockTypeOptions, moveSelectedBlock, toggleSelectedBlockVisibility, duplicateSelectedBlock, deleteSelectedBlock, insertBlockNearSelection, canUndo, canRedo, undoHistory, redoHistory, } }