DocDoeAI / src /components /study-chat /SourceControls.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
5.66 kB
"use client";
import { FileText, X } from "lucide-react";
import type { ApiDocument } from "@/lib/api/types";
import { cn } from "@/lib/utils";
function formatMaterialType(materialType: string | null | undefined): string {
if (!materialType || materialType === "unknown") return "Unclassified";
return materialType.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
}
function getMaterialTypeBadgeColor(materialType: string | null | undefined): string {
if (!materialType || materialType === "unknown") {
return "border-white/12 bg-white/[0.04] text-white/46";
}
const colors: Record<string, string> = {
notes: "border-white/14 bg-white/[0.06] text-white/72",
chapter: "border-blue-300/16 bg-blue-400/[0.08] text-blue-200/80",
textbook: "border-emerald-300/16 bg-emerald-400/[0.08] text-emerald-200/80",
handwritten_notes: "border-amber-300/16 bg-amber-400/[0.08] text-amber-200/80",
question_paper: "border-rose-300/16 bg-rose-400/[0.08] text-rose-200/80",
pyq_collection: "border-orange-300/16 bg-orange-400/[0.08] text-orange-200/80",
syllabus: "border-cyan-300/16 bg-cyan-400/[0.08] text-cyan-200/80",
formula_sheet: "border-pink-300/16 bg-pink-400/[0.08] text-pink-200/80",
mixed: "border-white/14 bg-white/[0.06] text-white/72",
};
return colors[materialType] || colors.notes;
}
function compactEducationBadge(document: ApiDocument): string | null {
if (document.education_extraction_status === "extraction_failed") return "Extraction failed";
if (document.education_extraction_status === "needs_review") return "Needs review";
if (document.material_type === "syllabus" && (document.syllabus_items_count ?? 0) > 0) {
return `${document.syllabus_items_count} topics`;
}
if (document.material_type === "question_paper" && (document.pyq_questions_count ?? 0) > 0) {
return `${document.pyq_questions_count} PYQ`;
}
return null;
}
export function SourcePill({
document,
active,
onToggle,
onRemove,
}: {
document: ApiDocument;
active: boolean;
onToggle: () => void;
onRemove?: () => void;
}) {
const educationBadge = compactEducationBadge(document);
return (
<button
type="button"
onClick={onToggle}
className={cn(
"group inline-flex min-w-0 items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/22",
active
? "border-white/[0.2] bg-white text-black"
: "border-white/[0.1] bg-white/[0.035] text-white/58 hover:border-white/[0.18] hover:text-white",
)}
>
<FileText className="size-3.5 shrink-0" />
<span className="max-w-[9rem] truncate sm:max-w-[12rem]">{document.title}</span>
<span className={cn("size-1.5 rounded-full", document.status === "ready" ? "bg-current" : "bg-current opacity-45")} />
{document.material_type && document.material_type !== "unknown" && (
<span className={cn(
"rounded-full border px-1.5 py-0.5 text-[9px] font-medium",
active ? getMaterialTypeBadgeColor(document.material_type).replace("text-", "text-black/") : getMaterialTypeBadgeColor(document.material_type)
)}>
{formatMaterialType(document.material_type)}
</span>
)}
{educationBadge ? (
<span className={cn(
"rounded-full border px-1.5 py-0.5 text-[9px] font-medium",
active ? "border-black/10 bg-black/[0.04] text-black/65" : "border-white/[0.08] bg-white/[0.025] text-white/40",
)}>
{educationBadge}
</span>
) : null}
{onRemove ? (
<span
role="button"
tabIndex={-1}
aria-label={`Remove ${document.title}`}
onClick={(event) => {
event.stopPropagation();
onRemove();
}}
className="ml-0.5 rounded-full opacity-55 transition group-hover:opacity-100"
>
<X className="size-3.5" />
</span>
) : null}
</button>
);
}
export function SelectedSourcesStrip({
selectedDocuments,
documents,
onToggleSource,
onClear,
}: {
selectedDocuments: ApiDocument[];
documents: ApiDocument[];
onToggleSource: (id: string) => void;
onClear: () => void;
}) {
const previewDocuments = selectedDocuments.length > 0 ? selectedDocuments : documents.slice(0, 4);
if (previewDocuments.length === 0) return null;
return (
<div className="flex min-w-0 max-w-full flex-col items-start gap-2 sm:flex-row sm:items-center">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-white/34">
{selectedDocuments.length > 0 ? "Selected sources" : "Recent sources"}
</span>
<div className="flex w-full min-w-0 max-w-full gap-2 overflow-x-auto pb-1 [scrollbar-width:none] sm:flex-1 [&::-webkit-scrollbar]:hidden">
{previewDocuments.map((document) => (
<SourcePill
key={document.id}
document={document}
active={selectedDocuments.some((item) => item.id === document.id)}
onToggle={() => onToggleSource(document.id)}
onRemove={selectedDocuments.some((item) => item.id === document.id) ? () => onToggleSource(document.id) : undefined}
/>
))}
</div>
{selectedDocuments.length > 0 ? (
<button
type="button"
onClick={onClear}
className="rounded-full px-2.5 py-1 text-[11px] font-semibold text-white/38 transition hover:bg-white/[0.04] hover:text-white/70"
>
Clear
</button>
) : null}
</div>
);
}