hotfix: 优化一些issue反馈 (#4758)

feat(Inputbar, Settings): add backspace delete model functionality and localization updates

- Implemented a new setting to enable backspace key functionality for deleting models/attachments in the Inputbar.
- Added corresponding localization strings for English, Japanese, Russian, Chinese (Simplified and Traditional) in the i18n files.
- Updated the QuickPanelBody styling to inherit border-radius.
- Migrated the new setting to the state management for persistence.

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
Teo 2025-04-13 21:07:00 +08:00 committed by GitHub
parent 5f7d8652bc
commit de7f806bbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 106 additions and 49 deletions

View File

@ -545,6 +545,7 @@ const QuickPanelBody = styled.div`
background-color: rgba(240, 240, 240, 0.5); background-color: rgba(240, 240, 240, 0.5);
backdrop-filter: blur(35px) saturate(150%); backdrop-filter: blur(35px) saturate(150%);
z-index: -1; z-index: -1;
border-radius: inherit;
body[theme-mode='dark'] & { body[theme-mode='dark'] & {
background-color: rgba(40, 40, 40, 0.4); background-color: rgba(40, 40, 40, 0.4);

View File

@ -1132,6 +1132,7 @@
"messages.input.show_estimated_tokens": "Show estimated tokens", "messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.input.enable_quick_triggers": "Enable '/' and '@' triggers", "messages.input.enable_quick_triggers": "Enable '/' and '@' triggers",
"messages.input.enable_delete_model": "Enable the backspace key to delete models/attachments.",
"messages.markdown_rendering_input_message": "Markdown render input message", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math engine", "messages.math_engine": "Math engine",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",

View File

@ -1131,6 +1131,7 @@
"messages.input.show_estimated_tokens": "推定トークン数を表示", "messages.input.show_estimated_tokens": "推定トークン数を表示",
"messages.input.title": "入力設定", "messages.input.title": "入力設定",
"messages.input.enable_quick_triggers": "'/' と '@' を有効にしてクイックメニューを表示します。", "messages.input.enable_quick_triggers": "'/' と '@' を有効にしてクイックメニューを表示します。",
"messages.input.enable_delete_model": "バックスペースキーでモデル/添付ファイルを削除します。",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング", "messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン", "messages.math_engine": "数式エンジン",
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec", "messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",

View File

@ -1131,6 +1131,7 @@
"messages.input.show_estimated_tokens": "Показывать затраты токенов", "messages.input.show_estimated_tokens": "Показывать затраты токенов",
"messages.input.title": "Настройки ввода", "messages.input.title": "Настройки ввода",
"messages.input.enable_quick_triggers": "Включите '/' и '@', чтобы вызвать быстрое меню.", "messages.input.enable_quick_triggers": "Включите '/' и '@', чтобы вызвать быстрое меню.",
"messages.input.enable_delete_model": "Включите удаление модели/вложения с помощью клавиши Backspace",
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown", "messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
"messages.math_engine": "Математический движок", "messages.math_engine": "Математический движок",
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",

View File

@ -1132,6 +1132,7 @@
"messages.input.show_estimated_tokens": "显示预估 Token 数", "messages.input.show_estimated_tokens": "显示预估 Token 数",
"messages.input.title": "输入设置", "messages.input.title": "输入设置",
"messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单", "messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单",
"messages.input.enable_delete_model": "启用删除键删除输入的模型/附件",
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息", "messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
"messages.math_engine": "数学公式引擎", "messages.math_engine": "数学公式引擎",
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", "messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",

View File

@ -1131,6 +1131,7 @@
"messages.input.show_estimated_tokens": "顯示預估 Token 數", "messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.input.title": "輸入設定", "messages.input.title": "輸入設定",
"messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單", "messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單",
"messages.input.enable_delete_model": "啟用刪除鍵刪除模型/附件",
"messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息", "messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"messages.math_engine": "Markdown 渲染輸入訊息", "messages.math_engine": "Markdown 渲染輸入訊息",
"messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", "messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",

View File

@ -80,7 +80,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
pasteLongTextThreshold, pasteLongTextThreshold,
showInputEstimatedTokens, showInputEstimatedTokens,
autoTranslateWithSpace, autoTranslateWithSpace,
enableQuickPanelTriggers enableQuickPanelTriggers,
enableBackspaceDeleteModel
} = useSettings() } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@ -467,21 +468,12 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
return event.preventDefault() return event.preventDefault()
} }
if (event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) { if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) {
setMentionModels((prev) => prev.slice(0, -1)) setMentionModels((prev) => prev.slice(0, -1))
return event.preventDefault() return event.preventDefault()
} }
if (event.key === 'Backspace' && text.trim() === '' && selectedKnowledgeBases.length > 0) { if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && files.length > 0) {
setSelectedKnowledgeBases((prev) => {
const newSelectedKnowledgeBases = prev.slice(0, -1)
updateAssistant({ ...assistant, knowledge_bases: newSelectedKnowledgeBases })
return newSelectedKnowledgeBases
})
return event.preventDefault()
}
if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) {
setFiles((prev) => prev.slice(0, -1)) setFiles((prev) => prev.slice(0, -1))
return event.preventDefault() return event.preventDefault()
} }

View File

@ -8,11 +8,13 @@ import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Avatar, Tooltip } from 'antd' import { Avatar, Tooltip } from 'antd'
import { useLiveQuery } from 'dexie-react-hooks'
import { first, sortBy } from 'lodash' import { first, sortBy } from 'lodash'
import { AtSign } from 'lucide-react' import { AtSign } from 'lucide-react'
import { FC, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import styled from 'styled-components'
export interface MentionModelsButtonRef { export interface MentionModelsButtonRef {
openQuickPanel: () => void openQuickPanel: () => void
@ -27,47 +29,84 @@ interface Props {
const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, ToolbarButton }) => { const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, ToolbarButton }) => {
const { providers } = useProviders() const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([])
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const quickPanel = useQuickPanel() const quickPanel = useQuickPanel()
const pinnedModels = useLiveQuery(
async () => {
const setting = await db.settings.get('pinned:models')
return setting?.value || []
},
[],
[]
)
const modelItems = useMemo(() => { const modelItems = useMemo(() => {
// Get all models from providers const items: QuickPanelListItem[] = []
const allModels = providers
.filter((p) => p.models && p.models.length > 0) if (pinnedModels.length > 0) {
.flatMap((p) => const pinnedItems = providers.flatMap((p) =>
p.models p.models
.filter((m) => !isEmbeddingModel(m)) .filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
.filter((m) => !isRerankModel(m)) .filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({ .map((m) => ({
model: m, label: (
provider: p, <>
isPinned: pinnedModels.includes(getModelUniqId(m)) <ProviderName>{p.isSystem ? t(`provider.${p.id}`) : p.name}</ProviderName>
<span style={{ opacity: 0.8 }}> | {m.name}</span>
</>
),
description: <ModelTagsWithLabel model={m} showLabel={false} size={10} style={{ opacity: 0.8 }} />,
icon: (
<Avatar src={getModelLogo(m.id)} size={20}>
{first(m.name)}
</Avatar>
),
action: () => onMentionModel(m),
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
})) }))
) )
// Sort by pinned status and name if (pinnedItems.length > 0) {
const newList: QuickPanelListItem[] = sortBy(allModels, ['isPinned', 'model.name']) items.push(...sortBy(pinnedItems, ['label']))
.reverse() }
.map((item) => ({ }
label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`,
description: <ModelTagsWithLabel model={item.model} showLabel={false} size={10} style={{ opacity: 0.8 }} />, providers.forEach((p) => {
icon: ( const providerModels = p.models
<Avatar src={getModelLogo(item.model.id)} size={20}> .filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
{first(item.model.name)} .filter((m) => !pinnedModels.includes(getModelUniqId(m)))
</Avatar> .map((m) => ({
), label: (
action: () => onMentionModel(item.model), <>
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(item.model)) <ProviderName>{p.isSystem ? t(`provider.${p.id}`) : p.name}</ProviderName>
})) <span style={{ opacity: 0.8 }}> | {m.name}</span>
newList.push({ </>
),
description: <ModelTagsWithLabel model={m} showLabel={false} size={10} style={{ opacity: 0.8 }} />,
icon: (
<Avatar src={getModelLogo(m.id)} size={20}>
{first(m.name)}
</Avatar>
),
action: () => onMentionModel(m),
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
}))
if (providerModels.length > 0) {
items.push(...sortBy(providerModels, ['label']))
}
})
items.push({
label: t('settings.models.add.add_model') + '...', label: t('settings.models.add.add_model') + '...',
icon: <PlusOutlined />, icon: <PlusOutlined />,
action: () => navigate('/settings/provider'), action: () => navigate('/settings/provider'),
isSelected: false isSelected: false
}) })
return newList
return items
}, [providers, t, pinnedModels, mentionModels, onMentionModel, navigate]) }, [providers, t, pinnedModels, mentionModels, onMentionModel, navigate])
const openQuickPanel = useCallback(() => { const openQuickPanel = useCallback(() => {
@ -90,14 +129,6 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
} }
}, [openQuickPanel, quickPanel]) }, [openQuickPanel, quickPanel])
useEffect(() => {
const loadPinnedModels = async () => {
const setting = await db.settings.get('pinned:models')
setPinnedModels(setting?.value || [])
}
loadPinnedModels()
}, [])
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
openQuickPanel openQuickPanel
})) }))
@ -112,3 +143,6 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
} }
export default MentionModelsButton export default MentionModelsButton
const ProviderName = styled.span`
font-weight: 500;
`

View File

@ -27,6 +27,7 @@ import {
setCodeShowLineNumbers, setCodeShowLineNumbers,
setCodeStyle, setCodeStyle,
setCodeWrappable, setCodeWrappable,
setEnableBackspaceDeleteModel,
setEnableQuickPanelTriggers, setEnableQuickPanelTriggers,
setFontSize, setFontSize,
setMathEngine, setMathEngine,
@ -91,7 +92,8 @@ const SettingsTab: FC<Props> = (props) => {
multiModelMessageStyle, multiModelMessageStyle,
thoughtAutoCollapse, thoughtAutoCollapse,
messageNavigation, messageNavigation,
enableQuickPanelTriggers enableQuickPanelTriggers,
enableBackspaceDeleteModel
} = useSettings() } = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => { const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -608,6 +610,15 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableBackspaceDeleteModel}
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<StyledSelect <StyledSelect

View File

@ -1224,6 +1224,14 @@ const migrateConfig = {
} catch (error) { } catch (error) {
return state return state
} }
},
'97': (state: RootState) => {
try {
state.settings.enableBackspaceDeleteModel = true
return state
} catch (error) {
return state
}
} }
} }

View File

@ -113,6 +113,7 @@ export interface SettingsState {
// 隐私设置 // 隐私设置
enableDataCollection: boolean enableDataCollection: boolean
enableQuickPanelTriggers: boolean enableQuickPanelTriggers: boolean
enableBackspaceDeleteModel: boolean
exportMenuOptions: { exportMenuOptions: {
image: boolean image: boolean
markdown: boolean markdown: boolean
@ -212,6 +213,7 @@ export const initialState: SettingsState = {
showOpenedMinappsInSidebar: true, showOpenedMinappsInSidebar: true,
enableDataCollection: false, enableDataCollection: false,
enableQuickPanelTriggers: false, enableQuickPanelTriggers: false,
enableBackspaceDeleteModel: true,
exportMenuOptions: { exportMenuOptions: {
image: true, image: true,
markdown: true, markdown: true,
@ -483,6 +485,9 @@ const settingsSlice = createSlice({
}, },
setEnableQuickPanelTriggers: (state, action: PayloadAction<boolean>) => { setEnableQuickPanelTriggers: (state, action: PayloadAction<boolean>) => {
state.enableQuickPanelTriggers = action.payload state.enableQuickPanelTriggers = action.payload
},
setEnableBackspaceDeleteModel: (state, action: PayloadAction<boolean>) => {
state.enableBackspaceDeleteModel = action.payload
} }
} }
}) })
@ -570,7 +575,8 @@ export const {
setShowOpenedMinappsInSidebar, setShowOpenedMinappsInSidebar,
setEnableDataCollection, setEnableDataCollection,
setEnableQuickPanelTriggers, setEnableQuickPanelTriggers,
setExportMenuOptions setExportMenuOptions,
setEnableBackspaceDeleteModel
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer