Skip to content

Commit 3fa17ee

Browse files
refactor autocomplete to use Radix UI Dialog instead of createPortal
1 parent e88b9d0 commit 3fa17ee

2 files changed

Lines changed: 89 additions & 98 deletions

File tree

app/components/chat/Chatbox.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,16 +420,16 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
420420
<MCPDialog isOpen={isMcpPanelOpen} onClose={() => setIsMcpPanelOpen(false)} initialTab="marketplace" />
421421

422422
{/* Tool Mention Autocomplete */}
423-
{autocomplete.isOpen && (
424-
<ToolMentionAutocomplete
425-
tools={autocomplete.filteredTools}
426-
selectedIndex={autocomplete.selectedIndex}
427-
position={autocomplete.dropdownPosition}
428-
onSelect={autocomplete.handleToolSelect}
429-
onHover={autocomplete.setSelectedIndex}
430-
searchQuery={autocomplete.searchQuery}
431-
/>
432-
)}
423+
<ToolMentionAutocomplete
424+
isOpen={autocomplete.isOpen}
425+
tools={autocomplete.filteredTools}
426+
selectedIndex={autocomplete.selectedIndex}
427+
position={autocomplete.dropdownPosition}
428+
onSelect={autocomplete.handleToolSelect}
429+
onHover={autocomplete.setSelectedIndex}
430+
onClose={autocomplete.handleClose}
431+
searchQuery={autocomplete.searchQuery}
432+
/>
433433
</>
434434
);
435435
};
Lines changed: 79 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,30 @@
11
import type { ToolItem } from '~/lib/hooks/useToolMentionAutocomplete';
2-
import { createPortal } from 'react-dom';
3-
import { useEffect, useRef, useState } from 'react';
2+
import { useEffect, useRef } from 'react';
43
import { classNames } from '~/utils/classNames';
4+
import * as Dialog from '@radix-ui/react-dialog';
55

66
interface ToolMentionAutocompleteProps {
7+
isOpen: boolean;
78
tools: ToolItem[];
89
selectedIndex: number;
910
position: { x: number; y: number } | null;
1011
onSelect: (toolName: string) => void;
1112
onHover: (index: number) => void;
13+
onClose: () => void;
1214
searchQuery: string;
1315
}
1416

1517
export function ToolMentionAutocomplete({
18+
isOpen,
1619
tools,
1720
selectedIndex,
1821
position,
1922
onSelect,
2023
onHover,
24+
onClose,
2125
searchQuery,
2226
}: ToolMentionAutocompleteProps) {
2327
const dropdownRef = useRef<HTMLDivElement>(null);
24-
const [adjustedPosition, setAdjustedPosition] = useState(position);
25-
26-
useEffect(() => {
27-
if (!position || !dropdownRef.current) {
28-
setAdjustedPosition(position);
29-
30-
return;
31-
}
32-
33-
const dropdown = dropdownRef.current;
34-
const rect = dropdown.getBoundingClientRect();
35-
const viewportHeight = window.innerHeight;
36-
const viewportWidth = window.innerWidth;
37-
38-
let { x, y } = position;
39-
40-
if (y + rect.height > viewportHeight) {
41-
y = position.y - rect.height - 10;
42-
}
43-
44-
if (x + rect.width > viewportWidth) {
45-
x = viewportWidth - rect.width - 10;
46-
}
47-
48-
setAdjustedPosition({ x, y });
49-
}, [position, tools]);
5028

5129
useEffect(() => {
5230
if (dropdownRef.current && selectedIndex >= 0) {
@@ -58,7 +36,7 @@ export function ToolMentionAutocomplete({
5836
}
5937
}, [selectedIndex]);
6038

61-
if (!adjustedPosition || tools.length === 0) {
39+
if (!isOpen || !position) {
6240
return null;
6341
}
6442

@@ -80,74 +58,87 @@ export function ToolMentionAutocomplete({
8058

8159
let globalIndex = 0;
8260

83-
const content = (
84-
<div
85-
ref={dropdownRef}
86-
className="fixed z-[9999] min-w-[400px] max-w-[500px] bg-codinit-elements-bg-depth-1 border border-codinit-elements-borderColor rounded-lg shadow-lg transition-theme"
87-
style={{
88-
left: `${adjustedPosition.x}px`,
89-
top: `${adjustedPosition.y}px`,
90-
}}
91-
>
92-
<div className="max-h-[300px] overflow-y-auto p-2">
93-
{tools.length === 0 ? (
94-
<div className="py-4 text-center text-sm text-codinit-elements-textTertiary">
95-
No tools found for &quot;{searchQuery}&quot;
96-
</div>
97-
) : (
98-
serverNames.map((serverName) => {
99-
const serverTools = groupedTools[serverName];
100-
101-
return (
102-
<div key={serverName} className="mb-2">
103-
{showServerGroups && (
104-
<div className="px-3 py-2 text-xs font-medium text-codinit-elements-textSecondary">
105-
📦 {serverName}
106-
</div>
107-
)}
108-
{serverTools.map((tool) => {
109-
const currentIndex = globalIndex++;
110-
const isSelected = currentIndex === selectedIndex;
61+
return (
62+
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
63+
<Dialog.Portal>
64+
<Dialog.Content
65+
onOpenAutoFocus={(e) => e.preventDefault()}
66+
onCloseAutoFocus={(e) => e.preventDefault()}
67+
onEscapeKeyDown={onClose}
68+
onPointerDownOutside={onClose}
69+
onInteractOutside={(e) => e.preventDefault()}
70+
className="fixed z-[9999] outline-none"
71+
style={{
72+
left: `${position.x}px`,
73+
top: `${position.y}px`,
74+
}}
75+
>
76+
<div
77+
ref={dropdownRef}
78+
className="min-w-[400px] max-w-[500px] bg-codinit-elements-bg-depth-1 border border-codinit-elements-borderColor rounded-lg shadow-lg transition-theme"
79+
>
80+
<div className="max-h-[300px] overflow-y-auto p-2">
81+
{tools.length === 0 ? (
82+
<div className="py-4 text-center text-sm text-codinit-elements-textTertiary">
83+
No tools found for &quot;{searchQuery}&quot;
84+
</div>
85+
) : (
86+
serverNames.map((serverName) => {
87+
const serverTools = groupedTools[serverName];
11188

11289
return (
113-
<div
114-
key={`${serverName}-${tool.name}`}
115-
onClick={() => onSelect(tool.name)}
116-
onMouseEnter={() => onHover(currentIndex)}
117-
data-selected={isSelected}
118-
className={classNames(
119-
'cursor-pointer rounded-md px-3 py-2 mb-1 transition-colors',
120-
isSelected
121-
? 'bg-accent-500 text-white'
122-
: 'hover:bg-codinit-elements-item-backgroundDefault text-codinit-elements-textPrimary',
123-
)}
124-
>
125-
<div className="flex flex-col gap-1 w-full">
126-
<div className="flex items-center gap-2">
127-
<span className="text-base">🔧</span>
128-
<span className="font-medium text-sm">{tool.name}</span>
90+
<div key={serverName} className="mb-2">
91+
{showServerGroups && (
92+
<div className="px-3 py-2 text-xs font-medium text-codinit-elements-textSecondary">
93+
📦 {serverName}
12994
</div>
130-
{tool.description && (
95+
)}
96+
{serverTools.map((tool) => {
97+
const currentIndex = globalIndex++;
98+
const isSelected = currentIndex === selectedIndex;
99+
100+
return (
131101
<div
102+
key={`${serverName}-${tool.name}`}
103+
onClick={() => onSelect(tool.name)}
104+
onMouseEnter={() => onHover(currentIndex)}
105+
data-selected={isSelected}
132106
className={classNames(
133-
'text-xs ml-6',
134-
isSelected ? 'text-white opacity-90' : 'text-codinit-elements-textSecondary',
107+
'cursor-pointer rounded-md px-3 py-2 mb-1 transition-colors',
108+
isSelected
109+
? 'bg-accent-500 text-white'
110+
: 'hover:bg-codinit-elements-item-backgroundDefault text-codinit-elements-textPrimary',
135111
)}
136112
>
137-
{tool.description.length > 100 ? `${tool.description.slice(0, 100)}...` : tool.description}
113+
<div className="flex flex-col gap-1 w-full">
114+
<div className="flex items-center gap-2">
115+
<span className="text-base">🔧</span>
116+
<span className="font-medium text-sm">{tool.name}</span>
117+
</div>
118+
{tool.description && (
119+
<div
120+
className={classNames(
121+
'text-xs ml-6',
122+
isSelected ? 'text-white opacity-90' : 'text-codinit-elements-textSecondary',
123+
)}
124+
>
125+
{tool.description.length > 100
126+
? `${tool.description.slice(0, 100)}...`
127+
: tool.description}
128+
</div>
129+
)}
130+
</div>
138131
</div>
139-
)}
140-
</div>
132+
);
133+
})}
141134
</div>
142135
);
143-
})}
144-
</div>
145-
);
146-
})
147-
)}
148-
</div>
149-
</div>
136+
})
137+
)}
138+
</div>
139+
</div>
140+
</Dialog.Content>
141+
</Dialog.Portal>
142+
</Dialog.Root>
150143
);
151-
152-
return createPortal(content, document.body);
153144
}

0 commit comments

Comments
 (0)