mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-15 10:52:36 +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
|
|
@ -45,6 +45,7 @@ export const authApi = {
|
|||
|
||||
// Products API
|
||||
export type StockStatus = 'in_stock' | 'out_of_stock' | 'unknown';
|
||||
export type AIStatus = 'verified' | 'corrected' | null;
|
||||
|
||||
export interface SparklinePoint {
|
||||
price: number;
|
||||
|
|
@ -67,6 +68,7 @@ export interface Product {
|
|||
created_at: string;
|
||||
current_price: number | null;
|
||||
currency: string | null;
|
||||
ai_status: AIStatus;
|
||||
sparkline?: SparklinePoint[];
|
||||
price_change_7d?: number | null;
|
||||
min_price?: number | null;
|
||||
|
|
|
|||
60
frontend/src/components/AIStatusBadge.tsx
Normal file
60
frontend/src/components/AIStatusBadge.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { AIStatus } from '../api/client';
|
||||
|
||||
interface AIStatusBadgeProps {
|
||||
status: AIStatus;
|
||||
size?: 'small' | 'normal';
|
||||
}
|
||||
|
||||
export default function AIStatusBadge({ status, size = 'normal' }: AIStatusBadgeProps) {
|
||||
if (!status) return null;
|
||||
|
||||
const isSmall = size === 'small';
|
||||
const fontSize = isSmall ? '0.65rem' : '0.75rem';
|
||||
const padding = isSmall ? '0.1rem 0.3rem' : '0.15rem 0.4rem';
|
||||
|
||||
if (status === 'verified') {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.2rem',
|
||||
fontSize,
|
||||
padding,
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.15)',
|
||||
color: '#10b981',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
title="AI verified this price is correct"
|
||||
>
|
||||
<span style={{ fontSize: isSmall ? '0.7rem' : '0.8rem' }}>✓</span>
|
||||
AI
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'corrected') {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.2rem',
|
||||
fontSize,
|
||||
padding,
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.15)',
|
||||
color: '#f59e0b',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
title="AI corrected this price (original scrape was incorrect)"
|
||||
>
|
||||
<span style={{ fontSize: isSmall ? '0.7rem' : '0.8rem' }}>⚡</span>
|
||||
AI
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { Product } from '../api/client';
|
||||
import Sparkline from './Sparkline';
|
||||
import AIStatusBadge from './AIStatusBadge';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
|
|
@ -502,9 +503,12 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
|
|||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="product-current-price">
|
||||
{formatPrice(product.current_price, product.currency)}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<span className="product-current-price">
|
||||
{formatPrice(product.current_price, product.currency)}
|
||||
</span>
|
||||
<AIStatusBadge status={product.ai_status} size="small" />
|
||||
</div>
|
||||
{product.price_change_7d !== null && product.price_change_7d !== undefined && (
|
||||
<span className={`product-price-change ${priceChangeClass}`}>
|
||||
{formatPriceChange(product.price_change_7d)} (7d)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useParams, useNavigate, Link } from 'react-router-dom';
|
|||
import Layout from '../components/Layout';
|
||||
import PriceChart from '../components/PriceChart';
|
||||
import StockTimeline from '../components/StockTimeline';
|
||||
import AIStatusBadge from '../components/AIStatusBadge';
|
||||
import { useToast } from '../context/ToastContext';
|
||||
import {
|
||||
productsApi,
|
||||
|
|
@ -438,10 +439,15 @@ export default function ProductDetail() {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="product-detail-price">
|
||||
{product.stock_status === 'out_of_stock'
|
||||
? 'Price unavailable'
|
||||
: formatPrice(product.current_price, product.currency)}
|
||||
<div className="product-detail-price" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span>
|
||||
{product.stock_status === 'out_of_stock'
|
||||
? 'Price unavailable'
|
||||
: formatPrice(product.current_price, product.currency)}
|
||||
</span>
|
||||
{product.stock_status !== 'out_of_stock' && (
|
||||
<AIStatusBadge status={product.ai_status} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{priceChange !== null && priceChange !== 0 && (
|
||||
|
|
|
|||
|
|
@ -1304,6 +1304,58 @@ export default function Settings() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{aiVerificationEnabled && (
|
||||
<div style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
background: 'var(--background)',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '0.8125rem',
|
||||
}}>
|
||||
<div style={{ fontWeight: 500, marginBottom: '0.5rem', color: 'var(--text)' }}>
|
||||
Price badges explained:
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.2rem',
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.15rem 0.4rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.15)',
|
||||
color: '#10b981',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
<span style={{ fontSize: '0.8rem' }}>✓</span> AI
|
||||
</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>
|
||||
AI verified the scraped price is correct
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.2rem',
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.15rem 0.4rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.15)',
|
||||
color: '#f59e0b',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
<span style={{ fontSize: '0.8rem' }}>⚡</span> AI
|
||||
</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>
|
||||
AI corrected an incorrect price (e.g., scraped savings amount instead of actual price)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(aiEnabled || aiVerificationEnabled) && (
|
||||
<>
|
||||
<div className="settings-form-group">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue