Merge: glassmorphism + gradient theme, and review-inbox transparency

App-wide glassmorphism (frosted cards/popovers/sheets/inputs/pills + gradient field, gradient primary actions, dark-glass sidebar) with contrast fixes; and a restructured review inbox showing each held item as Action -> Result -> Run log, with the run log surfacing latency, skills, tools called, memory hits, product-identity inclusion, raw output, and the assembled prompt (enriched assembler run endpoint).
This commit is contained in:
soroush.asadi
2026-06-16 07:23:23 +03:30
5 changed files with 287 additions and 64 deletions
+10 -5
View File
@@ -27,8 +27,11 @@ export function AppShell({ children }: { children: ReactNode }) {
const logout = useAuth((s) => s.logout)
return (
<div className="flex min-h-screen bg-background text-foreground">
<aside className="flex w-60 shrink-0 flex-col bg-sidebar text-sidebar-foreground">
<div className="flex min-h-screen text-foreground">
<aside
className="flex w-60 shrink-0 flex-col border-r border-white/15 text-sidebar-foreground backdrop-blur-2xl"
style={{ background: 'linear-gradient(180deg, oklch(0.27 0.1 287 / 0.78) 0%, oklch(0.2 0.085 298 / 0.78) 100%)' }}
>
<div className="flex items-center gap-3 px-5 py-4">
<span className="grid size-8 place-items-center rounded-md bg-sidebar-primary font-bold text-sidebar-primary-foreground">
T
@@ -93,9 +96,11 @@ function NavItem({
const active = to ? location.pathname === to : false
const className = cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm',
active ? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground' : 'text-sidebar-foreground/80',
muted ? 'opacity-50' : 'hover:bg-sidebar-accent/60',
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
active
? 'bg-white/15 font-medium text-white shadow-sm ring-1 ring-white/15 backdrop-blur-sm'
: 'text-sidebar-foreground/80',
muted ? 'opacity-50' : 'hover:bg-white/10 hover:text-white',
)
const content = (
+105 -13
View File
@@ -8,21 +8,21 @@
:root {
--radius: 0.625rem;
/* Light content surface — the "calm command center" body. */
--background: oklch(0.99 0.003 280);
--foreground: oklch(0.21 0.03 280);
--card: oklch(1 0 0);
--card-foreground: oklch(0.21 0.03 280);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.21 0.03 280);
/* Glassmorphism body — frosted surfaces over a vivid gradient field. */
--background: oklch(0.95 0.022 286);
--foreground: oklch(0.19 0.03 280);
--card: oklch(1 0 0 / 0.74);
--card-foreground: oklch(0.19 0.03 280);
--popover: oklch(1 0 0 / 0.92);
--popover-foreground: oklch(0.19 0.03 280);
/* Brand: indigo, rationed so it always means something. */
--primary: oklch(0.511 0.262 276.966);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.012 280);
--secondary-foreground: oklch(0.3 0.05 280);
--muted: oklch(0.967 0.006 280);
--muted-foreground: oklch(0.52 0.03 280);
--muted: oklch(0.95 0.01 280 / 0.6);
--muted-foreground: oklch(0.44 0.035 280);
--accent: oklch(0.95 0.03 280);
--accent-foreground: oklch(0.4 0.16 277);
--destructive: oklch(0.577 0.245 27.325);
@@ -57,6 +57,14 @@
body {
margin: 0;
font-family: "Hanken Grotesk Variable", system-ui, sans-serif;
/* Vivid gradient field behind the frosted-glass surfaces — gives the glass something to lift off. */
background:
radial-gradient(1200px 640px at 6% -10%, oklch(0.62 0.2 288 / 0.34), transparent 62%),
radial-gradient(1050px 720px at 112% 4%, oklch(0.7 0.16 210 / 0.27), transparent 58%),
radial-gradient(960px 680px at 48% 122%, oklch(0.72 0.18 334 / 0.22), transparent 62%),
var(--background);
background-attachment: fixed;
min-height: 100vh;
}
@theme inline {
@@ -110,11 +118,11 @@ body {
}
.dark {
--background: oklch(0.205 0.03 280);
--background: oklch(0.17 0.035 287);
--foreground: oklch(0.985 0 0);
--card: oklch(0.257 0.04 281);
--card: oklch(0.31 0.055 286 / 0.62);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.257 0.04 281);
--popover: oklch(0.26 0.05 286 / 0.94);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.673 0.182 276.935);
--primary-foreground: oklch(0.205 0.03 280);
@@ -143,9 +151,93 @@ body {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply text-foreground;
}
html {
@apply font-sans;
}
}
/* ---- Glassmorphism + gradients (app-wide, keyed on shadcn data-slots) ----
* Unlayered so they sit above Tailwind utilities; inline styles still win, so the
* gradient Team cards keep their own backgrounds. */
[data-slot="card"] {
backdrop-filter: blur(20px) saturate(155%);
-webkit-backdrop-filter: blur(20px) saturate(155%);
border: 1px solid color-mix(in oklch, white 65%, transparent);
box-shadow: 0 16px 44px -20px oklch(0.32 0.13 285 / 0.42), inset 0 1px 0 0 oklch(1 0 0 / 0.55);
}
.dark [data-slot="card"] {
border-color: color-mix(in oklch, white 16%, transparent);
box-shadow: 0 18px 48px -22px oklch(0 0 0 / 0.65), inset 0 1px 0 0 oklch(1 0 0 / 0.08);
}
[data-slot="popover-content"],
[data-slot="select-content"],
[data-slot="dropdown-menu-content"],
[data-slot="sheet-content"] {
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
border: 1px solid color-mix(in oklch, white 40%, transparent);
}
.dark [data-slot="popover-content"],
.dark [data-slot="select-content"],
.dark [data-slot="dropdown-menu-content"],
.dark [data-slot="sheet-content"] {
border-color: color-mix(in oklch, white 12%, transparent);
}
/* Primary actions become a gradient; secondary/outline become glass. */
[data-slot="button"][data-variant="default"] {
background-image: linear-gradient(135deg, oklch(0.58 0.24 277) 0%, oklch(0.56 0.25 305) 100%);
box-shadow: 0 8px 20px -10px oklch(0.5 0.23 288 / 0.7);
}
[data-slot="button"][data-variant="default"]:hover {
background-image: linear-gradient(135deg, oklch(0.62 0.24 277) 0%, oklch(0.6 0.25 305) 100%);
}
[data-slot="button"][data-variant="outline"],
[data-slot="button"][data-variant="secondary"] {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background-color: color-mix(in oklch, var(--card) 65%, transparent);
border: 1px solid color-mix(in oklch, white 40%, transparent);
}
.dark [data-slot="button"][data-variant="outline"],
.dark [data-slot="button"][data-variant="secondary"] {
border-color: color-mix(in oklch, white 14%, transparent);
}
/* Frosted form fields — kept more opaque than cards so input text stays high-contrast. */
[data-slot="select-trigger"],
[data-slot="input"],
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
textarea {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background-color: color-mix(in oklch, white 74%, transparent) !important;
border-color: color-mix(in oklch, oklch(0.5 0.04 285) 35%, transparent);
}
.dark [data-slot="select-trigger"],
.dark [data-slot="input"],
.dark input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.dark textarea {
background-color: color-mix(in oklch, oklch(0.32 0.05 286) 82%, transparent) !important;
border-color: color-mix(in oklch, white 16%, transparent);
}
/* Pills: frosted for neutral variants, gradient for the primary one. */
[data-slot="badge"][data-variant="secondary"],
[data-slot="badge"][data-variant="outline"] {
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
background-color: color-mix(in oklch, white 58%, transparent);
border-color: color-mix(in oklch, white 55%, transparent);
}
.dark [data-slot="badge"][data-variant="secondary"],
.dark [data-slot="badge"][data-variant="outline"] {
background-color: color-mix(in oklch, white 12%, transparent);
border-color: color-mix(in oklch, white 16%, transparent);
}
[data-slot="badge"][data-variant="default"] {
background-image: linear-gradient(135deg, oklch(0.58 0.24 277), oklch(0.56 0.25 305));
}
+164 -44
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { ChevronDown, ChevronRight, ShieldAlert } from 'lucide-react'
import { ChevronDown, ChevronRight, Clock, Cpu, ScrollText, ShieldAlert, Wrench } from 'lucide-react'
import { toast } from 'sonner'
import { AppShell } from '@/components/AppShell'
import { Badge } from '@/components/ui/badge'
@@ -30,6 +30,43 @@ interface ReviewItem {
createdAtUtc: string
}
interface RunDetail {
status: string
output: string | null
prompt: string | null
error: string | null
trace: string | null
resultJson: string | null
latencyMs: number | null
}
interface RunTrace {
agent?: string
autonomy?: string
skills?: string[]
tools?: string[]
docs?: string[]
memories?: number
product?: { productId?: string | null; identity?: boolean }
task?: { taskType?: string }
}
interface RunResult {
action?: string
risk?: string
skill?: string
toolCalls?: { tool: string; server?: string | null; ok: boolean }[]
}
function parseJson<T>(value: string | null | undefined): T | null {
if (!value) return null
try {
return JSON.parse(value) as T
} catch {
return null
}
}
export function ReviewsPage() {
const organizationId = useAuth((s) => s.organizationId)
const [items, setItems] = useState<ReviewItem[] | null>(null)
@@ -54,7 +91,8 @@ export function ReviewsPage() {
<div className="mb-6">
<h1 className="text-2xl font-semibold tracking-tight">Review inbox</h1>
<p className="text-sm text-muted-foreground">
Held agent actions awaiting your decision. Edit before approving your edits feed the metric.
Held agent actions awaiting your decision. See the action, the result, and the run log before
you approve. Your edits feed the metric.
</p>
</div>
@@ -90,19 +128,32 @@ export function ReviewsPage() {
function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: string) => void }) {
const [content, setContent] = useState(item.content)
const [childrenText, setChildrenText] = useState(item.childTitles.join('\n'))
const [showTrace, setShowTrace] = useState(false)
const [showLog, setShowLog] = useState(false)
const [run, setRun] = useState<RunDetail | null>(null)
const [busy, setBusy] = useState(false)
const destructive = item.risk.toLowerCase() === 'destructive'
const childCount = childrenText.split('\n').map((l) => l.trim()).filter(Boolean).length
// Lazily pull the full run (latency, tool calls, raw output, assembled prompt) the first time the
// approver opens the log.
const toggleLog = async () => {
const next = !showLog
setShowLog(next)
if (next && !run) {
try {
setRun(await api.get<RunDetail>(`/api/assembler/runs/${item.agentRunId}`))
} catch {
// The run row may be gone; the assembly trace on the item still renders below.
}
}
}
async function decide(action: 'approve' | 'sendback') {
setBusy(true)
try {
if (action === 'approve') {
const childTitles = childrenText
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
const childTitles = childrenText.split('\n').map((line) => line.trim()).filter(Boolean)
const result = await api.post<{ editDistance: number | null; decision: string }>(
`/api/governance/reviews/${item.id}/approve`,
{ content, childTitles },
@@ -138,23 +189,28 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
{destructive && <ShieldAlert />}
{item.risk}
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(item.createdAtUtc).toLocaleString()}
</span>
<span className="text-xs text-muted-foreground">{new Date(item.createdAtUtc).toLocaleString()}</span>
</div>
</div>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{/* Action — what approving will actually do. */}
<div
className={`rounded-lg border px-3 py-2 text-sm ${
destructive ? 'border-destructive/40 bg-destructive/10 text-destructive' : 'border-primary/30 bg-primary/5'
}`}
>
<span className="font-medium capitalize">{item.actionKind.replace(/-/g, ' ')}</span> ·{' '}
{destructive
? 'Destructive — always held for a human.'
: `On approve, this artifact is written to the board${childCount ? ` and ${childCount} child task${childCount === 1 ? '' : 's'} created` : ''}.`}
</div>
{/* Result — the proposed artifact + child tasks (both editable). */}
<div className="flex flex-col gap-2">
<Label htmlFor={`content-${item.id}`}>Proposed artifact</Label>
<MarkdownEditor
id={`content-${item.id}`}
value={content}
onChange={setContent}
rows={6}
mono
/>
<Label htmlFor={`content-${item.id}`}>Result · proposed artifact</Label>
<MarkdownEditor id={`content-${item.id}`} value={content} onChange={setContent} rows={6} mono />
</div>
{content !== item.content && (
@@ -165,13 +221,9 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
segment.kind === 'same' ? (
<span key={i}>{segment.text}</span>
) : segment.kind === 'removed' ? (
<del key={i} className="rounded bg-destructive/15 text-destructive">
{segment.text}
</del>
<del key={i} className="rounded bg-destructive/15 text-destructive">{segment.text}</del>
) : (
<ins key={i} className="rounded bg-seat-ai/15 font-medium text-primary no-underline">
{segment.text}
</ins>
<ins key={i} className="rounded bg-seat-ai/15 font-medium text-primary no-underline">{segment.text}</ins>
),
)}
</div>
@@ -189,36 +241,104 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
/>
</div>
{/* Run log — how the agent got here. */}
<button
type="button"
onClick={() => setShowTrace((v) => !v)}
className="flex items-center gap-1 self-start text-xs font-medium text-primary hover:underline"
onClick={toggleLog}
className="flex items-center gap-1.5 self-start text-xs font-medium text-primary hover:underline"
>
{showTrace ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
Reasoning trace
{showLog ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
<ScrollText className="size-3.5" /> Run log
</button>
{showTrace && (
<pre className="max-h-48 overflow-auto rounded-lg bg-muted p-3 text-xs">{formatTrace(item.trace)}</pre>
)}
{showLog && <RunLog item={item} run={run} />}
<div className="flex items-center justify-end gap-2">
<Button variant="outline" disabled={busy} onClick={() => decide('sendback')}>
Send back
</Button>
<Button disabled={busy} onClick={() => decide('approve')}>
{busy ? 'Working…' : 'Approve'}
</Button>
<Button variant="outline" disabled={busy} onClick={() => decide('sendback')}>Send back</Button>
<Button disabled={busy} onClick={() => decide('approve')}>{busy ? 'Working…' : 'Approve'}</Button>
</div>
</CardContent>
</Card>
)
}
function formatTrace(trace: string | null): string {
if (!trace) return 'No trace captured.'
try {
return JSON.stringify(JSON.parse(trace), null, 2)
} catch {
return trace
}
function RunLog({ item, run }: { item: ReviewItem; run: RunDetail | null }) {
// The assembly trace is on the review item; the run adds latency, tool-call outcomes, and raw output.
const trace = parseJson<RunTrace>(run?.trace ?? item.trace)
const result = parseJson<RunResult>(run?.resultJson)
const [showRaw, setShowRaw] = useState(false)
const [showPrompt, setShowPrompt] = useState(false)
const toolCalls = result?.toolCalls ?? []
return (
<div className="flex flex-col gap-2 rounded-lg border bg-muted/30 p-3 text-xs">
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Cpu className="size-3.5" /> {trace?.agent ?? 'agent'} · {trace?.autonomy ?? '—'}
</span>
{run?.latencyMs != null && (
<span className="inline-flex items-center gap-1">
<Clock className="size-3.5" /> {(run.latencyMs / 1000).toFixed(1)}s
</span>
)}
{trace?.product?.identity && <span>· product identity included</span>}
{typeof trace?.memories === 'number' && <span>· {trace.memories} memory hit{trace.memories === 1 ? '' : 's'}</span>}
</div>
<LogRow label="Skills applied" value={trace?.skills?.length ? trace.skills.join(', ') : 'none'} />
{trace?.tools?.length ? <LogRow label="Tools available" value={trace.tools.join(', ')} /> : null}
{trace?.docs?.length ? <LogRow label="Docs" value={trace.docs.join(', ')} /> : null}
<div className="flex gap-2">
<span className="w-28 shrink-0 font-medium text-foreground">Tools called</span>
<span className="min-w-0 flex-1">
{toolCalls.length === 0 ? (
<span className="text-muted-foreground">none</span>
) : (
<span className="flex flex-col gap-0.5">
{toolCalls.map((t, i) => (
<span key={i} className="inline-flex items-center gap-1">
<Wrench className="size-3" /> {t.tool}
{t.server ? ` · ${t.server}` : ''}
<Badge variant={t.ok ? 'outline' : 'destructive'} className="ml-1 h-4 px-1.5 text-[10px]">
{t.ok ? 'ok' : 'failed'}
</Badge>
</span>
))}
</span>
)}
</span>
</div>
{run?.error && <LogRow label="Error" value={run.error} />}
{run?.output && (
<div className="flex flex-col gap-1">
<button type="button" onClick={() => setShowRaw((v) => !v)} className="flex items-center gap-1 self-start font-medium text-foreground hover:underline">
{showRaw ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />} Raw model output
</button>
{showRaw && <pre className="max-h-48 overflow-auto rounded bg-background/60 p-2 whitespace-pre-wrap">{run.output}</pre>}
</div>
)}
{run?.prompt && (
<div className="flex flex-col gap-1">
<button type="button" onClick={() => setShowPrompt((v) => !v)} className="flex items-center gap-1 self-start font-medium text-foreground hover:underline">
{showPrompt ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />} Assembled prompt
</button>
{showPrompt && <pre className="max-h-56 overflow-auto rounded bg-background/60 p-2 whitespace-pre-wrap">{run.prompt}</pre>}
</div>
)}
{!run && !trace && <span className="text-muted-foreground">Loading run</span>}
</div>
)
}
function LogRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex gap-2">
<span className="w-28 shrink-0 font-medium text-foreground">{label}</span>
<span className="min-w-0 flex-1 break-words">{value}</span>
</div>
)
}
@@ -12,7 +12,12 @@ internal sealed record RunResponse(
string? ActionRisk,
string? Prompt,
string? Output,
string? Error);
string? Error,
string? Trace,
string? ResultJson,
long? LatencyMs,
DateTimeOffset CreatedAtUtc,
DateTimeOffset? CompletedAtUtc);
internal sealed record AgentActivityResponse(
Guid AgentId,
@@ -85,5 +85,6 @@ internal static class AssemblerEndpoints
private static RunResponse ToResponse(AgentRun run) => new(
run.Id, run.SeatId, run.WorkItemId, run.AgentId, run.Status.ToString(),
run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error);
run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error,
run.Trace, run.ResultJson, run.LatencyMs, run.CreatedAtUtc, run.CompletedAtUtc);
}