Layout/style tweaks, fix performance metrics settings page

This commit is contained in:
2025-03-23 22:01:41 -04:00
parent ce75496770
commit 1fcbf54989
10 changed files with 697 additions and 435 deletions

View File

@@ -15,7 +15,6 @@ import Forecasting from "@/pages/Forecasting";
import { Vendors } from '@/pages/Vendors'; import { Vendors } from '@/pages/Vendors';
import { Categories } from '@/pages/Categories'; import { Categories } from '@/pages/Categories';
import { Import } from '@/pages/Import'; import { Import } from '@/pages/Import';
import { AiValidationDebug } from "@/pages/AiValidationDebug"
import { AuthProvider } from './contexts/AuthContext'; import { AuthProvider } from './contexts/AuthContext';
import { Protected } from './components/auth/Protected'; import { Protected } from './components/auth/Protected';
import { FirstAccessiblePage } from './components/auth/FirstAccessiblePage'; import { FirstAccessiblePage } from './components/auth/FirstAccessiblePage';
@@ -129,11 +128,6 @@ function App() {
<Forecasting /> <Forecasting />
</Protected> </Protected>
} /> } />
<Route path="/ai-validation/debug" element={
<Protected page="ai_validation_debug">
<AiValidationDebug />
</Protected>
} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -160,6 +160,7 @@ export function AppSidebar() {
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarSeparator />
<SidebarFooter> <SidebarFooter>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>

View File

@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { AiValidationDialogs } from '../../../components/AiValidationDialogs';
import { Product } from '../../../types/product';
import { config } from '../../../config';
interface CurrentPrompt {
isOpen: boolean;
prompt: string;
isLoading: boolean;
debugData?: {
taxonomyStats: {
categories: number;
themes: number;
colors: number;
taxCodes: number;
sizeCategories: number;
suppliers: number;
companies: number;
artists: number;
} | null;
basePrompt: string;
sampleFullPrompt: string;
promptLength: number;
estimatedProcessingTime?: {
seconds: number | null;
sampleCount: number;
};
};
}
const ValidationStepNew: React.FC = () => {
const [aiValidationProgress, setAiValidationProgress] = useState(0);
const [aiValidationDetails, setAiValidationDetails] = useState('');
const [currentPrompt, setCurrentPrompt] = useState<CurrentPrompt>({
isOpen: false,
prompt: '',
isLoading: true,
});
const [isChangeReverted, setIsChangeReverted] = useState(false);
const [fieldData, setFieldData] = useState<Product[]>([]);
const showCurrentPrompt = async (products: Product[]) => {
setCurrentPrompt((prev) => ({ ...prev, isOpen: true, isLoading: true }));
try {
// Get the prompt
const promptResponse = await fetch(`${config.apiUrl}/ai-validation/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ products })
});
if (!promptResponse.ok) {
throw new Error('Failed to fetch AI prompt');
}
const promptData = await promptResponse.json();
// Get the debug data in the same request or as a separate request
const debugResponse = await fetch(`${config.apiUrl}/ai-validation/debug-info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: promptData.prompt })
});
let debugData;
if (debugResponse.ok) {
debugData = await debugResponse.json();
} else {
// If debug-info fails, use a fallback to get taxonomy stats
const fallbackResponse = await fetch(`${config.apiUrl}/ai-validation/debug`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ products: [products[0]] }) // Use first product for stats
});
if (fallbackResponse.ok) {
debugData = await fallbackResponse.json();
// Set promptLength correctly from the actual prompt
debugData.promptLength = promptData.prompt.length;
}
}
setCurrentPrompt((prev) => ({
...prev,
prompt: promptData.prompt,
isLoading: false,
debugData: debugData
}));
} catch (error) {
console.error('Error fetching prompt:', error);
setCurrentPrompt((prev) => ({
...prev,
prompt: 'Error loading prompt',
isLoading: false
}));
}
};
const revertAiChange = () => {
setIsChangeReverted(true);
};
const getFieldDisplayValueWithHighlight = (value: string, highlight: string) => {
// Implementation of getFieldDisplayValueWithHighlight
};
return (
<div>
<AiValidationDialogs
aiValidationProgress={aiValidationProgress}
aiValidationDetails={aiValidationDetails}
currentPrompt={currentPrompt}
setAiValidationProgress={setAiValidationProgress}
setAiValidationDetails={setAiValidationDetails}
setCurrentPrompt={setCurrentPrompt}
revertAiChange={revertAiChange}
isChangeReverted={isChangeReverted}
getFieldDisplayValueWithHighlight={getFieldDisplayValueWithHighlight}
fields={fieldData}
debugData={currentPrompt.debugData}
/>
</div>
);
};
export default ValidationStepNew;

