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:
2025-03-24 11:30:15 -04:00
parent 7eb4077224
commit dd4b3f7145
10 changed files with 1360 additions and 132 deletions

View File

@@ -23,6 +23,26 @@ CREATE TABLE IF NOT EXISTS templates (
UNIQUE(company, product_type) UNIQUE(company, product_type)
); );
-- AI Prompts table for storing validation prompts
CREATE TABLE IF NOT EXISTS ai_prompts (
id SERIAL PRIMARY KEY,
prompt_text TEXT NOT NULL,
prompt_type TEXT NOT NULL CHECK (prompt_type IN ('general', 'company_specific')),
company TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_company_prompt UNIQUE (company),
CONSTRAINT company_required_for_specific CHECK (
(prompt_type = 'general' AND company IS NULL) OR
(prompt_type = 'company_specific' AND company IS NOT NULL)
)
);
-- Create a unique partial index to ensure only one general prompt
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_general_prompt
ON ai_prompts (prompt_type)
WHERE prompt_type = 'general';
-- AI Validation Performance Tracking -- AI Validation Performance Tracking
CREATE TABLE IF NOT EXISTS ai_validation_performance ( CREATE TABLE IF NOT EXISTS ai_validation_performance (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -50,4 +70,10 @@ $$ language 'plpgsql';
CREATE TRIGGER update_templates_updated_at CREATE TRIGGER update_templates_updated_at
BEFORE UPDATE ON templates BEFORE UPDATE ON templates
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Trigger to automatically update the updated_at column for ai_prompts
CREATE TRIGGER update_ai_prompts_updated_at
BEFORE UPDATE ON ai_prompts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column(); EXECUTE FUNCTION update_updated_at_column();

View File

@@ -1,3 +1,5 @@
THIS IS THE OLD TEXT FILE PROMPT AND SHOULD NOT BE USED OR SEEN IN DEBUGGING
I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response (e.g. do not include its key or any value) unless the specific field guidelines below say otherwise. If a product appears to be from an empty or entirely invalid line, do not include it in your response. I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response (e.g. do not include its key or any value) unless the specific field guidelines below say otherwise. If a product appears to be from an empty or entirely invalid line, do not include it in your response.
Your response should be a JSON object with the following structure: Your response should be a JSON object with the following structure:

View File

@@ -0,0 +1,288 @@
const express = require('express');
const router = express.Router();
// Get all AI prompts
router.get('/', async (req, res) => {
try {
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM ai_prompts
ORDER BY prompt_type ASC, company ASC
`);
res.json(result.rows);
} catch (error) {
console.error('Error fetching AI prompts:', error);
res.status(500).json({
error: 'Failed to fetch AI prompts',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Get prompt by ID
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM ai_prompts
WHERE id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'AI prompt not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching AI prompt:', error);
res.status(500).json({
error: 'Failed to fetch AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Get prompt by company
router.get('/company/:companyId', async (req, res) => {
try {
const { companyId } = req.params;
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM ai_prompts
WHERE company = $1
`, [companyId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'AI prompt not found for this company' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching AI prompt by company:', error);
res.status(500).json({
error: 'Failed to fetch AI prompt by company',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Get general prompt
router.get('/type/general', async (req, res) => {
try {
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'general'
`);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'General AI prompt not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching general AI prompt:', error);
res.status(500).json({
error: 'Failed to fetch general AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Create new AI prompt
router.post('/', async (req, res) => {
try {
const {
prompt_text,
prompt_type,
company
} = req.body;
// Validate required fields
if (!prompt_text || !prompt_type) {
return res.status(400).json({ error: 'Prompt text and type are required' });
}
// Validate prompt type
if (!['general', 'company_specific'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general" or "company_specific"' });
}
// Validate company is provided for company-specific prompts
if (prompt_type === 'company_specific' && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
}
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
INSERT INTO ai_prompts (
prompt_text,
prompt_type,
company
) VALUES ($1, $2, $3)
RETURNING *
`, [
prompt_text,
prompt_type,
company
]);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating AI prompt:', error);
// Check for unique constraint violations
if (error instanceof Error && error.message.includes('unique constraint')) {
if (error.message.includes('unique_company_prompt')) {
return res.status(409).json({
error: 'A prompt already exists for this company',
details: error.message
});
} else if (error.message.includes('idx_unique_general_prompt')) {
return res.status(409).json({
error: 'A general prompt already exists',
details: error.message
});
}
}
res.status(500).json({
error: 'Failed to create AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Update AI prompt
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
prompt_text,
prompt_type,
company
} = req.body;
// Validate required fields
if (!prompt_text || !prompt_type) {
return res.status(400).json({ error: 'Prompt text and type are required' });
}
// Validate prompt type
if (!['general', 'company_specific'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general" or "company_specific"' });
}
// Validate company is provided for company-specific prompts
if (prompt_type === 'company_specific' && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
}
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
// Check if the prompt exists
const checkResult = await pool.query('SELECT * FROM ai_prompts WHERE id = $1', [id]);
if (checkResult.rows.length === 0) {
return res.status(404).json({ error: 'AI prompt not found' });
}
const result = await pool.query(`
UPDATE ai_prompts
SET
prompt_text = $1,
prompt_type = $2,
company = $3
WHERE id = $4
RETURNING *
`, [
prompt_text,
prompt_type,
company,
id
]);
res.json(result.rows[0]);
} catch (error) {
console.error('Error updating AI prompt:', error);
// Check for unique constraint violations
if (error instanceof Error && error.message.includes('unique constraint')) {
if (error.message.includes('unique_company_prompt')) {
return res.status(409).json({
error: 'A prompt already exists for this company',
details: error.message
});
} else if (error.message.includes('idx_unique_general_prompt')) {
return res.status(409).json({
error: 'A general prompt already exists',
details: error.message
});
}
}
res.status(500).json({
error: 'Failed to update AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Delete AI prompt
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query('DELETE FROM ai_prompts WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'AI prompt not found' });
}
res.json({ message: 'AI prompt deleted successfully' });
} catch (error) {
console.error('Error deleting AI prompt:', error);
res.status(500).json({
error: 'Failed to delete AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Error handling middleware
router.use((err, req, res, next) => {
console.error('AI prompts route error:', err);
res.status(500).json({
error: 'Internal server error',
details: err.message
});
});
module.exports = router;

View File

@@ -289,7 +289,76 @@ async function generateDebugResponse(productsToUse, res) {
}); });
try { try {
const prompt = await loadPrompt(promptConnection, productsToUse); // Get the local PostgreSQL pool to fetch prompts
const pool = res.app.locals.pool;
if (!pool) {
console.warn("⚠️ Local database pool not available for prompts");
throw new Error("Database connection not available");
}
// First, fetch the general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'general'
`);
if (generalPromptResult.rows.length === 0) {
console.warn("⚠️ No general prompt found in database");
throw new Error("No general prompt found in database");
}
// Get the general prompt text and info
const generalPrompt = generalPromptResult.rows[0];
console.log("📝 Loaded general prompt from database, ID:", generalPrompt.id);
// Fetch company-specific prompts if we have products to validate
let companyPrompts = [];
if (productsToUse && Array.isArray(productsToUse)) {
// Extract unique company IDs from products
const companyIds = new Set();
productsToUse.forEach(product => {
if (product.company) {
companyIds.add(String(product.company));
}
});
if (companyIds.size > 0) {
console.log(`🔍 Found ${companyIds.size} unique companies in products:`, Array.from(companyIds));
// Fetch company-specific prompts
const companyPromptsResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'company_specific'
AND company = ANY($1)
`, [Array.from(companyIds)]);
companyPrompts = companyPromptsResult.rows;
console.log(`📝 Loaded ${companyPrompts.length} company-specific prompts`);
}
}
// Find company names from taxonomy
const companyPromptsWithNames = companyPrompts.map(prompt => {
let companyName = "Unknown Company";
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
const companyData = taxonomy.companies.find(company =>
String(company[0]) === String(prompt.company)
);
if (companyData && companyData[1]) {
companyName = companyData[1];
}
}
return {
id: prompt.id,
company: prompt.company,
companyName: companyName,
prompt_text: prompt.prompt_text
};
});
// Now use loadPrompt to get the actual combined prompt
const prompt = await loadPrompt(promptConnection, productsToUse, res.app.locals.pool);
const fullPrompt = prompt + "\n" + JSON.stringify(productsToUse); const fullPrompt = prompt + "\n" + JSON.stringify(productsToUse);
// Create the response with taxonomy stats // Create the response with taxonomy stats
@@ -330,9 +399,16 @@ async function generateDebugResponse(productsToUse, res) {
: null, : null,
} }
: null, : null,
basePrompt: prompt, basePrompt: generalPrompt.prompt_text,
sampleFullPrompt: fullPrompt, sampleFullPrompt: fullPrompt,
promptLength: fullPrompt.length, promptLength: fullPrompt.length,
promptSources: {
generalPrompt: {
id: generalPrompt.id,
prompt_text: generalPrompt.prompt_text
},
companyPrompts: companyPromptsWithNames
}
}; };
console.log("Sending response with taxonomy stats:", response.taxonomyStats); console.log("Sending response with taxonomy stats:", response.taxonomyStats);
@@ -513,22 +589,107 @@ SELECT t.cat_id,t.name,null as master_cat_id,1 AS level_order FROM product_categ
} }
} }
// Load the prompt from file and inject taxonomy data // Load prompts from database and inject taxonomy data
async function loadPrompt(connection, productsToValidate = null) { async function loadPrompt(connection, productsToValidate = null, appPool = null) {
try { try {
const promptPath = path.join(
__dirname,
"..",
"prompts",
"product-validation.txt"
);
const basePrompt = await fs.readFile(promptPath, "utf8");
// Get taxonomy data using the provided MySQL connection // Get taxonomy data using the provided MySQL connection
const taxonomy = await getTaxonomyData(connection); const taxonomy = await getTaxonomyData(connection);
// Add system instructions to the prompt // Initialize default system instructions
const systemInstructions = `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`; const systemInstructions = `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`;
// Use the provided pool parameter instead of global.app
const pool = appPool;
if (!pool) {
console.warn("⚠️ Local database pool not available for prompts");
throw new Error("Database connection not available");
}
// Fetch the general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'general'
`);
if (generalPromptResult.rows.length === 0) {
console.warn("⚠️ No general prompt found in database");
throw new Error("No general prompt found in database");
}
// Get the general prompt text
const basePrompt = generalPromptResult.rows[0].prompt_text;
console.log("📝 Loaded general prompt from database");
// Fetch company-specific prompts if we have products to validate
let companyPrompts = [];
if (productsToValidate && Array.isArray(productsToValidate)) {
// Extract unique company IDs from products
const companyIds = new Set();
productsToValidate.forEach(product => {
if (product.company) {
companyIds.add(String(product.company));
}
});
if (companyIds.size > 0) {
console.log(`🔍 Found ${companyIds.size} unique companies in products:`, Array.from(companyIds));
// Fetch company-specific prompts
const companyPromptsResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'company_specific'
AND company = ANY($1)
`, [Array.from(companyIds)]);
companyPrompts = companyPromptsResult.rows;
console.log(`📝 Loaded ${companyPrompts.length} company-specific prompts`);
}
}
// Find company names from taxonomy for the validation endpoint
const companyPromptsWithNames = companyPrompts.map(prompt => {
let companyName = "Unknown Company";
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
const companyData = taxonomy.companies.find(company =>
String(company[0]) === String(prompt.company)
);
if (companyData && companyData[1]) {
companyName = companyData[1];
}
}
return {
id: prompt.id,
company: prompt.company,
companyName: companyName,
prompt_text: prompt.prompt_text
};
});
// Combine prompts - start with the general prompt
let combinedPrompt = basePrompt;
// Add any company-specific prompts with annotations
if (companyPrompts.length > 0) {
combinedPrompt += "\n\n--- COMPANY-SPECIFIC INSTRUCTIONS ---\n";
for (const prompt of companyPrompts) {
// Find company name from taxonomy
let companyName = "Unknown Company";
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
const companyData = taxonomy.companies.find(company =>
String(company[0]) === String(prompt.company)
);
if (companyData && companyData[1]) {
companyName = companyData[1];
}
}
combinedPrompt += `\n[SPECIFIC TO COMPANY: ${companyName} (ID: ${prompt.company})]:\n${prompt.prompt_text}\n`;
}
combinedPrompt += "\n--- END COMPANY-SPECIFIC INSTRUCTIONS ---\n";
}
// If we have products to validate, create a filtered prompt // If we have products to validate, create a filtered prompt
if (productsToValidate) { if (productsToValidate) {
@@ -656,7 +817,7 @@ ${JSON.stringify(mixedTaxonomy.sizeCategories)}${
----------Here is the product data to validate----------`; ----------Here is the product data to validate----------`;
// Return the filtered prompt // Return the filtered prompt
return systemInstructions + basePrompt + "\n" + taxonomySection; return systemInstructions + combinedPrompt + "\n" + taxonomySection;
} }
// Generate the full unfiltered prompt // Generate the full unfiltered prompt
@@ -687,7 +848,7 @@ ${JSON.stringify(taxonomy.artists)}
Here is the product data to validate:`; Here is the product data to validate:`;
return systemInstructions + basePrompt + "\n" + taxonomySection; return systemInstructions + combinedPrompt + "\n" + taxonomySection;
} catch (error) { } catch (error) {
console.error("Error loading prompt:", error); console.error("Error loading prompt:", error);
throw error; // Re-throw to be handled by the calling function throw error; // Re-throw to be handled by the calling function
@@ -735,7 +896,7 @@ router.post("/validate", async (req, res) => {
// Load the prompt with the products data to filter taxonomy // Load the prompt with the products data to filter taxonomy
console.log("🔄 Loading prompt with filtered taxonomy..."); console.log("🔄 Loading prompt with filtered taxonomy...");
const prompt = await loadPrompt(connection, products); const prompt = await loadPrompt(connection, products, req.app.locals.pool);
const fullPrompt = prompt + "\n" + JSON.stringify(products); const fullPrompt = prompt + "\n" + JSON.stringify(products);
promptLength = fullPrompt.length; // Store prompt length for performance metrics promptLength = fullPrompt.length; // Store prompt length for performance metrics
console.log("📝 Generated prompt length:", promptLength); console.log("📝 Generated prompt length:", promptLength);
@@ -884,7 +1045,72 @@ router.post("/validate", async (req, res) => {
console.error("⚠️ Failed to record performance metrics:", metricError); console.error("⚠️ Failed to record performance metrics:", metricError);
} }
// Include performance metrics in the response // Get sources of the prompts for tracking
let promptSources = null;
try {
// Get general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts WHERE prompt_type = 'general'
`);
// Extract unique company IDs from products
const companyIds = new Set();
products.forEach(product => {
if (product.company) {
companyIds.add(String(product.company));
}
});
let companyPrompts = [];
if (companyIds.size > 0) {
// Fetch company-specific prompts
const companyPromptsResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'company_specific'
AND company = ANY($1)
`, [Array.from(companyIds)]);
companyPrompts = companyPromptsResult.rows;
}
// Find company names from taxonomy
const companyPromptsWithNames = companyPrompts.map(prompt => {
let companyName = "Unknown Company";
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
const companyData = taxonomy.companies.find(company =>
String(company[0]) === String(prompt.company)
);
if (companyData && companyData[1]) {
companyName = companyData[1];
}
}
return {
id: prompt.id,
company: prompt.company,
companyName: companyName,
prompt_text: prompt.prompt_text
};
});
// Set prompt sources
if (generalPromptResult.rows.length > 0) {
const generalPrompt = generalPromptResult.rows[0];
promptSources = {
generalPrompt: {
id: generalPrompt.id,
prompt_text: generalPrompt.prompt_text
},
companyPrompts: companyPromptsWithNames
};
}
} catch (promptSourceError) {
console.error("⚠️ Error getting prompt sources:", promptSourceError);
// Don't fail the entire validation if just prompt sources retrieval fails
}
// Include prompt sources in the response
res.json({ res.json({
success: true, success: true,
changeDetails: changeDetails, changeDetails: changeDetails,
@@ -895,6 +1121,7 @@ router.post("/validate", async (req, res) => {
isEstimate: true, isEstimate: true,
productCount: products.length productCount: products.length
}, },
promptSources: promptSources,
...aiResponse, ...aiResponse,
}); });
} catch (parseError) { } catch (parseError) {

View File

@@ -18,6 +18,7 @@ const categoriesRouter = require('./routes/categories');
const importRouter = require('./routes/import'); const importRouter = require('./routes/import');
const aiValidationRouter = require('./routes/ai-validation'); const aiValidationRouter = require('./routes/ai-validation');
const templatesRouter = require('./routes/templates'); const templatesRouter = require('./routes/templates');
const aiPromptsRouter = require('./routes/ai-prompts');
// Get the absolute path to the .env file // Get the absolute path to the .env file
const envPath = '/var/www/html/inventory/.env'; const envPath = '/var/www/html/inventory/.env';
@@ -103,6 +104,7 @@ async function startServer() {
app.use('/api/import', importRouter); app.use('/api/import', importRouter);
app.use('/api/ai-validation', aiValidationRouter); app.use('/api/ai-validation', aiValidationRouter);
app.use('/api/templates', templatesRouter); app.use('/api/templates', templatesRouter);
app.use('/api/ai-prompts', aiPromptsRouter);
// Basic health check route // Basic health check route
app.get('/health', (req, res) => { app.get('/health', (req, res) => {

View File

@@ -1,9 +1,9 @@
const mysql = require('mysql2/promise'); const { Pool } = require('pg');
let pool; let pool;
function initPool(config) { function initPool(config) {
pool = mysql.createPool(config); pool = new Pool(config);
return pool; return pool;
} }

View File

@@ -24,6 +24,8 @@ import {
CurrentPrompt, CurrentPrompt,
} from "../hooks/useAiValidation"; } from "../hooks/useAiValidation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { interface TaxonomyStats {
categories: number; categories: number;
@@ -41,6 +43,10 @@ interface DebugData {
basePrompt: string; basePrompt: string;
sampleFullPrompt: string; sampleFullPrompt: string;
promptLength: number; promptLength: number;
promptSources?: {
generalPrompt?: { id: number; prompt_text: string };
companyPrompts?: Array<{ id: number; company: string; prompt_text: string }>;
};
estimatedProcessingTime?: { estimatedProcessingTime?: {
seconds: number | null; seconds: number | null;
sampleCount: number; sampleCount: number;
@@ -83,7 +89,8 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
debugData, debugData,
}) => { }) => {
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
const [activeTab, setActiveTab] = useState("full");
// Format time helper // Format time helper
const formatTime = (seconds: number): string => { const formatTime = (seconds: number): string => {
if (seconds < 60) { if (seconds < 60) {
@@ -103,6 +110,10 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
// Use the prompt length from the current prompt // Use the prompt length from the current prompt
const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0; 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 ( return (
<> <>
@@ -128,131 +139,225 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
{currentPrompt.isLoading ? ( {currentPrompt.isLoading ? (
<div className="flex justify-center items-center h-[100px]"></div> <div className="flex justify-center items-center h-[100px]"></div>
) : ( ) : (
<div className="grid grid-cols-3 gap-4"> <>
<Card className="py-2"> <div className="grid grid-cols-3 gap-4 mb-4">
<CardHeader className="py-2"> <Card className="py-2">
<CardTitle className="text-base">Prompt Length</CardTitle> <CardHeader className="py-2">
</CardHeader> <CardTitle className="text-base">Prompt Length</CardTitle>
<CardContent className="py-2"> </CardHeader>
<div className="flex flex-col space-y-2"> <CardContent className="py-2">
<div className="text-sm"> <div className="flex flex-col space-y-2">
<span className="text-muted-foreground"> <div className="text-sm">
Characters: <span className="text-muted-foreground">
</span>{" "} Characters:
<span className="font-semibold">{promptLength}</span> </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>
<div className="text-sm"> </CardContent>
<span className="text-muted-foreground">Tokens:</span>{" "} </Card>
<span className="font-semibold">
~{Math.round(promptLength / 4)}
</span>
</div>
</div>
</CardContent>
</Card>
<Card className="py-2"> <Card className="py-2">
<CardHeader className="py-2"> <CardHeader className="py-2">
<CardTitle className="text-base">Cost Estimate</CardTitle> <CardTitle className="text-base">Cost Estimate</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="py-2"> <CardContent className="py-2">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<label <label
htmlFor="costPerMillion" htmlFor="costPerMillion"
className="text-sm text-muted-foreground" className="text-sm text-muted-foreground"
> >
$ $
</label> </label>
<input <input
id="costPerMillion" id="costPerMillion"
className="w-[40px] px-1 border rounded-md text-sm" className="w-[40px] px-1 border rounded-md text-sm"
defaultValue={costPerMillionTokens.toFixed(2)} defaultValue={costPerMillionTokens.toFixed(2)}
onChange={(e) => { onChange={(e) => {
const value = parseFloat(e.target.value); const value = parseFloat(e.target.value);
if (!isNaN(value)) { if (!isNaN(value)) {
setCostPerMillionTokens(value); setCostPerMillionTokens(value);
} }
}} }}
/> />
<label <label
htmlFor="costPerMillion" htmlFor="costPerMillion"
className="text-sm text-muted-foreground ml-1" className="text-sm text-muted-foreground ml-1"
> >
per million input tokens per million input tokens
</label> </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>
<div className="text-sm"> </CardContent>
<span className="text-muted-foreground">Cost:</span>{" "} </Card>
<span className="font-semibold">
{calculateTokenCost(promptLength).toFixed(1)}¢
</span>
</div>
</div>
</CardContent>
</Card>
<Card className="py-2"> <Card className="py-2">
<CardHeader className="py-2"> <CardHeader className="py-2">
<CardTitle className="text-base"> <CardTitle className="text-base">
Processing Time Processing Time
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="py-2"> <CardContent className="py-2">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{debugData?.estimatedProcessingTime ? ( {currentPrompt.debugData?.estimatedProcessingTime ? (
debugData.estimatedProcessingTime.seconds ? ( currentPrompt.debugData.estimatedProcessingTime.seconds ? (
<> <>
<div className="text-sm"> <div className="text-sm">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Estimated time: Estimated time:
</span>{" "} </span>{" "}
<span className="font-semibold"> <span className="font-semibold">
{formatTime( {formatTime(
debugData.estimatedProcessingTime.seconds currentPrompt.debugData.estimatedProcessingTime.seconds
)} )}
</span> </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>
<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"> <div className="text-sm text-muted-foreground">
No historical data available for this prompt size No processing time data available
</div> </div>
) )}
) : ( </div>
<div className="text-sm text-muted-foreground"> </CardContent>
No processing time data available </Card>
</div> </div>
)}
</div> {/* Prompt Sources Section */}
</CardContent> {currentPrompt.debugData?.promptSources && (
</Card> <Card className="mb-4">
</div> <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> </div>
{/* Prompt Section */} {/* Prompt Section */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<ScrollArea className="h-full w-full"> {currentPrompt.isLoading ? (
{currentPrompt.isLoading ? ( <div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center h-full"> <Loader2 className="h-8 w-8 animate-spin" />
<Loader2 className="h-8 w-8 animate-spin" /> </div>
</div> ) : (
) : ( <>
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full"> {currentPrompt.debugData?.promptSources ? (
{currentPrompt.prompt} <Tabs
</Code> defaultValue="full"
)} value={activeTab}
</ScrollArea> 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>
</div> </div>
</DialogContent> </DialogContent>

View File

@@ -56,6 +56,15 @@ export interface CurrentPrompt {
basePrompt: string; basePrompt: string;
sampleFullPrompt: string; sampleFullPrompt: string;
promptLength: number; promptLength: number;
promptSources?: {
generalPrompt?: { id: number; prompt_text: string };
companyPrompts?: Array<{
id: number;
company: string;
companyName: string;
prompt_text: string
}>;
};
estimatedProcessingTime?: { estimatedProcessingTime?: {
seconds: number | null; seconds: number | null;
sampleCount: number; sampleCount: number;
@@ -323,6 +332,7 @@ export const useAiValidation = <T extends string>(
basePrompt: result.basePrompt || '', basePrompt: result.basePrompt || '',
sampleFullPrompt: result.sampleFullPrompt || '', sampleFullPrompt: result.sampleFullPrompt || '',
promptLength: result.promptLength || (promptContent ? promptContent.length : 0), promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
promptSources: result.promptSources,
estimatedProcessingTime: result.estimatedProcessingTime estimatedProcessingTime: result.estimatedProcessingTime
} }
})); }));
@@ -490,6 +500,27 @@ export const useAiValidation = <T extends string>(
throw new Error(result.error || 'AI validation failed'); 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 // Update progress with actual processing time if available
if (result.performanceMetrics) { if (result.performanceMetrics) {
console.log('Performance metrics:', result.performanceMetrics); console.log('Performance metrics:', result.performanceMetrics);

View 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>
);
}

View File

@@ -5,6 +5,7 @@ import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
import { CalculationSettings } from "@/components/settings/CalculationSettings"; import { CalculationSettings } from "@/components/settings/CalculationSettings";
import { TemplateManagement } from "@/components/settings/TemplateManagement"; import { TemplateManagement } from "@/components/settings/TemplateManagement";
import { UserManagement } from "@/components/settings/UserManagement"; import { UserManagement } from "@/components/settings/UserManagement";
import { PromptManagement } from "@/components/settings/PromptManagement";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Protected } from "@/components/auth/Protected"; import { Protected } from "@/components/auth/Protected";
@@ -41,6 +42,7 @@ const SETTINGS_GROUPS: SettingsGroup[] = [
label: "Content Management", label: "Content Management",
tabs: [ tabs: [
{ id: "templates", permission: "settings:templates", label: "Template Management" }, { 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> </Protected>
</TabsContent> </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"> <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"