Imported from prince-io/Librarian-AI (
frontend/src/components/AGENTS.md). Install upstream withnpx skills add prince-io/Librarian-AI --skill components. Copyright stays with the author.
frontend/src/components/
Purpose
Reusable UI components for landing page and app screens. Built with daisyUI 5 class names and Tailwind CSS 4 utilities, following the themeable design system (default clay claymorphism).
Ownership
Layout.jsx— App shell: daisyUI collapsible drawer (lg:drawer-open+ daisyUIis-drawer-open/is-drawer-closevariants). Sidebar container isw-14 is-drawer-open:w-64 lg:is-drawer-open:w-80— desktop expands to w-80 (mobile open stays w-64) and collapses to a w-14 icon rail when the toggle is unchecked (starts expanded; a chevron button —IconChevronsLeft, rotated 180° when collapsed — at the sidebar's top-right toggles it) and mobile is an overlay drawer (hidden when closed). The container isflex h-dvh flex-col overflow-hiddenso the footer stays pinned and only the conversation list scrolls. The toggle checkbox is controlled (checkedfromdrawerOpenstate; initial value =matchMedia("(min-width: 1024px)")so desktop starts expanded, mobile closed). A fixed top-left hamburger (IconMenu,lg:hidden) opens the mobile drawer and is hidden while it's open. The sidebar's header row (px-3 py-3,justify-betweenexpanded /justify-centerin the rail) holds the "Librarian" title alongside the action button — the desktop chevron (IconChevronsLeft,lg:inline-flex) and the mobile close (IconClosew-6,lg:hidden) — so the close affordance sits beside the heading in both modes.closeDrawer()only closes on mobile (desktop keeps the rail state). The ready-view header keepspl-14on mobile (clears the fixed hamburger) andlg:pl-6on desktopSidebar.jsx— iOS-style conversation list (rounded rows, active =bg-primary/10), single+ Ingest New Repoprimary pill button, footer nav with Settings and Sign Out side by side (grid-cols-2) as solid buttons matching the Ingest button size — Settings =btn-neutral, Sign Out =btn-error(destructive;btn-block). Accepts acollapsedprop (from Layout'sdrawerOpen): whencollapsedit renders an icon rail — default-size circleIconAdd(new chat), conversation list hidden, and default-size circleIconSettings(btn-neutral)/IconExit(btn-error) with nativetitletooltips (collapsed wrappers cancel the footerpx-3so the circles fit the w-14 rail and match the expanded button height); when expanded it renders the full list + labeled buttons. The "Librarian" heading lives inLayout's header row, not here.flex-1 min-h-0root +flex-1 overflow-y-autolist mean the footer stays pinned and the list scrollsSymbolGraphView.jsx— Repo symbol graph (2D only — the 3D view was removed): rendersSymbolGraph2DViewplus the shared detail panel (glass aside, collapsible — a chevron buttonIconChevronsLeftrotated 180° (pointing right, toward the close direction) sits to the left of the kind/file name in the panel header and closes it; the panel opens only via node click — no reopen button). Responsive panel: on mobile (<lg) the aside is a full-screen overlay within the main area (absolute inset-0 z-40on the graph'srelativeflex container — stays below the header, draws over the graph, legend, and filter) and on desktop it's a staticw-96side panel beside the graph. Clicking a file node fetches all its chunks (getFileChunks(repoHash, file)→/repositories/{hash}/chunks?file_path=) and reassembles the complete file source viaassembleFileCode(line-coverage merge that drops nested AST symbol overlap), rendered in a "Complete code" section below the "Entities in this file" chips (entities list shows only when the file actually contains entities); clicking an entity node shows its code snippet directly. The heading now carries kind + label + an Explain button (path moved to the body as a truncatedfile:start-endbreadcrumb) — Explain POSTs the node's code (full file source / entity snippet) + metadata to/repositories/{hash}/explain(SSE viaconsumeSSE, Clerk token) and streams a markdown explanation into the panel with loading/error states (non-SSE failures incl. the usage-cap 429 parse viareadErrorfromsrc/api/sse.js, rendered as aQuotaNoticein the panel);repoHashprop is the commit hash of the loaded graph — the graph JSON'srepofield is a display name, not a route identifier; background click clears the panel. All node/reset/loading/error/empty states handled hereSymbolGraph2DView.jsx— 2D graph via@xyflow/react(React Flow) +elkjs, mirroring the Understand-Anything dashboard design: kind-coded card nodes (colors fromuseGraphTheme()→--ds-graph-*tokens: file blue, class purple, interface sky, impl gold, method pink, function green, entity slate) with a colored left bar + mono label + kind tag; nested file-group layout (P3): each file is a React Flow parent node (FileGroupcontainer: filename header + border, target/source handles on top/bottom soimportsfile→file andused_in/cross-file edges attach at the box edges instead of being buried under the container) with its entities rendered as children inside it (parentId+extent: "parent"), so each file is a compact ~column instead of one giant shared rank; layout is two-pass ELK: per-file layered runs (ELK_FILE_OPTIONS, direction DOWN,aspectRatio0.4 → tall narrow columns; intra-file structural edges =uses) then a root layered run (ELK_OPTIONS, direction DOWN, aspectRatio 1.0) arranging the file groups (importsfile→file edges); entitypositionis relative to its file (FILE_PADDING+FILE_HEADER_HEIGHToffset); positions are cached in localStorage (ua-layout:v6:{repo}:{fingerprint}) so repeat loads skipelk.layout()entirely (capped atLAYOUT_CACHE_MAX_NODES); bump thev6segment whenever layout options/node dimensions change; only entity→entity (uses/used_in) + file→fileimportsedges are drawn (there is nodefines/containsedge type — file membership and containment are the nested file-group itself); step edges colored by edge type (--ds-graph-edge-*tokens viauseGraphTheme: imports yellow, used_in teal, uses green) with uniform width/opacity andMarkerType.ArrowClosedarrowheads (same color); selection focus dims unrelated nodes toopacity-20and unrelated edges toEDGE_OPACITY_DIM+ a neutral grey stroke while highlighting the selected node's incident edges (thickerEDGE_HIGHLIGHT_WIDTH, full opacity) and keeping direct neighbors atopacity-80— neighbors resolve via a precomputedSet(selectedNeighborsmemo) so selection is O(1) per node, and a selected entity's file container stays active (not dimmed); selection fade — selected node gets aprimaryring +.glow, neighbors a thin ring; legend overlay (top-left,w-70 md:w-80, collapsible — a "Legend" button withIconChevronsLeftrotated 90° (open) / -90° (closed) toggles the Edges + Nodes lists; on mobile the legend and filter menu are mutually exclusive — opening one closes the other, since they overlap on narrow screens) with Edges (imports/used_in/uses colors) + Nodes sections, React FlowcolorModedriven by--ds-graph-color-mode, MiniMap + zoom controls (Controls fit button animated viaonFitView→rf.fitView({ duration: 400 }), same as the load-timefitView), fitView on load, canvas surface =.graph-surface; performance:onlyRenderVisibleElements+ fixed nodewidth/heightso pan/zoom only renders viewport-visible nodes/edges,nodesConnectable={false}+edgesFocusable={false}. Node click →onSelect(file group or entity → shared detail panel), pane click clears. NOTE: custom nodedatacarries the raw backend node so the shared panel works unchanged; graph filter menu — top-right funnel (details.dropdown, controlledopenstate + closes on outside click) opens a panel (w-[17.5rem]phone,md:w-80≥768px) with a multi-select Node type checkbox group (grid-cols-2 gap-x-3 gap-y-1.5) (kinds present in the loaded graph, color-dotted) + a single-select Directory control (every unique directory path prefix from nodefiles — e.g.srcandsrc/tests, root-level files grouped as(root); custom button + inline list with no nesting indentation, selected value and each optiontruncateso long paths can't overflow the bar; chevronIconChevronsLeftrotated up/down; selecting a parent dir includes all nested children because matching is prefix-based); groups combine AND, kinds OR. Matches = entities passing both groups plus their parent file containers (a file shows only when ≥1 of its entities matches); the layout effect re-runs the two-pass ELK on afilteredBasesubgraph so non-matching nodes are hidden and the graph is re-laid out per filter; filtered layouts are not cached (only the unfiltered one stays inua-layout:v6). A count badge on the funnel shows active filters; aClear All(✕) pill next to it restores the full graph from the layout cache; zero matches show a centered "No nodes match the current filters" overlay (non-interactivepointer-events-none; the filter/reset buttons sit atz-30above thez-20overlays so they stay clickable); filters reset whengraph.repochangesMessageContent.jsx— Markdown-rendered assistant messages vs plain user messages; assistant content wrapped inprose prose-invert prose-sm max-w-none(Tailwind typography plugin) for readable paragraphs/headings/lists/code/tables. Renders[C1]citation markers as clickable clay chips (DOM-walk of the sanitized HTML, skippingpre/code/a/button) and reports clicks viaonCitationClick(citation, rect)for markers that exist in the message'scitationsmap; unmatched markers stay plain textQuotaNotice.jsx— Usage-cap 429 notice (daisyUIalert alert-warning alert-soft): warning-triangle icon, "Daily limit reached" title, per-group copy ("You've used N of L daily messages/ingests. More free up at "), abadge badge-warning badge-outlineused / limit, and an optional dismiss (×) button. Props{ quota: {group, used, limit, resets_at}, onDismiss }; renders null withoutquota. Fed byreadErrorfromsrc/api/sse.jsinAppPage(start + ready views) andSymbolGraphView(explain panel)CitationCard.jsx— Fixed-position clay popover anchored to a clicked citation (flips above/below to fit the viewport, clamped horizontally; no anchor arrow/tail). Closes on outside click/Escape/window resize/chat scroll; scroll routing is cursor-driven — a nativewheellistener (passive: false) on the card intercepts wheel events and redirects them to the code block (preRef.scrollTop += deltaY) so hovering the card scrolls the code instead of the chat behind it, while thescrollcapture listener ignores events originating inside the card (e.g. dragging the code scrollbar) and only collapses the card on real chat scrolls. Self-fetches the chunk viagetChunk(citation.repo_hash, citation.chunk_id)on mount (the citation JSON carriesrepo_hash; the route identifier is the commit hash, not a repo name/URL); shows file path + line span, symbol/language chips, and the chunkcontentin a scrollablepre, with loading/error statesRepoInput.jsx— GitHub URL input in a glass composer pill (.glass-composer, rounded-full) with a primary Process buttonProgressBar.jsx— Pipeline progress bar + step indicators (stages: ingest → scan → chunk → embed → ready)ChatMessages.jsx— Scrollable message list rendered in aflex flex-col-reversescroll container (the reversed flex itself is the scroller, so the browser's defaultscrollTop 0IS the bottom — opening a chat lands on the newest messages with no scroll JS, immune to async markdown, and streaming stays pinned automatically). Messages render newest-first; the sync-boundary divider is inserted during the reversed pass (trailing-divider case is unshifted to the visual bottom). AppPage keys it byactiveConvId(remount) and theloadingprop renders chat-alignedskeletonrows while history loads. Empty state (centered "Ready to chat" label,flex-1filler). daisyUI chat bubbles: user =chat-bubble-primary(right tail,chat-end), assistant =bg-base-200 text-base-content(left tail,chat-start). The daisyUI mask tail stays seamless — the tail-corner radius is owned by.chat-bubble(set to 0 on the tail side), so bubbles must NOT addrounded-*utilities that touch that corner. Typing/loading indicator renders inside the empty assistant placeholder bubble (no separate loading bubble). Sync-boundary divider: takes the conversation's currentrepoHashprop and renders a daisyUIdivider("Repo synced — messages above reference an older commit, below reference the latest commit") after the last message whoserepo_hashdiffers from the current commit — so it appears immediately after a sync (even before a new-commit message exists, as a trailing divider) and persists across refresh (it's derived from server data, not a session marker). Local streaming messages are stamped with the activerepo_hashbyAppPage, so the in-session list matches server truthLandingHero.jsx— Full-viewport claymorphism hero: GSAP entrance timeline (badge → title → subtitle → CTA), clay pill badge, puffyclay-pressCTA buttons, sprinkled infinitely-animatingClayShapesLandingFeatures.jsx— Feature bento grid (2 wide + 2 narrow) of puffy.claywhite cards (colored icon chips) with GSAP scroll-reveal and backgroundClayShapesLandingHowItWorks.jsx— 3-step workflow of puffy.claywhite cards (clay number chips, middle card offset for asymmetry), GSAP scroll-reveal, backgroundClayShapesLandingFooter.jsx— Footer with GSAP fade-in on scrollClayShapes.jsx— Decorative animated 3D clay geometric shapes (circle/ring/square/squircle/blob/egg/pill — all soft/rounded, no sharp points) scattered absolutely across a section. Each shape gets one seamless infinite GSAP timeline (ref-based,repeat: -1): rotation uses relative+=/-=so it keeps accumulating without snapping, and the bob togglesy(0 → float → 0). Uses a plainprefers-reduced-motionguard (NOTgsap.matchMedia, whichScrollTrigger.refresh()re-runs and would kill/recreate the loops). Colors are Tailwind text-color classes rendered ascurrentColorso shapes adapt to the active theme
Local Contracts
- All components use daisyUI class names (
btn,input,chat,menu,drawer, etc.) — no hand-written CSS - Theme/chrome follows the design system: colors via daisyUI semantic classes or
--ds-*tokens;border-2/rounded-noneutilities are tokenized insrc/styles/design-system.css(--border/--radius-box) so they adapt per theme — do NOT hardcodeborder-widthoverrides; explicit radius utilities (rounded-full/rounded-3xl) are allowed - Clay surfaces use
.clay/.clay-press(shadows only, compose with Tailwind bg classes); glass surfaces use.glass-*— never inlinebackdrop-blur/alpha values - GSAP animations registered via
ScrollTrigger.create()inuseLayoutEffectwith StrictMode guard; ambient loops (marquee, floating shapes) usegsap.matchMedia()with a(prefers-reduced-motion: no-preference)branch so motion is disabled for reduced-motion users - Landing page components accept no props — self-contained with Clerk hooks and GSAP timelines
- App components (Layout, Sidebar, ChatMessages, etc.) receive data via props from
AppPage
Work Guidance
- New shared UI elements go here, not in pages/
- Use daisyUI component classes first, Tailwind utilities for overrides,
!suffix only as last resort - Scroll-triggered animations use
gsap.context()+ctx.revert()for proper StrictMode cleanup - Keep components focused — if a component needs significant state, consider extracting to a hook
Verification
- All components rendered on either
/(Landing) or/app(App) routes - Build verified with
npm run build