import { useMemo, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from 'react'; import { SPECIAL_MENTIONS, insertMention, resolveMentions, trailingMentionQuery, } from '../mentions'; import type { Attachment, Person, SendOpts } from '../types'; interface Suggestion { key: string; label: string; insert: string; } /** * The message input: draft, @mention autocomplete, and attachment staging. Shared by the main * Thread and the ThreadPane (which passes a parentInteractionId so a reply lands in the thread). */ export function Composer({ members, canUpload, upload, onSend, onTyping, parentInteractionId, placeholder = 'Type a message… @ to mention', }: { members: Person[]; canUpload: boolean; upload: (file: File) => Promise; onSend: (text: string, opts?: SendOpts) => Promise; onTyping?: () => void; parentInteractionId?: string; placeholder?: string; }) { const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [staged, setStaged] = useState(null); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const query = trailingMentionQuery(draft); const suggestions = useMemo(() => { if (query === null) return []; const q = query.toLowerCase(); const specials = SPECIAL_MENTIONS.filter((s) => s.startsWith(q)).map((s) => ({ key: `@${s}`, label: `@${s}`, insert: s })); const people = members.filter((m) => m.name.toLowerCase().includes(q)).map((m) => ({ key: m.id, label: m.name, insert: m.name })); return [...specials, ...people].slice(0, 6); }, [query, members]); const showSuggest = query !== null && suggestions.length > 0; function pick(insert: string): void { setDraft((d) => insertMention(d, insert)); } async function onPickFile(e: ChangeEvent): Promise { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; setUploading(true); try { setStaged(await upload(file)); } catch { /* host surfaces upload errors */ } finally { setUploading(false); } } async function submit(e?: FormEvent): Promise { e?.preventDefault(); const text = draft.trim(); if ((!text && !staged) || sending) return; const mentions = resolveMentions(text, members); const att = staged; setDraft(''); setStaged(null); setSending(true); try { await onSend(text, { ...(mentions.length ? { mentions } : {}), ...(att ? { attachment: att } : {}), ...(parentInteractionId ? { parentInteractionId } : {}), }); } catch { setStaged(att); } finally { setSending(false); } } function onKeyDown(e: KeyboardEvent): void { if (showSuggest && e.key === 'Enter') { e.preventDefault(); pick(suggestions[0]!.insert); } } return (
{showSuggest ? (
    {suggestions.map((s) => (
  • ))}
) : null} {staged ? (
📎 {staged.name}
) : null}
{canUpload ? ( <> ) : null} { setDraft(e.target.value); onTyping?.(); }} onKeyDown={onKeyDown} />
); }