Lots of AI related tweaks/fixes
This commit is contained in:
@@ -80,7 +80,6 @@ function buildDescriptionUserPrompt(product, prompts) {
|
||||
parts.push('CRITICAL RULES:');
|
||||
parts.push('- If isValid is false, you MUST provide a suggestion with the improved description');
|
||||
parts.push('- If there are ANY issues, isValid MUST be false and suggestion MUST contain the corrected text');
|
||||
parts.push('- If the description is empty or very short, write a complete description based on the product name');
|
||||
parts.push('- Only set isValid to true if there are ZERO issues and the description needs no changes');
|
||||
parts.push('');
|
||||
parts.push('RESPOND WITH JSON:');
|
||||
|
||||
@@ -41,7 +41,6 @@ function sanitizeIssue(issue) {
|
||||
* @param {string} [product.company_name] - Company name
|
||||
* @param {string} [product.line_name] - Product line name
|
||||
* @param {string} [product.subline_name] - Product subline name
|
||||
* @param {string} [product.description] - Product description (for context)
|
||||
* @param {string[]} [product.siblingNames] - Names of other products in the same line
|
||||
* @param {Object} prompts - Prompts loaded from database
|
||||
* @param {string} prompts.general - General naming conventions
|
||||
@@ -73,10 +72,6 @@ function buildNameUserPrompt(product, prompts) {
|
||||
parts.push(`SUBLINE: ${product.subline_name}`);
|
||||
}
|
||||
|
||||
if (product.description) {
|
||||
parts.push(`DESCRIPTION (for context): ${product.description.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
// Add sibling context for naming decisions
|
||||
if (product.siblingNames && product.siblingNames.length > 0) {
|
||||
parts.push('');
|
||||
@@ -84,15 +79,6 @@ function buildNameUserPrompt(product, prompts) {
|
||||
product.siblingNames.forEach(name => {
|
||||
parts.push(`- ${name}`);
|
||||
});
|
||||
parts.push('');
|
||||
parts.push('Use this context to determine:');
|
||||
parts.push('- If this product needs a differentiator (multiple similar products exist)');
|
||||
parts.push('- If naming is consistent with sibling products');
|
||||
parts.push('- Which naming pattern is appropriate (single vs multiple products in line)');
|
||||
} else if (product.line_name) {
|
||||
parts.push('');
|
||||
parts.push('This appears to be the ONLY product in this line (no siblings in current batch).');
|
||||
parts.push('Use the single-product naming pattern: [Line Name] [Product Name] - [Company]');
|
||||
}
|
||||
|
||||
// Add response format instructions
|
||||
|
||||
@@ -59,7 +59,7 @@ function buildSanityCheckUserPrompt(products, prompts) {
|
||||
suggestion: 'Suggested fix or verification (optional)'
|
||||
}
|
||||
],
|
||||
summary: 'Brief overall assessment of the batch quality'
|
||||
summary: '1-2 sentences summarizing the batch quality'
|
||||
}, null, 2));
|
||||
|
||||
parts.push('');
|
||||
|
||||
@@ -101,9 +101,9 @@ function createNameValidationTask() {
|
||||
{ role: 'system', content: prompts.system },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
model: MODELS.SMALL, // openai/gpt-oss-20b - reasoning model
|
||||
model: MODELS.LARGE, // openai/gpt-oss-120b - reasoning model
|
||||
temperature: 0.2, // Low temperature for consistent results
|
||||
maxTokens: 1500, // Reasoning models need extra tokens for thinking
|
||||
maxTokens: 3000, // Reasoning models need extra tokens for thinking
|
||||
responseFormat: { type: 'json_object' }
|
||||
});
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export const BASE_IMPORT_FIELDS = [
|
||||
label: "Case Pack",
|
||||
key: "case_qty",
|
||||
description: "Number of units per case",
|
||||
alternateMatches: ["mc qty","case qty","case pack","box ct"],
|
||||
alternateMatches: ["mc qty","case qty","case pack","box ct","master"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
validations: [
|
||||
@@ -250,11 +250,11 @@ export const BASE_IMPORT_FIELDS = [
|
||||
width: 190,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
{
|
||||
{
|
||||
label: "COO",
|
||||
key: "coo",
|
||||
description: "2-letter country code (ISO)",
|
||||
alternateMatches: ["coo", "country of origin"],
|
||||
alternateMatches: ["coo", "country of origin", "origin"],
|
||||
fieldType: { type: "input" },
|
||||
width: 70,
|
||||
validations: [
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
import { ErrorSource, ErrorType, type RowData, type ValidationError } from '../store/types';
|
||||
import type { Field, SelectOption, Validation } from '../../../types';
|
||||
import { correctUpcValue } from '../utils/upcUtils';
|
||||
import {
|
||||
buildNameValidationPayload,
|
||||
buildDescriptionValidationPayload,
|
||||
} from '../utils/inlineAiPayload';
|
||||
|
||||
// Copy-down banner component
|
||||
import { CopyDownBanner } from './CopyDownBanner';
|
||||
@@ -541,19 +545,9 @@ const CellWrapper = memo(({
|
||||
// (line was just set, check if company + name exist)
|
||||
const currentRowForContext = useValidationStore.getState().rows[rowIndex];
|
||||
if (currentRowForContext?.company && currentRowForContext?.name) {
|
||||
const { setInlineAiValidating, setInlineAiSuggestion, inlineAi, fields } = useValidationStore.getState();
|
||||
const { setInlineAiValidating, setInlineAiSuggestion, inlineAi, fields, rows } = useValidationStore.getState();
|
||||
const contextProductIndex = currentRowForContext.__index;
|
||||
|
||||
// Helper to look up field option label
|
||||
const getFieldLabel = (fieldKey: string, val: unknown): string | undefined => {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Check if name should be validated
|
||||
const nameSuggestion = inlineAi.suggestions.get(contextProductIndex);
|
||||
const nameIsDismissed = nameSuggestion?.dismissed?.name;
|
||||
@@ -561,36 +555,17 @@ const CellWrapper = memo(({
|
||||
const nameValue = String(currentRowForContext.name).trim();
|
||||
|
||||
if (nameValue && !nameIsDismissed && !nameIsValidating) {
|
||||
// Trigger name validation
|
||||
// Trigger name validation with line override (use new line value)
|
||||
setInlineAiValidating(`${contextProductIndex}-name`, true);
|
||||
|
||||
const rows = useValidationStore.getState().rows;
|
||||
const siblingNames: string[] = [];
|
||||
const companyId = String(currentRowForContext.company);
|
||||
const lineId = String(valueToSave); // Use the new line value
|
||||
for (const row of rows) {
|
||||
if (row.__index === contextProductIndex) continue;
|
||||
if (String(row.company) !== companyId) continue;
|
||||
if (String(row.line) !== lineId) continue;
|
||||
if (row.name && typeof row.name === 'string' && row.name.trim()) {
|
||||
siblingNames.push(row.name);
|
||||
}
|
||||
}
|
||||
const payload = buildNameValidationPayload(currentRowForContext, fields, rows, {
|
||||
line: valueToSave as string | number,
|
||||
});
|
||||
|
||||
fetch('/api/ai/validate/inline/name', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
product: {
|
||||
name: nameValue,
|
||||
description: currentRowForContext.description as string,
|
||||
company_name: getFieldLabel('company', currentRowForContext.company),
|
||||
company_id: String(currentRowForContext.company),
|
||||
line_name: getFieldLabel('line', valueToSave),
|
||||
line_id: String(valueToSave),
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify({ product: payload }),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
@@ -616,17 +591,12 @@ const CellWrapper = memo(({
|
||||
// Trigger description validation
|
||||
setInlineAiValidating(`${contextProductIndex}-description`, true);
|
||||
|
||||
const payload = buildDescriptionValidationPayload(currentRowForContext, fields);
|
||||
|
||||
fetch('/api/ai/validate/inline/description', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
product: {
|
||||
name: nameValue,
|
||||
description: descValue,
|
||||
company_name: getFieldLabel('company', currentRowForContext.company),
|
||||
company_id: String(currentRowForContext.company),
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify({ product: payload }),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
@@ -740,56 +710,11 @@ const CellWrapper = memo(({
|
||||
setInlineAiValidating(validationKey, true);
|
||||
markInlineAiAutoValidated(productIndex, fieldKey);
|
||||
|
||||
// Helper to look up field option label
|
||||
const getFieldLabel = (fieldKey: string, val: unknown): string | undefined => {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Compute sibling products (same company + line + subline if set) for naming context
|
||||
// Build payload using centralized utility
|
||||
const rows = useValidationStore.getState().rows;
|
||||
const siblingNames: string[] = [];
|
||||
|
||||
if (currentRow.company && currentRow.line) {
|
||||
const companyId = String(currentRow.company);
|
||||
const lineId = String(currentRow.line);
|
||||
const sublineId = currentRow.subline ? String(currentRow.subline) : null;
|
||||
|
||||
for (const row of rows) {
|
||||
// Skip self
|
||||
if (row.__index === productIndex) continue;
|
||||
|
||||
// Must match company and line
|
||||
if (String(row.company) !== companyId) continue;
|
||||
if (String(row.line) !== lineId) continue;
|
||||
|
||||
// If current product has subline, siblings must match subline too
|
||||
if (sublineId && String(row.subline) !== sublineId) continue;
|
||||
|
||||
// Add name if it exists
|
||||
if (row.name && typeof row.name === 'string' && row.name.trim()) {
|
||||
siblingNames.push(row.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build product payload for API
|
||||
const productPayload = {
|
||||
name: fieldKey === 'name' ? String(valueToSave) : (currentRow.name as string),
|
||||
description: fieldKey === 'description' ? String(valueToSave) : (currentRow.description as string),
|
||||
company_name: currentRow.company ? getFieldLabel('company', currentRow.company) : undefined,
|
||||
company_id: currentRow.company ? String(currentRow.company) : undefined,
|
||||
line_name: currentRow.line ? getFieldLabel('line', currentRow.line) : undefined,
|
||||
line_id: currentRow.line ? String(currentRow.line) : undefined,
|
||||
subline_name: currentRow.subline ? getFieldLabel('subline', currentRow.subline) : undefined,
|
||||
subline_id: currentRow.subline ? String(currentRow.subline) : undefined,
|
||||
categories: currentRow.categories as string | undefined,
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
};
|
||||
const productPayload = fieldKey === 'name'
|
||||
? buildNameValidationPayload(currentRow, fields, rows, { name: String(valueToSave) })
|
||||
: buildDescriptionValidationPayload(currentRow, fields, { description: String(valueToSave) });
|
||||
|
||||
// Call the appropriate API endpoint
|
||||
const endpoint = fieldKey === 'name'
|
||||
@@ -1120,61 +1045,21 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
|
||||
// Trigger inline AI validation for name/description if template set those fields
|
||||
const productIndex = currentRow?.__index;
|
||||
if (productIndex) {
|
||||
const { setInlineAiValidating, setInlineAiSuggestion } = state;
|
||||
|
||||
// Helper to look up field option label
|
||||
const getFieldLabel = (fieldKey: string, val: unknown): string | undefined => {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const { setInlineAiValidating, setInlineAiSuggestion, rows } = state;
|
||||
|
||||
// Get the updated row data (after template applied)
|
||||
const updatedRow = { ...currentRow, ...updates };
|
||||
|
||||
// Compute sibling names for context
|
||||
const rows = state.rows;
|
||||
const siblingNames: string[] = [];
|
||||
if (updatedRow.company && updatedRow.line) {
|
||||
const companyId = String(updatedRow.company);
|
||||
const lineId = String(updatedRow.line);
|
||||
const sublineId = updatedRow.subline ? String(updatedRow.subline) : null;
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.__index === productIndex) continue;
|
||||
if (String(row.company) !== companyId) continue;
|
||||
if (String(row.line) !== lineId) continue;
|
||||
if (sublineId && String(row.subline) !== sublineId) continue;
|
||||
if (row.name && typeof row.name === 'string' && row.name.trim()) {
|
||||
siblingNames.push(row.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
const updatedRow = { ...currentRow, ...updates } as RowData;
|
||||
|
||||
// Trigger name validation if template set name
|
||||
if (templateFieldsSet.has('name') && updates.name && String(updates.name).trim()) {
|
||||
setInlineAiValidating(`${productIndex}-name`, true);
|
||||
|
||||
const productPayload = {
|
||||
name: String(updates.name),
|
||||
description: updatedRow.description as string,
|
||||
company_name: updatedRow.company ? getFieldLabel('company', updatedRow.company) : undefined,
|
||||
company_id: updatedRow.company ? String(updatedRow.company) : undefined,
|
||||
line_name: updatedRow.line ? getFieldLabel('line', updatedRow.line) : undefined,
|
||||
line_id: updatedRow.line ? String(updatedRow.line) : undefined,
|
||||
subline_name: updatedRow.subline ? getFieldLabel('subline', updatedRow.subline) : undefined,
|
||||
subline_id: updatedRow.subline ? String(updatedRow.subline) : undefined,
|
||||
categories: updatedRow.categories as string | undefined,
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
};
|
||||
const payload = buildNameValidationPayload(updatedRow, fields, rows);
|
||||
|
||||
fetch('/api/ai/validate/inline/name', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ product: productPayload }),
|
||||
body: JSON.stringify({ product: payload }),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
@@ -1195,18 +1080,12 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
|
||||
if (templateFieldsSet.has('description') && updates.description && String(updates.description).trim()) {
|
||||
setInlineAiValidating(`${productIndex}-description`, true);
|
||||
|
||||
const productPayload = {
|
||||
name: updatedRow.name as string,
|
||||
description: String(updates.description),
|
||||
company_name: updatedRow.company ? getFieldLabel('company', updatedRow.company) : undefined,
|
||||
company_id: updatedRow.company ? String(updatedRow.company) : undefined,
|
||||
categories: updatedRow.categories as string | undefined,
|
||||
};
|
||||
const payload = buildDescriptionValidationPayload(updatedRow, fields);
|
||||
|
||||
fetch('/api/ai/validate/inline/description', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ product: productPayload }),
|
||||
body: JSON.stringify({ product: payload }),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
} from '@/components/ui/popover';
|
||||
import type { Field, SelectOption } from '../../../../types';
|
||||
import type { ValidationError } from '../../store/types';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
|
||||
/** Time window (ms) during which this cell should not open after a popover closes */
|
||||
const POPOVER_CLOSE_DELAY = 150;
|
||||
|
||||
interface ComboboxCellProps {
|
||||
value: unknown;
|
||||
@@ -56,6 +60,9 @@ const ComboboxCellComponent = ({
|
||||
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
||||
const hasFetchedRef = useRef(false);
|
||||
|
||||
// Get store state for coordinating with popover close behavior
|
||||
const cellPopoverClosedAt = useValidationStore((s) => s.cellPopoverClosedAt);
|
||||
|
||||
const stringValue = String(value ?? '');
|
||||
const hasError = errors.length > 0;
|
||||
const errorMessage = errors[0]?.message;
|
||||
@@ -67,6 +74,10 @@ const ComboboxCellComponent = ({
|
||||
// Handle popover open - trigger fetch if needed
|
||||
const handleOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
// Block opening if a popover was just closed (click-outside behavior)
|
||||
if (isOpen && Date.now() - cellPopoverClosedAt < POPOVER_CLOSE_DELAY) {
|
||||
return;
|
||||
}
|
||||
setOpen(isOpen);
|
||||
if (isOpen && onFetchOptions && options.length === 0 && !hasFetchedRef.current) {
|
||||
hasFetchedRef.current = true;
|
||||
@@ -76,7 +87,7 @@ const ComboboxCellComponent = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[onFetchOptions, options.length]
|
||||
[onFetchOptions, options.length, cellPopoverClosedAt]
|
||||
);
|
||||
|
||||
// Handle selection
|
||||
|
||||
@@ -19,6 +19,10 @@ import { cn } from '@/lib/utils';
|
||||
import type { Field, SelectOption } from '../../../../types';
|
||||
import type { ValidationError } from '../../store/types';
|
||||
import { ErrorType } from '../../store/types';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
|
||||
/** Time window (ms) during which this cell should not focus after a popover closes */
|
||||
const POPOVER_CLOSE_DELAY = 150;
|
||||
|
||||
interface InputCellProps {
|
||||
value: unknown;
|
||||
@@ -43,6 +47,9 @@ const InputCellComponent = ({
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Get store state for coordinating with popover close behavior
|
||||
const cellPopoverClosedAt = useValidationStore((s) => s.cellPopoverClosedAt);
|
||||
|
||||
// Sync local value with prop value when not focused
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
@@ -70,8 +77,13 @@ const InputCellComponent = ({
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
// Block focus if a popover was just closed (click-outside behavior)
|
||||
if (Date.now() - cellPopoverClosedAt < POPOVER_CLOSE_DELAY) {
|
||||
inputRef.current?.blur();
|
||||
return;
|
||||
}
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
}, [cellPopoverClosedAt]);
|
||||
|
||||
// Update store only on blur - this is when validation runs too
|
||||
// Round price fields to 2 decimal places
|
||||
|
||||
@@ -41,6 +41,10 @@ import type { Field, SelectOption } from '../../../../types';
|
||||
import type { ValidationError, TaxonomySuggestion } from '../../store/types';
|
||||
import { ErrorType } from '../../store/types';
|
||||
import { useCellSuggestions } from '../../contexts/AiSuggestionsContext';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
|
||||
/** Time window (ms) during which this cell should not open after a popover closes */
|
||||
const POPOVER_CLOSE_DELAY = 150;
|
||||
|
||||
// Extended option type to include hex color values
|
||||
interface MultiSelectOption extends SelectOption {
|
||||
@@ -98,6 +102,18 @@ const MultiSelectCellComponent = ({
|
||||
}: MultiSelectCellProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Get store state for coordinating with popover close behavior
|
||||
const cellPopoverClosedAt = useValidationStore((s) => s.cellPopoverClosedAt);
|
||||
|
||||
// Handle popover open/close with check for recent popover close
|
||||
const handleOpenChange = useCallback((isOpen: boolean) => {
|
||||
// Block opening if a popover was just closed (click-outside behavior)
|
||||
if (isOpen && Date.now() - cellPopoverClosedAt < POPOVER_CLOSE_DELAY) {
|
||||
return;
|
||||
}
|
||||
setOpen(isOpen);
|
||||
}, [cellPopoverClosedAt]);
|
||||
|
||||
// Get AI suggestions for categories, themes, and colors
|
||||
const supportsSuggestions = SUGGESTION_FIELDS.includes(field.key as SuggestionField);
|
||||
const suggestions = useCellSuggestions(productIndex || '');
|
||||
@@ -177,7 +193,7 @@ const MultiSelectCellComponent = ({
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -20,6 +20,10 @@ import { X, Loader2, Sparkles, AlertCircle, Check, ChevronDown, ChevronUp } from
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { Field, SelectOption } from '../../../../types';
|
||||
import type { ValidationError } from '../../store/types';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
|
||||
/** Time window (ms) during which other cells should not open after a popover closes */
|
||||
const POPOVER_CLOSE_DELAY = 150;
|
||||
|
||||
/** AI suggestion data for a single field */
|
||||
interface AiFieldSuggestion {
|
||||
@@ -66,6 +70,12 @@ const MultilineInputComponent = ({
|
||||
const [editedSuggestion, setEditedSuggestion] = useState('');
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const preventReopenRef = useRef(false);
|
||||
// Tracks intentional closes (close button, accept/dismiss) vs click-outside closes
|
||||
const intentionalCloseRef = useRef(false);
|
||||
|
||||
// Get store state and actions for coordinating popover close behavior across cells
|
||||
const cellPopoverClosedAt = useValidationStore((s) => s.cellPopoverClosedAt);
|
||||
const setCellPopoverClosed = useValidationStore((s) => s.setCellPopoverClosed);
|
||||
|
||||
const hasError = errors.length > 0;
|
||||
const errorMessage = errors[0]?.message;
|
||||
@@ -102,6 +112,11 @@ const MultilineInputComponent = ({
|
||||
}
|
||||
}, [aiSuggestion?.suggestion]);
|
||||
|
||||
// Check if another cell's popover was recently closed (prevents immediate focus on click-outside)
|
||||
const wasPopoverRecentlyClosed = useCallback(() => {
|
||||
return Date.now() - cellPopoverClosedAt < POPOVER_CLOSE_DELAY;
|
||||
}, [cellPopoverClosedAt]);
|
||||
|
||||
// Handle trigger click to toggle the popover
|
||||
const handleTriggerClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -112,6 +127,13 @@ const MultilineInputComponent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Block opening if another popover was just closed
|
||||
if (wasPopoverRecentlyClosed()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process if not already open
|
||||
if (!popoverOpen) {
|
||||
setPopoverOpen(true);
|
||||
@@ -119,10 +141,10 @@ const MultilineInputComponent = ({
|
||||
setEditValue(localDisplayValue || String(value ?? ''));
|
||||
}
|
||||
},
|
||||
[popoverOpen, value, localDisplayValue]
|
||||
[popoverOpen, value, localDisplayValue, wasPopoverRecentlyClosed]
|
||||
);
|
||||
|
||||
// Handle immediate close of popover
|
||||
// Handle immediate close of popover (used by close button and actions - intentional closes)
|
||||
const handleClosePopover = useCallback(() => {
|
||||
// Only process if we have changes
|
||||
if (editValue !== value || editValue !== localDisplayValue) {
|
||||
@@ -134,28 +156,60 @@ const MultilineInputComponent = ({
|
||||
onBlur(editValue);
|
||||
}
|
||||
|
||||
// Mark this as an intentional close (not click-outside)
|
||||
intentionalCloseRef.current = true;
|
||||
|
||||
// Immediately close popover
|
||||
setPopoverOpen(false);
|
||||
setAiSuggestionExpanded(false);
|
||||
|
||||
// Prevent reopening
|
||||
// Prevent reopening this same cell
|
||||
preventReopenRef.current = true;
|
||||
setTimeout(() => {
|
||||
preventReopenRef.current = false;
|
||||
}, 100);
|
||||
}, [editValue, value, localDisplayValue, onChange, onBlur]);
|
||||
|
||||
// Handle popover open/close
|
||||
// Handle popover open/close (called by Radix for click-outside and escape key)
|
||||
const handlePopoverOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open && popoverOpen) {
|
||||
handleClosePopover();
|
||||
// Check if this was an intentional close (via close button or actions)
|
||||
const wasIntentional = intentionalCloseRef.current;
|
||||
intentionalCloseRef.current = false; // Reset for next time
|
||||
|
||||
if (wasIntentional) {
|
||||
// Intentional close already handled by handleClosePopover
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a click-outside close - save changes and signal other cells
|
||||
if (editValue !== value || editValue !== localDisplayValue) {
|
||||
setLocalDisplayValue(editValue);
|
||||
onChange(editValue);
|
||||
onBlur(editValue);
|
||||
}
|
||||
|
||||
setPopoverOpen(false);
|
||||
setAiSuggestionExpanded(false);
|
||||
|
||||
// Signal to other cells that a popover just closed via click-outside
|
||||
setCellPopoverClosed();
|
||||
|
||||
preventReopenRef.current = true;
|
||||
setTimeout(() => {
|
||||
preventReopenRef.current = false;
|
||||
}, 100);
|
||||
} else if (open && !popoverOpen) {
|
||||
// Block opening if another popover was just closed
|
||||
if (wasPopoverRecentlyClosed()) {
|
||||
return;
|
||||
}
|
||||
setEditValue(localDisplayValue || String(value ?? ''));
|
||||
setPopoverOpen(true);
|
||||
}
|
||||
},
|
||||
[value, popoverOpen, handleClosePopover, localDisplayValue]
|
||||
[value, popoverOpen, localDisplayValue, wasPopoverRecentlyClosed, editValue, onChange, onBlur, setCellPopoverClosed]
|
||||
);
|
||||
|
||||
// Handle direct input change
|
||||
@@ -212,6 +266,10 @@ const MultilineInputComponent = ({
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Block opening if another popover was just closed
|
||||
if (wasPopoverRecentlyClosed()) {
|
||||
return;
|
||||
}
|
||||
setAiSuggestionExpanded(true);
|
||||
setPopoverOpen(true);
|
||||
setEditValue(localDisplayValue || String(value ?? ''));
|
||||
@@ -267,7 +325,7 @@ const MultilineInputComponent = ({
|
||||
value={editValue}
|
||||
onChange={handleChange}
|
||||
onWheel={handleTextareaWheel}
|
||||
className="min-h-[120px] max-h-[200px] overflow-y-auto overscroll-contain border-none focus-visible:ring-0 rounded-t-md rounded-b-none pl-2 pr-4 py-1 resize-none"
|
||||
className="min-h-[120px] max-h-[200px] overflow-y-auto overscroll-contain border-none focus-visible:ring-0 rounded-t-md rounded-b-none pl-2 pr-4 py-1 resize-y"
|
||||
placeholder={`Enter ${field.label || 'text'}...`}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -324,7 +382,7 @@ const MultilineInputComponent = ({
|
||||
value={editedSuggestion}
|
||||
onChange={(e) => setEditedSuggestion(e.target.value)}
|
||||
onWheel={handleTextareaWheel}
|
||||
className="min-h-[80px] max-h-[150px] overflow-y-auto overscroll-contain text-sm bg-white dark:bg-black/20 border-purple-200 dark:border-purple-700 focus-visible:ring-purple-400 resize-none"
|
||||
className="min-h-[120px] max-h-[200px] overflow-y-auto overscroll-contain text-sm bg-white dark:bg-black/20 border-purple-200 dark:border-purple-700 focus-visible:ring-purple-400 resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ import { cn } from '@/lib/utils';
|
||||
import type { Field, SelectOption } from '../../../../types';
|
||||
import type { ValidationError } from '../../store/types';
|
||||
import { ErrorType } from '../../store/types';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
|
||||
/** Time window (ms) during which this cell should not open after a popover closes */
|
||||
const POPOVER_CLOSE_DELAY = 150;
|
||||
|
||||
interface SelectCellProps {
|
||||
value: unknown;
|
||||
@@ -62,6 +66,9 @@ const SelectCellComponent = ({
|
||||
const [isFetchingOptions, setIsFetchingOptions] = useState(false);
|
||||
const hasFetchedRef = useRef(false);
|
||||
|
||||
// Get store state for coordinating with popover close behavior
|
||||
const cellPopoverClosedAt = useValidationStore((s) => s.cellPopoverClosedAt);
|
||||
|
||||
// Combined loading state - either internal fetch or external loading
|
||||
const isLoadingOptions = isFetchingOptions || externalLoadingOptions;
|
||||
|
||||
@@ -78,6 +85,10 @@ const SelectCellComponent = ({
|
||||
// Handle opening the dropdown - fetch options if needed
|
||||
const handleOpenChange = useCallback(
|
||||
async (isOpen: boolean) => {
|
||||
// Block opening if a popover was just closed (click-outside behavior)
|
||||
if (isOpen && Date.now() - cellPopoverClosedAt < POPOVER_CLOSE_DELAY) {
|
||||
return;
|
||||
}
|
||||
if (isOpen && onFetchOptions && options.length === 0 && !hasFetchedRef.current) {
|
||||
hasFetchedRef.current = true;
|
||||
setIsFetchingOptions(true);
|
||||
@@ -89,7 +100,7 @@ const SelectCellComponent = ({
|
||||
}
|
||||
setOpen(isOpen);
|
||||
},
|
||||
[onFetchOptions, options.length]
|
||||
[onFetchOptions, options.length, cellPopoverClosedAt]
|
||||
);
|
||||
|
||||
// Handle selection
|
||||
|
||||
@@ -16,66 +16,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useValidationStore } from '../store/validationStore';
|
||||
import { useInitPhase } from '../store/selectors';
|
||||
import type { RowData } from '../store/types';
|
||||
import type { Field } from '../../../types';
|
||||
|
||||
/**
|
||||
* Build product payload for AI validation API
|
||||
*/
|
||||
function buildProductPayload(
|
||||
row: RowData,
|
||||
_field: 'name' | 'description',
|
||||
fields: Field<string>[],
|
||||
allRows: RowData[]
|
||||
) {
|
||||
// Helper to look up field option label
|
||||
const getFieldLabel = (fieldKey: string, val: unknown): string | undefined => {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Compute sibling names for context (same company + line + subline if set)
|
||||
const siblingNames: string[] = [];
|
||||
if (row.company && row.line) {
|
||||
const companyId = String(row.company);
|
||||
const lineId = String(row.line);
|
||||
const sublineId = row.subline ? String(row.subline) : null;
|
||||
|
||||
for (const otherRow of allRows) {
|
||||
// Skip self
|
||||
if (otherRow.__index === row.__index) continue;
|
||||
|
||||
// Must match company and line
|
||||
if (String(otherRow.company) !== companyId) continue;
|
||||
if (String(otherRow.line) !== lineId) continue;
|
||||
|
||||
// If current product has subline, siblings must match subline too
|
||||
if (sublineId && String(otherRow.subline) !== sublineId) continue;
|
||||
|
||||
// Add name if it exists
|
||||
if (otherRow.name && typeof otherRow.name === 'string' && otherRow.name.trim()) {
|
||||
siblingNames.push(otherRow.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: row.name as string,
|
||||
description: row.description as string | undefined,
|
||||
company_name: row.company ? getFieldLabel('company', row.company) : undefined,
|
||||
company_id: row.company ? String(row.company) : undefined,
|
||||
line_name: row.line ? getFieldLabel('line', row.line) : undefined,
|
||||
line_id: row.line ? String(row.line) : undefined,
|
||||
subline_name: row.subline ? getFieldLabel('subline', row.subline) : undefined,
|
||||
subline_id: row.subline ? String(row.subline) : undefined,
|
||||
categories: row.categories as string | undefined,
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
};
|
||||
}
|
||||
import {
|
||||
buildNameValidationPayload,
|
||||
buildDescriptionValidationPayload,
|
||||
} from '../utils/inlineAiPayload';
|
||||
|
||||
/**
|
||||
* Trigger validation for a single field
|
||||
@@ -83,7 +27,7 @@ function buildProductPayload(
|
||||
async function triggerValidation(
|
||||
productIndex: string,
|
||||
field: 'name' | 'description',
|
||||
payload: ReturnType<typeof buildProductPayload>
|
||||
payload: Record<string, unknown>
|
||||
) {
|
||||
const {
|
||||
setInlineAiValidating,
|
||||
@@ -181,14 +125,14 @@ export function useAutoInlineAiValidation() {
|
||||
|
||||
// Trigger name validation if context is sufficient
|
||||
if (hasNameContext && !nameAlreadyValidated && !nameCurrentlyValidating) {
|
||||
const payload = buildProductPayload(row, 'name', fields, rows);
|
||||
const payload = buildNameValidationPayload(row, fields, rows);
|
||||
triggerValidation(productIndex, 'name', payload);
|
||||
nameCount++;
|
||||
}
|
||||
|
||||
// Trigger description validation if context is sufficient
|
||||
if (hasDescContext && !descAlreadyValidated && !descCurrentlyValidating) {
|
||||
const payload = buildProductPayload(row, 'description', fields, rows);
|
||||
const payload = buildDescriptionValidationPayload(row, fields);
|
||||
triggerValidation(productIndex, 'description', payload);
|
||||
descCount++;
|
||||
}
|
||||
|
||||
@@ -13,18 +13,10 @@ import { useEffect } from 'react';
|
||||
import { useValidationStore } from '../store/validationStore';
|
||||
import { useUpcValidation } from './useUpcValidation';
|
||||
import type { Field } from '../../../types';
|
||||
|
||||
/**
|
||||
* Helper to look up field option label
|
||||
*/
|
||||
function getFieldLabel(fields: Field<string>[], fieldKey: string, val: unknown): string | undefined {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
import {
|
||||
buildNameValidationPayload,
|
||||
buildDescriptionValidationPayload,
|
||||
} from '../utils/inlineAiPayload';
|
||||
|
||||
/**
|
||||
* Trigger inline AI validation for a single row/field
|
||||
@@ -45,30 +37,10 @@ async function triggerInlineAiValidation(
|
||||
|
||||
setInlineAiValidating(validationKey, true);
|
||||
|
||||
// Compute sibling names for context
|
||||
const siblingNames: string[] = [];
|
||||
if (row.company && row.line) {
|
||||
const companyId = String(row.company);
|
||||
const lineId = String(row.line);
|
||||
for (const otherRow of rows) {
|
||||
if (otherRow.__index === productIndex) continue;
|
||||
if (String(otherRow.company) !== companyId) continue;
|
||||
if (String(otherRow.line) !== lineId) continue;
|
||||
if (otherRow.name && typeof otherRow.name === 'string' && otherRow.name.trim()) {
|
||||
siblingNames.push(otherRow.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const productPayload = {
|
||||
name: String(row.name),
|
||||
description: row.description ? String(row.description) : undefined,
|
||||
company_name: row.company ? getFieldLabel(fields, 'company', row.company) : undefined,
|
||||
company_id: row.company ? String(row.company) : undefined,
|
||||
line_name: row.line ? getFieldLabel(fields, 'line', row.line) : undefined,
|
||||
line_id: row.line ? String(row.line) : undefined,
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
};
|
||||
// Build payload using centralized utility
|
||||
const productPayload = field === 'name'
|
||||
? buildNameValidationPayload(row, fields, rows)
|
||||
: buildDescriptionValidationPayload(row, fields);
|
||||
|
||||
const endpoint = field === 'name'
|
||||
? '/api/ai/validate/inline/name'
|
||||
|
||||
@@ -23,15 +23,14 @@ export interface InlineAiValidationState {
|
||||
}
|
||||
|
||||
// Product data structure for validation
|
||||
// Note: company_id is needed by backend to load company-specific prompts, but line_id/subline_id are not needed
|
||||
export interface ProductForValidation {
|
||||
name?: string;
|
||||
description?: string;
|
||||
company_name?: string;
|
||||
company_id?: string | number;
|
||||
company_id?: string; // Needed by backend for prompt loading (not sent to AI model)
|
||||
line_name?: string;
|
||||
line_id?: string | number;
|
||||
subline_name?: string;
|
||||
subline_id?: string | number;
|
||||
categories?: string;
|
||||
// Sibling context for naming decisions
|
||||
siblingNames?: string[];
|
||||
|
||||
@@ -404,6 +404,10 @@ export interface ValidationState {
|
||||
|
||||
// === File (for output) ===
|
||||
file: File | null;
|
||||
|
||||
// === UI State ===
|
||||
/** Timestamp when a MultilineInput popover was last closed (for click-outside behavior) */
|
||||
cellPopoverClosedAt: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -510,6 +514,10 @@ export interface ValidationActions {
|
||||
// === Output ===
|
||||
getCleanedData: () => CleanRowData[];
|
||||
|
||||
// === UI State ===
|
||||
/** Called when a MultilineInput popover closes to prevent immediate focus on other cells */
|
||||
setCellPopoverClosed: () => void;
|
||||
|
||||
// === Reset ===
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@ const getInitialState = (): ValidationState => ({
|
||||
|
||||
// File
|
||||
file: null,
|
||||
|
||||
// UI State
|
||||
cellPopoverClosedAt: 0,
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -953,6 +956,16 @@ export const useValidationStore = create<ValidationStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// UI State
|
||||
// =========================================================================
|
||||
|
||||
setCellPopoverClosed: () => {
|
||||
set((state) => {
|
||||
state.cellPopoverClosedAt = Date.now();
|
||||
});
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Reset
|
||||
// =========================================================================
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Inline AI Validation Payload Builder
|
||||
*
|
||||
* Centralized utility for building payloads sent to the inline AI validation endpoints.
|
||||
* This ensures consistent payload structure across all validation triggers:
|
||||
* - Blur handler in ValidationTable
|
||||
* - Auto-validation on page load
|
||||
* - Copy-down validation
|
||||
* - Template application
|
||||
*
|
||||
* Note: IDs are not included as the AI model can't look them up - only names are useful.
|
||||
*/
|
||||
|
||||
import type { RowData } from '../store/types';
|
||||
import type { Field, SelectOption } from '../../../types';
|
||||
import { useValidationStore } from '../store/validationStore';
|
||||
|
||||
/**
|
||||
* Helper to look up field option label from field definitions
|
||||
*/
|
||||
export function getFieldLabel(
|
||||
fields: Field<string>[],
|
||||
fieldKey: string,
|
||||
val: unknown
|
||||
): string | undefined {
|
||||
const fieldDef = fields.find(f => f.key === fieldKey);
|
||||
if (fieldDef && fieldDef.fieldType.type === 'select' && 'options' in fieldDef.fieldType) {
|
||||
const option = fieldDef.fieldType.options?.find(o => o.value === String(val));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up line name from the productLinesCache
|
||||
* Line options are loaded dynamically per-company and stored in a separate cache
|
||||
*/
|
||||
function getLineName(companyId: string | number, lineId: string | number): string | undefined {
|
||||
const { productLinesCache } = useValidationStore.getState();
|
||||
const lineOptions = productLinesCache.get(String(companyId)) as SelectOption[] | undefined;
|
||||
if (lineOptions) {
|
||||
const option = lineOptions.find(o => o.value === String(lineId));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up subline name from the sublinesCache
|
||||
* Subline options are loaded dynamically per-line and stored in a separate cache
|
||||
*/
|
||||
function getSublineName(lineId: string | number, sublineId: string | number): string | undefined {
|
||||
const { sublinesCache } = useValidationStore.getState();
|
||||
const sublineOptions = sublinesCache.get(String(lineId)) as SelectOption[] | undefined;
|
||||
if (sublineOptions) {
|
||||
const option = sublineOptions.find(o => o.value === String(sublineId));
|
||||
return option?.label;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute sibling product names for naming context.
|
||||
* Siblings are products with the same company + line (+ subline if set).
|
||||
*/
|
||||
export function computeSiblingNames(
|
||||
row: RowData,
|
||||
allRows: RowData[]
|
||||
): string[] {
|
||||
const siblingNames: string[] = [];
|
||||
|
||||
if (!row.company || !row.line) {
|
||||
return siblingNames;
|
||||
}
|
||||
|
||||
const companyId = String(row.company);
|
||||
const lineId = String(row.line);
|
||||
const sublineId = row.subline ? String(row.subline) : null;
|
||||
|
||||
for (const otherRow of allRows) {
|
||||
// Skip self
|
||||
if (otherRow.__index === row.__index) continue;
|
||||
|
||||
// Must match company and line
|
||||
if (String(otherRow.company) !== companyId) continue;
|
||||
if (String(otherRow.line) !== lineId) continue;
|
||||
|
||||
// If current product has subline, siblings must match subline too
|
||||
if (sublineId && String(otherRow.subline) !== sublineId) continue;
|
||||
|
||||
// Add name if it exists
|
||||
if (otherRow.name && typeof otherRow.name === 'string' && otherRow.name.trim()) {
|
||||
siblingNames.push(otherRow.name);
|
||||
}
|
||||
}
|
||||
|
||||
return siblingNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for name validation endpoint
|
||||
*/
|
||||
export interface NameValidationPayload {
|
||||
name: string;
|
||||
company_name?: string;
|
||||
company_id?: string; // Needed by backend to load company-specific prompts (not sent to AI)
|
||||
line_name?: string;
|
||||
subline_name?: string;
|
||||
siblingNames?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for description validation endpoint
|
||||
*/
|
||||
export interface DescriptionValidationPayload {
|
||||
name: string;
|
||||
description: string;
|
||||
company_name?: string;
|
||||
company_id?: string; // Needed by backend to load company-specific prompts (not sent to AI)
|
||||
categories?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for overriding row values when building payloads
|
||||
*/
|
||||
export interface PayloadOverrides {
|
||||
name?: string;
|
||||
description?: string;
|
||||
line?: string | number; // Line ID override (for line change handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build payload for name validation API
|
||||
*
|
||||
* @param row - The row data
|
||||
* @param fields - Field definitions for label lookup
|
||||
* @param allRows - All rows for sibling computation
|
||||
* @param overrides - Optional value overrides (e.g., new name from blur handler, new line from line change)
|
||||
*/
|
||||
export function buildNameValidationPayload(
|
||||
row: RowData,
|
||||
fields: Field<string>[],
|
||||
allRows: RowData[],
|
||||
overrides?: PayloadOverrides
|
||||
): NameValidationPayload {
|
||||
// Use override line for sibling computation if provided
|
||||
const effectiveRow = overrides?.line !== undefined
|
||||
? { ...row, line: overrides.line }
|
||||
: row;
|
||||
const siblingNames = computeSiblingNames(effectiveRow, allRows);
|
||||
|
||||
// Determine line_name - use override if provided
|
||||
// Line options are stored in productLinesCache (keyed by company ID), not field options
|
||||
const lineValue = overrides?.line ?? row.line;
|
||||
const lineName = row.company && lineValue
|
||||
? getLineName(row.company, lineValue)
|
||||
: undefined;
|
||||
|
||||
// Subline options are stored in sublinesCache (keyed by line ID), not field options
|
||||
const sublineName = lineValue && row.subline
|
||||
? getSublineName(lineValue, row.subline)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name: overrides?.name ?? String(row.name || ''),
|
||||
company_name: row.company ? getFieldLabel(fields, 'company', row.company) : undefined,
|
||||
company_id: row.company ? String(row.company) : undefined, // For backend prompt loading
|
||||
line_name: lineName,
|
||||
subline_name: sublineName,
|
||||
siblingNames: siblingNames.length > 0 ? siblingNames : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build payload for description validation API
|
||||
*
|
||||
* @param row - The row data
|
||||
* @param fields - Field definitions for label lookup
|
||||
* @param overrides - Optional value overrides (e.g., from blur handler)
|
||||
*/
|
||||
export function buildDescriptionValidationPayload(
|
||||
row: RowData,
|
||||
fields: Field<string>[],
|
||||
overrides?: PayloadOverrides
|
||||
): DescriptionValidationPayload {
|
||||
return {
|
||||
name: overrides?.name ?? String(row.name || ''),
|
||||
description: overrides?.description ?? String(row.description || ''),
|
||||
company_name: row.company ? getFieldLabel(fields, 'company', row.company) : undefined,
|
||||
company_id: row.company ? String(row.company) : undefined, // For backend prompt loading
|
||||
categories: row.categories as string | undefined,
|
||||
};
|
||||
}
|
||||
@@ -484,7 +484,7 @@ export function PromptManagement() {
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2 justify-end pr-4">
|
||||
<div className="flex gap-0 justify-end">
|
||||
<Button variant="ghost" onClick={() => handleEdit(row.original)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
@@ -553,9 +553,9 @@ export function PromptManagement() {
|
||||
<Table>
|
||||
<TableHeader className="bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<TableRow key={headerGroup.id} className="">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
<TableHead key={header.id} className="whitespace-nowrap">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
@@ -569,7 +569,7 @@ export function PromptManagement() {
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="hover:bg-gray-100">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="pl-6">
|
||||
<TableCell key={cell.id} className="pl-3 whitespace-nowrap">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
@@ -614,7 +614,7 @@ export function PromptManagement() {
|
||||
<SelectLabel className="text-xs text-muted-foreground">Predefined Tasks</SelectLabel>
|
||||
{PREDEFINED_TASKS.map((task) => (
|
||||
<SelectItem key={task.value} value={task.value}>
|
||||
<span className="flex flex-col">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{task.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{task.description}</span>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user