Download src/components/student/StudentWorkspaceUI.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/student/StudentWorkspaceUI.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/student/StudentWorkspaceUI.tsx
-
curl -L -o StudentWorkspaceUI.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/student/StudentWorkspaceUI.tsx
16.4 kB
| "use client"; | |
| import Link from "next/link"; | |
| import type { ElementType, FormEvent, ReactNode } from "react"; | |
| import { | |
| AlertCircle, | |
| ArrowRight, | |
| Check, | |
| ChevronRight, | |
| LoaderCircle, | |
| Search, | |
| Sparkles, | |
| } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import { normalizeApiError } from "@/lib/api/client"; | |
| type Tone = "amber" | "emerald" | "blue" | "violet" | "neutral"; | |
| const toneStyles: Record< | |
| Tone, | |
| { | |
| icon: string; | |
| border: string; | |
| glow: string; | |
| text: string; | |
| } | |
| > = { | |
| amber: { | |
| icon: "border-amber-300/15 bg-amber-300/[0.08] text-amber-200", | |
| border: "border-amber-300/12 hover:border-amber-200/30", | |
| glow: "before:bg-amber-400/[0.08]", | |
| text: "text-amber-100", | |
| }, | |
| emerald: { | |
| icon: "border-emerald-300/15 bg-emerald-300/[0.08] text-emerald-200", | |
| border: "border-emerald-300/12 hover:border-emerald-200/30", | |
| glow: "before:bg-emerald-400/[0.08]", | |
| text: "text-emerald-100", | |
| }, | |
| blue: { | |
| icon: "border-blue-300/15 bg-blue-300/[0.08] text-blue-200", | |
| border: "border-blue-300/12 hover:border-blue-200/30", | |
| glow: "before:bg-blue-400/[0.08]", | |
| text: "text-blue-100", | |
| }, | |
| violet: { | |
| icon: "border-fuchsia-200/15 bg-fuchsia-200/[0.06] text-fuchsia-100", | |
| border: "border-fuchsia-200/10 hover:border-fuchsia-100/24", | |
| glow: "before:bg-fuchsia-300/[0.06]", | |
| text: "text-fuchsia-100", | |
| }, | |
| neutral: { | |
| icon: "border-white/[0.08] bg-white/[0.04] text-white/78", | |
| border: "border-white/[0.08] hover:border-white/[0.16]", | |
| glow: "before:bg-white/[0.05]", | |
| text: "text-white", | |
| }, | |
| }; | |
| const panelVariants = { | |
| compact: "rounded-2xl p-4 sm:p-5", | |
| standard: "rounded-[1.35rem] p-5 sm:p-6", | |
| hero: "rounded-[1.6rem] p-6 sm:p-8", | |
| }; | |
| function cleanStudentError(error: unknown) { | |
| const normalized = normalizeApiError(error); | |
| const lower = normalized.toLowerCase(); | |
| const unsafeTerms = [ | |
| "backend", | |
| "chunk", | |
| "token", | |
| "retrieval", | |
| "mock", | |
| "raw json", | |
| "debug", | |
| "server error", | |
| "api failed", | |
| "vector db", | |
| "embedding", | |
| "remotion", | |
| "provider", | |
| "model", | |
| "stack trace", | |
| "file path", | |
| "extraction_error", | |
| "extracted_text", | |
| "document path", | |
| "private filename", | |
| ]; | |
| if (unsafeTerms.some((term) => lower.includes(term))) { | |
| return "DocDoe could not prepare this right now. Please try again."; | |
| } | |
| if (/[{}[\]\\]/.test(normalized) || normalized.length > 180) { | |
| return "DocDoe could not prepare this right now. Please try again."; | |
| } | |
| return normalized || "DocDoe could not prepare this right now. Please try again."; | |
| } | |
| export interface StudentPageShellProps { | |
| eyebrow?: string; | |
| title?: string; | |
| description?: string; | |
| action?: ReactNode; | |
| children: ReactNode; | |
| className?: string; | |
| contentClassName?: string; | |
| variant?: "standard" | "dense" | "wide" | "centered"; | |
| } | |
| export function StudentPageShell({ | |
| eyebrow, | |
| title, | |
| description, | |
| action, | |
| children, | |
| className, | |
| contentClassName, | |
| variant = "standard", | |
| }: StudentPageShellProps) { | |
| const widths = { | |
| dense: "max-w-5xl", | |
| standard: "max-w-6xl", | |
| wide: "max-w-7xl", | |
| centered: "max-w-4xl", | |
| }; | |
| return ( | |
| <section | |
| className={cn( | |
| "relative mx-auto w-full px-4 py-6 sm:px-6 lg:px-8", | |
| variant === "centered" && "min-h-[calc(100vh-8rem)]", | |
| className, | |
| )} | |
| > | |
| <div className={cn("relative mx-auto w-full", widths[variant], contentClassName)}> | |
| {(eyebrow || title || description || action) && ( | |
| <div className="mb-5 flex flex-col gap-4 sm:mb-7 lg:flex-row lg:items-end lg:justify-between"> | |
| <div className="min-w-0 space-y-2"> | |
| {eyebrow && ( | |
| <p className="text-[11px] font-semibold uppercase text-white/46"> | |
| {eyebrow} | |
| </p> | |
| )} | |
| {title && ( | |
| <h1 className="max-w-3xl text-balance text-2xl font-semibold text-white sm:text-[2rem]"> | |
| {title} | |
| </h1> | |
| )} | |
| {description && ( | |
| <p className="max-w-2xl text-sm leading-6 text-white/62"> | |
| {description} | |
| </p> | |
| )} | |
| </div> | |
| {action && <div className="shrink-0">{action}</div>} | |
| </div> | |
| )} | |
| {children} | |
| </div> | |
| </section> | |
| ); | |
| } | |
| export interface PremiumPanelProps { | |
| children: ReactNode; | |
| className?: string; | |
| variant?: keyof typeof panelVariants; | |
| tone?: Tone; | |
| interactive?: boolean; | |
| as?: "div" | "section" | "article"; | |
| } | |
| export function PremiumPanel({ | |
| children, | |
| className, | |
| variant = "standard", | |
| tone = "neutral", | |
| interactive = false, | |
| as: Component = "div", | |
| }: PremiumPanelProps) { | |
| return ( | |
| <Component | |
| className={cn( | |
| "relative overflow-hidden border bg-[linear-gradient(145deg,rgba(255,255,255,0.055),rgba(255,255,255,0.018))] shadow-[0_18px_60px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.045)] backdrop-blur-xl", | |
| "before:pointer-events-none before:absolute before:-right-20 before:-top-20 before:size-48 before:rounded-full before:blur-3xl", | |
| panelVariants[variant], | |
| toneStyles[tone].border, | |
| toneStyles[tone].glow, | |
| interactive && "transition duration-200 hover:-translate-y-0.5 focus-within:border-white/20", | |
| className, | |
| )} | |
| > | |
| <div className="relative">{children}</div> | |
| </Component> | |
| ); | |
| } | |
| export interface ActionTileProps { | |
| title: string; | |
| description: string; | |
| icon: ElementType; | |
| href?: string; | |
| onClick?: () => void; | |
| cta?: string; | |
| tone?: Tone; | |
| disabled?: boolean; | |
| className?: string; | |
| } | |
| export function ActionTile({ | |
| title, | |
| description, | |
| icon: Icon, | |
| href, | |
| onClick, | |
| cta = "Open", | |
| tone = "neutral", | |
| disabled = false, | |
| className, | |
| }: ActionTileProps) { | |
| const content = ( | |
| <> | |
| <div className="flex items-start gap-3.5"> | |
| <span | |
| className={cn( | |
| "grid size-10 shrink-0 place-items-center rounded-xl border", | |
| toneStyles[tone].icon, | |
| )} | |
| > | |
| <Icon className="size-5" /> | |
| </span> | |
| <span className="min-w-0 flex-1"> | |
| <span className="block text-sm font-semibold text-white">{title}</span> | |
| <span className="mt-1 block text-[12.5px] leading-5 text-white/58">{description}</span> | |
| </span> | |
| </div> | |
| <span className={cn("mt-5 inline-flex items-center gap-1.5 text-xs font-semibold", toneStyles[tone].text)}> | |
| {cta} | |
| <ChevronRight className="size-3.5" /> | |
| </span> | |
| </> | |
| ); | |
| const classes = cn( | |
| "group block h-full rounded-2xl border bg-white/[0.025] p-4 text-left shadow-[inset_0_1px_0_rgba(255,255,255,0.035)] transition duration-200 hover:-translate-y-0.5 hover:bg-white/[0.04] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22", | |
| toneStyles[tone].border, | |
| disabled && "pointer-events-none opacity-45", | |
| className, | |
| ); | |
| if (href && !disabled) { | |
| return ( | |
| <Link href={href} className={classes}> | |
| {content} | |
| </Link> | |
| ); | |
| } | |
| if (onClick && !disabled) { | |
| return ( | |
| <button type="button" onClick={onClick} className={classes}> | |
| {content} | |
| </button> | |
| ); | |
| } | |
| return <div className={classes}>{content}</div>; | |
| } | |
| export interface EmptyWorkspaceProps { | |
| icon?: ElementType; | |
| title: string; | |
| description: string; | |
| examples?: string[]; | |
| actionText?: string; | |
| actionHref?: string; | |
| onAction?: () => void; | |
| className?: string; | |
| } | |
| export function EmptyWorkspace({ | |
| icon: Icon = Sparkles, | |
| title, | |
| description, | |
| examples = [], | |
| actionText, | |
| actionHref, | |
| onAction, | |
| className, | |
| }: EmptyWorkspaceProps) { | |
| const action = actionText ? ( | |
| actionHref ? ( | |
| <Link | |
| href={actionHref} | |
| className="inline-flex items-center justify-center gap-2 rounded-xl border border-white/[0.12] bg-white/[0.055] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.09] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22" | |
| > | |
| {actionText} | |
| <ArrowRight className="size-4" /> | |
| </Link> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={onAction} | |
| className="inline-flex items-center justify-center gap-2 rounded-xl border border-white/[0.12] bg-white/[0.055] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.09] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22" | |
| > | |
| {actionText} | |
| <ArrowRight className="size-4" /> | |
| </button> | |
| ) | |
| ) : null; | |
| return ( | |
| <PremiumPanel className={cn("flex min-h-[22rem] items-center justify-center text-center", className)}> | |
| <div className="mx-auto max-w-md"> | |
| <div className="mx-auto grid size-12 place-items-center rounded-2xl border border-white/[0.08] bg-white/[0.04] text-white/70"> | |
| <Icon className="size-5" /> | |
| </div> | |
| <h2 className="mt-4 text-lg font-semibold text-white">{title}</h2> | |
| <p className="mt-2 text-sm leading-6 text-white/58">{description}</p> | |
| {examples.length > 0 && ( | |
| <div className="mt-5 flex flex-wrap justify-center gap-2"> | |
| {examples.map((example) => ( | |
| <span | |
| key={example} | |
| className="rounded-full border border-white/[0.08] bg-white/[0.035] px-3 py-1.5 text-xs text-white/58" | |
| > | |
| {example} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| {action && <div className="mt-6">{action}</div>} | |
| </div> | |
| </PremiumPanel> | |
| ); | |
| } | |
| export interface StudyCommandBarProps { | |
| value: string; | |
| onChange: (value: string) => void; | |
| onSubmit: (value: string) => void; | |
| placeholder?: string; | |
| submitLabel?: string; | |
| chips?: string[]; | |
| className?: string; | |
| disabled?: boolean; | |
| } | |
| export function StudyCommandBar({ | |
| value, | |
| onChange, | |
| onSubmit, | |
| placeholder = "Search your studies", | |
| submitLabel = "Start", | |
| chips = [], | |
| className, | |
| disabled = false, | |
| }: StudyCommandBarProps) { | |
| function handleSubmit(event: FormEvent<HTMLFormElement>) { | |
| event.preventDefault(); | |
| const trimmed = value.trim(); | |
| if (trimmed && !disabled) onSubmit(trimmed); | |
| } | |
| return ( | |
| <PremiumPanel variant="compact" className={cn("space-y-3", className)} interactive> | |
| <form onSubmit={handleSubmit} className="flex flex-col gap-3 sm:flex-row"> | |
| <label className="relative min-w-0 flex-1"> | |
| <span className="sr-only">Study request</span> | |
| <Search className="pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/36" /> | |
| <input | |
| value={value} | |
| onChange={(event) => onChange(event.target.value)} | |
| placeholder={placeholder} | |
| disabled={disabled} | |
| className="h-12 w-full rounded-xl border border-white/[0.08] bg-black/28 px-10 text-sm text-white outline-none transition placeholder:text-white/32 focus:border-white/22 focus:bg-black/38" | |
| /> | |
| </label> | |
| <button | |
| type="submit" | |
| disabled={disabled || !value.trim()} | |
| className="inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-white px-5 text-sm font-semibold text-black transition hover:bg-white/88 disabled:cursor-not-allowed disabled:opacity-45" | |
| > | |
| {submitLabel} | |
| <ArrowRight className="size-4" /> | |
| </button> | |
| </form> | |
| {chips.length > 0 && ( | |
| <div className="flex flex-wrap gap-2"> | |
| {chips.map((chip) => ( | |
| <button | |
| key={chip} | |
| type="button" | |
| onClick={() => onChange(chip)} | |
| className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-xs text-white/62 transition hover:border-white/[0.16] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22" | |
| > | |
| {chip} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </PremiumPanel> | |
| ); | |
| } | |
| export interface SafeErrorBannerProps { | |
| error: unknown; | |
| title?: string; | |
| className?: string; | |
| } | |
| export function SafeErrorBanner({ error, title = "We could not finish that", className }: SafeErrorBannerProps) { | |
| return ( | |
| <div | |
| role="alert" | |
| className={cn( | |
| "rounded-2xl border border-rose-300/18 bg-rose-300/[0.055] p-4 text-sm text-rose-50 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]", | |
| className, | |
| )} | |
| > | |
| <div className="flex gap-3"> | |
| <AlertCircle className="mt-0.5 size-4 shrink-0 text-rose-200" /> | |
| <div className="min-w-0"> | |
| <p className="font-semibold">{title}</p> | |
| <p className="mt-1 leading-5 text-rose-50/72">{cleanStudentError(error)}</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export interface GeneratorWorkspaceProps { | |
| title: string; | |
| description: string; | |
| setup: ReactNode; | |
| children?: ReactNode; | |
| empty?: ReactNode; | |
| loading?: boolean; | |
| loadingLabel?: string; | |
| error?: unknown; | |
| footer?: ReactNode; | |
| className?: string; | |
| } | |
| export function GeneratorWorkspace({ | |
| title, | |
| description, | |
| setup, | |
| children, | |
| empty, | |
| loading = false, | |
| loadingLabel = "Preparing your study output", | |
| error, | |
| footer, | |
| className, | |
| }: GeneratorWorkspaceProps) { | |
| return ( | |
| <div className={cn("grid gap-5 lg:grid-cols-[minmax(17rem,0.82fr)_minmax(0,1.5fr)]", className)}> | |
| <PremiumPanel className="h-fit" variant="standard"> | |
| <div className="mb-5"> | |
| <p className="text-[11px] font-semibold uppercase text-white/42">Setup</p> | |
| <h2 className="mt-1 text-lg font-semibold text-white">{title}</h2> | |
| <p className="mt-2 text-sm leading-6 text-white/56">{description}</p> | |
| </div> | |
| {setup} | |
| </PremiumPanel> | |
| <div className="min-w-0 space-y-4"> | |
| {error ? <SafeErrorBanner error={error} /> : null} | |
| {loading ? ( | |
| <PremiumPanel className="min-h-[24rem]"> | |
| <div className="flex min-h-[18rem] flex-col items-center justify-center text-center"> | |
| <LoaderCircle className="size-7 animate-spin text-white/68" /> | |
| <h3 className="mt-4 text-base font-semibold text-white">{loadingLabel}</h3> | |
| <p className="mt-2 max-w-sm text-sm leading-6 text-white/54"> | |
| Keep this open while DocDoe prepares the result. | |
| </p> | |
| </div> | |
| </PremiumPanel> | |
| ) : ( | |
| children ?? empty | |
| )} | |
| {footer} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export interface StatusTimelineStep { | |
| id: string; | |
| label: string; | |
| description?: string; | |
| status?: "complete" | "current" | "waiting" | "error"; | |
| } | |
| export interface StatusTimelineProps { | |
| steps: StatusTimelineStep[]; | |
| className?: string; | |
| } | |
| export function StatusTimeline({ steps, className }: StatusTimelineProps) { | |
| return ( | |
| <div className={cn("space-y-3", className)}> | |
| {steps.map((step) => { | |
| const status = step.status ?? "waiting"; | |
| return ( | |
| <div key={step.id} className="flex gap-3"> | |
| <span | |
| className={cn( | |
| "mt-0.5 grid size-7 shrink-0 place-items-center rounded-full border", | |
| status === "complete" && "border-emerald-300/25 bg-emerald-300/[0.08] text-emerald-100", | |
| status === "current" && "border-white/20 bg-white/[0.08] text-white", | |
| status === "waiting" && "border-white/[0.08] bg-white/[0.025] text-white/34", | |
| status === "error" && "border-rose-300/22 bg-rose-300/[0.08] text-rose-100", | |
| )} | |
| > | |
| {status === "complete" ? ( | |
| <Check className="size-3.5" /> | |
| ) : status === "current" ? ( | |
| <LoaderCircle className="size-3.5 animate-spin" /> | |
| ) : status === "error" ? ( | |
| <AlertCircle className="size-3.5" /> | |
| ) : ( | |
| <span className="size-1.5 rounded-full bg-current" /> | |
| )} | |
| </span> | |
| <div className="min-w-0 pb-1"> | |
| <p className="text-sm font-semibold text-white">{step.label}</p> | |
| {step.description && <p className="mt-0.5 text-xs leading-5 text-white/50">{step.description}</p>} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } | |