Add confidence breakdown to dashboard job cards, default client to Amazon

- Backend: Added confidence_high/moderate/low/total_output_rows to
  JobListResponse, computed via a batch query joining output_rows
- Frontend JobCard: Shows a stacked progress bar with green/amber/red
  segments and counts for High/Moderate/Low confidence tiers
- Frontend StepConfigure: Auto-selects Amazon as default client when
  creating a new job (falls back to first client if Amazon not found)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-10 20:09:29 -04:00
parent 2c7677b76f
commit dd59c81603
5 changed files with 100 additions and 7 deletions

View file

@ -2,9 +2,12 @@ from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db
from app.models.job import LocaleInstance as LocaleInstanceModel
from app.models.output import OutputRow, ConfidenceTier
from app.schemas.common import PaginatedResponse
from app.schemas.job import JobCreate, JobListResponse, JobResponse, JobUpdate, LocaleInstanceResponse
from app.services.audit_service import AuditService
@ -68,8 +71,33 @@ async def list_jobs(
page=page,
page_size=page_size,
)
# Batch-load confidence counts for all jobs in one query
job_ids = [job.id for job in jobs]
confidence_map: dict[str, dict] = {}
if job_ids:
conf_query = (
select(
LocaleInstanceModel.job_id,
OutputRow.confidence_tier,
func.count().label("cnt"),
)
.join(OutputRow, OutputRow.instance_id == LocaleInstanceModel.id)
.where(LocaleInstanceModel.job_id.in_(job_ids))
.group_by(LocaleInstanceModel.job_id, OutputRow.confidence_tier)
)
conf_result = await db.execute(conf_query)
for row in conf_result.all():
jid = str(row.job_id)
if jid not in confidence_map:
confidence_map[jid] = {"high": 0, "moderate": 0, "low": 0, "total": 0}
tier_val = row.confidence_tier.value if hasattr(row.confidence_tier, "value") else row.confidence_tier
confidence_map[jid][tier_val] = row.cnt
confidence_map[jid]["total"] += row.cnt
items = []
for job in jobs:
jid = str(job.id)
conf = confidence_map.get(jid, {})
item = JobListResponse(
id=job.id,
client_id=job.client_id,
@ -86,6 +114,10 @@ async def list_jobs(
updated_at=job.updated_at,
client_name=job.client.name if job.client else None,
created_by_name=job.creator.name if job.creator else None,
confidence_high=conf.get("high", 0),
confidence_moderate=conf.get("moderate", 0),
confidence_low=conf.get("low", 0),
total_output_rows=conf.get("total", 0),
)
items.append(item)

View file

@ -99,5 +99,10 @@ class JobListResponse(BaseModel):
# Enrichment fields (populated by API layer)
client_name: str | None = None
created_by_name: str | None = None
# Confidence breakdown (populated by API layer)
confidence_high: int = 0
confidence_moderate: int = 0
confidence_low: int = 0
total_output_rows: int = 0
model_config = {"from_attributes": True}

View file

@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
import type { Job } from "@/lib/types";
import { formatDate } from "@/lib/utils";
import { cn } from "@/lib/utils";
import { Clock, User, FileText, DollarSign } from "lucide-react";
import { Clock, User, DollarSign } from "lucide-react";
const statusConfig: Record<
string,
@ -90,6 +90,46 @@ export function JobCard({ job }: JobCardProps) {
</div>
))}
</div>
{/* Confidence breakdown */}
{(job.total_output_rows ?? 0) > 0 && (
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-gray-100">
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden flex">
{(job.confidence_high ?? 0) > 0 && (
<div
className="h-full bg-amazon-success"
style={{ width: `${((job.confidence_high ?? 0) / job.total_output_rows!) * 100}%` }}
/>
)}
{(job.confidence_moderate ?? 0) > 0 && (
<div
className="h-full bg-amazon-warning"
style={{ width: `${((job.confidence_moderate ?? 0) / job.total_output_rows!) * 100}%` }}
/>
)}
{(job.confidence_low ?? 0) > 0 && (
<div
className="h-full bg-amazon-error/60"
style={{ width: `${((job.confidence_low ?? 0) / job.total_output_rows!) * 100}%` }}
/>
)}
</div>
<div className="flex items-center gap-3 text-xs shrink-0">
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amazon-success" />
<span className="text-gray-500">{job.confidence_high ?? 0}</span>
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amazon-warning" />
<span className="text-gray-500">{job.confidence_moderate ?? 0}</span>
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amazon-error/60" />
<span className="text-gray-500">{job.confidence_low ?? 0}</span>
</span>
</div>
</div>
)}
</div>
{/* Right side - meta */}
@ -102,10 +142,9 @@ export function JobCard({ job }: JobCardProps) {
<User className="h-3 w-3" />
{job.created_by}
</div>
{job.total_lines && (
<div className="flex items-center gap-1.5 text-xs text-gray-400 justify-end">
<FileText className="h-3 w-3" />
{job.total_lines} lines
{(job.total_output_rows ?? 0) > 0 && (
<div className="text-xs text-gray-400 text-right">
{job.total_output_rows} rows
</div>
)}
{(job.total_estimated_cost ?? 0) > 0 && (

View file

@ -55,9 +55,22 @@ export function StepConfigure({ data, onChange, onNext }: StepConfigureProps) {
useEffect(() => {
getClients()
.then((data) => setClients(data))
.then((fetched) => {
setClients(fetched);
// Auto-select Amazon if no client chosen yet
if (!data.client_id && fetched.length > 0) {
const amazon = fetched.find(
(c) => c.name.toLowerCase() === "amazon"
);
const defaultClient = amazon || fetched[0];
onChange({
client_id: defaultClient.id,
client_name: defaultClient.name,
});
}
})
.catch(() => {});
}, []);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const locales =
data.job_type === "MAIN" ? MAIN_LOCALES : DERIVED_LOCALES;

View file

@ -115,6 +115,10 @@ export interface Job {
total_token_usage?: number;
total_estimated_cost?: number;
error_message?: string;
confidence_high?: number;
confidence_moderate?: number;
confidence_low?: number;
total_output_rows?: number;
}
export interface LocaleInstance {