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:
parent
2c7677b76f
commit
dd59c81603
5 changed files with 100 additions and 7 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue