diff --git a/craft/src/state/PageContext.tsx b/craft/src/state/PageContext.tsx index f09d8b2..6353f12 100644 --- a/craft/src/state/PageContext.tsx +++ b/craft/src/state/PageContext.tsx @@ -12,6 +12,18 @@ import { useSiteDesign, SiteDesign } from './SiteDesignContext'; * because the shell claims to be a canvas but its render ignores `nodes`. */ const CANVAS_TYPES = new Set(['Container']); +/** Shells that wrap their content in a single . + * When the AI puts content directly under one of these, the children end up + * orphaned (the shell ignores data.nodes — it renders via the linkedNode) and + * Craft.js auto-creates the linkedNode at render time with a botched type + * field, which then crashes toNodeTree. Pre-create the linkedNode ourselves + * to keep the state shape Craft.js expects. */ +const SHELL_INNER: Record = { + Section: 'section-inner', + BackgroundSection: 'bg-section-inner', + FormContainer: 'form-inner', +}; + interface PageContextValue { pages: PageData[]; headerPage: PageData; @@ -340,9 +352,56 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (!cur.columns || cur.columns !== childIds.length) cur.columns = childIds.length; (nodes[id] as any).props = cur; } + // Section/BackgroundSection/FormContainer each render a single + // ... . If the AI + // nests content as direct children, Craft.js will auto-create the + // linkedNode on first render — and store its type as the Container + // component class rather than {resolvedName:'Container'}, which then + // crashes toNodeTree with "type (undefined) does not exist in resolver". + // Pre-create the linkedNode ourselves with the correct serialized type + // so Craft.js never has to materialize it. + const innerKey = SHELL_INNER[typeName ?? '']; + if (innerKey && childIds.length > 0) { + const innerId = `${id}__${innerKey}`; + nodes[innerId] = { + type: { resolvedName: 'Container' }, + isCanvas: true, + props: { tag: 'div' }, + displayName: 'Container', + custom: {}, + hidden: false, + parent: id, + nodes: [...childIds], + linkedNodes: {}, + }; + for (const cid of childIds) { + if (nodes[cid]) (nodes[cid] as any).parent = innerId; + } + (nodes[id] as any).nodes = []; + (nodes[id] as any).linkedNodes = { [innerKey]: innerId }; + } return id; }; const rootId = walk(tree, null); + // DIAGNOSTIC: dump the produced state + scan for type=undefined nodes + // so when the resolver invariant fires we can see which node is bad. + try { + const w = window as any; + w.__sitesmithLastTree = tree; + w.__sitesmithLastState = nodes; + const bad: Array<{id:string; node:any}> = []; + for (const [nid, n] of Object.entries(nodes)) { + const t = (n as any).type; + const name = t && typeof t === 'object' ? t.resolvedName : t; + if (!name) bad.push({ id: nid, node: n }); + } + if (bad.length > 0) { + // eslint-disable-next-line no-console + console.warn('[SITESMITH] nodes with undefined type:', bad); + } + // eslint-disable-next-line no-console + console.log('[SITESMITH] treeToState produced', Object.keys(nodes).length, 'nodes; rootId=', rootId); + } catch (_e) { /* diagnostic only */ } // Craft.js deserialize requires the root node keyed as 'ROOT' if (rootId !== 'ROOT') { nodes['ROOT'] = nodes[rootId]; diff --git a/craft/src/utils/apply-ai-response.ts b/craft/src/utils/apply-ai-response.ts index 8c6e4f4..75b2930 100644 --- a/craft/src/utils/apply-ai-response.ts +++ b/craft/src/utils/apply-ai-response.ts @@ -10,6 +10,12 @@ import { SitesmithResponse, SerializedTreeNode } from '../types/sitesmith'; * canvas but its render ignores `data.nodes`. */ const CANVAS_TYPES = new Set(['Container']); +const SHELL_INNER: Record = { + Section: 'section-inner', + BackgroundSection: 'bg-section-inner', + FormContainer: 'form-inner', +}; + /** * Flatten a SerializedTreeNode tree into a Craft.js node map ready for * `actions.deserialize()`. @@ -100,6 +106,35 @@ function buildNodeTree(query: any, tree: SerializedTreeNode): NodeTree { craftNodes[id].data.props.columns = colCount; } } + // Section/BackgroundSection/FormContainer wrap their content in a single + // . Pre-create the linkedNode + // so Craft.js doesn't auto-create one with a malformed type field. + const innerKey = SHELL_INNER[node.type.resolvedName]; + if (innerKey && craftNodes[id].data.nodes.length > 0) { + const innerId = `${id}__${innerKey}`; + const childIds: string[] = [...craftNodes[id].data.nodes]; + craftNodes[innerId] = { + id: innerId, + data: { + type: { resolvedName: 'Container' }, + props: { tag: 'div' }, + displayName: 'Container', + isCanvas: true, + parent: id, + nodes: childIds, + linkedNodes: {}, + hidden: false, + custom: {}, + }, + events: { selected: false, hovered: false, dragged: false }, + rules: { canDrag: () => true, canMoveIn: () => true, canMoveOut: () => true, canDrop: () => true }, + }; + for (const cid of childIds) { + if (craftNodes[cid]) craftNodes[cid].data.parent = innerId; + } + craftNodes[id].data.nodes = []; + craftNodes[id].data.linkedNodes = { [innerKey]: innerId }; + } return id; };