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
4 changes: 2 additions & 2 deletions app/_home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// (query-flow.tsx). The selection is in the URL (?studies=a,b), so a refresh or a shared link
// lands on it.
import { useSearchParams } from "next/navigation";
import { PortalHeader } from "../portal/portal-chrome.tsx";
import { SiteHeader } from "../_ui/site-header.tsx";
import { QueryFlow } from "./query-flow.tsx";

/** the selection written back in place: ticking studies is not a history entry */
Expand All @@ -16,7 +16,7 @@ export function HomePage() {
const q = useSearchParams();
const studies = [...new Set((q.get("studies") || q.get("study") || "").split(",").map((s) => s.trim()).filter((s) => /^[A-Za-z0-9._-]+$/.test(s)))];
return <>
<PortalHeader page="choose studies, then explore them or query them by gene" />
<SiteHeader page="choose studies, then explore them or query them by gene" current="studies" />
<main className="analysis-main">
<QueryFlow initial={{ studies, filters: [], query: "" }} onStudies={writeStudies} queryTitle="Select genes to query:" />
</main>
Expand Down
39 changes: 39 additions & 0 deletions app/_ui/site-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";
// THE app's header, the same on every page: the name (home), which page this is, the same links in
// the same order, a page's own tools (its assistant menu) and the sign-in.
import LoginButton from "../lab/login-button.tsx";

const LINKS = [
["studies", "/", "studies"],
["examples", "/lab?tab=examples", "examples"],
["reproductions", "/reproductions", "paper reproductions"],
["campaigns", "/campaign", "hypothesis campaigns"],
["verification", "/verification", "how we verify"],
["rationale", "/rationale", "rationale"],
] as const;
export type SiteLink = typeof LINKS[number][0];

export function SiteHeader({ page, current, onLink, children }: {
/** which page this is, beside the name */
page: string;
/** the link this page is, marked as the current one */
current?: SiteLink;
/** a page that can open a link in place (the lab's own tabs) does, and says so by returning true */
onLink?: (link: SiteLink) => boolean;
/** the page's own tools, before the sign-in */
children?: React.ReactNode;
}) {
return (
<header>
<h1><a href="/" className="text-foreground no-underline" title="choose studies, then explore or query them">cbio-lab</a></h1>
<span className="sub">{page}</span>
<span className="spacer" />
{LINKS.map(([key, href, label]) => (
<a key={key} className="toplink" href={href} aria-current={key === current ? "page" : undefined}
onClick={(e) => { if (!e.metaKey && !e.ctrlKey && !e.shiftKey && onLink?.(key)) e.preventDefault(); }}>{label}</a>
))}
{children}
<LoginButton />
</header>
);
}
4 changes: 3 additions & 1 deletion app/_ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ function TabsList({
)
}

// An unselected tab is full-strength text, teal on hover: shadcn's default fades it to 60%, which
// reads as disabled. The selected tab stands out by its own colour, underline or raised chip.
// Chromium marks a tab focused by a click as :focus-visible, so a tab remembers that a pointer
// focused it (data-pointer) and shows its focus ring again from the first key press.
function TabsTrigger({
Expand All @@ -71,7 +73,7 @@ function TabsTrigger({
onKeyDown={(e) => { delete e.currentTarget.dataset.pointer; onKeyDown?.(e) }}
onBlur={(e) => { delete e.currentTarget.dataset.pointer; onBlur?.(e) }}
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:hover:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[pointer]:focus-visible:border-transparent data-[pointer]:focus-visible:ring-0 data-[pointer]:focus-visible:outline-none",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
Expand Down
10 changes: 7 additions & 3 deletions app/_ui/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ export const CHIP = "rounded-pill px-2.5 text-sm font-normal aria-pressed:border
export const FIELD = "h-ctl bg-card text-md md:text-md";

/** a page's own sections (the lab's Explore / Saved / Analysis, a portal page's tabs): a line tab one step larger than LINE_TAB */
export const PAGE_TAB = "h-auto flex-none rounded-none px-3.5 pt-2 pb-1.5 text-base leading-normal after:bottom-[-1px] data-[state=active]:text-accent-foreground data-[state=active]:after:bg-primary";
export const PAGE_TAB = "h-auto flex-none rounded-none px-3.5 pt-2 pb-1.5 text-base leading-normal after:bottom-[-1px] data-[state=active]:text-accent-foreground dark:data-[state=active]:text-accent-foreground data-[state=active]:after:bg-primary";

/** a row of page tabs underlined by a rule (TabsList variant "line"), and one of its tabs */
export const LINE_TABS = "w-full flex-wrap justify-start gap-1.5 border-b border-border p-0 group-data-[orientation=horizontal]/tabs:h-auto";
export const LINE_TAB = "h-auto flex-none rounded-none px-3 pt-1.5 pb-1.5 text-md after:bottom-[-1px] data-[state=active]:text-accent-foreground data-[state=active]:after:bg-primary";
export const LINE_TAB = "h-auto flex-none rounded-none px-3 pt-1.5 pb-1.5 text-md after:bottom-[-1px] data-[state=active]:text-accent-foreground dark:data-[state=active]:text-accent-foreground data-[state=active]:after:bg-primary";

/** a body's own selector (a gene, a group) over what it shows: segmented tabs that wrap */
export const SEG_TABS = "h-auto max-w-full flex-wrap justify-start group-data-[orientation=horizontal]/tabs:h-auto";
export const SEG_TAB = "h-7 flex-none px-2.5 text-sm font-normal data-[state=active]:font-semibold";
export const SEG_TAB = "h-7 flex-none px-2.5 text-sm font-medium " +
// the selected one is a filled chip, in both schemes: a raised white chip on grey was too quiet to read.
// One weight for every state, so a chip keeps its width and the row does not shift when the choice moves.
"data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:hover:text-primary-foreground " +
"dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-primary dark:data-[state=active]:text-primary-foreground";
12 changes: 3 additions & 9 deletions app/campaign/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Campaign, Funnel, HypothesisRow } from "../../../lib/campaign.ts";
import { installAgentTools } from "../agent-tools.ts";
import { Prompt } from "../prompt.tsx";
import { AgentToolsMenu } from "../../_agent/AgentToolsMenu.tsx";
import LoginButton from "../../lab/login-button.tsx";
import { SiteHeader } from "../../_ui/site-header.tsx";
import { useCustomDataState } from "../../lab/custom-data-panel.tsx";
import { LoaderCircle, Lock, Play } from "lucide-react";
import { Button } from "../../_ui/button";
Expand Down Expand Up @@ -119,16 +119,10 @@ export default function CampaignPage() {

return (
<>
<header>
<h1><a href="/campaign" className="camp-home">Hypothesis campaigns</a></h1>
<span className="sub">cbio-lab as the referee for AI-generated hypotheses</span>
<span className="spacer" />
<a href="/lab" className="camp-nav">analysis app</a>
<a href="/rationale" className="camp-nav">why this exists</a>
<LoginButton />
<SiteHeader page="hypothesis campaigns — cbio-lab as the referee for AI-generated hypotheses" current="campaigns">
<AgentToolsMenu page="campaign" tools={TOOL_LIST} webmcp={webmcp}
intro="Your AI assistant — Claude in the Chrome sidebar — writes, checks and registers this campaign's hypotheses with these tools, and can lock and run it once you agree. The engine checks everything it submits." />
</header>
</SiteHeader>
<main className="camp">
<section className="camp-q">
<div className="camp-eyebrow">Research question · {c.study}</div>
Expand Down
12 changes: 3 additions & 9 deletions app/campaign/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
import { installIndexTools } from "./agent-tools.ts";
import { Prompt } from "./prompt.tsx";
import { AgentToolsMenu } from "../_agent/AgentToolsMenu.tsx";
import LoginButton from "../lab/login-button.tsx";
import { SiteHeader } from "../_ui/site-header.tsx";

type Row = { id: string; question: string; study: string; status: string; lockedAt: string | null; createdAt: string };

Expand All @@ -33,16 +33,10 @@ export default function CampaignIndex() {

return (
<>
<header>
<h1>Hypothesis campaigns</h1>
<span className="sub">cbio-lab as the referee for AI-generated hypotheses</span>
<span className="spacer" />
<a href="/lab" className="camp-nav">analysis app</a>
<a href="/rationale" className="camp-nav">why this exists</a>
<LoginButton />
<SiteHeader page="hypothesis campaigns — cbio-lab as the referee for AI-generated hypotheses" current="campaigns">
<AgentToolsMenu page="campaign list" tools={[["campaign_create", "createCampaign(question, study?)"], ["campaign_list", "listCampaigns()"]]} webmcp={webmcp}
intro="Your AI assistant — Claude in the Chrome sidebar — starts a campaign for your question with these tools; the campaign's own page then has the tools for its hypotheses." />
</header>
</SiteHeader>
<main className="camp">
<section className="camp-explain">
<div><strong>1 · You ask</strong><p>Your research question, in plain words, to your AI assistant — Claude in the Chrome sidebar.</p></div>
Expand Down
6 changes: 4 additions & 2 deletions app/datasets/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// The data sets page reads the warehouse at request time, which under Cache Components must stream
// inside a Suspense boundary; the fallback holds the page's shape while the catalog loads.
import { Suspense } from "react";
import { SiteHeader } from "../_ui/site-header.tsx";

export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
return (
return (<>
<SiteHeader page="data sets — the studies this deployment holds" />
<Suspense fallback={
<main className="repro-page ds-page">
<div className="repro-intro"><h1>Data sets</h1><p className="meta"><span className="spinner" /> loading the studies this deployment holds…</p></div>
<div className="skeleton-view" style={{ height: 480 }} />
</main>
}>{children}</Suspense>
);
</>);
}
5 changes: 0 additions & 5 deletions app/datasets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export default async function DatasetsPage() {
const sum = (k: keyof DatasetRow) => rows.reduce((a, r) => a + (Number(r[k]) || 0), 0);
return (
<main className="repro-page ds-page">
<div className="repro-nav">
<a href="/lab">← back to the app</a>
<a href="/reproductions">paper reproductions</a>
<a href="/rationale">rationale</a>
</div>
<PageTools kind="datasets" spec={rows} />
<div className="repro-intro">
<h1>Datasets</h1>
Expand Down
12 changes: 4 additions & 8 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ pre {
.skeleton-counts { display: inline-block; vertical-align: middle; width: 15rem; height: 0.9em; border-radius: var(--r-sm); }
.skeleton-facet { height: 13rem; }
.skeleton-view { height: 16rem; margin-bottom: 1rem; }
/* under a view's toolbar the figure stands where the drawing will be, as far below it as the drawing */
.viewtb + .skeleton-view { margin-top: var(--s3); }
.skeleton-addview { height: 3.4rem; margin-bottom: 1rem; }
/* A view recomputing after a settings change: the figure area only (Updating measures where
the header and toolbars end), opaque so the stale answer can't be read as the new one. */
Expand Down Expand Up @@ -601,6 +603,8 @@ main.analysis-main { max-width: min(132rem, 100%); }

/* ---------------- small shared idioms ---------------- */
.toplink { color: var(--accent-ink); font-size: var(--t-md); }
/* the page you are on: plain text colour, no underline (same weight, so the header does not shift) */
.toplink[aria-current=page] { color: var(--ink); text-decoration: none; }
.row.center { align-items: center; }
.row.baseline { align-items: baseline; }
.row.g5 { gap: 0.5rem; }
Expand Down Expand Up @@ -806,9 +810,6 @@ main.analysis-main { max-width: min(132rem, 100%); }
.chatfigbody svg { display: block; }

/* ---------------- /rationale — RATIONALE.md rendered as a page ---------------- */
.rationale-nav { margin-bottom: 1.2rem; display: flex; align-items: center; gap: var(--s4); }
.rationale-nav a { color: var(--accent-ink); font-size: var(--t-md); text-decoration: none; }
.rationale-nav a:hover { text-decoration: underline; }

.rationale-doc { line-height: 1.65; }
.rationale-doc h1 {
Expand Down Expand Up @@ -1331,9 +1332,6 @@ details.cohortpick .gearmenu-items { min-width: 22rem; max-width: 30rem; gap: 0;
gallery card per paper, same .card base every other page's cards use.
--------------------------------------------------------------------------- */
.repro-page { max-width: 900px; margin: 0 auto; padding: var(--s5) var(--s4) var(--s6); }
.repro-nav { display: flex; align-items: center; gap: var(--s4); margin-bottom: var(--s5); }
.repro-nav a { color: var(--accent-ink); font-size: var(--t-md); text-decoration: none; }
.repro-nav a:hover { text-decoration: underline; }
.repro-intro h1 { font-family: "IBM Plex Serif", Georgia, serif; font-size: var(--t-2xl); margin: 0 0 var(--s2); }
.repro-intro .lede { color: var(--muted); max-width: 68ch; line-height: 1.6; margin: 0; }
.repro-tiles { display: flex; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s4); }
Expand Down Expand Up @@ -1412,8 +1410,6 @@ details.cohortpick .gearmenu-items { min-width: 22rem; max-width: 30rem; gap: 0;

/* ---- hypothesis campaigns (/campaign) ---------------------------------------------------- */
.camp { max-width: 64rem; }
.camp-home, .camp-home:visited { color: inherit; text-decoration: none; }
.camp-nav { color: var(--accent-ink); font-size: var(--t-md); }
.camp-eyebrow { font-size: var(--t-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.camp-q { margin: var(--s3) 0 var(--s5); }
.camp-q h2 { font-family: "IBM Plex Serif", Georgia, serif; font-size: var(--t-xl); font-weight: 600; line-height: 1.3; margin: var(--s1) 0 var(--s4); }
Expand Down
10 changes: 2 additions & 8 deletions app/groups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// item (the Saved tab's "share…"), not here.
import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import LoginButton from "../lab/login-button.tsx";
import { useCustomDataState } from "../lab/custom-data-panel.tsx";
import { SiteHeader } from "../_ui/site-header.tsx";
import { requestLogin } from "../lab/custom-data-client.ts";
import { Waiting } from "../_oncoprint/OncoprintCard.tsx";
import { api } from "../lab/api.ts";
Expand Down Expand Up @@ -40,13 +40,7 @@ export default function GroupsPage() {

return (
<>
<header>
<h1>Groups</h1>
<span className="sub">who you share saved cohorts, analyses and custom data with</span>
<span className="spacer" />
<a href="/lab?tab=cohorts" className="camp-nav">your saved work</a>
<LoginButton />
</header>
<SiteHeader page="groups — who you share saved cohorts, analyses and custom data with" />
<main className="camp groups" data-tw>
{!login.loaded ? <div className="card"><Waiting what="your sign-in" /></div>
: !login.unlocked ? <div className="card"><p className="m-0 text-md">
Expand Down
Loading
Loading