mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-04-25 00:36:32 +02:00
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>
This commit is contained in:
parent
0a66d55d79
commit
be6dd6382e
7 changed files with 49 additions and 6 deletions
|
|
@ -178,6 +178,10 @@ async function runMigrations() {
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_verification_disabled') THEN
|
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;
|
ALTER TABLE products ADD COLUMN ai_verification_disabled BOOLEAN DEFAULT false;
|
||||||
END IF;
|
END IF;
|
||||||
|
-- Per-product AI extraction disable flag
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_extraction_disabled') THEN
|
||||||
|
ALTER TABLE products ADD COLUMN ai_extraction_disabled BOOLEAN DEFAULT false;
|
||||||
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,6 +326,7 @@ export interface Product {
|
||||||
target_price: number | null;
|
target_price: number | null;
|
||||||
notify_back_in_stock: boolean;
|
notify_back_in_stock: boolean;
|
||||||
ai_verification_disabled: boolean;
|
ai_verification_disabled: boolean;
|
||||||
|
ai_extraction_disabled: boolean;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,6 +491,7 @@ export const productQueries = {
|
||||||
target_price?: number | null;
|
target_price?: number | null;
|
||||||
notify_back_in_stock?: boolean;
|
notify_back_in_stock?: boolean;
|
||||||
ai_verification_disabled?: boolean;
|
ai_verification_disabled?: boolean;
|
||||||
|
ai_extraction_disabled?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<Product | null> => {
|
): Promise<Product | null> => {
|
||||||
const fields: string[] = [];
|
const fields: string[] = [];
|
||||||
|
|
@ -520,6 +522,10 @@ export const productQueries = {
|
||||||
fields.push(`ai_verification_disabled = $${paramIndex++}`);
|
fields.push(`ai_verification_disabled = $${paramIndex++}`);
|
||||||
values.push(updates.ai_verification_disabled);
|
values.push(updates.ai_verification_disabled);
|
||||||
}
|
}
|
||||||
|
if (updates.ai_extraction_disabled !== undefined) {
|
||||||
|
fields.push(`ai_extraction_disabled = $${paramIndex++}`);
|
||||||
|
values.push(updates.ai_extraction_disabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.length === 0) return null;
|
if (fields.length === 0) return null;
|
||||||
|
|
||||||
|
|
@ -607,6 +613,14 @@ export const productQueries = {
|
||||||
);
|
);
|
||||||
return result.rows[0]?.ai_verification_disabled === true;
|
return result.rows[0]?.ai_verification_disabled === true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isAiExtractionDisabled: async (id: number): Promise<boolean> => {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT ai_extraction_disabled FROM products WHERE id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.rows[0]?.ai_extraction_disabled === true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Price History types and queries
|
// Price History types and queries
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled } = req.body;
|
const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled, ai_extraction_disabled } = req.body;
|
||||||
|
|
||||||
const product = await productQueries.update(productId, userId, {
|
const product = await productQueries.update(productId, userId, {
|
||||||
name,
|
name,
|
||||||
|
|
@ -228,6 +228,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
|
||||||
target_price,
|
target_price,
|
||||||
notify_back_in_stock,
|
notify_back_in_stock,
|
||||||
ai_verification_disabled,
|
ai_verification_disabled,
|
||||||
|
ai_extraction_disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,10 @@ async function checkPrices(): Promise<void> {
|
||||||
// Check if AI verification is disabled for this product
|
// Check if AI verification is disabled for this product
|
||||||
const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id);
|
const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id);
|
||||||
|
|
||||||
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAi: ${skipAiVerification}`);
|
// Check if AI extraction is disabled for this product
|
||||||
|
const skipAiExtraction = await productQueries.isAiExtractionDisabled(product.id);
|
||||||
|
|
||||||
|
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAiVerify: ${skipAiVerification}, skipAiExtract: ${skipAiExtraction}`);
|
||||||
|
|
||||||
// Use voting scraper with preferred method and anchor price if available
|
// Use voting scraper with preferred method and anchor price if available
|
||||||
const scrapedData = await scrapeProductWithVoting(
|
const scrapedData = await scrapeProductWithVoting(
|
||||||
|
|
@ -40,7 +43,8 @@ async function checkPrices(): Promise<void> {
|
||||||
product.user_id,
|
product.user_id,
|
||||||
preferredMethod as ExtractionMethod | undefined,
|
preferredMethod as ExtractionMethod | undefined,
|
||||||
anchorPrice || undefined,
|
anchorPrice || undefined,
|
||||||
skipAiVerification
|
skipAiVerification,
|
||||||
|
skipAiExtraction
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`);
|
console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`);
|
||||||
|
|
|
||||||
|
|
@ -1356,13 +1356,15 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
|
||||||
* @param anchorPrice - The price the user previously confirmed. Used to select the correct
|
* @param anchorPrice - The price the user previously confirmed. Used to select the correct
|
||||||
* variant on refresh when multiple prices are found.
|
* variant on refresh when multiple prices are found.
|
||||||
* @param skipAiVerification - If true, skip AI verification entirely for this product.
|
* @param skipAiVerification - If true, skip AI verification entirely for this product.
|
||||||
|
* @param skipAiExtraction - If true, skip AI extraction fallback for this product.
|
||||||
*/
|
*/
|
||||||
export async function scrapeProductWithVoting(
|
export async function scrapeProductWithVoting(
|
||||||
url: string,
|
url: string,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
preferredMethod?: ExtractionMethod,
|
preferredMethod?: ExtractionMethod,
|
||||||
anchorPrice?: number,
|
anchorPrice?: number,
|
||||||
skipAiVerification?: boolean
|
skipAiVerification?: boolean,
|
||||||
|
skipAiExtraction?: boolean
|
||||||
): Promise<ScrapedProductWithCandidates> {
|
): Promise<ScrapedProductWithCandidates> {
|
||||||
const result: ScrapedProductWithCandidates = {
|
const result: ScrapedProductWithCandidates = {
|
||||||
name: null,
|
name: null,
|
||||||
|
|
@ -1608,8 +1610,8 @@ export async function scrapeProductWithVoting(
|
||||||
result.selectedMethod = bestCandidate.method;
|
result.selectedMethod = bestCandidate.method;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No candidates at all - try pure AI extraction
|
// No candidates at all - try pure AI extraction (unless disabled for this product)
|
||||||
if (userId && html) {
|
if (userId && html && !skipAiExtraction) {
|
||||||
console.log(`[Voting] No candidates found, trying AI extraction...`);
|
console.log(`[Voting] No candidates found, trying AI extraction...`);
|
||||||
try {
|
try {
|
||||||
const { tryAIExtraction } = await import('./ai-extractor');
|
const { tryAIExtraction } = await import('./ai-extractor');
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ export interface Product {
|
||||||
target_price: number | null;
|
target_price: number | null;
|
||||||
notify_back_in_stock: boolean;
|
notify_back_in_stock: boolean;
|
||||||
ai_verification_disabled: boolean;
|
ai_verification_disabled: boolean;
|
||||||
|
ai_extraction_disabled: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
current_price: number | null;
|
current_price: number | null;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
|
|
@ -133,6 +134,7 @@ export const productsApi = {
|
||||||
target_price?: number | null;
|
target_price?: number | null;
|
||||||
notify_back_in_stock?: boolean;
|
notify_back_in_stock?: boolean;
|
||||||
ai_verification_disabled?: boolean;
|
ai_verification_disabled?: boolean;
|
||||||
|
ai_extraction_disabled?: boolean;
|
||||||
}) => api.put<Product>(`/products/${id}`, data),
|
}) => api.put<Product>(`/products/${id}`, data),
|
||||||
|
|
||||||
delete: (id: number) => api.delete(`/products/${id}`),
|
delete: (id: number) => api.delete(`/products/${id}`),
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export default function ProductDetail() {
|
||||||
const [targetPrice, setTargetPrice] = useState<string>('');
|
const [targetPrice, setTargetPrice] = useState<string>('');
|
||||||
const [notifyBackInStock, setNotifyBackInStock] = useState(false);
|
const [notifyBackInStock, setNotifyBackInStock] = useState(false);
|
||||||
const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false);
|
const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false);
|
||||||
|
const [aiExtractionDisabled, setAiExtractionDisabled] = useState(false);
|
||||||
|
|
||||||
const REFRESH_INTERVALS = [
|
const REFRESH_INTERVALS = [
|
||||||
{ value: 300, label: '5 minutes' },
|
{ value: 300, label: '5 minutes' },
|
||||||
|
|
@ -64,6 +65,7 @@ export default function ProductDetail() {
|
||||||
}
|
}
|
||||||
setNotifyBackInStock(productRes.data.notify_back_in_stock || false);
|
setNotifyBackInStock(productRes.data.notify_back_in_stock || false);
|
||||||
setAiVerificationDisabled(productRes.data.ai_verification_disabled || false);
|
setAiVerificationDisabled(productRes.data.ai_verification_disabled || false);
|
||||||
|
setAiExtractionDisabled(productRes.data.ai_extraction_disabled || false);
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to load product details');
|
setError('Failed to load product details');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -142,6 +144,7 @@ export default function ProductDetail() {
|
||||||
target_price: target,
|
target_price: target,
|
||||||
notify_back_in_stock: notifyBackInStock,
|
notify_back_in_stock: notifyBackInStock,
|
||||||
ai_verification_disabled: aiVerificationDisabled,
|
ai_verification_disabled: aiVerificationDisabled,
|
||||||
|
ai_extraction_disabled: aiExtractionDisabled,
|
||||||
});
|
});
|
||||||
setProduct({
|
setProduct({
|
||||||
...product,
|
...product,
|
||||||
|
|
@ -149,6 +152,7 @@ export default function ProductDetail() {
|
||||||
target_price: target,
|
target_price: target,
|
||||||
notify_back_in_stock: notifyBackInStock,
|
notify_back_in_stock: notifyBackInStock,
|
||||||
ai_verification_disabled: aiVerificationDisabled,
|
ai_verification_disabled: aiVerificationDisabled,
|
||||||
|
ai_extraction_disabled: aiExtractionDisabled,
|
||||||
});
|
});
|
||||||
showToast('Notification settings saved');
|
showToast('Notification settings saved');
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -854,6 +858,18 @@ export default function ProductDetail() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label className="advanced-checkbox-group">
|
<label className="advanced-checkbox-group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aiExtractionDisabled}
|
||||||
|
onChange={(e) => setAiExtractionDisabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<div className="advanced-checkbox-label">
|
||||||
|
<span>Disable AI Extraction</span>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="advanced-checkbox-group" style={{ marginTop: '0.75rem' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={aiVerificationDisabled}
|
checked={aiVerificationDisabled}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue