Add per-product AI verification disable option

Users can now disable AI verification for individual products that
AI is having trouble with (e.g., Amazon products where AI keeps
picking the main buy box price instead of "other sellers").

Changes:
- Add ai_verification_disabled column to products table
- Add toggle in product detail page under "Advanced Settings"
- Pass skip flag to scrapeProductWithVoting
- Skip AI verification when flag is set

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
clucraft 2026-01-24 20:32:25 -05:00
parent d2e1cc70fc
commit b9d8d15e68
7 changed files with 147 additions and 6 deletions

View file

@ -168,6 +168,10 @@ async function runMigrations() {
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'anchor_price') THEN
ALTER TABLE products ADD COLUMN anchor_price DECIMAL(10,2);
END IF;
-- Per-product AI verification disable flag
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_verification_disabled') THEN
ALTER TABLE products ADD COLUMN ai_verification_disabled BOOLEAN DEFAULT false;
END IF;
END $$;
`);

View file

@ -313,6 +313,7 @@ export interface Product {
price_drop_threshold: number | null;
target_price: number | null;
notify_back_in_stock: boolean;
ai_verification_disabled: boolean;
created_at: Date;
}
@ -476,6 +477,7 @@ export const productQueries = {
price_drop_threshold?: number | null;
target_price?: number | null;
notify_back_in_stock?: boolean;
ai_verification_disabled?: boolean;
}
): Promise<Product | null> => {
const fields: string[] = [];
@ -502,6 +504,10 @@ export const productQueries = {
fields.push(`notify_back_in_stock = $${paramIndex++}`);
values.push(updates.notify_back_in_stock);
}
if (updates.ai_verification_disabled !== undefined) {
fields.push(`ai_verification_disabled = $${paramIndex++}`);
values.push(updates.ai_verification_disabled);
}
if (fields.length === 0) return null;
@ -581,6 +587,14 @@ export const productQueries = {
);
return result.rows[0]?.anchor_price ? parseFloat(result.rows[0].anchor_price) : null;
},
isAiVerificationDisabled: async (id: number): Promise<boolean> => {
const result = await pool.query(
'SELECT ai_verification_disabled FROM products WHERE id = $1',
[id]
);
return result.rows[0]?.ai_verification_disabled === true;
},
};
// Price History types and queries

View file

@ -219,7 +219,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
return;
}
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock } = req.body;
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled } = req.body;
const product = await productQueries.update(productId, userId, {
name,
@ -227,6 +227,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
price_drop_threshold,
target_price,
notify_back_in_stock,
ai_verification_disabled,
});
if (!product) {

View file

@ -29,14 +29,18 @@ async function checkPrices(): Promise<void> {
// Get anchor price for variant products (the price the user confirmed)
const anchorPrice = await productQueries.getAnchorPrice(product.id);
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}`);
// Check if AI verification is disabled for this product
const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id);
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAi: ${skipAiVerification}`);
// Use voting scraper with preferred method and anchor price if available
const scrapedData = await scrapeProductWithVoting(
product.url,
product.user_id,
preferredMethod as ExtractionMethod | undefined,
anchorPrice || undefined
anchorPrice || undefined,
skipAiVerification
);
console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`);

View file

