feat(input): 附件引用弹窗新增搜索过滤框
- DropdownMenu 内新增 Input 搜索框,autoFocus - filterMentionCandidates 同时受 mentionQuery 和 mentionSearchText 双重过滤 - 搜索时重置高亮索引避免越界 - 上/下箭头将焦点交还给候选列表复用键盘导航 - 所有关闭路径统一重置搜索文字 - 弹窗打开时自动 refetch 最新文件列表
This commit is contained in:
parent
f3c160f103
commit
7d5e25e325
@ -70,6 +70,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { Tag } from "@/components/ui/tag";
|
import { Tag } from "@/components/ui/tag";
|
||||||
import { useReferenceFiles } from "@/core/artifacts/references";
|
import { useReferenceFiles } from "@/core/artifacts/references";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
@ -286,6 +287,7 @@ export function InputBox({
|
|||||||
const [memoryPanelOpen, setMemoryPanelOpen] = useState(false);
|
const [memoryPanelOpen, setMemoryPanelOpen] = useState(false);
|
||||||
const [references, setReferences] = useState<PromptInputReference[]>([]);
|
const [references, setReferences] = useState<PromptInputReference[]>([]);
|
||||||
const [mentionQuery, setMentionQuery] = useState("");
|
const [mentionQuery, setMentionQuery] = useState("");
|
||||||
|
const [mentionSearchText, setMentionSearchText] = useState("");
|
||||||
const [mentionOpen, setMentionOpen] = useState(false);
|
const [mentionOpen, setMentionOpen] = useState(false);
|
||||||
const [activeMentionIndex, setActiveMentionIndex] = useState(0);
|
const [activeMentionIndex, setActiveMentionIndex] = useState(0);
|
||||||
const [mentionRange, setMentionRange] = useState<{
|
const [mentionRange, setMentionRange] = useState<{
|
||||||
@ -294,7 +296,15 @@ export function InputBox({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
|
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
|
||||||
const [isInputToolsTourReady, setIsInputToolsTourReady] = 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 态下禁用收缩,始终保持展开
|
// Welcome 态下禁用收缩,始终保持展开
|
||||||
const effectiveIsFocused =
|
const effectiveIsFocused =
|
||||||
@ -478,15 +488,24 @@ export function InputBox({
|
|||||||
|
|
||||||
const filteredMentionCandidates = useMemo(() => {
|
const filteredMentionCandidates = useMemo(() => {
|
||||||
const query = mentionQuery.trim().toLowerCase();
|
const query = mentionQuery.trim().toLowerCase();
|
||||||
if (!query) {
|
const search = mentionSearchText.trim().toLowerCase();
|
||||||
return mentionCandidates;
|
let result = mentionCandidates;
|
||||||
|
if (query) {
|
||||||
|
result = result.filter((candidate) =>
|
||||||
|
`${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return mentionCandidates.filter((candidate) =>
|
if (search) {
|
||||||
`${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}`
|
result = result.filter((candidate) =>
|
||||||
.toLowerCase()
|
`${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}`
|
||||||
.includes(query),
|
.toLowerCase()
|
||||||
);
|
.includes(search),
|
||||||
}, [mentionCandidates, mentionQuery]);
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [mentionCandidates, mentionQuery, mentionSearchText]);
|
||||||
const handleModelSelect = useCallback(
|
const handleModelSelect = useCallback(
|
||||||
(model_name: string) => {
|
(model_name: string) => {
|
||||||
onContextChange?.({
|
onContextChange?.({
|
||||||
@ -594,6 +613,7 @@ export function InputBox({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setMentionQuery("");
|
setMentionQuery("");
|
||||||
|
setMentionSearchText("");
|
||||||
setMentionOpen(false);
|
setMentionOpen(false);
|
||||||
setActiveMentionIndex(0);
|
setActiveMentionIndex(0);
|
||||||
setMentionRange(null);
|
setMentionRange(null);
|
||||||
@ -634,6 +654,7 @@ export function InputBox({
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
setMentionOpen(false);
|
setMentionOpen(false);
|
||||||
setMentionQuery("");
|
setMentionQuery("");
|
||||||
|
setMentionSearchText("");
|
||||||
setActiveMentionIndex(0);
|
setActiveMentionIndex(0);
|
||||||
setMentionRange(null);
|
setMentionRange(null);
|
||||||
return;
|
return;
|
||||||
@ -683,6 +704,7 @@ export function InputBox({
|
|||||||
}
|
}
|
||||||
} else if (event.key === "Escape") {
|
} else if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setMentionSearchText("");
|
||||||
setMentionOpen(false);
|
setMentionOpen(false);
|
||||||
setMentionRange(null);
|
setMentionRange(null);
|
||||||
}
|
}
|
||||||
@ -861,6 +883,7 @@ export function InputBox({
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
setMentionOpen(open);
|
setMentionOpen(open);
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
setMentionSearchText("");
|
||||||
setMentionRange(null);
|
setMentionRange(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -888,7 +911,30 @@ export function InputBox({
|
|||||||
<DropdownMenuLabel className="p-0 text-sm text-ws-fg-primary">
|
<DropdownMenuLabel className="p-0 text-sm text-ws-fg-primary">
|
||||||
{t.inputBox.addReference}
|
{t.inputBox.addReference}
|
||||||
</DropdownMenuLabel>
|
</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">
|
<DropdownMenuGroup className="flex min-h-0 flex-col gap-[10px] px-0">
|
||||||
<ScrollArea className="h-[320px] pt-[20px]" hideScrollbar={false}>
|
<ScrollArea className="h-[320px] pt-[20px]" hideScrollbar={false}>
|
||||||
{filteredMentionCandidates.map((candidate, index) => {
|
{filteredMentionCandidates.map((candidate, index) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user