DocDoeAI / src /components /study-chat /StudyChatShell.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
10.1 kB
"use client";
import { useEffect, useRef, useState, Suspense } from "react";
import { cn } from "@/lib/utils";
import NextLink from "next/link";
import { useSearchParams } from "next/navigation";
import { GraduationCap } from "lucide-react";
import { useAuth } from "@/lib/auth/useAuth";
import { normalizeStudyIntent } from "@/lib/study-intents";
import { MainWorkspace } from "./MainWorkspace";
import { MobileDrawerContent } from "./MobileDrawerContent";
import {
LeftWorkspaceRail,
MobileBottomNav,
MobilePanelDrawer,
SourceRail,
} from "./WorkspacePanels";
import { RightPanelTabs } from "./RightPanelTabs";
import type { MobilePanel, StudyChatShellProps } from "./types";
import { useStudyConversation } from "./useStudyConversation";
import { useStudySources } from "./useStudySources";
import { useStudyUploads } from "./useStudyUploads";
import { useWorkspaceResources } from "./useWorkspaceResources";
import { firstName } from "./utils";
import { SourceClassificationModal } from "./SourceClassificationModal";
import { MATERIAL_UPLOAD_ACCEPT, PDF_UPLOAD_ACCEPT } from "@/lib/uploads/document-upload-contract";
function StudyChatShellInner({
initialIntent = "teach_topic",
title,
subtitle,
hideLeftRail = false,
}: StudyChatShellProps) {
const searchParams = useSearchParams();
const { user } = useAuth();
const queryIntent = normalizeStudyIntent(searchParams.get("intent") ?? initialIntent);
const queryPrompt = searchParams.get("prompt");
const querySourceId = searchParams.get("sourceId");
const runOnOpen = searchParams.get("run") === "1";
const [workspaceSearch, setWorkspaceSearch] = useState("");
const [plusOpen, setPlusOpen] = useState(false);
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
const materialInputRef = useRef<HTMLInputElement | null>(null);
const paperInputRef = useRef<HTMLInputElement | null>(null);
const {
documents,
documentsLoading,
selectedSourceIds,
selectedDocuments,
sourceSearch,
setSourceSearch,
setSelectedSourceIds,
refreshSources,
toggleSource,
clearSelectedSources,
upsertAndSelectDocument,
} = useStudySources(querySourceId);
const { dashboard, videoJobs } = useWorkspaceResources();
const {
activeIntent,
chatNotice,
chatSessions,
input,
isHistoryLoading,
isLoading,
messages,
newChat,
openSession,
runAction,
startVideoRender,
setInput,
setMessages,
submit,
threadEndRef,
} = useStudyConversation({
closeMobilePanel: () => setMobilePanel(null),
closePlusMenu: () => setPlusOpen(false),
documents,
queryIntent,
queryPrompt,
querySourceId,
runOnOpen,
selectedDocuments,
selectedSourceIds,
setSelectedSourceIds,
user,
});
const {
sourceDraftMode,
setSourceDraftMode,
draftTitle,
setDraftTitle,
draftText,
setDraftText,
isSavingDraft,
isUploading,
uploadNotice,
canRetryUpload,
retryUpload,
handleMaterialFile,
handlePaperFile,
saveDraftSource,
classificationModal,
openClassificationModal,
closeClassificationModal,
handleClassificationComplete,
} = useStudyUploads({
documents,
selectedDocuments,
setMessages,
refreshSources,
upsertAndSelectDocument,
});
const studentName = firstName(user?.name);
const promptPlaceholder = "Ask anything...";
const heroTitle = title ?? "How can I help you today?";
const heroSubtitle = subtitle ?? "Ask anything, upload a readable PDF, or use the video plugin.";
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
setPlusOpen(false);
setMobilePanel(null);
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
const composerProps = {
value: input,
onChange: setInput,
onSubmit: () => void submit(),
placeholder: promptPlaceholder,
disabled: isLoading,
selectedDocuments,
onRemoveSource: toggleSource,
onMaterialFile: (file: File) => void handleMaterialFile(file),
onOpenMaterialPicker: () => materialInputRef.current?.click(),
onOpenPaperPicker: () => paperInputRef.current?.click(),
sourceDraftMode,
onSourceDraftModeChange: setSourceDraftMode,
draftTitle,
onDraftTitleChange: setDraftTitle,
draftText,
onDraftTextChange: setDraftText,
onSaveDraftSource: () => void saveDraftSource(),
isSavingDraft,
isUploading,
uploadNotice: uploadNotice || chatNotice,
onRetryUpload: canRetryUpload ? retryUpload : undefined,
plusOpen,
onPlusOpenChange: setPlusOpen,
};
const sourceRail = (
<SourceRail
documents={documents}
selectedSourceIds={selectedSourceIds}
isLoading={documentsLoading}
search={sourceSearch}
onSearchChange={setSourceSearch}
onToggleSource={toggleSource}
onUpload={() => materialInputRef.current?.click()}
onEditClassification={openClassificationModal}
/>
);
const rightPanel = (
<RightPanelTabs
documents={documents}
selectedDocuments={selectedDocuments}
messages={messages}
dashboard={dashboard}
videoJobs={videoJobs}
onAction={runAction}
onUpload={() => materialInputRef.current?.click()}
onMaterialFile={(file) => void handleMaterialFile(file)}
/>
);
return (
<section className="kb-workspace relative h-full min-h-0 overflow-hidden bg-[#030303] text-white">
<NextLink
href="/tuition"
className="absolute right-4 top-4 z-30 hidden min-h-9 items-center gap-2 rounded-xl border border-white/[0.1] bg-[#080808]/90 px-3 text-xs font-bold text-white/72 shadow-lg backdrop-blur transition hover:border-white/[0.18] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/24 lg:inline-flex"
>
<GraduationCap className="size-3.5" aria-hidden="true" />
Today&apos;s Tuition
</NextLink>
<input
id="study-material-file-input"
name="study-material-file"
ref={materialInputRef}
type="file"
accept={MATERIAL_UPLOAD_ACCEPT}
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
event.target.value = "";
if (file) void handleMaterialFile(file);
}}
/>
<input
id="study-paper-file-input"
name="study-paper-file"
ref={paperInputRef}
type="file"
accept={PDF_UPLOAD_ACCEPT}
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
event.target.value = "";
if (file) void handlePaperFile(file);
}}
/>
<div className={cn(
"relative z-10 grid h-full min-h-0",
hideLeftRail
? "grid-cols-1 xl:grid-cols-[minmax(0,1fr)_260px]"
: "grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)] xl:grid-cols-[220px_minmax(0,1fr)_260px]",
)}>
{hideLeftRail ? null : <LeftWorkspaceRail
documents={documents}
sessions={chatSessions}
selectedSourceIds={selectedSourceIds}
search={workspaceSearch}
onSearchChange={setWorkspaceSearch}
onNewChat={newChat}
onOpenSession={(sessionId) => void openSession(sessionId)}
onToggleSource={toggleSource}
onUpload={() => materialInputRef.current?.click()}
isHistoryLoading={isHistoryLoading}
/>}
<MainWorkspace
activeIntent={activeIntent}
composerProps={composerProps}
documents={documents}
heroSubtitle={heroSubtitle}
heroTitle={heroTitle}
isLoading={isLoading}
messages={messages}
onAction={runAction}
onClearSources={clearSelectedSources}
onNewChat={newChat}
onOpenProfile={() => setMobilePanel("profile")}
onOpenSources={() => setMobilePanel("sources")}
onStartRender={startVideoRender}
onToggleSource={toggleSource}
onUpload={() => materialInputRef.current?.click()}
selectedDocuments={selectedDocuments}
studentName={studentName}
threadEndRef={threadEndRef}
/>
{rightPanel}
</div>
<MobilePanelDrawer panel={mobilePanel} onClose={() => setMobilePanel(null)}>
<MobileDrawerContent
activeIntent={activeIntent}
dashboard={dashboard}
isLoading={isLoading}
messages={messages}
onAction={runAction}
panel={mobilePanel}
sourceRail={sourceRail}
userName={user?.name ?? "Student"}
videoJobs={videoJobs}
/>
</MobilePanelDrawer>
<MobileBottomNav
activePanel={mobilePanel}
onOpenPanel={(panel) => setMobilePanel(panel)}
onClosePanels={() => setMobilePanel(null)}
/>
<SourceClassificationModal
isOpen={classificationModal.isOpen}
onClose={closeClassificationModal}
documentId={classificationModal.documentId ?? ""}
documentTitle={classificationModal.documentTitle}
currentMaterialType={classificationModal.currentMaterialType}
autoDetectedType={classificationModal.autoDetectedType}
onClassificationComplete={handleClassificationComplete}
/>
</section>
);
}
export function StudyChatShell({
mode = "home",
initialIntent = "teach_topic",
title,
subtitle,
hideLeftRail = false,
}: StudyChatShellProps) {
return (
<Suspense fallback={
<div className="flex min-h-screen items-center justify-center">
<div className="text-white/50">Loading Study Chat...</div>
</div>
}>
<StudyChatShellInner
mode={mode}
initialIntent={initialIntent}
title={title}
subtitle={subtitle}
hideLeftRail={hideLeftRail}
/>
</Suspense>
);
}