feat(message-input): turn plus button into menu with attach files and quick messages
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -1569,6 +1569,11 @@
|
|||||||
"askAnything": "اسأل أي شيء...",
|
"askAnything": "اسأل أي شيء...",
|
||||||
"removeAttachmentAria": "إزالة {name}",
|
"removeAttachmentAria": "إزالة {name}",
|
||||||
"attachFiles": "إرفاق ملفات",
|
"attachFiles": "إرفاق ملفات",
|
||||||
|
"addActions": "إضافة",
|
||||||
|
"quickMessages": "الرسائل السريعة",
|
||||||
|
"quickMessagesEmpty": "لا توجد رسائل سريعة بعد",
|
||||||
|
"quickMessagesLoading": "جارٍ التحميل...",
|
||||||
|
"quickMessageUntitled": "بدون عنوان",
|
||||||
"dropFilesToAttach": "أسقط الملفات لإرفاقها",
|
"dropFilesToAttach": "أسقط الملفات لإرفاقها",
|
||||||
"loadingSettings": "جارٍ تحميل الإعدادات...",
|
"loadingSettings": "جارٍ تحميل الإعدادات...",
|
||||||
"loadingMode": "جارٍ تحميل الوضع...",
|
"loadingMode": "جارٍ تحميل الوضع...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -1569,6 +1569,11 @@
|
|||||||
"askAnything": "何でも質問してください...",
|
"askAnything": "何でも質問してください...",
|
||||||
"removeAttachmentAria": "{name} を削除",
|
"removeAttachmentAria": "{name} を削除",
|
||||||
"attachFiles": "ファイルを添付",
|
"attachFiles": "ファイルを添付",
|
||||||
|
"addActions": "追加",
|
||||||
|
"quickMessages": "クイックメッセージ",
|
||||||
|
"quickMessagesEmpty": "クイックメッセージはありません",
|
||||||
|
"quickMessagesLoading": "読み込み中...",
|
||||||
|
"quickMessageUntitled": "無題",
|
||||||
"dropFilesToAttach": "ファイルをドロップして添付",
|
"dropFilesToAttach": "ファイルをドロップして添付",
|
||||||
"loadingSettings": "設定を読み込み中...",
|
"loadingSettings": "設定を読み込み中...",
|
||||||
"loadingMode": "モードを読み込み中...",
|
"loadingMode": "モードを読み込み中...",
|
||||||
|
|||||||
@@ -1569,6 +1569,11 @@
|
|||||||
"askAnything": "무엇이든 물어보세요...",
|
"askAnything": "무엇이든 물어보세요...",
|
||||||
"removeAttachmentAria": "{name} 제거",
|
"removeAttachmentAria": "{name} 제거",
|
||||||
"attachFiles": "파일 첨부",
|
"attachFiles": "파일 첨부",
|
||||||
|
"addActions": "추가",
|
||||||
|
"quickMessages": "빠른 메시지",
|
||||||
|
"quickMessagesEmpty": "빠른 메시지가 없습니다",
|
||||||
|
"quickMessagesLoading": "불러오는 중...",
|
||||||
|
"quickMessageUntitled": "제목 없음",
|
||||||
"dropFilesToAttach": "파일을 놓아 첨부",
|
"dropFilesToAttach": "파일을 놓아 첨부",
|
||||||
"loadingSettings": "설정 불러오는 중...",
|
"loadingSettings": "설정 불러오는 중...",
|
||||||
"loadingMode": "모드 불러오는 중...",
|
"loadingMode": "모드 불러오는 중...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -1569,6 +1569,11 @@
|
|||||||
"askAnything": "请开始输入...",
|
"askAnything": "请开始输入...",
|
||||||
"removeAttachmentAria": "移除 {name}",
|
"removeAttachmentAria": "移除 {name}",
|
||||||
"attachFiles": "附加文件",
|
"attachFiles": "附加文件",
|
||||||
|
"addActions": "添加",
|
||||||
|
"quickMessages": "快捷消息",
|
||||||
|
"quickMessagesEmpty": "暂无快捷消息",
|
||||||
|
"quickMessagesLoading": "加载中...",
|
||||||
|
"quickMessageUntitled": "未命名",
|
||||||
"dropFilesToAttach": "拖拽文件到此处附加",
|
"dropFilesToAttach": "拖拽文件到此处附加",
|
||||||
"loadingSettings": "正在加载设置...",
|
"loadingSettings": "正在加载设置...",
|
||||||
"loadingMode": "正在加载模式...",
|
"loadingMode": "正在加载模式...",
|
||||||
|
|||||||
@@ -1569,6 +1569,11 @@
|
|||||||
"askAnything": "請開始輸入...",
|
"askAnything": "請開始輸入...",
|
||||||
"removeAttachmentAria": "移除 {name}",
|
"removeAttachmentAria": "移除 {name}",
|
||||||
"attachFiles": "附加檔案",
|
"attachFiles": "附加檔案",
|
||||||
|
"addActions": "新增",
|
||||||
|
"quickMessages": "快捷訊息",
|
||||||
|
"quickMessagesEmpty": "尚無快捷訊息",
|
||||||
|
"quickMessagesLoading": "載入中...",
|
||||||
|
"quickMessageUntitled": "未命名",
|
||||||
"dropFilesToAttach": "拖曳檔案到此處附加",
|
"dropFilesToAttach": "拖曳檔案到此處附加",
|
||||||
"loadingSettings": "正在載入設定...",
|
"loadingSettings": "正在載入設定...",
|
||||||
"loadingMode": "正在載入模式...",
|
"loadingMode": "正在載入模式...",
|
||||||
|
|||||||
Reference in New Issue
Block a user