@ -1355,12 +1355,14 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
*
* @param anchorPrice - The price the user previously confirmed. Used to select the correct
* variant on refresh when multiple prices are found.
* @param skipAiVerification - If true, skip AI verification entirely for this product.
*/
export async function scrapeProductWithVoting(
url: string,
userId?: number,
preferredMethod?: ExtractionMethod,
anchorPrice?: number
anchorPrice?: number,
skipAiVerification?: boolean
): Promise<ScrapedProductWithCandidates> {
const result: ScrapedProductWithCandidates = {
name: null,
@ -1637,10 +1639,12 @@ export async function scrapeProductWithVoting(
}
// If we have a price but AI is available, verify it
// SKIP verification if we have multiple candidates - let user choose from modal instead
// SKIP verification if:
// - User disabled AI verification for this product
// - We have multiple candidates (let user choose from modal instead)
// This prevents AI from "correcting" valid alternative prices (e.g., other sellers on Amazon)
const hasMultipleCandidates = allCandidates.length > 1;
if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates) {
if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates && !skipAiVerification) {
try {
const { tryAIVerification } = await import('./ai-extractor');
const verifyResult = await tryAIVerification(

View file

@ -65,6 +65,7 @@ export interface Product {
price_drop_threshold: number | null;
target_price: number | null;
notify_back_in_stock: boolean;
ai_verification_disabled: boolean;
created_at: string;
current_price: number | null;
currency: string | null;
@ -131,6 +132,7 @@ export const productsApi = {
price_drop_threshold?: number | null;
target_price?: number | null;
notify_back_in_stock?: boolean;
ai_verification_disabled?: boolean;
}) => api.put<Product>(`/products/${id}`, data),
delete: (id: number) => api.delete(`/products/${id}`),

View file

@ -30,6 +30,7 @@ export default function ProductDetail() {
const [priceDropThreshold, setPriceDropThreshold] = useState<string>('');
const [targetPrice, setTargetPrice] = useState<string>('');
const [notifyBackInStock, setNotifyBackInStock] = useState(false);
const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false);
const REFRESH_INTERVALS = [
{ value: 300, label: '5 minutes' },
@ -62,6 +63,7 @@ export default function ProductDetail() {
setTargetPrice(productRes.data.target_price.toString());
}
setNotifyBackInStock(productRes.data.notify_back_in_stock || false);
setAiVerificationDisabled(productRes.data.ai_verification_disabled || false);
} catch {
setError('Failed to load product details');
} finally {
@ -139,12 +141,14 @@ export default function ProductDetail() {
price_drop_threshold: threshold,
target_price: target,
notify_back_in_stock: notifyBackInStock,
ai_verification_disabled: aiVerificationDisabled,
});
setProduct({
...product,
price_drop_threshold: threshold,
target_price: target,
notify_back_in_stock: notifyBackInStock,
ai_verification_disabled: aiVerificationDisabled,
});
showToast('Notification settings saved');
} catch {
@ -763,6 +767,114 @@ export default function ProductDetail() {
</div>
</>
)}
<style>{`
.advanced-settings-card {
background: var(--surface);
border-radius: 0.75rem;
box-shadow: var(--shadow);
padding: 1.5rem;
margin-top: 2rem;
}
.advanced-settings-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.advanced-settings-icon {
font-size: 1.5rem;
}
.advanced-settings-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text);
}
.advanced-settings-description {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.advanced-checkbox-group {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--background);
border-radius: 0.375rem;
cursor: pointer;
}
.advanced-checkbox-group:hover {
background: var(--border);
}
.advanced-checkbox-group input[type="checkbox"] {
width: 1.125rem;
height: 1.125rem;
accent-color: var(--primary);
cursor: pointer;
}
.advanced-checkbox-label {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.advanced-checkbox-label span:first-child {
font-size: 0.875rem;
font-weight: 500;
color: var(--text);
}
.advanced-checkbox-label span:last-child {
font-size: 0.75rem;
color: var(--text-muted);
}
.advanced-settings-actions {
margin-top: 1rem;
}
`}</style>
<div className="advanced-settings-card">
<div className="advanced-settings-header">
<span className="advanced-settings-icon"></span>
<h2 className="advanced-settings-title">Advanced Settings</h2>
</div>
<p className="advanced-settings-description">
Fine-tune how price extraction works for this product.
</p>
<label className="advanced-checkbox-group">
<input
type="checkbox"
checked={aiVerificationDisabled}
onChange={(e) => setAiVerificationDisabled(e.target.checked)}
/>
<div className="advanced-checkbox-label">
<span>Disable AI Verification</span>
<span>Prevent AI from "correcting" the scraped price. Useful when AI keeps picking the wrong price (e.g., main price instead of other sellers on Amazon).</span>
</div>
</label>
<div className="advanced-settings-actions">
<button
className="btn btn-primary"
onClick={handleSaveNotifications}
disabled={isSavingNotifications}
>
{isSavingNotifications ? 'Saving...' : 'Save Settings'}
</button>
</div>
</div>
</Layout>
);
}