Implements save/restore system that captures HTML base, injected CSS, executed scripts, and floating Vue windows with their full definitions. Adds 4 MCP tools, backend CRUD API, Pinia store, and script logger.
285 lines
7.9 KiB
TypeScript
285 lines
7.9 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import { useWindowsStore } from './windows'
|
|
import {
|
|
getScriptLog,
|
|
clearScriptLog,
|
|
getWindowDefinitions,
|
|
clearWindowDefinitions,
|
|
type WindowDefinitionEntry
|
|
} from '../services/tools/handlers/canvasHandlers'
|
|
|
|
const API_URL = ''
|
|
|
|
// ============================================
|
|
// TYPES
|
|
// ============================================
|
|
|
|
export interface SnapshotWindow {
|
|
id: string
|
|
title: string
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
zIndex: number
|
|
source: 'db' | 'inline'
|
|
componentId?: string
|
|
definition?: {
|
|
id: string
|
|
name: string
|
|
template: string
|
|
setup?: string
|
|
style?: string
|
|
imports?: string[]
|
|
props?: string[]
|
|
}
|
|
componentProps: Record<string, any>
|
|
}
|
|
|
|
export interface SnapshotCSSBlock {
|
|
id: string
|
|
css: string
|
|
}
|
|
|
|
export interface CanvasSnapshot {
|
|
id: string
|
|
name: string
|
|
created: number
|
|
baseHTML: string | null
|
|
baseScripts: string[]
|
|
cssBlocks: SnapshotCSSBlock[]
|
|
windows: SnapshotWindow[]
|
|
}
|
|
|
|
export interface SnapshotSummary {
|
|
id: string
|
|
name: string
|
|
thumbnail: string | null
|
|
created_at: number
|
|
}
|
|
|
|
// ============================================
|
|
// STORE
|
|
// ============================================
|
|
|
|
export const useSnapshotsStore = defineStore('snapshots', () => {
|
|
const snapshots = ref<SnapshotSummary[]>([])
|
|
|
|
// ---- Capture current canvas state ----
|
|
function captureState(name: string): CanvasSnapshot {
|
|
const windowsStore = useWindowsStore()
|
|
const winDefs = getWindowDefinitions()
|
|
|
|
// 1. Capture base HTML (excluding floating window containers)
|
|
const container = document.getElementById('canvas-content')
|
|
let baseHTML: string | null = null
|
|
if (container) {
|
|
const clone = container.cloneNode(true) as HTMLElement
|
|
// Remove dynamic component wrappers (floating windows)
|
|
clone.querySelectorAll('.dynamic-component-wrapper').forEach(el => el.remove())
|
|
const html = clone.innerHTML.trim()
|
|
baseHTML = html || null
|
|
}
|
|
|
|
// 2. Capture scripts
|
|
const baseScripts = getScriptLog()
|
|
|
|
// 3. Capture CSS blocks
|
|
const cssBlocks: SnapshotCSSBlock[] = []
|
|
document.querySelectorAll('style[id^="canvas-css-"]').forEach(style => {
|
|
const id = style.id.replace('canvas-css-', '')
|
|
cssBlocks.push({ id, css: style.textContent || '' })
|
|
})
|
|
|
|
// 4. Capture windows with definitions
|
|
const windows: SnapshotWindow[] = []
|
|
for (const win of windowsStore.windowsList) {
|
|
const entry: WindowDefinitionEntry | undefined = winDefs.get(win.id)
|
|
const snapWin: SnapshotWindow = {
|
|
id: win.id,
|
|
title: win.title,
|
|
x: win.x,
|
|
y: win.y,
|
|
width: win.width,
|
|
height: win.height,
|
|
zIndex: win.zIndex,
|
|
source: entry?.source || 'inline',
|
|
componentProps: entry?.componentProps || {}
|
|
}
|
|
|
|
if (entry?.source === 'db' && entry.componentId) {
|
|
snapWin.componentId = entry.componentId
|
|
}
|
|
|
|
if (entry?.definition) {
|
|
snapWin.definition = {
|
|
id: entry.definition.id,
|
|
name: entry.definition.name,
|
|
template: entry.definition.template,
|
|
setup: entry.definition.setup,
|
|
style: entry.definition.style,
|
|
imports: entry.definition.imports,
|
|
props: entry.definition.props
|
|
}
|
|
}
|
|
|
|
windows.push(snapWin)
|
|
}
|
|
|
|
return {
|
|
id: `snap-${Date.now()}`,
|
|
name,
|
|
created: Date.now(),
|
|
baseHTML,
|
|
baseScripts,
|
|
cssBlocks,
|
|
windows
|
|
}
|
|
}
|
|
|
|
// ---- Save snapshot to backend ----
|
|
async function save(name: string): Promise<string> {
|
|
const snapshot = captureState(name)
|
|
|
|
const res = await fetch(`${API_URL}/api/snapshots`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: snapshot.id,
|
|
name: snapshot.name,
|
|
data: snapshot,
|
|
created_at: snapshot.created
|
|
})
|
|
})
|
|
const result = await res.json()
|
|
await list() // refresh list
|
|
return result.id
|
|
}
|
|
|
|
// ---- List snapshots ----
|
|
async function list(): Promise<SnapshotSummary[]> {
|
|
const res = await fetch(`${API_URL}/api/snapshots`)
|
|
const data = await res.json()
|
|
snapshots.value = data
|
|
return data
|
|
}
|
|
|
|
// ---- Load full snapshot ----
|
|
async function load(id: string): Promise<CanvasSnapshot> {
|
|
const res = await fetch(`${API_URL}/api/snapshots/${id}`)
|
|
const row = await res.json()
|
|
return row.data
|
|
}
|
|
|
|
// ---- Remove snapshot ----
|
|
async function remove(id: string): Promise<void> {
|
|
await fetch(`${API_URL}/api/snapshots/${id}`, { method: 'DELETE' })
|
|
await list()
|
|
}
|
|
|
|
// ---- Restore a snapshot ----
|
|
async function restore(id: string): Promise<void> {
|
|
const snapshot = await load(id)
|
|
const windowsStore = useWindowsStore()
|
|
|
|
// 1. Close all current windows
|
|
for (const win of windowsStore.windowsList) {
|
|
const container = document.getElementById(`inline-${win.id}`)
|
|
if (container) container.remove()
|
|
document.getElementById(`style-${win.id}`)?.remove()
|
|
}
|
|
windowsStore.clear()
|
|
clearWindowDefinitions()
|
|
clearScriptLog()
|
|
|
|
// 2. Remove existing canvas CSS
|
|
document.querySelectorAll('style[id^="canvas-css-"]').forEach(el => el.remove())
|
|
|
|
// 3. Restore base HTML
|
|
const canvasEl = document.getElementById('canvas-content')
|
|
if (canvasEl) {
|
|
if (snapshot.baseHTML) {
|
|
canvasEl.innerHTML = snapshot.baseHTML
|
|
// Execute inline scripts
|
|
canvasEl.querySelectorAll('script').forEach(oldScript => {
|
|
const newScript = document.createElement('script')
|
|
Array.from(oldScript.attributes).forEach(attr => {
|
|
newScript.setAttribute(attr.name, attr.value)
|
|
})
|
|
newScript.textContent = oldScript.textContent
|
|
oldScript.parentNode?.replaceChild(newScript, oldScript)
|
|
})
|
|
} else {
|
|
canvasEl.innerHTML = ''
|
|
}
|
|
}
|
|
|
|
// 4. Restore CSS blocks
|
|
for (const block of snapshot.cssBlocks) {
|
|
const styleEl = document.createElement('style')
|
|
styleEl.id = `canvas-css-${block.id}`
|
|
document.head.appendChild(styleEl)
|
|
styleEl.textContent = block.css
|
|
}
|
|
|
|
// 5. Re-execute scripts
|
|
if (canvasEl && snapshot.baseScripts.length > 0) {
|
|
for (const code of snapshot.baseScripts) {
|
|
try {
|
|
const context = {
|
|
canvas: canvasEl,
|
|
windows: windowsStore.windowsList,
|
|
getWindow: (wid: string) => document.querySelector(`[data-window-id="${wid}"]`),
|
|
$: (selector: string) => canvasEl.querySelector(selector),
|
|
$$: (selector: string) => canvasEl.querySelectorAll(selector)
|
|
}
|
|
const fn = new Function('ctx', `with(ctx) { ${code} }`)
|
|
fn(context)
|
|
} catch (e) {
|
|
console.warn('[Snapshot] Script replay error:', e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Restore windows — dynamically import to avoid circular deps
|
|
const { renderInlineComponent } = await import('../services/dynamicComponents')
|
|
if (canvasEl) {
|
|
for (const win of snapshot.windows) {
|
|
if (!win.definition) continue
|
|
|
|
const definition = {
|
|
id: win.id,
|
|
name: win.definition.name,
|
|
template: win.definition.template,
|
|
setup: win.definition.setup,
|
|
style: win.definition.style,
|
|
props: win.definition.props,
|
|
imports: win.definition.imports || ['ref', 'reactive', 'computed']
|
|
}
|
|
|
|
const layout = { x: win.x, y: win.y, width: win.width, height: win.height }
|
|
renderInlineComponent(definition, canvasEl, win.componentProps || {}, true, layout)
|
|
|
|
// Re-track definition
|
|
getWindowDefinitions().set(win.id, {
|
|
source: win.source,
|
|
componentId: win.componentId,
|
|
definition,
|
|
componentProps: win.componentProps || {}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
snapshots,
|
|
captureState,
|
|
save,
|
|
list,
|
|
load,
|
|
remove,
|
|
restore
|
|
}
|
|
})
|