From 1b80c44129d9904ded5d7ca9e71bad9c419b6a03 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 10:53:45 -0400 Subject: [PATCH 01/45] git workflow - taken from prototype UI branch --- .github/workflows/deploy-github-pages.yml | 116 ++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/deploy-github-pages.yml diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml new file mode 100644 index 0000000..d9a7ddf --- /dev/null +++ b/.github/workflows/deploy-github-pages.yml @@ -0,0 +1,116 @@ +name: Deploy to GitHub Pages + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, closed] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +jobs: + build-deploy: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Compute base path and destination + id: ctx + run: | + REPO_NAME="${GITHUB_REPOSITORY#*/}" + OWNER="${GITHUB_REPOSITORY%/*}" + if [ "$REPO_NAME" = "${OWNER}.github.io" ]; then + ROOT_BASE="" + else + ROOT_BASE="/$REPO_NAME" + fi + if [ "${{ github.event_name }}" = "pull_request" ]; then + PR_NUM="${{ github.event.pull_request.number }}" + echo "base=$ROOT_BASE/pr-$PR_NUM" >> "$GITHUB_OUTPUT" + echo "dest=pr-$PR_NUM" >> "$GITHUB_OUTPUT" + echo "preview=true" >> "$GITHUB_OUTPUT" + else + echo "base=$ROOT_BASE" >> "$GITHUB_OUTPUT" + echo "dest=." >> "$GITHUB_OUTPUT" + echo "preview=false" >> "$GITHUB_OUTPUT" + fi + + - run: npm ci + + - name: Build static export + env: + EXPORT: "true" + NEXT_PUBLIC_BASE_PATH: ${{ steps.ctx.outputs.base }} + run: npm run build + + - name: Disable Jekyll processing + run: touch out/.nojekyll + + - name: Deploy to gh-pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./out + destination_dir: ${{ steps.ctx.outputs.dest }} + keep_files: true + commit_message: "Deploy ${{ steps.ctx.outputs.dest }} from ${{ github.sha }}" + + - name: Comment preview URL on PR + if: steps.ctx.outputs.preview == 'true' + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const prNum = context.payload.pull_request.number; + const host = (repo === `${owner}.github.io`) + ? `https://${owner}.github.io` + : `https://${owner}.github.io/${repo}`; + const url = `${host}/pr-${prNum}/`; + const marker = ''; + const body = `${marker}\nπŸ”Ž **Pages preview:** ${url}\n\n_Built from ${context.sha.slice(0, 7)} Β· updates on each push to this PR._`; + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, issue_number: prNum, + }); + const existing = comments.find(c => c.body?.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: prNum, body }); + } + + cleanup: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Remove PR preview directory + run: | + set -e + PR_DIR="pr-${{ github.event.pull_request.number }}" + if [ -d "$PR_DIR" ]; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm -rf "$PR_DIR" + git commit -m "Cleanup preview for PR #${{ github.event.pull_request.number }}" + git push + else + echo "No preview directory to clean up." + fi \ No newline at end of file From 86f1bfd3b9629dce385dd90c12002660d34a6ed6 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 12:48:10 -0400 Subject: [PATCH 02/45] updated footer options + design --- src/app/Footer.tsx | 170 +++++++++++++++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 45 deletions(-) diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx index 3178112..05b071a 100644 --- a/src/app/Footer.tsx +++ b/src/app/Footer.tsx @@ -1,53 +1,133 @@ "use client"; -import {useState} from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; -const Footer: React.FC = () => { - const [isOpen, setIsOpen] = useState(false); +/** + * Footer β€” ported from sensein/brainkb-ui-prototype's Landing footer. + * + * Wraps itself in so the --bkb-* tokens resolve regardless of where + * the parent renders this (the root layout drops it after every route). + */ + +import React from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { FONTS, Theme } from "@/src/app/components/design-system"; - return ( -
-
-
- - BrainKB Logo - BrainKB - -
    -
  • - - About - -
  • -
  • - - Privacy Policy - -
  • -
  • - - Contact - -
  • -
+const COLUMNS: { h: string; items: { label: string; href?: string }[] }[] = [ +// { +// h: "Product", +// items: [ +// { label: "Explorer", href: "/knowledge-base" }, +// { label: "Dashboard", href: "/user/dashboard" }, +// { label: "SPARQL API", href: "/about" }, +// { label: "Changelog", href: "/about" }, +// ], +// }, + { + h: "Resources", + items: [ + { label: "Documentation", href: "http://docs.brainkb.org" }, + { label: "Ontologies", href: "https://brain-bican.github.io/models/" }, + { label: "Data sources", href: "/data-release" }, + ], + }, + { + h: "About", + items: [ +// { label: "Team", href: "/about" }, +// { label: "Governance", href: "/about" }, + { label: "Privacy", href: "/privacy-policy" }, + { label: "Contact", href: "/contact" }, + ], + }, +]; + +const Footer: React.FC = () => { + const year = new Date().getFullYear(); + return ( + +
+
+
+
+ BrainKB +
BrainKB
+
+
+ An open neuroscience knowledge graph. +
+
+ {COLUMNS.map((col) => ( +
+
+ {col.h} +
+ {col.items.map((it) => ( +
+ {it.href ? ( + + {it.label} + + ) : ( + {it.label} + )}
-
- - Β© 2024 - {new Date().getFullYear()} BrainKB & Senseable Intelligence Group. All Rights Reserved. - + ))}
-
- ); + ))} +
+ +
+ Β© {year} BrainKB Β·{" "} + + Senseable Intelligence Group + +
+
+
+ ); }; export default Footer; From b2ffeccf3f6d5e605fa70736bc2971b3b021d683 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 12:51:42 -0400 Subject: [PATCH 03/45] css updated to match https://sensein.group/brainkb-ui-prototype/ design --- src/app/globals.css | 496 +++++++++++++++++++++++++++++++ src/app/user/dashboard/fonts.css | 3 + 2 files changed, 499 insertions(+) create mode 100644 src/app/user/dashboard/fonts.css diff --git a/src/app/globals.css b/src/app/globals.css index 8c0002f..d129cd5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,8 @@ +/* Display + mono font faces used by the bkb design system. Loaded globally + so the navbar "BrainKB" wordmark and any other serif/mono text resolves + on every route, not just /user/* and /admin/*. */ +@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;600&display=swap"); + @tailwind base; @tailwind components; @tailwind utilities; @@ -145,3 +150,494 @@ thead.sticky { } } + +/* ───────────────────────────────────────────────────────────────────── + BrainKB design system (ported from sensein/brainkb-ui-prototype). + Used by /dashboard and /admin routes via the .bkb wrapper class. + Existing pages are unaffected β€” these tokens only apply inside .bkb. + ───────────────────────────────────────────────────────────────────── */ + +.bkb { + /* Default to the light theme. overrides at runtime. */ + --bkb-bg: oklch(0.985 0.004 85); + --bkb-surface: oklch(1 0 0); + --bkb-surfaceAlt: oklch(0.965 0.006 85); + --bkb-border: oklch(0.88 0.008 85); + --bkb-borderStrong: oklch(0.78 0.012 85); + --bkb-text: oklch(0.20 0.012 220); + --bkb-textMuted: oklch(0.48 0.010 220); + --bkb-textSubtle: oklch(0.62 0.008 220); + --bkb-primary: oklch(0.32 0.045 195); + --bkb-primaryInk: oklch(0.98 0.004 85); + --bkb-accent: oklch(0.62 0.11 170); + --bkb-agent: oklch(0.58 0.13 165); + --bkb-evidence: oklch(0.55 0.14 285); + --bkb-publication: oklch(0.62 0.12 65); + --bkb-danger: oklch(0.55 0.18 28); + --bkb-grid: oklch(0.20 0.012 220 / 0.05); + + font-family: Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + letter-spacing: -0.005em; + background: var(--bkb-bg); + color: var(--bkb-text); +} + +.bkb * { + box-sizing: border-box; +} + +.bkb-serif { + font-family: "Instrument Serif", "Times New Roman", Georgia, serif; + letter-spacing: -0.015em; +} + +.bkb-mono { + font-family: "JetBrains Mono", "SF Mono", ui-monospace, Consolas, monospace; +} + +.bkb-fade-in { + animation: bkb-fade 0.4s ease-out; +} + +@keyframes bkb-fade { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: none; } +} + +.bkb-hover-row:hover { + background: var(--bkb-surfaceAlt); +} + +.bkb-scroll::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.bkb-scroll::-webkit-scrollbar-thumb { + background: var(--bkb-border); + border-radius: 4px; +} +.bkb-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.bkb-dot-grid { + background-image: radial-gradient(var(--bkb-grid) 1px, transparent 1px); + background-size: 20px 20px; +} + +.bkb-pulse-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--bkb-accent); + box-shadow: 0 0 0 0 var(--bkb-accent); + animation: bkb-pulse 2s infinite; + display: inline-block; +} + +@keyframes bkb-pulse { + 0% { box-shadow: 0 0 0 0 var(--bkb-accent); } + 70% { box-shadow: 0 0 0 8px transparent; } + 100% { box-shadow: 0 0 0 0 transparent; } +} + +.bkb-btn { + font-family: Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + font-size: 13px; + font-weight: 500; + padding: 8px 14px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + transition: all .15s; + display: inline-flex; + align-items: center; + gap: 6px; + letter-spacing: -0.005em; +} + +.bkb-btn-primary { + background: var(--bkb-primary); + color: var(--bkb-primaryInk); +} +.bkb-btn-primary:hover { + filter: brightness(1.08); +} + +.bkb-btn-ghost { + background: transparent; + color: var(--bkb-text); + border-color: var(--bkb-border); +} +.bkb-btn-ghost:hover { + background: var(--bkb-surfaceAlt); + border-color: var(--bkb-borderStrong); +} + +.bkb-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.bkb-input { + font-family: Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + font-size: 13px; + background: var(--bkb-surface); + color: var(--bkb-text); + border: 1px solid var(--bkb-border); + border-radius: 6px; + padding: 8px 12px; + outline: none; + width: 100%; +} +.bkb-input:focus { + border-color: var(--bkb-primary); + box-shadow: 0 0 0 3px oklch(from var(--bkb-primary) l c h / 0.2); +} + +.bkb-card { + background: var(--bkb-surface); + border: 1px solid var(--bkb-border); + border-radius: 10px; +} + +.bkb-kbd { + font-family: "JetBrains Mono", "SF Mono", ui-monospace, Consolas, monospace; + font-size: 11px; + background: var(--bkb-surfaceAlt); + border: 1px solid var(--bkb-border); + border-bottom-width: 2px; + border-radius: 4px; + padding: 1px 5px; + color: var(--bkb-textMuted); +} + +.bkb-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 500; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--bkb-border); + background: var(--bkb-surfaceAlt); + color: var(--bkb-textMuted); +} + +.bkb-link { + color: var(--bkb-primary); + text-decoration: none; + border-bottom: 1px solid transparent; +} +.bkb-link:hover { + border-bottom-color: var(--bkb-primary); +} + +/* ─── Tailwind β†’ bkb token overrides ────────────────────────────────── + Scoped to .bkb so existing routes outside the bkb Theme are unchanged. + These translate the most common Tailwind utility classes used by the + tool pages (Profile, Ingest KGs, SIE, Resource extraction, Job status) + into bkb design tokens β€” surfaces, borders, type scale, accent colors β€” + without rewriting each page body. */ + +/* Surfaces. + Cards (div / button / a / li / aside) get the bkb surface white; full- + width section / header / main bands stay transparent so the cream body + bg from the root Theme shows through end-to-end. This is what stops the + alternating-band effect on pages like /about, /hmba-taxonomy, the home + feature sections, etc. */ +.bkb div.bg-white, +.bkb article.bg-white, +.bkb aside.bg-white, +.bkb a.bg-white, +.bkb button.bg-white, +.bkb li.bg-white, +.bkb form.bg-white, +.bkb .dark\:bg-gray-800, +.bkb .dark\:bg-gray-900, +.bkb .dark\:bg-gray-700 { background-color: var(--bkb-surface) !important; } + +.bkb div.bg-gray-50, +.bkb article.bg-gray-50, +.bkb aside.bg-gray-50, +.bkb div.bg-gray-100, +.bkb .dark\:bg-gray-800\/50, +.bkb .dark\:bg-gray-700\/50 { background-color: var(--bkb-surfaceAlt) !important; } + +/* Page-band sections (py-20 / py-16 / py-12 with a bg utility) blend + into the cream body so the entire page reads as a single canvas. */ +.bkb section.bg-white, +.bkb section.bg-gray-50, +.bkb section.bg-gray-100, +.bkb header.bg-white, +.bkb header.bg-gray-50, +.bkb main > div.bg-white, +.bkb main > div.bg-gray-50 { background-color: transparent !important; } + +/* Text */ +.bkb .text-gray-900, +.bkb .text-black, +.bkb .dark\:text-white { color: var(--bkb-text) !important; } +.bkb .text-gray-700, +.bkb .text-gray-600, +.bkb .dark\:text-gray-300, +.bkb .dark\:text-gray-200 { color: var(--bkb-textMuted) !important; } +.bkb .text-gray-500, +.bkb .text-gray-400, +.bkb .dark\:text-gray-400, +.bkb .dark\:text-gray-500 { color: var(--bkb-textSubtle) !important; } + +/* Borders */ +.bkb .border-gray-200, +.bkb .border-gray-300, +.bkb .dark\:border-gray-600, +.bkb .dark\:border-gray-700 { border-color: var(--bkb-border) !important; } + +/* Primary accent (every blue / sky / cyan / indigo / teal text shade + resolves to the bkb-primary teal so primary text is one ink. The + 200/300/400 light shades existed for "text on dark bg" placements; in + our flat-cream world they need to be readable, so we don't dim them. */ +.bkb .text-blue-400, +.bkb .text-blue-500, +.bkb .text-blue-600, +.bkb .text-blue-700, +.bkb .text-blue-800, +.bkb .text-blue-900, +.bkb .text-sky-400, +.bkb .text-sky-500, +.bkb .text-sky-600, +.bkb .text-sky-700, +.bkb .text-sky-800, +.bkb .text-sky-900, +.bkb .text-cyan-500, +.bkb .text-cyan-600, +.bkb .text-cyan-700, +.bkb .text-indigo-500, +.bkb .text-indigo-600, +.bkb .text-indigo-700, +.bkb .text-teal-500, +.bkb .text-teal-600, +.bkb .text-teal-700, +.bkb .dark\:text-blue-300, +.bkb .dark\:text-blue-400 { color: var(--bkb-primary) !important; } + +/* Light blue-200 / sky-200 / blue-100 / sky-100 are typically used as + subtle text-on-dark accents. On the flat cream they'd vanish; map them + to the bkb-textMuted so they stay visible without becoming primary. */ +.bkb .text-blue-100, +.bkb .text-blue-200, +.bkb .text-sky-100, +.bkb .text-sky-200, +.bkb .text-blue-300, +.bkb .text-sky-300 { color: var(--bkb-textMuted) !important; } + +.bkb .bg-blue-600, +.bkb .bg-blue-500, +.bkb .bg-blue-700, +.bkb .hover\:bg-blue-700:hover, +.bkb .hover\:bg-blue-600:hover, +.bkb .bg-sky-600, +.bkb .bg-sky-500 { background-color: var(--bkb-primary) !important; color: var(--bkb-primaryInk) !important; } + +.bkb .bg-blue-50, +.bkb .bg-blue-100, +.bkb .dark\:bg-blue-900, +.bkb .dark\:bg-blue-900\/20 { + background-color: color-mix(in oklch, var(--bkb-primary), transparent 92%) !important; +} + +.bkb .border-blue-500, +.bkb .border-blue-600, +.bkb .border-sky-500 { border-color: var(--bkb-primary) !important; } + +/* Status accents */ +.bkb .text-green-600, +.bkb .text-green-700, +.bkb .text-emerald-500, +.bkb .text-emerald-600, +.bkb .dark\:text-green-400 { color: var(--bkb-accent) !important; } +.bkb .bg-green-50, +.bkb .bg-green-100, +.bkb .dark\:bg-green-900, +.bkb .dark\:bg-green-900\/20 { + background-color: color-mix(in oklch, var(--bkb-accent), transparent 92%) !important; +} +.bkb .bg-green-500, +.bkb .bg-green-600 { background-color: var(--bkb-accent) !important; color: var(--bkb-primaryInk) !important; } + +.bkb .text-red-600, +.bkb .text-red-700, +.bkb .text-red-500, +.bkb .dark\:text-red-400 { color: var(--bkb-danger) !important; } +.bkb .bg-red-50, +.bkb .bg-red-100, +.bkb .dark\:bg-red-900\/20 { + background-color: color-mix(in oklch, var(--bkb-danger), transparent 92%) !important; +} +.bkb .border-red-200, +.bkb .border-red-300 { border-color: color-mix(in oklch, var(--bkb-danger), transparent 65%) !important; } +.bkb .bg-red-500, +.bkb .bg-red-600 { background-color: var(--bkb-danger) !important; color: var(--bkb-primaryInk) !important; } + +.bkb .text-yellow-600, +.bkb .text-yellow-700, +.bkb .text-amber-500, +.bkb .text-amber-600, +.bkb .dark\:text-yellow-200, +.bkb .dark\:text-yellow-400 { color: var(--bkb-publication) !important; } +.bkb .bg-yellow-50, +.bkb .bg-yellow-100, +.bkb .dark\:bg-yellow-900\/20 { + background-color: color-mix(in oklch, var(--bkb-publication), transparent 92%) !important; +} +.bkb .border-yellow-200, +.bkb .border-yellow-300, +.bkb .dark\:border-yellow-800 { border-color: color-mix(in oklch, var(--bkb-publication), transparent 65%) !important; } + +.bkb .text-purple-500, +.bkb .text-purple-600, +.bkb .text-pink-500 { color: var(--bkb-evidence) !important; } +.bkb .bg-purple-50, +.bkb .bg-purple-100 { + background-color: color-mix(in oklch, var(--bkb-evidence), transparent 92%) !important; +} + +/* Card-like containers β€” soften the shadows the prototype's flat look avoids */ +.bkb .shadow, +.bkb .shadow-md, +.bkb .shadow-lg, +.bkb .shadow-xl, +.bkb .shadow-2xl, +.bkb .hover\:shadow-lg:hover, +.bkb .hover\:shadow-xl:hover, +.bkb .hover\:shadow-2xl:hover { + box-shadow: 0 1px 3px rgba(20, 30, 40, 0.04), 0 1px 2px rgba(20, 30, 40, 0.04) !important; +} + +/* Typography β€” every heading inside .bkb uses the Instrument Serif display + face at weight 400, so page titles, section titles, and card titles + (e.g. "Monthly Releases", "Nightly Releases") all read with the same + editorial voice instead of falling back to bold sans-serif. + Body text stays Inter via the root . */ +.bkb h1, +.bkb h2, +.bkb h3, +.bkb h4, +.bkb h5, +.bkb h6, +.bkb .text-3xl, +.bkb .text-4xl, +.bkb .text-5xl, +.bkb .text-6xl { + font-family: "Instrument Serif", "Times New Roman", Georgia, serif !important; + font-weight: 400 !important; + letter-spacing: -0.02em; + line-height: 1.15; +} +.bkb .text-3xl { font-size: 32px !important; line-height: 1.1; } +.bkb .text-2xl { font-size: 26px !important; line-height: 1.15; } +.bkb .text-xl { font-size: 20px !important; line-height: 1.25; } + +/* Mono code / kbd / pre fall back to JetBrains Mono so technical content + (SPARQL snippets, page_keys, prefix tags, IDs) is consistent. */ +.bkb code, +.bkb pre, +.bkb kbd, +.bkb samp, +.bkb .font-mono { + font-family: "JetBrains Mono", "SF Mono", ui-monospace, Consolas, monospace !important; +} + +/* Buttons: when a page uses a Tailwind button shape with our overridden + blue/green/red bg, also tighten the radius/shadow to match bkb-btn. */ +.bkb button.rounded-lg, +.bkb a.rounded-lg { + border-radius: 6px; +} + +/* ─── Gradient overrides ───────────────────────────────────────────── + The legacy About / Use Cases / Key Features pages lean heavily on + skyβ†’emerald and blueβ†’pink gradient backgrounds and gradient text. The + bkb prototype design is intentionally flat; collapse every gradient + inside .bkb to a single tone from the bkb palette. */ + +/* Kill every Tailwind gradient inside .bkb. Pages that want a tinted + layer should use a flat token; otherwise content blends straight into + the cream body bg from the root Theme. */ +.bkb [class*="bg-gradient-to-"] { + background-image: none !important; +} + +/* Section / page-wrapper blocks (heroes, big intros, alternating slabs) + become transparent so the cream body bg shows through end-to-end β€” + that's the same single tone the dashboard, admin and home pages share. + Cards (which carry borders / radius / shadows) override below. */ +.bkb section[class*="bg-gradient-to-"], +.bkb header[class*="bg-gradient-to-"], +.bkb div[class*="min-h-"][class*="bg-gradient-to-"], +.bkb section.py-20, +.bkb section.py-16, +.bkb section.py-12 { + background-color: transparent !important; +} + +/* Cards / tiles that previously used pastel gradients become flat white + bkb surfaces so they read consistently with the dashboard cards. */ +.bkb [class*="bg-gradient-to-br"][class*="from-sky-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-emerald-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-purple-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-amber-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-yellow-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-blue-50"], +.bkb [class*="bg-gradient-to-br"][class*="from-gray-50"] { + background-color: var(--bkb-surface) !important; + border: 1px solid var(--bkb-border); +} + +/* Gradient text used for big page titles ("About BrainKB", "BrainKB" + wordmark, hero numbers): kill the bg-clip and surface a flat + bkb-primary so the type is one consistent ink. */ +.bkb .bg-clip-text { + background-clip: initial !important; + -webkit-background-clip: initial !important; +} +.bkb .text-transparent { + color: var(--bkb-primary) !important; +} + +/* Saturated gradients ("from-sky-500 to-blue-500" etc.) are typically + used for icon-tiles or accent panels that contain `text-white` or + white SVG strokes. Keep them dark by collapsing the gradient to the + solid bkb-primary teal so the white content inside stays visible. + Do this for both _-500 and _-600 stops because both shapes appear. */ +.bkb [class*="bg-gradient-to-"][class*="from-sky-500"], +.bkb [class*="bg-gradient-to-"][class*="from-sky-600"], +.bkb [class*="bg-gradient-to-"][class*="from-emerald-500"], +.bkb [class*="bg-gradient-to-"][class*="from-emerald-600"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-500"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-600"], +.bkb [class*="bg-gradient-to-"][class*="from-purple-500"], +.bkb [class*="bg-gradient-to-"][class*="from-purple-600"], +.bkb [class*="bg-gradient-to-"][class*="from-pink-500"], +.bkb [class*="bg-gradient-to-"][class*="from-pink-600"], +.bkb [class*="bg-gradient-to-"][class*="from-teal-500"] { + background-color: var(--bkb-primary) !important; + background-image: none !important; + color: var(--bkb-primaryInk); +} + +/* When such a panel uses text-white explicitly (e.g. the HMBA hero card + "from-sky-500 via-blue-500 to-emerald-500"), keep the white ink intact + so it stays readable on the teal solid. */ +.bkb [class*="bg-gradient-to-"][class*="from-sky-500"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-blue-500"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-blue-600"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-emerald-500"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-purple-500"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-pink-500"] .text-white, +.bkb [class*="bg-gradient-to-"][class*="from-teal-500"] .text-white { + color: var(--bkb-primaryInk) !important; +} + diff --git a/src/app/user/dashboard/fonts.css b/src/app/user/dashboard/fonts.css new file mode 100644 index 0000000..c4aad5d --- /dev/null +++ b/src/app/user/dashboard/fonts.css @@ -0,0 +1,3 @@ +/* Pull in the prototype's fonts: Instrument Serif (display) + JetBrains Mono (mono). + Inter is already loaded by the root layout via next/font. */ +@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;600&display=swap"); From 671de24bd7db8b3fc37de255d76d3c41d9c4fe54 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 13:26:15 -0400 Subject: [PATCH 04/45] updated font to make it consistent --- src/app/globals.css | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index d129cd5..878fa2a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -539,6 +539,44 @@ thead.sticky { .bkb .text-3xl { font-size: 32px !important; line-height: 1.1; } .bkb .text-2xl { font-size: 26px !important; line-height: 1.15; } .bkb .text-xl { font-size: 20px !important; line-height: 1.25; } +/* Compress the top of the Tailwind type scale so legacy pages that picked + text-5xl/6xl for their hero (about / contact / privacy / tools-and-libs) + don't tower over tool pages that picked text-3xl (sie / extract / jobs) + or text-2xl (see / ingest-kg). With these caps, the scale runs ~26β†’48px + end to end instead of 24β†’60px. */ +.bkb .text-4xl { font-size: 36px !important; line-height: 1.1; } +.bkb .text-5xl { font-size: 42px !important; line-height: 1.05; } +.bkb .text-6xl { font-size: 48px !important; line-height: 1.0; } + +/* Page-title H1s are the most divergent across the codebase β€” every page + picked a different text-Nxl. Unify all H1s that carry a Tailwind size + utility into one editorial range (32β†’44px responsive) so every route has + the same first-impression. We match on [class*="text-…"] to win against + .bkb .text-3xl etc. on specificity (0,2,1 > 0,2,0). H1s with inline + `style={{ fontSize }}` (admin / dashboard / page-access-gate) are left + alone β€” they already use the prototype's display sizes. */ +.bkb h1[class*="text-xl"], +.bkb h1[class*="text-2xl"], +.bkb h1[class*="text-3xl"], +.bkb h1[class*="text-4xl"], +.bkb h1[class*="text-5xl"], +.bkb h1[class*="text-6xl"] { + font-size: clamp(30px, 3.4vw, 40px) !important; + line-height: 1.1 !important; +} + +/* Section H2s ("What is BrainKB?", "BrainKB Data Releases", "Graph + Statistics") were similarly all over the map β€” text-2xl through + text-5xl. Cap them in the 24β†’32px band so every section reads with the + same hierarchy below the page H1. */ +.bkb h2[class*="text-xl"], +.bkb h2[class*="text-2xl"], +.bkb h2[class*="text-3xl"], +.bkb h2[class*="text-4xl"], +.bkb h2[class*="text-5xl"] { + font-size: clamp(24px, 2.6vw, 32px) !important; + line-height: 1.15 !important; +} /* Mono code / kbd / pre fall back to JetBrains Mono so technical content (SPARQL snippets, page_keys, prefix tags, IDs) is consistent. */ @@ -641,3 +679,40 @@ thead.sticky { color: var(--bkb-primaryInk) !important; } +/* Same panels often nest light "100/200" text shades (text-sky-100, + text-blue-100, text-emerald-100, etc.) as the body copy on top of the + originally-saturated gradient. Our default mapping for those shades is + bkb-textMuted (a dim ink that's correct on the cream body bg) β€” but on a + dark teal panel that ink disappears. Force them back to primaryInk so + the playground / HMBA / dashboard heroes stay readable. */ +.bkb [class*="bg-gradient-to-"][class*="from-sky-500"] [class*="text-sky-1"], +.bkb [class*="bg-gradient-to-"][class*="from-sky-500"] [class*="text-blue-1"], +.bkb [class*="bg-gradient-to-"][class*="from-sky-500"] [class*="text-emerald-1"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-500"] [class*="text-sky-1"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-500"] [class*="text-blue-1"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-600"] [class*="text-sky-1"], +.bkb [class*="bg-gradient-to-"][class*="from-blue-600"] [class*="text-blue-1"], +.bkb [class*="bg-gradient-to-"][class*="from-emerald-500"] [class*="text-emerald-1"], +.bkb [class*="bg-gradient-to-"][class*="from-emerald-500"] [class*="text-sky-1"], +.bkb [class*="bg-gradient-to-"][class*="from-emerald-500"] [class*="text-green-1"], +.bkb [class*="bg-gradient-to-"][class*="from-purple-500"] [class*="text-purple-1"], +.bkb [class*="bg-gradient-to-"][class*="from-purple-500"] [class*="text-pink-1"], +.bkb [class*="bg-gradient-to-"][class*="from-pink-500"] [class*="text-pink-1"], +.bkb [class*="bg-gradient-to-"][class*="from-teal-500"] [class*="text-teal-1"] { + color: var(--bkb-primaryInk) !important; +} + +/* Gradient-text headings (`text-transparent bg-clip-text bg-gradient-to-r + from-sky-600 via-blue-600 to-emerald-600` β€” used on /about, /knowledge-base + list pages, etc.) are a special case. The bg-gradient was meant to be + *clipped* to the glyphs, but our gradient-collapse rule above turns the + from-sky-600 into a solid dark-teal *background*, so the heading reads as a + big dark block with invisible (because text-transparent β†’ bkb-primary β†’ + same teal) text. Strip the bg, force the text back to bkb-primary. */ +.bkb .text-transparent.bg-clip-text[class*="bg-gradient-to-"], +.bkb .bg-clip-text.text-transparent[class*="bg-gradient-to-"] { + background-color: transparent !important; + background-image: none !important; + color: var(--bkb-primary) !important; +} + From 3b5e3390ab7415f54149732ea012fbc1e30d1c06 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 13:43:04 -0400 Subject: [PATCH 05/45] updated hero section based on https://github.com/sensein/brainkb-ui-prototype --- src/app/components/design-system/BkbHero.tsx | 156 ++++++++++ src/app/components/design-system/index.tsx | 286 +++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 src/app/components/design-system/BkbHero.tsx create mode 100644 src/app/components/design-system/index.tsx diff --git a/src/app/components/design-system/BkbHero.tsx b/src/app/components/design-system/BkbHero.tsx new file mode 100644 index 0000000..50dd651 --- /dev/null +++ b/src/app/components/design-system/BkbHero.tsx @@ -0,0 +1,156 @@ +"use client"; + +/** + * BkbHero β€” dark teal hero section ported from sensein/brainkb-ui-prototype. + * + * Visual: gradient teal/black background, dotted grid overlay, animated graph + * artwork floating on the right, "v2.0 Β· NIH Reach Tools" pill, large serif + * headline with the italic accent on "neuroscience", and a short description. + * + * The prototype's search bar and statistics grid are intentionally omitted + * here per the current design direction. + */ + +import React from "react"; +import { FONTS } from "./index"; + +function FloatingGraph() { + const nodes = [ + { x: 180, y: 60, r: 7, c: "oklch(0.78 0.13 170)", label: "Agent" }, + { x: 80, y: 130, r: 5, c: "oklch(0.80 0.13 285)", label: "" }, + { x: 280, y: 130, r: 6, c: "oklch(0.82 0.12 75)", label: "Pub" }, + { x: 120, y: 230, r: 5, c: "oklch(0.78 0.13 170)", label: "" }, + { x: 220, y: 240, r: 7, c: "oklch(0.80 0.13 285)", label: "Evidence" }, + { x: 180, y: 310, r: 5, c: "oklch(0.82 0.12 75)", label: "" }, + { x: 40, y: 260, r: 4, c: "oklch(0.78 0.13 170)", label: "" }, + { x: 320, y: 60, r: 4, c: "oklch(0.82 0.12 75)", label: "" }, + ]; + const edges: [number, number][] = [ + [0, 1], [0, 2], [1, 3], [2, 4], [3, 4], [4, 5], [3, 6], [1, 6], [2, 7], + ]; + return ( + + {edges.map(([a, b], i) => ( + + ))} + {nodes.map((n, i) => ( + + + + {n.label && ( + + {n.label} + + )} + + ))} + + ); +} + +export function BkbHero() { + return ( +
+ {/* Dot grid overlay */} + + + + + + + + + + {/* Floating graph artwork (top right) */} +
+ +
+ +
+
+ + BrainKB +
+ +

