feat(message-input): turn plus button into menu with attach files and quick messages

This commit is contained in:
xintaofei
2026-04-24 11:07:56 +08:00
parent 61778f152b
commit 7caf730369
11 changed files with 171 additions and 11 deletions

View File

@@ -19,6 +19,8 @@ import {
FileSearch,
GitFork,
ListPlus,
MessageSquareText,
Paperclip,
Plus,
Search,
Send,
@@ -33,13 +35,16 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ImagePreviewDialog } from "@/components/ui/image-preview-dialog"
import { cn, randomUUID } from "@/lib/utils"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { readFileBase64 } from "@/lib/api"
import { readFileBase64, quickMessagesList } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { disposeTauriListener } from "@/lib/tauri-listener"
import type {
@@ -50,6 +55,7 @@ import type {
PromptCapabilitiesInfo,
PromptDraft,
PromptInputBlock,
QuickMessage,
SessionConfigOptionInfo,
SessionModeInfo,
} from "@/lib/types"
@@ -409,6 +415,8 @@ export function MessageInput({
})
const [attachments, setAttachments] = useState<InputAttachment[]>([])
const [isDragActive, setIsDragActive] = useState(false)
const [quickMessages, setQuickMessages] = useState<QuickMessage[]>([])
const [quickMessagesLoading, setQuickMessagesLoading] = useState(false)
const [previewAttachmentId, setPreviewAttachmentId] = useState<string | null>(
null
)
@@ -1299,6 +1307,47 @@ export function MessageInput({
}
}, [appendResourceAttachments, defaultPath, disabled])
const loadQuickMessages = useCallback(async () => {
setQuickMessagesLoading(true)
try {
const list = await quickMessagesList()
setQuickMessages(list)
} catch (error) {
console.error("[MessageInput] load quick messages failed:", error)
} finally {
setQuickMessagesLoading(false)
}
}, [])
const handleAddMenuOpenChange = useCallback(
(open: boolean) => {
if (!open) return
cursorPosRef.current = textareaRef.current?.selectionStart ?? null
loadQuickMessages().catch((error) => {
console.error("[MessageInput] quick messages refresh failed:", error)
})
},
[loadQuickMessages]
)
const handleQuickMessageSelect = useCallback((message: QuickMessage) => {
const insertion = message.content
if (!insertion) return
const current = textRef.current
const rawPos = cursorPosRef.current ?? current.length
const pos = Math.max(0, Math.min(rawPos, current.length))
const before = current.slice(0, pos)
const after = current.slice(pos)
setText(before + insertion + after)
requestAnimationFrame(() => {
const ta = textareaRef.current
if (!ta) return
ta.focus()
const newPos = pos + insertion.length
ta.setSelectionRange(newPos, newPos)
})
}, [])
useEffect(() => {
if (!attachmentTabId) return
@@ -1994,16 +2043,77 @@ export function MessageInput({
/>
<div className="@container flex shrink-0 items-end justify-between gap-2 px-2 pb-2">
<div className="flex min-w-0 items-end gap-2">
<Button
onClick={handlePickFiles}
disabled={disabled}
variant="outline"
size="icon"
className="h-6 w-6 shrink-0 bg-transparent"
title={t("attachFiles")}
>
<Plus className="size-4" />
</Button>
<DropdownMenu onOpenChange={handleAddMenuOpenChange}>
<DropdownMenuTrigger asChild>
<Button
disabled={disabled}
variant="outline"
size="icon"
className="h-6 w-6 shrink-0 bg-transparent"
title={t("addActions")}
aria-label={t("addActions")}
>
<Plus className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="min-w-48"
>
<DropdownMenuItem
onClick={() => {
handlePickFiles().catch((error) => {
console.error(
"[MessageInput] pick files from menu failed:",
error
)
})
}}
>
<Paperclip className="size-4" />
{t("attachFiles")}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<MessageSquareText className="size-4" />
{t("quickMessages")}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="min-w-40 overflow-y-auto"
style={{
maxHeight:
"min(32rem, var(--radix-dropdown-menu-content-available-height))",
}}
>
{quickMessagesLoading && quickMessages.length === 0 ? (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
{t("quickMessagesLoading")}
</div>
) : quickMessages.length === 0 ? (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
{t("quickMessagesEmpty")}
</div>
) : (
quickMessages.map((message) => (
<DropdownMenuItem
key={message.id}
onClick={() => handleQuickMessageSelect(message)}
>
<span className="truncate">
{message.title || (
<span className="italic text-muted-foreground">
{t("quickMessageUntitled")}
</span>
)}
</span>
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button