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;