diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7579c9d474..cf3efe3dca 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -314,6 +314,10 @@ "input.web_search.builtin.disabled_content": "The current model does not support web search", "input.web_search.no_web_search": "Disable Web Search", "input.web_search.no_web_search.description": "Do not enable web search", + "input.tools.collapse": "Collapse", + "input.tools.expand": "Expand", + "input.tools.collapse_in": "Collapse", + "input.tools.collapse_out": "Remove from collapse", "input.thinking": "Thinking", "input.thinking.mode.default": "Default", "input.thinking.mode.default.tip": "The model will automatically determine the number of tokens to think", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 140ea19a16..d2a7c50ca9 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -314,6 +314,10 @@ "input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません", "input.web_search.no_web_search": "ウェブ検索を無効にする", "input.web_search.no_web_search.description": "ウェブ検索を無効にする", + "input.tools.collapse": "折りたたむ", + "input.tools.expand": "展開", + "input.tools.collapse_in": "折りたたむ", + "input.tools.collapse_out": "展開", "input.thinking": "思考", "input.thinking.mode.default": "デフォルト", "input.thinking.mode.custom": "カスタム", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index fc5a35278c..3d7d925dce 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -314,6 +314,10 @@ "input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск", "input.web_search.no_web_search": "Отключить веб-поиск", "input.web_search.no_web_search.description": "Отключить веб-поиск", + "input.tools.collapse": "Свернуть", + "input.tools.expand": "Развернуть", + "input.tools.collapse_in": "Свернуть", + "input.tools.collapse_out": "Развернуть", "input.thinking": "Мыслим", "input.thinking.mode.default": "По умолчанию", "input.thinking.mode.default.tip": "Модель автоматически определяет количество токенов для размышления", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5fb9880ee3..bd842b58c4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -199,6 +199,10 @@ "input.web_search.builtin.disabled_content": "当前模型不支持网络搜索功能", "input.web_search.no_web_search": "不使用网络", "input.web_search.no_web_search.description": "不启用网络搜索功能", + "input.tools.collapse": "折叠", + "input.tools.expand": "展开", + "input.tools.collapse_in": "加入折叠", + "input.tools.collapse_out": "移出折叠", "message.new.branch": "分支", "message.new.branch.created": "新分支已创建", "message.new.context": "清除上下文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4cc142714b..9003d65fb0 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -314,6 +314,10 @@ "input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能", "input.web_search.no_web_search": "關閉網路搜尋", "input.web_search.no_web_search.description": "關閉網路搜尋", + "input.tools.collapse": "折疊", + "input.tools.expand": "展開", + "input.tools.collapse_in": "加入折疊", + "input.tools.collapse_out": "移出折疊", "input.thinking": "思考", "input.thinking.mode.default": "預設", "input.thinking.mode.default.tip": "模型會自動確定思考的 token 數", diff --git a/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx b/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx index 297ebc97f4..889919b7f5 100644 --- a/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/GenerateImageButton.tsx @@ -15,10 +15,6 @@ interface Props { const GenerateImageButton: FC = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => { const { t } = useTranslation() - if (!isGenerateImageModel(model)) { - return null - } - return ( = ({ assistant: _assistant, setActiveTopic, topic }) = const [tokenCount, setTokenCount] = useState(0) - const quickPhrasesButtonRef = useRef(null) - const mentionModelsButtonRef = useRef(null) - const knowledgeBaseButtonRef = useRef(null) - const mcpToolsButtonRef = useRef(null) - const attachmentButtonRef = useRef(null) - const webSearchButtonRef = useRef(null) - const thinkingButtonRef = useRef(null) + const inputbarToolsRef = useRef(null) // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedEstimate = useCallback( @@ -314,7 +284,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = description: '', icon: , action: () => { - attachmentButtonRef.current?.openQuickPanel() + inputbarToolsRef.current?.openQuickPanel() } }, ...knowledgeBases.map((base) => { @@ -333,92 +303,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = ], symbol: 'file' }) - }, [knowledgeBases, openKnowledgeFileList, quickPanel, t]) - - const quickPanelMenu = useMemo(() => { - return [ - { - label: t('settings.quickPhrase.title'), - description: '', - icon: , - isMenu: true, - action: () => { - quickPhrasesButtonRef.current?.openQuickPanel() - } - }, - { - label: t('agents.edit.model.select.title'), - description: '', - icon: , - isMenu: true, - action: () => { - mentionModelsButtonRef.current?.openQuickPanel() - } - }, - { - label: t('chat.input.knowledge_base'), - description: '', - icon: , - isMenu: true, - disabled: files.length > 0, - action: () => { - knowledgeBaseButtonRef.current?.openQuickPanel() - } - }, - { - label: t('settings.mcp.title'), - description: t('settings.mcp.not_support'), - icon: , - isMenu: true, - action: () => { - mcpToolsButtonRef.current?.openQuickPanel() - } - }, - { - label: `MCP ${t('settings.mcp.tabs.prompts')}`, - description: '', - icon: , - isMenu: true, - action: () => { - mcpToolsButtonRef.current?.openPromptList() - } - }, - { - label: `MCP ${t('settings.mcp.tabs.resources')}`, - description: '', - icon: , - isMenu: true, - action: () => { - mcpToolsButtonRef.current?.openResourcesList() - } - }, - { - label: t('chat.input.web_search'), - description: '', - icon: , - isMenu: true, - action: () => { - webSearchButtonRef.current?.openQuickPanel() - } - }, - { - label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'), - description: '', - icon: , - isMenu: true, - action: openSelectFileMenu - }, - { - label: t('translate.title'), - description: t('translate.menu.description'), - icon: , - action: () => { - if (!text) return - translate() - } - } - ] - }, [files.length, model, openSelectFileMenu, t, text, translate]) + }, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef]) const handleKeyDown = (event: React.KeyboardEvent) => { const isEnterPressed = event.keyCode == 13 @@ -566,6 +451,16 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const lastSymbol = newText[cursorPosition - 1] if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') { + const quickPanelMenu = + inputbarToolsRef.current?.getQuickPanelMenu({ + t, + files, + model, + text: newText, + openSelectFileMenu, + translate + }) || [] + quickPanel.open({ title: t('settings.quickPanel.title'), list: quickPanelMenu, @@ -574,7 +469,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') { - mentionModelsButtonRef.current?.openQuickPanel() + inputbarToolsRef.current?.openMentionModelsPanel() } } @@ -936,75 +831,30 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = + - - - - - - - {showThinkingButton && ( - - )} - - {showKnowledgeIcon && ( - 0} - /> - )} - - - - - - - - - - - - - {isExpended ? : } - - - = ({ assistant: _assistant, setActiveTopic, topic }) = ToolbarButton={ToolbarButton} onClick={onNewContext} /> - - {loading && ( @@ -1118,7 +966,8 @@ const Toolbar = styled.div` padding: 0 8px; padding-bottom: 0; margin-bottom: 4px; - height: 36px; + height: 30px; + gap: 16px; ` const ToolbarMenu = styled.div` diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx new file mode 100644 index 0000000000..248b182f4d --- /dev/null +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -0,0 +1,639 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd' +import { QuickPanelListItem } from '@renderer/components/QuickPanel' +import { isGenerateImageModel, isVisionModel } from '@renderer/config/models' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools' +import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types' +import { classNames } from '@renderer/utils' +import { Divider, Dropdown, Tooltip } from 'antd' +import { ItemType } from 'antd/es/menu/interface' +import { + AtSign, + Check, + CircleChevronRight, + FileSearch, + Globe, + Languages, + LucideSquareTerminal, + Maximize, + MessageSquareDiff, + Minimize, + PaintbrushVertical, + Paperclip, + Zap +} from 'lucide-react' +import { Dispatch, ReactNode, SetStateAction, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +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' +import NewContextButton from './NewContextButton' +import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' +import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton' +import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton' + +export interface InputbarToolsRef { + getQuickPanelMenu: (params: { + t: (key: string, options?: any) => string + files: FileType[] + model: Model + text: string + openSelectFileMenu: () => void + translate: () => void + }) => QuickPanelListItem[] + openMentionModelsPanel: () => void + openQuickPanel: () => void +} + +export interface InputbarToolsProps { + assistant: Assistant + model: Model + + files: FileType[] + setFiles: (files: FileType[]) => void + showThinkingButton: boolean + showKnowledgeIcon: boolean + selectedKnowledgeBases: KnowledgeBase[] + handleKnowledgeBaseSelect: (bases?: KnowledgeBase[]) => void + setText: Dispatch> + resizeTextArea: () => void + mentionModels: Model[] + onMentionModel: (model: Model) => void + onEnableGenerateImage: () => void + isExpended: boolean + onToggleExpended: () => void + + addNewTopic: () => void + clearTopic: () => void + onNewContext: () => void + + newTopicShortcut: string + cleanTopicShortcut: string +} + +interface ToolButtonConfig { + key: string + component: ReactNode + condition?: boolean + visible?: boolean + label?: string + icon?: ReactNode +} + +const DraggablePortal = ({ children, isDragging }) => { + return isDragging ? createPortal(children, document.body) : children +} + +const InputbarTools = ({ + ref, + assistant, + model, + files, + setFiles, + showThinkingButton, + showKnowledgeIcon, + selectedKnowledgeBases, + handleKnowledgeBaseSelect, + setText, + resizeTextArea, + mentionModels, + onMentionModel, + onEnableGenerateImage, + isExpended, + onToggleExpended, + addNewTopic, + clearTopic, + onNewContext, + newTopicShortcut, + cleanTopicShortcut +}: InputbarToolsProps & { ref?: React.RefObject }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const quickPhrasesButtonRef = useRef(null) + const mentionModelsButtonRef = useRef(null) + const knowledgeBaseButtonRef = useRef(null) + const mcpToolsButtonRef = useRef(null) + const attachmentButtonRef = useRef(null) + const webSearchButtonRef = useRef(null) + const thinkingButtonRef = useRef(null) + + const toolOrder = useAppSelector((state) => state.inputTools.toolOrder) + const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed) + + const [targetTool, setTargetTool] = useState(null) + + const toggleToolVisibility = useCallback( + (toolKey: string, isVisible: boolean | undefined) => { + const newToolOrder = { + visible: [...toolOrder.visible], + hidden: [...toolOrder.hidden] + } + + if (isVisible === true) { + newToolOrder.visible = newToolOrder.visible.filter((key) => key !== toolKey) + newToolOrder.hidden.push(toolKey) + } else { + newToolOrder.hidden = newToolOrder.hidden.filter((key) => key !== toolKey) + newToolOrder.visible.push(toolKey) + } + + dispatch(setToolOrder(newToolOrder)) + setTargetTool(null) + }, + [dispatch, toolOrder.hidden, toolOrder.visible] + ) + + const getQuickPanelMenuImpl = (params: { + t: (key: string, options?: any) => string + files: FileType[] + model: Model + text: string + openSelectFileMenu: () => void + translate: () => void + }): QuickPanelListItem[] => { + const { t, files, model, text, openSelectFileMenu, translate } = params + + return [ + { + label: t('settings.quickPhrase.title'), + description: '', + icon: , + isMenu: true, + action: () => { + quickPhrasesButtonRef.current?.openQuickPanel() + } + }, + { + label: t('agents.edit.model.select.title'), + description: '', + icon: , + isMenu: true, + action: () => { + mentionModelsButtonRef.current?.openQuickPanel() + } + }, + { + label: t('chat.input.knowledge_base'), + description: '', + icon: , + isMenu: true, + disabled: files.length > 0, + action: () => { + knowledgeBaseButtonRef.current?.openQuickPanel() + } + }, + { + label: t('settings.mcp.title'), + description: t('settings.mcp.not_support'), + icon: , + isMenu: true, + action: () => { + mcpToolsButtonRef.current?.openQuickPanel() + } + }, + { + label: `MCP ${t('settings.mcp.tabs.prompts')}`, + description: '', + icon: , + isMenu: true, + action: () => { + mcpToolsButtonRef.current?.openPromptList() + } + }, + { + label: `MCP ${t('settings.mcp.tabs.resources')}`, + description: '', + icon: , + isMenu: true, + action: () => { + mcpToolsButtonRef.current?.openResourcesList() + } + }, + { + label: t('chat.input.web_search'), + description: '', + icon: , + isMenu: true, + action: () => { + webSearchButtonRef.current?.openQuickPanel() + } + }, + { + label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'), + description: '', + icon: , + isMenu: true, + action: openSelectFileMenu + }, + { + label: t('translate.title'), + description: t('translate.menu.description'), + icon: , + action: () => { + if (!text) return + translate() + } + } + ] + } + + const handleDragEnd = (result: DropResult) => { + const { source, destination } = result + + if (!destination) return + + const sourceId = source.droppableId + const destinationId = destination.droppableId + + const newToolOrder = { + visible: [...toolOrder.visible], + hidden: [...toolOrder.hidden] + } + + const sourceArray = sourceId === 'inputbar-tools-visible' ? 'visible' : 'hidden' + const destArray = destinationId === 'inputbar-tools-visible' ? 'visible' : 'hidden' + + if (sourceArray === destArray) { + const items = newToolOrder[sourceArray] + const [removed] = items.splice(source.index, 1) + items.splice(destination.index, 0, removed) + } else { + const removed = newToolOrder[sourceArray][source.index] + newToolOrder[sourceArray].splice(source.index, 1) + newToolOrder[destArray].splice(destination.index, 0, removed) + } + + dispatch(setToolOrder(newToolOrder)) + } + + useImperativeHandle(ref, () => ({ + getQuickPanelMenu: getQuickPanelMenuImpl, + openMentionModelsPanel: () => mentionModelsButtonRef.current?.openQuickPanel(), + openQuickPanel: () => attachmentButtonRef.current?.openQuickPanel() + })) + + const toolButtons = useMemo(() => { + return [ + { + key: 'new_topic', + label: t('chat.input.new_topic', { Command: '' }), + component: ( + + + + + + ) + }, + { + key: 'attachment', + label: t('chat.input.upload'), + component: ( + + ) + }, + { + key: 'thinking', + label: t('chat.input.thinking'), + component: ( + + ), + condition: showThinkingButton + }, + { + key: 'web_search', + label: t('chat.input.web_search'), + component: + }, + { + key: 'knowledge_base', + label: t('chat.input.knowledge_base'), + component: ( + 0} + /> + ), + condition: showKnowledgeIcon + }, + { + key: 'mcp_tools', + label: t('settings.mcp.title'), + component: ( + + ) + }, + { + key: 'generate_image', + label: t('chat.input.generate_image'), + component: ( + + ), + condition: isGenerateImageModel(model) + }, + { + key: 'mention_models', + label: t('agents.edit.model.select.title'), + component: ( + + ) + }, + { + key: 'quick_phrases', + label: t('settings.quickPhrase.title'), + component: ( + + ) + }, + { + key: 'clear_topic', + label: t('chat.input.clear', { Command: '' }), + component: ( + + + + + + ) + }, + { + key: 'toggle_expand', + label: isExpended ? t('chat.input.collapse') : t('chat.input.expand'), + component: ( + + + {isExpended ? : } + + + ) + }, + { + key: 'new_context', + label: t('chat.input.new.context', { Command: '' }), + component: + } + ] + }, [ + addNewTopic, + assistant, + cleanTopicShortcut, + clearTopic, + files, + handleKnowledgeBaseSelect, + isExpended, + mentionModels, + model, + newTopicShortcut, + onEnableGenerateImage, + onMentionModel, + onNewContext, + onToggleExpended, + resizeTextArea, + selectedKnowledgeBases, + setFiles, + setText, + showKnowledgeIcon, + showThinkingButton, + t + ]) + + const visibleTools = useMemo(() => { + return toolOrder.visible.map((v) => ({ + ...toolButtons.find((tool) => tool.key === v), + visible: true + })) as ToolButtonConfig[] + }, [toolButtons, toolOrder]) + + const hiddenTools = useMemo(() => { + return toolOrder.hidden.map((v) => ({ + ...toolButtons.find((tool) => tool.key === v), + visible: false + })) as ToolButtonConfig[] + }, [toolButtons, toolOrder]) + + const showDivider = useMemo(() => { + return ( + hiddenTools.filter((tool) => tool.condition ?? true).length > 0 && + visibleTools.filter((tool) => tool.condition ?? true).length !== 0 + ) + }, [hiddenTools, visibleTools]) + + const showCollapseButton = useMemo(() => { + return hiddenTools.filter((tool) => tool.condition ?? true).length > 0 + }, [hiddenTools]) + + const getMenuItems = useMemo(() => { + const baseItems: ItemType[] = [...visibleTools, ...hiddenTools].map((tool) => ({ + label: tool.label, + key: tool.key, + icon: ( +
+ {tool.visible ? : undefined} +
+ ), + onClick: () => { + toggleToolVisibility(tool.key, tool.visible) + } + })) + + if (targetTool) { + baseItems.push({ + type: 'divider' + }) + baseItems.push({ + label: `${targetTool.visible ? t('chat.input.tools.collapse_in') : t('chat.input.tools.collapse_out')} "${targetTool.label}"`, + key: 'selected_' + targetTool.key, + icon:
, + onClick: () => { + toggleToolVisibility(targetTool.key, targetTool.visible) + } + }) + } + + return baseItems + }, [hiddenTools, t, targetTool, toggleToolVisibility, visibleTools]) + + return ( + + { + const target = e.target as HTMLElement + const isToolButton = target.closest('[data-key]') + if (!isToolButton) { + setTargetTool(null) + } + }}> + + + {(provided) => ( + + {visibleTools.map( + (tool, index) => + (tool.condition ?? true) && ( + + {(provided, snapshot) => ( + + setTargetTool(tool)} + ref={provided.innerRef} + {...provided.draggableProps} + {...provided.dragHandleProps} + style={{ + ...provided.draggableProps.style + }}> + {tool.component} + + + )} + + ) + )} + + {provided.placeholder} + + )} + + + {showDivider && } + + + {(provided) => ( + + {hiddenTools.map( + (tool, index) => + (tool.condition ?? true) && ( + + {(provided, snapshot) => ( + + setTargetTool(tool)} + ref={provided.innerRef} + {...provided.draggableProps} + {...provided.dragHandleProps} + style={{ + ...provided.draggableProps.style, + transitionDelay: `${index * 0.02}s` + }}> + {tool.component} + + + )} + + ) + )} + {provided.placeholder} + + )} + + + + {showCollapseButton && ( + + dispatch(setIsCollapsed(!isCollapse))}> + + + + )} + + + ) +} + +const ToolsContainer = styled.div` + min-width: 0; + display: flex; + align-items: center; + position: relative; +` + +const VisibleTools = styled.div` + height: 30px; + display: flex; + align-items: center; + overflow-x: auto; + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; +` + +const HiddenTools = styled.div` + height: 30px; + display: flex; + align-items: center; + overflow-x: auto; + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; +` + +const ToolWrapper = styled.div` + width: 30px; + margin-right: 6px; + transition: + width 0.2s, + margin-right 0.2s, + opacity 0.2s; + &.is-collapsed { + width: 0px; + margin-right: 0px; + overflow: hidden; + opacity: 0; + } +` + +export default InputbarTools diff --git a/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx b/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx index 2cf0ba2dab..c8c2b9fced 100644 --- a/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/NewContextButton.tsx @@ -3,7 +3,6 @@ import { Tooltip } from 'antd' import { Eraser } from 'lucide-react' import { FC } from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' interface Props { onNewContext: () => void @@ -17,20 +16,12 @@ const NewContextButton: FC = ({ onNewContext, ToolbarButton }) => { useShortcut('toggle_new_context', onNewContext) return ( - - - - - - - + + + + + ) } -const Container = styled.div` - @media (max-width: 800px) { - display: none; - } -` - export default NewContextButton diff --git a/src/renderer/src/pages/home/Inputbar/TokenCount.tsx b/src/renderer/src/pages/home/Inputbar/TokenCount.tsx index d73d18b82c..0f556a1d15 100644 --- a/src/renderer/src/pages/home/Inputbar/TokenCount.tsx +++ b/src/renderer/src/pages/home/Inputbar/TokenCount.tsx @@ -62,7 +62,6 @@ const Container = styled.div` z-index: 10; padding: 3px 10px; user-select: none; - border: 0.5px solid var(--color-text-3); border-radius: 20px; display: flex; align-items: center; diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 64ce0ac403..e398787f5b 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -8,6 +8,7 @@ import agents from './agents' import assistants from './assistants' import backup from './backup' import copilot from './copilot' +import inputToolsReducer from './inputTools' import knowledge from './knowledge' import llm from './llm' import mcp from './mcp' @@ -39,7 +40,8 @@ const rootReducer = combineReducers({ copilot, // messages: messagesReducer, messages: newMessagesReducer, - messageBlocks: messageBlocksReducer + messageBlocks: messageBlocksReducer, + inputTools: inputToolsReducer }) const persistedReducer = persistReducer( diff --git a/src/renderer/src/store/inputTools.ts b/src/renderer/src/store/inputTools.ts new file mode 100644 index 0000000000..181afedf99 --- /dev/null +++ b/src/renderer/src/store/inputTools.ts @@ -0,0 +1,51 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export type ToolOrder = { + visible: string[] + hidden: string[] +} + +export const DEFAULT_TOOL_ORDER: ToolOrder = { + visible: [ + 'new_topic', + 'attachment', + 'thinking', + 'web_search', + 'knowledge_base', + 'mcp_tools', + 'generate_image', + 'mention_models', + 'quick_phrases', + 'clear_topic', + 'toggle_expand', + 'new_context' + ], + hidden: [] +} + +export type InputToolsState = { + toolOrder: ToolOrder + isCollapsed: boolean +} + +const initialState: InputToolsState = { + toolOrder: DEFAULT_TOOL_ORDER, + isCollapsed: false +} + +const inputToolsSlice = createSlice({ + name: 'inputTools', + initialState, + reducers: { + setToolOrder: (state, action: PayloadAction) => { + state.toolOrder = action.payload + }, + setIsCollapsed: (state, action: PayloadAction) => { + state.isCollapsed = action.payload + } + } +}) + +export const { setToolOrder, setIsCollapsed } = inputToolsSlice.actions + +export default inputToolsSlice.reducer diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 1f9145a464..6edf827440 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -11,6 +11,7 @@ import { isEmpty } from 'lodash' import { createMigrate } from 'redux-persist' import { RootState } from '.' +import { DEFAULT_TOOL_ORDER } from './inputTools' import { INITIAL_PROVIDERS, moveProvider } from './llm' import { mcpSlice } from './mcp' import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings' @@ -1455,6 +1456,15 @@ const migrateConfig = { } catch (error) { return state } + }, + '108': (state: RootState) => { + try { + state.inputTools.toolOrder = DEFAULT_TOOL_ORDER + state.inputTools.isCollapsed = false + return state + } catch (error) { + return state + } } }