Skip to content

Commit be6dd63

Browse files
clucraftclaude
andcommitted
Add per-product AI extraction disable option
- Add ai_extraction_disabled column to products table - Add toggle in Advanced Settings alongside AI verification disable - Pass skipAiExtraction flag through scheduler to scraper - Skip AI extraction fallback when flag is set for product Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0a66d55 commit be6dd63

File tree

7 files changed

+49
-6
lines changed

7 files changed

+49
-6
lines changed

backend/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ async function runMigrations() {
178178
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_verification_disabled') THEN
179179
ALTER TABLE products ADD COLUMN ai_verification_disabled BOOLEAN DEFAULT false;
180180
END IF;
181+
-- Per-product AI extraction disable flag
182+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_extraction_disabled') THEN
183+
ALTER TABLE products ADD COLUMN ai_extraction_disabled BOOLEAN DEFAULT false;
184+
END IF;
181185
END $$;
182186
`);
183187

backend/src/models/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export interface Product {
326326
target_price: number | null;
327327
notify_back_in_stock: boolean;
328328
ai_verification_disabled: boolean;
329+
ai_extraction_disabled: boolean;
329330
created_at: Date;
330331
}
331332

@@ -490,6 +491,7 @@ export const productQueries = {
490491
target_price?: number | null;
491492
notify_back_in_stock?: boolean;
492493
ai_verification_disabled?: boolean;
494+
ai_extraction_disabled?: boolean;
493495
}
494496
): Promise<Product | null> => {
495497
const fields: string[] = [];
@@ -520,6 +522,10 @@ export const productQueries = {
520522
fields.push(`ai_verification_disabled = $${paramIndex++}`);
521523
values.push(updates.ai_verification_disabled);
522524
}
525+
if (updates.ai_extraction_disabled !== undefined) {
526+
fields.push(`ai_extraction_disabled = $${paramIndex++}`);
527+
values.push(updates.ai_extraction_disabled);
528+
}
523529

524530
if (fields.length === 0) return null;
525531

@@ -607,6 +613,14 @@ export const productQueries = {
607613
);
608614
return result.rows[0]?.ai_verification_disabled === true;
609615
},
616+
617+
isAiExtractionDisabled: async (id: number): Promise<boolean> => {
618+
const result = await pool.query(
619+
'SELECT ai_extraction_disabled FROM products WHERE id = $1',
620+
[id]
621+
);
622+
return result.rows[0]?.ai_extraction_disabled === true;
623+
},
610624
};
611625

612626
// 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,7 +219,7 @@ 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, ai_verification_disabled } = req.body;
222+
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled, ai_extraction_disabled } = req.body;
223223

224224
const product = await productQueries.update(productId, userId, {
225225
name,
@@ -228,6 +228,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
228228
target_price,
229229
notify_back_in_stock,
230230
ai_verification_disabled,
231+
ai_extraction_disabled,
231232
});
232233

233234
if (!product) {

backend/src/services/scheduler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ async function checkPrices(): Promise<void> {
3232
// Check if AI verification is disabled for this product
3333
const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id);
3434

35-
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAi: ${skipAiVerification}`);
35+
// Check if AI extraction is disabled for this product
36+
const skipAiExtraction = await productQueries.isAiExtractionDisabled(product.id);
37+
38+
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAiVerify: ${skipAiVerification}, skipAiExtract: ${skipAiExtraction}`);
3639

3740
// Use voting scraper with preferred method and anchor price if available
3841
const scrapedData = await scrapeProductWithVoting(
3942
product.url,
4043
product.user_id,
4144
preferredMethod as ExtractionMethod | undefined,
4245
anchorPrice || undefined,
43-
skipAiVerification
46+
skipAiVerification,
47+
skipAiExtraction
4448
);
4549

4650
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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,13 +1356,15 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
13561356
* @param anchorPrice - The price the user previously confirmed. Used to select the correct
13571357
* variant on refresh when multiple prices are found.
13581358
* @param skipAiVerification - If true, skip AI verification entirely for this product.
1359+
* @param skipAiExtraction - If true, skip AI extraction fallback for this product.
13591360
*/
13601361
export async function scrapeProductWithVoting(
13611362
url: string,
13621363
userId?: number,
13631364
preferredMethod?: ExtractionMethod,
13641365
anchorPrice?: number,
1365-
skipAiVerification?: boolean
1366+
skipAiVerification?: boolean,
1367+
skipAiExtraction?: boolean
13661368
): Promise<ScrapedProductWithCandidates> {
13671369
const result: ScrapedProductWithCandidates = {
13681370
name: null,
@@ -1608,8 +1610,8 @@ export async function scrapeProductWithVoting(
16081610
result.selectedMethod = bestCandidate.method;
16091611
}
16101612
} else {
1611-
// No candidates at all - try pure AI extraction
1612-
if (userId && html) {
1613+
// No candidates at all - try pure AI extraction (unless disabled for this product)
1614+
if (userId && html && !skipAiExtraction) {
16131615
console.log(`[Voting] No candidates found, trying AI extraction...`);
16141616
try {
16151617
const { tryAIExtraction } = await import('./ai-extractor');

frontend/src/api/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface Product {
6666
target_price: number | null;
6767
notify_back_in_stock: boolean;
6868
ai_verification_disabled: boolean;
69+
ai_extraction_disabled: boolean;
6970
created_at: string;
7071
current_price: number | null;
7172
currency: string | null;
@@ -133,6 +134,7 @@ export const productsApi = {
133134
target_price?: number | null;
134135
notify_back_in_stock?: boolean;
135136
ai_verification_disabled?: boolean;
137+
ai_extraction_disabled?: boolean;
136138
}) => api.put<Product>(`/products/${id}`, data),
137139

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

frontend/src/pages/ProductDetail.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default function ProductDetail() {
3131
const [targetPrice, setTargetPrice] = useState<string>('');
3232
const [notifyBackInStock, setNotifyBackInStock] = useState(false);
3333
const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false);
34+
const [aiExtractionDisabled, setAiExtractionDisabled] = useState(false);
3435

3536
const REFRESH_INTERVALS = [
3637
{ value: 300, label: '5 minutes' },
@@ -64,6 +65,7 @@ export default function ProductDetail() {
6465
}
6566
setNotifyBackInStock(productRes.data.notify_back_in_stock || false);
6667
setAiVerificationDisabled(productRes.data.ai_verification_disabled || false);
68+
setAiExtractionDisabled(productRes.data.ai_extraction_disabled || false);
6769
} catch {
6870
setError('Failed to load product details');
6971
} finally {
@@ -142,13 +144,15 @@ export default function ProductDetail() {
142144
target_price: target,
143145
notify_back_in_stock: notifyBackInStock,
144146
ai_verification_disabled: aiVerificationDisabled,
147+
ai_extraction_disabled: aiExtractionDisabled,
145148
});
146149
setProduct({
147150
...product,
148151
price_drop_threshold: threshold,
149152
target_price: target,
150153
notify_back_in_stock: notifyBackInStock,
151154
ai_verification_disabled: aiVerificationDisabled,
155+
ai_extraction_disabled: aiExtractionDisabled,
152156
});
153157
showToast('Notification settings saved');
154158
} catch {
@@ -854,6 +858,18 @@ export default function ProductDetail() {
854858
</p>
855859

856860
<label className="advanced-checkbox-group">
861+
<input
862+
type="checkbox"
863+
checked={aiExtractionDisabled}
864+
onChange={(e) => setAiExtractionDisabled(e.target.checked)}
865+
/>
866+
<div className="advanced-checkbox-label">
867+
<span>Disable AI Extraction</span>
868+
<span>Prevent AI from being used as a fallback when standard scraping fails to find a price. Useful if AI keeps extracting wrong prices.</span>
869+
</div>
870+
</label>
871+
872+
<label className="advanced-checkbox-group" style={{ marginTop: '0.75rem' }}>
857873
<input
858874
type="checkbox"
859875
checked={aiVerificationDisabled}

0 commit comments

Comments
 (0)