From 3e289ada3cb37f53950d45e928f64af4e0aa2c84 Mon Sep 17 00:00:00 2001 From: Fini Date: Fri, 8 May 2026 08:30:00 +0800 Subject: [PATCH] feat(ai): enable vision self-check loop on composite designs (Rank 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toggle VALIDATION_ENABLED from false to true so the post-generation vision LLM validation loop runs. The loop itself was fully built in design-validation.ts long ago — only ai-runtime-config:109 was holding it at runtime. Add VALIDATION_NODE_COUNT_THRESHOLD=30 size-gate so atomic single-tool outputs (one badge, one chart) skip the +30-90s vision round-trip. Composite multi-section briefs (full-page mockups, dashboards) easily clear the threshold and benefit from the screenshot -> vision LLM -> safe-fix -> re-screenshot rounds. Pre-validation heuristics (ms-cheap tree walks) still run regardless of size. Plumbing was already done before this commit: - design-screenshot.ts captureRegion() shipped in Phase 1.5 - design-validation.ts MAX_VALIDATION_ROUNDS=3 loop fully implemented - validate.ts has 4 vision provider paths (Anthropic Agent SDK, Codex CLI, OpenCode SDK, Gemini CLI) This commit only flips the flag and adds the size-gate heuristic. Tests: 4223/4223 pass; format:check + tsc clean. Predicted KPI lift: M3 composite +5-10pp (speculative). Vision catches what schema-coerce + role-hint can't — mis-positioned sibling sections, missing component spacing, color-contrast issues. Gating by node count keeps user-perceived latency contained to designs that actually need it. Out-of-scope for this commit (followups if needed): - ab-corpus glm/minimax/deepseek client image_url part injection (lets the eval harness exercise vision for KPI verification) - builtin Zig agent-native runtime image part support (only matters if the embedded provider becomes the default) --- apps/web/src/services/ai/ai-runtime-config.ts | 12 ++++++- apps/web/src/services/ai/design-validation.ts | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/web/src/services/ai/ai-runtime-config.ts b/apps/web/src/services/ai/ai-runtime-config.ts index c5de5c630..10e64fa8b 100644 --- a/apps/web/src/services/ai/ai-runtime-config.ts +++ b/apps/web/src/services/ai/ai-runtime-config.ts @@ -106,7 +106,17 @@ export const DESIGN_STREAM_TIMEOUTS = { } as const; /** When false, skips the vision LLM validation loop (pre-validation heuristics still run) */ -export const VALIDATION_ENABLED = false; +export const VALIDATION_ENABLED = true; + +/** + * Minimum total node count in the active page to trigger the vision LLM + * validation loop. Briefs that produce fewer nodes than this threshold + * skip vision (saves +30-90s + vision API tokens) — pre-validation + * heuristics still run regardless. Composite designs (multi-section + * dashboards, full-page mockups) easily clear this; atomic single-tool + * outputs (one badge, one chart) stay fast. + */ +export const VALIDATION_NODE_COUNT_THRESHOLD = 30; export const VALIDATION_TIMEOUT_MS = 180_000; export const MAX_VALIDATION_ROUNDS = 3; diff --git a/apps/web/src/services/ai/design-validation.ts b/apps/web/src/services/ai/design-validation.ts index 5515c815a..f9fd4150f 100644 --- a/apps/web/src/services/ai/design-validation.ts +++ b/apps/web/src/services/ai/design-validation.ts @@ -9,6 +9,7 @@ import { DEFAULT_FRAME_ID, useDocumentStore } from '@/stores/document-store'; import { VALIDATION_ENABLED, + VALIDATION_NODE_COUNT_THRESHOLD, VALIDATION_TIMEOUT_MS, MAX_VALIDATION_ROUNDS, VALIDATION_QUALITY_THRESHOLD, @@ -40,6 +41,21 @@ function getValidationSystemPrompt(): string { // Node tree dump — simplified for LLM context // --------------------------------------------------------------------------- +function countNodesInActivePage(): number { + const store = useDocumentStore.getState(); + const root = store.getNodeById(DEFAULT_FRAME_ID); + if (!root) return 0; + let count = 0; + function walk(node: PenNode): void { + count++; + if ('children' in node && Array.isArray(node.children)) { + for (const child of node.children) walk(child); + } + } + walk(root); + return count; +} + function buildNodeTreeDump(rootId: string): string { const store = useDocumentStore.getState(); const lines: string[] = []; @@ -276,6 +292,21 @@ export async function runPostGenerationValidation(options?: { return { applied: totalApplied, skipped: false }; } + // Skip vision loop on small designs — vision validation only earns its + // ~30-90s latency on composite multi-section briefs. Single-component + // outputs (one badge, one chart) are not worth the round-trip. + const nodeCount = countNodesInActivePage(); + if (nodeCount < VALIDATION_NODE_COUNT_THRESHOLD) { + clearVisualReference(); + emit( + 'done', + preFixCount > 0 + ? `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''} (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})` + : `[done] Pre-checks complete (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})`, + ); + return { applied: totalApplied, skipped: false }; + } + for (let round = 1; round <= MAX_VALIDATION_ROUNDS; round++) { const isFirstRound = round === 1;