Skip to content

Commit b9d8d15

Browse files
clucraftclaude
andcommitted
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>
1 parent d2e1cc7 commit b9d8d15

File tree

7 files changed

+147
-6
lines changed

7 files changed

+147
-6
lines changed

backend/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ async function runMigrations() {
168168
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'anchor_price') THEN
169169
ALTER TABLE products ADD COLUMN anchor_price DECIMAL(10,2);
170170
END IF;
171+
-- Per-product AI verification disable flag
172+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_verification_disabled') THEN
173+
ALTER TABLE products ADD COLUMN ai_verification_disabled BOOLEAN DEFAULT false;
174+
END IF;
171175
END $$;
172176
`);
173177

backend/src/models/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export interface Product {
313313
price_drop_threshold: number | null;
314314
target_price: number | null;
315315
notify_back_in_stock: boolean;
316+
ai_verification_disabled: boolean;
316317
created_at: Date;
317318
}
318319

@@ -476,6 +477,7 @@ export const productQueries = {
476477
price_drop_threshold?: number | null;
477478
target_price?: number | null;
478479
notify_back_in_stock?: boolean;
480+
ai_verification_disabled?: boolean;
479481
}
480482
): Promise<Product | null> => {
481483
const fields: string[] = [];
@@ -502,6 +504,10 @@ export const productQueries = {
502504
fields.push(`notify_back_in_stock = $${paramIndex++}`);
503505
values.push(updates.notify_back_in_stock);
504506
}
507+
if (updates.ai_verification_disabled !== undefined) {
508+
fields.push(`ai_verification_disabled = $${paramIndex++}`);
509+
values.push(updates.ai_verification_disabled);
510+
}
505511

506512
if (fields.length === 0) return null;
507513

@@ -581,6 +587,14 @@ export const productQueries = {
581587
);
582588
return result.rows[0]?.anchor_price ? parseFloat(result.rows[0].anchor_price) : null;
583589
},
590+
591+
isAiVerificationDisabled: async (id: number): Promise<boolean> => {
592+
const result = await pool.query(
593+
'SELECT ai_verification_disabled FROM products WHERE id = $1',
594+
[id]
595+
);
596+
return result.rows[0]?.ai_verification_disabled === true;
597+
},
584598
};
585599

586600
// Price History types and queries

backend/src/routes/products.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,15 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
219219
return;
220220
}
221221

222-
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock } = req.body;
222+
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled } = req.body;
223223

224224
const product = await productQueries.update(productId, userId, {
225225
name,
226226
refresh_interval,
227227
price_drop_threshold,
228228
target_price,
229229
notify_back_in_stock,
230+
ai_verification_disabled,
230231
});
231232

232233
if (!product) {

backend/src/services/scheduler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ async function checkPrices(): Promise<void> {
2929
// Get anchor price for variant products (the price the user confirmed)
3030
const anchorPrice = await productQueries.getAnchorPrice(product.id);
3131

32-
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}`);
32+
// Check if AI verification is disabled for this product
33+
const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id);
34+
35+
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAi: ${skipAiVerification}`);
3336

3437
// Use voting scraper with preferred method and anchor price if available
3538
const scrapedData = await scrapeProductWithVoting(
3639
product.url,
3740
product.user_id,
3841
preferredMethod as ExtractionMethod | undefined,
39-
anchorPrice || undefined
42+
anchorPrice || undefined,
43+
skipAiVerification
4044
);
4145

4246
console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`);

