From 8475797c7ff035fa1db8a85b18de945f3e6e79ae Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 9 May 2026 21:27:00 +0800 Subject: [PATCH] feat(ai): show per-category breakdown in pre-check status line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: when the pre-validation pass auto-fixes issues, the chat panel just says "Pre-checks: fixed 5 issues" — generic and uninformative. The user can't tell whether 5 invisible-container fixes happened (structural, mostly safe), 5 unexpected-rotation fixes (aesthetic, worth reviewing), or 5 mixed-sibling-padding fixes (consistency, worth reviewing). With 10 detector categories now (4 original + 6 aesthetic added in 53435bf7 / 7aef1b14 / cd1e4325 / ad025c95), the per-category visibility starts to matter. What: - runPreValidationFixesDetailed() returns { total, byCategory } where byCategory is a per-category count of APPLIED fixes (excludes the info-severity skips and the protected-status-bar skip). - runPreValidationFixes() kept as a thin wrapper returning .total so no caller needs to change. - design-validation.ts now uses the detailed result and formats the breakdown as e.g. "fixed 5 (3 text-effect, 2 unexpected-rotation)" in the chat panel — sorted by count descending so the dominant category surfaces first. Both the no-vision-validation path and the size-gated skip path show the breakdown when it exists. Falls back to the legacy "fixed N issues" format when byCategory is empty (defensive — should never happen if total > 0). 1080 / 1080 AI tests still pass — the new return shape is additive and the wrapper preserves the integer contract. --- .../src/services/ai/design-pre-validation.ts | 22 ++++++++++++-- apps/web/src/services/ai/design-validation.ts | 29 +++++++++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) 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 };