diff --git a/apps/backend/src/services/generate/prompts.ts b/apps/backend/src/services/generate/prompts.ts index 89a5f4c..ac2e5fc 100644 --- a/apps/backend/src/services/generate/prompts.ts +++ b/apps/backend/src/services/generate/prompts.ts @@ -104,19 +104,24 @@ export const buildSystemPrompt = (deps?: PackageInfo[]): string => export const buildCorrectivePrompt = ( error: GenerationError, successfulPatches: readonly Patch[] = [], + currentHtml?: string, ): string => { const applied = successfulPatches.length > 0 ? `\nAPPLIED: ${JSON.stringify(successfulPatches)}\nContinue from here.` : ""; + const pageState = currentHtml + ? `\nCURRENT PAGE STATE:\n${currentHtml}\n` + : ""; + if (error._tag === "JsonParseError") { - return `JSON ERROR: ${error.message} + return `${pageState}JSON ERROR: ${error.message} Bad: ${error.line.slice(0, 100)} Fix: valid JSONL, one JSON/line, single quotes in HTML attrs${applied}`; } - return `PATCH ERROR "${error.patch.selector}": ${error.reason} + return `${pageState}PATCH ERROR "${error.patch.selector}": ${error.reason} Fix: selector must exist, use #id only${applied}`; }; diff --git a/apps/backend/src/services/generate/service.ts b/apps/backend/src/services/generate/service.ts index 6fbeea1..c0a9ebe 100644 --- a/apps/backend/src/services/generate/service.ts +++ b/apps/backend/src/services/generate/service.ts @@ -3,7 +3,7 @@ import { streamText, type TextStreamPart } from "ai"; import type { LanguageModelConfig } from "@cuttlekit/common/server"; import { MemoryService, type MemorySearchResult } from "../memory/index.js"; import { accumulateLinesWithFlush } from "../../stream/utils.js"; -import { PatchValidator, renderCETree, type Patch, type ValidationContext } from "../vdom/index.js"; +import { PatchValidator, renderCETree, getCompactHtmlFromCtx, type Patch, type ValidationContext } from "../vdom/index.js"; import { ModelRegistry } from "../model-registry.js"; import { PatchSchema, @@ -305,6 +305,7 @@ export class GenerateService extends Effect.Service()( Effect.gen(function* () { const successfulPatches = yield* Ref.get(patchesRef); yield* Ref.set(patchesRef, []); // Reset for next attempt + const compactHtml = yield* getCompactHtmlFromCtx(validationCtx); yield* Effect.log( `[Attempt ${attempt}] ${genError._tag}, retrying...`, { @@ -323,6 +324,7 @@ export class GenerateService extends Effect.Service()( content: buildCorrectivePrompt( genError, successfulPatches, + compactHtml, ), }, ], diff --git a/apps/backend/src/services/vdom/patch-validator.ts b/apps/backend/src/services/vdom/patch-validator.ts index cd51bce..3c60343 100644 --- a/apps/backend/src/services/vdom/patch-validator.ts +++ b/apps/backend/src/services/vdom/patch-validator.ts @@ -1,5 +1,5 @@ import { Effect, Data, pipe } from "effect"; -import { Window } from "happy-dom"; +import { Window, HTMLElement as HappyHTMLElement } from "happy-dom"; import { applyPatch, type Patch } from "@cuttlekit/common/client"; import { makeCEShell, @@ -203,3 +203,35 @@ export class PatchValidator extends Effect.Service()( }), }, ) {} + +/** + * Extract compact HTML from a ValidationContext. + * Clones the DOM and strips CE template content, keeping only data-children content. + */ +export const getCompactHtmlFromCtx = (ctx: ValidationContext) => + Effect.gen(function* () { + if (ctx.registry.size === 0) return ctx.window.document.body.innerHTML; + + // Clone to avoid mutating real VDOM + const clone = yield* Effect.sync(() => + ctx.window.document.body.cloneNode(true) as unknown as HappyHTMLElement + ); + + // Strip rendered CE template content, keeping only data-children content + yield* pipe( + [...ctx.registry.keys()], + Effect.forEach((tag) => + pipe( + [...clone.querySelectorAll(tag)], + Effect.forEach((el) => + Effect.sync(() => { + const childrenContainer = el.querySelector("[data-children]"); + el.innerHTML = childrenContainer ? childrenContainer.innerHTML : ""; + }), + ), + ), + ), + ); + + return clone.innerHTML; + });