backend/src/services/scraper.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,12 +1355,14 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
13551355
*
13561356
* @param anchorPrice - The price the user previously confirmed. Used to select the correct
13571357
* variant on refresh when multiple prices are found.
1358+
* @param skipAiVerification - If true, skip AI verification entirely for this product.
13581359
*/
13591360
export async function scrapeProductWithVoting(
13601361
url: string,
13611362
userId?: number,
13621363
preferredMethod?: ExtractionMethod,
1363-
anchorPrice?: number
1364+
anchorPrice?: number,
1365+
skipAiVerification?: boolean
13641366
): Promise<ScrapedProductWithCandidates> {
13651367
const result: ScrapedProductWithCandidates = {
13661368
name: null,
@@ -1637,10 +1639,12 @@ export async function scrapeProductWithVoting(
16371639
}
16381640

16391641
// If we have a price but AI is available, verify it
1640-
// SKIP verification if we have multiple candidates - let user choose from modal instead
1642+
// SKIP verification if:
1643+
// - User disabled AI verification for this product
1644+
// - We have multiple candidates (let user choose from modal instead)
16411645
// This prevents AI from "correcting" valid alternative prices (e.g., other sellers on Amazon)
16421646
const hasMultipleCandidates = allCandidates.length > 1;
1643-
if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates) {
1647+
if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates && !skipAiVerification) {
16441648
try {
16451649
const { tryAIVerification } = await import('./ai-extractor');
16461650
const verifyResult = await tryAIVerification(

frontend/src/api/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface Product {
6565
price_drop_threshold: number | null;
6666
target_price: number | null;
6767
notify_back_in_stock: boolean;
68+
ai_verification_disabled: boolean;
6869
created_at: string;
6970
current_price: number | null;
7071
currency: string | null;
@@ -131,6 +132,7 @@ export const productsApi = {
131132
price_drop_threshold?: number | null;
132133
target_price?: number | null;
133134
notify_back_in_stock?: boolean;
135+
ai_verification_disabled?: boolean;
134136
}) => api.put<Product>(`/products/${id}`, data),
135137

136138
delete: (id: number) => api.delete(`/products/${id}`),

frontend/src/pages/ProductDetail.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function ProductDetail() {
3030
const [priceDropThreshold, setPriceDropThreshold] = useState<string>('');
3131
const [targetPrice, setTargetPrice] = useState<string>('');
3232
const [notifyBackInStock, setNotifyBackInStock] = useState(false);
33+
const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false);
3334

3435
const REFRESH_INTERVALS = [
3536
{ value: 300, label: '5 minutes' },
@@ -62,6 +63,7 @@ export default function ProductDetail() {
6263
setTargetPrice(productRes.data.target_price.toString());
6364
}
6465
setNotifyBackInStock(productRes.data.notify_back_in_stock || false);
66+
setAiVerificationDisabled(productRes.data.ai_verification_disabled || false);
6567
} catch {
6668
setError('Failed to load product details');
6769
} finally {
@@ -139,12 +141,14 @@ export default function ProductDetail() {
139141
price_drop_threshold: threshold,
140142
target_price: target,
141143
notify_back_in_stock: notifyBackInStock,
144+
ai_verification_disabled: aiVerificationDisabled,
142145
});
143146
setProduct({
144147
...product,
145148
price_drop_threshold: threshold,
146149
target_price: target,
147150
notify_back_in_stock: notifyBackInStock,
151+
ai_verification_disabled: aiVerificationDisabled,
148152
});
149153
showToast('Notification settings saved');
150154
} catch {
@@ -763,6 +767,114 @@ export default function ProductDetail() {
763767
</div>
764768
</>
765769
)}
770+
771+
<style>{`
772+
.advanced-settings-card {
773+
background: var(--surface);
774+
border-radius: 0.75rem;
775+
box-shadow: var(--shadow);
776+
padding: 1.5rem;
777+
margin-top: 2rem;
778+
}
779+
780+
.advanced-settings-header {
781+
display: flex;
782+
align-items: center;
783+
gap: 0.75rem;
784+
margin-bottom: 1rem;
785+
}
786+
787+
.advanced-settings-icon {
788+
font-size: 1.5rem;
789+
}
790+
791+
.advanced-settings-title {
792+
font-size: 1.125rem;
793+
font-weight: 600;
794+
color: var(--text);
795+
}
796+
797+
.advanced-settings-description {
798+
color: var(--text-muted);
799+
font-size: 0.875rem;
800+
margin-bottom: 1.5rem;
801+
line-height: 1.5;
802+
}
803+
804+
.advanced-checkbox-group {
805+
display: flex;
806+
align-items: center;
807+
gap: 0.75rem;
808+
padding: 0.75rem;
809+
background: var(--background);
810+
border-radius: 0.375rem;
811+
cursor: pointer;
812+
}
813+
814+
.advanced-checkbox-group:hover {
815+
background: var(--border);
816+
}
817+
818+
.advanced-checkbox-group input[type="checkbox"] {
819+
width: 1.125rem;
820+
height: 1.125rem;
821+
accent-color: var(--primary);
822+
cursor: pointer;
823+
}
824+
825+
.advanced-checkbox-label {
826+
display: flex;
827+
flex-direction: column;
828+
gap: 0.125rem;
829+
}
830+
831+
.advanced-checkbox-label span:first-child {
832+
font-size: 0.875rem;
833+
font-weight: 500;
834+
color: var(--text);
835+
}
836+
837+
.advanced-checkbox-label span:last-child {
838+
font-size: 0.75rem;
839+
color: var(--text-muted);
840+
}
841+
842+
.advanced-settings-actions {
843+
margin-top: 1rem;
844+
}
845+
`}</style>
846+
847+
<div className="advanced-settings-card">
848+
<div className="advanced-settings-header">
849+
<span className="advanced-settings-icon">⚙️</span>
850+
<h2 className="advanced-settings-title">Advanced Settings</h2>
851+
</div>
852+
<p className="advanced-settings-description">
853+
Fine-tune how price extraction works for this product.
854+
</p>
855+
856+
<label className="advanced-checkbox-group">
857+
<input
858+
type="checkbox"
859+
checked={aiVerificationDisabled}
860+
onChange={(e) => setAiVerificationDisabled(e.target.checked)}
861+
/>
862+
<div className="advanced-checkbox-label">
863+
<span>Disable AI Verification</span>
864+
<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>
865+
</div>
866+
</label>
867+
868+
<div className="advanced-settings-actions">
869+
<button
870+
className="btn btn-primary"
871+
onClick={handleSaveNotifications}
872+
disabled={isSavingNotifications}
873+
>
874+
{isSavingNotifications ? 'Saving...' : 'Save Settings'}
875+
</button>
876+
</div>
877+
</div>
766878
</Layout>
767879
);
768880
}

0 commit comments

Comments
 (0)