feat(input): 附件引用弹窗新增搜索过滤框

- DropdownMenu 内新增 Input 搜索框,autoFocus
- filterMentionCandidates 同时受 mentionQuery 和 mentionSearchText 双重过滤
- 搜索时重置高亮索引避免越界
- 上/下箭头将焦点交还给候选列表复用键盘导航
- 所有关闭路径统一重置搜索文字
- 弹窗打开时自动 refetch 最新文件列表
This commit is contained in:
mt 2026-06-11 09:50:29 +08:00
parent f3c160f103
commit 7d5e25e325

View File

@ -70,6 +70,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Tag } from "@/components/ui/tag";
import { useReferenceFiles } from "@/core/artifacts/references";
import { urlOfArtifact } from "@/core/artifacts/utils";
@ -286,6 +287,7 @@ export function InputBox({
const [memoryPanelOpen, setMemoryPanelOpen] = useState(false);
const [references, setReferences] = useState<PromptInputReference[]>([]);
const [mentionQuery, setMentionQuery] = useState("");
const [mentionSearchText, setMentionSearchText] = useState("");
const [mentionOpen, setMentionOpen] = useState(false);
const [activeMentionIndex, setActiveMentionIndex] = useState(0);
const [mentionRange, setMentionRange] = useState<{
@ -294,7 +296,15 @@ export function InputBox({
} | null>(null);
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
const [isInputToolsTourReady, setIsInputToolsTourReady] = useState(false);
const { data: referenceFilesData } = useReferenceFiles(threadIdFromProps);
const { data: referenceFilesData, refetch: refetchReferenceFiles } =
useReferenceFiles(threadIdFromProps);
// 打开附件引用弹窗时刷新数据
useEffect(() => {
if (mentionOpen) {
refetchReferenceFiles();
}
}, [mentionOpen, refetchReferenceFiles]);
// Welcome 态下禁用收缩,始终保持展开
const effectiveIsFocused =
@ -478,15 +488,24 @@ export function InputBox({
const filteredMentionCandidates = useMemo(() => {
const query = mentionQuery.trim().toLowerCase();
if (!query) {
return mentionCandidates;
}
return mentionCandidates.filter((candidate) =>
const search = mentionSearchText.trim().toLowerCase();
let result = mentionCandidates;
if (query) {
result = result.filter((candidate) =>
`${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}`
.toLowerCase()
.includes(query),
);
}, [mentionCandidates, mentionQuery]);
}
if (search) {
result = result.filter((candidate) =>
`${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}`
.toLowerCase()
.includes(search),
);
}
return result;
}, [mentionCandidates, mentionQuery, mentionSearchText]);
const handleModelSelect = useCallback(
(model_name: string) => {
onContextChange?.({
@ -594,6 +613,7 @@ export function InputBox({
});
}
setMentionQuery("");
setMentionSearchText("");
setMentionOpen(false);
setActiveMentionIndex(0);
setMentionRange(null);
@ -634,6 +654,7 @@ export function InputBox({
if (!token) {
setMentionOpen(false);
setMentionQuery("");
setMentionSearchText("");
setActiveMentionIndex(0);
setMentionRange(null);
return;
@ -683,6 +704,7 @@ export function InputBox({
}
} else if (event.key === "Escape") {
event.preventDefault();
setMentionSearchText("");
setMentionOpen(false);
setMentionRange(null);
}
@ -861,6 +883,7 @@ export function InputBox({
onOpenChange={(open) => {
setMentionOpen(open);
if (!open) {
setMentionSearchText("");
setMentionRange(null);
}
}}
@ -888,7 +911,30 @@ export function InputBox({
<DropdownMenuLabel className="p-0 text-sm text-ws-fg-primary">
{t.inputBox.addReference}
</DropdownMenuLabel>
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
<Input
className="mt-3 h-8 text-sm"
placeholder="搜索文件..."
value={mentionSearchText}
autoFocus
onChange={(e) => {
setMentionSearchText(e.target.value);
setActiveMentionIndex(0);
}}
onKeyDown={(e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
// 将焦点交还给 dropdown让现有的键盘导航逻辑处理
const items =
document.querySelectorAll<HTMLElement>(
'[data-testid="mention-candidate-item"]',
);
if (items.length > 0) {
(items[0] as HTMLElement).focus();
}
}
}}
/>
<DropdownMenuSeparator className="mx-0 mt-3 mb-0" />
<DropdownMenuGroup className="flex min-h-0 flex-col gap-[10px] px-0">
<ScrollArea className="h-[320px] pt-[20px]" hideScrollbar={false}>
{filteredMentionCandidates.map((candidate, index) => {