Don't validate empty descriptions, other validation enhancements
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface AiDescriptionCompareProps {
|
export interface AiDescriptionCompareProps {
|
||||||
@@ -28,6 +28,10 @@ export interface AiDescriptionCompareProps {
|
|||||||
issues: string[];
|
issues: string[];
|
||||||
onAccept: (editedSuggestion: string) => void;
|
onAccept: (editedSuggestion: string) => void;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
|
/** Called to re-roll (re-run) the AI validation */
|
||||||
|
onRevalidate?: () => void;
|
||||||
|
/** Whether re-validation is in progress */
|
||||||
|
isRevalidating?: boolean;
|
||||||
productName?: string;
|
productName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,8 @@ export function AiDescriptionCompare({
|
|||||||
issues,
|
issues,
|
||||||
onAccept,
|
onAccept,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
onRevalidate,
|
||||||
|
isRevalidating = false,
|
||||||
productName,
|
productName,
|
||||||
className,
|
className,
|
||||||
}: AiDescriptionCompareProps) {
|
}: AiDescriptionCompareProps) {
|
||||||
@@ -226,6 +232,18 @@ export function AiDescriptionCompare({
|
|||||||
>
|
>
|
||||||
Ignore
|
Ignore
|
||||||
</Button>
|
</Button>
|
||||||
|
{onRevalidate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 px-3 text-xs text-purple-500 hover:text-purple-700 dark:text-purple-400"
|
||||||
|
disabled={isRevalidating}
|
||||||
|
onClick={onRevalidate}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-3 w-3 mr-1 ${isRevalidating ? "animate-spin" : ""}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export function ProductEditForm({
|
|||||||
|
|
||||||
const watchCompany = watch("company");
|
const watchCompany = watch("company");
|
||||||
const watchLine = watch("line");
|
const watchLine = watch("line");
|
||||||
|
const watchDescription = watch("description");
|
||||||
|
|
||||||
// Populate form on mount
|
// Populate form on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -452,7 +453,7 @@ export function ProductEditForm({
|
|||||||
|
|
||||||
const handleValidateDescription = useCallback(async () => {
|
const handleValidateDescription = useCallback(async () => {
|
||||||
const values = getValues();
|
const values = getValues();
|
||||||
if (!values.description?.trim()) return;
|
if (!values.description?.trim() || values.description.trim().length < 10) return;
|
||||||
clearDescriptionResult();
|
clearDescriptionResult();
|
||||||
setValidatingField("description");
|
setValidatingField("description");
|
||||||
const companyLabel = fieldOptions.companies.find((c) => c.value === values.company)?.label;
|
const companyLabel = fieldOptions.companies.find((c) => c.value === values.company)?.label;
|
||||||
@@ -550,7 +551,7 @@ export function ProductEditForm({
|
|||||||
<div key={fc.key} className="col-span-full flex flex-col gap-0.5 rounded-md border border-muted-foreground/50 bg-transparent px-3 py-1 text-sm hover:border-input hover:bg-muted/50 transition-colors">
|
<div key={fc.key} className="col-span-full flex flex-col gap-0.5 rounded-md border border-muted-foreground/50 bg-transparent px-3 py-1 text-sm hover:border-input hover:bg-muted/50 transition-colors">
|
||||||
<div className="flex items-center justify-between relative">
|
<div className="flex items-center justify-between relative">
|
||||||
<span className="text-muted-foreground text-xs shrink-0">{fc.label}</span>
|
<span className="text-muted-foreground text-xs shrink-0">{fc.label}</span>
|
||||||
{isDescription && (
|
{isDescription && (watchDescription?.trim().length ?? 0) >= 10 && (
|
||||||
descriptionResult?.suggestion ? (
|
descriptionResult?.suggestion ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -632,11 +633,13 @@ export function ProductEditForm({
|
|||||||
<AiSuggestionBadge
|
<AiSuggestionBadge
|
||||||
suggestion={nameResult.suggestion}
|
suggestion={nameResult.suggestion}
|
||||||
issues={nameResult.issues}
|
issues={nameResult.issues}
|
||||||
onAccept={() => {
|
onAccept={(editedValue) => {
|
||||||
setValue("name", nameResult.suggestion!, { shouldDirty: true });
|
setValue("name", editedValue, { shouldDirty: true });
|
||||||
clearNameResult();
|
clearNameResult();
|
||||||
}}
|
}}
|
||||||
onDismiss={clearNameResult}
|
onDismiss={clearNameResult}
|
||||||
|
onRevalidate={handleValidateName}
|
||||||
|
isRevalidating={validatingField === "name"}
|
||||||
compact
|
compact
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
@@ -762,6 +765,8 @@ export function ProductEditForm({
|
|||||||
clearDescriptionResult();
|
clearDescriptionResult();
|
||||||
setDescDialogOpen(false);
|
setDescDialogOpen(false);
|
||||||
}}
|
}}
|
||||||
|
onRevalidate={handleValidateDescription}
|
||||||
|
isRevalidating={validatingField === "description"}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
* Used for inline validation suggestions on Name and Description fields.
|
* Used for inline validation suggestions on Name and Description fields.
|
||||||
*
|
*
|
||||||
* For description fields, starts collapsed (just icon + count) and expands on click.
|
* For description fields, starts collapsed (just icon + count) and expands on click.
|
||||||
* For name fields, uses compact inline mode.
|
* For name fields, uses compact inline mode with an editable suggestion.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Check, X, Sparkles, AlertCircle, ChevronDown, ChevronUp, Info } from 'lucide-react';
|
import { Check, X, Sparkles, AlertCircle, ChevronDown, ChevronUp, Info, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -24,10 +24,14 @@ interface AiSuggestionBadgeProps {
|
|||||||
suggestion: string;
|
suggestion: string;
|
||||||
/** List of issues found (optional) */
|
/** List of issues found (optional) */
|
||||||
issues?: string[];
|
issues?: string[];
|
||||||
/** Called when user accepts the suggestion */
|
/** Called when user accepts the suggestion (receives the possibly-edited value) */
|
||||||
onAccept: () => void;
|
onAccept: (editedValue: string) => void;
|
||||||
/** Called when user dismisses the suggestion */
|
/** Called when user dismisses the suggestion */
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
|
/** Called to refresh (re-run) the AI validation */
|
||||||
|
onRevalidate?: () => void;
|
||||||
|
/** Whether re-validation is in progress */
|
||||||
|
isRevalidating?: boolean;
|
||||||
/** Additional CSS classes */
|
/** Additional CSS classes */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Whether to show the suggestion as compact (inline) - used for name field */
|
/** Whether to show the suggestion as compact (inline) - used for name field */
|
||||||
@@ -41,13 +45,21 @@ export function AiSuggestionBadge({
|
|||||||
issues = [],
|
issues = [],
|
||||||
onAccept,
|
onAccept,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
onRevalidate,
|
||||||
|
isRevalidating = false,
|
||||||
className,
|
className,
|
||||||
compact = false,
|
compact = false,
|
||||||
collapsible = false
|
collapsible = false
|
||||||
}: AiSuggestionBadgeProps) {
|
}: AiSuggestionBadgeProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [editedValue, setEditedValue] = useState(suggestion);
|
||||||
|
|
||||||
// Compact mode for name fields - inline suggestion with accept/dismiss
|
// Reset edited value when suggestion changes (e.g. after refresh)
|
||||||
|
useEffect(() => {
|
||||||
|
setEditedValue(suggestion);
|
||||||
|
}, [suggestion]);
|
||||||
|
|
||||||
|
// Compact mode for name fields - inline editable suggestion with accept/dismiss
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -58,24 +70,27 @@ export function AiSuggestionBadge({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-1.5">
|
<div className="flex items-start gap-1.5 flex-1 min-w-0">
|
||||||
<Sparkles className="h-3 w-3 text-purple-500 flex-shrink-0 mt-0.5" />
|
<Sparkles className="h-3 w-3 text-purple-500 flex-shrink-0 mt-0.5" />
|
||||||
|
|
||||||
<span className="text-purple-700 dark:text-purple-300">
|
<input
|
||||||
{suggestion}
|
type="text"
|
||||||
</span>
|
value={editedValue}
|
||||||
|
onChange={(e) => setEditedValue(e.target.value)}
|
||||||
|
className="flex-1 min-w-0 bg-transparent text-purple-700 dark:text-purple-300 text-xs outline-none border-b border-transparent focus:border-purple-300 dark:focus:border-purple-600 transition-colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
<div className="flex items-center gap-[0px] flex-shrink-0">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={200}>
|
<Tooltip delayDuration={200}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-5 w-5 p-0 text-green-600 hover:text-green-700 hover:bg-green-100"
|
className="h-4 w-4 p-0 [&_svg]:size-3.5 text-green-600 hover:text-green-700 hover:bg-green-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAccept();
|
onAccept(editedValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check className="h-3 w-3" />
|
<Check className="h-3 w-3" />
|
||||||
@@ -92,7 +107,7 @@ export function AiSuggestionBadge({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-5 w-5 p-0 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
|
className="h-4 w-4 p-0 [&_svg]:size-3.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDismiss();
|
onDismiss();
|
||||||
@@ -106,19 +121,46 @@ export function AiSuggestionBadge({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
{/* Refresh button */}
|
||||||
|
{onRevalidate && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={200}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-4 w-4 p-0 mr-[1px] [&_svg]:size-3 text-purple-400 hover:text-purple-600 hover:bg-purple-100"
|
||||||
|
disabled={isRevalidating}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRevalidate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn(isRevalidating && "animate-spin")} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
<p>Refresh suggestion</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
{/* Info icon with issues tooltip */}
|
{/* Info icon with issues tooltip */}
|
||||||
{issues.length > 0 && (
|
{issues.length > 0 && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={200}>
|
<Tooltip delayDuration={200}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
className="flex-shrink-0 text-purple-400 hover:text-purple-600 transition-colors"
|
size="sm"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 p-0 [&_svg]:size-3.5 text-purple-400 hover:text-purple-600 hover:bg-purple-100"
|
||||||
>
|
>
|
||||||
<Info className="h-3.5 w-3.5" />
|
<Info className="h-3.5 w-3.5" />
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
@@ -246,7 +288,7 @@ export function AiSuggestionBadge({
|
|||||||
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
|
className="h-7 px-3 text-xs bg-white border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 dark:bg-green-950/30 dark:border-green-700 dark:text-green-400"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAccept();
|
onAccept(suggestion);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check className="h-3 w-3 mr-1" />
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
|||||||
@@ -588,9 +588,9 @@ const CellWrapper = memo(({
|
|||||||
// Check if description should be validated
|
// Check if description should be validated
|
||||||
const descIsDismissed = nameSuggestion?.dismissed?.description;
|
const descIsDismissed = nameSuggestion?.dismissed?.description;
|
||||||
const descIsValidating = inlineAi.validating.has(`${contextProductIndex}-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
|
// Trigger description validation
|
||||||
setInlineAiValidating(`${contextProductIndex}-description`, true);
|
setInlineAiValidating(`${contextProductIndex}-description`, true);
|
||||||
|
|
||||||
@@ -687,7 +687,9 @@ const CellWrapper = memo(({
|
|||||||
// Trigger inline AI validation for name/description fields
|
// Trigger inline AI validation for name/description fields
|
||||||
// This validates spelling, grammar, and naming conventions using Groq
|
// This validates spelling, grammar, and naming conventions using Groq
|
||||||
// Only trigger if value actually changed to avoid unnecessary API calls
|
// 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 currentRow = useValidationStore.getState().rows[rowIndex];
|
||||||
const fields = useValidationStore.getState().fields;
|
const fields = useValidationStore.getState().fields;
|
||||||
if (currentRow) {
|
if (currentRow) {
|
||||||
@@ -751,6 +753,66 @@ const CellWrapper = memo(({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [rowIndex, field.key, isEmbeddingField, aiSuggestions, isInlineAiField, productIndex]);
|
}, [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)
|
// Stable callback for fetching options (for line/subline dropdowns)
|
||||||
const handleFetchOptions = useCallback(async () => {
|
const handleFetchOptions = useCallback(async () => {
|
||||||
const state = useValidationStore.getState();
|
const state = useValidationStore.getState();
|
||||||
@@ -854,6 +916,7 @@ const CellWrapper = memo(({
|
|||||||
onDismissAiSuggestion: () => {
|
onDismissAiSuggestion: () => {
|
||||||
useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'description');
|
useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'description');
|
||||||
},
|
},
|
||||||
|
onRevalidate: handleRevalidate,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -925,12 +988,18 @@ const CellWrapper = memo(({
|
|||||||
<AiSuggestionBadge
|
<AiSuggestionBadge
|
||||||
suggestion={fieldSuggestion.suggestion!}
|
suggestion={fieldSuggestion.suggestion!}
|
||||||
issues={fieldSuggestion.issues}
|
issues={fieldSuggestion.issues}
|
||||||
onAccept={() => {
|
onAccept={(editedValue) => {
|
||||||
useValidationStore.getState().acceptInlineAiSuggestion(productIndex, 'name');
|
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={() => {
|
onDismiss={() => {
|
||||||
useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'name');
|
useValidationStore.getState().dismissInlineAiSuggestion(productIndex, 'name');
|
||||||
}}
|
}}
|
||||||
|
onRevalidate={handleRevalidate}
|
||||||
|
isRevalidating={isInlineAiValidating}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ interface MultilineInputProps {
|
|||||||
isAiValidating?: boolean;
|
isAiValidating?: boolean;
|
||||||
/** Called when user dismisses/clears the AI suggestion (also called after applying) */
|
/** Called when user dismisses/clears the AI suggestion (also called after applying) */
|
||||||
onDismissAiSuggestion?: () => void;
|
onDismissAiSuggestion?: () => void;
|
||||||
|
/** Called to manually trigger AI re-validation */
|
||||||
|
onRevalidate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultilineInputComponent = ({
|
const MultilineInputComponent = ({
|
||||||
@@ -64,6 +66,7 @@ const MultilineInputComponent = ({
|
|||||||
aiSuggestion,
|
aiSuggestion,
|
||||||
isAiValidating,
|
isAiValidating,
|
||||||
onDismissAiSuggestion,
|
onDismissAiSuggestion,
|
||||||
|
onRevalidate,
|
||||||
}: MultilineInputProps) => {
|
}: MultilineInputProps) => {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const [editValue, setEditValue] = useState('');
|
const [editValue, setEditValue] = useState('');
|
||||||
@@ -408,6 +411,8 @@ const MultilineInputComponent = ({
|
|||||||
productName={productName}
|
productName={productName}
|
||||||
onAccept={handleAcceptSuggestion}
|
onAccept={handleAcceptSuggestion}
|
||||||
onDismiss={handleDismissSuggestion}
|
onDismiss={handleDismissSuggestion}
|
||||||
|
onRevalidate={onRevalidate}
|
||||||
|
isRevalidating={isAiValidating}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div data-col="left" className="flex flex-col min-h-0 w-full">
|
<div data-col="left" className="flex flex-col min-h-0 w-full">
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ export function useAutoInlineAiValidation() {
|
|||||||
typeof row.name === 'string' &&
|
typeof row.name === 'string' &&
|
||||||
row.name.trim();
|
row.name.trim();
|
||||||
|
|
||||||
// Check description context: company + line + name (description can be empty)
|
// Check description context: company + line + name + description with ≥10 chars
|
||||||
// We want to validate descriptions even when empty so AI can suggest one
|
const descriptionValue = typeof row.description === 'string' ? row.description.trim() : '';
|
||||||
const hasDescContext = hasNameContext;
|
const hasDescContext = hasNameContext && descriptionValue.length >= 10;
|
||||||
|
|
||||||
// Skip if already auto-validated (shouldn't happen on first run, but be safe)
|
// Skip if already auto-validated (shouldn't happen on first run, but be safe)
|
||||||
const nameAlreadyValidated = inlineAi.autoValidated.has(`${productIndex}-name`);
|
const nameAlreadyValidated = inlineAi.autoValidated.has(`${productIndex}-name`);
|
||||||
|
|||||||
Reference in New Issue
Block a user