"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 (
{(eyebrow || title || description || action) && (
{eyebrow && (

{eyebrow}

)} {title && (

{title}

)} {description && (

{description}

)}
{action &&
{action}
}
)} {children}
); } 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 (
{children}
); } 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 = ( <>
{title} {description}
{cta} ); 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 ( {content} ); } if (onClick && !disabled) { return ( ); } return
{content}
; } 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 ? ( {actionText} ) : ( ) ) : null; return (

{title}

{description}

{examples.length > 0 && (
{examples.map((example) => ( {example} ))}
)} {action &&
{action}
}
); } 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) { event.preventDefault(); const trimmed = value.trim(); if (trimmed && !disabled) onSubmit(trimmed); } return (
{chips.length > 0 && (
{chips.map((chip) => ( ))}
)}
); } export interface SafeErrorBannerProps { error: unknown; title?: string; className?: string; } export function SafeErrorBanner({ error, title = "We could not finish that", className }: SafeErrorBannerProps) { return (

{title}

{cleanStudentError(error)}

); } 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 (

Setup

{title}

{description}

{setup}
{error ? : null} {loading ? (

{loadingLabel}

Keep this open while DocDoe prepares the result.

) : ( children ?? empty )} {footer}
); } 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 (
{steps.map((step) => { const status = step.status ?? "waiting"; return (
{status === "complete" ? ( ) : status === "current" ? ( ) : status === "error" ? ( ) : ( )}

{step.label}

{step.description &&

{step.description}

}
); })}
); }