From 842488905aa83eb2dc3735aa757123f29dc08e17 Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 12:04:28 -0400 Subject: [PATCH 01/11] Unify dashboard empty states, navigation icons, and customer-facing copy. Migrate timeline empties to StateView, replace unicode nav glyphs with SVGs, and remove developer-only hints from assistant messaging. Co-authored-by: Cursor --- frontend/src/components/layout/MobileNav.tsx | 25 +++-- frontend/src/components/layout/Sidebar.tsx | 22 ++-- .../src/components/memory/TimelineView.tsx | 8 +- frontend/src/components/ui/StateView.tsx | 12 ++- frontend/src/components/ui/icons.tsx | 101 ++++++++++++++++++ frontend/src/lib/assistant.ts | 2 +- 6 files changed, 146 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/ui/icons.tsx diff --git a/frontend/src/components/layout/MobileNav.tsx b/frontend/src/components/layout/MobileNav.tsx index ea705ee..08b31d3 100644 --- a/frontend/src/components/layout/MobileNav.tsx +++ b/frontend/src/components/layout/MobileNav.tsx @@ -1,12 +1,20 @@ import type { ViewId } from "../../types"; import { useApp } from "../../context/AppContext"; +import { + IconAgent, + IconGraph, + IconHome, + IconReview, + IconSearch, + IconSpark, +} from "../ui/icons"; -const ITEMS: { id: ViewId; label: string; icon: string }[] = [ - { id: "home", label: "Overview", icon: "⌂" }, - { id: "ask", label: "Search", icon: "?" }, - { id: "explore", label: "Map", icon: "◎" }, - { id: "agents", label: "AI", icon: "⚡" }, - { id: "review", label: "Review", icon: "⚖" }, +const ITEMS: { id: ViewId; label: string; Icon: typeof IconHome }[] = [ + { id: "home", label: "Overview", Icon: IconHome }, + { id: "ask", label: "Search", Icon: IconSearch }, + { id: "explore", label: "Map", Icon: IconGraph }, + { id: "agents", label: "AI", Icon: IconAgent }, + { id: "review", label: "Review", Icon: IconReview }, ]; /** Bottom tab bar for phone/tablet — primary navigation on small screens. */ @@ -22,9 +30,10 @@ export function MobileNav() { className={`mobile-nav__item ${view === item.id ? "mobile-nav__item--active" : ""}`} onClick={() => setView(item.id)} aria-current={view === item.id ? "page" : undefined} + aria-label={item.label} > - {item.icon} + {item.label} @@ -36,7 +45,7 @@ export function MobileNav() { aria-label="Open Cortex Assist" > - ✦ + Assist diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 734d0c7..612990c 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,12 +1,19 @@ import type { ViewId } from "../../types"; import { useApp } from "../../context/AppContext"; +import { + IconAgent, + IconGraph, + IconHome, + IconReview, + IconSearch, +} from "../ui/icons"; -const NAV: { id: ViewId; label: string; hint: string; icon: string }[] = [ - { id: "home", label: "Overview", hint: "Executive summary", icon: "⌂" }, - { id: "ask", label: "Search", hint: "Ask memory", icon: "?" }, - { id: "explore", label: "Memory map", hint: "Connections & lineage", icon: "◎" }, - { id: "agents", label: "AI agents", hint: "Inject & capture", icon: "⚡" }, - { id: "review", label: "Conflicts", hint: "Human review", icon: "⚖" }, +const NAV: { id: ViewId; label: string; hint: string; Icon: typeof IconHome }[] = [ + { id: "home", label: "Overview", hint: "Executive summary", Icon: IconHome }, + { id: "ask", label: "Search", hint: "Ask memory", Icon: IconSearch }, + { id: "explore", label: "Memory map", hint: "Connections & lineage", Icon: IconGraph }, + { id: "agents", label: "AI agents", hint: "Inject & capture", Icon: IconAgent }, + { id: "review", label: "Conflicts", hint: "Human review", Icon: IconReview }, ]; export function Sidebar() { @@ -26,9 +33,10 @@ export function Sidebar() { className={`sidebar__link ${view === item.id ? "sidebar__link--active" : ""}`} onClick={() => setView(item.id)} aria-current={view === item.id ? "page" : undefined} + aria-label={`${item.label} — ${item.hint}`} > - {item.icon} + {item.label} diff --git a/frontend/src/components/memory/TimelineView.tsx b/frontend/src/components/memory/TimelineView.tsx index d6b4fa6..2d51bb9 100644 --- a/frontend/src/components/memory/TimelineView.tsx +++ b/frontend/src/components/memory/TimelineView.tsx @@ -1,5 +1,7 @@ import type { DecisionResult } from "../../types"; import { formatRelativeTime, formatSource } from "../../lib/format"; +import { StateView } from "../ui/StateView"; +import { IconEmpty } from "../ui/icons"; type Props = { decisions: DecisionResult[]; @@ -13,9 +15,9 @@ export function TimelineView({ decisions, onSelect }: Props) { if (sorted.length === 0) { return ( -
-

Your organizational timeline will appear here after you search or load memories.

-
+ } title="Timeline is empty"> + Your organizational timeline will appear here after you search or load memories. + ); } diff --git a/frontend/src/components/ui/StateView.tsx b/frontend/src/components/ui/StateView.tsx index c176257..2ea12e1 100644 --- a/frontend/src/components/ui/StateView.tsx +++ b/frontend/src/components/ui/StateView.tsx @@ -1,7 +1,8 @@ import type { ReactNode } from "react"; +import { IconEmpty } from "./icons"; type Props = { - icon?: string; + icon?: ReactNode; title: string; children?: ReactNode; action?: ReactNode; @@ -9,7 +10,8 @@ type Props = { }; /** Empty / error / informational state shared across views. */ -export function StateView({ icon = "◇", title, children, action, tone = "neutral" }: Props) { +export function StateView({ icon, title, children, action, tone = "neutral" }: Props) { + const glyph = icon ?? ; return (
- {icon} + {glyph}

{title}

- {children ?

{children}

: null} - {action ?
{action}
: null} + {children ?
{children}
: null} + {action ?
{action}
: null}
); } diff --git a/frontend/src/components/ui/icons.tsx b/frontend/src/components/ui/icons.tsx new file mode 100644 index 0000000..c547ade --- /dev/null +++ b/frontend/src/components/ui/icons.tsx @@ -0,0 +1,101 @@ +import type { SVGProps, ReactNode } from "react"; + +type IconProps = SVGProps & { size?: number }; + +function Icon({ size = 18, children, ...props }: IconProps & { children: ReactNode }) { + return ( + + {children} + + ); +} + +export function IconHome(props: IconProps) { + return ( + + + + ); +} + +export function IconSearch(props: IconProps) { + return ( + + + + + ); +} + +export function IconGraph(props: IconProps) { + return ( + + + + + + + ); +} + +export function IconAgent(props: IconProps) { + return ( + + + + ); +} + +export function IconReview(props: IconProps) { + return ( + + + + ); +} + +export function IconEmpty(props: IconProps) { + return ( + + + + + ); +} + +export function IconSpark(props: IconProps) { + return ( + + + + ); +} + +export function IconLock(props: IconProps) { + return ( + + + + + ); +} + +export function IconLink(props: IconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/lib/assistant.ts b/frontend/src/lib/assistant.ts index e1d022b..cdb4fd5 100644 --- a/frontend/src/lib/assistant.ts +++ b/frontend/src/lib/assistant.ts @@ -32,7 +32,7 @@ export const WELCOME_MESSAGES: AssistantMessage[] = [ export function summarizeQueryResults(response: QueryResponse): string { const { total, latency_ms, query, results } = response; if (total === 0) { - return `I searched your workspace memory for **"${query}"** but didn't find matching decisions yet. If you're on a fresh install, run \`make demo\` to seed example memories, or capture a decision with **Remember** via the API.`; + return `I searched your workspace memory for **"${query}"** but didn't find matching decisions yet. Connect your tools or capture a decision via **AI agents** to build organizational memory.`; } const top = results[0]; From 9106d95fccfba4e2dda7fe4d6273251176cb097f Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 12:04:40 -0400 Subject: [PATCH 02/11] Improve first-run onboarding and Connection workspace UX. Add a four-step onboarding flow, persist settings under cortex_settings_v2, surface workspace presets and live-demo context on Home, and wire replay tour. Co-authored-by: Cursor --- .../components/onboarding/OnboardingModal.tsx | 175 ++++++++++++++---- frontend/src/lib/onboarding.ts | 22 ++- frontend/src/lib/settings.ts | 54 ++++++ frontend/src/views/HomeView.tsx | 153 ++++++++------- 4 files changed, 300 insertions(+), 104 deletions(-) create mode 100644 frontend/src/lib/settings.ts diff --git a/frontend/src/components/onboarding/OnboardingModal.tsx b/frontend/src/components/onboarding/OnboardingModal.tsx index f4a0ac4..4b432d8 100644 --- a/frontend/src/components/onboarding/OnboardingModal.tsx +++ b/frontend/src/components/onboarding/OnboardingModal.tsx @@ -1,69 +1,174 @@ import { useState } from "react"; import { markOnboardingComplete } from "../../lib/onboarding"; +import { useApp } from "../../context/AppContext"; +import { IconSpark } from "../ui/icons"; type Props = { onComplete: () => void; onOpenCopilot: () => void; + onFinishAsk: (workspace: string, query: string) => void; }; -const STEPS = [ - { - title: "Your organization's living memory", - body: "Cortex captures decisions — not documents — from Slack, GitHub, Jira, and more. Ask natural questions and get answers with who decided, why, and what's affected.", - icon: "◈", - }, - { - title: "Ask in plain language", - body: 'Try questions like "Why did we choose CockroachDB for payments?" or "What affects checkout?" Cortex searches structured memory, not scattered threads.', - icon: "?", - }, - { - title: "Meet Assist", - body: "Cortex Assist guides you, runs searches, and explains results. Use the memory map to see how people, systems, and decisions connect over time.", - icon: "✦", - }, +const WORKSPACE_PRESETS = [ + { id: "local-dev", label: "Demo workspace", hint: "Pre-seeded decisions for demos" }, + { id: "oss-tiangolo-fastapi", label: "OSS: FastAPI", hint: "Open-source graph import" }, + { id: "oss-adr", label: "OSS: ADRs", hint: "Architecture decision records" }, ] as const; -export function OnboardingModal({ onComplete, onOpenCopilot }: Props) { +const DEMO_QUERY = "Why CockroachDB for payments?"; + +export function OnboardingModal({ onComplete, onOpenCopilot, onFinishAsk }: Props) { + const { setWorkspaceId, apiKey, setApiKey, saveApiKey } = useApp(); const [step, setStep] = useState(0); - const current = STEPS[step]; - const isLast = step === STEPS.length - 1; + const [workspace, setWorkspace] = useState("local-dev"); + const isLast = step === 3; - function finish() { - markOnboardingComplete(); + function finishToAsk(): void { + markOnboardingComplete(workspace); + setWorkspaceId(workspace); onComplete(); - if (isLast) onOpenCopilot(); + onFinishAsk(workspace, DEMO_QUERY); + } + + function finishToAssist(): void { + markOnboardingComplete(workspace); + setWorkspaceId(workspace); + onComplete(); + onOpenCopilot(); } return ( -
+
- {STEPS.map((_, i) => ( + {[0, 1, 2, 3].map((i) => ( ))}
- - {current.icon} - -

- {current.title} -

-

{current.body}

+ + {step === 0 ? ( + <> + + ◈ + +

+ Your organization's living memory +

+

+ Cortex captures decisions — not documents — from Slack, GitHub, Jira, + and more. Ask natural questions and get answers with who decided, why, and what's + affected. +

+ + ) : null} + + {step === 1 ? ( + <> +

+ Choose a workspace +

+

+ Workspaces isolate organizational memory. Start with the demo workspace or pick an OSS + preset. +

+
+ {WORKSPACE_PRESETS.map((w) => ( + + ))} +
+ + ) : null} + + {step === 2 ? ( + <> +

+ Try a demo search +

+

+ We'll open Search with a sample question so you can see trust scores, coverage, and + decision stories in seconds. +

+
{DEMO_QUERY}
+ + ) : null} + + {step === 3 ? ( + <> +

+ Connection (optional) +

+

+ The public demo works without a key. If your deployment requires authentication, add + your API key now — you can always change it under Connection later. +

+ + setApiKey(e.target.value)} + placeholder="Optional" + autoComplete="off" + /> + + ) : null} +