216 lines
8.3 KiB
TypeScript
216 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useState } from "react";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
|
|
import { listApprovalQueue } from "@/lib/review-client";
|
|
import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
|
|
|
|
export default function DashboardPage() {
|
|
const [notes, setNotes] = useState<NoteSummary[]>([]);
|
|
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
|
|
const [pendingReviewCount, setPendingReviewCount] = useState(0);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
const [nextNotes, nextWorkspaces, nextApprovalQueue] = await Promise.all([
|
|
listNoteSummaries(),
|
|
listWorkspaceSummaries(),
|
|
listApprovalQueue(),
|
|
]);
|
|
setNotes(nextNotes);
|
|
setWorkspaces(nextWorkspaces);
|
|
setPendingReviewCount(nextApprovalQueue.length);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Unable to load dashboard data");
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const savedViews = [
|
|
{
|
|
id: "workspace-all",
|
|
name: "All workspaces",
|
|
scope: "workspace",
|
|
description: "Current workspace inventory derived from backend-backed workspace data.",
|
|
query: "visibility:any sort:updated",
|
|
resultCount: workspaces.length,
|
|
},
|
|
{
|
|
id: "draft-notes",
|
|
name: "Draft notes",
|
|
scope: "search",
|
|
description: "Draft notes currently tracked across the active knowledge base.",
|
|
query: "status:draft",
|
|
resultCount: notes.filter((note) => note.status === "draft").length,
|
|
},
|
|
{
|
|
id: "pending-review",
|
|
name: "Pending review",
|
|
scope: "review",
|
|
description: "Current agent-mediated items awaiting review.",
|
|
query: "state:draft|proposed",
|
|
resultCount: pendingReviewCount,
|
|
},
|
|
];
|
|
|
|
const operatorWorkflows = [
|
|
{
|
|
id: "workflow-approvals",
|
|
name: "Approval triage",
|
|
owner: "Operator",
|
|
queueCount: pendingReviewCount,
|
|
sla: "< 4h",
|
|
status: pendingReviewCount > 3 ? "at_risk" : "healthy",
|
|
},
|
|
{
|
|
id: "workflow-workspaces",
|
|
name: "Workspace coverage",
|
|
owner: "Knowledge Ops",
|
|
queueCount: workspaces.length,
|
|
sla: "< 1d",
|
|
status: workspaces.length > 5 ? "at_risk" : "healthy",
|
|
},
|
|
] as const;
|
|
|
|
const recentNotes = notes.slice(0, 3);
|
|
|
|
function getSavedViewHref(view: (typeof savedViews)[number]) {
|
|
if (view.id === "workspace-all") {
|
|
return "/workspaces";
|
|
}
|
|
|
|
if (view.id === "draft-notes") {
|
|
return `/search?q=${encodeURIComponent("draft")}`;
|
|
}
|
|
|
|
return "/reviews";
|
|
}
|
|
|
|
function getWorkflowHref(workflow: (typeof operatorWorkflows)[number]) {
|
|
if (workflow.id === "workflow-workspaces") {
|
|
return "/workspaces";
|
|
}
|
|
|
|
return "/reviews";
|
|
}
|
|
|
|
const summaryCards = [
|
|
{
|
|
id: "summary-workspaces",
|
|
label: "Active workspaces",
|
|
value: workspaces.length,
|
|
href: "/workspaces",
|
|
},
|
|
{
|
|
id: "summary-notes",
|
|
label: "Tracked notes",
|
|
value: notes.length,
|
|
href: "/search",
|
|
},
|
|
{
|
|
id: "summary-reviews",
|
|
label: "Pending review surfaces",
|
|
value: pendingReviewCount,
|
|
href: "/reviews",
|
|
},
|
|
] as const;
|
|
|
|
return (
|
|
<AppShell
|
|
title="Dashboard"
|
|
description="Operational entry point for recent notes, active workspaces, and agent-relevant follow-ups."
|
|
actions={<div className="badge">Operational shell</div>}
|
|
>
|
|
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--ml-space-4)" }}>
|
|
{summaryCards.map((card) => (
|
|
<Link key={card.id} href={card.href} className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
|
<div style={{ color: "var(--ml-text-secondary)" }}>{card.label}</div>
|
|
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{card.value}</div>
|
|
</Link>
|
|
))}
|
|
</section>
|
|
|
|
<section style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.1fr) minmax(320px, 0.9fr)", gap: "var(--ml-space-4)" }}>
|
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
|
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Saved views</div>
|
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
|
{savedViews.map((view) => (
|
|
<Link
|
|
key={view.id}
|
|
href={getSavedViewHref(view)}
|
|
className="surface-muted"
|
|
style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}
|
|
>
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
|
<strong>{view.name}</strong>
|
|
<span className="badge">{view.scope}</span>
|
|
</div>
|
|
<div style={{ color: "var(--ml-text-secondary)" }}>{view.description}</div>
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
|
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
|
|
<span style={{ color: "var(--ml-text-secondary)" }}>{view.resultCount} results</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
|
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Operator workflows</div>
|
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
|
{operatorWorkflows.map((workflow) => (
|
|
<Link
|
|
key={workflow.id}
|
|
href={getWorkflowHref(workflow)}
|
|
className="surface-muted"
|
|
style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}
|
|
>
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
|
<strong>{workflow.name}</strong>
|
|
<span className="badge">{workflow.status}</span>
|
|
</div>
|
|
<div style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</div>
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
|
<span style={{ color: "var(--ml-text-secondary)" }}>Queue: {workflow.queueCount}</span>
|
|
<span style={{ color: "var(--ml-text-secondary)" }}>SLA: {workflow.sla}</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</section>
|
|
|
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
|
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
|
|
{error ? <div style={{ color: "var(--ml-text-secondary)" }}>{error}</div> : null}
|
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
|
{recentNotes.map((note) => (
|
|
<div key={note.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
|
<Link href={`/notes/${note.id}`}>
|
|
<strong>{note.title}</strong>
|
|
</Link>
|
|
<span style={{ color: "var(--ml-text-secondary)" }}>{note.updatedBy}</span>
|
|
</div>
|
|
<Link href={`/notes/${note.id}`} style={{ color: "var(--ml-text-secondary)" }}>
|
|
{note.excerpt}
|
|
</Link>
|
|
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
|
|
{note.tags.map((tag) => (
|
|
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`} className="badge">
|
|
#{tag}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</AppShell>
|
|
);
|
|
}
|