Download src/components/dashboard/SourcesPanel.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/dashboard/SourcesPanel.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/dashboard/SourcesPanel.tsx
-
curl -L -o SourcesPanel.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/dashboard/SourcesPanel.tsx
11.8 kB
| "use client"; | |
| import { useMemo, useState } from "react"; | |
| import { Check, FileUp, MoreVertical, Plus, Search, UploadCloud, Wifi, WifiOff } from "lucide-react"; | |
| import LiquidGlass from "liquid-glass-react"; | |
| import { cn } from "@/lib/utils"; | |
| import { sourceFilters, type DashboardSource } from "@/components/dashboard/dashboardData"; | |
| import { PreviewBadge } from "@/components/dashboard/dashboardPrimitives"; | |
| import { MaterialTypeBadge } from "@/components/dashboard/SourceBlockedState"; | |
| import { getPublicDemoMode } from "@/lib/env"; | |
| import { hasLiveSession } from "@/lib/api/mode"; | |
| type SourcesPanelProps = { | |
| className?: string; | |
| selectedSourceId: string; | |
| selectedSourceIds?: string[]; | |
| documents?: DashboardSource[]; | |
| onSourceOpen: (sourceId: string) => void; | |
| onSourceToggle?: (sourceId: string) => void; | |
| onUploadClick?: () => void; | |
| }; | |
| export function SourcesPanel({ | |
| className, | |
| selectedSourceId, | |
| selectedSourceIds = [selectedSourceId], | |
| documents = [], | |
| onSourceOpen, | |
| onSourceToggle, | |
| onUploadClick, | |
| }: SourcesPanelProps) { | |
| const [activeFilter, setActiveFilter] = useState("All"); | |
| const [query, setQuery] = useState(""); | |
| const isLive = getPublicDemoMode() === "live" || hasLiveSession(); | |
| const liveBadge = isLive ? ( | |
| <span className="ml-2 inline-flex items-center gap-1 rounded-full border border-emerald-400/30 bg-emerald-500/10 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider text-emerald-300"> | |
| <Wifi className="size-3" /> Connected to AI | |
| </span> | |
| ) : ( | |
| <span className="ml-2 inline-flex items-center gap-1 rounded-full border border-amber-400/30 bg-amber-500/10 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider text-amber-300"> | |
| <WifiOff className="size-3" /> Offline preview | |
| </span> | |
| ); | |
| const visibleSources = useMemo( | |
| () => | |
| documents.filter((source) => { | |
| const filterMatches = | |
| activeFilter === "All" || | |
| source.type === activeFilter || | |
| (activeFilter === "Books" && source.type === "Book") || | |
| (activeFilter === "PYQs" && source.type === "PYQ") || | |
| (activeFilter === "Links" && source.type === "Link") || | |
| (activeFilter === "Images" && source.type === "Image"); | |
| const searchText = `${source.title} ${source.subtitle} ${source.type}`.toLowerCase(); | |
| return filterMatches && searchText.includes(query.trim().toLowerCase()); | |
| }), | |
| [activeFilter, query, documents], | |
| ); | |
| const pinnedSources = visibleSources.filter((source) => source.group === "Pinned"); | |
| const recentSources = visibleSources.filter((source) => source.group === "Recent"); | |
| const hasSources = visibleSources.length > 0; | |
| const selectedCount = selectedSourceIds.length; | |
| const sourceUsageLabel = | |
| selectedCount > 1 | |
| ? `Using ${selectedCount} sources` | |
| : selectedCount === 1 | |
| ? "Using 1 source" | |
| : "Choose sources for DocDoe"; | |
| return ( | |
| <LiquidGlass | |
| blurAmount={0.04} | |
| saturation={120} | |
| elasticity={0.16} | |
| displacementScale={30} | |
| aberrationIntensity={1.3} | |
| cornerRadius={24} | |
| mode="standard" | |
| className={cn( | |
| "flex min-h-0 w-full min-w-0 flex-col overflow-hidden rounded-[24px] border border-white/[0.08] bg-[#0b0614]/35 shadow-[0_22px_80px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.05)] sm:min-h-[400px] lg:h-full lg:min-h-0", | |
| className, | |
| )} | |
| > | |
| <div className="flex items-center justify-between border-b border-white/[0.06] bg-white/[0.02] px-3 py-2.5 sm:px-4 sm:py-3"> | |
| <div> | |
| <h2 className="text-[13px] font-bold text-white/90">Sources {liveBadge}</h2> | |
| <p className="mt-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-white/30">{sourceUsageLabel}</p> | |
| </div> | |
| <button | |
| type="button" | |
| aria-label="Add new source" | |
| onClick={onUploadClick} | |
| className="group flex size-9 cursor-pointer items-center justify-center rounded-xl border border-violet-300/18 bg-violet-400/[0.08] text-violet-200 transition hover:border-violet-300/35 hover:bg-violet-400/[0.12] sm:size-7 sm:rounded-lg" | |
| > | |
| <Plus className="size-4 transition-transform group-hover:rotate-90" strokeWidth={2.5} /> | |
| </button> | |
| </div> | |
| <div className="space-y-2.5 p-3"> | |
| <label className="flex h-9 items-center gap-2.5 rounded-xl border border-white/5 bg-black/24 px-3 text-white/40 focus-within:border-violet-500/30 transition-all"> | |
| <Search className="size-3.5 shrink-0" strokeWidth={2} /> | |
| <input | |
| className="min-w-0 flex-1 bg-transparent text-[12px] text-white outline-none placeholder:text-white/20" | |
| onChange={(e) => setQuery(e.target.value)} | |
| placeholder="Search documents..." | |
| type="search" | |
| value={query} | |
| /> | |
| </label> | |
| <div className="flex gap-1 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> | |
| {sourceFilters.filter(f => f !== "Images").map((tab) => ( | |
| <button | |
| key={tab} | |
| onClick={() => setActiveFilter(tab)} | |
| className={cn( | |
| "h-8 shrink-0 rounded-xl px-3 text-[11px] font-bold transition-all sm:h-7 sm:rounded-lg sm:px-2.5", | |
| tab === activeFilter | |
| ? "bg-violet-500/20 text-violet-100 shadow-[0_0_15px_rgba(124,58,237,0.15)]" | |
| : "text-white/40 hover:bg-white/5 hover:text-white/60", | |
| )} | |
| > | |
| {tab} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-3 [scrollbar-color:rgba(255,255,255,0.1)_transparent] [scrollbar-width:thin]"> | |
| {hasSources ? ( | |
| <div className="space-y-4"> | |
| {pinnedSources.length > 0 && ( | |
| <SourceGroup title="Pinned" sources={pinnedSources} selectedSourceId={selectedSourceId} selectedSourceIds={selectedSourceIds} onSourceOpen={onSourceOpen} onSourceToggle={onSourceToggle} /> | |
| )} | |
| <SourceGroup title="Recent" sources={recentSources} selectedSourceId={selectedSourceId} selectedSourceIds={selectedSourceIds} onSourceOpen={onSourceOpen} onSourceToggle={onSourceToggle} /> | |
| </div> | |
| ) : ( | |
| <div className="py-12 text-center"> | |
| <p className="text-[12px] font-medium text-white/35">No sources found</p> | |
| </div> | |
| )} | |
| <button | |
| onClick={onUploadClick} | |
| type="button" | |
| className="group mt-5 flex w-full items-center gap-3 rounded-2xl border border-dashed border-white/5 bg-white/[0.02] p-3 transition hover:border-violet-500/20 hover:bg-violet-500/[0.04] sm:mt-6" | |
| > | |
| <div className="grid size-9 place-items-center rounded-xl bg-white/5 text-white/30 group-hover:bg-violet-500/10 group-hover:text-violet-400 transition-colors"> | |
| <UploadCloud className="size-4.5" /> | |
| </div> | |
| <div className="min-w-0 text-left"> | |
| <p className="text-[12px] font-bold text-white/60 group-hover:text-white transition-colors">Add new source</p> | |
| <p className="text-[10px] text-white/20 font-medium truncate">PDF, Notes, PYQs</p> | |
| </div> | |
| </button> | |
| </div> | |
| </LiquidGlass> | |
| ); | |
| } | |
| function SourceGroup({ | |
| title, | |
| sources, | |
| selectedSourceId, | |
| selectedSourceIds, | |
| onSourceOpen, | |
| onSourceToggle, | |
| }: { | |
| title: string; | |
| sources: DashboardSource[]; | |
| selectedSourceId: string; | |
| selectedSourceIds: string[]; | |
| onSourceOpen: (sourceId: string) => void; | |
| onSourceToggle?: (sourceId: string) => void; | |
| }) { | |
| if (sources.length === 0) { | |
| return null; | |
| } | |
| return ( | |
| <div> | |
| <p className="mb-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-white/42">{title}</p> | |
| <div className="grid gap-2"> | |
| {sources.map((source) => ( | |
| <SourcePanelRow | |
| key={source.id} | |
| source={source} | |
| isSelected={source.id === selectedSourceId} | |
| isIncluded={selectedSourceIds.includes(source.id)} | |
| onOpen={() => onSourceOpen(source.id)} | |
| onToggle={() => onSourceToggle?.(source.id)} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function SourcePanelRow({ | |
| source, | |
| isSelected, | |
| isIncluded, | |
| onOpen, | |
| onToggle, | |
| }: { | |
| source: DashboardSource; | |
| isSelected: boolean; | |
| isIncluded: boolean; | |
| onOpen: () => void; | |
| onToggle: () => void; | |
| }) { | |
| const Icon = source.icon; | |
| const statusLabel = | |
| source.status === "processing" || source.status === "uploaded" | |
| ? "Preparing" | |
| : source.status === "failed" | |
| ? "Could not read" | |
| : "Ready"; | |
| return ( | |
| <div | |
| className={cn( | |
| "group flex w-full cursor-pointer items-center gap-3 rounded-[18px] border p-2.5 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-300/45", | |
| isSelected | |
| ? "border-violet-300/28 bg-violet-500/[0.1] shadow-[0_0_26px_rgba(124,58,237,0.16),inset_0_1px_0_rgba(255,255,255,0.04)]" | |
| : "border-white/[0.06] bg-white/[0.03] hover:border-violet-300/18 hover:bg-white/[0.052]", | |
| )} | |
| > | |
| <button | |
| type="button" | |
| aria-pressed={isSelected} | |
| onClick={onOpen} | |
| className="flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none" | |
| > | |
| <span className={cn( | |
| "grid size-10 shrink-0 place-items-center rounded-2xl bg-gradient-to-br", | |
| source.materialType === "resume_or_personal_doc" | |
| ? "from-white/[0.08] to-white/[0.02]" | |
| : source.tone, | |
| )}> | |
| <Icon className="size-[18px] text-white/82" strokeWidth={1.85} aria-hidden="true" /> | |
| </span> | |
| <span className="min-w-0 flex-1"> | |
| <span className="block truncate text-[12.5px] font-semibold text-white">{source.title}</span> | |
| <span className="mt-0.5 flex flex-wrap items-center gap-1 truncate text-[11px] text-white/48"> | |
| {source.subject || "Study"} | |
| {source.materialType && source.materialType !== "unknown" && ( | |
| <MaterialTypeBadge materialType={source.materialType} /> | |
| )} | |
| {source.materialType === "unknown" && ( | |
| <span className="text-[9px] font-semibold uppercase tracking-wider text-white/25"> | |
| • Classify | |
| </span> | |
| )} | |
| <span className="text-white/28 sm:hidden">- {statusLabel}</span> | |
| </span> | |
| </span> | |
| </button> | |
| <span className="flex shrink-0 items-center gap-1.5"> | |
| {isSelected ? <PreviewBadge>On</PreviewBadge> : null} | |
| <button | |
| type="button" | |
| aria-pressed={isIncluded} | |
| aria-label={isIncluded ? `Remove ${source.title} from DocDoe sources` : `Use ${source.title} with DocDoe`} | |
| onClick={onToggle} | |
| className={cn( | |
| "grid size-6 place-items-center rounded-lg border transition", | |
| isIncluded | |
| ? "border-violet-300/30 bg-violet-500/20 text-violet-100" | |
| : "border-white/8 bg-white/[0.035] text-white/25 hover:text-white/60", | |
| )} | |
| > | |
| {isIncluded ? <Check className="size-3.5" strokeWidth={2.5} /> : <span className="size-2 rounded-full border border-current" />} | |
| </button> | |
| <span className="hidden rounded-full border border-white/5 bg-white/[0.03] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white/35 sm:inline-flex"> | |
| {statusLabel} | |
| </span> | |
| <FileUp className="hidden size-3.5 text-violet-200/55 group-hover:block" strokeWidth={1.8} aria-hidden="true" /> | |
| <MoreVertical className="size-4 text-white/38" strokeWidth={1.8} aria-hidden="true" /> | |
| </span> | |
| </div> | |
| ); | |
| } | |