-
{title}
+
+ DocSentinel workspace
+
+
{title}
{description ? (
-
{description}
+
{description}
) : null}
{actions ?
{actions}
: null}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 7ef7051..af19aab 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -4,12 +4,12 @@
:root {
color-scheme: dark;
- --canvas: 10 12 15;
- --panel: 16 19 23;
- --panel-2: 21 25 31;
- --line: 37 43 51;
- --muted: 139 149 161;
- --text: 230 237 243;
+ --canvas: 8 11 16;
+ --panel: 13 17 23;
+ --panel-2: 19 24 32;
+ --line: 40 48 60;
+ --muted: 148 159 174;
+ --text: 236 242 248;
--accent: 88 166 255;
--good: 63 185 80;
--warn: 210 153 34;
@@ -30,7 +30,11 @@ body {
margin: 0;
min-width: 320px;
min-height: 100vh;
- background: rgb(var(--canvas));
+ background:
+ radial-gradient(circle at 65% -20%, rgb(var(--accent) / 0.07), transparent 34rem),
+ rgb(var(--canvas));
+ color: rgb(var(--text));
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button,
@@ -40,6 +44,11 @@ textarea {
font: inherit;
}
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+
::-webkit-scrollbar {
height: 10px;
width: 10px;
@@ -55,7 +64,7 @@ textarea {
}
.focus-ring {
- @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/55 focus-visible:ring-offset-1 focus-visible:ring-offset-canvas;
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas;
}
::selection {
diff --git a/frontend/src/navigation.ts b/frontend/src/navigation.ts
new file mode 100644
index 0000000..398b0ed
--- /dev/null
+++ b/frontend/src/navigation.ts
@@ -0,0 +1,115 @@
+import {
+ BrainCircuit,
+ Database,
+ FolderKanban,
+ Gauge,
+ ListChecks,
+ Network,
+ Settings,
+ type LucideIcon
+} from "lucide-react";
+
+export type NavigationItem = {
+ to: string;
+ label: string;
+ shortLabel: string;
+ description: string;
+ keywords: string[];
+ icon: LucideIcon;
+};
+
+export type NavigationGroup = {
+ label: string;
+ items: NavigationItem[];
+};
+
+export const navigationGroups: NavigationGroup[] = [
+ {
+ label: "Workspace",
+ items: [
+ {
+ to: "/",
+ label: "Overview",
+ shortLabel: "Overview",
+ description: "Security posture and work requiring attention",
+ keywords: ["dashboard", "command center", "metrics", "activity"],
+ icon: Gauge
+ }
+ ]
+ },
+ {
+ label: "Review & govern",
+ items: [
+ {
+ to: "/assessments",
+ label: "Assessment queue",
+ shortLabel: "Assessments",
+ description: "Submit evidence, review findings, and approve outcomes",
+ keywords: ["review", "findings", "reports", "documents", "remediation"],
+ icon: ListChecks
+ },
+ {
+ to: "/governance",
+ label: "Governance",
+ shortLabel: "Governance",
+ description: "Projects, controls, evidence, and readiness",
+ keywords: ["controls", "policy", "compliance", "pallas", "projects"],
+ icon: FolderKanban
+ }
+ ]
+ },
+ {
+ label: "Agent operations",
+ items: [
+ {
+ to: "/kb",
+ label: "Knowledge base",
+ shortLabel: "Knowledge",
+ description: "Approved sources and retrieval inspection",
+ keywords: ["rag", "documents", "context", "sources", "reindex"],
+ icon: Database
+ },
+ {
+ to: "/skills",
+ label: "Skill library",
+ shortLabel: "Skills",
+ description: "Assessment personas, prompts, and frameworks",
+ keywords: ["prompts", "personas", "agents", "frameworks"],
+ icon: BrainCircuit
+ },
+ {
+ to: "/integrations",
+ label: "Agent gateway",
+ shortLabel: "Gateway",
+ description: "MCP and A2A endpoints, access, and capabilities",
+ keywords: ["mcp", "a2a", "protocols", "integrations", "tools"],
+ icon: Network
+ }
+ ]
+ },
+ {
+ label: "System",
+ items: [
+ {
+ to: "/settings",
+ label: "Runtime settings",
+ shortLabel: "Settings",
+ description: "Model provider, API health, and local runtime",
+ keywords: ["llm", "provider", "model", "configuration", "health"],
+ icon: Settings
+ }
+ ]
+ }
+];
+
+export const navigationItems = navigationGroups.flatMap((group) =>
+ group.items.map((item) => ({ ...item, group: group.label }))
+);
+
+export function navigationItemForPath(pathname: string) {
+ return (
+ navigationItems.find((item) =>
+ item.to === "/" ? pathname === "/" : pathname.startsWith(item.to)
+ ) ?? navigationItems[0]
+ );
+}
diff --git a/frontend/src/pages/Assessments.tsx b/frontend/src/pages/Assessments.tsx
index 5be7bce..f741c39 100644
--- a/frontend/src/pages/Assessments.tsx
+++ b/frontend/src/pages/Assessments.tsx
@@ -1,5 +1,15 @@
-import { Check, FileUp, MessageSquare, RefreshCw, Send, Shield, X } from "lucide-react";
+import * as Dialog from "@radix-ui/react-dialog";
+import {
+ Check,
+ FileUp,
+ MessageSquare,
+ RefreshCw,
+ Send,
+ Shield,
+ X
+} from "lucide-react";
import { FormEvent, useEffect, useMemo, useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
import { SeverityBadge, StatusBadge, taskTitle } from "../components/domain";
import { ThreatEvidencePanel } from "../components/ThreatEvidencePanel";
@@ -57,6 +67,8 @@ const statusOptions = [
];
export default function Assessments() {
+ const location = useLocation();
+ const navigate = useNavigate();
const [tasks, setTasks] = useState
([]);
const [skills, setSkills] = useState([]);
const [selectedId, setSelectedId] = useState(null);
@@ -68,6 +80,19 @@ export default function Assessments() {
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
+ const [submitError, setSubmitError] = useState(null);
+ const submitOpen = location.hash === "#new-assessment";
+
+ function setSubmitOpen(open: boolean) {
+ navigate(
+ {
+ pathname: location.pathname,
+ search: location.search,
+ hash: open ? "new-assessment" : ""
+ },
+ { replace: true }
+ );
+ }
async function loadTasks(nextSelectedId = selectedId) {
setLoading(true);
@@ -129,11 +154,11 @@ export default function Assessments() {
const form = new FormData(event.currentTarget);
const files = form.getAll("files").filter((value): value is File => value instanceof File && value.size > 0);
if (!files.length) {
- setError("Select at least one document.");
+ setSubmitError("Select at least one document.");
return;
}
setBusy(true);
- setError(null);
+ setSubmitError(null);
try {
const created = await submitAssessment({
files,
@@ -145,8 +170,11 @@ export default function Assessments() {
});
event.currentTarget.reset();
await loadTasks(created.task_id);
+ setSubmitOpen(false);
} catch (err) {
- setError(err instanceof Error ? err.message : "Assessment submission failed.");
+ setSubmitError(
+ err instanceof Error ? err.message : "Assessment submission failed."
+ );
} finally {
setBusy(false);
}
@@ -205,48 +233,108 @@ export default function Assessments() {
setSubmitOpen(true)} type="button">
+
+ New assessment
+
+ }
/>
-