+ The open +
+ neuroscience +
+ knowledge graph. +

+ +

+ Building open, trustworthy knowledge graph infrastructure to integrate fragmented neuroscience knowledge and data to accelerate reproducible discovery. +

+
+
+ ); +} diff --git a/src/app/components/design-system/index.tsx b/src/app/components/design-system/index.tsx new file mode 100644 index 0000000..bbbc74e --- /dev/null +++ b/src/app/components/design-system/index.tsx @@ -0,0 +1,286 @@ +"use client"; + +/** + * BrainKB design system β€” tokens, fonts, icons, and the wrapper. + * + */ + +import React from "react"; + +export const TOKENS = { + light: { + bg: "oklch(0.985 0.004 85)", + surface: "oklch(1 0 0)", + surfaceAlt: "oklch(0.965 0.006 85)", + border: "oklch(0.88 0.008 85)", + borderStrong: "oklch(0.78 0.012 85)", + text: "oklch(0.20 0.012 220)", + textMuted: "oklch(0.48 0.010 220)", + textSubtle: "oklch(0.62 0.008 220)", + primary: "oklch(0.32 0.045 195)", + primaryInk: "oklch(0.98 0.004 85)", + accent: "oklch(0.62 0.11 170)", + agent: "oklch(0.58 0.13 165)", + evidence: "oklch(0.55 0.14 285)", + publication: "oklch(0.62 0.12 65)", + danger: "oklch(0.55 0.18 28)", + grid: "oklch(0.20 0.012 220 / 0.05)", + }, + dark: { + bg: "oklch(0.19 0.022 200)", + surface: "oklch(0.23 0.025 200)", + surfaceAlt: "oklch(0.26 0.028 200)", + border: "oklch(0.34 0.025 200)", + borderStrong: "oklch(0.42 0.028 200)", + text: "oklch(0.96 0.006 85)", + textMuted: "oklch(0.74 0.012 200)", + textSubtle: "oklch(0.58 0.015 200)", + primary: "oklch(0.68 0.13 170)", + primaryInk: "oklch(0.18 0.022 200)", + accent: "oklch(0.74 0.14 170)", + agent: "oklch(0.74 0.14 165)", + evidence: "oklch(0.72 0.13 285)", + publication: "oklch(0.78 0.12 75)", + danger: "oklch(0.68 0.18 28)", + grid: "oklch(1 0 0 / 0.04)", + }, + teal: { + bg: "oklch(0.97 0.012 180)", + surface: "oklch(1 0 0)", + surfaceAlt: "oklch(0.94 0.018 180)", + border: "oklch(0.86 0.022 180)", + borderStrong: "oklch(0.74 0.030 180)", + text: "oklch(0.22 0.035 200)", + textMuted: "oklch(0.44 0.025 200)", + textSubtle: "oklch(0.58 0.020 200)", + primary: "oklch(0.38 0.065 190)", + primaryInk: "oklch(0.98 0.008 85)", + accent: "oklch(0.55 0.12 170)", + agent: "oklch(0.52 0.13 165)", + evidence: "oklch(0.50 0.14 285)", + publication: "oklch(0.58 0.12 65)", + danger: "oklch(0.55 0.18 28)", + grid: "oklch(0.22 0.035 200 / 0.06)", + }, +} as const; + +export type ThemeName = keyof typeof TOKENS; + +export const FONTS = { + display: '"Instrument Serif", "Times New Roman", Georgia, serif', + body: 'Inter, -apple-system, BlinkMacSystemFont, system-ui, sans-serif', + mono: '"JetBrains Mono", "SF Mono", ui-monospace, Consolas, monospace', +}; + +export function Theme({ + theme = "light", + children, + style, + className = "", +}: { + theme?: ThemeName; + children: React.ReactNode; + style?: React.CSSProperties; + className?: string; +}) { + const t = TOKENS[theme] || TOKENS.light; + const vars: Record = {}; + Object.entries(t).forEach(([k, v]) => { + vars[`--bkb-${k}`] = v; + }); + return ( +
+ {children} +
+ ); +} + +type IconProps = { + name: string; + size?: number; + style?: React.CSSProperties; +}; + +export const Icon: React.FC = ({ name, size = 16, style }) => { + const paths: Record = { + search: ( + <> + + + + ), + graph: ( + <> + + + + + + ), + home: , + dash: ( + <> + + + + + + ), + agent: ( + <> + + + + + + ), + evidence: ( + <> + + + + ), + pub: , + project: , + person: ( + <> + + + + ), + key: ( + <> + + + + ), + history: ( + <> + + + + ), + upload: , + settings: ( + <> + + + + ), + arrow: , + down: , + up: , + plus: , + check: , + x: , + menu: , + filter: , + sparkle: , + flow: ( + <> + + + + + + ), + doc: ( + <> + + + + ), + sync: ( + <> + + + + + ), + database: ( + <> + + + + ), + logout: , + star: , + bookmark: , + eye: ( + <> + + + + ), + play: , + pause: ( + <> + + + + ), + chevron: , + copy: ( + <> + + + + ), + bell: ( + <> + + + + ), + info: ( + <> + + + + ), + dots: ( + <> + + + + + ), + sort: , + lock: ( + <> + + + + ), + shield: , + }; + return ( + + {paths[name] || null} + + ); +}; + +export const Logo: React.FC<{ size?: number; style?: React.CSSProperties }> = ({ size = 24, style }) => ( + + + + + + + +); + From 5598723dca8e3d884af07279fb703f95295a4203 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 14:11:17 -0400 Subject: [PATCH 06/45] updated hero section graph display --- src/app/components/design-system/BkbHero.tsx | 54 +++++++++++--------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/app/components/design-system/BkbHero.tsx b/src/app/components/design-system/BkbHero.tsx index 50dd651..0a64928 100644 --- a/src/app/components/design-system/BkbHero.tsx +++ b/src/app/components/design-system/BkbHero.tsx @@ -17,13 +17,13 @@ import { FONTS } from "./index"; function FloatingGraph() { const nodes = [ { x: 180, y: 60, r: 7, c: "oklch(0.78 0.13 170)", label: "Agent" }, - { x: 80, y: 130, r: 5, c: "oklch(0.80 0.13 285)", label: "" }, - { x: 280, y: 130, r: 6, c: "oklch(0.82 0.12 75)", label: "Pub" }, - { x: 120, y: 230, r: 5, c: "oklch(0.78 0.13 170)", label: "" }, + { x: 80, y: 130, r: 5, c: "oklch(0.80 0.13 285)", label: "Atlases" }, + { x: 280, y: 130, r: 6, c: "oklch(0.82 0.12 75)", label: "Insights" }, + { x: 120, y: 230, r: 5, c: "oklch(0.78 0.13 170)", label: "Datasets" }, { x: 220, y: 240, r: 7, c: "oklch(0.80 0.13 285)", label: "Evidence" }, - { x: 180, y: 310, r: 5, c: "oklch(0.82 0.12 75)", label: "" }, - { x: 40, y: 260, r: 4, c: "oklch(0.78 0.13 170)", label: "" }, - { x: 320, y: 60, r: 4, c: "oklch(0.82 0.12 75)", label: "" }, + { x: 180, y: 310, r: 5, c: "oklch(0.82 0.12 75)", label: "Databases" }, + { x: 40, y: 260, r: 4, c: "oklch(0.78 0.13 170)", label: "Literature" }, + { x: 320, y: 60, r: 4, c: "oklch(0.82 0.12 75)", label: "Discovery" }, ]; const edges: [number, number][] = [ [0, 1], [0, 2], [1, 3], [2, 4], [3, 4], [4, 5], [3, 6], [1, 6], [2, 7], @@ -41,23 +41,31 @@ function FloatingGraph() { strokeWidth="1" /> ))} - {nodes.map((n, i) => ( - - - - {n.label && ( - - {n.label} - - )} - - ))} + {nodes.map((n, i) => { + // Flip the label to the left of the circle when the node is near the + // right edge of the 360-wide viewBox β€” otherwise long labels like + // "Discovery" get clipped by the SVG bounds (text-anchor=start + + // x=320 + ~54px of text spills past x=360). + const flipLeft = n.x > 240; + return ( + + + + {n.label && ( + + {n.label} + + )} + + ); + })} ); } From 6366222d736cb6c75cafa6ec9b2018412efcb8cf Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 14:21:49 -0400 Subject: [PATCH 07/45] GLOBUS related environment variable added --- .env.local | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.env.local b/.env.local index da2ecaf..eaf048c 100644 --- a/.env.local +++ b/.env.local @@ -9,6 +9,11 @@ GITHUB_CLIENT_SECRET=XXXXXX ORCID_CLIENT_ID=APP-XXXXXX ORCID_CLIENT_SECRET=XXXX +# Globus OAuth β€” set on the usermanagement_service backend to enable Globus +# sign-in. Register an app at https://app.globus.org/settings/developers +GLOBUS_CLIENT_ID= +GLOBUS_CLIENT_SECRET= + NEXTAUTH_SECRET=ANY_RANDOM_STRING_SECRET NEXTAUTH_URL=http://localhost:3000 #FOR LOCAL DEPLOYMENT @@ -52,6 +57,15 @@ NEXT_PUBLIC_GET_ENDPOINT_USER_PROFILE_USER_MANAGEMENT_SERVICE=http://localhost:8 NEXT_PUBLIC_UPDATE_ENDPOINT_USER_PROFILE_USER_MANAGEMENT_SERVICE=http://localhost:8008/api/users/profile NEXT_PUBLIC_GET_ENDPOINT_USER_ACTIVITY_USER_MANAGEMENT_SERVICE=http://localhost:8008/api/users/activities +######################################################################################################## +######### User Management Backend (RBAC + admin dashboard) ############################################ +# Base URL for usermanagement_service. Powers /api/auth/providers (sign-in +# dropdown), /api/users/me, /api/admin/*, and /api/access/page/ +# RBAC checks. Falls back to http://localhost:8004 in dev if unset. +######################################################################################################## + +NEXT_PUBLIC_USER_MANAGEMENT_API_BASE=http://127.0.0.1:8004 + ######################################################################################################## ######### Chat Service ################################################################################# From d1ff3ac3b605978aadf2622b8db782586eee55ef Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 17:30:44 -0400 Subject: [PATCH 08/45] env local updated to reflect the change -- backend handles the authentication --- .env.local | 30 ++++++++++----------- src/app/user/profile/page.tsx | 49 ++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/.env.local b/.env.local index eaf048c..f1fd6b6 100644 --- a/.env.local +++ b/.env.local @@ -1,19 +1,10 @@ ######################################################################################################## -######### OAuth Credentials (Optional) ############################################################### -# Required only if you want to enable login via GitHub or access the admin dashboard. -# ORCID login is not supported in local development. +######### NextAuth (this UI) ########################################################################## +# OAuth provider credentials (GitHub / ORCID / Globus client ID + secret) are +# NOT read by this UI. They live on the usermanagement_service backend's .env +# (see BrainKB/.env). The backend exposes /api/auth/providers, which this UI +# calls to render the sign-in buttons, and handles the full OAuth flow itself. ######################################################################################################## -GITHUB_CLIENT_ID=XXXXXX -GITHUB_CLIENT_SECRET=XXXXXX - -ORCID_CLIENT_ID=APP-XXXXXX -ORCID_CLIENT_SECRET=XXXX - -# Globus OAuth β€” set on the usermanagement_service backend to enable Globus -# sign-in. Register an app at https://app.globus.org/settings/developers -GLOBUS_CLIENT_ID= -GLOBUS_CLIENT_SECRET= - NEXTAUTH_SECRET=ANY_RANDOM_STRING_SECRET NEXTAUTH_URL=http://localhost:3000 #FOR LOCAL DEPLOYMENT @@ -43,8 +34,8 @@ NEXT_PUBLIC_API_ADMIN_GET_STRUCTURED_RESOURCE_ENDPOINT=http://localhost:8007/api # Common JWT credentials for accessing backend services. ######################################################################################################## -NEXT_PUBLIC_JWT_USER=XXXXXX -NEXT_PUBLIC_JWT_PASSWORD=XXXXXX +NEXT_PUBLIC_JWT_USER=test@example.com +NEXT_PUBLIC_JWT_PASSWORD=testpassword1 ######################################################################################################## ######### User Profile Management ##################################################################### @@ -61,7 +52,12 @@ NEXT_PUBLIC_GET_ENDPOINT_USER_ACTIVITY_USER_MANAGEMENT_SERVICE=http://localhost: ######### User Management Backend (RBAC + admin dashboard) ############################################ # Base URL for usermanagement_service. Powers /api/auth/providers (sign-in # dropdown), /api/users/me, /api/admin/*, and /api/access/page/ -# RBAC checks. Falls back to http://localhost:8004 in dev if unset. +# RBAC checks. +# +# In the backend's compose / start scripts, USERMANAGEMENT_SERVICE_PORT +# defaults to 8004 (other services on the same stack: API token :8000, +# query :8010, ML :8007). When running non-docker make sure the service +# binds to the same port, e.g. `uvicorn core.main:app --reload --port 8004`. ######################################################################################################## NEXT_PUBLIC_USER_MANAGEMENT_API_BASE=http://127.0.0.1:8004 diff --git a/src/app/user/profile/page.tsx b/src/app/user/profile/page.tsx index adbc8ff..1bf43d3 100644 --- a/src/app/user/profile/page.tsx +++ b/src/app/user/profile/page.tsx @@ -927,18 +927,36 @@ export default function Profile() { - {/* Modal */} - {/* Modal */} - {isEditing && (() => { - - return true; - })() && ( + {/* Edit Profile dialog */} + {isEditing && (
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 p-4 sm:p-6 overflow-auto" + onClick={() => setIsEditing(false)} + role="dialog" + aria-modal="true" + aria-labelledby="edit-profile-title" + >
-

Edit Profile

+ className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-5xl max-h-[90vh] flex flex-col overflow-hidden border border-gray-200 dark:border-gray-700" + onClick={(e) => e.stopPropagation()} + style={{ zIndex: 10000 }} + > +
+

+ Edit Profile +

+ +
+
{/* Basic Information */}
@@ -1634,17 +1652,19 @@ export default function Profile() {
- {/* Action Buttons */} -
+
+
@@ -1652,7 +1672,6 @@ export default function Profile() {
)} - {/* End model*/}
); } From 5fe01264a4f25564c216c267825e7a865602e270 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 21:49:56 -0400 Subject: [PATCH 09/45] updated hero KG --- src/app/components/design-system/BkbHero.tsx | 281 ++++++++++++------- 1 file changed, 175 insertions(+), 106 deletions(-) diff --git a/src/app/components/design-system/BkbHero.tsx b/src/app/components/design-system/BkbHero.tsx index 0a64928..1487d21 100644 --- a/src/app/components/design-system/BkbHero.tsx +++ b/src/app/components/design-system/BkbHero.tsx @@ -15,57 +15,115 @@ import React from "react"; import { FONTS } from "./index"; function FloatingGraph() { - const nodes = [ - { x: 180, y: 60, r: 7, c: "oklch(0.78 0.13 170)", label: "Agent" }, - { x: 80, y: 130, r: 5, c: "oklch(0.80 0.13 285)", label: "Atlases" }, - { x: 280, y: 130, r: 6, c: "oklch(0.82 0.12 75)", label: "Insights" }, - { x: 120, y: 230, r: 5, c: "oklch(0.78 0.13 170)", label: "Datasets" }, - { x: 220, y: 240, r: 7, c: "oklch(0.80 0.13 285)", label: "Evidence" }, - { x: 180, y: 310, r: 5, c: "oklch(0.82 0.12 75)", label: "Databases" }, - { x: 40, y: 260, r: 4, c: "oklch(0.78 0.13 170)", label: "Literature" }, - { x: 320, y: 60, r: 4, c: "oklch(0.82 0.12 75)", label: "Discovery" }, - ]; - const edges: [number, number][] = [ - [0, 1], [0, 2], [1, 3], [2, 4], [3, 4], [4, 5], [3, 6], [1, 6], [2, 7], - ]; + // Three-column flow: + // Left column (purple sources) → Center (Knowledge Graph hub) → Right column (teal outputs, Discovery accent in coral). + // Inbound edges (purple, with arrowheads) animate dashes left→right; outbound + // edges (teal) animate center→right. Each labeled box drifts vertically on + // a 4s cycle with staggered delays so the diagram feels alive without being + // distracting. The central hub has an outer pulse ring + dashed orbit ring + // with a few satellite dots, evoking a knowledge-graph nucleus. return ( - - {edges.map(([a, b], i) => ( - - ))} - {nodes.map((n, i) => { - // Flip the label to the left of the circle when the node is near the - // right edge of the 360-wide viewBox — otherwise long labels like - // "Discovery" get clipped by the SVG bounds (text-anchor=start + - // x=320 + ~54px of text spills past x=360). - const flipLeft = n.x > 240; - return ( - - - - {n.label && ( - - {n.label} - - )} - - ); - })} + + + + + + + + + + + + {/* Outer pulse ring + dashed orbit ring around the hub */} + + + + {/* Inbound flow lines (sources → hub) */} + + + + + + {/* Outbound flow lines (hub → outputs) */} + + + + + {/* Source nodes (left column, purple) */} + + + Literature + + + + Datasets + + + + Experiments + + + + Databases + + + {/* Central hub: Knowledge Graph */} + + {/* Satellite dots on orbit ring */} + + + + + Knowledge + Graph + + {/* Output nodes (right column) — Discovery in coral accent */} + + + Evidence + + + + Insights + + + + Discovery + + + {/* Subtle vertical connectors between the three output cards */} + + ); } @@ -92,72 +150,83 @@ export function BkbHero() { - {/* Floating graph artwork (top right) */} + {/* Two-column layout on wide viewports (text left, graph right); + stacks vertically below ~960px via flex-wrap. flex-basis values + give the graph priority for width when both columns can fit, but + let the text column reflow naturally when wrapped. */}
- -
+
+
+ + BrainKB +
-
-
- - BrainKB -
+

+ The open +
+ neuroscience +
+ knowledge graph. +

-

- The open -
- neuroscience -
- knowledge graph. -

+

+ Building open, trustworthy knowledge graph infrastructure to integrate fragmented neuroscience knowledge and data to accelerate reproducible discovery. +

+
-

- Building open, trustworthy knowledge graph infrastructure to integrate fragmented neuroscience knowledge and data to accelerate reproducible discovery. -

+ +
); From 9481810607679923c17027e729181f6f0c0fcab5 Mon Sep 17 00:00:00 2001 From: Tek Raj Chhetri Date: Wed, 29 Apr 2026 21:50:44 -0400 Subject: [PATCH 10/45] admin dashboard added for managing access to pages/tools --- src/app/admin/AdminShell.tsx | 120 ++++++++ src/app/admin/dashboard/page.tsx | 125 ++++++++ src/app/admin/layout.tsx | 18 ++ src/app/admin/page-access/page.tsx | 344 +++++++++++++++++++++ src/app/admin/page.tsx | 5 + src/app/admin/roles/page.tsx | 465 +++++++++++++++++++++++++++++ src/app/admin/users/page.tsx | 248 +++++++++++++++ 7 files changed, 1325 insertions(+) create mode 100644 src/app/admin/AdminShell.tsx create mode 100644 src/app/admin/dashboard/page.tsx create mode 100644 src/app/admin/layout.tsx create mode 100644 src/app/admin/page-access/page.tsx create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/admin/roles/page.tsx create mode 100644 src/app/admin/users/page.tsx diff --git a/src/app/admin/AdminShell.tsx b/src/app/admin/AdminShell.tsx new file mode 100644 index 0000000..3362da2 --- /dev/null +++ b/src/app/admin/AdminShell.tsx @@ -0,0 +1,120 @@ +"use client"; + +/** + * AdminShell β€” sidebar + content layout for /admin/*. + * + * Gates the entire surface behind the Admin role. While useCurrentUser is + * still loading, shows a skeleton (rather than flashing a 403 to a user + * who is in fact admin). Non-admins get a denied page. + */ + +import React from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { FONTS, Icon, Logo } from "@/src/app/components/design-system"; +import { useCurrentUser } from "@/src/hooks/useCurrentUser"; + +const ADMIN_NAV: { href: string; label: string; icon: string }[] = [ + { href: "/admin/dashboard", label: "Statistics", icon: "dash" }, + { href: "/admin/users", label: "Users", icon: "person" }, + { href: "/admin/roles", label: "Roles & permissions", icon: "shield" }, + { href: "/admin/page-access", label: "Page access", icon: "lock" }, +]; + +function AdminSidebar() { + const pathname = usePathname() || "/admin/dashboard"; + const isActive = (href: string) => pathname === href || pathname.startsWith(href + "/"); + return ( + + ); +} + +function Denied({ reason }: { reason: string }) { + return ( +
+
+ Forbidden +
+

+ Admin access required +

+

{reason}

+ + Back to dashboard + +
+ ); +} + +export function AdminShell({ children }: { children: React.ReactNode }) { + const { user, loading, isAdmin } = useCurrentUser(); + + if (loading) { + return ( +
Checking access…
+ ); + } + + if (!user) { + return ; + } + + if (!isAdmin) { + return ( + + ); + } + + return ( +
+ +
{children}
+
+ ); +} diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..3490f77 --- /dev/null +++ b/src/app/admin/dashboard/page.tsx @@ -0,0 +1,125 @@ +"use client"; + +/** + * /admin β€” Statistics landing. + * + * Pulls a few cheap counts from the backend (users, roles, permissions, + * page-access entries) and surfaces the current admin's identity. Each + * card links into the relevant management surface. + */ + +import React from "react"; +import Link from "next/link"; +import { FONTS, Icon } from "@/src/app/components/design-system"; +import { adminApi } from "@/src/services/api/userManagement"; +import { useCurrentUser } from "@/src/hooks/useCurrentUser"; + +interface Counts { + users: number | null; + roles: number | null; + permissions: number | null; + pages: number | null; +} + +export default function AdminStatsPage() { + const { user } = useCurrentUser(); + const [counts, setCounts] = React.useState({ users: null, roles: null, permissions: null, pages: null }); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; + (async () => { + try { + const [usersC, roles, perms, pages] = await Promise.all([ + adminApi.countUsers().catch(() => ({ count: 0 })), + adminApi.listRoles().catch(() => []), + adminApi.listPermissions().catch(() => []), + adminApi.listPageAccess().catch(() => []), + ]); + if (cancelled) return; + setCounts({ users: usersC.count, roles: roles.length, permissions: perms.length, pages: pages.length }); + } catch (e: any) { + if (!cancelled) setError(e?.message ?? "request failed"); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + return ( +
+

+ Statistics +

+
+ Manage users, roles, permissions, and page-level access for BrainKB. +
+ +
+ {[ + { n: counts.users, l: "Users", icon: "person", c: "var(--bkb-agent)", href: "/admin/users" }, + { n: counts.roles, l: "Roles", icon: "shield", c: "var(--bkb-primary)", href: "/admin/roles" }, + { n: counts.permissions, l: "Permissions", icon: "key", c: "var(--bkb-evidence)", href: "/admin/roles" }, + { n: counts.pages, l: "Pages registered", icon: "lock", c: "var(--bkb-publication)", href: "/admin/page-access" }, + ].map((s) => ( + +
+ +
+
+
+ {s.l} +
+
+ {s.n ?? "β€”"} +
+
+ + ))} +
+ + {error && ( +
+
Failed to reach the user management API.
+
{error}
+
+ )} + +
+
Signed in as
+
+ {[ + { l: "Email", v: user?.email ?? "β€”" }, + { l: "Profile ID", v: user?.profile_id != null ? String(user.profile_id) : "β€”" }, + { l: "Auth source", v: user?.auth_source ?? "β€”" }, + { l: "Roles", v: user?.roles?.join(", ") || "β€”" }, + { l: "Scopes", v: user?.scopes?.join(", ") || "β€”" }, + { l: "User ID", v: user?.user_id != null ? String(user.user_id) : "β€”" }, + ].map((h) => ( +
+
{h.l}
+
{h.v}
+
+ ))} +
+
+
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..8d08145 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { Theme } from "@/src/app/components/design-system"; +import "../user/dashboard/fonts.css"; +import { AdminShell } from "./AdminShell"; + +export const metadata: Metadata = { + title: "Admin", +}; + +// The legacy site navbar is mounted by the root layout; this layout only +// supplies the bkb Theme tokens that the admin pages use. +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/admin/page-access/page.tsx b/src/app/admin/page-access/page.tsx new file mode 100644 index 0000000..7ccdae6 --- /dev/null +++ b/src/app/admin/page-access/page.tsx @@ -0,0 +1,344 @@ +"use client"; + +/** + * /admin/page-access β€” manage which roles + users can see each UI page. + * + * Wired to: + * GET /api/admin/page-access + * PUT /api/admin/page-access/{page_key} + * DELETE /api/admin/page-access/{page_key} + * + * The UI then calls /api/access/page/{page_key} (via usePageAccess) to gate + * routes; this admin surface is what feeds those checks. + */ + +import React from "react"; +import { FONTS, Icon } from "@/src/app/components/design-system"; +import { + adminApi, + type PageAccess, + type AvailableRole, +} from "@/src/services/api/userManagement"; +import { TOOL_REGISTRY } from "@/src/config/toolRegistry"; + +export default function AdminPageAccessPage() { + const [pages, setPages] = React.useState([]); + const [roles, setRoles] = React.useState([]); + const [selectedKey, setSelectedKey] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [saving, setSaving] = React.useState(false); + + // Editor state + const [editKey, setEditKey] = React.useState(""); + const [editDesc, setEditDesc] = React.useState(""); + const [editPublic, setEditPublic] = React.useState(false); + const [editRoles, setEditRoles] = React.useState>(new Set()); + const [editEmails, setEditEmails] = React.useState(""); + + const reload = React.useCallback(async () => { + setLoading(true); + setError(null); + try { + const [ps, rs] = await Promise.all([adminApi.listPageAccess(), adminApi.listRoles()]); + setPages(ps); + setRoles(rs); + } catch (e: any) { + setError(e?.message ?? "request failed"); + } finally { + setLoading(false); + } + }, []); + + React.useEffect(() => { + void reload(); + }, [reload]); + + // Hydrate editor when selection changes. + React.useEffect(() => { + if (!selectedKey) { + setEditKey(""); + setEditDesc(""); + setEditPublic(false); + setEditRoles(new Set()); + setEditEmails(""); + return; + } + const p = pages.find((x) => x.page_key === selectedKey); + if (!p) return; + setEditKey(p.page_key); + setEditDesc(p.description ?? ""); + setEditPublic(p.is_public); + setEditRoles(new Set(p.allowed_roles)); + setEditEmails(p.allowed_user_emails.join("\n")); + }, [selectedKey, pages]); + + function startNew() { + setSelectedKey(null); + setEditKey(""); + setEditDesc(""); + setEditPublic(false); + setEditRoles(new Set()); + setEditEmails(""); + } + + async function save() { + if (!editKey.trim()) return; + setSaving(true); + try { + const emails = editEmails + .split(/[\s,;]+/) + .map((s) => s.trim()) + .filter(Boolean); + await adminApi.upsertPageAccess(editKey.trim(), { + page_key: editKey.trim(), + description: editDesc.trim() || null, + is_public: editPublic, + allowed_roles: Array.from(editRoles), + allowed_user_emails: emails, + }); + setSelectedKey(editKey.trim()); + await reload(); + } catch (e: any) { + alert(`Could not save: ${e?.message ?? "request failed"}`); + } finally { + setSaving(false); + } + } + + async function deletePage(key: string) { + if (!confirm(`Delete page-access entry "${key}"? Routes that reference this key will fall back to "not_found".`)) return; + try { + await adminApi.deletePageAccess(key); + if (selectedKey === key) setSelectedKey(null); + await reload(); + } catch (e: any) { + alert(`Could not delete: ${e?.message ?? "request failed"}`); + } + } + + function toggleRole(name: string) { + setEditRoles((s) => { + const next = new Set(s); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + } + + // Page-keys the UI knows about that the admin hasn't yet registered. These + // are shown as one-click stub-create buttons so getting started doesn't + // require typing keys by hand. + const knownKeys = new Set(pages.map((p) => p.page_key)); + const seedables = TOOL_REGISTRY.filter((t) => !knownKeys.has(t.pageKey)); + + function preFillFromTool(t: typeof TOOL_REGISTRY[number]) { + setSelectedKey(null); + setEditKey(t.pageKey); + setEditDesc(t.title); + setEditPublic(false); + setEditRoles(new Set()); + setEditEmails(""); + } + + return ( +
+
+
+

+ Page access +

+
+ Map UI page keys (e.g. admin.users) to allowed roles and user overrides. + Tools without an entry are denied by default. +
+
+ +
+ + {!loading && seedables.length > 0 && ( +
+
+ {seedables.length} workflow tool{seedables.length === 1 ? "" : "s"} aren't registered yet β€” they're currently denied for everyone. +
+
+ Click one to pre-fill the editor, then assign roles or user emails and save. +
+
+ {seedables.map((t) => ( + + ))} +
+
+ )} + + {error && ( +
+
{error}
+
+ )} + +
+ {/* List */} +
+
+
Registered pages
+
+ {loading ? "Loading…" : `${pages.length} entries`} +
+
+
+ {pages.map((p) => { + const active = selectedKey === p.page_key; + return ( +
setSelectedKey(p.page_key)} + className="bkb-hover-row" + style={{ + padding: "10px 16px", + borderBottom: "1px solid var(--bkb-border)", + cursor: "pointer", + background: active ? "var(--bkb-surfaceAlt)" : "transparent", + display: "grid", + gridTemplateColumns: "1fr auto", + gap: 8, + alignItems: "center", + }} + > +
+
+ {p.page_key} +
+
+ {p.is_public ? "public" : `${p.allowed_roles.length} role(s) Β· ${p.allowed_user_emails.length} user(s)`} +
+
+ +
+ ); + })} + {!loading && pages.length === 0 && ( +
+ No pages registered yet. +
+ )} +
+
+ + {/* Editor */} +
+
+ {selectedKey ? `Editing ${selectedKey}` : "Create a new page entry"} +
+ +
+
+ + setEditKey(e.target.value)} + disabled={!!selectedKey} + placeholder="e.g. admin.users" + /> +
+
+ + setEditDesc(e.target.value)} + placeholder="Optional" + /> +
+
+ + + +
+
Allowed roles
+
+ {roles.map((r) => { + const on = editRoles.has(r.name); + return ( + + ); + })} +
+
+ +
+
+ User-email overrides (one per line, comma, or space) +
+