mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-12 02:58:34 +08:00
[Feature] Add setting for Enter/Ctrl+Enter to send messages (#550)
* i18n: add translations for send shortcut setting * feat: configurable keyboard shortcut for sending messages * refactor,review: using storage key for send shortcut * Increase the width of the trigger in the settings dialog. Previously, at 160px, it hide the letter “d” from the word “Send.” * Update components/chat-input.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: from review, ctrl send support for mac * refactor: from review, reduce local storage read * fix: make send shortcut setting reactive without page refresh --------- Co-authored-by: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import { useDiagram } from "@/contexts/diagram-context"
|
|||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
|
import { STORAGE_KEYS } from "@/lib/storage"
|
||||||
import type { FlattenedModel } from "@/lib/types/model-config"
|
import type { FlattenedModel } from "@/lib/types/model-config"
|
||||||
import { extractUrlContent, type UrlData } from "@/lib/url-utils"
|
import { extractUrlContent, type UrlData } from "@/lib/url-utils"
|
||||||
import { FilePreviewList } from "./file-preview-list"
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
@@ -192,6 +193,7 @@ export function ChatInput({
|
|||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showUrlDialog, setShowUrlDialog] = useState(false)
|
const [showUrlDialog, setShowUrlDialog] = useState(false)
|
||||||
const [isExtractingUrl, setIsExtractingUrl] = useState(false)
|
const [isExtractingUrl, setIsExtractingUrl] = useState(false)
|
||||||
|
const [sendShortcut, setSendShortcut] = useState("ctrl-enter")
|
||||||
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
(status === "streaming" || status === "submitted") && !error
|
(status === "streaming" || status === "submitted") && !error
|
||||||
@@ -208,13 +210,36 @@ export function ChatInput({
|
|||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
}, [input, adjustTextareaHeight])
|
}, [input, adjustTextareaHeight])
|
||||||
|
|
||||||
|
// Load send shortcut preference from localStorage and listen for changes
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.sendShortcut)
|
||||||
|
if (stored) setSendShortcut(stored)
|
||||||
|
|
||||||
|
const handleChange = (e: CustomEvent<string>) =>
|
||||||
|
setSendShortcut(e.detail)
|
||||||
|
window.addEventListener(
|
||||||
|
"sendShortcutChange",
|
||||||
|
handleChange as EventListener,
|
||||||
|
)
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener(
|
||||||
|
"sendShortcutChange",
|
||||||
|
handleChange as EventListener,
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onChange(e)
|
onChange(e)
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
const shouldSend =
|
||||||
|
sendShortcut === "enter"
|
||||||
|
? e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey
|
||||||
|
: (e.metaKey || e.ctrlKey) && e.key === "Enter"
|
||||||
|
|
||||||
|
if (shouldSend) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const form = e.currentTarget.closest("form")
|
const form = e.currentTarget.closest("form")
|
||||||
if (form && input.trim() && !isDisabled) {
|
if (form && input.trim() && !isDisabled) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
import { STORAGE_KEYS } from "@/lib/storage"
|
||||||
|
|
||||||
// Reusable setting item component for consistent layout
|
// Reusable setting item component for consistent layout
|
||||||
function SettingItem({
|
function SettingItem({
|
||||||
@@ -103,6 +104,7 @@ function SettingsContent({
|
|||||||
() => getStoredAccessCodeRequired() ?? false,
|
() => getStoredAccessCodeRequired() ?? false,
|
||||||
)
|
)
|
||||||
const [currentLang, setCurrentLang] = useState("en")
|
const [currentLang, setCurrentLang] = useState("en")
|
||||||
|
const [sendShortcut, setSendShortcut] = useState("ctrl-enter")
|
||||||
|
|
||||||
// Proxy settings state (Electron only)
|
// Proxy settings state (Electron only)
|
||||||
const [httpProxy, setHttpProxy] = useState("")
|
const [httpProxy, setHttpProxy] = useState("")
|
||||||
@@ -155,6 +157,11 @@ function SettingsContent({
|
|||||||
// Default to true if not set
|
// Default to true if not set
|
||||||
setCloseProtection(storedCloseProtection !== "false")
|
setCloseProtection(storedCloseProtection !== "false")
|
||||||
|
|
||||||
|
const storedSendShortcut = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.sendShortcut,
|
||||||
|
)
|
||||||
|
setSendShortcut(storedSendShortcut || "ctrl-enter")
|
||||||
|
|
||||||
setError("")
|
setError("")
|
||||||
|
|
||||||
// Load proxy settings (Electron only)
|
// Load proxy settings (Electron only)
|
||||||
@@ -425,6 +432,43 @@ function SettingsContent({
|
|||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Send Shortcut */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.sendShortcut}
|
||||||
|
description={dict.settings.sendShortcutDescription}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={sendShortcut}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSendShortcut(value)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.sendShortcut,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("sendShortcutChange", {
|
||||||
|
detail: value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="send-shortcut-select"
|
||||||
|
className="w-[170px] h-9 rounded-xl"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="enter">
|
||||||
|
{dict.settings.enterToSend}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="ctrl-enter">
|
||||||
|
{dict.settings.ctrlEnterToSend}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
{/* Proxy Settings - Electron only */}
|
{/* Proxy Settings - Electron only */}
|
||||||
{typeof window !== "undefined" &&
|
{typeof window !== "undefined" &&
|
||||||
window.electronAPI?.isElectron && (
|
window.electronAPI?.isElectron && (
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
"closeProtectionDescription": "Show confirmation when leaving the page.",
|
"closeProtectionDescription": "Show confirmation when leaving the page.",
|
||||||
"diagramStyle": "Diagram Style",
|
"diagramStyle": "Diagram Style",
|
||||||
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
||||||
|
"sendShortcut": "Send Shortcut",
|
||||||
|
"sendShortcutDescription": "Choose how to send messages.",
|
||||||
|
"enterToSend": "Enter to send",
|
||||||
|
"ctrlEnterToSend": "Cmd/Ctrl+Enter to send",
|
||||||
"diagramActions": "Diagram Actions",
|
"diagramActions": "Diagram Actions",
|
||||||
"diagramActionsDescription": "Manage diagram history and exports",
|
"diagramActionsDescription": "Manage diagram history and exports",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
"closeProtectionDescription": "ページを離れる際に確認を表示します。",
|
"closeProtectionDescription": "ページを離れる際に確認を表示します。",
|
||||||
"diagramStyle": "ダイアグラムスタイル",
|
"diagramStyle": "ダイアグラムスタイル",
|
||||||
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
||||||
|
"sendShortcut": "送信ショートカット",
|
||||||
|
"sendShortcutDescription": "メッセージの送信方法を選択します。",
|
||||||
|
"enterToSend": "Enterで送信",
|
||||||
|
"ctrlEnterToSend": "Cmd/Ctrl+Enterで送信",
|
||||||
"diagramActions": "ダイアグラム操作",
|
"diagramActions": "ダイアグラム操作",
|
||||||
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
||||||
"history": "履歴",
|
"history": "履歴",
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
"closeProtectionDescription": "离开页面时显示确认。",
|
"closeProtectionDescription": "离开页面时显示确认。",
|
||||||
"diagramStyle": "图表样式",
|
"diagramStyle": "图表样式",
|
||||||
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
||||||
|
"sendShortcut": "发送快捷键",
|
||||||
|
"sendShortcutDescription": "选择发送消息的方式。",
|
||||||
|
"enterToSend": "回车发送",
|
||||||
|
"ctrlEnterToSend": "Cmd/Ctrl+回车发送",
|
||||||
"diagramActions": "图表操作",
|
"diagramActions": "图表操作",
|
||||||
"diagramActionsDescription": "管理图表历史记录和导出",
|
"diagramActionsDescription": "管理图表历史记录和导出",
|
||||||
"history": "历史记录",
|
"history": "历史记录",
|
||||||
|
|||||||
@@ -22,4 +22,7 @@ export const STORAGE_KEYS = {
|
|||||||
// Multi-model configuration
|
// Multi-model configuration
|
||||||
modelConfigs: "next-ai-draw-io-model-configs",
|
modelConfigs: "next-ai-draw-io-model-configs",
|
||||||
selectedModelId: "next-ai-draw-io-selected-model-id",
|
selectedModelId: "next-ai-draw-io-selected-model-id",
|
||||||
|
|
||||||
|
// Chat input preferences
|
||||||
|
sendShortcut: "next-ai-draw-io-send-shortcut",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
Reference in New Issue
Block a user