Layout/style tweaks, remove text file prompts, integrate system prompt into database/settings

This commit is contained in:
2025-03-24 12:26:21 -04:00
parent dd4b3f7145
commit 228ae8b2a9
7 changed files with 232 additions and 315 deletions

View File

@@ -27,13 +27,14 @@ CREATE TABLE IF NOT EXISTS templates (
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')),
prompt_type TEXT NOT NULL CHECK (prompt_type IN ('general', 'company_specific', 'system')),
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 = 'system' AND company IS NULL) OR
(prompt_type = 'company_specific' AND company IS NOT NULL)
)
);
@@ -43,6 +44,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_general_prompt
ON ai_prompts (prompt_type)
WHERE prompt_type = 'general';
-- Create a unique partial index to ensure only one system prompt
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_system_prompt
ON ai_prompts (prompt_type)
WHERE prompt_type = 'system';
-- AI Validation Performance Tracking
CREATE TABLE IF NOT EXISTS ai_validation_performance (
id SERIAL PRIMARY KEY,

View File

@@ -1,228 +0,0 @@
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:
{
"correctedData": [], // Array of corrected products
"changes": [], // Array of strings describing each change made
"warnings": [] // Array of strings with warnings or suggestions for manual review (see below for details)
}
IMPORTANT: For all fields that use IDs (categories, supplier, company, line, subline, ship_restrictions, tax_cat, artist, themes, etc.), you MUST return the ID values, not the display names. The system will handle converting IDs to display names.
Using the provided guidelines, focus on:
1. Correcting typos and any incorrect spelling or grammar
2. Standardizing product names
3. Correcting and enhancing descriptions by adding details, keywords, and SEO-friendly language
4. Fixing any obvious errors or inconsistencies between similar products in measurements, prices, or quantities
5. Adding correct categories, themes, and colors
Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is. All data passed in should be validated, corrected, and returned. All values returned should be strings, not numbers. Do not leave out any fields that were present in the original data.
Possible reasons for including a warning in the warnings array:
- If you're unable to make a change you're confident about but you believe one needs to be made
- If there are inconsistencies in the data that could be valid but need to be reviewed
- If not enough information is provided to make a change that you believe is needed
- If you infer a value for a required field based on context
----------PRODUCT FIELD GUIDELINES----------
Fields: supplier, private_notes, company, line, subline, artist
Changes: Not allowed
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return these fields exactly as provided with no changes
Fields: upc, supplier_no, notions_no, item_number
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim outside white space and return these fields exactly as provided with no other changes
Fields: hts_code
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim white space and any non-numeric characters, then return as a string. Do not validate in any other way.
Fields: image_url
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all comma-separated values to valid https:// URLs and return
Fields: msrp, cost_each
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip any currency symbols and return as a string with exactly two decimal places, even if the last place is a 0.
Fields: qty_per_unit, case_qty
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip non-numeric characters and return
Fields: ship_restrictions
Changes: Only add a value if it's not already present
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a value exactly as provided, or return 0 if no value is provided.
Fields: eta
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return a full month name, day is optional, no year ever (e.g. “January” or “March 3”). This value is not required if not provided.
Fields: name
Changes: Allowed to conform to guidelines, to fix typos or formatting
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most reasonable value possible based on the naming guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: description
Changes: Full creative control allowed within guidelines
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most accurate description possible based on the description guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: weight, length, width, height
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return your best guess based on the other information you have or the dimensions for similar products.
Instructions: Always return a reasonable value (weights in ounces and dimensions in inches) that is validated against similar provided products and your knowledge of general object measurements (e.g. a sheet of paper is not going to be 3 inches thick, a pack of stickers is not going to be 250 ounces, this sheet of paper is very likely going to be the same size as that other sheet of paper from the same line). If a value is unusual or unreasonable, even wildly so, change it to match similar products or to be more reasonable. When correcting unreasonable weights or dimensions, prioritize comparisons to products from the same company and product line first, then broader category matches or common knowledge if necessary.Do not return 0 or null for any of these fields.
Fields: coo
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all country names and abbreviations to the official ISO 3166-1 alpha-2 two-character country code. Convert any value with more than two characters to two characters only (e.g. "United States" or "USA" should both return "US").
Fields: tax_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a valid numerical tax code ID from the Available Tax Codes array below. Give preference to the value provided, but correct it if another value is more accurate. You must return a value for this field. 0 should be the default value in most cases.
Fields: size_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return if not applicable (e.g. if no size categories apply based on what you know about the product).
Instructions: If present or if applicable, return one valid numerical size category ID from the Available Size Categories array below. Give preference to the value provided, but correct it if another value is more accurate. If the product name contains a match for one of the size categories (such as 12x12, 6x6, 2oz, etc) you MUST return that size category with the results. A value is not required if none of the size categories apply.
Fields: themes
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no themes apply based on what you know about the product).
Instructions: If present, confirm that each provided theme matches what you understand to be a theme of the product. Remove any themes that do not match and add any themes that are missing. Most products will have zero or one theme. Return a comma-separated list of numerical theme IDs from the Available Themes array below. If you choose a sub-theme, you do not need to include its parent theme in the list.
Fields: colors
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no colors apply based on what you know about the product).
Instructions: If present or if applicable, return a comma-separated list of numerical color IDs from the Available Colors array below, using the product name as the primary guide (e.g. if the name contains Blue or a blue variant, you should return the blue color ID). A value is not required if none of the colors apply. Most products will have zero colors.
Fields: categories
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return at least one value for this field, even if it's not provided in the original data. If no value is provided, return the most appropriate category or categories based on the other information you have.
Instructions: Always return a comma-separated list of one or more valid numerical category IDs from the Available Categories array below. Give preference to the values provided, particularly if the other information isn't enough to determine a category, but correct them or add new categories if another value is more accurate. Do not return categories in the Deals or Black Friday categories, and strip these from the list if present. If you choose a subcategory at any level, you do not need to include its parent categories in the list. You must return at least one category and you can return multiple categories if applicable. All categories have equal value so their order is not important. Always try to return the most specific categories possible (e.g. one in the third level of the category hierarchy is better than one in the second level).
----------PRODUCT NAMING GUIDELINES----------
If there's only one of this type of product in a line: [Line Name] [Product Name] - [Company]
Example: "Cosmos Infinity Chipboard - Stamperia"
Example: "Serene Petals 6x6 Paper Pad - Prima"
Multiple similar products in a line: [Differentiator] [Product Type] - [Line Name] - [Company]
Example: "Ice & Shells Stencil - Arctic Antarctic - Stamperia"
Example: "Astronomy Paper - Cosmos Infinity - Stamperia"
Standalone products: [Product Name] - [Company]
Example: "Hedwig Puffy Stickers - Paper House Productions"
Example: "Heart Tree Dies - Lawn Fawn"
Color-based products: [Color] [Product Name] - [Company]
Example: "Green Valley Enamel Dots - Altenew"
Example: "Magenta Aqua Pigment - Brutus Monroe"
Complex products: [Differentiator] [Line] [Product Type] - [Company]
Example: "Size 6 Round Black Velvet Watercolor Brush - Silver Brush Limited" (Size 6 Round is the differentiator, Black Velvet is the line, Watercolor Brush is the product type)
These should not be included in the name, unless there are multiple products that are otherwise identical:
- Product size
- Product weight
- Number of pages
- How many are in the package
Naming Conventions:
- Paper sizes: Use "12x12", "8x8", "6x6" (no spaces or units of measure)
- Company names must match backend exactly
- Always capitalize every word in the name, including short articles like "The" and "An"
- Use "Idea-ology" (not "idea-ology" or "Ideaology")
- All stamps are "Stamp Set" (not "Clear Stamps" or "Rubber Stamps")
- All dies are "Dies" or "Die" (not "Die Set")
- Brands with their own naming conventions should be respected, such as "Doodle Cuts" for dies from Doodlebug
Special Brand Rules - Ranger:
Format: [Product Name] - [Designer Line] - Ranger
Possible Designers: Dylusions, Dina Wakley MEdia, Simon Hurley create., Wendy Vecchi
Example: "Stacked Stencil - Dina Wakley MEdia - Ranger"
Special Brand Rules - Tim Holtz products from Ranger:
Format: [Color] [Product Name/Type] - Tim Holtz Distress - Ranger
Example: "Mermaid Lagoon Tim Holtz Distress Oxide Ink Pad - Ranger"
Special Brand Rules - Tim Holtz products from Sizzix or Stampers Anonymous:
Format: [Product Name] [Product Type] by Tim Holtz - [Company]
Example: "Leaf Fragments Thinlits Dies by Tim Holtz - Sizzix"
Special Brand Rules - Tim Holtz products from Advantus/Idea-ology:
Format: [Product Name] - Tim Holtz Idea-ology
Example: "Tiny Vials - Tim Holtz Idea-ology"
Special Brand Rules - Dies from Sizzix:
Include die type plus "Dies" or "Die"
Examples:
"Art Nouveau 3-D Textured Impressions Embossing Folder - Sizzix"
"Pocket Pals Thinlits Dies - Sizzix"
"Butterfly Wishes Framelits Dies & Stamps - Sizzix"
Important Notes
- Ensure that product names are consistent across all products of the same type
- Use the minimum amount of information needed to uniquely identify the product
- Put detailed specifications in the product description, not its name
Edge Cases
- If the product is missing a company name, infer one from the other products included in the data
- If the product is missing a clear differentiator and needs one to be unique, infer and add one from the other data provided (e.g. the description, existing size categories, etc.)
Incorrect example: MVP Rugby - Collection Pack - Photoplay
Notes: there should be no dash between the line and the product
Incorrect Example: A2 Easel Cards - Black - Photoplay
Notes: the differentiating factor should come first: “Black A2 Easel Cards - Photoplay”. Size is ok to include here because this is the name printed on the package.
Incorrect Example: 6” - Scriber Needle Modeling Tool
Notes: this product only comes in one size, so 6” isnt needed. The company name should also be included.
Incorrect Example: Slick - White - Tulip Dimensional Fabric Paint 4oz
Notes: color should be first, then type, then product, then company, so “White Slick Dimensional Fabric Paint - Tulip”. It appears theres only one size available so no need to differentiate in the name.
Incorrect Example: Silhouette Adhesive Cork Sheets 5”X7” 8/Pkg
Notes: should be “Adhesive Cork Sheets - Silhouette”
Incorrect Example: Galaxy - Opaque - American Crafts Color Pour Resin Dyes
Notes: “Galaxy Opaque Dye Set - Color Pour Resin - American Crafts”
Incorrect Example: Slate - Lion Brand Truboo Yarn
Notes: [Differentiator] [Line] [Product Type] - [Company] : “Slate Truboo Yarn - Lion Brand”
Incorrect Example: Rose Quartz Dylusions Shimmer Paint
Notes: “Rose Quartz Shimmer Paint - Dylusions - Ranger”
----------PRODUCT DESCRIPTION GUIDELINES----------
Product descriptions are an extremely important part of the listing and are the most important part of your response. Care should be taken to ensure they are correct, helpful, and SEO-friendly.
If a description is provided in the data, use it as a starting point. Correct any spelling errors, typos, poor grammar, or awkward phrasing. If necessary and you have the information, add more details, describe how the customer could use it, etc. Use complete sentences and keep SEO in mind.
If no description is provided, make one up using the product name, the information you have, and the other provided guidelines. At minimum, a description should be one complete sentence that starts with a capital letter and ends with a period. Unless the product is extremely complex, 2-4 sentences is usually sufficient if you have enough information.
Important Notes:
- Every description should state exactly what's included in the product (e.g. "Includes one 12x12 sheet of patterned cardstock." or "Includes one 6x12 sheet with 27 unique stickers." or "Includes 55 pieces." or "Package includes machine, power cord, 12 sheets of cardstock, 3 dies, and project instructions.")
- Do not use the word "our" in the description (this usually shows up when we copy a description from the manufacturer). Instead use "these" or "[Company name] [product]" or similar. (e.g. don't use "Our journals are hand-made in the USA", instead use "These journals are hand made..." or "Archer & Olive journals are handmade...")
- Don't include statements that add no value like “this is perfect for all your paper crafts”. If the product helps to solve a unique problem or has a unique feature, by all means describe it, but if its just a normal sheet of paper or pack of stickers, you dont have to pretend like its the best thing ever. At the same time, ensure that you add enough copy to ensure good SEO.
- State as many facts as you can about the product, considering the viewpoint of the customer and what they would want to know when looking at it. They probably want to know dimensions, what products its compatible with, how thick the paper is, how many sheets are included, whether the sheets are double-sided or not, which items are in the kit, etc. Say as much as you possibly can with the information that you have.
- !!DO NOT make up information if you aren't sure about it. A minimal correct description is better than a long incorrect one!!
Avoid/remove:
- The word "Imported"
- Any warnings about Prop 65, choking hazards, etc
- The manufacturer's name if it's included as the very first thing in the description
- Any statement similar to "comes in a variety of colors, each sold separately"

View File

@@ -106,6 +106,33 @@ router.get('/type/general', async (req, res) => {
}
});
// Get system prompt
router.get('/type/system', 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 = 'system'
`);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'System AI prompt not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching system AI prompt:', error);
res.status(500).json({
error: 'Failed to fetch system AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Create new AI prompt
router.post('/', async (req, res) => {
try {
@@ -121,8 +148,8 @@ router.post('/', async (req, res) => {
}
// 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"' });
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
}
// Validate company is provided for company-specific prompts
@@ -130,6 +157,11 @@ router.post('/', async (req, res) => {
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
}
// Validate company is not provided for general or system prompts
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
}
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
@@ -164,6 +196,11 @@ router.post('/', async (req, res) => {
error: 'A general prompt already exists',
details: error.message
});
} else if (error.message.includes('idx_unique_system_prompt')) {
return res.status(409).json({
error: 'A system prompt already exists',
details: error.message
});
}
}
@@ -190,14 +227,19 @@ router.put('/:id', async (req, res) => {
}
// 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"' });
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
}
// 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' });
}
// Validate company is not provided for general or system prompts
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
}
const pool = req.app.locals.pool;
if (!pool) {
@@ -241,6 +283,11 @@ router.put('/:id', async (req, res) => {
error: 'A general prompt already exists',
details: error.message
});
} else if (error.message.includes('idx_unique_system_prompt')) {
return res.status(409).json({
error: 'A system prompt already exists',
details: error.message
});
}
}

View File

@@ -296,7 +296,22 @@ async function generateDebugResponse(productsToUse, res) {
throw new Error("Database connection not available");
}
// First, fetch the general prompt
// First, fetch the system prompt
const systemPromptResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'system'
`);
// Get system prompt or use default
let systemPrompt = null;
if (systemPromptResult.rows.length > 0) {
systemPrompt = systemPromptResult.rows[0];
console.log("📝 Loaded system prompt from database, ID:", systemPrompt.id);
} else {
console.warn("⚠️ No system prompt found in database, will use default");
}
// Then, fetch the general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'general'
@@ -337,7 +352,7 @@ async function generateDebugResponse(productsToUse, res) {
}
}
// Find company names from taxonomy
// 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)) {
@@ -399,10 +414,21 @@ async function generateDebugResponse(productsToUse, res) {
: null,
}
: null,
basePrompt: generalPrompt.prompt_text,
basePrompt: systemPrompt ? systemPrompt.prompt_text + "\n\n" + generalPrompt.prompt_text : generalPrompt.prompt_text,
sampleFullPrompt: fullPrompt,
promptLength: fullPrompt.length,
promptSources: {
...(systemPrompt ? {
systemPrompt: {
id: systemPrompt.id,
prompt_text: systemPrompt.prompt_text
}
} : {
systemPrompt: {
id: 0,
prompt_text: `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.`
}
}),
generalPrompt: {
id: generalPrompt.id,
prompt_text: generalPrompt.prompt_text
@@ -595,9 +621,6 @@ async function loadPrompt(connection, productsToValidate = null, appPool = null)
// Get taxonomy data using the provided MySQL connection
const taxonomy = await getTaxonomyData(connection);
// 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) {
@@ -605,6 +628,23 @@ async function loadPrompt(connection, productsToValidate = null, appPool = null)
throw new Error("Database connection not available");
}
// Fetch the system prompt
const systemPromptResult = await pool.query(`
SELECT * FROM ai_prompts
WHERE prompt_type = 'system'
`);
// Default system instructions in case the system prompt is not found
let 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.`;
// If system prompt exists in the database, use it
if (systemPromptResult.rows.length > 0) {
systemInstructions = systemPromptResult.rows[0].prompt_text;
console.log("📝 Loaded system prompt from database");
} else {
console.warn("⚠️ No system prompt found in database, using default");
}
// Fetch the general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts
@@ -646,26 +686,6 @@ async function loadPrompt(connection, productsToValidate = null, appPool = null)
}
}
// 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;
@@ -816,8 +836,8 @@ ${JSON.stringify(mixedTaxonomy.sizeCategories)}${
----------Here is the product data to validate----------`;
// Return the filtered prompt
return systemInstructions + combinedPrompt + "\n" + taxonomySection;
// Return the filtered prompt with system instructions first
return systemInstructions + "\n\n" + combinedPrompt + "\n" + taxonomySection;
}
// Generate the full unfiltered prompt
@@ -848,7 +868,8 @@ ${JSON.stringify(taxonomy.artists)}
Here is the product data to validate:`;
return systemInstructions + combinedPrompt + "\n" + taxonomySection;
// Return the full prompt with system instructions first
return systemInstructions + "\n\n" + combinedPrompt + "\n" + taxonomySection;
} catch (error) {
console.error("Error loading prompt:", error);
throw error; // Re-throw to be handled by the calling function
@@ -1049,6 +1070,11 @@ router.post("/validate", async (req, res) => {
let promptSources = null;
try {
// Get system prompt
const systemPromptResult = await pool.query(`
SELECT * FROM ai_prompts WHERE prompt_type = 'system'
`);
// Get general prompt
const generalPromptResult = await pool.query(`
SELECT * FROM ai_prompts WHERE prompt_type = 'general'
@@ -1074,7 +1100,7 @@ router.post("/validate", async (req, res) => {
companyPrompts = companyPromptsResult.rows;
}
// Find company names from taxonomy
// 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)) {
@@ -1097,7 +1123,24 @@ router.post("/validate", async (req, res) => {
// Set prompt sources
if (generalPromptResult.rows.length > 0) {
const generalPrompt = generalPromptResult.rows[0];
let systemPrompt = null;
if (systemPromptResult.rows.length > 0) {
systemPrompt = systemPromptResult.rows[0];
}
promptSources = {
...(systemPrompt ? {
systemPrompt: {
id: systemPrompt.id,
prompt_text: systemPrompt.prompt_text
}
} : {
systemPrompt: {
id: 0,
prompt_text: `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.`
}
}),
generalPrompt: {
id: generalPrompt.id,
prompt_text: generalPrompt.prompt_text

View File

@@ -252,39 +252,26 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
{/* Prompt Sources Section */}
{currentPrompt.debugData?.promptSources && (
<Card className="mb-4">
<Card className="py-2">
<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>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-purple-50">
System
</Badge>
<Badge variant="outline" className="bg-green-50">
General
</Badge>
{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>
)}
{currentPrompt.debugData.promptSources.companyPrompts?.map((prompt, idx) => (
<Badge key={idx} variant="outline" className="bg-blue-50">
{prompt.companyName}
</Badge>
))}
</div>
</CardContent>
</Card>
@@ -306,10 +293,11 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
defaultValue="full"
value={activeTab}
onValueChange={setActiveTab}
className="w-full h-full flex flex-col"
className="h-full flex flex-col"
>
<TabsList className="mb-2 flex-shrink-0">
<TabsList className="mb-2 w-fit">
<TabsTrigger value="full">Full Prompt</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
<TabsTrigger value="general">General Prompt</TabsTrigger>
{hasCompanyPrompts && (
<TabsTrigger value="company">Company Prompts</TabsTrigger>
@@ -324,6 +312,12 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
</Code>
</TabsContent>
<TabsContent value="system" className="m-0 p-0 h-full">
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
{currentPrompt.debugData.promptSources.systemPrompt?.prompt_text || "No system prompt available"}
</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"}

View File

@@ -57,6 +57,7 @@ export interface CurrentPrompt {
sampleFullPrompt: string;
promptLength: number;
promptSources?: {
systemPrompt?: { id: number; prompt_text: string };
generalPrompt?: { id: number; prompt_text: string };
companyPrompts?: Array<{
id: number;

View File

@@ -30,16 +30,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
@@ -60,14 +51,14 @@ interface FieldOption {
interface PromptFormData {
id?: number;
prompt_text: string;
prompt_type: 'general' | 'company_specific';
prompt_type: 'general' | 'company_specific' | 'system';
company: string | null;
}
interface AiPrompt {
id: number;
prompt_text: string;
prompt_type: 'general' | 'company_specific';
prompt_type: 'general' | 'company_specific' | 'system';
company: string | null;
created_at: string;
updated_at: string;
@@ -82,7 +73,10 @@ export function PromptManagement() {
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 [sorting, setSorting] = useState<SortingState>([
{ id: "prompt_type", desc: true },
{ id: "company", desc: false }
]);
const [searchQuery, setSearchQuery] = useState("");
const [formData, setFormData] = useState<PromptFormData>({
prompt_text: "",
@@ -114,10 +108,14 @@ export function PromptManagement() {
},
});
// Check if a general prompt already exists
// Check if general and system prompts already exist
const generalPromptExists = useMemo(() => {
return prompts?.some(prompt => prompt.prompt_type === 'general');
}, [prompts]);
const systemPromptExists = useMemo(() => {
return prompts?.some(prompt => prompt.prompt_type === 'system');
}, [prompts]);
const createMutation = useMutation({
mutationFn: async (data: PromptFormData) => {
@@ -220,10 +218,10 @@ export function PromptManagement() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// If prompt_type is general, ensure company is null
// If prompt_type is general or system, ensure company is null
const submitData = {
...formData,
company: formData.prompt_type === 'general' ? null : formData.company,
company: formData.prompt_type === 'company_specific' ? formData.company : null,
};
if (editingPrompt) {
@@ -246,12 +244,24 @@ export function PromptManagement() {
const handleCreateClick = () => {
resetForm();
// If general prompt exists, default to company-specific
if (generalPromptExists) {
// If general prompt and system prompt exist, default to company-specific
if (generalPromptExists && systemPromptExists) {
setFormData(prev => ({
...prev,
prompt_type: 'company_specific'
}));
} else if (generalPromptExists && !systemPromptExists) {
// If general exists but system doesn't, suggest system prompt
setFormData(prev => ({
...prev,
prompt_type: 'system'
}));
} else if (!generalPromptExists) {
// If no general prompt, suggest that first
setFormData(prev => ({
...prev,
prompt_type: 'general'
}));
}
setIsFormOpen(true);
@@ -271,7 +281,26 @@ export function PromptManagement() {
),
cell: ({ row }) => {
const type = row.getValue("prompt_type") as string;
return type === 'general' ? 'General' : 'Company Specific';
if (type === 'general') return 'General';
if (type === 'system') return 'System';
return 'Company Specific';
},
},
{
accessorFn: (row) => row.prompt_text.length,
id: "length",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Length
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ getValue }) => {
const length = getValue() as number;
return <span>{length.toLocaleString()}</span>;
},
},
{
@@ -431,24 +460,44 @@ export function PromptManagement() {
<Label htmlFor="prompt_type">Prompt Type</Label>
<Select
value={formData.prompt_type}
onValueChange={(value: 'general' | 'company_specific') =>
onValueChange={(value: 'general' | 'company_specific' | 'system') =>
setFormData({ ...formData, prompt_type: value })
}
disabled={generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id}
disabled={(generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id) ||
(systemPromptExists && formData.prompt_type !== 'system' && !editingPrompt?.id)}
>
<SelectTrigger>
<SelectValue placeholder="Select prompt type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general" disabled={generalPromptExists && !editingPrompt?.prompt_type?.includes('general')}>
<SelectItem
value="general"
disabled={generalPromptExists && !editingPrompt?.prompt_type?.includes('general')}
>
General
</SelectItem>
<SelectItem
value="system"
disabled={systemPromptExists && !editingPrompt?.prompt_type?.includes('system')}
>
System
</SelectItem>
<SelectItem value="company_specific">Company Specific</SelectItem>
</SelectContent>
</Select>
{generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && (
{generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && systemPromptExists && formData.prompt_type !== 'system' && (
<p className="text-xs text-muted-foreground">
A general prompt already exists. You can only create company-specific prompts.
General and system prompts already exist. You can only create company-specific prompts.
</p>
)}
{generalPromptExists && !systemPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && (
<p className="text-xs text-muted-foreground">
A general prompt already exists. You can create a system prompt or company-specific prompts.
</p>
)}
{systemPromptExists && !generalPromptExists && formData.prompt_type !== 'system' && !editingPrompt?.id && (
<p className="text-xs text-muted-foreground">
A system prompt already exists. You can create a general prompt or company-specific prompts.
</p>
)}
</div>
@@ -481,10 +530,15 @@ export function PromptManagement() {
id="prompt_text"
value={formData.prompt_text}
onChange={(e) => setFormData({ ...formData, prompt_text: e.target.value })}
placeholder="Enter your validation prompt text..."
placeholder={`Enter your ${formData.prompt_type === 'system' ? 'system instructions' : 'validation prompt'} text...`}
className="h-80 font-mono text-sm"
required
/>
{formData.prompt_type === 'system' && (
<p className="text-xs text-muted-foreground mt-1">
System prompts provide the initial instructions to the AI. This sets the tone and approach for all validations.
</p>
)}
</div>
</div>