sitesmith: apply-ai-response utility (replace + patch + ask) + PageContext helpers
Add apply-ai-response.ts with serializeTreeForCraft, buildNodeTree, findNodeIdByAiNodeId, and useApplyAiResponse hook covering replace (site/page/section), patch (5 ops), and ask. Extend PageContext with replaceAllPages, replaceCurrentPage, setHeader, setFooter helpers that mirror the existing actions.deserialize/loadState pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useRef, ReactNode } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import { PageData } from '../types';
|
||||
import { SerializedTreeNode } from '../types/sitesmith';
|
||||
import { useSiteDesign, SiteDesign } from './SiteDesignContext';
|
||||
|
||||
interface PageContextValue {
|
||||
@@ -19,6 +20,11 @@ interface PageContextValue {
|
||||
setHeaderCraftState: (craftState: string) => void;
|
||||
setFooterCraftState: (craftState: string) => void;
|
||||
setPagesCraftState: (pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => void;
|
||||
/** AI helpers — replace entire site or page with a new tree */
|
||||
replaceAllPages: (pages: { name: string; tree: SerializedTreeNode }[]) => void;
|
||||
replaceCurrentPage: (page: { name: string; tree: SerializedTreeNode }) => void;
|
||||
setHeader: (tree: SerializedTreeNode) => void;
|
||||
setFooter: (tree: SerializedTreeNode) => void;
|
||||
siteDesign: SiteDesign;
|
||||
}
|
||||
|
||||
@@ -50,6 +56,10 @@ const PageContext = createContext<PageContextValue>({
|
||||
setHeaderCraftState: () => {},
|
||||
setFooterCraftState: () => {},
|
||||
setPagesCraftState: () => {},
|
||||
replaceAllPages: () => {},
|
||||
replaceCurrentPage: () => {},
|
||||
setHeader: () => {},
|
||||
setFooter: () => {},
|
||||
siteDesign: {} as SiteDesign,
|
||||
});
|
||||
|
||||
@@ -273,6 +283,110 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
})));
|
||||
}, []);
|
||||
|
||||
/** Flatten a SerializedTreeNode into a Craft.js SerializedNodes JSON string */
|
||||
const treeToState = useCallback((tree: SerializedTreeNode): string => {
|
||||
let counter = 0;
|
||||
const nodes: Record<string, unknown> = {};
|
||||
const walk = (node: SerializedTreeNode, parent: string | null): string => {
|
||||
const id = (node.props.node_id as string | undefined) || `ai-auto-${counter++}`;
|
||||
const childIds: string[] = [];
|
||||
nodes[id] = {
|
||||
type: node.type,
|
||||
isCanvas: true,
|
||||
props: node.props,
|
||||
displayName: node.type.resolvedName,
|
||||
custom: {},
|
||||
hidden: false,
|
||||
parent,
|
||||
nodes: childIds,
|
||||
linkedNodes: {},
|
||||
};
|
||||
for (const child of node.nodes ?? []) {
|
||||
childIds.push(walk(child, id));
|
||||
}
|
||||
return id;
|
||||
};
|
||||
const rootId = walk(tree, null);
|
||||
// Craft.js deserialize requires the root node keyed as 'ROOT'
|
||||
if (rootId !== 'ROOT') {
|
||||
nodes['ROOT'] = nodes[rootId];
|
||||
(nodes['ROOT'] as any).parent = null;
|
||||
delete nodes[rootId];
|
||||
// Fix up parent references from ROOT's children
|
||||
for (const childId of (nodes['ROOT'] as any).nodes) {
|
||||
if (nodes[childId]) (nodes[childId] as any).parent = 'ROOT';
|
||||
}
|
||||
}
|
||||
return JSON.stringify(nodes);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* AI helper: replace all pages with newly generated trees.
|
||||
* Stores each page's serialized state without touching the live canvas
|
||||
* (the canvas still shows the currently active page — call switchPage() if needed).
|
||||
*/
|
||||
const replaceAllPages = useCallback((newPages: { name: string; tree: SerializedTreeNode }[]) => {
|
||||
if (newPages.length === 0) return;
|
||||
const built = newPages.map((p, i) => ({
|
||||
id: i === 0 ? 'home' : `page_${Date.now()}_${i}`,
|
||||
name: p.name,
|
||||
slug: slugify(p.name),
|
||||
craftState: treeToState(p.tree),
|
||||
headCode: '',
|
||||
}));
|
||||
setPages(built);
|
||||
// Load the first page into the live canvas
|
||||
const firstState = built[0].craftState;
|
||||
setActivePageId(built[0].id);
|
||||
activePageIdRef.current = built[0].id;
|
||||
loadState(firstState, EMPTY_CANVAS);
|
||||
}, [treeToState, loadState]);
|
||||
|
||||
/**
|
||||
* AI helper: replace the current page's tree.
|
||||
* Deserializes the new tree into the live Craft.js canvas and persists it.
|
||||
*/
|
||||
const replaceCurrentPage = useCallback((page: { name: string; tree: SerializedTreeNode }) => {
|
||||
const craftState = treeToState(page.tree);
|
||||
const currentId = activePageIdRef.current;
|
||||
if (currentId === HEADER_ID) {
|
||||
setHeaderPage((prev) => ({ ...prev, name: page.name, craftState }));
|
||||
} else if (currentId === FOOTER_ID) {
|
||||
setFooterPage((prev) => ({ ...prev, name: page.name, craftState }));
|
||||
} else {
|
||||
setPages((prev) =>
|
||||
prev.map((p) => (p.id === currentId ? { ...p, name: page.name, craftState } : p)),
|
||||
);
|
||||
}
|
||||
loadState(craftState, EMPTY_CANVAS);
|
||||
}, [treeToState, loadState]);
|
||||
|
||||
/**
|
||||
* AI helper: replace the shared header tree.
|
||||
* Updates stored state; does NOT switch the canvas to header view.
|
||||
*/
|
||||
const setHeader = useCallback((tree: SerializedTreeNode) => {
|
||||
const craftState = treeToState(tree);
|
||||
setHeaderPage((prev) => ({ ...prev, craftState }));
|
||||
// If the canvas is currently showing the header, refresh it live
|
||||
if (activePageIdRef.current === HEADER_ID) {
|
||||
loadState(craftState, EMPTY_HEADER);
|
||||
}
|
||||
}, [treeToState, loadState]);
|
||||
|
||||
/**
|
||||
* AI helper: replace the shared footer tree.
|
||||
* Updates stored state; does NOT switch the canvas to footer view.
|
||||
*/
|
||||
const setFooter = useCallback((tree: SerializedTreeNode) => {
|
||||
const craftState = treeToState(tree);
|
||||
setFooterPage((prev) => ({ ...prev, craftState }));
|
||||
// If the canvas is currently showing the footer, refresh it live
|
||||
if (activePageIdRef.current === FOOTER_ID) {
|
||||
loadState(craftState, EMPTY_FOOTER);
|
||||
}
|
||||
}, [treeToState, loadState]);
|
||||
|
||||
return (
|
||||
<PageContext.Provider
|
||||
value={{
|
||||
@@ -291,6 +405,10 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
setHeaderCraftState,
|
||||
setFooterCraftState,
|
||||
setPagesCraftState,
|
||||
replaceAllPages,
|
||||
replaceCurrentPage,
|
||||
setHeader,
|
||||
setFooter,
|
||||
siteDesign: design,
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user