"use client" import * as React from "react" import { Link, useLocation } from "react-router" import { useNavigate } from "react-router" import { OsFolderGlyph } from "@/components/data-views/os-folder-glyph" import { LibraryAskSearchBar } from "@/components/library-ask-search-bar" import { LibraryBasicSearchBar } from "@/components/library-basic-search-bar" import { LibraryHubCreateTileButton } from "@/components/library-hub-create-tile-button" import { ModeSwitchSearchBar } from "@/components/mode-switch-search-bar" import { useAskLeo, useAskLeoPageContext } from "@/components/ask-leo-context" import { PrimaryPageTemplate } from "@/components/templates/primary-page-template" import { Shortcut } from "@/components/ui/dropdown-menu" import { useSidebar } from "@/components/ui/sidebar" import { useProductDashboardHref } from "@/contexts/product-route-sync" import { setLibrarySearchMode, useLibrarySearchMode, } from "@/hooks/use-library-search-mode" import { useAltKeyLabel, useModKeyLabel } from "@/hooks/use-mod-key-label" import { DEFAULT_LIBRARY_FOLDERS, type LibraryFolder, type LibraryFolderColorKey, } from "@/lib/mock/library-folders" import { LIBRARY_ITEMS, type LibraryItem, type LibraryItemType, type LibraryLevel } from "@/lib/mock/library" import { LIBRARY_HUB_ASK_LEO_PROMPTS } from "@/lib/library-hub-search" import { LIBRARY_ALL_PATH, LIBRARY_NAV_MY_AUTHOR, currentLibraryBasePath, libraryNavHref, libraryRouteHref, } from "@/lib/library-nav" import { libraryDedicatedSearchRecents } from "@/lib/library-recent-searches" import { libraryBasicSearchRecents } from "@/lib/library-basic-search-recents" import { recordLibraryRecentSearch } from "@/lib/library-recent-searches" import { LIBRARY_DRAFT_WITH_AI_PROMPT } from "@/lib/library-authoring" import { cn } from "@/lib/utils" const NEW_QUESTION_AUTHORING_PATH = "/library/new" const TEMPLATE_PROMPT = "Walk me through choosing an item template (single choice, multi-select, short answer, true / false) and produce a starter item with name, options, notes, and tags." const IMPORT_PROMPT = "Guide me through importing assessment questions in bulk. Ask about source format (CSV, QTI, copy/paste), then outline what columns and mappings I need." /** Rotating example queries — read like something a user would actually type into search. */ const HUB_COMPOSER_PLACEHOLDERS = [ "items tagged with Tag 1 and Manual Therapy", "everything Owner A edited this month", "PT 520 items tagged Gait & Posture", "find LIB-2026-001 and anything like it", "drafts from the most recent reference set", "Type 1 items I still need for the demo block", ] as const interface ScopeChip { id: string label: string href: string count: number folderGlyph: { colorKey: LibraryFolderColorKey icon: string variant?: "solid" | "outline" } } interface CreateTile { id: string label: string description: string icon: string iconTint: string onClick: () => void badge?: "AI" | null shortcutKeys?: string } interface FolderTile extends LibraryFolder { count: number } function buildScopeChips(items: LibraryItem[], libraryBasePath: string): ScopeChip[] { const mine = items.filter( i => i.author === LIBRARY_NAV_MY_AUTHOR || i.createdBy === LIBRARY_NAV_MY_AUTHOR, ).length return [ { id: "all", label: "All", count: items.length, href: libraryRouteHref(LIBRARY_ALL_PATH, libraryBasePath), folderGlyph: { colorKey: "muted", icon: "fa-layer-group", variant: "outline" }, }, { id: "my", label: "Mine", count: mine, href: libraryNavHref({ scope: "my", basePath: libraryBasePath }), folderGlyph: { colorKey: "brand", icon: "fa-user" }, }, ] } function buildFolderTiles(items: LibraryItem[], folders: LibraryFolder[]): FolderTile[] { const byFolder = new Map() for (const i of items) byFolder.set(i.folderId, (byFolder.get(i.folderId) ?? 0) + 1) return folders .filter(f => f.parentId === null) .map(f => ({ ...f, count: byFolder.get(f.id) ?? 0 })) } // Static derivations of immutable mock data — computed once at module load, // not per render of the hub. Re-derive only if the underlying mock arrays change. const HUB_FOLDER_TILES = buildFolderTiles(LIBRARY_ITEMS, DEFAULT_LIBRARY_FOLDERS) function isMineLibraryItem(i: LibraryItem): boolean { return i.author === LIBRARY_NAV_MY_AUTHOR || i.createdBy === LIBRARY_NAV_MY_AUTHOR } const HUB_MINE_RECENTS = [...LIBRARY_ITEMS] .filter(isMineLibraryItem) .sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)) .slice(0, 3) const HUB_ASK_LEO_PAGE_CONTEXT = { title: "Question hub", description: "Browse and organize assessment items with AI-assisted workflows. The hub search field opens discovery results on `/library/find` with your wording applied to the list; use the library’s Search in the sidebar for `/library/list`. Pick a suggestion below when you want a full Ask Leo thread.", suggestions: [...LIBRARY_HUB_ASK_LEO_PROMPTS], data: { surface: "library-discovery-hub" as const }, } function formatRelativeDate(iso: string): string { const now = Date.now() const t = new Date(iso).getTime() if (Number.isNaN(t)) return iso const diffDays = Math.round((now - t) / 86_400_000) if (diffDays <= 0) return "today" if (diffDays === 1) return "yesterday" if (diffDays < 7) return `${diffDays}d ago` if (diffDays < 30) return `${Math.round(diffDays / 7)}w ago` if (diffDays < 365) return `${Math.round(diffDays / 30)}mo ago` return `${Math.round(diffDays / 365)}y ago` } export function LibraryHubClient() { const dashboardHref = useProductDashboardHref() const navigate = useNavigate() const { pathname } = useLocation() const libraryBasePath = currentLibraryBasePath(pathname) const { openWithPrompt } = useAskLeo() const { setOpen: setMainSidebarOpen } = useSidebar() const mod = useModKeyLabel() const alt = useAltKeyLabel() const [hubComposerValue, setHubComposerValue] = React.useState("") const searchMode = useLibrarySearchMode() const [basicKeyword, setBasicKeyword] = React.useState("") const [basicType, setBasicType] = React.useState("") const [basicDifficulty, setBasicDifficulty] = React.useState("") const [isSearchPending, startSearchTransition] = React.useTransition() const scopes = React.useMemo( () => buildScopeChips(LIBRARY_ITEMS, libraryBasePath), [libraryBasePath], ) const folderTiles = HUB_FOLDER_TILES const recents = HUB_MINE_RECENTS useAskLeoPageContext(HUB_ASK_LEO_PAGE_CONTEXT) const sendLeoSuggestion = React.useCallback( (prompt: string) => { openWithPrompt(prompt) }, [openWithPrompt], ) /** * Navigate to the full-page authoring composer (`/library/new`). * Mirrors the Placements "New placement" pre-collapse: animates the sidebar * closed first so the user sees one smooth transition into the focused flow * (the route also mounts `SidebarAutoCollapse` to lock it shut while there). */ const openCreateQuestion = React.useCallback(() => { setMainSidebarOpen(false) window.setTimeout(() => navigate(libraryRouteHref(NEW_QUESTION_AUTHORING_PATH, libraryBasePath)), 260) }, [libraryBasePath, navigate, setMainSidebarOpen]) const openDraftWithLeo = React.useCallback(() => { openWithPrompt(LIBRARY_DRAFT_WITH_AI_PROMPT) }, [openWithPrompt]) const onHubComposerSubmit = React.useCallback( (message: string) => { const trimmed = message.trim() if (trimmed) recordLibraryRecentSearch(trimmed) startSearchTransition(() => { navigate(libraryNavHref({ scope: "all", q: trimmed, hubFind: true, basePath: libraryBasePath })) }) }, [libraryBasePath, navigate], ) const onHubRecentSelect = React.useCallback( (query: string) => { setHubComposerValue(query) onHubComposerSubmit(query) }, [onHubComposerSubmit], ) const onBasicSearchSubmit = React.useCallback( (next: { keyword: string type: LibraryItemType | "" difficulty: LibraryLevel | "" }) => { const parts = [next.keyword.trim()] if (next.type === "multiple_choice") parts.push("multiple choice") if (next.type === "true_false") parts.push("true false") if (next.type === "short_answer") parts.push("short answer") if (next.difficulty) parts.push(next.difficulty) const q = parts.filter(Boolean).join(" ").trim() startSearchTransition(() => { navigate(libraryNavHref({ scope: "all", q, hubFind: true, basePath: libraryBasePath })) }) }, [libraryBasePath, navigate], ) const createShortcut = `${mod}${alt}N` const hubFolderBrowserTileClass = cn( "flex w-full flex-col items-center gap-2 rounded-xl border border-transparent p-3 text-center transition-colors", "hover:border-border/80 hover:bg-muted/35", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", ) const createTiles = React.useMemo( () => [ { id: "scratch", label: "Start from scratch", description: "Start with an empty editor and build the item by hand.", icon: "fa-plus", iconTint: "bg-brand/15 text-brand", onClick: openCreateQuestion, shortcutKeys: createShortcut, }, { id: "ask-leo", label: "Draft with Leo", description: "Describe the outcome and let Leo propose stem, options, and rationale.", icon: "fa-star-christmas", iconTint: "bg-brand/15 text-brand", badge: "AI", onClick: openDraftWithLeo, }, { id: "template", label: "From template", description: "Pick choice-style, multi-select, short answer or true / false — Leo fills the scaffold.", icon: "fa-clone", iconTint: "bg-sky-500/15 text-sky-700 dark:text-sky-300", onClick: () => sendLeoSuggestion(TEMPLATE_PROMPT), }, { id: "import", label: "Import", description: "Bring in CSV, QTI, or paste from another tool — Leo will map the columns.", icon: "fa-file-import", iconTint: "bg-muted text-muted-foreground", onClick: () => sendLeoSuggestion(IMPORT_PROMPT), }, ], [openCreateQuestion, openDraftWithLeo, sendLeoSuggestion, createShortcut], ) return ( {/* ⌘⌥K (Ask Leo toggle) is bound globally in AskLeoProvider — do not double-bind here. */}

Question hub

( )} leo={(footer) => ( )} />
{/* Create a question */}

Create a question

{createTiles.map(tile => ( ))}
{recents.length > 0 && (

Continue where you left off

View all