From 5dd13df45f2405b3f82b76f2aa3549987b03b964 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Wed, 15 Apr 2026 11:39:39 +0800 Subject: [PATCH] feat(06-04): improve @ mention trigger and anchored candidate panel - trigger mention candidates when typing @ in any input position - keep focus and input expansion after selecting a candidate - anchor candidate panel to textarea area and update e2e selector assertions --- .../src/components/workspace/input-box.tsx | 210 ++++++++++-------- frontend/tests/e2e/input-and-compose.spec.ts | 10 +- 2 files changed, 127 insertions(+), 93 deletions(-) diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 5cfed58c..a90c6b2a 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -71,6 +71,7 @@ import { useUploadedFiles } from "@/core/uploads/hooks"; import { useIframeSkill } from "@/hooks/use-iframe-skill"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { urlOfArtifact } from "@/core/artifacts/utils"; import { ModelSelector, @@ -82,12 +83,6 @@ import { ModelSelectorTrigger, } from "../ai-elements/model-selector"; import { Suggestion, Suggestions } from "../ai-elements/suggestion"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; import { ModeHoverGuide } from "./mode-hover-guide"; import { Tooltip } from "./tooltip"; @@ -113,11 +108,15 @@ function getPathTail(path: string | undefined): string { function findMentionToken(text: string, caret: number) { const uptoCaret = text.slice(0, caret); - const match = /(?:^|\s)@([^\s@]*)$/.exec(uptoCaret); - if (!match) { + const atIndex = uptoCaret.lastIndexOf("@"); + if (atIndex < 0) { return null; } - return { query: match[1] ?? "", start: match.index + match[0].indexOf("@") }; + const query = uptoCaret.slice(atIndex + 1); + if (/\s/.test(query)) { + return null; + } + return { query, start: atIndex, end: caret }; } export function InputBox({ @@ -185,6 +184,10 @@ export function InputBox({ const [mentionQuery, setMentionQuery] = useState(""); const [mentionOpen, setMentionOpen] = useState(false); const [activeMentionIndex, setActiveMentionIndex] = useState(0); + const [mentionRange, setMentionRange] = useState<{ + start: number; + end: number; + } | null>(null); const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps); // isNewThread 时禁用收缩,始终保持展开(除非已提交消息) @@ -340,17 +343,24 @@ export function InputBox({ }); const current = textInput.value ?? ""; - const token = findMentionToken(current, current.length); - if (token) { - const before = current.slice(0, token.start); - const after = current.slice(current.length); - textInput.setInput(`${before}${after}`.trimEnd()); + const range = mentionRange ?? findMentionToken(current, current.length); + if (range) { + const before = current.slice(0, range.start); + const after = current.slice(range.end); + const nextInput = `${before}${after}`; + textInput.setInput(nextInput); + requestAnimationFrame(() => { + textareaRef.current?.focus(); + textareaRef.current?.setSelectionRange(before.length, before.length); + }); } setMentionQuery(""); setMentionOpen(false); setActiveMentionIndex(0); + setMentionRange(null); + setIsFocused(true); }, - [textInput], + [mentionRange, textInput], ); const handleTextareaChange = useCallback( @@ -362,9 +372,11 @@ export function InputBox({ setMentionOpen(false); setMentionQuery(""); setActiveMentionIndex(0); + setMentionRange(null); return; } setMentionQuery(token.query); + setMentionRange({ start: token.start, end: token.end }); setMentionOpen(true); setActiveMentionIndex(0); }, @@ -408,6 +420,7 @@ export function InputBox({ } else if (event.key === "Escape") { event.preventDefault(); setMentionOpen(false); + setMentionRange(null); } }, [ @@ -494,45 +507,6 @@ export function InputBox({ }} className="relative w-full" > - {references.length > 0 && ( -