File size: 7,161 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | "use client";
import { useState } from "react";
import {
ArrowUp,
LoaderCircle,
} from "lucide-react";
import type { ApiDocument } from "@/lib/api/types";
import { cn } from "@/lib/utils";
import { ComposerPlusMenu } from "./ComposerPlusMenu";
import { ComposerSourceDraft } from "./ComposerSourceDraft";
import { SourcePill } from "./SourceControls";
import type { SourceDraftMode } from "./types";
export function StudyComposer({
value,
onChange,
onSubmit,
placeholder,
disabled,
selectedDocuments,
onRemoveSource,
onMaterialFile,
onOpenMaterialPicker,
onOpenPaperPicker,
sourceDraftMode,
onSourceDraftModeChange,
draftTitle,
onDraftTitleChange,
draftText,
onDraftTextChange,
onSaveDraftSource,
isSavingDraft,
isUploading,
uploadNotice,
onRetryUpload,
plusOpen,
onPlusOpenChange,
compact = false,
variant = "default",
}: {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
placeholder: string;
disabled: boolean;
selectedDocuments: ApiDocument[];
onRemoveSource: (id: string) => void;
onMaterialFile: (file: File) => void;
onOpenMaterialPicker: () => void;
onOpenPaperPicker: () => void;
sourceDraftMode: SourceDraftMode;
onSourceDraftModeChange: (mode: SourceDraftMode) => void;
draftTitle: string;
onDraftTitleChange: (value: string) => void;
draftText: string;
onDraftTextChange: (value: string) => void;
onSaveDraftSource: () => void;
isSavingDraft: boolean;
isUploading: boolean;
uploadNotice: string | null;
onRetryUpload?: () => void;
plusOpen: boolean;
onPlusOpenChange: (open: boolean) => void;
compact?: boolean;
variant?: "default" | "hero";
}) {
const [dragging, setDragging] = useState(false);
function handleDrop(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
setDragging(false);
const file = event.dataTransfer.files?.[0];
if (file) onMaterialFile(file);
}
return (
<div
onDragOver={(event) => {
event.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
className={cn(
"kb-composer relative rounded-[1.35rem] border border-white/[0.095] bg-[#080808]/92 p-3 shadow-[0_18px_70px_rgba(0,0,0,0.38),inset_0_1px_0_rgba(255,255,255,0.04)] transition duration-200 focus-within:border-white/[0.22] focus-within:bg-[#0b0b0c] sm:p-4",
variant === "hero" && !compact
? "kb-composer-hero border-white/[0.11] p-3.5 shadow-[0_22px_80px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.05)] sm:p-5"
: "",
compact ? "rounded-[1.15rem] p-2.5 sm:p-3" : "",
dragging
? "border-white/[0.28] bg-white/[0.07] shadow-[0_24px_90px_rgba(0,0,0,0.5)]"
: "",
)}
>
{dragging ? (
<div className="pointer-events-none absolute inset-2 z-10 grid place-items-center rounded-[1.05rem] border border-dashed border-white/[0.22] bg-black/72 text-sm font-semibold text-white/72">
Drop your study source
</div>
) : null}
{selectedDocuments.length > 0 ? (
<div className="mb-2 flex gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<span className="text-[9px] self-center px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300 border border-emerald-400/20">grounded to {selectedDocuments.length} source(s) • multi-turn</span>
{selectedDocuments.map((document) => (
<SourcePill
key={document.id}
document={document}
active
onToggle={() => onRemoveSource(document.id)}
onRemove={() => onRemoveSource(document.id)}
/>
))}
</div>
) : null}
{sourceDraftMode ? (
<ComposerSourceDraft
sourceDraftMode={sourceDraftMode}
draftTitle={draftTitle}
onDraftTitleChange={onDraftTitleChange}
draftText={draftText}
onDraftTextChange={onDraftTextChange}
onSaveDraftSource={onSaveDraftSource}
onClose={() => onSourceDraftModeChange(null)}
isSavingDraft={isSavingDraft}
/>
) : null}
<label className="sr-only" htmlFor={compact ? "docdoe-study-composer-sticky" : "docdoe-study-composer"}>
Ask DocDoe
</label>
<textarea
id={compact ? "docdoe-study-composer-sticky" : "docdoe-study-composer"}
aria-label="Ask DocDoe"
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
rows={compact ? 1 : 2}
className={cn(
"w-full resize-none border-0 bg-transparent px-1 text-[16px] leading-7 text-white outline-none placeholder:text-white/34 focus:border-0 focus:shadow-none focus:ring-0",
compact ? "min-h-[3rem]" : variant === "hero" ? "min-h-[5.5rem] sm:min-h-[6.25rem]" : "min-h-[5.15rem] sm:min-h-[5.7rem]",
)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
onSubmit();
}
}}
/>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1">
<ComposerPlusMenu
plusOpen={plusOpen}
onPlusOpenChange={onPlusOpenChange}
onOpenMaterialPicker={onOpenMaterialPicker}
onOpenPaperPicker={onOpenPaperPicker}
onSourceDraftModeChange={onSourceDraftModeChange}
/>
{uploadNotice ? (
<span className="ml-2 hidden text-xs text-white/42 sm:inline">{uploadNotice}</span>
) : null}
</div>
<div className="flex items-center gap-2">
{onRetryUpload ? (
<button
type="button"
onClick={onRetryUpload}
className="inline-flex h-9 items-center rounded-full border border-white/[0.12] px-3 text-xs font-semibold text-white/64 transition hover:border-white/24 hover:text-white"
>
Retry
</button>
) : null}
<button
type="button"
disabled={disabled || isUploading || !value.trim()}
onClick={onSubmit}
aria-label="Send message"
aria-busy={disabled || isUploading}
className="inline-flex size-10 items-center justify-center rounded-full bg-white text-black shadow-[0_8px_24px_rgba(255,255,255,0.1)] transition hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 disabled:cursor-not-allowed disabled:opacity-40"
>
{disabled || isUploading ? <LoaderCircle className="size-3.5 animate-spin" /> : <ArrowUp className="size-4" />}
</button>
</div>
</div>
{uploadNotice ? <p className="mt-1.5 text-xs leading-5 text-white/42 sm:hidden">{uploadNotice}</p> : null}
{!compact ? (
<p className="mt-2 hidden text-[11px] text-white/28 sm:block">Enter to send · Shift+Enter for a new line</p>
) : null}
</div>
);
}
|