mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
Feat: Supports sorting of textarea function buttons by dragging (#6268)
* feat(inputbar): add collapsible tools and localization for tool actions * refactor(inputbar): simplify tool rendering logic in InputbarTools * refactor(inputbar): enhance tool visibility logic and improve rendering structure in InputbarTools * fix(inputbar): correct tooltip text for collapse/expand action in InputbarTools * refactor(Inputbar): simplify Toolbar structure and improve styling
This commit is contained in:
parent
92aa9f9c8e
commit
bc9e8e64d3
@ -314,6 +314,10 @@
|
|||||||
"input.web_search.builtin.disabled_content": "The current model does not support web search",
|
"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": "Disable Web Search",
|
||||||
"input.web_search.no_web_search.description": "Do not enable 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": "Thinking",
|
||||||
"input.thinking.mode.default": "Default",
|
"input.thinking.mode.default": "Default",
|
||||||
"input.thinking.mode.default.tip": "The model will automatically determine the number of tokens to think",
|
"input.thinking.mode.default.tip": "The model will automatically determine the number of tokens to think",
|
||||||
|
|||||||
@ -314,6 +314,10 @@
|
|||||||
"input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません",
|
"input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません",
|
||||||
"input.web_search.no_web_search": "ウェブ検索を無効にする",
|
"input.web_search.no_web_search": "ウェブ検索を無効にする",
|
||||||
"input.web_search.no_web_search.description": "ウェブ検索を無効にする",
|
"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": "思考",
|
||||||
"input.thinking.mode.default": "デフォルト",
|
"input.thinking.mode.default": "デフォルト",
|
||||||
"input.thinking.mode.custom": "カスタム",
|
"input.thinking.mode.custom": "カスタム",
|
||||||
|
|||||||
@ -314,6 +314,10 @@
|
|||||||
"input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск",
|
"input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск",
|
||||||
"input.web_search.no_web_search": "Отключить веб-поиск",
|
"input.web_search.no_web_search": "Отключить веб-поиск",
|
||||||
"input.web_search.no_web_search.description": "Отключить веб-поиск",
|
"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": "Мыслим",
|
||||||
"input.thinking.mode.default": "По умолчанию",
|
"input.thinking.mode.default": "По умолчанию",
|
||||||
"input.thinking.mode.default.tip": "Модель автоматически определяет количество токенов для размышления",
|
"input.thinking.mode.default.tip": "Модель автоматически определяет количество токенов для размышления",
|
||||||
|
|||||||
@ -199,6 +199,10 @@
|
|||||||
"input.web_search.builtin.disabled_content": "当前模型不支持网络搜索功能",
|
"input.web_search.builtin.disabled_content": "当前模型不支持网络搜索功能",
|
||||||
"input.web_search.no_web_search": "不使用网络",
|
"input.web_search.no_web_search": "不使用网络",
|
||||||
"input.web_search.no_web_search.description": "不启用网络搜索功能",
|
"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": "分支",
|
||||||
"message.new.branch.created": "新分支已创建",
|
"message.new.branch.created": "新分支已创建",
|
||||||
"message.new.context": "清除上下文",
|
"message.new.context": "清除上下文",
|
||||||
|
|||||||
@ -314,6 +314,10 @@
|
|||||||
"input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能",
|
"input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能",
|
||||||
"input.web_search.no_web_search": "關閉網路搜尋",
|
"input.web_search.no_web_search": "關閉網路搜尋",
|
||||||
"input.web_search.no_web_search.description": "關閉網路搜尋",
|
"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": "思考",
|
||||||
"input.thinking.mode.default": "預設",
|
"input.thinking.mode.default": "預設",
|
||||||
"input.thinking.mode.default.tip": "模型會自動確定思考的 token 數",
|
"input.thinking.mode.default.tip": "模型會自動確定思考的 token 數",
|
||||||
|
|||||||
@ -15,10 +15,6 @@ interface Props {
|
|||||||
const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => {
|
const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (!isGenerateImageModel(model)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { HolderOutlined } from '@ant-design/icons'
|
import { HolderOutlined } from '@ant-design/icons'
|
||||||
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
import { QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import Logger from '@renderer/config/logger'
|
import Logger from '@renderer/config/logger'
|
||||||
import {
|
import {
|
||||||
@ -39,42 +39,18 @@ import { Button, Tooltip } from 'antd'
|
|||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
import { debounce, isEmpty } from 'lodash'
|
||||||
import {
|
import { CirclePause, FileSearch, FileText, Upload } from 'lucide-react'
|
||||||
AtSign,
|
|
||||||
CirclePause,
|
|
||||||
FileSearch,
|
|
||||||
FileText,
|
|
||||||
Globe,
|
|
||||||
Languages,
|
|
||||||
LucideSquareTerminal,
|
|
||||||
Maximize,
|
|
||||||
MessageSquareDiff,
|
|
||||||
Minimize,
|
|
||||||
PaintbrushVertical,
|
|
||||||
Paperclip,
|
|
||||||
Upload,
|
|
||||||
Zap
|
|
||||||
} from 'lucide-react'
|
|
||||||
// import { CompletionUsage } from 'openai/resources'
|
|
||||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import NarrowLayout from '../Messages/NarrowLayout'
|
import NarrowLayout from '../Messages/NarrowLayout'
|
||||||
import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton'
|
|
||||||
import AttachmentPreview from './AttachmentPreview'
|
import AttachmentPreview from './AttachmentPreview'
|
||||||
import GenerateImageButton from './GenerateImageButton'
|
import InputbarTools, { InputbarToolsRef } from './InputbarTools'
|
||||||
import KnowledgeBaseButton, { KnowledgeBaseButtonRef } from './KnowledgeBaseButton'
|
|
||||||
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
||||||
import MCPToolsButton, { MCPToolsButtonRef } from './MCPToolsButton'
|
|
||||||
import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButton'
|
|
||||||
import MentionModelsInput from './MentionModelsInput'
|
import MentionModelsInput from './MentionModelsInput'
|
||||||
import NewContextButton from './NewContextButton'
|
|
||||||
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
|
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -135,13 +111,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
|
|
||||||
const [tokenCount, setTokenCount] = useState(0)
|
const [tokenCount, setTokenCount] = useState(0)
|
||||||
|
|
||||||
const quickPhrasesButtonRef = useRef<QuickPhrasesButtonRef>(null)
|
const inputbarToolsRef = useRef<InputbarToolsRef>(null)
|
||||||
const mentionModelsButtonRef = useRef<MentionModelsButtonRef>(null)
|
|
||||||
const knowledgeBaseButtonRef = useRef<KnowledgeBaseButtonRef>(null)
|
|
||||||
const mcpToolsButtonRef = useRef<MCPToolsButtonRef>(null)
|
|
||||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
|
||||||
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
|
|
||||||
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedEstimate = useCallback(
|
const debouncedEstimate = useCallback(
|
||||||
@ -314,7 +284,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
description: '',
|
description: '',
|
||||||
icon: <Upload />,
|
icon: <Upload />,
|
||||||
action: () => {
|
action: () => {
|
||||||
attachmentButtonRef.current?.openQuickPanel()
|
inputbarToolsRef.current?.openQuickPanel()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...knowledgeBases.map((base) => {
|
...knowledgeBases.map((base) => {
|
||||||
@ -333,92 +303,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
],
|
],
|
||||||
symbol: 'file'
|
symbol: 'file'
|
||||||
})
|
})
|
||||||
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t])
|
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
|
||||||
|
|
||||||
const quickPanelMenu = useMemo<QuickPanelListItem[]>(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('settings.quickPhrase.title'),
|
|
||||||
description: '',
|
|
||||||
icon: <Zap />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
quickPhrasesButtonRef.current?.openQuickPanel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('agents.edit.model.select.title'),
|
|
||||||
description: '',
|
|
||||||
icon: <AtSign />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
mentionModelsButtonRef.current?.openQuickPanel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('chat.input.knowledge_base'),
|
|
||||||
description: '',
|
|
||||||
icon: <FileSearch />,
|
|
||||||
isMenu: true,
|
|
||||||
disabled: files.length > 0,
|
|
||||||
action: () => {
|
|
||||||
knowledgeBaseButtonRef.current?.openQuickPanel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('settings.mcp.title'),
|
|
||||||
description: t('settings.mcp.not_support'),
|
|
||||||
icon: <LucideSquareTerminal />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
mcpToolsButtonRef.current?.openQuickPanel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `MCP ${t('settings.mcp.tabs.prompts')}`,
|
|
||||||
description: '',
|
|
||||||
icon: <LucideSquareTerminal />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
mcpToolsButtonRef.current?.openPromptList()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `MCP ${t('settings.mcp.tabs.resources')}`,
|
|
||||||
description: '',
|
|
||||||
icon: <LucideSquareTerminal />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
mcpToolsButtonRef.current?.openResourcesList()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('chat.input.web_search'),
|
|
||||||
description: '',
|
|
||||||
icon: <Globe />,
|
|
||||||
isMenu: true,
|
|
||||||
action: () => {
|
|
||||||
webSearchButtonRef.current?.openQuickPanel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
|
||||||
description: '',
|
|
||||||
icon: <Paperclip />,
|
|
||||||
isMenu: true,
|
|
||||||
action: openSelectFileMenu
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('translate.title'),
|
|
||||||
description: t('translate.menu.description'),
|
|
||||||
icon: <Languages />,
|
|
||||||
action: () => {
|
|
||||||
if (!text) return
|
|
||||||
translate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, [files.length, model, openSelectFileMenu, t, text, translate])
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isEnterPressed = event.keyCode == 13
|
const isEnterPressed = event.keyCode == 13
|
||||||
@ -566,6 +451,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const lastSymbol = newText[cursorPosition - 1]
|
const lastSymbol = newText[cursorPosition - 1]
|
||||||
|
|
||||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
||||||
|
const quickPanelMenu =
|
||||||
|
inputbarToolsRef.current?.getQuickPanelMenu({
|
||||||
|
t,
|
||||||
|
files,
|
||||||
|
model,
|
||||||
|
text: newText,
|
||||||
|
openSelectFileMenu,
|
||||||
|
translate
|
||||||
|
}) || []
|
||||||
|
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
title: t('settings.quickPanel.title'),
|
title: t('settings.quickPanel.title'),
|
||||||
list: quickPanelMenu,
|
list: quickPanelMenu,
|
||||||
@ -574,7 +469,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
||||||
mentionModelsButtonRef.current?.openQuickPanel()
|
inputbarToolsRef.current?.openMentionModelsPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,75 +831,30 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
<HolderOutlined />
|
<HolderOutlined />
|
||||||
</DragHandle>
|
</DragHandle>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
<InputbarTools
|
||||||
|
ref={inputbarToolsRef}
|
||||||
|
assistant={assistant}
|
||||||
|
model={model}
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
showThinkingButton={showThinkingButton}
|
||||||
|
showKnowledgeIcon={showKnowledgeIcon}
|
||||||
|
selectedKnowledgeBases={selectedKnowledgeBases}
|
||||||
|
handleKnowledgeBaseSelect={handleKnowledgeBaseSelect}
|
||||||
|
setText={setText}
|
||||||
|
resizeTextArea={resizeTextArea}
|
||||||
|
mentionModels={mentionModels}
|
||||||
|
onMentionModel={onMentionModel}
|
||||||
|
onEnableGenerateImage={onEnableGenerateImage}
|
||||||
|
isExpended={isExpended}
|
||||||
|
onToggleExpended={onToggleExpended}
|
||||||
|
addNewTopic={addNewTopic}
|
||||||
|
clearTopic={clearTopic}
|
||||||
|
onNewContext={onNewContext}
|
||||||
|
newTopicShortcut={newTopicShortcut}
|
||||||
|
cleanTopicShortcut={cleanTopicShortcut}
|
||||||
|
/>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
|
||||||
<MessageSquareDiff size={19} />
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
<AttachmentButton
|
|
||||||
ref={attachmentButtonRef}
|
|
||||||
model={model}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
/>
|
|
||||||
{showThinkingButton && (
|
|
||||||
<ThinkingButton
|
|
||||||
ref={thinkingButtonRef}
|
|
||||||
model={model}
|
|
||||||
assistant={assistant}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
|
||||||
{showKnowledgeIcon && (
|
|
||||||
<KnowledgeBaseButton
|
|
||||||
ref={knowledgeBaseButtonRef}
|
|
||||||
selectedBases={selectedKnowledgeBases}
|
|
||||||
onSelect={handleKnowledgeBaseSelect}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
disabled={files.length > 0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MCPToolsButton
|
|
||||||
assistant={assistant}
|
|
||||||
ref={mcpToolsButtonRef}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
setInputValue={setText}
|
|
||||||
resizeTextArea={resizeTextArea}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GenerateImageButton
|
|
||||||
model={model}
|
|
||||||
assistant={assistant}
|
|
||||||
onEnableGenerateImage={onEnableGenerateImage}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
/>
|
|
||||||
<MentionModelsButton
|
|
||||||
ref={mentionModelsButtonRef}
|
|
||||||
mentionModels={mentionModels}
|
|
||||||
onMentionModel={onMentionModel}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
/>
|
|
||||||
<QuickPhrasesButton
|
|
||||||
ref={quickPhrasesButtonRef}
|
|
||||||
setInputValue={setText}
|
|
||||||
resizeTextArea={resizeTextArea}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
assistantObj={assistant}
|
|
||||||
/>
|
|
||||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={clearTopic}>
|
|
||||||
<PaintbrushVertical size={18} />
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip placement="top" title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
|
||||||
{isExpended ? <Minimize size={18} /> : <Maximize size={18} />}
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
<NewContextButton onNewContext={onNewContext} ToolbarButton={ToolbarButton} />
|
|
||||||
<TokenCount
|
<TokenCount
|
||||||
estimateTokenCount={estimateTokenCount}
|
estimateTokenCount={estimateTokenCount}
|
||||||
inputTokenCount={inputTokenCount}
|
inputTokenCount={inputTokenCount}
|
||||||
@ -1012,8 +862,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
onClick={onNewContext}
|
onClick={onNewContext}
|
||||||
/>
|
/>
|
||||||
</ToolbarMenu>
|
|
||||||
<ToolbarMenu>
|
|
||||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||||
{loading && (
|
{loading && (
|
||||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||||
@ -1118,7 +966,8 @@ const Toolbar = styled.div`
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
height: 36px;
|
height: 30px;
|
||||||
|
gap: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarMenu = styled.div`
|
const ToolbarMenu = styled.div`
|
||||||
|
|||||||
639
src/renderer/src/pages/home/Inputbar/InputbarTools.tsx
Normal file
639
src/renderer/src/pages/home/Inputbar/InputbarTools.tsx
Normal file
@ -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<SetStateAction<string>>
|
||||||
|
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<InputbarToolsRef | null> }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const quickPhrasesButtonRef = useRef<QuickPhrasesButtonRef>(null)
|
||||||
|
const mentionModelsButtonRef = useRef<MentionModelsButtonRef>(null)
|
||||||
|
const knowledgeBaseButtonRef = useRef<KnowledgeBaseButtonRef>(null)
|
||||||
|
const mcpToolsButtonRef = useRef<MCPToolsButtonRef>(null)
|
||||||
|
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||||
|
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
|
||||||
|
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
|
||||||
|
|
||||||
|
const toolOrder = useAppSelector((state) => state.inputTools.toolOrder)
|
||||||
|
const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed)
|
||||||
|
|
||||||
|
const [targetTool, setTargetTool] = useState<ToolButtonConfig | null>(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: <Zap />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
quickPhrasesButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('agents.edit.model.select.title'),
|
||||||
|
description: '',
|
||||||
|
icon: <AtSign />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
mentionModelsButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.input.knowledge_base'),
|
||||||
|
description: '',
|
||||||
|
icon: <FileSearch />,
|
||||||
|
isMenu: true,
|
||||||
|
disabled: files.length > 0,
|
||||||
|
action: () => {
|
||||||
|
knowledgeBaseButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings.mcp.title'),
|
||||||
|
description: t('settings.mcp.not_support'),
|
||||||
|
icon: <LucideSquareTerminal />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
mcpToolsButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `MCP ${t('settings.mcp.tabs.prompts')}`,
|
||||||
|
description: '',
|
||||||
|
icon: <LucideSquareTerminal />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
mcpToolsButtonRef.current?.openPromptList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `MCP ${t('settings.mcp.tabs.resources')}`,
|
||||||
|
description: '',
|
||||||
|
icon: <LucideSquareTerminal />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
mcpToolsButtonRef.current?.openResourcesList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.input.web_search'),
|
||||||
|
description: '',
|
||||||
|
icon: <Globe />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
webSearchButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||||
|
description: '',
|
||||||
|
icon: <Paperclip />,
|
||||||
|
isMenu: true,
|
||||||
|
action: openSelectFileMenu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('translate.title'),
|
||||||
|
description: t('translate.menu.description'),
|
||||||
|
icon: <Languages />,
|
||||||
|
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<ToolButtonConfig[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'new_topic',
|
||||||
|
label: t('chat.input.new_topic', { Command: '' }),
|
||||||
|
component: (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||||
|
<MessageSquareDiff size={19} />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attachment',
|
||||||
|
label: t('chat.input.upload'),
|
||||||
|
component: (
|
||||||
|
<AttachmentButton
|
||||||
|
ref={attachmentButtonRef}
|
||||||
|
model={model}
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'thinking',
|
||||||
|
label: t('chat.input.thinking'),
|
||||||
|
component: (
|
||||||
|
<ThinkingButton ref={thinkingButtonRef} model={model} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||||
|
),
|
||||||
|
condition: showThinkingButton
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'web_search',
|
||||||
|
label: t('chat.input.web_search'),
|
||||||
|
component: <WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'knowledge_base',
|
||||||
|
label: t('chat.input.knowledge_base'),
|
||||||
|
component: (
|
||||||
|
<KnowledgeBaseButton
|
||||||
|
ref={knowledgeBaseButtonRef}
|
||||||
|
selectedBases={selectedKnowledgeBases}
|
||||||
|
onSelect={handleKnowledgeBaseSelect}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
disabled={files.length > 0}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
condition: showKnowledgeIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mcp_tools',
|
||||||
|
label: t('settings.mcp.title'),
|
||||||
|
component: (
|
||||||
|
<MCPToolsButton
|
||||||
|
assistant={assistant}
|
||||||
|
ref={mcpToolsButtonRef}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
setInputValue={setText}
|
||||||
|
resizeTextArea={resizeTextArea}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'generate_image',
|
||||||
|
label: t('chat.input.generate_image'),
|
||||||
|
component: (
|
||||||
|
<GenerateImageButton
|
||||||
|
model={model}
|
||||||
|
assistant={assistant}
|
||||||
|
onEnableGenerateImage={onEnableGenerateImage}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
condition: isGenerateImageModel(model)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mention_models',
|
||||||
|
label: t('agents.edit.model.select.title'),
|
||||||
|
component: (
|
||||||
|
<MentionModelsButton
|
||||||
|
ref={mentionModelsButtonRef}
|
||||||
|
mentionModels={mentionModels}
|
||||||
|
onMentionModel={onMentionModel}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quick_phrases',
|
||||||
|
label: t('settings.quickPhrase.title'),
|
||||||
|
component: (
|
||||||
|
<QuickPhrasesButton
|
||||||
|
ref={quickPhrasesButtonRef}
|
||||||
|
setInputValue={setText}
|
||||||
|
resizeTextArea={resizeTextArea}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
assistantObj={assistant}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clear_topic',
|
||||||
|
label: t('chat.input.clear', { Command: '' }),
|
||||||
|
component: (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={clearTopic}>
|
||||||
|
<PaintbrushVertical size={18} />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'toggle_expand',
|
||||||
|
label: isExpended ? t('chat.input.collapse') : t('chat.input.expand'),
|
||||||
|
component: (
|
||||||
|
<Tooltip placement="top" title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||||
|
{isExpended ? <Minimize size={18} /> : <Maximize size={18} />}
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'new_context',
|
||||||
|
label: t('chat.input.new.context', { Command: '' }),
|
||||||
|
component: <NewContextButton onNewContext={onNewContext} ToolbarButton={ToolbarButton} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, [
|
||||||
|
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: (
|
||||||
|
<div style={{ width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
{tool.visible ? <Check size={16} /> : undefined}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: <div style={{ width: 20, height: 20 }}></div>,
|
||||||
|
onClick: () => {
|
||||||
|
toggleToolVisibility(targetTool.key, targetTool.visible)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseItems
|
||||||
|
}, [hiddenTools, t, targetTool, toggleToolVisibility, visibleTools])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items: getMenuItems }} trigger={['contextMenu']}>
|
||||||
|
<ToolsContainer
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
const isToolButton = target.closest('[data-key]')
|
||||||
|
if (!isToolButton) {
|
||||||
|
setTargetTool(null)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
|
<Droppable droppableId="inputbar-tools-visible" direction="horizontal">
|
||||||
|
{(provided) => (
|
||||||
|
<VisibleTools ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
{visibleTools.map(
|
||||||
|
(tool, index) =>
|
||||||
|
(tool.condition ?? true) && (
|
||||||
|
<Draggable key={tool.key} draggableId={tool.key} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<DraggablePortal isDragging={snapshot.isDragging}>
|
||||||
|
<ToolWrapper
|
||||||
|
data-key={tool.key}
|
||||||
|
onContextMenu={() => setTargetTool(tool)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style
|
||||||
|
}}>
|
||||||
|
{tool.component}
|
||||||
|
</ToolWrapper>
|
||||||
|
</DraggablePortal>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{provided.placeholder}
|
||||||
|
</VisibleTools>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
|
||||||
|
{showDivider && <Divider type="vertical" style={{ margin: '0 4px' }} />}
|
||||||
|
|
||||||
|
<Droppable droppableId="inputbar-tools-hidden" direction="horizontal">
|
||||||
|
{(provided) => (
|
||||||
|
<HiddenTools ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
{hiddenTools.map(
|
||||||
|
(tool, index) =>
|
||||||
|
(tool.condition ?? true) && (
|
||||||
|
<Draggable key={tool.key} draggableId={tool.key} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<DraggablePortal isDragging={snapshot.isDragging}>
|
||||||
|
<ToolWrapper
|
||||||
|
data-key={tool.key}
|
||||||
|
className={classNames({
|
||||||
|
'is-collapsed': isCollapse
|
||||||
|
})}
|
||||||
|
onContextMenu={() => setTargetTool(tool)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
transitionDelay: `${index * 0.02}s`
|
||||||
|
}}>
|
||||||
|
{tool.component}
|
||||||
|
</ToolWrapper>
|
||||||
|
</DraggablePortal>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{provided.placeholder}
|
||||||
|
</HiddenTools>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
|
||||||
|
{showCollapseButton && (
|
||||||
|
<Tooltip
|
||||||
|
placement="top"
|
||||||
|
title={isCollapse ? t('chat.input.tools.expand') : t('chat.input.tools.collapse')}
|
||||||
|
arrow>
|
||||||
|
<ToolbarButton type="text" onClick={() => dispatch(setIsCollapsed(!isCollapse))}>
|
||||||
|
<CircleChevronRight
|
||||||
|
size={18}
|
||||||
|
style={{
|
||||||
|
transform: isCollapse ? 'scaleX(1)' : 'scaleX(-1)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</ToolsContainer>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -3,7 +3,6 @@ import { Tooltip } from 'antd'
|
|||||||
import { Eraser } from 'lucide-react'
|
import { Eraser } from 'lucide-react'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onNewContext: () => void
|
onNewContext: () => void
|
||||||
@ -17,20 +16,12 @@ const NewContextButton: FC<Props> = ({ onNewContext, ToolbarButton }) => {
|
|||||||
useShortcut('toggle_new_context', onNewContext)
|
useShortcut('toggle_new_context', onNewContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
<ToolbarButton type="text" onClick={onNewContext}>
|
||||||
<ToolbarButton type="text" onClick={onNewContext}>
|
<Eraser size={18} />
|
||||||
<Eraser size={18} />
|
</ToolbarButton>
|
||||||
</ToolbarButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default NewContextButton
|
export default NewContextButton
|
||||||
|
|||||||
@ -62,7 +62,6 @@ const Container = styled.div`
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 0.5px solid var(--color-text-3);
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import agents from './agents'
|
|||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
import backup from './backup'
|
import backup from './backup'
|
||||||
import copilot from './copilot'
|
import copilot from './copilot'
|
||||||
|
import inputToolsReducer from './inputTools'
|
||||||
import knowledge from './knowledge'
|
import knowledge from './knowledge'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
import mcp from './mcp'
|
import mcp from './mcp'
|
||||||
@ -39,7 +40,8 @@ const rootReducer = combineReducers({
|
|||||||
copilot,
|
copilot,
|
||||||
// messages: messagesReducer,
|
// messages: messagesReducer,
|
||||||
messages: newMessagesReducer,
|
messages: newMessagesReducer,
|
||||||
messageBlocks: messageBlocksReducer
|
messageBlocks: messageBlocksReducer,
|
||||||
|
inputTools: inputToolsReducer
|
||||||
})
|
})
|
||||||
|
|
||||||
const persistedReducer = persistReducer(
|
const persistedReducer = persistReducer(
|
||||||
|
|||||||
51
src/renderer/src/store/inputTools.ts
Normal file
51
src/renderer/src/store/inputTools.ts
Normal file
@ -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<ToolOrder>) => {
|
||||||
|
state.toolOrder = action.payload
|
||||||
|
},
|
||||||
|
setIsCollapsed: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isCollapsed = action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setToolOrder, setIsCollapsed } = inputToolsSlice.actions
|
||||||
|
|
||||||
|
export default inputToolsSlice.reducer
|
||||||
@ -11,6 +11,7 @@ import { isEmpty } from 'lodash'
|
|||||||
import { createMigrate } from 'redux-persist'
|
import { createMigrate } from 'redux-persist'
|
||||||
|
|
||||||
import { RootState } from '.'
|
import { RootState } from '.'
|
||||||
|
import { DEFAULT_TOOL_ORDER } from './inputTools'
|
||||||
import { INITIAL_PROVIDERS, moveProvider } from './llm'
|
import { INITIAL_PROVIDERS, moveProvider } from './llm'
|
||||||
import { mcpSlice } from './mcp'
|
import { mcpSlice } from './mcp'
|
||||||
import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings'
|
import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings'
|
||||||
@ -1455,6 +1456,15 @@ const migrateConfig = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'108': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
state.inputTools.toolOrder = DEFAULT_TOOL_ORDER
|
||||||
|
state.inputTools.isCollapsed = false
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user