refactor: quick panel and inputbar tools (#10142)

* refactor: add QuickPanelReservedSymbol

* refactor: pass assistant id instead of assistant object

* refactor: simplify InputbarTools props

* refactor: add root symbol

* refactor: merge file panel to attachment button

* refactor: extract ActionButton for reuse

* chore: update CLAUDE.md

* fix: colors

* refactor: add more reserved symbols

* fix: keep updateAssistant stable

* refactor(components): replace styled-components with cn utility in ActionIconButton

- Replace styled-components with cn utility from @heroui/react for better maintainability
- Add new --icon and --color-icon CSS variables for consistent icon coloring
- Simplify button styling using Tailwind CSS classes

---------

Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
one 2025-09-16 11:10:14 +08:00 committed by GitHub
parent 5451e2f34a
commit 1a5138c5b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 421 additions and 458 deletions

View File

@ -9,6 +9,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Prerequisites**: Node.js v22.x.x or higher, Yarn 4.9.1
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.9.1 --activate`
- **Install Dependencies**: `yarn install`
- **Add New Dependencies**: `yarn add -D` for renderer-specific dependencies, `yarn add` for others.
### Development

View File

@ -53,6 +53,7 @@
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--icon: #00000099;
}
.dark {
@ -87,6 +88,7 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--icon: #ffffff99;
}
@theme inline {
@ -128,6 +130,7 @@
--color-sidebar-ring: var(--sidebar-ring);
--animate-marquee: marquee var(--duration) infinite linear;
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
--color-icon: var(--icon);
@keyframes marquee {
from {
transform: translateX(0);

View File

@ -0,0 +1,30 @@
import { cn } from '@heroui/react'
import { Button, ButtonProps } from 'antd'
import React, { memo } from 'react'
interface ActionIconButtonProps extends ButtonProps {
children: React.ReactNode
active?: boolean
}
/**
* A simple action button rendered as an icon
*/
const ActionIconButton: React.FC<ActionIconButtonProps> = ({ children, active = false, className, ...props }) => {
return (
<Button
type="text"
shape="circle"
className={cn(
'flex h-[30px] w-[30px] cursor-pointer flex-row items-center justify-center border-none p-0 text-base transition-all duration-300 ease-in-out [&_.anticon]:text-icon [&_.icon-a-addchat]:mb-[-2px] [&_.icon-a-addchat]:text-lg [&_.icon]:text-icon [&_.iconfont]:text-icon [&_.lucide]:text-icon',
active &&
'[&_.anticon]:text-primary! [&_.icon]:text-primary! [&_.iconfont]:text-primary! [&_.lucide]:text-primary!',
className
)}
{...props}>
{children}
</Button>
)
}
export default memo(ActionIconButton)

View File

@ -0,0 +1 @@
export { default as ActionIconButton } from './ActionIconButton'

View File

@ -1,4 +1,4 @@
import { ToolbarButton } from '@renderer/pages/home/Inputbar/Inputbar'
import { ActionIconButton } from '@renderer/components/Buttons'
import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
import { Tooltip } from 'antd'
import { debounce } from 'lodash'
@ -364,23 +364,23 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
<ToolBar>
{showUserToggle && (
<Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={userOutlinedButtonOnClick}>
<ActionIconButton onClick={userOutlinedButtonOnClick}>
<User size={18} style={{ color: includeUser ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)}
<Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}>
<ActionIconButton onClick={caseSensitiveButtonOnClick}>
<CaseSensitive
size={18}
style={{ color: isCaseSensitive ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</ActionIconButton>
</Tooltip>
<Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={wholeWordButtonOnClick}>
<ActionIconButton onClick={wholeWordButtonOnClick}>
<WholeWord size={18} style={{ color: isWholeWord ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
</ToolBar>
</InputWrapper>
@ -397,15 +397,15 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
)}
</SearchResults>
<ToolBar>
<ToolbarButton type="text" onClick={prevButtonOnClick} disabled={allRanges.length === 0}>
<ActionIconButton onClick={prevButtonOnClick} disabled={allRanges.length === 0}>
<ChevronUp size={18} />
</ToolbarButton>
<ToolbarButton type="text" onClick={nextButtonOnClick} disabled={allRanges.length === 0}>
</ActionIconButton>
<ActionIconButton onClick={nextButtonOnClick} disabled={allRanges.length === 0}>
<ChevronDown size={18} />
</ToolbarButton>
<ToolbarButton type="text" onClick={closeButtonOnClick}>
</ActionIconButton>
<ActionIconButton onClick={closeButtonOnClick}>
<X size={18} />
</ToolbarButton>
</ActionIconButton>
</ToolBar>
</SearchBarContainer>
</NarrowLayout>

View File

@ -1,5 +1,18 @@
import React from 'react'
export enum QuickPanelReservedSymbol {
Root = '/',
File = 'file',
KnowledgeBase = '#',
MentionModels = '@',
QuickPhrases = 'quick-phrases',
Thinking = 'thinking',
WebSearch = '?',
Mcp = 'mcp',
McpPrompt = 'mcp-prompt',
McpResource = 'mcp-resource'
}
export type QuickPanelCloseAction = 'enter' | 'click' | 'esc' | 'outsideclick' | 'enter_empty' | string | undefined
export type QuickPanelTriggerInfo = {
type: 'input' | 'button'

View File

@ -172,7 +172,7 @@ export function useAssistant(id: string) {
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
[assistant, dispatch]
),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistant: useCallback((assistant: Partial<Assistant>) => dispatch(updateAssistant(assistant)), [dispatch]),
updateAssistantSettings
}
}

View File

@ -362,8 +362,9 @@
"translate": "Translate to {{target_language}}",
"translating": "Translating...",
"upload": {
"attachment": "Upload attachment",
"document": "Upload document file (model does not support images)",
"label": "Upload image or document file",
"image_or_document": "Upload image or document file",
"upload_from_local": "Upload local file..."
},
"url_context": "URL Context",

View File

@ -363,8 +363,9 @@
"translate": "翻译成 {{target_language}}",
"translating": "翻译中...",
"upload": {
"attachment": "上传附件",
"document": "上传文档(模型不支持图片)",
"label": "上传图片或文档",
"image_or_document": "上传图片或文档",
"upload_from_local": "上传本地文件..."
},
"url_context": "网页上下文",

View File

@ -362,8 +362,9 @@
"translate": "翻譯成 {{target_language}}",
"translating": "翻譯中...",
"upload": {
"attachment": "上傳附件",
"document": "上傳文件(模型不支援圖片)",
"label": "上傳圖片或文件",
"image_or_document": "上傳圖片或文件",
"upload_from_local": "上傳本地文件..."
},
"url_context": "網頁上下文",

View File

@ -1,12 +1,17 @@
import { FileType } from '@renderer/types'
import { filterSupportedFiles } from '@renderer/utils/file'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { FileType, KnowledgeBase, KnowledgeItem } from '@renderer/types'
import { filterSupportedFiles, formatFileSize } from '@renderer/utils/file'
import { Tooltip } from 'antd'
import { Paperclip } from 'lucide-react'
import { FC, useCallback, useImperativeHandle, useState } from 'react'
import dayjs from 'dayjs'
import { FileSearch, FileText, Paperclip, Upload } from 'lucide-react'
import { Dispatch, FC, SetStateAction, useCallback, useImperativeHandle, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
export interface AttachmentButtonRef {
openQuickPanel: () => void
openFileSelectDialog: () => void
}
interface Props {
@ -14,24 +19,17 @@ interface Props {
couldAddImageFile: boolean
extensions: string[]
files: FileType[]
setFiles: (files: FileType[]) => void
ToolbarButton: any
setFiles: Dispatch<SetStateAction<FileType[]>>
disabled?: boolean
}
const AttachmentButton: FC<Props> = ({
ref,
couldAddImageFile,
extensions,
files,
setFiles,
ToolbarButton,
disabled
}) => {
const AttachmentButton: FC<Props> = ({ ref, couldAddImageFile, extensions, files, setFiles, disabled }) => {
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const { bases: knowledgeBases } = useKnowledgeBases()
const [selecting, setSelecting] = useState<boolean>(false)
const onSelectFile = useCallback(async () => {
const openFileSelectDialog = useCallback(async () => {
if (selecting) {
return
}
@ -70,23 +68,88 @@ const AttachmentButton: FC<Props> = ({
}
}, [extensions, files, selecting, setFiles, t])
const openKnowledgeFileList = useCallback(
(base: KnowledgeBase) => {
quickPanel.open({
title: base.name,
list: base.items
.filter((file): file is KnowledgeItem => ['file'].includes(file.type))
.map((file) => {
const fileContent = file.content as FileType
return {
label: fileContent.origin_name || fileContent.name,
description:
formatFileSize(fileContent.size) + ' · ' + dayjs(fileContent.created_at).format('YYYY-MM-DD HH:mm'),
icon: <FileText />,
isSelected: files.some((f) => f.path === fileContent.path),
action: async ({ item }) => {
item.isSelected = !item.isSelected
if (fileContent.path) {
setFiles((prevFiles) => {
const fileExists = prevFiles.some((f) => f.path === fileContent.path)
if (fileExists) {
return prevFiles.filter((f) => f.path !== fileContent.path)
} else {
return fileContent ? [...prevFiles, fileContent] : prevFiles
}
})
}
}
}
}),
symbol: QuickPanelReservedSymbol.File,
multiple: true
})
},
[files, quickPanel, setFiles]
)
const items = useMemo(() => {
return [
{
label: t('chat.input.upload.upload_from_local'),
description: '',
icon: <Upload />,
action: () => openFileSelectDialog()
},
...knowledgeBases.map((base) => {
const length = base.items?.filter(
(item): item is KnowledgeItem => ['file', 'note'].includes(item.type) && typeof item.content !== 'string'
).length
return {
label: base.name,
description: `${length} ${t('files.count')}`,
icon: <FileSearch />,
disabled: length === 0,
isMenu: true,
action: () => openKnowledgeFileList(base)
}
})
]
}, [knowledgeBases, openFileSelectDialog, openKnowledgeFileList, t])
const openQuickPanel = useCallback(() => {
onSelectFile()
}, [onSelectFile])
quickPanel.open({
title: t('chat.input.upload.attachment'),
list: items,
symbol: QuickPanelReservedSymbol.File
})
}, [items, quickPanel, t])
useImperativeHandle(ref, () => ({
openQuickPanel
openQuickPanel,
openFileSelectDialog
}))
return (
<Tooltip
placement="top"
title={couldAddImageFile ? t('chat.input.upload.label') : t('chat.input.upload.document')}
title={couldAddImageFile ? t('chat.input.upload.image_or_document') : t('chat.input.upload.document')}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
<Paperclip size={18} style={{ color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }} />
</ToolbarButton>
<ActionIconButton onClick={openFileSelectDialog} active={files.length > 0} disabled={disabled}>
<Paperclip size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,3 +1,4 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import { isGenerateImageModel } from '@renderer/config/models'
import { Assistant, Model } from '@renderer/types'
import { Tooltip } from 'antd'
@ -8,11 +9,10 @@ import { useTranslation } from 'react-i18next'
interface Props {
assistant: Assistant
model: Model
ToolbarButton: any
onEnableGenerateImage: () => void
}
const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => {
const GenerateImageButton: FC<Props> = ({ model, assistant, onEnableGenerateImage }) => {
const { t } = useTranslation()
return (
@ -23,9 +23,12 @@ const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEna
}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-primary)' : 'var(--color-icon)'} />
</ToolbarButton>
<ActionIconButton
onClick={onEnableGenerateImage}
active={assistant.enableGenerateImage}
disabled={!isGenerateImageModel(model)}>
<Image size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,25 +1,23 @@
import { HolderOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelReservedSymbol, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
import TranslateButton from '@renderer/components/TranslateButton'
import {
isAutoEnableImageGenerationModel,
isGenerateImageModel,
isGenerateImageModels,
isMandatoryWebSearchModel,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
isVisionModel,
isVisionModels,
isWebSearchModel
} from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { useTimer } from '@renderer/hooks/useTimer'
import useTranslate from '@renderer/hooks/useTranslate'
@ -27,7 +25,6 @@ import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService'
import PasteService from '@renderer/services/PasteService'
import { spanManagerService } from '@renderer/services/SpanManagerService'
import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
@ -36,9 +33,9 @@ import WebSearchService from '@renderer/services/WebSearchService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime'
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
import { Assistant, FileType, KnowledgeBase, Model, Topic } from '@renderer/types'
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, filterSupportedFiles, formatFileSize } from '@renderer/utils'
import { classNames, delay, filterSupportedFiles } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats'
import {
getFilesFromDropEvent,
@ -46,14 +43,12 @@ import {
getTextFromDropEvent,
isSendMessageKeyPressed
} from '@renderer/utils/input'
import { isPromptToolUse, isSupportedToolUse } from '@renderer/utils/mcp-tools'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Button, Tooltip } from 'antd'
import { Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash'
import { CirclePause, FileSearch, FileText, Upload } from 'lucide-react'
import { CirclePause } from 'lucide-react'
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -114,7 +109,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const [textareaHeight, setTextareaHeight] = useState<number>()
const startDragY = useRef<number>(0)
const startHeight = useRef<number>(0)
const { bases: knowledgeBases } = useKnowledgeBases()
const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode)
const isVisionAssistant = useMemo(() => isVisionModel(model), [model])
const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model])
@ -134,11 +128,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
[mentionedModels, isGenerateImageAssistant]
)
// 仅允许在不含图片文件时mention非视觉模型
const couldMentionNotVisionModel = useMemo(() => {
return !files.some((file) => file.type === FileTypes.IMAGE)
}, [files])
// 允许在支持视觉或生成图片时添加图片文件
const couldAddImageFile = useMemo(() => {
return isVisionSupported || isGenerateImageSupported
@ -185,8 +174,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const inputTokenCount = showInputEstimatedTokens ? tokenCount : 0
const newTopicShortcut = useShortcutDisplay('new_topic')
const cleanTopicShortcut = useShortcutDisplay('clear_topic')
const inputEmpty = isEmpty(text.trim()) && files.length === 0
_text = text
@ -279,72 +266,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
}, [isTranslating, text, getLanguageByLangcode, targetLanguage, setTimeoutTimer, resizeTextArea])
const openKnowledgeFileList = useCallback(
(base: KnowledgeBase) => {
quickPanel.open({
title: base.name,
list: base.items
.filter((file): file is KnowledgeItem => ['file'].includes(file.type))
.map((file) => {
const fileContent = file.content as FileType
return {
label: fileContent.origin_name || fileContent.name,
description:
formatFileSize(fileContent.size) + ' · ' + dayjs(fileContent.created_at).format('YYYY-MM-DD HH:mm'),
icon: <FileText />,
isSelected: files.some((f) => f.path === fileContent.path),
action: async ({ item }) => {
item.isSelected = !item.isSelected
if (fileContent.path) {
setFiles((prevFiles) => {
const fileExists = prevFiles.some((f) => f.path === fileContent.path)
if (fileExists) {
return prevFiles.filter((f) => f.path !== fileContent.path)
} else {
return fileContent ? [...prevFiles, fileContent] : prevFiles
}
})
}
}
}
}),
symbol: 'file',
multiple: true
})
},
[files, quickPanel]
)
const openSelectFileMenu = useCallback(() => {
quickPanel.open({
title: t('chat.input.upload.label'),
list: [
{
label: t('chat.input.upload.upload_from_local'),
description: '',
icon: <Upload />,
action: () => {
inputbarToolsRef.current?.openAttachmentQuickPanel()
}
},
...knowledgeBases.map((base) => {
const length = base.items?.filter(
(item): item is KnowledgeItem => ['file', 'note'].includes(item.type) && typeof item.content !== 'string'
).length
return {
label: base.name,
description: `${length} ${t('files.count')}`,
icon: <FileSearch />,
disabled: length === 0,
isMenu: true,
action: () => openKnowledgeFileList(base)
}
})
],
symbol: 'file'
})
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// 按下Tab键自动选中${xxx}
if (event.key === 'Tab' && inputFocus) {
@ -512,35 +433,31 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const lastSymbol = newText[cursorPosition - 1]
// 触发符号为 '/':若当前未打开或符号不同,则切换/打开
if (enableQuickPanelTriggers && lastSymbol === '/') {
if (quickPanel.isVisible && quickPanel.symbol !== '/') {
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.Root) {
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
quickPanel.close('switch-symbol')
}
if (!quickPanel.isVisible || quickPanel.symbol !== '/') {
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.Root) {
const quickPanelMenu =
inputbarToolsRef.current?.getQuickPanelMenu({
t,
files,
couldAddImageFile,
text: newText,
openSelectFileMenu,
translate
}) || []
quickPanel.open({
title: t('settings.quickPanel.title'),
list: quickPanelMenu,
symbol: '/'
symbol: QuickPanelReservedSymbol.Root
})
}
}
// 触发符号为 '@':若当前未打开或符号不同,则切换/打开
if (enableQuickPanelTriggers && lastSymbol === '@') {
if (quickPanel.isVisible && quickPanel.symbol !== '@') {
if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.MentionModels) {
if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
quickPanel.close('switch-symbol')
}
if (!quickPanel.isVisible || quickPanel.symbol !== '@') {
if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) {
inputbarToolsRef.current?.openMentionModelsPanel({
type: 'input',
position: cursorPosition - 1,
@ -549,7 +466,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
}
},
[enableQuickPanelTriggers, quickPanel, t, files, couldAddImageFile, openSelectFileMenu, translate]
[enableQuickPanelTriggers, quickPanel, t, translate]
)
const onPaste = useCallback(
@ -765,11 +682,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
}, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon])
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
updateAssistant({ ...assistant, knowledge_bases: bases })
setSelectedKnowledgeBases(bases ?? [])
}
const handleRemoveModel = (model: Model) => {
setMentionedModels(mentionedModels.filter((m) => m.id !== model.id))
}
@ -783,10 +695,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
setSelectedKnowledgeBases(newKnowledgeBases ?? [])
}
const onEnableGenerateImage = () => {
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
}
useEffect(() => {
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
updateAssistant({ ...assistant, enableWebSearch: false })
@ -806,24 +714,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
}, [assistant, model, updateAssistant])
const onMentionModel = useCallback(
(model: Model) => {
// 我想应该没有模型是只支持视觉而不支持文本的?
if (isVisionModel(model) || couldMentionNotVisionModel) {
setMentionedModels((prev) => {
const modelId = getModelUniqId(model)
const exists = prev.some((m) => getModelUniqId(m) === modelId)
return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model]
})
} else {
logger.error('Cannot add non-vision model when images are uploaded')
}
},
[couldMentionNotVisionModel]
)
const onClearMentionModels = useCallback(() => setMentionedModels([]), [setMentionedModels])
const onToggleExpanded = () => {
const currentlyExpanded = expanded || !!textareaHeight
const shouldExpand = !currentlyExpanded
@ -848,8 +738,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
const isExpanded = expanded || !!textareaHeight
const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)
const showMcpTools = isSupportedToolUse(assistant) || isPromptToolUse(assistant)
if (isMultiSelectMode) {
return null
@ -921,47 +809,38 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
<Toolbar>
<InputbarTools
ref={inputbarToolsRef}
assistant={assistant}
assistantId={assistant.id}
model={model}
files={files}
extensions={supportedExts}
setFiles={setFiles}
showThinkingButton={showThinkingButton}
showKnowledgeIcon={showKnowledgeIcon && showMcpTools}
showMcpTools={showMcpTools}
selectedKnowledgeBases={selectedKnowledgeBases}
handleKnowledgeBaseSelect={handleKnowledgeBaseSelect}
setText={setText}
resizeTextArea={resizeTextArea}
mentionModels={mentionedModels}
onMentionModel={onMentionModel}
onClearMentionModels={onClearMentionModels}
couldMentionNotVisionModel={couldMentionNotVisionModel}
selectedKnowledgeBases={selectedKnowledgeBases}
setSelectedKnowledgeBases={setSelectedKnowledgeBases}
mentionedModels={mentionedModels}
setMentionedModels={setMentionedModels}
couldAddImageFile={couldAddImageFile}
onEnableGenerateImage={onEnableGenerateImage}
isExpanded={isExpanded}
onToggleExpanded={onToggleExpanded}
addNewTopic={addNewTopic}
clearTopic={clearTopic}
onNewContext={onNewContext}
newTopicShortcut={newTopicShortcut}
cleanTopicShortcut={cleanTopicShortcut}
/>
<ToolbarMenu>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
<SendMessageButton sendMessage={sendMessage} disabled={inputEmpty} />
{loading && (
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2 }}>
<ActionIconButton onClick={onPause} style={{ marginRight: -2 }}>
<CirclePause size={20} color="var(--color-error)" />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)}
</ToolbarMenu>
@ -1076,45 +955,4 @@ const ToolbarMenu = styled.div`
gap: 6px;
`
export const ToolbarButton = styled(Button)`
width: 30px;
height: 30px;
font-size: 16px;
border-radius: 50%;
transition: all 0.3s ease;
color: var(--color-icon);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0;
&.anticon,
&.iconfont {
transition: all 0.3s ease;
color: var(--color-icon);
}
.icon-a-addchat {
font-size: 18px;
margin-bottom: -2px;
}
&:hover {
background-color: var(--color-background-soft);
.anticon,
.iconfont {
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-primary) !important;
.anticon,
.iconfont,
.chevron-icon {
color: var(--color-white-soft);
}
&:hover {
background-color: var(--color-primary);
}
}
`
export default Inputbar

View File

@ -1,12 +1,26 @@
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { isGeminiModel, isGenerateImageModel, isMandatoryWebSearchModel } from '@renderer/config/models'
import {
isGeminiModel,
isGenerateImageModel,
isMandatoryWebSearchModel,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
isVisionModel
} from '@renderer/config/models'
import { isSupportUrlContextProvider } from '@renderer/config/providers'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { getModelUniqId } from '@renderer/services/ModelService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools'
import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types'
import { FileType, FileTypes, KnowledgeBase, Model } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { isPromptToolUse, isSupportedToolUse } from '@renderer/utils/mcp-tools'
import { Divider, Dropdown, Tooltip } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import {
@ -32,7 +46,6 @@ import styled from 'styled-components'
import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton'
import GenerateImageButton from './GenerateImageButton'
import { ToolbarButton } from './Inputbar'
import KnowledgeBaseButton, { KnowledgeBaseButtonRef } from './KnowledgeBaseButton'
import MCPToolsButton, { MCPToolsButtonRef } from './MCPToolsButton'
import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButton'
@ -42,47 +55,33 @@ import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
import UrlContextButton, { UrlContextButtonRef } from './UrlContextbutton'
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
const logger = loggerService.withContext('InputbarTools')
export interface InputbarToolsRef {
getQuickPanelMenu: (params: {
t: (key: string, options?: any) => string
files: FileType[]
couldAddImageFile: boolean
text: string
openSelectFileMenu: () => void
translate: () => void
}) => QuickPanelListItem[]
getQuickPanelMenu: (params: { text: string; translate: () => void }) => QuickPanelListItem[]
openMentionModelsPanel: (triggerInfo?: { type: 'input' | 'button'; position?: number; originalText?: string }) => void
openAttachmentQuickPanel: () => void
}
export interface InputbarToolsProps {
assistant: Assistant
assistantId: string
model: Model
files: FileType[]
setFiles: (files: FileType[]) => void
setFiles: Dispatch<SetStateAction<FileType[]>>
extensions: string[]
showThinkingButton: boolean
showKnowledgeIcon: boolean
showMcpTools: boolean
selectedKnowledgeBases: KnowledgeBase[]
handleKnowledgeBaseSelect: (bases?: KnowledgeBase[]) => void
setText: Dispatch<SetStateAction<string>>
resizeTextArea: () => void
mentionModels: Model[]
onMentionModel: (model: Model) => void
onClearMentionModels: () => void
couldMentionNotVisionModel: boolean
selectedKnowledgeBases: KnowledgeBase[]
setSelectedKnowledgeBases: Dispatch<SetStateAction<KnowledgeBase[]>>
mentionedModels: Model[]
setMentionedModels: Dispatch<SetStateAction<Model[]>>
couldAddImageFile: boolean
onEnableGenerateImage: () => void
isExpanded: boolean
onToggleExpanded: () => void
addNewTopic: () => void
clearTopic: () => void
onNewContext: () => void
newTopicShortcut: string
cleanTopicShortcut: string
}
interface ToolButtonConfig {
@ -100,34 +99,27 @@ const DraggablePortal = ({ children, isDragging }) => {
const InputbarTools = ({
ref,
assistant,
assistantId,
model,
files,
setFiles,
showThinkingButton,
showKnowledgeIcon,
showMcpTools,
selectedKnowledgeBases,
handleKnowledgeBaseSelect,
setText,
resizeTextArea,
mentionModels,
onMentionModel,
onClearMentionModels,
couldMentionNotVisionModel,
selectedKnowledgeBases,
setSelectedKnowledgeBases,
mentionedModels,
setMentionedModels,
couldAddImageFile,
onEnableGenerateImage,
isExpanded: isExpended,
onToggleExpanded: onToggleExpended,
addNewTopic,
clearTopic,
onNewContext,
newTopicShortcut,
cleanTopicShortcut,
extensions
}: InputbarToolsProps & { ref?: React.RefObject<InputbarToolsRef | null> }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { assistant, updateAssistant } = useAssistant(assistantId)
const quickPhrasesButtonRef = useRef<QuickPhrasesButtonRef>(null)
const mentionModelsButtonRef = useRef<MentionModelsButtonRef>(null)
@ -143,6 +135,54 @@ const InputbarTools = ({
const [targetTool, setTargetTool] = useState<ToolButtonConfig | null>(null)
const showThinkingButton = useMemo(
() => isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model),
[model]
)
const showMcpServerButton = useMemo(() => isSupportedToolUse(assistant) || isPromptToolUse(assistant), [assistant])
const knowledgeSidebarEnabled = useSidebarIconShow('knowledge')
const showKnowledgeBaseButton = knowledgeSidebarEnabled && showMcpServerButton
const handleKnowledgeBaseSelect = useCallback(
(bases?: KnowledgeBase[]) => {
updateAssistant({ knowledge_bases: bases })
setSelectedKnowledgeBases(bases ?? [])
},
[setSelectedKnowledgeBases, updateAssistant]
)
// 仅允许在不含图片文件时mention非视觉模型
const couldMentionNotVisionModel = useMemo(() => {
return !files.some((file) => file.type === FileTypes.IMAGE)
}, [files])
const onMentionModel = useCallback(
(model: Model) => {
// 我想应该没有模型是只支持视觉而不支持文本的?
if (isVisionModel(model) || couldMentionNotVisionModel) {
setMentionedModels((prev) => {
const modelId = getModelUniqId(model)
const exists = prev.some((m) => getModelUniqId(m) === modelId)
return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model]
})
} else {
logger.error('Cannot add non-vision model when images are uploaded')
}
},
[couldMentionNotVisionModel, setMentionedModels]
)
const onClearMentionModels = useCallback(() => setMentionedModels([]), [setMentionedModels])
const onEnableGenerateImage = useCallback(() => {
updateAssistant({ enableGenerateImage: !assistant.enableGenerateImage })
}, [assistant.enableGenerateImage, updateAssistant])
const newTopicShortcut = useShortcutDisplay('new_topic')
const clearTopicShortcut = useShortcutDisplay('clear_topic')
const toggleToolVisibility = useCallback(
(toolKey: string, isVisible: boolean | undefined) => {
const newToolOrder = {
@ -164,15 +204,8 @@ const InputbarTools = ({
[dispatch, toolOrder.hidden, toolOrder.visible]
)
const getQuickPanelMenuImpl = (params: {
t: (key: string, options?: any) => string
files: FileType[]
couldAddImageFile: boolean
text: string
openSelectFileMenu: () => void
translate: () => void
}): QuickPanelListItem[] => {
const { t, files, couldAddImageFile, text, openSelectFileMenu, translate } = params
const getQuickPanelMenuImpl = (params: { text: string; translate: () => void }): QuickPanelListItem[] => {
const { text, translate } = params
return [
{
@ -249,11 +282,13 @@ const InputbarTools = ({
}
},
{
label: couldAddImageFile ? t('chat.input.upload.label') : t('chat.input.upload.document'),
label: couldAddImageFile ? t('chat.input.upload.attachment') : t('chat.input.upload.document'),
description: '',
icon: <Paperclip />,
isMenu: true,
action: openSelectFileMenu
action: () => {
attachmentButtonRef.current?.openQuickPanel()
}
},
{
label: t('translate.title'),
@ -313,15 +348,15 @@ const InputbarTools = ({
title={t('chat.input.new_topic', { Command: newTopicShortcut })}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<ActionIconButton onClick={addNewTopic}>
<MessageSquareDiff size={19} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)
},
{
key: 'attachment',
label: t('chat.input.upload.label'),
label: t('chat.input.upload.image_or_document'),
component: (
<AttachmentButton
ref={attachmentButtonRef}
@ -329,28 +364,25 @@ const InputbarTools = ({
extensions={extensions}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
/>
)
},
{
key: 'thinking',
label: t('chat.input.thinking.label'),
component: (
<ThinkingButton ref={thinkingButtonRef} model={model} assistant={assistant} ToolbarButton={ToolbarButton} />
),
component: <ThinkingButton ref={thinkingButtonRef} model={model} assistantId={assistant.id} />,
condition: showThinkingButton
},
{
key: 'web_search',
label: t('chat.input.web_search.label'),
component: <WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />,
component: <WebSearchButton ref={webSearchButtonRef} assistantId={assistant.id} />,
condition: !isMandatoryWebSearchModel(model)
},
{
key: 'url_context',
label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />,
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
condition: isGeminiModel(model) && isSupportUrlContextProvider(getProviderByModel(model))
},
{
@ -361,36 +393,29 @@ const InputbarTools = ({
ref={knowledgeBaseButtonRef}
selectedBases={selectedKnowledgeBases}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
),
condition: showKnowledgeIcon
condition: showKnowledgeBaseButton
},
{
key: 'mcp_tools',
label: t('settings.mcp.title'),
component: (
<MCPToolsButton
assistant={assistant}
assistantId={assistant.id}
ref={mcpToolsButtonRef}
ToolbarButton={ToolbarButton}
setInputValue={setText}
resizeTextArea={resizeTextArea}
/>
),
condition: showMcpTools
condition: showMcpServerButton
},
{
key: 'generate_image',
label: t('chat.input.generate_image'),
component: (
<GenerateImageButton
model={model}
assistant={assistant}
onEnableGenerateImage={onEnableGenerateImage}
ToolbarButton={ToolbarButton}
/>
<GenerateImageButton model={model} assistant={assistant} onEnableGenerateImage={onEnableGenerateImage} />
),
condition: isGenerateImageModel(model)
},
@ -400,10 +425,9 @@ const InputbarTools = ({
component: (
<MentionModelsButton
ref={mentionModelsButtonRef}
mentionedModels={mentionModels}
mentionedModels={mentionedModels}
onMentionModel={onMentionModel}
onClearMentionModels={onClearMentionModels}
ToolbarButton={ToolbarButton}
couldMentionNotVisionModel={couldMentionNotVisionModel}
files={files}
setText={setText}
@ -418,8 +442,7 @@ const InputbarTools = ({
ref={quickPhrasesButtonRef}
setInputValue={setText}
resizeTextArea={resizeTextArea}
ToolbarButton={ToolbarButton}
assistantObj={assistant}
assistantId={assistant.id}
/>
)
},
@ -429,12 +452,12 @@ const InputbarTools = ({
component: (
<Tooltip
placement="top"
title={t('chat.input.clear.label', { Command: cleanTopicShortcut })}
title={t('chat.input.clear.label', { Command: clearTopicShortcut })}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={clearTopic}>
<ActionIconButton onClick={clearTopic}>
<PaintbrushVertical size={18} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)
},
@ -447,22 +470,22 @@ const InputbarTools = ({
title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
<ActionIconButton onClick={onToggleExpended}>
{isExpended ? <Minimize size={18} /> : <Maximize size={18} />}
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)
},
{
key: 'new_context',
label: t('chat.input.new.context', { Command: '' }),
component: <NewContextButton onNewContext={onNewContext} ToolbarButton={ToolbarButton} />
component: <NewContextButton onNewContext={onNewContext} />
}
]
}, [
addNewTopic,
assistant,
cleanTopicShortcut,
clearTopicShortcut,
clearTopic,
couldAddImageFile,
couldMentionNotVisionModel,
@ -470,7 +493,7 @@ const InputbarTools = ({
files,
handleKnowledgeBaseSelect,
isExpended,
mentionModels,
mentionedModels,
model,
newTopicShortcut,
onClearMentionModels,
@ -482,8 +505,8 @@ const InputbarTools = ({
selectedKnowledgeBases,
setFiles,
setText,
showKnowledgeIcon,
showMcpTools,
showKnowledgeBaseButton,
showMcpServerButton,
showThinkingButton,
t
])
@ -628,14 +651,14 @@ const InputbarTools = ({
placement="top"
title={isCollapse ? t('chat.input.tools.expand') : t('chat.input.tools.collapse')}
arrow>
<ToolbarButton type="text" onClick={() => dispatch(setIsCollapsed(!isCollapse))}>
<ActionIconButton onClick={() => dispatch(setIsCollapsed(!isCollapse))}>
<CircleChevronRight
size={18}
style={{
transform: isCollapse ? 'scaleX(1)' : 'scaleX(-1)'
}}
/>
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)}
</ToolsContainer>

View File

@ -1,4 +1,5 @@
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { useAppSelector } from '@renderer/store'
import { KnowledgeBase } from '@renderer/types'
import { Tooltip } from 'antd'
@ -16,10 +17,9 @@ interface Props {
selectedBases?: KnowledgeBase[]
onSelect: (bases: KnowledgeBase[]) => void
disabled?: boolean
ToolbarButton: any
}
const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled, ToolbarButton }) => {
const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled }) => {
const { t } = useTranslation()
const navigate = useNavigate()
const quickPanel = useQuickPanel()
@ -77,7 +77,7 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
quickPanel.open({
title: t('chat.input.knowledge_base'),
list: baseItems,
symbol: '#',
symbol: QuickPanelReservedSymbol.KnowledgeBase,
multiple: true,
afterAction({ item }) {
item.isSelected = !item.isSelected
@ -86,7 +86,7 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
}, [baseItems, quickPanel, t])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '#') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.KnowledgeBase) {
quickPanel.close()
} else {
openQuickPanel()
@ -95,7 +95,7 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
// 监听 selectedBases 变化,动态更新已打开的 QuickPanel 列表状态
useEffect(() => {
if (quickPanel.isVisible && quickPanel.symbol === '#') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.KnowledgeBase) {
// 直接使用重新计算的 baseItems因为它已经包含了最新的 isSelected 状态
quickPanel.updateList(baseItems)
}
@ -107,12 +107,12 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
return (
<Tooltip placement="top" title={t('chat.input.knowledge_base')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel} disabled={disabled}>
<FileSearch
size={18}
color={selectedBases && selectedBases.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'}
/>
</ToolbarButton>
<ActionIconButton
onClick={handleOpenQuickPanel}
active={selectedBases && selectedBases.length > 0}
disabled={disabled}>
<FileSearch size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,4 +1,5 @@
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { isGeminiModel } from '@renderer/config/models'
import { isGeminiWebSearchProvider, isSupportUrlContextProvider } from '@renderer/config/providers'
import { useAssistant } from '@renderer/hooks/useAssistant'
@ -6,7 +7,7 @@ import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useTimer } from '@renderer/hooks/useTimer'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { EventEmitter } from '@renderer/services/EventService'
import { Assistant, MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
import { MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { Form, Input, Tooltip } from 'antd'
import { CircleX, Hammer, Plus } from 'lucide-react'
@ -21,11 +22,10 @@ export interface MCPToolsButtonRef {
}
interface Props {
assistant: Assistant
assistantId: string
ref?: React.RefObject<MCPToolsButtonRef | null>
setInputValue: React.Dispatch<React.SetStateAction<string>>
resizeTextArea: () => void
ToolbarButton: any
}
// 添加类型定义
@ -113,14 +113,14 @@ const extractPromptContent = (response: any): string | null => {
return null
}
const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, ToolbarButton, ...props }) => {
const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, assistantId }) => {
const { activedMcpServers } = useMCPServers()
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const navigate = useNavigate()
const [form] = Form.useForm()
const { updateAssistant, assistant } = useAssistant(props.assistant.id)
const { assistant, updateAssistant } = useAssistant(assistantId)
const model = assistant.model
const { setTimeoutTimer } = useTimer()
@ -228,7 +228,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
quickPanel.open({
title: t('settings.mcp.title'),
list: menuItems,
symbol: 'mcp',
symbol: QuickPanelReservedSymbol.Mcp,
multiple: true,
afterAction({ item }) {
item.isSelected = !item.isSelected
@ -377,7 +377,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
quickPanel.open({
title: t('settings.mcp.title'),
list: prompts,
symbol: 'mcp-prompt',
symbol: QuickPanelReservedSymbol.McpPrompt,
multiple: true
})
}, [promptList, quickPanel, t])
@ -465,13 +465,13 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
quickPanel.open({
title: t('settings.mcp.title'),
list: resourcesList,
symbol: 'mcp-resource',
symbol: QuickPanelReservedSymbol.McpResource,
multiple: true
})
}, [resourcesList, quickPanel, t])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.Mcp) {
quickPanel.close()
} else {
openQuickPanel()
@ -486,12 +486,9 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
return (
<Tooltip placement="top" title={t('settings.mcp.title')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<Hammer
size={18}
color={assistant.mcpServers && assistant.mcpServers.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'}
/>
</ToolbarButton>
<ActionIconButton onClick={handleOpenQuickPanel} active={assistant.mcpServers && assistant.mcpServers.length > 0}>
<Hammer size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,6 +1,6 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
import { useQuickPanel } from '@renderer/components/QuickPanel'
import { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
import { type QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { getModelLogo, isEmbeddingModel, isRerankModel, isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
@ -27,7 +27,6 @@ interface Props {
onClearMentionModels: () => void
couldMentionNotVisionModel: boolean
files: FileType[]
ToolbarButton: any
setText: React.Dispatch<React.SetStateAction<string>>
}
@ -38,7 +37,6 @@ const MentionModelsButton: FC<Props> = ({
onClearMentionModels,
couldMentionNotVisionModel,
files,
ToolbarButton,
setText
}) => {
const { providers } = useProviders()
@ -242,7 +240,7 @@ const MentionModelsButton: FC<Props> = ({
quickPanel.open({
title: t('agents.edit.model.select.title'),
list: modelItems,
symbol: '@',
symbol: QuickPanelReservedSymbol.MentionModels,
multiple: true,
triggerInfo: triggerInfo || { type: 'button' },
afterAction({ item }) {
@ -274,7 +272,7 @@ const MentionModelsButton: FC<Props> = ({
)
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '@') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.MentionModels) {
quickPanel.close()
} else {
openQuickPanel({ type: 'button' })
@ -286,7 +284,7 @@ const MentionModelsButton: FC<Props> = ({
useEffect(() => {
// 检查files是否变化
if (filesRef.current !== files) {
if (quickPanel.isVisible && quickPanel.symbol === '@') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.MentionModels) {
quickPanel.close()
}
filesRef.current = files
@ -295,7 +293,7 @@ const MentionModelsButton: FC<Props> = ({
// 监听 mentionedModels 变化,动态更新已打开的 QuickPanel 列表状态
useEffect(() => {
if (quickPanel.isVisible && quickPanel.symbol === '@') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.MentionModels) {
// 直接使用重新计算的 modelItems因为它已经包含了最新的 isSelected 状态
quickPanel.updateList(modelItems)
}
@ -307,9 +305,9 @@ const MentionModelsButton: FC<Props> = ({
return (
<Tooltip placement="top" title={t('agents.edit.model.select.title')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<AtSign size={18} color={mentionedModels.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'} />
</ToolbarButton>
<ActionIconButton onClick={handleOpenQuickPanel} active={mentionedModels.length > 0}>
<AtSign size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,15 +1,14 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { Tooltip } from 'antd'
import { Eraser } from 'lucide-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
onNewContext: () => void
ToolbarButton: any
}
const NewContextButton: FC<Props> = ({ onNewContext, ToolbarButton }) => {
const NewContextButton: FC<Props> = ({ onNewContext }) => {
const newContextShortcut = useShortcutDisplay('toggle_new_context')
const { t } = useTranslation()
@ -21,9 +20,9 @@ const NewContextButton: FC<Props> = ({ onNewContext, ToolbarButton }) => {
title={t('chat.input.new.context', { Command: newContextShortcut })}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={onNewContext}>
<ActionIconButton onClick={onNewContext}>
<Eraser size={18} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,11 +1,14 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import {
type QuickPanelListItem,
type QuickPanelOpenOptions,
QuickPanelReservedSymbol
} from '@renderer/components/QuickPanel'
import { useQuickPanel } from '@renderer/components/QuickPanel'
import { QuickPanelListItem, QuickPanelOpenOptions } from '@renderer/components/QuickPanel/types'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useTimer } from '@renderer/hooks/useTimer'
import QuickPhraseService from '@renderer/services/QuickPhraseService'
import { useAppSelector } from '@renderer/store'
import { QuickPhrase } from '@renderer/types'
import { Assistant } from '@renderer/types'
import { Input, Modal, Radio, Space, Tooltip } from 'antd'
import { BotMessageSquare, Plus, Zap } from 'lucide-react'
import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
@ -20,21 +23,16 @@ interface Props {
ref?: React.RefObject<QuickPhrasesButtonRef | null>
setInputValue: React.Dispatch<React.SetStateAction<string>>
resizeTextArea: () => void
ToolbarButton: any
assistantObj: Assistant
assistantId: string
}
const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton, assistantObj }: Props) => {
const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, assistantId }: Props) => {
const [quickPhrasesList, setQuickPhrasesList] = useState<QuickPhrase[]>([])
const [isModalOpen, setIsModalOpen] = useState(false)
const [formData, setFormData] = useState({ title: '', content: '', location: 'global' })
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const activeAssistantId = useAppSelector(
(state) =>
state.assistants.assistants.find((a) => a.id === assistantObj.id)?.id || state.assistants.defaultAssistant.id
)
const { assistant, updateAssistant } = useAssistant(activeAssistantId)
const { assistant, updateAssistant } = useAssistant(assistantId)
const { setTimeoutTimer } = useTimer()
const loadQuickListPhrases = useCallback(
@ -135,7 +133,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
() => ({
title: t('settings.quickPhrase.title'),
list: phraseItems,
symbol: 'quick-phrases'
symbol: QuickPanelReservedSymbol.QuickPhrases
}),
[phraseItems, t]
)
@ -145,7 +143,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
}, [quickPanel, quickPanelOpenOptions])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === 'quick-phrases') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.QuickPhrases) {
quickPanel.close()
} else {
openQuickPanel()
@ -159,9 +157,9 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
return (
<>
<Tooltip placement="top" title={t('settings.quickPhrase.title')} mouseLeaveDelay={0} arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<ActionIconButton onClick={handleOpenQuickPanel}>
<Zap size={18} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
<Modal

View File

@ -1,3 +1,4 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import {
MdiLightbulbAutoOutline,
MdiLightbulbOffOutline,
@ -6,11 +7,11 @@ import {
MdiLightbulbOn50,
MdiLightbulbOn80
} from '@renderer/components/Icons/SVGIcon'
import { useQuickPanel } from '@renderer/components/QuickPanel'
import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { getThinkModelType, isDoubaoThinkingAutoModel, MODEL_SUPPORTED_OPTIONS } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { getReasoningEffortOptionsLabel } from '@renderer/i18n/label'
import { Assistant, Model, ThinkingOption } from '@renderer/types'
import { Model, ThinkingOption } from '@renderer/types'
import { Tooltip } from 'antd'
import { FC, ReactElement, useCallback, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -22,14 +23,13 @@ export interface ThinkingButtonRef {
interface Props {
ref?: React.RefObject<ThinkingButtonRef | null>
model: Model
assistant: Assistant
ToolbarButton: any
assistantId: string
}
const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): ReactElement => {
const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement => {
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const { updateAssistantSettings } = useAssistant(assistant.id)
const { assistant, updateAssistantSettings } = useAssistant(assistantId)
const currentReasoningEffort = useMemo(() => {
return assistant.settings?.reasoning_effort || 'off'
@ -49,27 +49,6 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
return MODEL_SUPPORTED_OPTIONS[modelType]
}, [model, modelType])
const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => {
const iconColor = isActive ? 'var(--color-primary)' : 'var(--color-icon)'
switch (true) {
case option === 'minimal':
return <MdiLightbulbOn30 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
case option === 'low':
return <MdiLightbulbOn50 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
case option === 'medium':
return <MdiLightbulbOn80 width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
case option === 'high':
return <MdiLightbulbOn width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
case option === 'auto':
return <MdiLightbulbAutoOutline width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
case option === 'off':
return <MdiLightbulbOffOutline width={18} height={18} style={{ color: iconColor, marginTop: -2 }} />
default:
return <MdiLightbulbOffOutline width={18} height={18} style={{ color: iconColor }} />
}
}, [])
const onThinkingChange = useCallback(
(option?: ThinkingOption) => {
const isEnabled = option !== undefined && option !== 'off'
@ -98,11 +77,11 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
level: option,
label: getReasoningEffortOptionsLabel(option),
description: '',
icon: createThinkingIcon(option),
icon: ThinkingIcon(option),
isSelected: currentReasoningEffort === option,
action: () => onThinkingChange(option)
}))
}, [createThinkingIcon, currentReasoningEffort, supportedOptions, onThinkingChange])
}, [currentReasoningEffort, supportedOptions, onThinkingChange])
const isThinkingEnabled = currentReasoningEffort !== undefined && currentReasoningEffort !== 'off'
@ -114,12 +93,12 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
quickPanel.open({
title: t('assistants.settings.reasoning_effort.label'),
list: panelItems,
symbol: 'thinking'
symbol: QuickPanelReservedSymbol.Thinking
})
}, [quickPanel, panelItems, t])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === 'thinking') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.Thinking) {
quickPanel.close()
return
}
@ -131,12 +110,6 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
openQuickPanel()
}, [openQuickPanel, quickPanel, isThinkingEnabled, supportedOptions, disableThinking])
// 获取当前应显示的图标
const getThinkingIcon = useCallback(() => {
// 不再判断选项是否支持,依赖 useAssistant 更新选项为支持选项的行为
return createThinkingIcon(currentReasoningEffort, currentReasoningEffort !== 'off')
}, [createThinkingIcon, currentReasoningEffort])
useImperativeHandle(ref, () => ({
openQuickPanel
}))
@ -151,11 +124,41 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
{getThinkingIcon()}
</ToolbarButton>
<ActionIconButton onClick={handleOpenQuickPanel} active={currentReasoningEffort !== 'off'}>
{ThinkingIcon(currentReasoningEffort)}
</ActionIconButton>
</Tooltip>
)
}
const ThinkingIcon = (option?: ThinkingOption) => {
let IconComponent: React.FC<React.SVGProps<SVGSVGElement>> | null = null
switch (option) {
case 'minimal':
IconComponent = MdiLightbulbOn30
break
case 'low':
IconComponent = MdiLightbulbOn50
break
case 'medium':
IconComponent = MdiLightbulbOn80
break
case 'high':
IconComponent = MdiLightbulbOn
break
case 'auto':
IconComponent = MdiLightbulbAutoOutline
break
case 'off':
IconComponent = MdiLightbulbOffOutline
break
default:
IconComponent = MdiLightbulbOffOutline
break
}
return <IconComponent className="icon" width={18} height={18} style={{ marginTop: -2 }} />
}
export default ThinkingButton

View File

@ -11,7 +11,6 @@ type Props = {
estimateTokenCount: number
inputTokenCount: number
contextCount: { current: number; max: number }
ToolbarButton: any
} & React.HTMLAttributes<HTMLDivElement>
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount }) => {

View File

@ -1,6 +1,6 @@
import { ActionIconButton } from '@renderer/components/Buttons'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useTimer } from '@renderer/hooks/useTimer'
import { Assistant } from '@renderer/types'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { Tooltip } from 'antd'
import { Link } from 'lucide-react'
@ -13,13 +13,12 @@ export interface UrlContextButtonRef {
interface Props {
ref?: React.RefObject<UrlContextButtonRef | null>
assistant: Assistant
ToolbarButton: any
assistantId: string
}
const UrlContextButton: FC<Props> = ({ assistant, ToolbarButton }) => {
const UrlContextButton: FC<Props> = ({ assistantId }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const { assistant, updateAssistant } = useAssistant(assistantId)
const { setTimeoutTimer } = useTimer()
const urlContentNewState = !assistant.enableUrlContext
@ -48,14 +47,9 @@ const UrlContextButton: FC<Props> = ({ assistant, ToolbarButton }) => {
return (
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
<ToolbarButton type="text" onClick={handleToggle}>
<Link
size={18}
style={{
color: assistant.enableUrlContext ? 'var(--color-primary)' : 'var(--color-icon)'
}}
/>
</ToolbarButton>
<ActionIconButton onClick={handleToggle} active={assistant.enableUrlContext}>
<Link size={18} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,7 +1,8 @@
import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
import { isGeminiModel, isWebSearchModel } from '@renderer/config/models'
import { isGeminiWebSearchProvider } from '@renderer/config/providers'
import { useAssistant } from '@renderer/hooks/useAssistant'
@ -9,7 +10,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import { getProviderByModel } from '@renderer/services/AssistantService'
import WebSearchService from '@renderer/services/WebSearchService'
import { Assistant, WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { Tooltip } from 'antd'
@ -23,17 +24,16 @@ export interface WebSearchButtonRef {
interface Props {
ref?: React.RefObject<WebSearchButtonRef | null>
assistant: Assistant
ToolbarButton: any
assistantId: string
}
const logger = loggerService.withContext('WebSearchButton')
const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const WebSearchButton: FC<Props> = ({ ref, assistantId }) => {
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const { providers } = useWebSearchProviders()
const { updateAssistant } = useAssistant(assistant.id)
const { assistant, updateAssistant } = useAssistant(assistantId)
const { setTimeoutTimer } = useTimer()
// 注意assistant.enableWebSearch 有不同的语义
@ -44,24 +44,24 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
({ pid, size = 18, color }: { pid?: WebSearchProviderId; size?: number; color?: string }) => {
switch (pid) {
case 'bocha':
return <BochaLogo width={size} height={size} color={color} />
return <BochaLogo className="icon" width={size} height={size} color={color} />
case 'exa':
// size微调视觉上和其他图标平衡一些
return <ExaLogo width={size - 2} height={size} color={color} />
return <ExaLogo className="icon" width={size - 2} height={size} color={color} />
case 'tavily':
return <TavilyLogo width={size} height={size} color={color} />
return <TavilyLogo className="icon" width={size} height={size} color={color} />
case 'zhipu':
return <ZhipuLogo width={size} height={size} color={color} />
return <ZhipuLogo className="icon" width={size} height={size} color={color} />
case 'searxng':
return <SearXNGLogo width={size} height={size} color={color} />
return <SearXNGLogo className="icon" width={size} height={size} color={color} />
case 'local-baidu':
return <BaiduOutlined size={size} style={{ color, fontSize: size }} />
case 'local-bing':
return <BingLogo width={size} height={size} color={color} />
return <BingLogo className="icon" width={size} height={size} color={color} />
case 'local-google':
return <GoogleOutlined size={size} style={{ color, fontSize: size }} />
default:
return <Globe size={size} style={{ color, fontSize: size }} />
return <Globe className="icon" size={size} style={{ color, fontSize: size }} />
}
},
[]
@ -165,13 +165,13 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
quickPanel.open({
title: t('chat.input.web_search.label'),
list: providerItems,
symbol: '?',
symbol: QuickPanelReservedSymbol.WebSearch,
pageSize: 9
})
}, [quickPanel, t, providerItems])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '?') {
if (quickPanel.isVisible && quickPanel.symbol === QuickPanelReservedSymbol.WebSearch) {
quickPanel.close()
} else {
openQuickPanel()
@ -190,17 +190,15 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
openQuickPanel
}))
const color = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
return (
<Tooltip
placement="top"
title={enableWebSearch ? t('common.close') : t('chat.input.web_search.label')}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={onClick}>
<WebSearchIcon color={color} pid={assistant.webSearchProviderId} />
</ToolbarButton>
<ActionIconButton onClick={onClick} active={!!enableWebSearch}>
<WebSearchIcon pid={assistant.webSearchProviderId} />
</ActionIconButton>
</Tooltip>
)
}

View File

@ -1,4 +1,5 @@
import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons'
import CustomTag from '@renderer/components/Tags/CustomTag'
import TranslateButton from '@renderer/components/TranslateButton'
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
@ -25,7 +26,6 @@ import styled from 'styled-components'
import AttachmentButton, { AttachmentButtonRef } from '../Inputbar/AttachmentButton'
import { FileNameRender, getFileIcon } from '../Inputbar/AttachmentPreview'
import { ToolbarButton } from '../Inputbar/Inputbar'
interface Props {
message: Message
@ -346,27 +346,26 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
setFiles={setFiles}
couldAddImageFile={couldAddImageFile}
extensions={extensions}
ToolbarButton={ToolbarButton}
/>
)}
</ActionBarLeft>
<ActionBarMiddle />
<ActionBarRight>
<Tooltip title={t('common.cancel')}>
<ToolbarButton type="text" onClick={onCancel}>
<ActionIconButton onClick={onCancel}>
<X size={16} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
<Tooltip title={t('common.save')}>
<ToolbarButton type="text" onClick={handleSave}>
<ActionIconButton onClick={handleSave}>
<Save size={16} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
{message.role === 'user' && (
<Tooltip title={t('chat.resend')}>
<ToolbarButton type="text" onClick={handleResend}>
<ActionIconButton onClick={handleResend}>
<Send size={16} />
</ToolbarButton>
</ActionIconButton>
</Tooltip>
)}
</ActionBarRight>

View File

@ -46,8 +46,8 @@ const assistantsSlice = createSlice({
removeAssistant: (state, action: PayloadAction<{ id: string }>) => {
state.assistants = state.assistants.filter((c) => c.id !== action.payload.id)
},
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
updateAssistant: (state, action: PayloadAction<Partial<Assistant>>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? { ...c, ...action.payload } : c))
},
updateAssistantSettings: (
state,