Add prompts table and settings page to create/read/update/delete from it, incorporate company specific prompts into ai validation
This commit is contained in:
@@ -24,6 +24,8 @@ import {
|
||||
CurrentPrompt,
|
||||
} from "../hooks/useAiValidation";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface TaxonomyStats {
|
||||
categories: number;
|
||||
@@ -41,6 +43,10 @@ interface DebugData {
|
||||
basePrompt: string;
|
||||
sampleFullPrompt: string;
|
||||
promptLength: number;
|
||||
promptSources?: {
|
||||
generalPrompt?: { id: number; prompt_text: string };
|
||||
companyPrompts?: Array<{ id: number; company: string; prompt_text: string }>;
|
||||
};
|
||||
estimatedProcessingTime?: {
|
||||
seconds: number | null;
|
||||
sampleCount: number;
|
||||
@@ -83,7 +89,8 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
||||
debugData,
|
||||
}) => {
|
||||
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
|
||||
|
||||
const [activeTab, setActiveTab] = useState("full");
|
||||
|
||||
// Format time helper
|
||||
const formatTime = (seconds: number): string => {
|
||||
if (seconds < 60) {
|
||||
@@ -103,6 +110,10 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
||||
|
||||
// Use the prompt length from the current prompt
|
||||
const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0;
|
||||
|
||||
// Check if we have company-specific prompts
|
||||
const hasCompanyPrompts = currentPrompt.debugData?.promptSources?.companyPrompts &&
|
||||
currentPrompt.debugData.promptSources.companyPrompts.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -128,131 +139,225 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
||||
{currentPrompt.isLoading ? (
|
||||
<div className="flex justify-center items-center h-[100px]"></div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<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 className="grid grid-cols-3 gap-4 mb-4">
|
||||
<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>
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Tokens:</span>{" "}
|
||||
<span className="font-semibold">
|
||||
~{Math.round(promptLength / 4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<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>
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Cost:</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{calculateTokenCost(promptLength).toFixed(1)}¢
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<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">
|
||||
{currentPrompt.debugData?.estimatedProcessingTime ? (
|
||||
currentPrompt.debugData.estimatedProcessingTime.seconds ? (
|
||||
<>
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Estimated time:
|
||||
</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{formatTime(
|
||||
currentPrompt.debugData.estimatedProcessingTime.seconds
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Based on{" "}
|
||||
{currentPrompt.debugData.estimatedProcessingTime.sampleCount}{" "}
|
||||
similar validation
|
||||
{currentPrompt.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-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
|
||||
No processing time data available
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No processing time data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Prompt Sources Section */}
|
||||
{currentPrompt.debugData?.promptSources && (
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="py-2">
|
||||
<CardTitle className="text-base">
|
||||
Prompt Sources
|
||||
{hasCompanyPrompts && (
|
||||
<Badge className="ml-2 bg-blue-500" variant="secondary">
|
||||
{currentPrompt.debugData.promptSources.companyPrompts?.length} Company-Specific
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="py-2">
|
||||
<div className="text-sm">
|
||||
<p className="mb-2">
|
||||
<Badge variant="outline" className="mr-2">
|
||||
General
|
||||
</Badge>
|
||||
Base prompt for all products
|
||||
</p>
|
||||
|
||||
{hasCompanyPrompts && (
|
||||
<div className="mt-2">
|
||||
<p className="font-medium mb-1">Company Specific:</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{currentPrompt.debugData.promptSources.companyPrompts?.map((prompt, idx) => (
|
||||
<li key={idx}>
|
||||
<span className="font-semibold">{prompt.companyName}</span>
|
||||
<span className="text-xs text-muted-foreground ml-1">(ID: {prompt.company})</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
{currentPrompt.isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentPrompt.debugData?.promptSources ? (
|
||||
<Tabs
|
||||
defaultValue="full"
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full h-full flex flex-col"
|
||||
>
|
||||
<TabsList className="mb-2 flex-shrink-0">
|
||||
<TabsTrigger value="full">Full Prompt</TabsTrigger>
|
||||
<TabsTrigger value="general">General Prompt</TabsTrigger>
|
||||
{hasCompanyPrompts && (
|
||||
<TabsTrigger value="company">Company Prompts</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<TabsContent value="full" className="m-0 p-0 h-full">
|
||||
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||
{currentPrompt.prompt}
|
||||
</Code>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="general" className="m-0 p-0 h-full">
|
||||
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||
{currentPrompt.debugData.promptSources.generalPrompt?.prompt_text || "No general prompt available"}
|
||||
</Code>
|
||||
</TabsContent>
|
||||
|
||||
{hasCompanyPrompts && (
|
||||
<TabsContent value="company" className="m-0 p-0 h-full">
|
||||
<div className="space-y-4">
|
||||
{currentPrompt.debugData.promptSources.companyPrompts?.map((prompt, idx) => (
|
||||
<div key={idx} className="border rounded-md p-2">
|
||||
<div className="bg-muted p-2 mb-2 rounded-sm">
|
||||
<strong>{prompt.companyName}</strong> <span className="text-sm text-muted-foreground">(ID: {prompt.company})</span>
|
||||
</div>
|
||||
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||
{prompt.prompt_text}
|
||||
</Code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</Tabs>
|
||||
) : (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||
{currentPrompt.prompt}
|
||||
</Code>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -56,6 +56,15 @@ export interface CurrentPrompt {
|
||||
basePrompt: string;
|
||||
sampleFullPrompt: string;
|
||||
promptLength: number;
|
||||
promptSources?: {
|
||||
generalPrompt?: { id: number; prompt_text: string };
|
||||
companyPrompts?: Array<{
|
||||
id: number;
|
||||
company: string;
|
||||
companyName: string;
|
||||
prompt_text: string
|
||||
}>;
|
||||
};
|
||||
estimatedProcessingTime?: {
|
||||
seconds: number | null;
|
||||
sampleCount: number;
|
||||
@@ -323,6 +332,7 @@ export const useAiValidation = <T extends string>(
|
||||
basePrompt: result.basePrompt || '',
|
||||
sampleFullPrompt: result.sampleFullPrompt || '',
|
||||
promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
|
||||
promptSources: result.promptSources,
|
||||
estimatedProcessingTime: result.estimatedProcessingTime
|
||||
}
|
||||
}));
|
||||
@@ -490,6 +500,27 @@ export const useAiValidation = <T extends string>(
|
||||
throw new Error(result.error || 'AI validation failed');
|
||||
}
|
||||
|
||||
// Store the prompt sources if they exist
|
||||
if (result.promptSources) {
|
||||
setCurrentPrompt(prev => {
|
||||
// Create debugData if it doesn't exist
|
||||
const prevDebugData = prev.debugData || {
|
||||
taxonomyStats: null,
|
||||
basePrompt: '',
|
||||
sampleFullPrompt: '',
|
||||
promptLength: 0
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
debugData: {
|
||||
...prevDebugData,
|
||||
promptSources: result.promptSources
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Update progress with actual processing time if available
|
||||
if (result.performanceMetrics) {
|
||||
console.log('Performance metrics:', result.performanceMetrics);
|
||||
|
||||
530
inventory/src/components/settings/PromptManagement.tsx
Normal file
530
inventory/src/components/settings/PromptManagement.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ArrowUpDown, Pencil, Trash2, PlusCircle } from "lucide-react";
|
||||
import config from "@/config";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
flexRender,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface FieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface PromptFormData {
|
||||
id?: number;
|
||||
prompt_text: string;
|
||||
prompt_type: 'general' | 'company_specific';
|
||||
company: string | null;
|
||||
}
|
||||
|
||||
interface AiPrompt {
|
||||
id: number;
|
||||
prompt_text: string;
|
||||
prompt_type: 'general' | 'company_specific';
|
||||
company: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface FieldOptions {
|
||||
companies: FieldOption[];
|
||||
}
|
||||
|
||||
export function PromptManagement() {
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [promptToDelete, setPromptToDelete] = useState<AiPrompt | null>(null);
|
||||
const [editingPrompt, setEditingPrompt] = useState<AiPrompt | null>(null);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [formData, setFormData] = useState<PromptFormData>({
|
||||
prompt_text: "",
|
||||
prompt_type: "general",
|
||||
company: null,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: prompts, isLoading } = useQuery<AiPrompt[]>({
|
||||
queryKey: ["ai-prompts"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/ai-prompts`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch AI prompts");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const { data: fieldOptions } = useQuery<FieldOptions>({
|
||||
queryKey: ["fieldOptions"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/import/field-options`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch field options");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
// Check if a general prompt already exists
|
||||
const generalPromptExists = useMemo(() => {
|
||||
return prompts?.some(prompt => prompt.prompt_type === 'general');
|
||||
}, [prompts]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: PromptFormData) => {
|
||||
const response = await fetch(`${config.apiUrl}/ai-prompts`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || error.error || "Failed to create prompt");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||
toast.success("Prompt created successfully");
|
||||
resetForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to create prompt");
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (data: PromptFormData) => {
|
||||
if (!data.id) throw new Error("Prompt ID is required for update");
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/ai-prompts/${data.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || error.error || "Failed to update prompt");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||
toast.success("Prompt updated successfully");
|
||||
resetForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to update prompt");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const response = await fetch(`${config.apiUrl}/ai-prompts/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete prompt");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||
toast.success("Prompt deleted successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to delete prompt");
|
||||
},
|
||||
});
|
||||
|
||||
const handleEdit = (prompt: AiPrompt) => {
|
||||
setEditingPrompt(prompt);
|
||||
setFormData({
|
||||
id: prompt.id,
|
||||
prompt_text: prompt.prompt_text,
|
||||
prompt_type: prompt.prompt_type,
|
||||
company: prompt.company,
|
||||
});
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (prompt: AiPrompt) => {
|
||||
setPromptToDelete(prompt);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (promptToDelete) {
|
||||
deleteMutation.mutate(promptToDelete.id);
|
||||
setIsDeleteOpen(false);
|
||||
setPromptToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If prompt_type is general, ensure company is null
|
||||
const submitData = {
|
||||
...formData,
|
||||
company: formData.prompt_type === 'general' ? null : formData.company,
|
||||
};
|
||||
|
||||
if (editingPrompt) {
|
||||
updateMutation.mutate(submitData);
|
||||
} else {
|
||||
createMutation.mutate(submitData);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
prompt_text: "",
|
||||
prompt_type: "general",
|
||||
company: null,
|
||||
});
|
||||
setEditingPrompt(null);
|
||||
setIsFormOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
resetForm();
|
||||
|
||||
// If general prompt exists, default to company-specific
|
||||
if (generalPromptExists) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
prompt_type: 'company_specific'
|
||||
}));
|
||||
}
|
||||
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const columns = useMemo<ColumnDef<AiPrompt>[]>(() => [
|
||||
{
|
||||
accessorKey: "prompt_type",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Type
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue("prompt_type") as string;
|
||||
return type === 'general' ? 'General' : 'Company Specific';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "company",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Company
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const companyId = row.getValue("company");
|
||||
if (!companyId) return 'N/A';
|
||||
return fieldOptions?.companies.find(c => c.value === companyId)?.label || companyId;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "updated_at",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Last Updated
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => new Date(row.getValue("updated_at")).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2 justify-end pr-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [fieldOptions]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!prompts) return [];
|
||||
return prompts.filter((prompt) => {
|
||||
const searchString = searchQuery.toLowerCase();
|
||||
return (
|
||||
prompt.prompt_type.toLowerCase().includes(searchString) ||
|
||||
(prompt.company && prompt.company.toLowerCase().includes(searchString))
|
||||
);
|
||||
});
|
||||
}, [prompts, searchQuery]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">AI Validation Prompts</h2>
|
||||
<Button onClick={handleCreateClick}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
Create New Prompt
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
placeholder="Search prompts..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div>Loading prompts...</div>
|
||||
) : (
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="hover:bg-gray-100">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="pl-6">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center">
|
||||
No prompts found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt Form Dialog */}
|
||||
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingPrompt ? "Edit Prompt" : "Create New Prompt"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingPrompt
|
||||
? "Update this AI validation prompt."
|
||||
: "Create a new AI validation prompt that will be used during product validation."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="prompt_type">Prompt Type</Label>
|
||||
<Select
|
||||
value={formData.prompt_type}
|
||||
onValueChange={(value: 'general' | 'company_specific') =>
|
||||
setFormData({ ...formData, prompt_type: value })
|
||||
}
|
||||
disabled={generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select prompt type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="general" disabled={generalPromptExists && !editingPrompt?.prompt_type?.includes('general')}>
|
||||
General
|
||||
</SelectItem>
|
||||
<SelectItem value="company_specific">Company Specific</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
A general prompt already exists. You can only create company-specific prompts.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formData.prompt_type === 'company_specific' && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="company">Company</Label>
|
||||
<Select
|
||||
value={formData.company || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, company: value })}
|
||||
required={formData.prompt_type === 'company_specific'}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select company" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldOptions?.companies.map((company) => (
|
||||
<SelectItem key={company.value} value={company.value}>
|
||||
{company.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="prompt_text">Prompt Text</Label>
|
||||
<Textarea
|
||||
id="prompt_text"
|
||||
value={formData.prompt_text}
|
||||
onChange={(e) => setFormData({ ...formData, prompt_text: e.target.value })}
|
||||
placeholder="Enter your validation prompt text..."
|
||||
className="h-80 font-mono text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => {
|
||||
resetForm();
|
||||
setIsFormOpen(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{editingPrompt ? "Update" : "Create"} Prompt
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Prompt</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this prompt? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => {
|
||||
setIsDeleteOpen(false);
|
||||
setPromptToDelete(null);
|
||||
}}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm}>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
|
||||
import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
||||
import { TemplateManagement } from "@/components/settings/TemplateManagement";
|
||||
import { UserManagement } from "@/components/settings/UserManagement";
|
||||
import { PromptManagement } from "@/components/settings/PromptManagement";
|
||||
import { motion } from 'framer-motion';
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Protected } from "@/components/auth/Protected";
|
||||
@@ -41,6 +42,7 @@ const SETTINGS_GROUPS: SettingsGroup[] = [
|
||||
label: "Content Management",
|
||||
tabs: [
|
||||
{ id: "templates", permission: "settings:templates", label: "Template Management" },
|
||||
{ id: "ai-prompts", permission: "settings:templates", label: "AI Prompts" },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -216,6 +218,21 @@ export function Settings() {
|
||||
</Protected>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="ai-prompts" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||
<Protected
|
||||
permission="settings:templates"
|
||||
fallback={
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
You don't have permission to access AI Prompts.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
}
|
||||
>
|
||||
<PromptManagement />
|
||||
</Protected>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="user-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||
<Protected
|
||||
permission="settings:user_management"
|
||||
|
||||
Reference in New Issue
Block a user