diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 92cbd56..fb1a15d 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -2,7 +2,7 @@ import { NavLink } from 'react-router-dom'; import { cn } from '../lib/utils'; import { LayoutDashboard, Zap, Receipt, ShieldAlert, Wallet, Scale, - Lightbulb, TrendingUp, Upload, Settings, LogOut, + Lightbulb, TrendingUp, Upload, Settings, LogOut, Crosshair, Bot, } from 'lucide-react'; import { logout, getUser } from '../lib/auth'; @@ -13,6 +13,8 @@ const navItems = [ { path: '/disputes', label: 'Disputes', icon: ShieldAlert }, { path: '/accounts', label: 'Accounts', icon: Wallet }, { path: '/legal', label: 'Legal', icon: Scale }, + { path: '/priorities', label: 'Priorities', icon: Crosshair }, + { path: '/agents', label: 'Agent Board', icon: Bot }, { path: '/recommendations', label: 'AI Recs', icon: Lightbulb }, { path: '/cashflow', label: 'Cash Flow', icon: TrendingUp }, { path: '/upload', label: 'Upload', icon: Upload }, diff --git a/ui/src/index.css b/ui/src/index.css index 4ee8b40..64bc961 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -715,3 +715,790 @@ h1, h2, .font-display { padding: 1.25rem; } } + +/* ═══════════════════════════════════════════════════════════════ + EISENHOWER PRIORITY MATRIX + Tactical operations board — 2×2 quadrant grid with crosshair + ═══════════════════════════════════════════════════════════════ */ + +.eisenhower-root { + display: flex; + flex-direction: column; + gap: 1rem; + animation: fadeIn 0.3s ease-out; +} + +/* ── Header ───────────────────────────────────────────── */ + +.eisenhower-header { + display: flex; + align-items: center; + justify-content: space-between; +} +.eisenhower-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} +.eisenhower-header-icon { + color: var(--chitty-400); + opacity: 0.8; +} +.eisenhower-title { + font-family: 'Syne', sans-serif; + font-size: 1.25rem; + font-weight: 700; + color: var(--chrome-text); + letter-spacing: -0.01em; +} +.eisenhower-subtitle { + font-size: 0.7rem; + color: var(--chrome-text-muted); + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-top: 0.1rem; +} +.eisenhower-refresh { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; + border: 1px solid var(--chrome-border); + background: transparent; + color: var(--chrome-text-muted); + cursor: pointer; + transition: all 0.2s; +} +.eisenhower-refresh:hover { + background: var(--chrome-border); + color: var(--chrome-text); +} + +/* ── Error ────────────────────────────────────────────── */ + +.eisenhower-error { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: rgba(244, 63, 94, 0.1); + border: 1px solid rgba(244, 63, 94, 0.25); + color: var(--urgency-red); + font-size: 0.8rem; +} + +/* ── Chips row ────────────────────────────────────────── */ + +.eisenhower-chips { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} +.eisenhower-chip { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.6rem; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.02em; + border: 1px solid transparent; + transition: transform 0.15s; +} +.eisenhower-chip:hover { + transform: translateY(-1px); +} +.eisenhower-chip-label { + text-transform: uppercase; + letter-spacing: 0.05em; +} +.eisenhower-chip-count { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + opacity: 0.7; + margin-left: 0.15rem; +} + +/* Quadrant colors */ +.eisenhower-q1 { --q-accent: #f43f5e; --q-bg: rgba(244, 63, 94, 0.08); --q-border: rgba(244, 63, 94, 0.2); --q-text: #fda4af; } +.eisenhower-q2 { --q-accent: #6366f1; --q-bg: rgba(99, 102, 241, 0.08); --q-border: rgba(99, 102, 241, 0.2); --q-text: #a5b4fc; } +.eisenhower-q3 { --q-accent: #f59e0b; --q-bg: rgba(245, 158, 11, 0.08); --q-border: rgba(245, 158, 11, 0.2); --q-text: #fcd34d; } +.eisenhower-q4 { --q-accent: #64748b; --q-bg: rgba(100, 116, 139, 0.06); --q-border: rgba(100, 116, 139, 0.15); --q-text: #94a3b8; } + +.eisenhower-chip.eisenhower-q1 { background: var(--q-bg); border-color: var(--q-border); color: var(--q-text); } +.eisenhower-chip.eisenhower-q2 { background: var(--q-bg); border-color: var(--q-border); color: var(--q-text); } +.eisenhower-chip.eisenhower-q3 { background: var(--q-bg); border-color: var(--q-border); color: var(--q-text); } +.eisenhower-chip.eisenhower-q4 { background: var(--q-bg); border-color: var(--q-border); color: var(--q-text); } + +/* ── Matrix grid ──────────────────────────────────────── */ + +.eisenhower-grid { + position: relative; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + gap: 0; + min-height: 60vh; +} + +/* Crosshair lines */ +.eisenhower-crosshair-h { + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--chrome-border) 15%, var(--chitty-400) 50%, var(--chrome-border) 85%, transparent); + opacity: 0.5; + z-index: 2; + pointer-events: none; +} +.eisenhower-crosshair-v { + position: absolute; + left: 50%; + top: 0; + bottom: 0; + width: 1px; + background: linear-gradient(180deg, transparent, var(--chrome-border) 15%, var(--chitty-400) 50%, var(--chrome-border) 85%, transparent); + opacity: 0.5; + z-index: 2; + pointer-events: none; +} +.eisenhower-crosshair-dot { + position: absolute; + top: 50%; + left: 50%; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--chitty-400); + transform: translate(-50%, -50%); + z-index: 3; + pointer-events: none; + box-shadow: 0 0 12px var(--glow-brand), 0 0 24px var(--glow-brand); + animation: eisenhower-pulse 3s ease-in-out infinite; +} + +@keyframes eisenhower-pulse { + 0%, 100% { box-shadow: 0 0 8px var(--glow-brand), 0 0 16px var(--glow-brand); opacity: 0.8; } + 50% { box-shadow: 0 0 16px var(--glow-brand), 0 0 32px var(--glow-brand); opacity: 1; } +} + +/* Axis labels */ +.eisenhower-axis-y { + position: absolute; + left: -2rem; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + z-index: 1; + pointer-events: none; +} +.eisenhower-axis-x { + position: absolute; + bottom: -1.5rem; + left: 0; + right: 0; + display: flex; + justify-content: space-around; + z-index: 1; + pointer-events: none; +} +.eisenhower-axis-label { + font-size: 0.55rem; + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--chrome-text-muted); + opacity: 0.5; +} +.eisenhower-axis-label--top { + writing-mode: vertical-lr; + transform: rotate(180deg); + margin-top: 1rem; +} +.eisenhower-axis-label--bottom { + writing-mode: vertical-lr; + transform: rotate(180deg); + margin-bottom: 1rem; +} + +/* ── Quadrant panel ───────────────────────────────────── */ + +.eisenhower-quadrant { + display: flex; + flex-direction: column; + min-height: 25vh; + background: var(--q-bg); + transition: background 0.2s; +} +.eisenhower-quadrant:hover { + background: color-mix(in srgb, var(--q-bg) 80%, var(--q-accent) 5%); +} + +.eisenhower-quadrant--tl { border-radius: 16px 0 0 0; border-right: none; border-bottom: none; } +.eisenhower-quadrant--tr { border-radius: 0 16px 0 0; border-left: none; border-bottom: none; } +.eisenhower-quadrant--bl { border-radius: 0 0 0 16px; border-right: none; border-top: none; } +.eisenhower-quadrant--br { border-radius: 0 0 16px 0; border-left: none; border-top: none; } + +.eisenhower-quadrant-header { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.75rem 0.85rem 0.4rem; + color: var(--q-accent); +} +.eisenhower-quadrant-label { + font-family: 'Syne', sans-serif; + font-weight: 700; + font-size: 0.8rem; + letter-spacing: 0.01em; +} +.eisenhower-quadrant-tactical { + font-family: 'JetBrains Mono', monospace; + font-size: 0.55rem; + text-transform: uppercase; + letter-spacing: 0.1em; + opacity: 0.5; + margin-left: auto; +} +.eisenhower-quadrant-count { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + background: var(--q-border); + color: var(--q-text); + padding: 0.1rem 0.4rem; + border-radius: 999px; + font-weight: 600; +} + +.eisenhower-quadrant-list { + flex: 1; + padding: 0.25rem 0.5rem 0.75rem; + overflow-y: auto; + max-height: 45vh; +} +.eisenhower-quadrant-list::-webkit-scrollbar { + width: 3px; +} +.eisenhower-quadrant-list::-webkit-scrollbar-thumb { + background: var(--q-border); + border-radius: 4px; +} + +.eisenhower-empty { + display: flex; + align-items: center; + justify-content: center; + height: 4rem; + font-size: 0.75rem; + color: var(--chrome-text-muted); + opacity: 0.4; + font-style: italic; +} + +/* ── Item row ─────────────────────────────────────────── */ + +.eisenhower-row { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.45rem 0.4rem; + border-radius: 0.5rem; + transition: background 0.15s; + animation: fadeInUp 0.3s ease-out both; +} +.eisenhower-row:hover { + background: rgba(255, 255, 255, 0.03); +} + +.eisenhower-row-icon { + flex-shrink: 0; + width: 1.5rem; + height: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.35rem; + background: var(--q-border); + color: var(--q-text); + margin-top: 0.05rem; +} + +.eisenhower-row-body { + flex: 1; + min-width: 0; +} +.eisenhower-row-title { + font-size: 0.78rem; + font-weight: 600; + color: var(--chrome-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3; +} +.eisenhower-row-subtitle { + font-size: 0.65rem; + color: var(--chrome-text-muted); + opacity: 0.7; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.eisenhower-row-meta { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.1rem; +} +.eisenhower-row-amount { + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + font-weight: 600; + color: var(--chrome-text); +} +.eisenhower-row-date { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + color: var(--chrome-text-muted); + opacity: 0.6; +} +.eisenhower-row-date--overdue { + color: var(--urgency-red); + opacity: 1; + font-weight: 600; +} +.eisenhower-row-date--soon { + color: var(--urgency-amber); + opacity: 1; +} + +/* ── Loading state ────────────────────────────────────── */ + +.eisenhower-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; + height: 50vh; + color: var(--chrome-text-muted); + font-size: 0.85rem; +} +.eisenhower-loading-icon { + color: var(--chitty-400); + animation: eisenhower-spin 2s linear infinite; +} +@keyframes eisenhower-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ── Mobile responsive ────────────────────────────────── */ + +@media (max-width: 768px) { + .eisenhower-grid { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto auto; + gap: 0.5rem; + min-height: auto; + } + .eisenhower-quadrant { + min-height: auto; + border-radius: 12px; + border: 1px solid var(--q-border); + } + .eisenhower-quadrant--tl, + .eisenhower-quadrant--tr, + .eisenhower-quadrant--bl, + .eisenhower-quadrant--br { + border-radius: 12px; + } + .eisenhower-crosshair-h, + .eisenhower-crosshair-v, + .eisenhower-crosshair-dot, + .eisenhower-axis-y, + .eisenhower-axis-x { + display: none; + } + .eisenhower-quadrant-list { + max-height: 30vh; + } + .eisenhower-quadrant-tactical { + display: none; + } +} + +/* ═══════════════════════════════════════════════════════════════ + AGENT BOARD — Kanban pipeline for agent tasks + ═══════════════════════════════════════════════════════════════ */ + +:root { + --ab-pending: #f59e0b; + --ab-claimed: #6366f1; + --ab-running: #3b82f6; + --ab-completed: #10b981; + --ab-failed: #f43f5e; + --ab-blocked: #64748b; +} + +.ab-root { + display: flex; + flex-direction: column; + gap: 1rem; + animation: fadeIn 0.3s ease-out; +} + +/* ── Header ───────────────────────────────────────────── */ + +.ab-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; +} +.ab-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} +.ab-header-icon { + color: var(--chitty-400); + opacity: 0.8; +} +.ab-title { + font-family: 'Syne', sans-serif; + font-size: 1.25rem; + font-weight: 700; + color: var(--chrome-text); + letter-spacing: -0.01em; +} +.ab-subtitle { + font-size: 0.7rem; + color: var(--chrome-text-muted); + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-top: 0.1rem; +} +.ab-header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} +.ab-toggle { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.65rem; + border-radius: 999px; + border: 1px solid var(--chrome-border); + background: transparent; + color: var(--chrome-text-muted); + font-size: 0.7rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} +.ab-toggle:hover { + border-color: var(--ab-completed); + color: var(--chrome-text); +} +.ab-toggle--active { + border-color: var(--ab-completed); + background: rgba(16, 185, 129, 0.1); + color: #6ee7b7; +} +.ab-refresh { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; + border: 1px solid var(--chrome-border); + background: transparent; + color: var(--chrome-text-muted); + cursor: pointer; + transition: all 0.2s; +} +.ab-refresh:hover { + background: var(--chrome-border); + color: var(--chrome-text); +} + +/* ── Error ────────────────────────────────────────────── */ + +.ab-error { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: rgba(244, 63, 94, 0.1); + border: 1px solid rgba(244, 63, 94, 0.25); + color: var(--urgency-red); + font-size: 0.8rem; +} + +/* ── Kanban board ─────────────────────────────────────── */ + +.ab-board { + display: grid; + grid-template-columns: repeat(var(--ab-cols, 5), 1fr); + gap: 0.5rem; + min-height: 55vh; +} + +/* ── Column ───────────────────────────────────────────── */ + +.ab-column { + display: flex; + flex-direction: column; + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; + border: 1px solid var(--chrome-border); + overflow: hidden; + transition: border-color 0.2s; +} +.ab-column:hover { + border-color: color-mix(in srgb, var(--chrome-border) 60%, white 10%); +} + +.ab-column--pending { --col-accent: var(--ab-pending); } +.ab-column--claimed { --col-accent: var(--ab-claimed); } +.ab-column--running { --col-accent: var(--ab-running); } +.ab-column--completed { --col-accent: var(--ab-completed); } +.ab-column--failed { --col-accent: var(--ab-failed); } +.ab-column--blocked { --col-accent: var(--ab-blocked); } + +.ab-column-header { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.6rem 0.7rem; + border-bottom: 1px solid var(--chrome-border); + background: rgba(255, 255, 255, 0.015); +} +.ab-column-icon { + color: var(--col-accent); +} +.ab-column-label { + font-family: 'Syne', sans-serif; + font-weight: 700; + font-size: 0.75rem; + color: var(--chrome-text); +} +.ab-column-tactical { + font-family: 'JetBrains Mono', monospace; + font-size: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--col-accent); + opacity: 0.5; + margin-left: auto; +} +.ab-column-count { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + background: color-mix(in srgb, var(--col-accent) 15%, transparent); + color: var(--col-accent); + padding: 0.1rem 0.4rem; + border-radius: 999px; + font-weight: 600; +} + +.ab-column-list { + flex: 1; + padding: 0.4rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.35rem; +} +.ab-column-list::-webkit-scrollbar { + width: 3px; +} +.ab-column-list::-webkit-scrollbar-thumb { + background: var(--chrome-border); + border-radius: 4px; +} + +.ab-empty { + display: flex; + align-items: center; + justify-content: center; + height: 4rem; + font-size: 0.7rem; + color: var(--chrome-text-muted); + opacity: 0.35; + font-style: italic; +} + +/* ── Task card ────────────────────────────────────────── */ + +.ab-card { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 0.55rem 0.6rem; + transition: all 0.15s; + animation: fadeInUp 0.3s ease-out both; + cursor: default; +} +.ab-card:hover { + background: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); +} + +.ab-card-top { + display: flex; + align-items: center; + gap: 0.3rem; + margin-bottom: 0.3rem; + flex-wrap: wrap; +} +.ab-card-type { + font-size: 0.55rem; + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--chrome-text-muted); + opacity: 0.7; + background: rgba(255, 255, 255, 0.05); + padding: 0.1rem 0.35rem; + border-radius: 3px; +} +.ab-card-agent { + font-size: 0.55rem; + font-family: 'JetBrains Mono', monospace; + letter-spacing: 0.04em; + padding: 0.1rem 0.35rem; + border-radius: 3px; + font-weight: 600; +} + +/* Agent color variants */ +.ab-agent--blue { background: rgba(59, 130, 246, 0.15); color: #93c5fd; } +.ab-agent--purple { background: rgba(139, 92, 246, 0.15); color: #c4b5fd; } +.ab-agent--green { background: rgba(16, 185, 129, 0.15); color: #6ee7b7; } +.ab-agent--yellow { background: rgba(234, 179, 8, 0.15); color: #fde68a; } +.ab-agent--orange { background: rgba(249, 115, 22, 0.15); color: #fdba74; } +.ab-agent--red { background: rgba(244, 63, 94, 0.15); color: #fda4af; } +.ab-agent--pink { background: rgba(236, 72, 153, 0.15); color: #f9a8d4; } +.ab-agent--gray { background: rgba(156, 163, 175, 0.15); color: #d1d5db; } +.ab-agent--brown { background: rgba(180, 130, 80, 0.15); color: #d4a574; } +.ab-agent--slate { background: rgba(100, 116, 139, 0.12); color: #94a3b8; } + +.ab-card-title { + font-size: 0.78rem; + font-weight: 600; + color: var(--chrome-text); + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.ab-card-desc { + font-size: 0.65rem; + color: var(--chrome-text-muted); + opacity: 0.6; + margin-top: 0.2rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; +} + +.ab-card-footer { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.35rem; + flex-wrap: wrap; +} +.ab-card-priority { + font-family: 'JetBrains Mono', monospace; + font-size: 0.55rem; + font-weight: 700; + color: var(--col-accent); + opacity: 0.8; +} +.ab-card-date { + font-family: 'JetBrains Mono', monospace; + font-size: 0.55rem; + color: var(--chrome-text-muted); + opacity: 0.6; +} +.ab-card-date--overdue { + color: var(--urgency-red); + opacity: 1; + font-weight: 600; +} +.ab-card-verify { + display: flex; + align-items: center; + gap: 0.15rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--chrome-text-muted); + opacity: 0.5; + margin-left: auto; +} + +/* ── Loading state ────────────────────────────────────── */ + +.ab-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; + height: 50vh; + color: var(--chrome-text-muted); + font-size: 0.85rem; +} +.ab-loading-icon { + color: var(--chitty-400); + animation: eisenhower-spin 2s linear infinite; +} + +/* ── Mobile responsive ────────────────────────────────── */ + +@media (max-width: 1024px) { + .ab-board { + grid-template-columns: repeat(3, 1fr); + gap: 0.4rem; + } +} +@media (max-width: 768px) { + .ab-board { + grid-template-columns: 1fr; + gap: 0.5rem; + } + .ab-column { + max-height: 40vh; + } + .ab-column-tactical { + display: none; + } +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 61f290c..3733e2e 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -12,6 +12,8 @@ import { CashFlow } from './pages/CashFlow'; import { Recommendations } from './pages/Recommendations'; import { Settings } from './pages/Settings'; import { ActionQueue } from './pages/ActionQueue'; +import { Priorities } from './pages/Priorities'; +import { AgentBoard } from './pages/AgentBoard'; import { Login } from './pages/Login'; import { isAuthenticated } from './lib/auth'; import { FocusModeProvider } from './lib/focus-mode'; @@ -40,6 +42,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render( } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/ui/src/pages/AgentBoard.tsx b/ui/src/pages/AgentBoard.tsx new file mode 100644 index 0000000..b0ead1c --- /dev/null +++ b/ui/src/pages/AgentBoard.tsx @@ -0,0 +1,240 @@ +import { useEffect, useState, useMemo, useCallback } from 'react'; +import { api, type Task } from '../lib/api'; +import { formatDate, daysUntil, cn } from '../lib/utils'; +import { + Bot, RefreshCw, Clock, Play, CheckCircle2, XCircle, Ban, + ChevronRight, ArrowRight, AlertTriangle, +} from 'lucide-react'; + +// ── Status columns matching Notion Agent Task Board ────────── + +type BoardStatus = 'pending' | 'claimed' | 'running' | 'completed' | 'failed' | 'blocked'; + +const COLUMNS: { status: BoardStatus; label: string; tactical: string; icon: typeof Clock; cssVar: string }[] = [ + { status: 'pending', label: 'Pending', tactical: 'QUEUE', icon: Clock, cssVar: '--ab-pending' }, + { status: 'claimed', label: 'Claimed', tactical: 'ASSIGNED', icon: ArrowRight, cssVar: '--ab-claimed' }, + { status: 'running', label: 'Running', tactical: 'ACTIVE', icon: Play, cssVar: '--ab-running' }, + { status: 'completed', label: 'Done', tactical: 'SHIPPED', icon: CheckCircle2, cssVar: '--ab-completed' }, + { status: 'failed', label: 'Failed', tactical: 'ERROR', icon: XCircle, cssVar: '--ab-failed' }, + { status: 'blocked', label: 'Blocked', tactical: 'WAITING', icon: Ban, cssVar: '--ab-blocked' }, +]; + +// Map backend_status values to board columns +function mapStatus(backendStatus: string): BoardStatus { + const s = backendStatus.toLowerCase(); + if (s === 'pending' || s === 'open' || s === 'new') return 'pending'; + if (s === 'claimed' || s === 'assigned') return 'claimed'; + if (s === 'running' || s === 'in_progress' || s === 'active') return 'running'; + if (s === 'completed' || s === 'done' || s === 'resolved' || s === 'verified') return 'completed'; + if (s === 'failed' || s === 'error') return 'failed'; + if (s === 'blocked' || s === 'waiting' || s === 'deferred') return 'blocked'; + return 'pending'; +} + +// Agent name badge colors +const AGENT_COLORS: Record = { + 'chittyagent-tasks': 'ab-agent--blue', + 'chittyagent-notion': 'ab-agent--purple', + 'chittyagent-ui': 'ab-agent--green', + 'chittyagent-notes': 'ab-agent--yellow', + 'chittyagent-ship': 'ab-agent--orange', + 'chittyagent-dispute': 'ab-agent--red', + 'chittyagent-imessage': 'ab-agent--pink', + 'chittyagent-canon': 'ab-agent--gray', + 'chittyagent-chatgpt': 'ab-agent--brown', + 'chittyagent-cleaner': 'ab-agent--slate', + 'chittyagent-cloudflare': 'ab-agent--blue', + 'chittyagent-finance': 'ab-agent--green', + 'chittyagent-helper': 'ab-agent--purple', + 'chittyagent-orchestrator': 'ab-agent--orange', + 'chittyagent-resolve': 'ab-agent--red', +}; + +// Task type badge labels +const TYPE_LABELS: Record = { + evidence_ingest: 'Evidence', + email_monitor: 'Email', + note_sync: 'Notes', + error_triage: 'Triage', + notion_sync: 'Notion', + deploy: 'Deploy', + cleanup: 'Cleanup', + general: 'General', + legal: 'Legal', + financial: 'Finance', + compliance: 'Compliance', + dispute: 'Dispute', +}; + +// ── Component ──────────────────────────────────────────────── + +export function AgentBoard() { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showCompleted, setShowCompleted] = useState(false); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await api.getTasks({ limit: 200 }); + setTasks(Array.isArray(res) ? res : res.tasks ?? []); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to load tasks'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const grouped = useMemo(() => { + const result: Record = { + pending: [], claimed: [], running: [], completed: [], failed: [], blocked: [], + }; + for (const task of tasks) { + const status = mapStatus(task.backend_status); + if (status === 'completed' && !showCompleted) continue; + result[status].push(task); + } + // Sort each column by priority (lower = higher priority) + for (const col of Object.values(result)) { + col.sort((a, b) => a.priority - b.priority); + } + return result; + }, [tasks, showCompleted]); + + const activeCols = useMemo(() => { + if (showCompleted) return COLUMNS; + // Always show pending/claimed/running/failed/blocked; hide completed if empty and toggled off + return COLUMNS.filter(c => c.status !== 'completed' || grouped.completed.length > 0); + }, [showCompleted, grouped]); + + const totalByStatus = useMemo(() => { + const counts: Record = { pending: 0, claimed: 0, running: 0, completed: 0, failed: 0, blocked: 0 }; + for (const task of tasks) { + counts[mapStatus(task.backend_status)]++; + } + return counts; + }, [tasks]); + + if (loading) { + return ( +
+ +

Loading agent operations...

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

Agent Board

+

+ {tasks.length} tasks · {totalByStatus.running} running · {totalByStatus.pending} queued +

+
+
+
+ + +
+
+ + {error && ( +
+ + {error} +
+ )} + + {/* Kanban board */} +
+ {activeCols.map((col) => { + const Icon = col.icon; + const items = grouped[col.status]; + return ( +
+
+ + {col.label} + {col.tactical} + {items.length > 0 && ( + {items.length} + )} +
+
+ {items.length === 0 ? ( +
+ {col.status === 'failed' ? 'No failures' : col.status === 'blocked' ? 'Nothing blocked' : 'Empty'} +
+ ) : ( + items.map((task, idx) => ( + + )) + )} +
+
+ ); + })} +
+
+ ); +} + +// ── Task card ──────────────────────────────────────────────── + +function TaskCard({ task, index }: { task: Task; index: number }) { + const agent = task.assigned_to || task.source || ''; + const agentShort = agent.replace('chittyagent-', '').replace('notion-task-triager', 'triager'); + const agentClass = AGENT_COLORS[agent] || 'ab-agent--slate'; + const typeLabel = TYPE_LABELS[task.task_type] || task.task_type; + const hasDue = Boolean(task.due_date); + const days = hasDue ? daysUntil(task.due_date!) : null; + const overdue = days !== null && days < 0; + + return ( +
+
+ {typeLabel} + {agentShort && ( + {agentShort} + )} +
+
{task.title}
+ {task.description && ( +
{task.description}
+ )} +
+ + P{task.priority} + + {hasDue && ( + + {overdue ? `${Math.abs(days!)}d late` : days === 0 ? 'Today' : days !== null && days <= 14 ? `${days}d` : formatDate(task.due_date!)} + + )} + {task.verification_type && task.verification_type !== 'none' && ( + + + {task.verification_type} + + )} +
+
+ ); +} diff --git a/ui/src/pages/Priorities.tsx b/ui/src/pages/Priorities.tsx new file mode 100644 index 0000000..d5dfa24 --- /dev/null +++ b/ui/src/pages/Priorities.tsx @@ -0,0 +1,370 @@ +import { useEffect, useState, useMemo } from 'react'; +import { api, type Obligation, type Dispute, type Task, type Recommendation } from '../lib/api'; +import { formatCurrency, formatDate, daysUntil } from '../lib/utils'; +import { + Crosshair, Flame, CalendarClock, Users, Trash2, + Receipt, ShieldAlert, ListChecks, Lightbulb, + AlertTriangle, RefreshCw, +} from 'lucide-react'; + +// ── Quadrant types ─────────────────────────────────────────── + +type Quadrant = 'do_first' | 'schedule' | 'delegate' | 'eliminate'; +type SourceKind = 'obligation' | 'dispute' | 'task' | 'recommendation'; + +interface PriorityItem { + id: string; + kind: SourceKind; + title: string; + subtitle: string | null; + amount: string | null; + dueDate: string | null; + daysLeft: number | null; + urgencyScore: number | null; + priority: number; + status: string; + quadrant: Quadrant; +} + +// ── Classification logic ───────────────────────────────────── + +function classifyObligation(ob: Obligation): Quadrant { + const score = ob.urgency_score ?? 0; + const due = ob.due_date ? daysUntil(ob.due_date) : 999; + const amount = parseFloat(ob.amount_due ?? '0'); + const isImportant = amount >= 200 || ob.category === 'mortgage' || ob.category === 'legal' || ob.category === 'tax'; + const isUrgent = score >= 50 || due <= 7 || ob.status === 'overdue'; + if (isImportant && isUrgent) return 'do_first'; + if (isImportant) return 'schedule'; + if (isUrgent) return 'delegate'; + return 'eliminate'; +} + +function classifyDispute(d: Dispute): Quadrant { + const isImportant = d.priority <= 2 || parseFloat(d.amount_at_stake ?? '0') >= 1000; + const isUrgent = d.next_action_date ? daysUntil(d.next_action_date) <= 7 : d.status === 'open'; + if (isImportant && isUrgent) return 'do_first'; + if (isImportant) return 'schedule'; + if (isUrgent) return 'delegate'; + return 'eliminate'; +} + +function classifyTask(t: Task): Quadrant { + const isImportant = t.priority <= 2 || t.task_type === 'legal' || t.task_type === 'financial'; + const isUrgent = t.due_date ? daysUntil(t.due_date) <= 7 : t.priority <= 1; + if (isImportant && isUrgent) return 'do_first'; + if (isImportant) return 'schedule'; + if (isUrgent) return 'delegate'; + return 'eliminate'; +} + +function classifyRecommendation(r: Recommendation): Quadrant { + const isImportant = r.priority <= 2 || r.rec_type === 'legal' || r.rec_type === 'payment'; + const isUrgent = r.priority <= 1 || r.action_type === 'pay_now'; + if (isImportant && isUrgent) return 'do_first'; + if (isImportant) return 'schedule'; + if (isUrgent) return 'delegate'; + return 'eliminate'; +} + +// ── Quadrant metadata ──────────────────────────────────────── + +const QUADRANTS: Record = { + do_first: { + label: 'Do First', + tactical: 'CRITICAL — ACT NOW', + icon: Flame, + cssClass: 'eisenhower-q1', + }, + schedule: { + label: 'Schedule', + tactical: 'STRATEGIC — PLAN IT', + icon: CalendarClock, + cssClass: 'eisenhower-q2', + }, + delegate: { + label: 'Delegate', + tactical: 'TACTICAL — HAND OFF', + icon: Users, + cssClass: 'eisenhower-q3', + }, + eliminate: { + label: 'Eliminate', + tactical: 'LOW VALUE — DROP IT', + icon: Trash2, + cssClass: 'eisenhower-q4', + }, +}; + +const KIND_ICON: Record = { + obligation: Receipt, + dispute: ShieldAlert, + task: ListChecks, + recommendation: Lightbulb, +}; + +const KIND_LABEL: Record = { + obligation: 'Bill', + dispute: 'Dispute', + task: 'Task', + recommendation: 'Rec', +}; + +// ── Component ──────────────────────────────────────────────── + +export function Priorities() { + const [obligations, setObligations] = useState([]); + const [disputes, setDisputes] = useState([]); + const [tasks, setTasks] = useState([]); + const [recs, setRecs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const load = async () => { + setLoading(true); + setError(null); + try { + const [obRes, dispRes, taskRes, recRes] = await Promise.all([ + api.getObligations().catch(() => [] as Obligation[]), + api.getDisputes().catch(() => [] as Dispute[]), + api.getTasks({ limit: 100 }).catch(() => ({ tasks: [] as Task[], total: 0, limit: 100, offset: 0 })), + api.getRecommendations().catch(() => [] as Recommendation[]), + ]); + setObligations(obRes.filter(o => o.status !== 'paid')); + setDisputes(dispRes.filter(d => d.status !== 'resolved' && d.status !== 'closed')); + setTasks(Array.isArray(taskRes) ? taskRes : taskRes.tasks?.filter((t: Task) => t.backend_status !== 'completed') ?? []); + setRecs(recRes); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to load'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { load(); }, []); + + const items = useMemo(() => { + const all: PriorityItem[] = []; + + for (const ob of obligations) { + all.push({ + id: `ob-${ob.id}`, + kind: 'obligation', + title: ob.payee, + subtitle: ob.category + (ob.subcategory ? ` · ${ob.subcategory}` : ''), + amount: ob.amount_due, + dueDate: ob.due_date, + daysLeft: ob.due_date ? daysUntil(ob.due_date) : null, + urgencyScore: ob.urgency_score, + priority: ob.urgency_score ? (ob.urgency_score >= 70 ? 1 : ob.urgency_score >= 50 ? 2 : ob.urgency_score >= 30 ? 3 : 4) : 4, + status: ob.status, + quadrant: classifyObligation(ob), + }); + } + + for (const d of disputes) { + all.push({ + id: `disp-${d.id}`, + kind: 'dispute', + title: d.title, + subtitle: `${d.counterparty} · ${d.dispute_type}`, + amount: d.amount_at_stake, + dueDate: d.next_action_date, + daysLeft: d.next_action_date ? daysUntil(d.next_action_date) : null, + urgencyScore: null, + priority: d.priority, + status: d.status, + quadrant: classifyDispute(d), + }); + } + + for (const t of tasks) { + all.push({ + id: `task-${t.id}`, + kind: 'task', + title: t.title, + subtitle: t.task_type + (t.source ? ` · ${t.source}` : ''), + amount: null, + dueDate: t.due_date, + daysLeft: t.due_date ? daysUntil(t.due_date) : null, + urgencyScore: null, + priority: t.priority, + status: t.backend_status, + quadrant: classifyTask(t), + }); + } + + for (const r of recs) { + all.push({ + id: `rec-${r.id}`, + kind: 'recommendation', + title: r.title, + subtitle: r.rec_type + (r.obligation_payee ? ` · ${r.obligation_payee}` : '') + (r.dispute_title ? ` · ${r.dispute_title}` : ''), + amount: null, + dueDate: null, + daysLeft: null, + urgencyScore: null, + priority: r.priority, + status: 'active', + quadrant: classifyRecommendation(r), + }); + } + + return all.sort((a, b) => a.priority - b.priority); + }, [obligations, disputes, tasks, recs]); + + const grouped = useMemo(() => { + const result: Record = { + do_first: [], + schedule: [], + delegate: [], + eliminate: [], + }; + for (const item of items) { + result[item.quadrant].push(item); + } + return result; + }, [items]); + + if (loading) { + return ( +
+ +

