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, FileSearch,
GitFork, GitFork,
ListPlus, ListPlus,
MessageSquareText,
Paperclip,
Plus, Plus,
Search, Search,
Send, Send,
@@ -33,13 +35,16 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { ImagePreviewDialog } from "@/components/ui/image-preview-dialog" import { ImagePreviewDialog } from "@/components/ui/image-preview-dialog"
import { cn, randomUUID } from "@/lib/utils" import { cn, randomUUID } from "@/lib/utils"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts" import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings" import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { readFileBase64 } from "@/lib/api" import { readFileBase64, quickMessagesList } from "@/lib/api"
import { openFileDialog } from "@/lib/platform" import { openFileDialog } from "@/lib/platform"
import { disposeTauriListener } from "@/lib/tauri-listener" import { disposeTauriListener } from "@/lib/tauri-listener"
import type { import type {
@@ -50,6 +55,7 @@ import type {
PromptCapabilitiesInfo, PromptCapabilitiesInfo,
PromptDraft, PromptDraft,
PromptInputBlock, PromptInputBlock,
QuickMessage,
SessionConfigOptionInfo, SessionConfigOptionInfo,
SessionModeInfo, SessionModeInfo,
} from "@/lib/types" } from "@/lib/types"
@@ -409,6 +415,8 @@ export function MessageInput({
}) })
const [attachments, setAttachments] = useState<InputAttachment[]>([]) const [attachments, setAttachments] = useState<InputAttachment[]>([])
const [isDragActive, setIsDragActive] = useState(false) const [isDragActive, setIsDragActive] = useState(false)
const [quickMessages, setQuickMessages] = useState<QuickMessage[]>([])
const [quickMessagesLoading, setQuickMessagesLoading] = useState(false)
const [previewAttachmentId, setPreviewAttachmentId] = useState<string | null>( const [previewAttachmentId, setPreviewAttachmentId] = useState<string | null>(
null null
) )
@@ -1299,6 +1307,47 @@ export function MessageInput({
} }
}, [appendResourceAttachments, defaultPath, disabled]) }, [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(() => { useEffect(() => {
if (!attachmentTabId) return 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="@container flex shrink-0 items-end justify-between gap-2 px-2 pb-2">
<div className="flex min-w-0 items-end gap-2"> <div className="flex min-w-0 items-end gap-2">
<DropdownMenu onOpenChange={handleAddMenuOpenChange}>
<DropdownMenuTrigger asChild>
<Button <Button
onClick={handlePickFiles}
disabled={disabled} disabled={disabled}
variant="outline" variant="outline"
size="icon" size="icon"
className="h-6 w-6 shrink-0 bg-transparent" className="h-6 w-6 shrink-0 bg-transparent"
title={t("attachFiles")} title={t("addActions")}
aria-label={t("addActions")}
> >
<Plus className="size-4" /> <Plus className="size-4" />
</Button> </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> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button

View File

@@ -1569,6 +1569,11 @@
"askAnything": "اسأل أي شيء...", "askAnything": "اسأل أي شيء...",
"removeAttachmentAria": "إزالة {name}", "removeAttachmentAria": "إزالة {name}",
"attachFiles": "إرفاق ملفات", "attachFiles": "إرفاق ملفات",
"addActions": "إضافة",
"quickMessages": "الرسائل السريعة",
"quickMessagesEmpty": "لا توجد رسائل سريعة بعد",
"quickMessagesLoading": "جارٍ التحميل...",
"quickMessageUntitled": "بدون عنوان",
"dropFilesToAttach": "أسقط الملفات لإرفاقها", "dropFilesToAttach": "أسقط الملفات لإرفاقها",
"loadingSettings": "جارٍ تحميل الإعدادات...", "loadingSettings": "جارٍ تحميل الإعدادات...",
"loadingMode": "جارٍ تحميل الوضع...", "loadingMode": "جارٍ تحميل الوضع...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "Fragen Sie alles...", "askAnything": "Fragen Sie alles...",
"removeAttachmentAria": "{name} entfernen", "removeAttachmentAria": "{name} entfernen",
"attachFiles": "Dateien anhängen", "attachFiles": "Dateien anhängen",
"addActions": "Hinzufügen",
"quickMessages": "Schnellnachrichten",
"quickMessagesEmpty": "Noch keine Schnellnachrichten",
"quickMessagesLoading": "Wird geladen...",
"quickMessageUntitled": "Ohne Titel",
"dropFilesToAttach": "Dateien zum Anhängen ablegen", "dropFilesToAttach": "Dateien zum Anhängen ablegen",
"loadingSettings": "Einstellungen werden geladen...", "loadingSettings": "Einstellungen werden geladen...",
"loadingMode": "Modus wird geladen...", "loadingMode": "Modus wird geladen...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "Ask anything...", "askAnything": "Ask anything...",
"removeAttachmentAria": "Remove {name}", "removeAttachmentAria": "Remove {name}",
"attachFiles": "Attach files", "attachFiles": "Attach files",
"addActions": "Add",
"quickMessages": "Quick messages",
"quickMessagesEmpty": "No quick messages yet",
"quickMessagesLoading": "Loading...",
"quickMessageUntitled": "Untitled",
"dropFilesToAttach": "Drop files to attach", "dropFilesToAttach": "Drop files to attach",
"loadingSettings": "Loading settings...", "loadingSettings": "Loading settings...",
"loadingMode": "Loading mode...", "loadingMode": "Loading mode...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "Pregunta lo que sea...", "askAnything": "Pregunta lo que sea...",
"removeAttachmentAria": "Quitar {name}", "removeAttachmentAria": "Quitar {name}",
"attachFiles": "Adjuntar archivos", "attachFiles": "Adjuntar archivos",
"addActions": "Añadir",
"quickMessages": "Mensajes rápidos",
"quickMessagesEmpty": "Aún no hay mensajes rápidos",
"quickMessagesLoading": "Cargando...",
"quickMessageUntitled": "Sin título",
"dropFilesToAttach": "Suelta archivos para adjuntar", "dropFilesToAttach": "Suelta archivos para adjuntar",
"loadingSettings": "Cargando ajustes...", "loadingSettings": "Cargando ajustes...",
"loadingMode": "Cargando modo...", "loadingMode": "Cargando modo...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "Posez n'importe quelle question...", "askAnything": "Posez n'importe quelle question...",
"removeAttachmentAria": "Retirer {name}", "removeAttachmentAria": "Retirer {name}",
"attachFiles": "Joindre des fichiers", "attachFiles": "Joindre des fichiers",
"addActions": "Ajouter",
"quickMessages": "Messages rapides",
"quickMessagesEmpty": "Aucun message rapide pour l'instant",
"quickMessagesLoading": "Chargement...",
"quickMessageUntitled": "Sans titre",
"dropFilesToAttach": "Déposez des fichiers à joindre", "dropFilesToAttach": "Déposez des fichiers à joindre",
"loadingSettings": "Chargement des paramètres...", "loadingSettings": "Chargement des paramètres...",
"loadingMode": "Chargement du mode...", "loadingMode": "Chargement du mode...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "何でも質問してください...", "askAnything": "何でも質問してください...",
"removeAttachmentAria": "{name} を削除", "removeAttachmentAria": "{name} を削除",
"attachFiles": "ファイルを添付", "attachFiles": "ファイルを添付",
"addActions": "追加",
"quickMessages": "クイックメッセージ",
"quickMessagesEmpty": "クイックメッセージはありません",
"quickMessagesLoading": "読み込み中...",
"quickMessageUntitled": "無題",
"dropFilesToAttach": "ファイルをドロップして添付", "dropFilesToAttach": "ファイルをドロップして添付",
"loadingSettings": "設定を読み込み中...", "loadingSettings": "設定を読み込み中...",
"loadingMode": "モードを読み込み中...", "loadingMode": "モードを読み込み中...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "무엇이든 물어보세요...", "askAnything": "무엇이든 물어보세요...",
"removeAttachmentAria": "{name} 제거", "removeAttachmentAria": "{name} 제거",
"attachFiles": "파일 첨부", "attachFiles": "파일 첨부",
"addActions": "추가",
"quickMessages": "빠른 메시지",
"quickMessagesEmpty": "빠른 메시지가 없습니다",
"quickMessagesLoading": "불러오는 중...",
"quickMessageUntitled": "제목 없음",
"dropFilesToAttach": "파일을 놓아 첨부", "dropFilesToAttach": "파일을 놓아 첨부",
"loadingSettings": "설정 불러오는 중...", "loadingSettings": "설정 불러오는 중...",
"loadingMode": "모드 불러오는 중...", "loadingMode": "모드 불러오는 중...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "Pergunte qualquer coisa...", "askAnything": "Pergunte qualquer coisa...",
"removeAttachmentAria": "Remover {name}", "removeAttachmentAria": "Remover {name}",
"attachFiles": "Anexar arquivos", "attachFiles": "Anexar arquivos",
"addActions": "Adicionar",
"quickMessages": "Mensagens rápidas",
"quickMessagesEmpty": "Ainda não há mensagens rápidas",
"quickMessagesLoading": "Carregando...",
"quickMessageUntitled": "Sem título",
"dropFilesToAttach": "Solte arquivos para anexar", "dropFilesToAttach": "Solte arquivos para anexar",
"loadingSettings": "Carregando configurações...", "loadingSettings": "Carregando configurações...",
"loadingMode": "Carregando modo...", "loadingMode": "Carregando modo...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "请开始输入...", "askAnything": "请开始输入...",
"removeAttachmentAria": "移除 {name}", "removeAttachmentAria": "移除 {name}",
"attachFiles": "附加文件", "attachFiles": "附加文件",
"addActions": "添加",
"quickMessages": "快捷消息",
"quickMessagesEmpty": "暂无快捷消息",
"quickMessagesLoading": "加载中...",
"quickMessageUntitled": "未命名",
"dropFilesToAttach": "拖拽文件到此处附加", "dropFilesToAttach": "拖拽文件到此处附加",
"loadingSettings": "正在加载设置...", "loadingSettings": "正在加载设置...",
"loadingMode": "正在加载模式...", "loadingMode": "正在加载模式...",

View File

@@ -1569,6 +1569,11 @@
"askAnything": "請開始輸入...", "askAnything": "請開始輸入...",
"removeAttachmentAria": "移除 {name}", "removeAttachmentAria": "移除 {name}",
"attachFiles": "附加檔案", "attachFiles": "附加檔案",
"addActions": "新增",
"quickMessages": "快捷訊息",
"quickMessagesEmpty": "尚無快捷訊息",
"quickMessagesLoading": "載入中...",
"quickMessageUntitled": "未命名",
"dropFilesToAttach": "拖曳檔案到此處附加", "dropFilesToAttach": "拖曳檔案到此處附加",
"loadingSettings": "正在載入設定...", "loadingSettings": "正在載入設定...",
"loadingMode": "正在載入模式...", "loadingMode": "正在載入模式...",