diff --git a/apps/web/src/services/ai/design-pre-validation.ts b/apps/web/src/services/ai/design-pre-validation.ts index 79677fa6b..e9cac0ab5 100644 --- a/apps/web/src/services/ai/design-pre-validation.ts +++ b/apps/web/src/services/ai/design-pre-validation.ts @@ -21,19 +21,33 @@ import type { PenNode } from '@/types/pen'; * Detected issues that are skipped — `info` severity (detect-only) and * protected status-bar removals — are not counted, so callers can rely on * the return value as a faithful "did anything change" signal. + * + * Use {@link runPreValidationFixesDetailed} when the caller needs the + * per-category breakdown for surfacing in the UI. */ export function runPreValidationFixes(): number { + return runPreValidationFixesDetailed().total; +} + +export interface PreValidationResult { + total: number; + /** Number of fixes APPLIED per detector category (not detected). */ + byCategory: Record; +} + +export function runPreValidationFixesDetailed(): PreValidationResult { const store = useDocumentStore.getState(); const root = store.getNodeById(DEFAULT_FRAME_ID); - if (!root) return 0; + if (!root) return { total: 0, byCategory: {} }; const issues = detectAllIssues(root, store.document); return applyFixes(issues); } -function applyFixes(issues: Issue[]): number { +function applyFixes(issues: Issue[]): PreValidationResult { const store = useDocumentStore.getState(); let applied = 0; + const byCategory: Record = {}; for (const issue of issues) { // 'info' severity is detect-only — the detector emits the issue for // debug visibility but is not confident enough that an auto-fix would @@ -51,16 +65,18 @@ function applyFixes(issues: Issue[]): number { } store.removeNode(issue.nodeId); applied++; + byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1; console.log(`[Pre-validation] ${issue.nodeId}: removed (${issue.reason})`); } else { store.updateNode(issue.nodeId, { [issue.property]: issue.suggestedValue, } as Partial); applied++; + byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1; console.log( `[Pre-validation] ${issue.nodeId}: ${issue.property} → ${JSON.stringify(issue.suggestedValue)} (${issue.reason})`, ); } } - return applied; + return { total: applied, byCategory }; } diff --git a/apps/web/src/services/ai/design-validation.ts b/apps/web/src/services/ai/design-validation.ts index 3e150b511..fa9f01e81 100644 --- a/apps/web/src/services/ai/design-validation.ts +++ b/apps/web/src/services/ai/design-validation.ts @@ -20,7 +20,15 @@ import type { PenNode } from '@/types/pen'; import type { AIProviderType } from '@/types/agent-settings'; import { getCurrentVisualReference, clearVisualReference } from './visual-ref-orchestrator'; import { resolveSkills } from '@zseven-w/pen-ai-skills'; -import { runPreValidationFixes } from './design-pre-validation'; +import { runPreValidationFixesDetailed } from './design-pre-validation'; + +/** Stringify per-category counts as "3 text-effect, 2 unexpected-rotation". */ +function formatCategoryBreakdown(byCategory: Record): string { + return Object.entries(byCategory) + .sort(([, a], [, b]) => b - a) + .map(([cat, n]) => `${n} ${cat}`) + .join(', '); +} import { captureRootFrameScreenshot } from './design-screenshot'; import { applyValidationFixes, @@ -287,11 +295,14 @@ export async function runPostGenerationValidation(options?: { // Pre-validation: pure code checks (no LLM needed) emit('streaming', '[pending] Running pre-checks...'); - const preFixCount = runPreValidationFixes(); + const preFix = runPreValidationFixesDetailed(); + const preFixCount = preFix.total; if (preFixCount > 0) { totalApplied += preFixCount; - log[log.length - 1] = - `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`; + const breakdown = formatCategoryBreakdown(preFix.byCategory); + log[log.length - 1] = breakdown + ? `[done] Pre-checks: fixed ${preFixCount} (${breakdown})` + : `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`; } else { log[log.length - 1] = '[done] Pre-checks: OK'; } @@ -300,10 +311,13 @@ export async function runPostGenerationValidation(options?: { // If LLM validation is disabled, stop after pre-checks if (!VALIDATION_ENABLED) { clearVisualReference(); + const breakdown = formatCategoryBreakdown(preFix.byCategory); emit( 'done', preFixCount > 0 - ? `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}` + ? breakdown + ? `[done] Pre-checks: fixed ${preFixCount} (${breakdown})` + : `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}` : '[done] Pre-checks complete', ); return { applied: totalApplied, skipped: false }; @@ -315,10 +329,13 @@ export async function runPostGenerationValidation(options?: { const nodeCount = countNodesInActivePage(); if (nodeCount < VALIDATION_NODE_COUNT_THRESHOLD) { clearVisualReference(); + const breakdown = formatCategoryBreakdown(preFix.byCategory); emit( 'done', preFixCount > 0 - ? `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''} (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})` + ? breakdown + ? `[done] Pre-checks: fixed ${preFixCount} (${breakdown}) (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})` + : `[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 };