Enhance ai validation changes dialog

This commit is contained in:
2025-03-24 14:17:02 -04:00
parent 114018080a
commit 00a02aa788

View File

@@ -8,7 +8,7 @@ import {
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Loader2, CheckIcon } from "lucide-react";
import { Loader2, CheckIcon, XIcon } from "lucide-react";
import { Code } from "@/components/ui/code";
import {
Table,
@@ -49,7 +49,12 @@ interface DebugData {
promptSources?: {
systemPrompt?: { id: number; prompt_text: string };
generalPrompt?: { id: number; prompt_text: string };
companyPrompts?: Array<{ id: number; company: string; companyName?: string; prompt_text: string }>;
companyPrompts?: Array<{
id: number;
company: string;
companyName?: string;
prompt_text: string;
}>;
};
estimatedProcessingTime?: {
seconds: number | null;
@@ -93,13 +98,69 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
debugData,
}) => {
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
const hasCompanyPrompts = currentPrompt.debugData?.promptSources?.companyPrompts &&
currentPrompt.debugData.promptSources.companyPrompts.length > 0;
const hasCompanyPrompts =
currentPrompt.debugData?.promptSources?.companyPrompts &&
currentPrompt.debugData.promptSources.companyPrompts.length > 0;
// Create our own state to track changes
const [localReversionState, setLocalReversionState] = useState<
Record<string, boolean>
>({});
// Initialize local state from the isChangeReverted function when component mounts
// or when aiValidationDetails changes
React.useEffect(() => {
if (
aiValidationDetails.changeDetails &&
aiValidationDetails.changeDetails.length > 0
) {
const initialState: Record<string, boolean> = {};
aiValidationDetails.changeDetails.forEach((product) => {
product.changes.forEach((change) => {
const key = `${product.productIndex}-${change.field}`;
initialState[key] = isChangeReverted(
product.productIndex,
change.field
);
});
});
setLocalReversionState(initialState);
}
}, [aiValidationDetails.changeDetails, isChangeReverted]);
// This function will toggle the local state for a given change
const toggleChangeAcceptance = (productIndex: number, fieldKey: string) => {
const key = `${productIndex}-${fieldKey}`;
const currentlyRejected = !!localReversionState[key];
// Toggle the local state
setLocalReversionState((prev) => ({
...prev,
[key]: !prev[key],
}));
// Only call revertAiChange when toggling to rejected state
// Since revertAiChange is specifically for rejecting changes
if (!currentlyRejected) {
revertAiChange(productIndex, fieldKey);
}
};
// Function to check local reversion state
const isChangeLocallyReverted = (
productIndex: number,
fieldKey: string
): boolean => {
const key = `${productIndex}-${fieldKey}`;
return !!localReversionState[key];
};
// Use "full" as the default tab
const defaultTab = "full";
const [activeTab, setActiveTab] = useState(defaultTab);
// Update activeTab when the dialog is opened with new data
React.useEffect(() => {
if (currentPrompt.isOpen) {
@@ -155,7 +216,9 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<div className="grid grid-cols-3 gap-4 mb-4">
<Card className="py-2">
<CardHeader className="py-2">
<CardTitle className="text-base">Prompt Length</CardTitle>
<CardTitle className="text-base">
Prompt Length
</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
@@ -163,10 +226,14 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<span className="text-muted-foreground">
Characters:
</span>{" "}
<span className="font-semibold">{promptLength}</span>
<span className="font-semibold">
{promptLength}
</span>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Tokens:</span>{" "}
<span className="text-muted-foreground">
Tokens:
</span>{" "}
<span className="font-semibold">
~{Math.round(promptLength / 4)}
</span>
@@ -177,7 +244,9 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<Card className="py-2">
<CardHeader className="py-2">
<CardTitle className="text-base">Cost Estimate</CardTitle>
<CardTitle className="text-base">
Cost Estimate
</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
@@ -225,7 +294,8 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
{currentPrompt.debugData?.estimatedProcessingTime ? (
currentPrompt.debugData.estimatedProcessingTime.seconds ? (
currentPrompt.debugData.estimatedProcessingTime
.seconds ? (
<>
<div className="text-sm">
<span className="text-muted-foreground">
@@ -233,23 +303,28 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</span>{" "}
<span className="font-semibold">
{formatTime(
currentPrompt.debugData.estimatedProcessingTime.seconds
currentPrompt.debugData
.estimatedProcessingTime.seconds
)}
</span>
</div>
<div className="text-xs text-muted-foreground">
Based on{" "}
{currentPrompt.debugData.estimatedProcessingTime.sampleCount}{" "}
{
currentPrompt.debugData
.estimatedProcessingTime.sampleCount
}{" "}
similar validation
{currentPrompt.debugData.estimatedProcessingTime
.sampleCount !== 1
{currentPrompt.debugData
.estimatedProcessingTime.sampleCount !== 1
? "s"
: ""}
</div>
</>
) : (
<div className="text-sm text-muted-foreground">
No historical data available for this prompt size
No historical data available for this prompt
size
</div>
)
) : (
@@ -278,179 +353,276 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
{/* Prompt Sources Card - Fixed at the top of the content area */}
<Card className="py-2 mb-4 flex-shrink-0">
<CardHeader className="py-2">
<CardTitle className="text-base">Prompt Sources</CardTitle>
<CardTitle className="text-base">
Prompt Sources
</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-wrap gap-2">
<Badge
variant="outline"
<Badge
variant="outline"
className="bg-purple-100 hover:bg-purple-200 cursor-pointer"
onClick={() => document.getElementById('system-message')?.scrollIntoView({ behavior: 'smooth' })}
onClick={() =>
document
.getElementById("system-message")
?.scrollIntoView({ behavior: "smooth" })
}
>
System
</Badge>
<Badge
variant="outline"
<Badge
variant="outline"
className="bg-green-100 hover:bg-green-200 cursor-pointer"
onClick={() => document.getElementById('general-section')?.scrollIntoView({ behavior: 'smooth' })}
onClick={() =>
document
.getElementById("general-section")
?.scrollIntoView({ behavior: "smooth" })
}
>
General
</Badge>
{currentPrompt.debugData.promptSources?.companyPrompts?.map((company, idx) => (
<Badge
key={idx}
variant="outline"
className="bg-blue-100 hover:bg-blue-200 cursor-pointer"
onClick={() => document.getElementById('company-section')?.scrollIntoView({ behavior: 'smooth' })}
>
{company.companyName || `Company ${company.company}`}
</Badge>
))}
<Badge
variant="outline"
{currentPrompt.debugData.promptSources?.companyPrompts?.map(
(company, idx) => (
<Badge
key={idx}
variant="outline"
className="bg-blue-100 hover:bg-blue-200 cursor-pointer"
onClick={() =>
document
.getElementById("company-section")
?.scrollIntoView({ behavior: "smooth" })
}
>
{company.companyName ||
`Company ${company.company}`}
</Badge>
)
)}
<Badge
variant="outline"
className="bg-amber-100 hover:bg-amber-200 cursor-pointer"
onClick={() => document.getElementById('taxonomy-section')?.scrollIntoView({ behavior: 'smooth' })}
onClick={() =>
document
.getElementById("taxonomy-section")
?.scrollIntoView({ behavior: "smooth" })
}
>
Taxonomy
</Badge>
<Badge
variant="outline"
<Badge
variant="outline"
className="bg-pink-100 hover:bg-pink-200 cursor-pointer"
onClick={() => document.getElementById('product-section')?.scrollIntoView({ behavior: 'smooth' })}
onClick={() =>
document
.getElementById("product-section")
?.scrollIntoView({ behavior: "smooth" })
}
>
Products
</Badge>
</div>
</CardContent>
</Card>
<ScrollArea className="flex-1 w-full overflow-y-auto">
{currentPrompt.debugData.apiFormat.map((message, idx: number) => (
<div key={idx} className="border rounded-md p-2 mb-4">
<div
id={message.role === 'system' ? 'system-message' : ''}
className={`p-2 mb-2 rounded-sm font-medium ${
message.role === 'system'
? 'bg-purple-50 text-purple-800'
: 'bg-green-50 text-green-800'
}`}
{currentPrompt.debugData.apiFormat.map(
(message, idx: number) => (
<div
key={idx}
className="border rounded-md p-2 mb-4"
>
Role: {message.role}
<div
id={
message.role === "system"
? "system-message"
: ""
}
className={`p-2 mb-2 rounded-sm font-medium ${
message.role === "system"
? "bg-purple-50 text-purple-800"
: "bg-green-50 text-green-800"
}`}
>
Role: {message.role}
</div>
<Code
className={`whitespace-pre-wrap p-4 break-normal max-w-full ${
message.role === "system"
? "bg-purple-50/30"
: "bg-green-50/30"
}`}
>
{message.role === "user" ? (
<div className="text-wrapper">
{(() => {
const content = message.content;
// Find section boundaries by looking for specific markers
const companySpecificStartIndex =
content.indexOf(
"--- COMPANY-SPECIFIC INSTRUCTIONS ---"
);
const companySpecificEndIndex =
content.indexOf(
"--- END COMPANY-SPECIFIC INSTRUCTIONS ---"
);
const taxonomyStartIndex =
content.indexOf(
"All Available Categories:"
);
const taxonomyFallbackStartIndex =
content.indexOf(
"Available Categories:"
);
const actualTaxonomyStartIndex =
taxonomyStartIndex >= 0
? taxonomyStartIndex
: taxonomyFallbackStartIndex;
const productDataStartIndex =
content.indexOf(
"----------Here is the product data to validate----------"
);
// If we can't find any markers, just return the content as-is
if (
actualTaxonomyStartIndex < 0 &&
productDataStartIndex < 0 &&
companySpecificStartIndex < 0
) {
return content;
}
// Determine section indices
let generalEndIndex = content.length;
if (companySpecificStartIndex >= 0) {
generalEndIndex =
companySpecificStartIndex;
} else if (
actualTaxonomyStartIndex >= 0
) {
generalEndIndex =
actualTaxonomyStartIndex;
} else if (productDataStartIndex >= 0) {
generalEndIndex = productDataStartIndex;
}
// Determine where taxonomy starts
let taxonomyEndIndex = content.length;
if (productDataStartIndex >= 0) {
taxonomyEndIndex =
productDataStartIndex;
}
// Segments to render with appropriate styling
const segments = [];
// General section (beginning to company/taxonomy/product)
if (generalEndIndex > 0) {
segments.push(
<div
id="general-section"
key="general"
className="border-l-4 border-green-500 pl-4 py-0 my-1"
>
<div className="text-xs font-semibold text-green-700 mb-2">
General Prompt
</div>
<pre className="whitespace-pre-wrap">
{content.substring(
0,
generalEndIndex
)}
</pre>
</div>
);
}
// Company-specific section if present
if (
companySpecificStartIndex >= 0 &&
companySpecificEndIndex >= 0
) {
segments.push(
<div
id="company-section"
key="company"
className="border-l-4 border-blue-500 pl-4 py-0 my-1"
>
<div className="text-xs font-semibold text-blue-700 mb-2">
Company-Specific Instructions
</div>
<pre className="whitespace-pre-wrap">
{content.substring(
companySpecificStartIndex,
companySpecificEndIndex +
"--- END COMPANY-SPECIFIC INSTRUCTIONS ---"
.length
)}
</pre>
</div>
);
}
// Taxonomy section
if (actualTaxonomyStartIndex >= 0) {
const taxEnd = taxonomyEndIndex;
segments.push(
<div
id="taxonomy-section"
key="taxonomy"
className="border-l-4 border-amber-500 pl-4 py-0 my-1"
>
<div className="text-xs font-semibold text-amber-700 mb-2">
Taxonomy Data
</div>
<pre className="whitespace-pre-wrap">
{content.substring(
actualTaxonomyStartIndex,
taxEnd
)}
</pre>
</div>
);
}
// Product data section
if (productDataStartIndex >= 0) {
segments.push(
<div
id="product-section"
key="product"
className="border-l-4 border-pink-500 pl-4 py-0 my-1"
>
<div className="text-xs font-semibold text-pink-700 mb-2">
Product Data
</div>
<pre className="whitespace-pre-wrap">
{content.substring(
productDataStartIndex
)}
</pre>
</div>
);
}
return <>{segments}</>;
})()}
</div>
) : (
<pre className="whitespace-pre-wrap">
{message.content}
</pre>
)}
</Code>
</div>
<Code className={`whitespace-pre-wrap p-4 break-normal max-w-full ${
message.role === 'system'
? 'bg-purple-50/30'
: 'bg-green-50/30'
}`}>
{message.role === 'user' ? (
<div className="text-wrapper">
{(() => {
const content = message.content;
// Find section boundaries by looking for specific markers
const companySpecificStartIndex = content.indexOf('--- COMPANY-SPECIFIC INSTRUCTIONS ---');
const companySpecificEndIndex = content.indexOf('--- END COMPANY-SPECIFIC INSTRUCTIONS ---');
const taxonomyStartIndex = content.indexOf('All Available Categories:');
const taxonomyFallbackStartIndex = content.indexOf('Available Categories:');
const actualTaxonomyStartIndex = taxonomyStartIndex >= 0 ? taxonomyStartIndex : taxonomyFallbackStartIndex;
const productDataStartIndex = content.indexOf('----------Here is the product data to validate----------');
// If we can't find any markers, just return the content as-is
if (actualTaxonomyStartIndex < 0 && productDataStartIndex < 0 && companySpecificStartIndex < 0) {
return content;
}
// Determine section indices
let generalEndIndex = content.length;
if (companySpecificStartIndex >= 0) {
generalEndIndex = companySpecificStartIndex;
} else if (actualTaxonomyStartIndex >= 0) {
generalEndIndex = actualTaxonomyStartIndex;
} else if (productDataStartIndex >= 0) {
generalEndIndex = productDataStartIndex;
}
// Determine where taxonomy starts
let taxonomyEndIndex = content.length;
if (productDataStartIndex >= 0) {
taxonomyEndIndex = productDataStartIndex;
}
// Segments to render with appropriate styling
const segments = [];
// General section (beginning to company/taxonomy/product)
if (generalEndIndex > 0) {
segments.push(
<div id="general-section" key="general" className="border-l-4 border-green-500 pl-4 py-0 my-1">
<div className="text-xs font-semibold text-green-700 mb-2">
General Prompt
</div>
<pre className="whitespace-pre-wrap">
{content.substring(0, generalEndIndex)}
</pre>
</div>
);
}
// Company-specific section if present
if (companySpecificStartIndex >= 0 && companySpecificEndIndex >= 0) {
segments.push(
<div id="company-section" key="company" className="border-l-4 border-blue-500 pl-4 py-0 my-1">
<div className="text-xs font-semibold text-blue-700 mb-2">
Company-Specific Instructions
</div>
<pre className="whitespace-pre-wrap">
{content.substring(companySpecificStartIndex, companySpecificEndIndex + '--- END COMPANY-SPECIFIC INSTRUCTIONS ---'.length)}
</pre>
</div>
);
}
// Taxonomy section
if (actualTaxonomyStartIndex >= 0) {
const taxEnd = taxonomyEndIndex;
segments.push(
<div id="taxonomy-section" key="taxonomy" className="border-l-4 border-amber-500 pl-4 py-0 my-1">
<div className="text-xs font-semibold text-amber-700 mb-2">
Taxonomy Data
</div>
<pre className="whitespace-pre-wrap">
{content.substring(actualTaxonomyStartIndex, taxEnd)}
</pre>
</div>
);
}
// Product data section
if (productDataStartIndex >= 0) {
segments.push(
<div id="product-section" key="product" className="border-l-4 border-pink-500 pl-4 py-0 my-1">
<div className="text-xs font-semibold text-pink-700 mb-2">
Product Data
</div>
<pre className="whitespace-pre-wrap">
{content.substring(productDataStartIndex)}
</pre>
</div>
);
}
return <>{segments}</>;
})()}
</div>
) : (
<pre className="whitespace-pre-wrap">{message.content}</pre>
)}
</Code>
</div>
))}
)
)}
</ScrollArea>
</div>
) : (
@@ -566,14 +738,14 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
}
>
<DialogContent className="max-w-4xl">
<DialogContent className="max-w-6xl w-[90vw]">
<DialogHeader>
<DialogTitle>AI Validation Results</DialogTitle>
<DialogDescription>
Review the changes and warnings suggested by the AI
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh]">
<ScrollArea className="max-h-[70vh]">
{aiValidationDetails.changeDetails &&
aiValidationDetails.changeDetails.length > 0 ? (
<div className="mb-6 space-y-6">
@@ -595,10 +767,16 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]">Field</TableHead>
<TableHead>Original Value</TableHead>
<TableHead>Corrected Value</TableHead>
<TableHead className="text-right">Action</TableHead>
<TableHead className="">Field</TableHead>
<TableHead className="w-[35%]">
Original Value
</TableHead>
<TableHead className="w-[35%]">
Corrected Value
</TableHead>
<TableHead className="text-right">
Accept Changes?
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -609,7 +787,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
const fieldLabel = field
? field.label
: change.field;
const isReverted = isChangeReverted(
const isReverted = isChangeLocallyReverted(
product.productIndex,
change.field
);
@@ -632,7 +810,6 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
dangerouslySetInnerHTML={{
__html: originalHtml,
}}
className={isReverted ? "font-medium" : ""}
/>
</TableCell>
<TableCell>
@@ -640,36 +817,46 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
dangerouslySetInnerHTML={{
__html: correctedHtml,
}}
className={!isReverted ? "font-medium" : ""}
/>
</TableCell>
<TableCell className="text-right">
<div className="mt-2">
{isReverted ? (
<Button
variant="ghost"
size="sm"
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
disabled
>
<CheckIcon className="w-4 h-4 mr-1" />
Reverted
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
// Call the revert function directly
revertAiChange(
product.productIndex,
change.field
);
}}
>
Revert Change
</Button>
)}
<TableCell className="text-right align-top">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
// Toggle to Accepted state if currently rejected
toggleChangeAcceptance(
product.productIndex,
change.field
);
}}
className={
!isReverted
? "bg-green-100 text-green-600 border-green-300 flex items-center"
: "border-gray-200 text-gray-600 hover:bg-green-50 hover:text-green-600 hover:border-green-200 flex items-center"
}
>
<CheckIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
// Toggle to Rejected state if currently accepted
toggleChangeAcceptance(
product.productIndex,
change.field
);
}}
className={
isReverted
? "bg-red-100 text-red-600 border-red-300 flex items-center"
: "border-gray-200 text-gray-600 hover:bg-red-50 hover:text-red-600 hover:border-red-200 flex items-center"
}
>
<XIcon className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>