diff --git a/inventory-server/db/setup-schema.sql b/inventory-server/db/setup-schema.sql index b05c2d6..9be396b 100644 --- a/inventory-server/db/setup-schema.sql +++ b/inventory-server/db/setup-schema.sql @@ -23,6 +23,26 @@ CREATE TABLE IF NOT EXISTS templates ( 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 CREATE TABLE IF NOT EXISTS ai_validation_performance ( id SERIAL PRIMARY KEY, @@ -50,4 +70,10 @@ $$ language 'plpgsql'; CREATE TRIGGER update_templates_updated_at BEFORE UPDATE ON templates 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(); \ No newline at end of file diff --git a/inventory-server/src/prompts/product-validation.txt b/inventory-server/src/prompts/product-validation.txt index 1b1b364..bec04df 100644 --- a/inventory-server/src/prompts/product-validation.txt +++ b/inventory-server/src/prompts/product-validation.txt @@ -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. Your response should be a JSON object with the following structure: diff --git a/inventory-server/src/routes/ai-prompts.js b/inventory-server/src/routes/ai-prompts.js new file mode 100644 index 0000000..3cfc127 --- /dev/null +++ b/inventory-server/src/routes/ai-prompts.js @@ -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; \ No newline at end of file diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js index 7d0e2db..2b30ba5 100644 --- a/inventory-server/src/routes/ai-validation.js +++ b/inventory-server/src/routes/ai-validation.js @@ -289,7 +289,76 @@ async function generateDebugResponse(productsToUse, res) { }); 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); // Create the response with taxonomy stats @@ -330,9 +399,16 @@ async function generateDebugResponse(productsToUse, res) { : null, } : null, - basePrompt: prompt, + basePrompt: generalPrompt.prompt_text, sampleFullPrompt: fullPrompt, promptLength: fullPrompt.length, + promptSources: { + generalPrompt: { + id: generalPrompt.id, + prompt_text: generalPrompt.prompt_text + }, + companyPrompts: companyPromptsWithNames + } }; 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 -async function loadPrompt(connection, productsToValidate = null) { +// Load prompts from database and inject taxonomy data +async function loadPrompt(connection, productsToValidate = null, appPool = null) { 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 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.`; + + // 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 (productsToValidate) { @@ -656,7 +817,7 @@ ${JSON.stringify(mixedTaxonomy.sizeCategories)}${ ----------Here is the product data to validate----------`; // Return the filtered prompt - return systemInstructions + basePrompt + "\n" + taxonomySection; + return systemInstructions + combinedPrompt + "\n" + taxonomySection; } // Generate the full unfiltered prompt @@ -687,7 +848,7 @@ ${JSON.stringify(taxonomy.artists)} Here is the product data to validate:`; - return systemInstructions + basePrompt + "\n" + taxonomySection; + return systemInstructions + combinedPrompt + "\n" + taxonomySection; } catch (error) { console.error("Error loading prompt:", error); 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 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); promptLength = fullPrompt.length; // Store prompt length for performance metrics console.log("📝 Generated prompt length:", promptLength); @@ -884,7 +1045,72 @@ router.post("/validate", async (req, res) => { 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({ success: true, changeDetails: changeDetails, @@ -895,6 +1121,7 @@ router.post("/validate", async (req, res) => { isEstimate: true, productCount: products.length }, + promptSources: promptSources, ...aiResponse, }); } catch (parseError) { diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 879dfa3..d09dbc6 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -18,6 +18,7 @@ const categoriesRouter = require('./routes/categories'); const importRouter = require('./routes/import'); const aiValidationRouter = require('./routes/ai-validation'); const templatesRouter = require('./routes/templates'); +const aiPromptsRouter = require('./routes/ai-prompts'); // Get the absolute path to the .env file const envPath = '/var/www/html/inventory/.env'; @@ -103,6 +104,7 @@ async function startServer() { app.use('/api/import', importRouter); app.use('/api/ai-validation', aiValidationRouter); app.use('/api/templates', templatesRouter); + app.use('/api/ai-prompts', aiPromptsRouter); // Basic health check route app.get('/health', (req, res) => { diff --git a/inventory-server/src/utils/db.js b/inventory-server/src/utils/db.js index 0ca0f63..5ece8ba 100644 --- a/inventory-server/src/utils/db.js +++ b/inventory-server/src/utils/db.js @@ -1,9 +1,9 @@ -const mysql = require('mysql2/promise'); +const { Pool } = require('pg'); let pool; function initPool(config) { - pool = mysql.createPool(config); + pool = new Pool(config); return pool; } diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx index a2fd85d..6dcca11 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx @@ -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 = ({ 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 = ({ // 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 = ({ {currentPrompt.isLoading ? (
) : ( -
- - - Prompt Length - - -
-
- - Characters: - {" "} - {promptLength} + <> +
+ + + Prompt Length + + +
+
+ + Characters: + {" "} + {promptLength} +
+
+ Tokens:{" "} + + ~{Math.round(promptLength / 4)} + +
-
- Tokens:{" "} - - ~{Math.round(promptLength / 4)} - -
-
- - + + - - - Cost Estimate - - -
-
- - { - const value = parseFloat(e.target.value); - if (!isNaN(value)) { - setCostPerMillionTokens(value); - } - }} - /> - + + + Cost Estimate + + +
+
+ + { + const value = parseFloat(e.target.value); + if (!isNaN(value)) { + setCostPerMillionTokens(value); + } + }} + /> + +
+
+ Cost:{" "} + + {calculateTokenCost(promptLength).toFixed(1)}¢ + +
-
- Cost:{" "} - - {calculateTokenCost(promptLength).toFixed(1)}¢ - -
-
- - + + - - - - Processing Time - - - -
- {debugData?.estimatedProcessingTime ? ( - debugData.estimatedProcessingTime.seconds ? ( - <> -
- - Estimated time: - {" "} - - {formatTime( - debugData.estimatedProcessingTime.seconds - )} - + + + + Processing Time + + + +
+ {currentPrompt.debugData?.estimatedProcessingTime ? ( + currentPrompt.debugData.estimatedProcessingTime.seconds ? ( + <> +
+ + Estimated time: + {" "} + + {formatTime( + currentPrompt.debugData.estimatedProcessingTime.seconds + )} + +
+
+ Based on{" "} + {currentPrompt.debugData.estimatedProcessingTime.sampleCount}{" "} + similar validation + {currentPrompt.debugData.estimatedProcessingTime + .sampleCount !== 1 + ? "s" + : ""} +
+ + ) : ( +
+ No historical data available for this prompt size
-
- Based on{" "} - {debugData.estimatedProcessingTime.sampleCount}{" "} - similar validation - {debugData.estimatedProcessingTime - .sampleCount !== 1 - ? "s" - : ""} -
- + ) ) : (
- No historical data available for this prompt size + No processing time data available
- ) - ) : ( -
- No processing time data available -
- )} -
-
-
-
+ )} +
+
+
+
+ + {/* Prompt Sources Section */} + {currentPrompt.debugData?.promptSources && ( + + + + Prompt Sources + {hasCompanyPrompts && ( + + {currentPrompt.debugData.promptSources.companyPrompts?.length} Company-Specific + + )} + + + +
+

+ + General + + Base prompt for all products +

+ + {hasCompanyPrompts && ( +
+

Company Specific:

+
    + {currentPrompt.debugData.promptSources.companyPrompts?.map((prompt, idx) => ( +
  • + {prompt.companyName} + (ID: {prompt.company}) +
  • + ))} +
+
+ )} +
+
+
+ )} + )}
{/* Prompt Section */}
- - {currentPrompt.isLoading ? ( -
- -
- ) : ( - - {currentPrompt.prompt} - - )} -
+ {currentPrompt.isLoading ? ( +
+ +
+ ) : ( + <> + {currentPrompt.debugData?.promptSources ? ( + + + Full Prompt + General Prompt + {hasCompanyPrompts && ( + Company Prompts + )} + + +
+ + + + {currentPrompt.prompt} + + + + + + {currentPrompt.debugData.promptSources.generalPrompt?.prompt_text || "No general prompt available"} + + + + {hasCompanyPrompts && ( + +
+ {currentPrompt.debugData.promptSources.companyPrompts?.map((prompt, idx) => ( +
+
+ {prompt.companyName} (ID: {prompt.company}) +
+ + {prompt.prompt_text} + +
+ ))} +
+
+ )} +
+
+
+ ) : ( + + + {currentPrompt.prompt} + + + )} + + )}
diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx index bbd10d8..a4694c8 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx @@ -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 = ( 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 = ( 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); diff --git a/inventory/src/components/settings/PromptManagement.tsx b/inventory/src/components/settings/PromptManagement.tsx new file mode 100644 index 0000000..cea958f --- /dev/null +++ b/inventory/src/components/settings/PromptManagement.tsx @@ -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(null); + const [editingPrompt, setEditingPrompt] = useState(null); + const [sorting, setSorting] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [formData, setFormData] = useState({ + prompt_text: "", + prompt_type: "general", + company: null, + }); + + const queryClient = useQueryClient(); + + const { data: prompts, isLoading } = useQuery({ + 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({ + 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[]>(() => [ + { + accessorKey: "prompt_type", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const type = row.getValue("prompt_type") as string; + return type === 'general' ? 'General' : 'Company Specific'; + }, + }, + { + accessorKey: "company", + header: ({ column }) => ( + + ), + 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 }) => ( + + ), + cell: ({ row }) => new Date(row.getValue("updated_at")).toLocaleDateString(), + }, + { + id: "actions", + cell: ({ row }) => ( +
+ + +
+ ), + }, + ], [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 ( +
+
+

AI Validation Prompts

+ +
+ +
+ setSearchQuery(e.target.value)} + className="max-w-sm" + /> +
+ + {isLoading ? ( +
Loading prompts...
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No prompts found + + + )} + +
+
+ )} + + {/* Prompt Form Dialog */} + + + + {editingPrompt ? "Edit Prompt" : "Create New Prompt"} + + {editingPrompt + ? "Update this AI validation prompt." + : "Create a new AI validation prompt that will be used during product validation."} + + + +
+
+
+ + + {generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && ( +

+ A general prompt already exists. You can only create company-specific prompts. +

+ )} +
+ + {formData.prompt_type === 'company_specific' && ( +
+ + +
+ )} + +
+ +