Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions frontend/src/components/layout/MobileNav.tsx
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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}
>
<span className="mobile-nav__icon" aria-hidden>
{item.icon}
<item.Icon size={20} />
</span>
<span className="mobile-nav__label">{item.label}</span>
</button>
Expand All @@ -36,7 +45,7 @@ export function MobileNav() {
aria-label="Open Cortex Assist"
>
<span className="mobile-nav__icon" aria-hidden>
<IconSpark size={20} />
</span>
<span className="mobile-nav__label">Assist</span>
</button>
Expand Down
22 changes: 15 additions & 7 deletions frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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}`}
>
<span className="sidebar__icon" aria-hidden>
{item.icon}
<item.Icon size={18} />
</span>
<span className="sidebar__text">
<span className="sidebar__label">{item.label}</span>
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/memory/TimelineView.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -13,9 +15,9 @@ export function TimelineView({ decisions, onSelect }: Props) {

if (sorted.length === 0) {
return (
<section className="timeline-empty">
<p>Your organizational timeline will appear here after you search or load memories.</p>
</section>
<StateView icon={<IconEmpty size={28} />} title="Timeline is empty">
Your organizational timeline will appear here after you search or load memories.
</StateView>
);
}

Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/ui/StateView.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import type { ReactNode } from "react";
import { IconEmpty } from "./icons";

type Props = {
icon?: string;
icon?: ReactNode;
title: string;
children?: ReactNode;
action?: ReactNode;
tone?: "neutral" | "error";
};

/** 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 ?? <IconEmpty size={28} />;
return (
<section
className={`state-view ${tone === "error" ? "state-view--error" : ""}`}
role={tone === "error" ? "alert" : "status"}
aria-live={tone === "error" ? "assertive" : "polite"}
>
<span className="state-view__icon" aria-hidden>
{icon}
{glyph}
</span>
<h3 className="state-view__title">{title}</h3>
{children ? <p className="state-view__body">{children}</p> : null}
{action ? <div style={{ marginTop: "0.75rem" }}>{action}</div> : null}
{children ? <div className="state-view__body">{children}</div> : null}
{action ? <div className="state-view__action">{action}</div> : null}
</section>
);
}
101 changes: 101 additions & 0 deletions frontend/src/components/ui/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { SVGProps, ReactNode } from "react";

type IconProps = SVGProps<SVGSVGElement> & { size?: number };

function Icon({ size = 18, children, ...props }: IconProps & { children: ReactNode }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden={props["aria-label"] ? undefined : true}
{...props}
>
{children}
</svg>
);
}

export function IconHome(props: IconProps) {
return (
<Icon {...props}>
<path d="M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1z" />
</Icon>
);
}

export function IconSearch(props: IconProps) {
return (
<Icon {...props}>
<circle cx="11" cy="11" r="6" />
<path d="m20 20-4-4" />
</Icon>
);
}

export function IconGraph(props: IconProps) {
return (
<Icon {...props}>
<circle cx="6" cy="18" r="2.5" />
<circle cx="18" cy="6" r="2.5" />
<circle cx="18" cy="18" r="2.5" />
<path d="M8.2 16.5 15.8 8.5M8.2 16.5 15.8 16.5" />
</Icon>
);
}

export function IconAgent(props: IconProps) {
return (
<Icon {...props}>
<path d="M13 3 5 14h6l-1 7 9-12h-6z" />
</Icon>
);
}

export function IconReview(props: IconProps) {
return (
<Icon {...props}>
<path d="M7 4h10v16l-5-3-5 3z" />
</Icon>
);
}

export function IconEmpty(props: IconProps) {
return (
<Icon {...props}>
<rect x="5" y="5" width="14" height="14" rx="2" />
<path d="M9 12h6" />
</Icon>
);
}

export function IconSpark(props: IconProps) {
return (
<Icon {...props}>
<path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8" />
</Icon>
);
}

export function IconLock(props: IconProps) {
return (
<Icon {...props}>
<rect x="6" y="10" width="12" height="10" rx="2" />
<path d="M8 10V8a4 4 0 1 1 8 0v2" />
</Icon>
);
}

export function IconLink(props: IconProps) {
return (
<Icon {...props}>
<path d="M10 14a4 4 0 0 1 0-6l2-2a4 4 0 0 1 6 6l-1 1" />
<path d="M14 10a4 4 0 0 1 0 6l-2 2a4 4 0 0 1-6-6l1-1" />
</Icon>
);
}
2 changes: 1 addition & 1 deletion frontend/src/lib/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading