mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-04-25 00:36:32 +02:00
Add AI status badges to show verification status on prices
- Add ai_status column to price_history table (verified/corrected/null)
- Track AI verification status through scraper and scheduler
- Display badges next to prices:
- ✓ AI (green): AI verified the price is correct
- ⚡ AI (orange): AI corrected an incorrect price
- Show badges on Dashboard product cards and ProductDetail page
- Add legend explaining badges in Settings when AI Verification is enabled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d91489f12
commit
ccbc188487
11 changed files with 173 additions and 23 deletions
|
|
@ -77,6 +77,16 @@ async function runMigrations() {
|
|||
ON stock_status_history(product_id, changed_at);
|
||||
`);
|
||||
|
||||
// Add ai_status column to price_history table
|
||||
await client.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'price_history' AND column_name = 'ai_status') THEN
|
||||
ALTER TABLE price_history ADD COLUMN ai_status VARCHAR(20);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
console.log('Database migrations completed');
|
||||
} catch (error) {
|
||||
console.error('Migration error:', error);
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ function getJitterSeconds(): number {
|
|||
export interface ProductWithLatestPrice extends Product {
|
||||
current_price: number | null;
|
||||
currency: string | null;
|
||||
ai_status: AIStatus;
|
||||
}
|
||||
|
||||
export interface SparklinePoint {
|
||||
|
|
@ -340,10 +341,10 @@ export interface ProductWithSparkline extends ProductWithLatestPrice {
|
|||
export const productQueries = {
|
||||
findByUserId: async (userId: number): Promise<ProductWithLatestPrice[]> => {
|
||||
const result = await pool.query(
|
||||
`SELECT p.*, ph.price as current_price, ph.currency
|
||||
`SELECT p.*, ph.price as current_price, ph.currency, ph.ai_status
|
||||
FROM products p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT price, currency FROM price_history
|
||||
SELECT price, currency, ai_status FROM price_history
|
||||
WHERE product_id = p.id
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 1
|
||||
|
|
@ -358,10 +359,10 @@ export const productQueries = {
|
|||
findByUserIdWithSparkline: async (userId: number): Promise<ProductWithSparkline[]> => {
|
||||
// Get all products with current price
|
||||
const productsResult = await pool.query(
|
||||
`SELECT p.*, ph.price as current_price, ph.currency
|
||||
`SELECT p.*, ph.price as current_price, ph.currency, ph.ai_status
|
||||
FROM products p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT price, currency FROM price_history
|
||||
SELECT price, currency, ai_status FROM price_history
|
||||
WHERE product_id = p.id
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 1
|
||||
|
|
@ -432,10 +433,10 @@ export const productQueries = {
|
|||
|
||||
findById: async (id: number, userId: number): Promise<ProductWithLatestPrice | null> => {
|
||||
const result = await pool.query(
|
||||
`SELECT p.*, ph.price as current_price, ph.currency
|
||||
`SELECT p.*, ph.price as current_price, ph.currency, ph.ai_status
|
||||
FROM products p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT price, currency FROM price_history
|
||||
SELECT price, currency, ai_status FROM price_history
|
||||
WHERE product_id = p.id
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 1
|
||||
|
|
@ -553,11 +554,14 @@ export const productQueries = {
|
|||
};
|
||||
|
||||
// Price History types and queries
|
||||
export type AIStatus = 'verified' | 'corrected' | null;
|
||||
|
||||
export interface PriceHistory {
|
||||
id: number;
|
||||
product_id: number;
|
||||
price: number;
|
||||
currency: string;
|
||||
ai_status: AIStatus;
|
||||
recorded_at: Date;
|
||||
}
|
||||
|
||||
|
|
@ -586,13 +590,14 @@ export const priceHistoryQueries = {
|
|||
create: async (
|
||||
productId: number,
|
||||
price: number,
|
||||
currency: string = 'USD'
|
||||
currency: string = 'USD',
|
||||
aiStatus: AIStatus = null
|
||||
): Promise<PriceHistory> => {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO price_history (product_id, price, currency)
|
||||
VALUES ($1, $2, $3)
|
||||
`INSERT INTO price_history (product_id, price, currency, ai_status)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *`,
|
||||
[productId, price, currency]
|
||||
[productId, price, currency, aiStatus]
|
||||
);
|
||||
return result.rows[0];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ router.post('/:productId/refresh', async (req: AuthRequest, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Scrape product data including price and stock status
|
||||
const scrapedData = await scrapeProduct(product.url);
|
||||
// Scrape product data including price and stock status (pass userId for AI verification)
|
||||
const scrapedData = await scrapeProduct(product.url, userId);
|
||||
|
||||
// Update stock status and record change if different
|
||||
if (scrapedData.stockStatus !== product.stock_status) {
|
||||
|
|
@ -77,7 +77,8 @@ router.post('/:productId/refresh', async (req: AuthRequest, res: Response) => {
|
|||
newPrice = await priceHistoryQueries.create(
|
||||
productId,
|
||||
scrapedData.price.price,
|
||||
scrapedData.price.currency
|
||||
scrapedData.price.currency,
|
||||
scrapedData.aiStatus
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ router.post('/:productId/refresh', async (req: AuthRequest, res: Response) => {
|
|||
: 'Price refreshed successfully',
|
||||
price: newPrice,
|
||||
stockStatus: scrapedData.stockStatus,
|
||||
aiStatus: scrapedData.aiStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error refreshing price:', error);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ router.post('/', async (req: AuthRequest, res: Response) => {
|
|||
await priceHistoryQueries.create(
|
||||
product.id,
|
||||
scrapedData.price.price,
|
||||
scrapedData.price.currency
|
||||
scrapedData.price.currency,
|
||||
scrapedData.aiStatus
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,10 +126,11 @@ async function checkPrices(): Promise<void> {
|
|||
await priceHistoryQueries.create(
|
||||
product.id,
|
||||
scrapedData.price.price,
|
||||
scrapedData.price.currency
|
||||
scrapedData.price.currency,
|
||||
scrapedData.aiStatus
|
||||
);
|
||||
console.log(
|
||||
`Recorded new price for product ${product.id}: ${scrapedData.price.currency} ${scrapedData.price.price}`
|
||||
`Recorded new price for product ${product.id}: ${scrapedData.price.currency} ${scrapedData.price.price}${scrapedData.aiStatus ? ` (AI: ${scrapedData.aiStatus})` : ''}`
|
||||
);
|
||||
} else {
|
||||
console.log(`Price unchanged for product ${product.id}`);
|
||||
|
|
|
|||
|
|
@ -81,12 +81,15 @@ async function scrapeWithBrowser(url: string): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
export type AIStatus = 'verified' | 'corrected' | null;
|
||||
|
||||
export interface ScrapedProduct {
|
||||
name: string | null;
|
||||
price: ParsedPrice | null;
|
||||
imageUrl: string | null;
|
||||
url: string;
|
||||
stockStatus: StockStatus;
|
||||
aiStatus: AIStatus;
|
||||
}
|
||||
|
||||
// Site-specific scraper configurations
|
||||
|
|
@ -721,6 +724,7 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
|
|||
imageUrl: null,
|
||||
url,
|
||||
stockStatus: 'unknown',
|
||||
aiStatus: null,
|
||||
};
|
||||
|
||||
let html: string = '';
|
||||
|
|
@ -833,11 +837,14 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
|
|||
if (verifyResult) {
|
||||
if (verifyResult.isCorrect) {
|
||||
console.log(`[AI Verify] Confirmed price $${result.price.price} is correct (confidence: ${verifyResult.confidence})`);
|
||||
result.aiStatus = 'verified';
|
||||
} else if (verifyResult.suggestedPrice && verifyResult.confidence > 0.6) {
|
||||
console.log(`[AI Verify] Price correction: $${result.price.price} -> $${verifyResult.suggestedPrice.price} (${verifyResult.reason})`);
|
||||
result.price = verifyResult.suggestedPrice;
|
||||
result.aiStatus = 'corrected';
|
||||
} else {
|
||||
console.log(`[AI Verify] Price might be incorrect but no confident suggestion: ${verifyResult.reason}`);
|
||||
// Don't set aiStatus if verification was inconclusive
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue