module Web.View.Hubs.AgentAuditDashboard where import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude import Web.Routes () data AgentAuditDashboardView = AgentAuditDashboardView { hub :: !Hub , proposals :: ![AgentProposal] , reviews :: ![AgentReviewRecord] , widgets :: ![Widget] } instance View AgentAuditDashboardView where html AgentAuditDashboardView { .. } = [hsx|

Agent Audit Dashboard

{hub.name}

← Hub
{kpiCard "Total Proposals" (show totalProposals) "text-gray-800"} {kpiCard "Pending" (show pendingCount) "text-yellow-700"} {kpiCard "Acceptance Rate" (showPct acceptanceRate) "text-green-700"} {kpiCard "Rejection Rate" (showPct rejectionRate) "text-red-700"}

Proposals by Type

{forEach allTypes (renderTypeCount proposals)}

Unreviewed Queue ({show pendingCount})

{renderPendingQueue pending}

Recent Proposals (last 20)

{forEach recent (renderRecentRow widgets)}
Type Source Widget Status Confidence Age

Attribution Log (model × type)

{forEach allTypes renderTypeHeader} {forEach allModels (renderModelRow allTypes proposals)}
Model
|] where totalProposals = length proposals pending = filter (\p -> p.status == "pending") proposals pendingCount = length pending accepted = filter (\r -> r.decision == "accepted") reviews rejected = filter (\r -> r.decision == "rejected") reviews reviewed = length accepted + length rejected acceptanceRate = if reviewed == 0 then 0 else fromIntegral (length accepted) / fromIntegral reviewed :: Double rejectionRate = if reviewed == 0 then 0 else fromIntegral (length rejected) / fromIntegral reviewed :: Double recent = take 20 proposals allTypes = ["summary", "requirement_draft", "duplicate_flag", "policy_flag", "impl_proposal"] allModels = nub (map (.modelRef) proposals) renderTypeHeader :: Text -> Html renderTypeHeader t = [hsx|{t}|] renderModelRow :: [Text] -> [AgentProposal] -> Text -> Html renderModelRow types props m = [hsx| {m} {forEach types (renderMatrixCell props m)} |] renderMatrixCell :: [AgentProposal] -> Text -> Text -> Html renderMatrixCell props m t = let cnt = length (filter (\p -> p.modelRef == m && p.proposalType == t) props) display = if cnt == 0 then "—" else show cnt in [hsx|{display}|] kpiCard :: Text -> Text -> Text -> Html kpiCard label value colorClass = [hsx|

{label}

colorClass}>{value}

|] renderQueueRow :: AgentProposal -> Html renderQueueRow p = [hsx| " text-xs px-2 py-0.5 rounded font-medium"}> {p.proposalType} {show p.createdAt} Review → |] renderRecentRow :: [Widget] -> AgentProposal -> Html renderRecentRow widgets p = [hsx| " text-xs px-2 py-0.5 rounded font-medium"}> {p.proposalType} {widgetName widgets p.sourceWidgetId} " text-xs px-2 py-0.5 rounded"}> {p.status} {maybe "—" (\c -> show (round (c * 100) :: Int) <> "%") p.confidence} {show p.createdAt} |] renderTypeCount :: [AgentProposal] -> Text -> Html renderTypeCount proposals t = let cnt = length (filter (\p -> p.proposalType == t) proposals) in [hsx|
" text-xs px-2 py-0.5 rounded font-medium"}>{t} {show cnt}
|] renderPendingQueue :: [AgentProposal] -> Html renderPendingQueue [] = [hsx|

No pending proposals.

|] renderPendingQueue pending = [hsx| {forEach (sortByCreatedAt pending) renderQueueRow}
|] widgetName :: [Widget] -> Maybe (Id Widget) -> Text widgetName _ Nothing = "—" widgetName widgets (Just wid) = maybe "—" (.name) (find (\w -> w.id == wid) widgets) sortByCreatedAt :: [AgentProposal] -> [AgentProposal] sortByCreatedAt = sortBy (\a b -> compare a.createdAt b.createdAt) showPct :: Double -> Text showPct d = show (round (d * 100) :: Int) <> "%" typeBadge :: Text -> Text typeBadge "summary" = "bg-blue-100 text-blue-800" typeBadge "requirement_draft" = "bg-indigo-100 text-indigo-800" typeBadge "duplicate_flag" = "bg-orange-100 text-orange-800" typeBadge "policy_flag" = "bg-red-100 text-red-800" typeBadge "impl_proposal" = "bg-green-100 text-green-800" typeBadge _ = "bg-gray-100 text-gray-600" statusBadge :: Text -> Text statusBadge "pending" = "bg-yellow-100 text-yellow-800" statusBadge "accepted" = "bg-green-100 text-green-800" statusBadge "rejected" = "bg-red-100 text-red-800" statusBadge "superseded" = "bg-gray-100 text-gray-500" statusBadge _ = "bg-gray-100 text-gray-600"