Fix line/subline regressions, add in AI validation tracking and improve AI results dialog

This commit is contained in:
2025-02-26 00:38:17 -05:00
parent 2df5428712
commit 6b101a91f6
5 changed files with 758 additions and 85 deletions

View File

@@ -171,16 +171,34 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
// Determine if the field should be disabled based on its key and context
const isFieldDisabled = useMemo(() => {
// If the field is already disabled by the parent component, respect that
if (field.disabled) return true;
// Special handling for line and subline fields
if (field.key === 'line') {
// Enable the line field if we have product lines available
return !productLines || productLines.length === 0;
// Never disable line field if it already has a value
if (value) return false;
// The line field should be enabled if:
// 1. We have a company selected (even if product lines are still loading)
// 2. We have product lines available
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options?.length) && !productLines?.length;
}
if (field.key === 'subline') {
// Enable subline field if we have sublines available
return !sublines || sublines.length === 0;
// Never disable subline field if it already has a value
if (value) return false;
// The subline field should be enabled if:
// 1. We have a line selected (even if sublines are still loading)
// 2. We have sublines available
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options?.length) && !sublines?.length;
}
// For other fields, use the disabled property
return field.disabled;
}, [field.key, field.disabled, productLines, sublines]);
}, [field.key, field.disabled, field.fieldType, productLines, sublines, value]);
// For debugging
useEffect(() => {
@@ -269,21 +287,71 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
if (fieldType.type === "select" || fieldType.type === "multi-select") {
if (fieldType.type === "select") {
// For line and subline fields, ensure we're using the latest options
if (field.key === 'line' && productLines?.length) {
const option = productLines.find((opt: SelectOption) => opt.value === value);
return option?.label || value;
if (field.key === 'line') {
// Log current state for debugging
console.log('Getting display value for line:', {
value,
productLines: productLines?.length ?? 0,
options: fieldType.options?.length ?? 0
});
// First try to find in productLines if available
if (productLines?.length) {
const matchingOption = productLines.find((opt: SelectOption) =>
String(opt.value) === String(value));
if (matchingOption) {
console.log('Found line in productLines:', value, '->', matchingOption.label);
return matchingOption.label;
}
}
// Fall back to fieldType options if productLines not available yet
if (fieldType.options?.length) {
const fallbackOptionLine = fieldType.options.find((opt: SelectOption) =>
String(opt.value) === String(value));
if (fallbackOptionLine) {
console.log('Found line in fallback options:', value, '->', fallbackOptionLine.label);
return fallbackOptionLine.label;
}
}
console.log('Unable to find display value for line:', value);
return value;
}
if (field.key === 'subline' && sublines?.length) {
const option = sublines.find((opt: SelectOption) => opt.value === value);
return option?.label || value;
if (field.key === 'subline') {
// Log current state for debugging
console.log('Getting display value for subline:', {
value,
sublines: sublines?.length ?? 0,
options: fieldType.options?.length ?? 0
});
// First try to find in sublines if available
if (sublines?.length) {
const matchingOption = sublines.find((opt: SelectOption) =>
String(opt.value) === String(value));
if (matchingOption) {
console.log('Found subline in sublines:', value, '->', matchingOption.label);
return matchingOption.label;
}
}
// Fall back to fieldType options if sublines not available yet
if (fieldType.options?.length) {
const fallbackOptionSubline = fieldType.options.find((opt: SelectOption) =>
String(opt.value) === String(value));
if (fallbackOptionSubline) {
console.log('Found subline in fallback options:', value, '->', fallbackOptionSubline.label);
return fallbackOptionSubline.label;
}
}
console.log('Unable to find display value for subline:', value);
return value;
}
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value;
return fieldType.options?.find((opt: SelectOption) => String(opt.value) === String(value))?.label || value;
}
if (Array.isArray(value)) {
const options = field.key === 'line' && productLines?.length ? productLines :
field.key === 'subline' && sublines?.length ? sublines :
fieldType.options;
return value.map(v => options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ");
return value.map(v => options.find((opt: SelectOption) => String(opt.value) === String(v))?.label || v).join(", ");
}
return value;
}
@@ -881,21 +949,23 @@ function useTemplates<T extends string>(
const { __index, __errors, __template, ...templateData } = selectedRow;
// Clean numeric values and prepare template data
const cleanedData = Object.entries(templateData).reduce((acc, [key, value]) => {
const cleanedData: Record<string, any> = {};
// Process each key-value pair
Object.entries(templateData).forEach(([key, value]) => {
// Handle numeric values with dollar signs
if (typeof value === 'string' && value.includes('$')) {
acc[key] = value.replace(/[$,\s]/g, '').trim();
cleanedData[key] = value.replace(/[$,\s]/g, '').trim();
}
// Handle array values (like categories or ship_restrictions)
else if (Array.isArray(value)) {
acc[key] = value;
cleanedData[key] = value;
}
// Handle other values
else {
acc[key] = value;
cleanedData[key] = value;
}
return acc;
}, {} as Record<string, any>);
});
// Log the cleaned data before sending
console.log('Saving template with cleaned data:', {
@@ -1049,6 +1119,19 @@ const SaveTemplateDialog = memo(({
);
});
// Add a new interface to handle the AI validation details response
interface ChangeDetail {
field: string;
original: any;
corrected: any;
}
interface ProductChangeDetail {
productIndex: number;
title: string;
changes: ChangeDetail[];
}
export const ValidationStep = <T extends string>({
initialData,
file,
@@ -1083,13 +1166,18 @@ export const ValidationStep = <T extends string>({
queryKey: ["sublines", globalSelections?.line],
queryFn: async () => {
if (!globalSelections?.line) return [];
console.log('Fetching sublines for line:', globalSelections.line);
const response = await fetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`);
if (!response.ok) {
console.error('Failed to fetch sublines:', response.status, response.statusText);
throw new Error("Failed to fetch sublines");
}
return response.json();
const data = await response.json();
console.log('Received sublines:', data);
return data;
},
enabled: !!globalSelections?.line,
staleTime: 30000, // Cache for 30 seconds
});
// Apply global selections to initial data and validate
@@ -1163,7 +1251,9 @@ export const ValidationStep = <T extends string>({
...field.fieldType,
options: productLines || (field.fieldType.type === 'select' ? field.fieldType.options : []),
},
disabled: (!productLines || productLines.length === 0) && !globalSelections?.line
// Only disable if no company is selected or if product lines failed to load
// when a company is selected
disabled: !globalSelections?.company || (globalSelections?.company && productLines !== undefined && productLines.length === 0)
} as Field<T>;
}
if (field.key === 'subline') {
@@ -1179,7 +1269,7 @@ export const ValidationStep = <T extends string>({
}
return field;
});
}, [fields, productLines, sublines, globalSelections?.line]);
}, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]);
const [data, setData] = useState<RowData<T>[]>(initialDataWithGlobals);
@@ -1208,10 +1298,13 @@ export const ValidationStep = <T extends string>({
const [aiValidationDetails, setAiValidationDetails] = useState<{
changes: string[];
warnings: string[];
changeDetails: ProductChangeDetail[];
isOpen: boolean;
originalData?: (Data<T> & ExtendedMeta)[]; // Store original data for reverting changes
}>({
changes: [],
warnings: [],
changeDetails: [],
isOpen: false,
});
@@ -1219,6 +1312,11 @@ export const ValidationStep = <T extends string>({
isOpen: boolean;
status: string;
step: number;
estimatedSeconds?: number;
startTime?: Date;
promptLength?: number;
elapsedSeconds?: number;
progressPercent?: number;
}>({
isOpen: false,
status: "",
@@ -1259,15 +1357,18 @@ export const ValidationStep = <T extends string>({
async (rows: (Data<T> & ExtendedMeta)[], indexes?: number[]) => {
setData(rows);
// Use fieldsWithUpdatedOptions to ensure we have the latest field definitions with proper options
const currentFields = fieldsWithUpdatedOptions as unknown as Fields<T>;
if (rowHook?.constructor.name === "AsyncFunction" || tableHook?.constructor.name === "AsyncFunction") {
const updatedData = await addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook, indexes);
const updatedData = await addErrorsAndRunHooks<T>(rows, currentFields, rowHook, tableHook, indexes);
setData(updatedData as (Data<T> & ExtendedMeta)[]);
} else {
const result = await addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook, indexes);
const result = await addErrorsAndRunHooks<T>(rows, currentFields, rowHook, tableHook, indexes);
setData(result as (Data<T> & ExtendedMeta)[]);
}
},
[rowHook, tableHook, fields],
[rowHook, tableHook, fieldsWithUpdatedOptions],
);
const updateRows = useCallback(
@@ -1544,17 +1645,59 @@ export const ValidationStep = <T extends string>({
}
}, [data, submitData]);
// Add AI validation function
// Update the AI validation function
const handleAiValidation = async () => {
try {
if (isAiValidating) return;
// Store the original data before any changes
const originalDataCopy = [...data];
setIsAiValidating(true);
const startTime = new Date();
setAiValidationProgress({
isOpen: true,
status: "Preparing data for validation...",
step: 1
step: 1,
startTime,
elapsedSeconds: 0,
progressPercent: 0,
...(aiValidationProgress.estimatedSeconds ? {
estimatedSeconds: aiValidationProgress.estimatedSeconds,
promptLength: aiValidationProgress.promptLength
} : {})
});
console.log('Sending data for validation:', data);
// Set up an interval to update progress
window.aiValidationTimer = setInterval(() => {
const now = new Date();
const elapsedSeconds = Math.floor((now.getTime() - startTime.getTime()) / 1000);
setAiValidationProgress(prev => {
// Calculate progress percentage
let progressPercent = 0;
if (prev.estimatedSeconds && prev.estimatedSeconds > 0) {
// Cap at 99% if we exceed estimated time but aren't done yet
progressPercent = Math.min(99, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100));
} else {
// If no estimate, use step-based progress (25% per step), also capped at 99%
progressPercent = Math.min(99, (prev.step * 25) + Math.min(24, Math.floor((elapsedSeconds % 10) / 10 * 25)));
}
// Extract the base status message without any time information
const baseStatus = prev.status.replace(/\s\(\d+[ms].+\)$/, '').replace(/\s\(\d+m \d+s.+\)$/, '');
return {
...prev,
elapsedSeconds,
progressPercent,
// Just use the base status message without time information
status: baseStatus
};
});
}, 1000);
// Clean the data to ensure we only send what's needed
const cleanedData = data.map(item => {
const { __errors, __index, ...cleanProduct } = item;
@@ -1563,9 +1706,36 @@ export const ValidationStep = <T extends string>({
console.log('Cleaned data for validation:', cleanedData);
// If we don't have an estimated time yet, try to get one
if (!aiValidationProgress.estimatedSeconds) {
try {
const debugResponse = await fetch(`${config.apiUrl}/ai-validation/debug`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products: cleanedData })
});
if (debugResponse.ok) {
const debugData = await debugResponse.json();
if (debugData.estimatedProcessingTime?.seconds) {
setAiValidationProgress(prev => ({
...prev,
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
promptLength: debugData.promptLength
}));
}
}
} catch (estimateError) {
console.error('Error getting time estimate:', estimateError);
// Continue without an estimate
}
}
setAiValidationProgress(prev => ({
...prev,
status: "Sending data to AI service and awaiting response...",
status: "Sending data to AI service...",
step: 2
}));
@@ -1587,12 +1757,6 @@ export const ValidationStep = <T extends string>({
throw new Error(`AI validation failed: ${response.status} ${response.statusText}`);
}
setAiValidationProgress(prev => ({
...prev,
status: "Processing AI response...",
step: 3
}));
const result = await response.json();
console.log('AI validation response:', result);
@@ -1600,51 +1764,172 @@ export const ValidationStep = <T extends string>({
throw new Error(result.error || 'AI validation failed');
}
// Update progress with actual processing time if available
if (result.performanceMetrics) {
console.log('Performance metrics:', result.performanceMetrics);
setAiValidationProgress(prev => ({
...prev,
status: "Processing AI response...",
step: 3,
// Update with actual metrics from the server
estimatedSeconds: result.performanceMetrics.processingTimeSeconds || prev.estimatedSeconds,
promptLength: result.performanceMetrics.promptLength || prev.promptLength,
progressPercent: 75 // 75% complete when we're processing the AI response
}));
} else {
setAiValidationProgress(prev => ({
...prev,
status: "Processing AI response...",
step: 3,
progressPercent: 75
}));
}
setAiValidationProgress(prev => ({
...prev,
status: "Applying corrections...",
step: 4
step: 4,
progressPercent: 90 // 90% complete when applying corrections
}));
// Update the data with AI suggestions
if (result.correctedData && Array.isArray(result.correctedData)) {
// Log the differences
data.forEach((original, index) => {
const corrected = result.correctedData[index];
if (corrected) {
const changes = Object.keys(corrected).filter(key => {
const originalValue = original[key as keyof typeof original];
const correctedValue = corrected[key as keyof typeof corrected];
return JSON.stringify(originalValue) !== JSON.stringify(correctedValue);
});
if (changes.length > 0) {
console.log(`Changes for row ${index + 1}:`, changes.map(key => ({
field: key,
original: original[key as keyof typeof original],
corrected: corrected[key as keyof typeof corrected]
})));
// Process data to properly handle comma-separated values for multi-select fields
const processedData = result.correctedData.map((corrected: any) => {
const processed = { ...corrected };
// Process each field
Object.keys(processed).forEach(key => {
const fieldConfig = fields.find(f => f.key === key);
// Handle multi-select fields (comma-separated values)
if (fieldConfig?.fieldType.type === 'multi-select' && typeof processed[key] === 'string') {
// Split comma-separated values and trim each value
processed[key] = processed[key].split(',').map((v: string) => v.trim()).filter(Boolean);
}
}
// For select and multi-select fields, ensure we're working with IDs
// We don't convert IDs to display names here because we want to preserve IDs in the data
if (fieldConfig?.fieldType.type === 'select' || fieldConfig?.fieldType.type === 'multi-select') {
const options = fieldConfig.fieldType.options || [];
// If the value is a string that matches a label but not a value, convert it to the corresponding ID
if (!Array.isArray(processed[key]) && typeof processed[key] === 'string') {
// Check if the value is already an ID
const isAlreadyId = options.some(opt => String(opt.value) === String(processed[key]));
if (!isAlreadyId) {
// Try to find the option by label
const matchingOption = options.find(opt => opt.label === processed[key]);
if (matchingOption) {
// Convert label to ID
const originalValue = processed[key];
processed[key] = matchingOption.value;
console.log(`Converted label "${originalValue}" to ID "${matchingOption.value}" for field "${key}"`);
}
}
}
// Handle array of values (multi-select)
if (Array.isArray(processed[key])) {
processed[key] = processed[key].map((val: string | number) => {
// Check if the value is already an ID
const isAlreadyId = options.some(opt => String(opt.value) === String(val));
if (!isAlreadyId) {
// Try to find the option by label
const matchingOption = options.find(opt => opt.label === val);
if (matchingOption) {
// Convert label to ID
return matchingOption.value;
}
}
return val;
});
}
}
});
return processed;
});
// Preserve the __index and __errors from the original data
const newData = result.correctedData.map((item: any, idx: number) => ({
const newData = processedData.map((item: any, idx: number) => ({
...item,
__index: data[idx]?.__index,
__errors: data[idx]?.__errors,
}));
// Update the data and run validations
await updateData(newData);
console.log('About to update data with AI corrections:', {
originalDataSample: data.slice(0, 2),
newDataSample: newData.slice(0, 2),
correctionCount: result.changes?.length || 0
});
// First validate the new data to ensure all validation rules are applied
try {
// Use the current fields with updated options for validation
const currentFields = fieldsWithUpdatedOptions as unknown as Fields<T>;
// Validate the data with the hooks
const validatedData = await addErrorsAndRunHooks<T>(
newData,
currentFields,
rowHook,
tableHook
);
// Update the component state with the validated data
setData(validatedData as (Data<T> & ExtendedMeta)[]);
// Force a small delay to ensure React updates the state before showing the results
await new Promise(resolve => setTimeout(resolve, 50));
// Log a sample of the state after update, focus on line/subline fields
console.log('State after AI validation:', {
sampleLineValues: validatedData.slice(0, 3).map(row => ({
line: row.line,
subline: row.subline,
lineDisplay: getFieldDisplayValue('line', row.line),
sublineDisplay: getFieldDisplayValue('subline', row.subline)
})),
fieldsWithOptions: fieldsWithUpdatedOptions
.filter(f => f.fieldType.type === 'select' && ['line', 'subline'].includes(f.key as string))
.map(f => ({
key: f.key,
options: 'options' in f.fieldType ? f.fieldType.options?.length || 0 : 0
}))
});
console.log('Data updated after AI validation:', {
dataLength: validatedData.length,
hasErrors: validatedData.some(row => row.__errors && Object.keys(row.__errors).length > 0)
});
// Show changes and warnings in dialog after data is updated
setAiValidationDetails({
changes: result.changes || [],
warnings: result.warnings || [],
changeDetails: result.changeDetails || [],
isOpen: true,
originalData: originalDataCopy // Use the stored original data
});
} catch (error) {
console.error('Error validating AI corrections:', error);
// Fall back to basic update without validation
setData(newData);
// Still show the result dialog even if validation failed
setAiValidationDetails({
changes: result.changes || [],
warnings: result.warnings || [],
changeDetails: result.changeDetails || [],
isOpen: true,
originalData: originalDataCopy, // Use the stored original data
});
}
}
// Show changes and warnings in dialog
setAiValidationDetails({
changes: result.changes || [],
warnings: result.warnings || [],
isOpen: true,
});
setAiValidationProgress(prev => ({
...prev,
status: "Validation complete!",
@@ -1654,7 +1939,6 @@ export const ValidationStep = <T extends string>({
setTimeout(() => {
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
}, 1000);
} catch (error) {
console.error('AI Validation Error:', error);
toast({
@@ -1668,7 +1952,18 @@ export const ValidationStep = <T extends string>({
step: -1
}));
} finally {
// Clear the interval when we're done (success or error)
if (window.aiValidationTimer) {
clearInterval(window.aiValidationTimer);
window.aiValidationTimer = undefined;
}
setIsAiValidating(false);
// Only set to 100% when actually complete (or in error state)
setAiValidationProgress(prev => ({
...prev,
progressPercent: prev.step === -1 ? prev.progressPercent : 100 // Only show 100% if successful completion
}));
}
};
@@ -1719,7 +2014,8 @@ export const ValidationStep = <T extends string>({
// Log the response stats
console.log('Debug response stats:', {
promptLength: debugData.promptLength,
taxonomyStats: debugData.taxonomyStats
taxonomyStats: debugData.taxonomyStats,
estimatedProcessingTime: debugData.estimatedProcessingTime
});
setCurrentPrompt(prev => ({
@@ -1727,6 +2023,15 @@ export const ValidationStep = <T extends string>({
prompt: debugData.sampleFullPrompt,
isLoading: false
}));
// Store the estimated processing time for later use
if (debugData.estimatedProcessingTime?.seconds) {
setAiValidationProgress(prev => ({
...prev,
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
promptLength: debugData.promptLength
}));
}
} catch (error) {
console.error('Error fetching prompt:', error);
toast({
@@ -1738,6 +2043,83 @@ export const ValidationStep = <T extends string>({
}
};
// Function to get display value for field values (including IDs)
const getFieldDisplayValue = (fieldKey: string, value: any): string => {
const field = fields.find(f => f.key === fieldKey);
if (!field) return String(value);
// Handle different field types
if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') {
const options = field.fieldType.options || [];
// Handle array of values (multi-select)
if (Array.isArray(value)) {
return value.map(v => {
const option = options.find(opt => String(opt.value) === String(v));
return option ? option.label : v;
}).join(', ');
}
// Handle single value
const option = options.find(opt => String(opt.value) === String(value));
return option ? option.label : String(value);
}
return String(value);
};
// Add a function to revert a specific AI validation change
const revertAiChange = (productIndex: number, fieldKey: string) => {
// Ensure we have the original data
if (!aiValidationDetails.originalData) {
console.error('Cannot revert: original data not available');
return;
}
// Find the original product and current product
const originalProduct = aiValidationDetails.originalData[productIndex];
if (!originalProduct) {
console.error(`Cannot revert: original product at index ${productIndex} not found`);
return;
}
const currentProduct = data[productIndex];
if (!currentProduct) {
console.error(`Cannot revert: current product at index ${productIndex} not found`);
return;
}
// Get the original value in a type-safe way
const originalValue = fieldKey in originalProduct ?
(originalProduct as Record<string, any>)[fieldKey] : undefined;
console.log(`Reverting change to field "${fieldKey}" for product at index ${productIndex}`, {
original: originalValue,
current: fieldKey in currentProduct ? (currentProduct as Record<string, any>)[fieldKey] : undefined
});
// Create a new data array with the reverted field
const newData = [...data];
// Create a new product object with the reverted field
newData[productIndex] = {
...newData[productIndex],
[fieldKey]: originalValue
} as Data<T> & ExtendedMeta; // Cast to ensure type safety
// Update the data state
setData(newData);
// Re-validate to update error states
updateData(newData, [productIndex]);
// Show success notification
toast({
title: "Change reverted",
description: `Reverted the change to "${fieldKey}"`,
});
};
return (
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
<CopyDownDialog
@@ -1809,19 +2191,43 @@ export const ValidationStep = <T extends string>({
<div
className="h-full bg-primary transition-all duration-500"
style={{
width: `${(aiValidationProgress.step / 5) * 100}%`,
width: `${aiValidationProgress.progressPercent ?? (aiValidationProgress.step / 5) * 100}%`,
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined
}}
/>
</div>
</div>
<div className="text-sm text-muted-foreground w-12 text-right">
{aiValidationProgress.step === -1 ? '❌' : `${Math.round((aiValidationProgress.step / 5) * 100)}%`}
{aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`}
</div>
</div>
<p className="text-center text-sm text-muted-foreground">
{aiValidationProgress.status}
</p>
{aiValidationProgress.estimatedSeconds && aiValidationProgress.elapsedSeconds !== undefined && aiValidationProgress.step > 0 && aiValidationProgress.step < 5 && (
<div className="text-center text-sm">
{(() => {
// Calculate time remaining using the elapsed seconds
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
// Format time remaining
if (remainingSeconds < 60) {
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
} else {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = Math.round(remainingSeconds % 60);
return `Approximately ${minutes}m ${seconds}s remaining`;
}
})()}
{aiValidationProgress.promptLength && (
<p className="mt-1 text-xs text-muted-foreground">
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
</p>
)}
</div>
)}
</div>
</DialogContent>
</Dialog>
@@ -1829,7 +2235,7 @@ export const ValidationStep = <T extends string>({
open={aiValidationDetails.isOpen}
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>AI Validation Results</DialogTitle>
<DialogDescription>
@@ -1837,18 +2243,102 @@ export const ValidationStep = <T extends string>({
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh]">
{aiValidationDetails.changes.length > 0 && (
<div className="mb-4">
<h3 className="font-semibold mb-2">Changes Made:</h3>
<ul className="space-y-2">
{aiValidationDetails.changes.map((change, i) => (
<li key={i} className="flex gap-2">
<span className="text-green-500"></span>
<span>{change}</span>
</li>
))}
</ul>
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (
<div className="mb-6 space-y-6">
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
{aiValidationDetails.changeDetails.map((product, i) => (
<div key={i} className="border rounded-md p-4">
<h4 className="font-medium text-base mb-3">{product.title}</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-1/4">Field</TableHead>
<TableHead className="w-3/8">Original Value</TableHead>
<TableHead className="w-3/8">Corrected Value</TableHead>
<TableHead className="w-1/8 text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{product.changes.map((change, j) => {
const field = fields.find(f => f.key === change.field);
return (
<TableRow key={j}>
<TableCell className="font-medium">{field?.label || change.field}</TableCell>
<TableCell>
<div className="space-y-1">
<div>{getFieldDisplayValue(change.field, change.original)}</div>
{/* Show raw value if it's an ID */}
{change.original && typeof change.original === 'string' &&
!isNaN(Number(change.original)) &&
getFieldDisplayValue(change.field, change.original) !== change.original && (
<div className="text-xs text-muted-foreground">ID: {change.original}</div>
)}
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div>{getFieldDisplayValue(change.field, change.corrected)}</div>
{/* Show raw value if it's an ID */}
{change.corrected && typeof change.corrected === 'string' &&
!isNaN(Number(change.corrected)) &&
getFieldDisplayValue(change.field, change.corrected) !== change.corrected && (
<div className="text-xs text-muted-foreground">ID: {change.corrected}</div>
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="mt-2">
<Button
variant="outline"
size="sm"
onClick={() => {
// Find the product in the current data
const currentDataIndex = data.findIndex(
d => d.__index === data[product.productIndex]?.__index
);
if (currentDataIndex >= 0) {
// Create new data array with the original value for this field
const newData = [...data];
newData[currentDataIndex] = {
...newData[currentDataIndex],
[change.field]: change.original
};
// Update the data
updateData(newData, [currentDataIndex]);
// Show toast
toast({
title: "Change reverted",
description: `Reverted ${change.field} to original value`,
});
}
}}
>
Revert Change
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
))}
</div>
) : (
aiValidationDetails.changes.length > 0 && (
<div className="mb-4">
<h3 className="font-semibold mb-2">Changes Made:</h3>
<ul className="space-y-2">
{aiValidationDetails.changes.map((change, i) => (
<li key={i} className="flex gap-2">
<span className="text-green-500"></span>
<span>{change}</span>
</li>
))}
</ul>
</div>
)
)}
{aiValidationDetails.warnings.length > 0 && (
<div>
@@ -2077,3 +2567,10 @@ export const ValidationStep = <T extends string>({
</div>
)
}
// Add TypeScript declaration for our global timer variable
declare global {
interface Window {
aiValidationTimer?: NodeJS.Timeout;
}
}

View File

@@ -23,6 +23,10 @@ interface DebugData {
basePrompt: string
sampleFullPrompt: string
promptLength: number
estimatedProcessingTime?: {
seconds: number | null
sampleCount: number
}
}
export function AiValidationDebug() {
@@ -147,6 +151,23 @@ export function AiValidationDebug() {
Cost: <span id="tokenCost">{((Math.round(debugData.promptLength / 4) / 1_000_000) * 3 * 100).toFixed(1)}</span>¢
</div>
</div>
{debugData.estimatedProcessingTime && (
<div className="mt-4 p-3 bg-muted rounded-md">
<h3 className="text-sm font-medium mb-2">Processing Time Estimate</h3>
{debugData.estimatedProcessingTime.seconds ? (
<div className="space-y-1">
<div className="text-sm">
Estimated time: {formatTime(debugData.estimatedProcessingTime.seconds)}
</div>
<div className="text-xs text-muted-foreground">
Based on {debugData.estimatedProcessingTime.sampleCount} similar validation{debugData.estimatedProcessingTime.sampleCount !== 1 ? 's' : ''}
</div>
</div>
) : (
<div className="text-sm text-muted-foreground">No historical data available for this prompt size</div>
)}
</div>
)}
</div>
</CardContent>
</Card>
@@ -165,4 +186,15 @@ export function AiValidationDebug() {
)}
</div>
)
}
// Helper function to format time in a human-readable way
function formatTime(seconds: number): string {
if (seconds < 60) {
return `${Math.round(seconds)} seconds`;
} else {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes}m ${remainingSeconds}s`;
}
}