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