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)
| Type |
Source Widget |
Status |
Confidence |
Age |
{forEach recent (renderRecentRow widgets)}
Attribution Log (model × type)
| Model |
{forEach allTypes renderTypeHeader}
{forEach allModels (renderModelRow allTypes proposals)}
|]
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"