diff --git a/CLAUDE.md b/CLAUDE.md index 5c66ab4fed..58b01fe853 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css index 4d9e0d4f00..f05b01b65c 100644 --- a/src/renderer/src/assets/styles/tailwind.css +++ b/src/renderer/src/assets/styles/tailwind.css @@ -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); diff --git a/src/renderer/src/components/Buttons/ActionIconButton.tsx b/src/renderer/src/components/Buttons/ActionIconButton.tsx new file mode 100644 index 0000000000..1448008090 --- /dev/null +++ b/src/renderer/src/components/Buttons/ActionIconButton.tsx @@ -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 = ({ children, active = false, className, ...props }) => { + return ( + + ) +} + +export default memo(ActionIconButton) diff --git a/src/renderer/src/components/Buttons/index.ts b/src/renderer/src/components/Buttons/index.ts new file mode 100644 index 0000000000..623c2f21cc --- /dev/null +++ b/src/renderer/src/components/Buttons/index.ts @@ -0,0 +1 @@ +export { default as ActionIconButton } from './ActionIconButton' diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx index 084a79a439..d322f41616 100644 --- a/src/renderer/src/components/ContentSearch.tsx +++ b/src/renderer/src/components/ContentSearch.tsx @@ -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( {showUserToggle && ( - + - + )} - + - + - + - + @@ -397,15 +397,15 @@ export const ContentSearch = React.forwardRef( )} - + - - + + - - + + - + diff --git a/src/renderer/src/components/QuickPanel/types.ts b/src/renderer/src/components/QuickPanel/types.ts index 97e072dea0..812ec153a6 100644 --- a/src/renderer/src/components/QuickPanel/types.ts +++ b/src/renderer/src/components/QuickPanel/types.ts @@ -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' diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index 258048f0b1..096c91b5a1 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -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) => dispatch(updateAssistant(assistant)), [dispatch]), updateAssistantSettings } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index c4e7d2aa72..4ac54e374a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ecf4e8c431..248dbad623 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -363,8 +363,9 @@ "translate": "翻译成 {{target_language}}", "translating": "翻译中...", "upload": { + "attachment": "上传附件", "document": "上传文档(模型不支持图片)", - "label": "上传图片或文档", + "image_or_document": "上传图片或文档", "upload_from_local": "上传本地文件..." }, "url_context": "网页上下文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 5f4118bd95..4b31ca44be 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -362,8 +362,9 @@ "translate": "翻譯成 {{target_language}}", "translating": "翻譯中...", "upload": { + "attachment": "上傳附件", "document": "上傳文件(模型不支援圖片)", - "label": "上傳圖片或文件", + "image_or_document": "上傳圖片或文件", "upload_from_local": "上傳本地文件..." }, "url_context": "網頁上下文", diff --git a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx index dd7cd8b301..df0dbf9f5a 100644 --- a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx @@ -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> disabled?: boolean } -const AttachmentButton: FC = ({ - ref, - couldAddImageFile, - extensions, - files, - setFiles, - ToolbarButton, - disabled -}) => { +const AttachmentButton: FC = ({ ref, couldAddImageFile, extensions, files, setFiles, disabled }) => { const { t } = useTranslation() + const quickPanel = useQuickPanel() + const { bases: knowledgeBases } = useKnowledgeBases() const [selecting, setSelecting] = useState(false) - const onSelectFile = useCallback(async () => { + const openFileSelectDialog = useCallback(async () => { if (selecting) { return } @@ -70,23 +68,88 @@ const AttachmentButton: FC = ({ } }, [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: , + 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: , + 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: , + 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 ( - - - + 0} disabled={disabled}> + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx b/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx index eadcea8b82..dc6553188c 100644 --- a/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx @@ -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 = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => { +const GenerateImageButton: FC = ({ model, assistant, onEnableGenerateImage }) => { const { t } = useTranslation() return ( @@ -23,9 +23,12 @@ const GenerateImageButton: FC = ({ model, ToolbarButton, assistant, onEna } mouseLeaveDelay={0} arrow> - - - + + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 14d209fb27..864ea71a21 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -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 = ({ assistant: _assistant, setActiveTopic, topic }) = const [textareaHeight, setTextareaHeight] = useState() const startDragY = useRef(0) const startHeight = useRef(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 = ({ 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 = ({ 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 = ({ 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: , - 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: , - 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: , - disabled: length === 0, - isMenu: true, - action: () => openKnowledgeFileList(base) - } - }) - ], - symbol: 'file' - }) - }, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef]) - const handleKeyDown = (event: React.KeyboardEvent) => { // 按下Tab键,自动选中${xxx} if (event.key === 'Tab' && inputFocus) { @@ -512,35 +433,31 @@ const Inputbar: FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ assistant: _assistant, setActiveTopic, topic }) = {loading && ( - + - + )} @@ -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 diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx index 82a0071ee9..36b17a8bc2 100644 --- a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -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> extensions: string[] - showThinkingButton: boolean - showKnowledgeIcon: boolean - showMcpTools: boolean - selectedKnowledgeBases: KnowledgeBase[] - handleKnowledgeBaseSelect: (bases?: KnowledgeBase[]) => void setText: Dispatch> resizeTextArea: () => void - mentionModels: Model[] - onMentionModel: (model: Model) => void - onClearMentionModels: () => void - couldMentionNotVisionModel: boolean + selectedKnowledgeBases: KnowledgeBase[] + setSelectedKnowledgeBases: Dispatch> + mentionedModels: Model[] + setMentionedModels: Dispatch> 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 }) => { const { t } = useTranslation() const dispatch = useAppDispatch() + const { assistant, updateAssistant } = useAssistant(assistantId) const quickPhrasesButtonRef = useRef(null) const mentionModelsButtonRef = useRef(null) @@ -143,6 +135,54 @@ const InputbarTools = ({ const [targetTool, setTargetTool] = useState(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: , 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> - + - + ) }, { key: 'attachment', - label: t('chat.input.upload.label'), + label: t('chat.input.upload.image_or_document'), component: ( ) }, { key: 'thinking', label: t('chat.input.thinking.label'), - component: ( - - ), + component: , condition: showThinkingButton }, { key: 'web_search', label: t('chat.input.web_search.label'), - component: , + component: , condition: !isMandatoryWebSearchModel(model) }, { key: 'url_context', label: t('chat.input.url_context'), - component: , + component: , 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: ( ), - condition: showMcpTools + condition: showMcpServerButton }, { key: 'generate_image', label: t('chat.input.generate_image'), component: ( - + ), condition: isGenerateImageModel(model) }, @@ -400,10 +425,9 @@ const InputbarTools = ({ component: ( ) }, @@ -429,12 +452,12 @@ const InputbarTools = ({ component: ( - + - + ) }, @@ -447,22 +470,22 @@ const InputbarTools = ({ title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} mouseLeaveDelay={0} arrow> - + {isExpended ? : } - + ) }, { key: 'new_context', label: t('chat.input.new.context', { Command: '' }), - component: + component: } ] }, [ 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> - dispatch(setIsCollapsed(!isCollapse))}> + dispatch(setIsCollapsed(!isCollapse))}> - + )} diff --git a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx index bdb1a45353..552017e424 100644 --- a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx @@ -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 = ({ ref, selectedBases, onSelect, disabled, ToolbarButton }) => { +const KnowledgeBaseButton: FC = ({ ref, selectedBases, onSelect, disabled }) => { const { t } = useTranslation() const navigate = useNavigate() const quickPanel = useQuickPanel() @@ -77,7 +77,7 @@ const KnowledgeBaseButton: FC = ({ 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 = ({ 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 = ({ 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 = ({ ref, selectedBases, onSelect, disabled return ( - - 0 ? 'var(--color-primary)' : 'var(--color-icon)'} - /> - + 0} + disabled={disabled}> + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index ae11cc1914..6680177b24 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -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 setInputValue: React.Dispatch> resizeTextArea: () => void - ToolbarButton: any } // 添加类型定义 @@ -113,14 +113,14 @@ const extractPromptContent = (response: any): string | null => { return null } -const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, ToolbarButton, ...props }) => { +const MCPToolsButton: FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ ref, setInputValue, resizeTextArea, Toolbar return ( - - 0 ? 'var(--color-primary)' : 'var(--color-icon)'} - /> - + 0}> + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx index b252de981d..ceaa748bf5 100644 --- a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx @@ -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> } @@ -38,7 +37,6 @@ const MentionModelsButton: FC = ({ onClearMentionModels, couldMentionNotVisionModel, files, - ToolbarButton, setText }) => { const { providers } = useProviders() @@ -242,7 +240,7 @@ const MentionModelsButton: FC = ({ 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 = ({ ) 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 = ({ 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 = ({ // 监听 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 = ({ return ( - - 0 ? 'var(--color-primary)' : 'var(--color-icon)'} /> - + 0}> + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx b/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx index 26c9941cff..7cf9237a20 100644 --- a/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx @@ -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 = ({ onNewContext, ToolbarButton }) => { +const NewContextButton: FC = ({ onNewContext }) => { const newContextShortcut = useShortcutDisplay('toggle_new_context') const { t } = useTranslation() @@ -21,9 +20,9 @@ const NewContextButton: FC = ({ onNewContext, ToolbarButton }) => { title={t('chat.input.new.context', { Command: newContextShortcut })} mouseLeaveDelay={0} arrow> - + - + ) } diff --git a/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx b/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx index a6d0de67c6..e01447b3fa 100644 --- a/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx @@ -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 setInputValue: React.Dispatch> 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([]) 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 ( <> - + - + model: Model - assistant: Assistant - ToolbarButton: any + assistantId: string } -const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): ReactElement => { +const ThinkingButton: FC = ({ 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 = ({ 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 - case option === 'low': - return - case option === 'medium': - return - case option === 'high': - return - case option === 'auto': - return - case option === 'off': - return - default: - return - } - }, []) - const onThinkingChange = useCallback( (option?: ThinkingOption) => { const isEnabled = option !== undefined && option !== 'off' @@ -98,11 +77,11 @@ const ThinkingButton: FC = ({ 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 = ({ 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 = ({ 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 = ({ ref, model, assistant, ToolbarButton }): Re } mouseLeaveDelay={0} arrow> - - {getThinkingIcon()} - + + {ThinkingIcon(currentReasoningEffort)} + ) } +const ThinkingIcon = (option?: ThinkingOption) => { + let IconComponent: React.FC> | 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 +} + export default ThinkingButton diff --git a/src/renderer/src/pages/home/Inputbar/TokenCount.tsx b/src/renderer/src/pages/home/Inputbar/TokenCount.tsx index d1d8f5f06e..7316e3549f 100644 --- a/src/renderer/src/pages/home/Inputbar/TokenCount.tsx +++ b/src/renderer/src/pages/home/Inputbar/TokenCount.tsx @@ -11,7 +11,6 @@ type Props = { estimateTokenCount: number inputTokenCount: number contextCount: { current: number; max: number } - ToolbarButton: any } & React.HTMLAttributes const TokenCount: FC = ({ estimateTokenCount, inputTokenCount, contextCount }) => { diff --git a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx index e5e4c939b7..3b96d0cd0f 100644 --- a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx +++ b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx @@ -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 - assistant: Assistant - ToolbarButton: any + assistantId: string } -const UrlContextButton: FC = ({ assistant, ToolbarButton }) => { +const UrlContextButton: FC = ({ 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 = ({ assistant, ToolbarButton }) => { return ( - - - + + + ) } diff --git a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx index 343b5dbe44..cb3c5fb6d9 100644 --- a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx @@ -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 - assistant: Assistant - ToolbarButton: any + assistantId: string } const logger = loggerService.withContext('WebSearchButton') -const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { +const WebSearchButton: FC = ({ 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 = ({ ref, assistant, ToolbarButton }) => { ({ pid, size = 18, color }: { pid?: WebSearchProviderId; size?: number; color?: string }) => { switch (pid) { case 'bocha': - return + return case 'exa': // size微调,视觉上和其他图标平衡一些 - return + return case 'tavily': - return + return case 'zhipu': - return + return case 'searxng': - return + return case 'local-baidu': return case 'local-bing': - return + return case 'local-google': return default: - return + return } }, [] @@ -165,13 +165,13 @@ const WebSearchButton: FC = ({ 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 = ({ ref, assistant, ToolbarButton }) => { openQuickPanel })) - const color = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)' - return ( - - - + + + ) } diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index 3d079fb24b..953dccaa29 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -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 = ({ message, topicId, onSave, onResend, onC setFiles={setFiles} couldAddImageFile={couldAddImageFile} extensions={extensions} - ToolbarButton={ToolbarButton} /> )} - + - + - + - + {message.role === 'user' && ( - + - + )} diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 15a735bc7f..8f53977835 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -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) => { - state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c)) + updateAssistant: (state, action: PayloadAction>) => { + state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? { ...c, ...action.payload } : c)) }, updateAssistantSettings: ( state,