View File

@@ -1,23 +1,72 @@
import React from 'react'; import React, { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import {
import { ScrollArea } from '@/components/ui/scroll-area'; Dialog,
import { Button } from '@/components/ui/button'; DialogContent,
import { Loader2, CheckIcon } from 'lucide-react'; DialogHeader,
import { Code } from '@/components/ui/code'; DialogTitle,
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; DialogDescription,
import { AiValidationDetails, AiValidationProgress, CurrentPrompt } from '../hooks/useAiValidation'; } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Loader2, CheckIcon } from "lucide-react";
import { Code } from "@/components/ui/code";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
AiValidationDetails,
AiValidationProgress,
CurrentPrompt,
} from "../hooks/useAiValidation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface TaxonomyStats {
categories: number;
themes: number;
colors: number;
taxCodes: number;
sizeCategories: number;
suppliers: number;
companies: number;
artists: number;
}
interface DebugData {
taxonomyStats: TaxonomyStats | null;
basePrompt: string;
sampleFullPrompt: string;
promptLength: number;
estimatedProcessingTime?: {
seconds: number | null;
sampleCount: number;
};
}
interface AiValidationDialogsProps { interface AiValidationDialogsProps {
aiValidationProgress: AiValidationProgress; aiValidationProgress: AiValidationProgress;
aiValidationDetails: AiValidationDetails; aiValidationDetails: AiValidationDetails;
currentPrompt: CurrentPrompt; currentPrompt: CurrentPrompt;
setAiValidationProgress: React.Dispatch<React.SetStateAction<AiValidationProgress>>; setAiValidationProgress: React.Dispatch<
setAiValidationDetails: React.Dispatch<React.SetStateAction<AiValidationDetails>>; React.SetStateAction<AiValidationProgress>
>;
setAiValidationDetails: React.Dispatch<
React.SetStateAction<AiValidationDetails>
>;
setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>; setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>;
revertAiChange: (productIndex: number, fieldKey: string) => void; revertAiChange: (productIndex: number, fieldKey: string) => void;
isChangeReverted: (productIndex: number, fieldKey: string) => boolean; isChangeReverted: (productIndex: number, fieldKey: string) => boolean;
getFieldDisplayValueWithHighlight: (fieldKey: string, originalValue: any, correctedValue: any) => { originalHtml: string, correctedHtml: string }; getFieldDisplayValueWithHighlight: (
fieldKey: string,
originalValue: any,
correctedValue: any
) => { originalHtml: string; correctedHtml: string };
fields: readonly any[]; fields: readonly any[];
debugData?: DebugData;
} }
export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
@@ -30,31 +79,182 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
revertAiChange, revertAiChange,
isChangeReverted, isChangeReverted,
getFieldDisplayValueWithHighlight, getFieldDisplayValueWithHighlight,
fields fields,
debugData,
}) => { }) => {
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
// Format time helper
const 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`;
}
};
// Calculate token costs
const calculateTokenCost = (promptLength: number): number => {
const estimatedTokens = Math.round(promptLength / 4);
return (estimatedTokens / 1_000_000) * costPerMillionTokens * 100; // In cents
};
// Use the prompt length from the current prompt
const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0;
return ( return (
<> <>
{/* Current Prompt Dialog */} {/* Current Prompt Dialog with Debug Info */}
<Dialog <Dialog
open={currentPrompt.isOpen} open={currentPrompt.isOpen}
onOpenChange={(open) => setCurrentPrompt(prev => ({ ...prev, isOpen: open }))} onOpenChange={(open) =>
setCurrentPrompt((prev) => ({ ...prev, isOpen: open }))
}
> >
<DialogContent className="max-w-4xl h-[80vh]"> <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle>Current AI Prompt</DialogTitle> <DialogTitle>Current AI Prompt</DialogTitle>
<DialogDescription> <DialogDescription>
This is the exact prompt that would be sent to the AI for validation This is the current prompt that would be sent to the AI for
validation
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="flex-1">
{currentPrompt.isLoading ? ( <div className="flex flex-col h-[calc(90vh-120px)] overflow-hidden">
<div className="flex items-center justify-center h-full"> {/* Debug Information Section */}
<Loader2 className="h-8 w-8 animate-spin" /> <div className="mb-4 flex-shrink-0">
</div> {currentPrompt.isLoading ? (
) : ( <div className="flex justify-center items-center h-[100px]"></div>
<Code className="whitespace-pre-wrap p-4">{currentPrompt.prompt}</Code> ) : (
)} <div className="grid grid-cols-3 gap-4">
</ScrollArea> <Card className="py-2">
<CardHeader className="py-2">
<CardTitle className="text-base">Prompt Length</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
<div className="text-sm">
<span className="text-muted-foreground">
Characters:
</span>{" "}
<span className="font-semibold">{promptLength}</span>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Tokens:</span>{" "}
<span className="font-semibold">
~{Math.round(promptLength / 4)}
</span>
</div>
</div>
</CardContent>
</Card>
<Card className="py-2">
<CardHeader className="py-2">
<CardTitle className="text-base">Cost Estimate</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center">
<label
htmlFor="costPerMillion"
className="text-sm text-muted-foreground"
>
$
</label>
<input
id="costPerMillion"
className="w-[40px] px-1 border rounded-md text-sm"
defaultValue={costPerMillionTokens.toFixed(2)}
onChange={(e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value)) {
setCostPerMillionTokens(value);
}
}}
/>
<label
htmlFor="costPerMillion"
className="text-sm text-muted-foreground ml-1"
>
per million input tokens
</label>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Cost:</span>{" "}
<span className="font-semibold">
{calculateTokenCost(promptLength).toFixed(1)}¢
</span>
</div>
</div>
</CardContent>
</Card>
<Card className="py-2">
<CardHeader className="py-2">
<CardTitle className="text-base">
Processing Time
</CardTitle>
</CardHeader>
<CardContent className="py-2">
<div className="flex flex-col space-y-2">
{debugData?.estimatedProcessingTime ? (
debugData.estimatedProcessingTime.seconds ? (
<>
<div className="text-sm">
<span className="text-muted-foreground">
Estimated time:
</span>{" "}
<span className="font-semibold">
{formatTime(
debugData.estimatedProcessingTime.seconds
)}
</span>
</div>
<div className="text-xs text-muted-foreground">
Based on{" "}
{debugData.estimatedProcessingTime.sampleCount}{" "}
similar validation
{debugData.estimatedProcessingTime
.sampleCount !== 1
? "s"
: ""}
</div>
</>
) : (
<div className="text-sm text-muted-foreground">
No historical data available for this prompt size
</div>
)
) : (
<div className="text-sm text-muted-foreground">
No processing time data available
</div>
)}
</div>
</CardContent>
</Card>
</div>
)}
</div>
{/* Prompt Section */}
<div className="flex-1 min-h-0">
<ScrollArea className="h-full w-full">
{currentPrompt.isLoading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : (
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
{currentPrompt.prompt}
</Code>
)}
</ScrollArea>
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -64,7 +264,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
onOpenChange={(open) => { onOpenChange={(open) => {
// Only allow closing if validation failed // Only allow closing if validation failed
if (!open && aiValidationProgress.step === -1) { if (!open && aiValidationProgress.step === -1) {
setAiValidationProgress(prev => ({ ...prev, isOpen: false })); setAiValidationProgress((prev) => ({ ...prev, isOpen: false }));
} }
}} }}
> >
@@ -79,14 +279,25 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<div <div
className="h-full bg-primary transition-all duration-500" className="h-full bg-primary transition-all duration-500"
style={{ style={{
width: `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`, width: `${
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined aiValidationProgress.progressPercent ??
Math.round((aiValidationProgress.step / 5) * 100)
}%`,
backgroundColor:
aiValidationProgress.step === -1
? "var(--destructive)"
: undefined,
}} }}
/> />
</div> </div>
</div> </div>
<div className="text-sm text-muted-foreground w-12 text-right"> <div className="text-sm text-muted-foreground w-12 text-right">
{aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`} {aiValidationProgress.step === -1
? "❌"
: `${
aiValidationProgress.progressPercent ??
Math.round((aiValidationProgress.step / 5) * 100)
}%`}
</div> </div>
</div> </div>
<p className="text-center text-sm text-muted-foreground"> <p className="text-center text-sm text-muted-foreground">
@@ -94,32 +305,43 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</p> </p>
{(() => { {(() => {
// Only show time remaining if we have an estimate and are in progress // Only show time remaining if we have an estimate and are in progress
return aiValidationProgress.estimatedSeconds && return (
aiValidationProgress.estimatedSeconds &&
aiValidationProgress.elapsedSeconds !== undefined && aiValidationProgress.elapsedSeconds !== undefined &&
aiValidationProgress.step > 0 && aiValidationProgress.step > 0 &&
aiValidationProgress.step < 5 && ( aiValidationProgress.step < 5 && (
<div className="text-center text-sm"> <div className="text-center text-sm">
{(() => { {(() => {
// Calculate time remaining using the elapsed seconds // Calculate time remaining using the elapsed seconds
const elapsedSeconds = aiValidationProgress.elapsedSeconds; const elapsedSeconds =
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; aiValidationProgress.elapsedSeconds;
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds); const totalEstimatedSeconds =
aiValidationProgress.estimatedSeconds;
const remainingSeconds = Math.max(
0,
totalEstimatedSeconds - elapsedSeconds
);
// Format time remaining // Format time remaining
if (remainingSeconds < 60) { if (remainingSeconds < 60) {
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`; return `Approximately ${Math.round(
} else { remainingSeconds
const minutes = Math.floor(remainingSeconds / 60); )} seconds remaining`;
const seconds = Math.round(remainingSeconds % 60); } else {
return `Approximately ${minutes}m ${seconds}s remaining`; 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 {aiValidationProgress.promptLength && (
</p> <p className="mt-1 text-xs text-muted-foreground">
)} Prompt length:{" "}
</div> {aiValidationProgress.promptLength.toLocaleString()}{" "}
characters
</p>
)}
</div>
)
); );
})()} })()}
</div> </div>
@@ -129,7 +351,9 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
{/* AI Validation Results Dialog */} {/* AI Validation Results Dialog */}
<Dialog <Dialog
open={aiValidationDetails.isOpen} open={aiValidationDetails.isOpen}
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))} onOpenChange={(open) =>
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
}
> >
<DialogContent className="max-w-4xl"> <DialogContent className="max-w-4xl">
<DialogHeader> <DialogHeader>
@@ -139,13 +363,18 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="max-h-[60vh]"> <ScrollArea className="max-h-[60vh]">
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? ( {aiValidationDetails.changeDetails &&
aiValidationDetails.changeDetails.length > 0 ? (
<div className="mb-6 space-y-6"> <div className="mb-6 space-y-6">
<h3 className="font-semibold text-lg">Detailed Changes:</h3> <h3 className="font-semibold text-lg">Detailed Changes:</h3>
{aiValidationDetails.changeDetails.map((product, i) => { {aiValidationDetails.changeDetails.map((product, i) => {
// Find the title change if it exists // Find the title change if it exists
const titleChange = product.changes.find(c => c.field === 'title'); const titleChange = product.changes.find(
const titleValue = titleChange ? titleChange.corrected : product.title; (c) => c.field === "title"
);
const titleValue = titleChange
? titleChange.corrected
: product.title;
return ( return (
<div key={`product-${i}`} className="border rounded-md p-4"> <div key={`product-${i}`} className="border rounded-md p-4">
@@ -163,29 +392,43 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{product.changes.map((change, j) => { {product.changes.map((change, j) => {
const field = fields.find(f => f.key === change.field); const field = fields.find(
const fieldLabel = field ? field.label : change.field; (f) => f.key === change.field
const isReverted = isChangeReverted(product.productIndex, change.field); );
const fieldLabel = field
? field.label
: change.field;
const isReverted = isChangeReverted(
product.productIndex,
change.field
);
// Get highlighted differences // Get highlighted differences
const { originalHtml, correctedHtml } = getFieldDisplayValueWithHighlight( const { originalHtml, correctedHtml } =
change.field, getFieldDisplayValueWithHighlight(
change.original, change.field,
change.corrected change.original,
); change.corrected
);
return ( return (
<TableRow key={`change-${j}`}> <TableRow key={`change-${j}`}>
<TableCell className="font-medium">{fieldLabel}</TableCell> <TableCell className="font-medium">
{fieldLabel}
</TableCell>
<TableCell> <TableCell>
<div <div
dangerouslySetInnerHTML={{ __html: originalHtml }} dangerouslySetInnerHTML={{
__html: originalHtml,
}}
className={isReverted ? "font-medium" : ""} className={isReverted ? "font-medium" : ""}
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<div <div
dangerouslySetInnerHTML={{ __html: correctedHtml }} dangerouslySetInnerHTML={{
__html: correctedHtml,
}}
className={!isReverted ? "font-medium" : ""} className={!isReverted ? "font-medium" : ""}
/> />
</TableCell> </TableCell>
@@ -207,7 +450,10 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
size="sm" size="sm"
onClick={() => { onClick={() => {
// Call the revert function directly // Call the revert function directly
revertAiChange(product.productIndex, change.field); revertAiChange(
product.productIndex,
change.field
);
}} }}
> >
Revert Change Revert Change
@@ -226,12 +472,17 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</div> </div>
) : ( ) : (
<div className="py-8 text-center text-muted-foreground"> <div className="py-8 text-center text-muted-foreground">
{aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? ( {aiValidationDetails.warnings &&
aiValidationDetails.warnings.length > 0 ? (
<div> <div>
<p className="mb-4">No changes were made, but the AI provided some warnings:</p> <p className="mb-4">
No changes were made, but the AI provided some warnings:
</p>
<ul className="list-disc pl-8 text-left"> <ul className="list-disc pl-8 text-left">
{aiValidationDetails.warnings.map((warning, i) => ( {aiValidationDetails.warnings.map((warning, i) => (
<li key={`warning-${i}`} className="mb-2">{warning}</li> <li key={`warning-${i}`} className="mb-2">
{warning}
</li>
))} ))}
</ul> </ul>
</div> </div>

View File

@@ -1200,6 +1200,7 @@ const ValidationContainer = <T extends string>({
isChangeReverted={aiValidation.isChangeReverted} isChangeReverted={aiValidation.isChangeReverted}
getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight} getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight}
fields={fields} fields={fields}
debugData={aiValidation.currentPrompt.debugData}
/> />
{/* Product Search Dialog */} {/* Product Search Dialog */}

View File

@@ -42,6 +42,25 @@ export interface CurrentPrompt {
isOpen: boolean; isOpen: boolean;
prompt: string | null; prompt: string | null;
isLoading: boolean; isLoading: boolean;
debugData?: {
taxonomyStats: {
categories: number;
themes: number;
colors: number;
taxCodes: number;
sizeCategories: number;
suppliers: number;
companies: number;
artists: number;
} | null;
basePrompt: string;
sampleFullPrompt: string;
promptLength: number;
estimatedProcessingTime?: {
seconds: number | null;
sampleCount: number;
};
};
} }
// Declare global interface for the timer // Declare global interface for the timer
@@ -250,7 +269,11 @@ export const useAiValidation = <T extends string>(
// Function to show current prompt // Function to show current prompt
const showCurrentPrompt = useCallback(async () => { const showCurrentPrompt = useCallback(async () => {
try { try {
setCurrentPrompt(prev => ({ ...prev, isLoading: true, isOpen: true })); setCurrentPrompt(prev => ({
...prev,
isLoading: true,
isOpen: true
}));
// Debug log the data being sent // Debug log the data being sent
console.log('Sending products data:', { console.log('Sending products data:', {
@@ -272,7 +295,7 @@ export const useAiValidation = <T extends string>(
}); });
// Use POST to send products in request body // Use POST to send products in request body
const response = await fetch(`${getApiUrl()}/ai-validation/debug`, { const response = await fetch(`${await getApiUrl()}/ai-validation/debug`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -294,7 +317,14 @@ export const useAiValidation = <T extends string>(
setCurrentPrompt(prev => ({ setCurrentPrompt(prev => ({
...prev, ...prev,
prompt: promptContent, prompt: promptContent,
isLoading: false isLoading: false,
debugData: {
taxonomyStats: result.taxonomyStats || null,
basePrompt: result.basePrompt || '',
sampleFullPrompt: result.sampleFullPrompt || '',
promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
estimatedProcessingTime: result.estimatedProcessingTime
}
})); }));
} else { } else {
throw new Error('No prompt returned from server'); throw new Error('No prompt returned from server');

View File

@@ -133,8 +133,9 @@ export function PerformanceMetrics() {
} }
}; };
function getCategoryName(_cat_id: number): import("react").ReactNode { function getCategoryName(cat_id: number): import("react").ReactNode {
throw new Error('Function not implemented.'); // Simple implementation that just returns the ID as a string
return `Category ${cat_id}`;
} }
return ( return (
@@ -217,15 +218,19 @@ export function PerformanceMetrics() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{abcConfigs.map((config) => ( {abcConfigs && abcConfigs.length > 0 ? abcConfigs.map((config) => (
<TableRow key={`${config.cat_id}-${config.vendor}`}> <TableRow key={`${config.cat_id}-${config.vendor}`}>
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell> <TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
<TableCell>{config.vendor || 'All Vendors'}</TableCell> <TableCell>{config.vendor || 'All Vendors'}</TableCell>
<TableCell className="text-right">{config.a_threshold}%</TableCell> <TableCell className="text-right">{config.a_threshold !== undefined ? `${config.a_threshold}%` : '0%'}</TableCell>
<TableCell className="text-right">{config.b_threshold}%</TableCell> <TableCell className="text-right">{config.b_threshold !== undefined ? `${config.b_threshold}%` : '0%'}</TableCell>
<TableCell className="text-right">{config.classification_period_days}</TableCell> <TableCell className="text-right">{config.classification_period_days || 0}</TableCell>
</TableRow> </TableRow>
))} )) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-4">No ABC configurations available</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
<Button onClick={handleUpdateABCConfig}> <Button onClick={handleUpdateABCConfig}>
@@ -253,14 +258,26 @@ export function PerformanceMetrics() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{turnoverConfigs.map((config) => ( {turnoverConfigs && turnoverConfigs.length > 0 ? turnoverConfigs.map((config) => (
<TableRow key={`${config.cat_id}-${config.vendor}`}> <TableRow key={`${config.cat_id}-${config.vendor}`}>
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell> <TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
<TableCell>{config.vendor || 'All Vendors'}</TableCell> <TableCell>{config.vendor || 'All Vendors'}</TableCell>
<TableCell className="text-right">{config.calculation_period_days}</TableCell> <TableCell className="text-right">{config.calculation_period_days}</TableCell>
<TableCell className="text-right">{config.target_rate.toFixed(2)}</TableCell> <TableCell className="text-right">
{config.target_rate !== undefined && config.target_rate !== null
? (typeof config.target_rate === 'number'
? config.target_rate.toFixed(2)
: (isNaN(parseFloat(String(config.target_rate)))
? '0.00'
: parseFloat(String(config.target_rate)).toFixed(2)))
: '0.00'}
</TableCell>
</TableRow> </TableRow>
))} )) : (
<TableRow>
<TableCell colSpan={4} className="text-center py-4">No turnover configurations available</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
<Button onClick={handleUpdateTurnoverConfig}> <Button onClick={handleUpdateTurnoverConfig}>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
@@ -32,7 +32,7 @@ interface PermissionCategory {
} }
export function UserManagement() { export function UserManagement() {
const { token, fetchCurrentUser, user } = useContext(AuthContext); const { token, fetchCurrentUser } = useContext(AuthContext);
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [selectedUser, setSelectedUser] = useState<User | null>(null); const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isAddingUser, setIsAddingUser] = useState(false); const [isAddingUser, setIsAddingUser] = useState(false);
@@ -199,7 +199,7 @@ export function UserManagement() {
// Check if permissions are objects (from the form) and convert to IDs for the API // Check if permissions are objects (from the form) and convert to IDs for the API
if (userData.permissions.length > 0 && typeof userData.permissions[0] === 'object') { if (userData.permissions.length > 0 && typeof userData.permissions[0] === 'object') {
// The backend expects permission IDs, not just the code strings // The backend expects permission IDs, not just the code strings
formattedUserData.permissions = userData.permissions.map(p => p.id); formattedUserData.permissions = userData.permissions.map((p: { id: any; }) => p.id);
} }
} }
@@ -334,9 +334,6 @@ export function UserManagement() {
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<div> <div>
<CardTitle>User Management</CardTitle> <CardTitle>User Management</CardTitle>
<CardDescription>
Manage users and their permissions
</CardDescription>
</div> </div>
<Button onClick={handleAddUser}> <Button onClick={handleAddUser}>
Add User Add User

View File

@@ -1,200 +0,0 @@
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Code } from "@/components/ui/code"
import { useToast } from "@/hooks/use-toast"
import { Loader2 } from "lucide-react"
import config from "@/config"
interface TaxonomyStats {
categories: number
themes: number
colors: number
taxCodes: number
sizeCategories: number
suppliers: number
companies: number
artists: number
}
interface DebugData {
taxonomyStats: TaxonomyStats | null
basePrompt: string
sampleFullPrompt: string
promptLength: number
estimatedProcessingTime?: {
seconds: number | null
sampleCount: number
}
}
export function AiValidationDebug() {
const [isLoading, setIsLoading] = useState(false)
const [debugData, setDebugData] = useState<DebugData | null>(null)
const { toast } = useToast()
const fetchDebugData = async () => {
setIsLoading(true)
try {
// Use a sample product to avoid loading full taxonomy
const sampleProduct = {
title: "Sample Product",
description: "A sample product for testing",
SKU: "SAMPLE-001",
price: "9.99",
cost_each: "5.00",
qty_per_unit: "1",
case_qty: "12"
}
const response = await fetch(`${config.apiUrl}/ai-validation/debug`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products: [sampleProduct] })
})
if (!response.ok) {
throw new Error('Failed to fetch debug data')
}
const data = await response.json()
setDebugData(data)
} catch (error) {
console.error('Error fetching debug data:', error)
toast({
variant: "destructive",
title: "Error",
description: error instanceof Error ? error.message : "Failed to fetch debug data"
})
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchDebugData()
}, [])
return (
<div className="container mx-auto py-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight">AI Validation Debug</h1>
<div className="space-x-4">
<Button
variant="outline"
onClick={fetchDebugData}
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Refresh Data
</Button>
</div>
</div>
{debugData && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Taxonomy Stats</CardTitle>
</CardHeader>
<CardContent>
{debugData.taxonomyStats ? (
<div className="space-y-2">
<div>Categories: {debugData.taxonomyStats.categories}</div>
<div>Themes: {debugData.taxonomyStats.themes}</div>
<div>Colors: {debugData.taxonomyStats.colors}</div>
<div>Tax Codes: {debugData.taxonomyStats.taxCodes}</div>
<div>Size Categories: {debugData.taxonomyStats.sizeCategories}</div>
<div>Suppliers: {debugData.taxonomyStats.suppliers}</div>
<div>Companies: {debugData.taxonomyStats.companies}</div>
<div>Artists: {debugData.taxonomyStats.artists}</div>
</div>
) : (
<div>No taxonomy data available</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Prompt Length</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<div>Characters: {debugData.promptLength}</div>
<div>Tokens (est.): ~{Math.round(debugData.promptLength / 4)}</div>
</div>
<div className="space-y-2">
<label htmlFor="costPerMillion" className="text-sm text-muted-foreground">
Cost per million tokens ($)
</label>
<input
id="costPerMillion"
type="number"
className="w-full px-3 py-2 border rounded-md"
defaultValue="2.50"
onChange={(e) => {
const costPerMillion = parseFloat(e.target.value)
if (!isNaN(costPerMillion)) {
const tokens = Math.round(debugData.promptLength / 4)
const cost = (tokens / 1_000_000) * costPerMillion * 100 // Convert to cents
const costElement = document.getElementById('tokenCost')
if (costElement) {
costElement.textContent = cost.toFixed(1)
}
}
}}
/>
<div className="text-sm">
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>
<Card className="col-span-full">
<CardHeader>
<CardTitle>Full Sample Prompt</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px] w-full rounded-md border p-4">
<Code className="whitespace-pre-wrap">{debugData.sampleFullPrompt}</Code>
</ScrollArea>
</CardContent>
</Card>
</div>
)}
</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`;
}
}

View File

@@ -10,17 +10,52 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
import { Protected } from "@/components/auth/Protected"; import { Protected } from "@/components/auth/Protected";
import { useContext, useMemo } from "react"; import { useContext, useMemo } from "react";
import { AuthContext } from "@/contexts/AuthContext"; import { AuthContext } from "@/contexts/AuthContext";
import { Separator } from "@/components/ui/separator";
// Define available settings tabs with their permission requirements // Define types for settings structure
const SETTINGS_TABS = [ interface SettingsTab {
{ id: "data-management", permission: "settings:data_management", label: "Data Management" }, id: string;
{ id: "stock-management", permission: "settings:stock_management", label: "Stock Management" }, permission: string;
{ id: "performance-metrics", permission: "settings:performance_metrics", label: "Performance Metrics" }, label: string;
{ id: "calculation-settings", permission: "settings:calculation_settings", label: "Calculation Settings" }, }
{ id: "templates", permission: "settings:templates", label: "Template Management" },
{ id: "user-management", permission: "settings:user_management", label: "User Management" } interface SettingsGroup {
id: string;
label: string;
tabs: SettingsTab[];
}
// Define available settings tabs with their permission requirements and groups
const SETTINGS_GROUPS: SettingsGroup[] = [
{
id: "inventory",
label: "Inventory Settings",
tabs: [
{ id: "stock-management", permission: "settings:stock_management", label: "Stock Management" },
{ id: "performance-metrics", permission: "settings:performance_metrics", label: "Performance Metrics" },
{ id: "calculation-settings", permission: "settings:calculation_settings", label: "Calculation Settings" },
]
},
{
id: "content",
label: "Content Management",
tabs: [
{ id: "templates", permission: "settings:templates", label: "Template Management" },
]
},
{
id: "system",
label: "System",
tabs: [
{ id: "user-management", permission: "settings:user_management", label: "User Management" },
{ id: "data-management", permission: "settings:data_management", label: "Data Management" },
]
}
]; ];
// Flatten tabs for easier access
const SETTINGS_TABS = SETTINGS_GROUPS.flatMap(group => group.tabs);
export function Settings() { export function Settings() {
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext);
@@ -62,131 +97,140 @@ export function Settings() {
); );
} }
// Function to check if the user has access to any tab in a group
const hasAccessToGroup = (group: SettingsGroup): boolean => {
if (user?.is_admin) return true;
return group.tabs.some(tab => user?.permissions?.includes(tab.permission));
};
return ( return (
<motion.div layout className="container mx-auto py-6"> <motion.div layout className="container mx-auto py-6">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-3xl font-bold">Settings</h1> <h1 className="text-3xl font-bold">Settings</h1>
</div> </div>
<Tabs defaultValue={defaultTab} className="space-y-4"> <Tabs defaultValue={defaultTab} orientation="vertical" className="flex flex-row min-h-[500px]">
<TabsList> <div className="w-60 border-r pr-8">
<Protected permission="settings:data_management"> <TabsList className="flex flex-col h-auto justify-start items-stretch p-0 bg-transparent">
<TabsTrigger value="data-management">Data Management</TabsTrigger> {SETTINGS_GROUPS.map((group) => (
</Protected> hasAccessToGroup(group) && (
<Protected permission="settings:stock_management"> <div key={group.id} className="">
<TabsTrigger value="stock-management">Stock Management</TabsTrigger> <h3 className="font-semibold text-sm px-3 py-2 bg-muted border text-foreground rounded-md mb-2">
</Protected> {group.label}
<Protected permission="settings:performance_metrics"> </h3>
<TabsTrigger value="performance-metrics"> <div className="space-y-1 pl-1">
Performance Metrics {group.tabs.map((tab) => (
</TabsTrigger> <Protected key={tab.id} permission={tab.permission}>
</Protected> <TabsTrigger
<Protected permission="settings:calculation_settings"> value={tab.id}
<TabsTrigger value="calculation-settings"> className="w-full justify-start px-3 py-2 text-sm font-normal text-muted-foreground data-[state=active]:font-medium data-[state=active]:text-accent-foreground data-[state=active]:shadow-none rounded-md data-[state=active]:underline"
Calculation Settings >
</TabsTrigger> {tab.label}
</Protected> </TabsTrigger>
<Protected permission="settings:templates"> </Protected>
<TabsTrigger value="templates"> ))}
Template Management </div>
</TabsTrigger> {/* Only add separator if not the last group */}
</Protected> {group.id !== SETTINGS_GROUPS[SETTINGS_GROUPS.length - 1].id && (
<Protected permission="settings:user_management"> <Separator className="mt-4 mb-4 opacity-70" />
<TabsTrigger value="user-management"> )}
User Management </div>
</TabsTrigger> )
</Protected> ))}
</TabsList> </TabsList>
</div>
<TabsContent value="data-management"> <div className="pl-8 w-full">
<Protected <TabsContent value="data-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
permission="settings:data_management" <Protected
fallback={ permission="settings:data_management"
<Alert> fallback={
<AlertDescription> <Alert>
You don't have permission to access Data Management. <AlertDescription>
</AlertDescription> You don't have permission to access Data Management.
</Alert> </AlertDescription>
} </Alert>
> }
<DataManagement /> >
</Protected> <DataManagement />
</TabsContent> </Protected>
</TabsContent>
<TabsContent value="stock-management"> <TabsContent value="stock-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected <Protected
permission="settings:stock_management" permission="settings:stock_management"
fallback={ fallback={
<Alert> <Alert>
<AlertDescription> <AlertDescription>
You don't have permission to access Stock Management. You don't have permission to access Stock Management.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
} }
> >
<StockManagement /> <StockManagement />
</Protected> </Protected>
</TabsContent> </TabsContent>
<TabsContent value="performance-metrics"> <TabsContent value="performance-metrics" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected <Protected
permission="settings:performance_metrics" permission="settings:performance_metrics"
fallback={ fallback={
<Alert> <Alert>
<AlertDescription> <AlertDescription>
You don't have permission to access Performance Metrics. You don't have permission to access Performance Metrics.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
} }
> >
<PerformanceMetrics /> <PerformanceMetrics />
</Protected> </Protected>
</TabsContent> </TabsContent>
<TabsContent value="calculation-settings"> <TabsContent value="calculation-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected <Protected
permission="settings:calculation_settings" permission="settings:calculation_settings"
fallback={ fallback={
<Alert> <Alert>
<AlertDescription> <AlertDescription>
You don't have permission to access Calculation Settings. You don't have permission to access Calculation Settings.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
} }
> >
<CalculationSettings /> <CalculationSettings />
</Protected> </Protected>
</TabsContent> </TabsContent>
<TabsContent value="templates"> <TabsContent value="templates" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected <Protected
permission="settings:templates" permission="settings:templates"
fallback={ fallback={
<Alert> <Alert>
<AlertDescription> <AlertDescription>
You don't have permission to access Template Management. You don't have permission to access Template Management.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
} }
> >
<TemplateManagement /> <TemplateManagement />
</Protected> </Protected>
</TabsContent> </TabsContent>
<TabsContent value="user-management"> <TabsContent value="user-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected <Protected
permission="settings:user_management" permission="settings:user_management"
fallback={ fallback={
<Alert> <Alert>
<AlertDescription> <AlertDescription>
You don't have permission to access User Management. You don't have permission to access User Management.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
} }
> >
<UserManagement /> <UserManagement />
</Protected> </Protected>
</TabsContent> </TabsContent>
</div>
</Tabs> </Tabs>
</motion.div> </motion.div>
); );