Split off AI prompt into separate file, auto include taxonomy in prompt, create prompt debug page
This commit is contained in:
@@ -17,6 +17,7 @@ import { Vendors } from '@/pages/Vendors';
|
||||
import { Categories } from '@/pages/Categories';
|
||||
import { Import } from '@/pages/Import';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -71,6 +72,7 @@ function App() {
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/forecasting" element={<Forecasting />} />
|
||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
206
inventory/src/pages/AiValidationDebug.tsx
Normal file
206
inventory/src/pages/AiValidationDebug.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
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 CacheStatus {
|
||||
isCacheValid: boolean
|
||||
lastUpdated: string | null
|
||||
timeUntilExpiry: string
|
||||
}
|
||||
|
||||
interface TaxonomyStats {
|
||||
categories: number
|
||||
themes: number
|
||||
colors: number
|
||||
taxCodes: number
|
||||
sizeCategories: number
|
||||
}
|
||||
|
||||
interface DebugData {
|
||||
cacheStatus: CacheStatus
|
||||
taxonomyStats: TaxonomyStats | null
|
||||
basePrompt: string
|
||||
sampleFullPrompt: string
|
||||
promptLength: 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 {
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/debug`)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCache = async () => {
|
||||
if (!confirm('Are you sure you want to refresh the cache?')) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/refresh-cache`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh cache')
|
||||
}
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Cache refreshed successfully"
|
||||
})
|
||||
fetchDebugData()
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to refresh cache')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing cache:', error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Failed to refresh cache"
|
||||
})
|
||||
} 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>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={refreshCache}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Force Cache Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{debugData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cache Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div>Valid: {debugData.cacheStatus.isCacheValid ? "Yes" : "No"}</div>
|
||||
<div>Last Updated: {debugData.cacheStatus.lastUpdated || "never"}</div>
|
||||
<div>Expires in: {debugData.cacheStatus.timeUntilExpiry}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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>
|
||||
) : (
|
||||
<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="3"
|
||||
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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "UPC",
|
||||
key: "upc",
|
||||
description: "Universal Product Code/Barcode",
|
||||
alternateMatches: ["upc","UPC","barcode", "bar code", "JAN", "EAN"],
|
||||
alternateMatches: ["barcode", "bar code", "jan", "ean"],
|
||||
fieldType: { type: "input" },
|
||||
width: 140,
|
||||
validations: [
|
||||
@@ -94,7 +94,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "MSRP",
|
||||
key: "msrp",
|
||||
description: "Manufacturer's Suggested Retail Price",
|
||||
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail","msrp","MSRP"],
|
||||
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. retail","default price"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
price: true
|
||||
@@ -136,7 +136,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Case Pack",
|
||||
key: "case_qty",
|
||||
description: "Number of units per case",
|
||||
alternateMatches: ["mc qty","MC Qty","case qty","Case Qty"],
|
||||
alternateMatches: ["mc qty","case qty","case pack"],
|
||||
fieldType: { type: "input" },
|
||||
width: 50,
|
||||
validations: [
|
||||
@@ -208,6 +208,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Weight",
|
||||
key: "weight",
|
||||
description: "Product weight (in lbs)",
|
||||
alternateMatches: ["weight (lbs.)"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
validations: [
|
||||
@@ -295,6 +296,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Description",
|
||||
key: "description",
|
||||
description: "Detailed product description",
|
||||
alternateMatches: ["details/description"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
multiline: true
|
||||
|
||||
Reference in New Issue
Block a user