refactor: streamline Composer component by removing unused state and simplifying popover handling

This commit is contained in:
Anish Sarkar 2026-04-14 01:50:37 +05:30
parent 2b40592f0b
commit 3d1a504395
3 changed files with 41 additions and 101 deletions

View file

@ -28,8 +28,7 @@ import {
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { type FC, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
agentToolsAtom,
disabledToolsAtom,
@ -339,10 +338,7 @@ const Composer: FC = () => {
const [showPromptPicker, setShowPromptPicker] = useState(false);
const [mentionQuery, setMentionQuery] = useState("");
const [actionQuery, setActionQuery] = useState("");
const [containerPos, setContainerPos] = useState({ bottom: "200px", left: "50%", top: "auto" });
const editorRef = useRef<InlineMentionEditorRef>(null);
const editorContainerRef = useRef<HTMLDivElement>(null);
const composerBoxRef = useRef<HTMLDivElement>(null);
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
const promptPickerRef = useRef<PromptPickerRef>(null);
const viewportRef = useRef<Element | null>(null);
@ -363,38 +359,13 @@ const Composer: FC = () => {
viewportRef.current = document.querySelector(".aui-thread-viewport");
}, []);
// Compute picker positions using ResizeObserver to avoid layout reads during render
useLayoutEffect(() => {
if (!editorContainerRef.current) return;
const updatePosition = () => {
if (!editorContainerRef.current) return;
const rect = editorContainerRef.current.getBoundingClientRect();
const composerRect = composerBoxRef.current?.getBoundingClientRect();
setContainerPos({
bottom: `${window.innerHeight - rect.top + 8}px`,
left: `${rect.left}px`,
top: composerRect ? `${composerRect.bottom + 8}px` : "auto",
});
};
updatePosition();
const ro = new ResizeObserver(updatePosition);
ro.observe(editorContainerRef.current);
if (composerBoxRef.current) {
ro.observe(composerBoxRef.current);
}
return () => ro.disconnect();
}, []);
const electronAPI = useElectronAPI();
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
const clipboardLoadedRef = useRef(false);
useEffect(() => {
if (!electronAPI || clipboardLoadedRef.current) return;
clipboardLoadedRef.current = true;
electronAPI.getQuickAskText().then((text) => {
electronAPI.getQuickAskText().then((text: string) => {
if (text) {
setClipboardInitialText(text);
}
@ -705,28 +676,54 @@ const Composer: FC = () => {
);
return (
<ComposerPrimitive.Root
className="aui-composer-root relative flex w-full flex-col gap-2"
style={showPromptPicker && clipboardInitialText ? { marginBottom: 220 } : undefined}
>
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col gap-2">
<ChatSessionStatus
isAiResponding={isAiResponding}
respondingToUserId={respondingToUserId}
currentUserId={currentUser?.id ?? null}
members={members ?? []}
/>
<div
ref={composerBoxRef}
className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow"
>
{showDocumentPopover && (
<div className="absolute bottom-full left-0 z-[9999] mb-2">
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={Number(search_space_id)}
onSelectionChange={handleDocumentsMention}
onDone={() => {
setShowDocumentPopover(false);
setMentionQuery("");
}}
initialSelectedDocuments={mentionedDocuments}
externalSearch={mentionQuery}
/>
</div>
)}
{showPromptPicker && (
<div
className={cn(
"absolute left-0 z-[9999]",
clipboardInitialText ? "top-full mt-2" : "bottom-full mb-2"
)}
>
<PromptPicker
ref={promptPickerRef}
onSelect={clipboardInitialText ? handleQuickAskSelect : handleActionSelect}
onDone={() => {
setShowPromptPicker(false);
setActionQuery("");
}}
externalSearch={actionQuery}
/>
</div>
)}
<div className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow">
{clipboardInitialText && (
<ClipboardChip
text={clipboardInitialText}
onDismiss={() => setClipboardInitialText(undefined)}
/>
)}
{/* Inline editor with @mention support */}
<div ref={editorContainerRef} className="aui-composer-input-wrapper px-4 pt-3 pb-6">
<div className="aui-composer-input-wrapper px-4 pt-3 pb-6">
<InlineMentionEditor
ref={editorRef}
placeholder={currentPlaceholder}
@ -741,49 +738,6 @@ const Composer: FC = () => {
className="min-h-[24px]"
/>
</div>
{/* Document picker popover (portal to body for proper z-index stacking) */}
{showDocumentPopover &&
typeof document !== "undefined" &&
createPortal(
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={Number(search_space_id)}
onSelectionChange={handleDocumentsMention}
onDone={() => {
setShowDocumentPopover(false);
setMentionQuery("");
}}
initialSelectedDocuments={mentionedDocuments}
externalSearch={mentionQuery}
containerStyle={{
bottom: containerPos.bottom,
left: containerPos.left,
}}
/>,
document.body
)}
{showPromptPicker &&
typeof document !== "undefined" &&
createPortal(
<PromptPicker
ref={promptPickerRef}
onSelect={clipboardInitialText ? handleQuickAskSelect : handleActionSelect}
onDone={() => {
setShowPromptPicker(false);
setActionQuery("");
}}
externalSearch={actionQuery}
containerStyle={{
position: "fixed",
...(clipboardInitialText
? { top: containerPos.top }
: { bottom: containerPos.bottom }),
left: containerPos.left,
zIndex: 50,
}}
/>,
document.body
)}
<ComposerAction isBlockedByOtherUser={isBlockedByOtherUser} />
<ConnectorIndicator showTrigger={false} />
<ConnectToolsBanner isThreadEmpty={isThreadEmpty} />

View file

@ -29,8 +29,6 @@ interface DocumentMentionPickerProps {
onDone: () => void;
initialSelectedDocuments?: Pick<Document, "id" | "title" | "document_type">[];
externalSearch?: string;
/** Positioning styles for the container */
containerStyle?: React.CSSProperties;
}
const PAGE_SIZE = 20;
@ -75,7 +73,6 @@ export const DocumentMentionPicker = forwardRef<
onDone,
initialSelectedDocuments = [],
externalSearch = "",
containerStyle,
},
ref
) {
@ -396,11 +393,7 @@ export const DocumentMentionPicker = forwardRef<
return (
<div
className="fixed shadow-2xl rounded-lg border border-border dark:border-white/5 overflow-hidden bg-popover dark:bg-neutral-900 flex flex-col w-[280px] sm:w-[320px] select-none"
style={{
zIndex: 9999,
...containerStyle,
}}
className="shadow-2xl rounded-lg border border-border dark:border-white/5 overflow-hidden bg-popover dark:bg-neutral-900 flex flex-col w-[280px] sm:w-[320px] select-none"
onKeyDown={handleKeyDown}
role="listbox"
tabIndex={-1}

View file

@ -28,11 +28,10 @@ interface PromptPickerProps {
onSelect: (action: { name: string; prompt: string; mode: "transform" | "explore" }) => void;
onDone: () => void;
externalSearch?: string;
containerStyle?: React.CSSProperties;
}
export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(function PromptPicker(
{ onSelect, onDone, externalSearch = "", containerStyle },
{ onSelect, onDone, externalSearch = "" },
ref
) {
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
@ -113,13 +112,7 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
);
return (
<div
className="fixed shadow-2xl rounded-lg border border-border dark:border-white/5 overflow-hidden bg-popover dark:bg-neutral-900 flex flex-col w-[280px] sm:w-[320px] select-none"
style={{
zIndex: 9999,
...containerStyle,
}}
>
<div className="shadow-2xl rounded-lg border border-border dark:border-white/5 overflow-hidden bg-popover dark:bg-neutral-900 flex flex-col w-[280px] sm:w-[320px] select-none">
<div ref={scrollContainerRef} className="max-h-[180px] sm:max-h-[280px] overflow-y-auto">
{isLoading ? (
<div className="py-1 px-2">