Mapping priority matrix...

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

Priority Matrix

+

+ {items.length} items across {obligations.length} bills, {disputes.length} disputes, {tasks.length} tasks, {recs.length} recs +

+
+
+ +
+ + {error && ( +
+ + {error} +
+ )} + + {/* Quadrant count chips */} +
+ {(['do_first', 'schedule', 'delegate', 'eliminate'] as Quadrant[]).map((q) => { + const meta = QUADRANTS[q]; + const Icon = meta.icon; + return ( +
+ + {meta.label} + {grouped[q].length} +
+ ); + })} +
+ + {/* Matrix grid */} +
+ {/* Axis labels */} +
+ IMPORTANT + NOT IMPORTANT +
+
+ URGENT + NOT URGENT +
+ + {/* Crosshair center */} +
+
+
+ + {/* Q1 — Do First (top-left) */} + + {/* Q2 — Schedule (top-right) */} + + {/* Q3 — Delegate (bottom-left) */} + + {/* Q4 — Eliminate (bottom-right) */} + +
+
+ ); +} + +// ── Quadrant panel component ───────────────────────────────── + +function QuadrantPanel({ quadrant, items, position }: { quadrant: Quadrant; items: PriorityItem[]; position: string }) { + const meta = QUADRANTS[quadrant]; + const Icon = meta.icon; + + return ( +
+
+ + {meta.label} + {meta.tactical} + {items.length > 0 && ( + {items.length} + )} +
+
+ {items.length === 0 ? ( +
All clear
+ ) : ( + items.map((item, idx) => ( + + )) + )} +
+
+ ); +} + +// ── Individual priority item row ───────────────────────────── + +function PriorityRow({ item, index }: { item: PriorityItem; index: number }) { + const KindIcon = KIND_ICON[item.kind]; + const overdue = item.daysLeft !== null && item.daysLeft < 0; + const dueSoon = item.daysLeft !== null && item.daysLeft >= 0 && item.daysLeft <= 3; + + return ( +
+
+ +
+
+
{item.title}
+ {item.subtitle && ( +
{item.subtitle}
+ )} +
+
+ {item.amount && ( + {formatCurrency(item.amount)} + )} + {item.dueDate && ( + + {overdue ? `${Math.abs(item.daysLeft!)}d late` : item.daysLeft === 0 ? 'Today' : item.daysLeft !== null && item.daysLeft <= 14 ? `${item.daysLeft}d` : formatDate(item.dueDate)} + + )} +
+
+ ); +}