From 177f7778b9351145d2c6976eb84972ed50d57818 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 11 Mar 2026 16:23:20 -0400 Subject: [PATCH] Don't validate empty descriptions, other validation enhancements --- .../components/ai/AiDescriptionCompare.tsx | 20 ++++- .../product-editor/ProductEditForm.tsx | 13 ++- .../components/AiSuggestionBadge.tsx | 86 ++++++++++++++----- .../components/ValidationTable.tsx | 79 +++++++++++++++-- .../components/cells/MultilineInput.tsx | 5 ++ .../hooks/useAutoInlineAiValidation.ts | 6 +- 6 files changed, 174 insertions(+), 35 deletions(-) diff --git a/inventory/src/components/ai/AiDescriptionCompare.tsx b/inventory/src/components/ai/AiDescriptionCompare.tsx index 0553219..e49fc57 100644 --- a/inventory/src/components/ai/AiDescriptionCompare.tsx +++ b/inventory/src/components/ai/AiDescriptionCompare.tsx @@ -18,7 +18,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; -import { Sparkles, AlertCircle, Check } from "lucide-react"; +import { Sparkles, AlertCircle, Check, RefreshCw } from "lucide-react"; import { cn } from "@/lib/utils"; export interface AiDescriptionCompareProps { @@ -28,6 +28,10 @@ export interface AiDescriptionCompareProps { issues: string[]; onAccept: (editedSuggestion: string) => void; onDismiss: () => void; + /** Called to re-roll (re-run) the AI validation */ + onRevalidate?: () => void; + /** Whether re-validation is in progress */ + isRevalidating?: boolean; productName?: string; className?: string; } @@ -39,6 +43,8 @@ export function AiDescriptionCompare({ issues, onAccept, onDismiss, + onRevalidate, + isRevalidating = false, productName, className, }: AiDescriptionCompareProps) { @@ -226,6 +232,18 @@ export function AiDescriptionCompare({ > Ignore + {onRevalidate && ( + + )} diff --git a/inventory/src/components/product-editor/ProductEditForm.tsx b/inventory/src/components/product-editor/ProductEditForm.tsx index f94333b..e844639 100644 --- a/inventory/src/components/product-editor/ProductEditForm.tsx +++ b/inventory/src/components/product-editor/ProductEditForm.tsx @@ -218,6 +218,7 @@ export function ProductEditForm({ const watchCompany = watch("company"); const watchLine = watch("line"); + const watchDescription = watch("description"); // Populate form on mount useEffect(() => { @@ -452,7 +453,7 @@ export function ProductEditForm({ const handleValidateDescription = useCallback(async () => { const values = getValues(); - if (!values.description?.trim()) return; + if (!values.description?.trim() || values.description.trim().length < 10) return; clearDescriptionResult(); setValidatingField("description"); const companyLabel = fieldOptions.companies.find((c) => c.value === values.company)?.label; @@ -550,7 +551,7 @@ export function ProductEditForm({
{fc.label} - {isDescription && ( + {isDescription && (watchDescription?.trim().length ?? 0) >= 10 && ( descriptionResult?.suggestion ? ( + + +

Refresh suggestion

+
+ + + )} {/* Info icon with issues tooltip */} {issues.length > 0 && ( - - + + { e.stopPropagation(); - onAccept(); + onAccept(suggestion); }} > diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx index 4f027df..7aa13cd 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx @@ -588,9 +588,9 @@ const CellWrapper = memo(({ // Check if description should be validated const descIsDismissed = nameSuggestion?.dismissed?.description; const descIsValidating = inlineAi.validating.has(`${contextProductIndex}-description`); - const descValue = currentRowForContext.description && String(currentRowForContext.description).trim(); + const descValue = currentRowForContext.description ? String(currentRowForContext.description).trim() : ''; - if (descValue && !descIsDismissed && !descIsValidating) { + if (descValue.length >= 10 && !descIsDismissed && !descIsValidating) { // Trigger description validation setInlineAiValidating(`${contextProductIndex}-description`, true); @@ -687,7 +687,9 @@ const CellWrapper = memo(({ // Trigger inline AI validation for name/description fields // This validates spelling, grammar, and naming conventions using Groq // Only trigger if value actually changed to avoid unnecessary API calls - if (isInlineAiField && valueChanged && valueToSave && String(valueToSave).trim()) { + const trimmedValue = valueToSave ? String(valueToSave).trim() : ''; + const meetsMinLength = field.key === 'description' ? trimmedValue.length >= 10 : trimmedValue.length > 0; + if (isInlineAiField && valueChanged && meetsMinLength) { const currentRow = useValidationStore.getState().rows[rowIndex]; const fields = useValidationStore.getState().fields; if (currentRow) { @@ -751,6 +753,66 @@ const CellWrapper = memo(({ }, 0); }, [rowIndex, field.key, isEmbeddingField, aiSuggestions, isInlineAiField, productIndex]); + // Manual re-validate: triggers inline AI validation regardless of value changes + const handleRevalidate = useCallback(() => { + if (!isInlineAiField) return; + const state = useValidationStore.getState(); + const currentRow = state.rows[rowIndex]; + if (!currentRow) return; + + const fieldKey = field.key as 'name' | 'description'; + const currentValue = String(currentRow[fieldKey] ?? '').trim(); + + // Name requires non-empty, description requires ≥10 chars + if (fieldKey === 'name' && !currentValue) return; + if (fieldKey === 'description' && currentValue.length < 10) return; + + const validationKey = `${productIndex}-${fieldKey}`; + if (state.inlineAi.validating.has(validationKey)) return; + + const { setInlineAiValidating, setInlineAiSuggestion, markInlineAiAutoValidated, fields: storeFields, rows } = state; + setInlineAiValidating(validationKey, true); + markInlineAiAutoValidated(productIndex, fieldKey); + + // Clear dismissed state so new result shows + const suggestions = state.inlineAi.suggestions.get(productIndex); + if (suggestions?.dismissed?.[fieldKey]) { + // Reset dismissed by re-setting suggestion (will be overwritten by API result) + setInlineAiSuggestion(productIndex, fieldKey, { + isValid: true, + suggestion: undefined, + issues: [], + }); + } + + const payload = fieldKey === 'name' + ? buildNameValidationPayload(currentRow, storeFields, rows) + : buildDescriptionValidationPayload(currentRow, storeFields); + + const endpoint = fieldKey === 'name' + ? '/api/ai/validate/inline/name' + : '/api/ai/validate/inline/description'; + + fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ product: payload }), + }) + .then(res => res.json()) + .then(result => { + if (result.success !== false) { + setInlineAiSuggestion(productIndex, fieldKey, { + isValid: result.isValid ?? true, + suggestion: result.suggestion, + issues: result.issues || [], + latencyMs: result.latencyMs, + }); + } + }) + .catch(err => console.error(`[InlineAI] manual ${fieldKey} revalidation error:`, err)) + .finally(() => setInlineAiValidating(validationKey, false)); + }, [rowIndex, field.key, isInlineAiField, productIndex]); + // Stable callback for fetching options (for line/subline dropdowns) const handleFetchOptions = useCallback(async () => { const state = useValidationStore.getState(); @@ -854,6 +916,7 @@ const CellWrapper = memo(({ onDismissAiSuggestion: () => { useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'description'); }, + onRevalidate: handleRevalidate, })} />
@@ -925,12 +988,18 @@ const CellWrapper = memo(({ { - useValidationStore.getState().acceptInlineAiSuggestion(productIndex, 'name'); + onAccept={(editedValue) => { + const state = useValidationStore.getState(); + // Update the cell with the (possibly edited) value + state.updateCell(rowIndex, 'name', editedValue); + // Dismiss the suggestion + state.dismissInlineAiSuggestion(productIndex, 'name'); }} onDismiss={() => { useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'name'); }} + onRevalidate={handleRevalidate} + isRevalidating={isInlineAiValidating} compact />
diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx index 74d0185..5a88c7a 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx @@ -51,6 +51,8 @@ interface MultilineInputProps { isAiValidating?: boolean; /** Called when user dismisses/clears the AI suggestion (also called after applying) */ onDismissAiSuggestion?: () => void; + /** Called to manually trigger AI re-validation */ + onRevalidate?: () => void; } const MultilineInputComponent = ({ @@ -64,6 +66,7 @@ const MultilineInputComponent = ({ aiSuggestion, isAiValidating, onDismissAiSuggestion, + onRevalidate, }: MultilineInputProps) => { const [popoverOpen, setPopoverOpen] = useState(false); const [editValue, setEditValue] = useState(''); @@ -408,6 +411,8 @@ const MultilineInputComponent = ({ productName={productName} onAccept={handleAcceptSuggestion} onDismiss={handleDismissSuggestion} + onRevalidate={onRevalidate} + isRevalidating={isAiValidating} /> ) : (
diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts index 941dc3b..263ccc4 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts @@ -108,9 +108,9 @@ export function useAutoInlineAiValidation() { typeof row.name === 'string' && row.name.trim(); - // Check description context: company + line + name (description can be empty) - // We want to validate descriptions even when empty so AI can suggest one - const hasDescContext = hasNameContext; + // Check description context: company + line + name + description with ≥10 chars + const descriptionValue = typeof row.description === 'string' ? row.description.trim() : ''; + const hasDescContext = hasNameContext && descriptionValue.length >= 10; // Skip if already auto-validated (shouldn't happen on first run, but be safe) const nameAlreadyValidated = inlineAi.autoValidated.has(`${productIndex}-name`);