mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 07:19:02 +08:00
refactor(inputbar): enforce image upload and model mentioning restrictions (#7314)
* feat(inputbar): feat: enforce image upload restrictions - allow image uploads when mentioning vision models - disallow image uploads when non-vision models are mentioned * refactor(Inputbar): improve handleDrop * fix(Inputbar): Quick panel does not refresh when file changes * fix(AttachmentButton): Fix the conditional judgment logic when mentionedModels is optional * stash * fix(Inputbar): Fix the issue where quickPanel does not close when files are updated Use useRef to track changes in files, ensuring that quickPanel is properly closed when files are updated * refactor(Inputbar): 重构附件按钮和工具条逻辑,简化文件类型支持判断 将文件类型支持判断逻辑从组件中提取到父组件,通过props传递couldAddImageFile和extensions 移除不必要的依赖和计算,优化组件性能 * fix(Inputbar): 修正文件上传逻辑并重命名快速面板方法 修复couldAddTextFile条件判断错误 将openQuickPanel重命名为openAttachmentQuickPanel以明确功能 * feat(MessageEditor): 添加基于话题ID的文件类型限制功能 根据关联消息的模型类型动态限制可添加的文件类型 * fix(MessageEditor): 仅在用户消息时显示附件按钮 根据消息角色决定是否显示附件按钮,避免非用户消息出现不必要的附件功能 * feat(MessageMenu): 添加模型筛选功能以支持视觉模型选择 根据关联消息内容动态筛选可提及的模型 当用户消息包含图片时仅显示视觉模型 * fix: 修复模型过滤器默认值处理 修复SelectModelPopup组件中modelFilter未传入时的默认值处理,使用默认值会导致卡死 * feat(输入栏): 添加模型集合功能并优化文件类型支持 添加 isVisionModels 和 isGenerateImageModels 工具函数用于判断模型集合 优化输入栏对文件类型的支持逻辑,重命名 supportExts 为 supportedExts 移除调试日志并简化模型支持判断逻辑 * refactor(Inputbar): 移除未使用的model属性并优化代码结构 清理AttachmentButton和InputbarTools组件中未使用的model属性 优化MessageEditor中的状态管理,使用useAppSelector替代store.getState 修复拼写错误(failback -> fallback)
This commit is contained in:
parent
68d0b13a64
commit
f500cc6c9a
@ -34,13 +34,15 @@ const ITEM_HEIGHT = 36
|
|||||||
|
|
||||||
interface PopupParams {
|
interface PopupParams {
|
||||||
model?: Model
|
model?: Model
|
||||||
|
modelFilter?: (model: Model) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends PopupParams {
|
interface Props extends PopupParams {
|
||||||
resolve: (value: Model | undefined) => void
|
resolve: (value: Model | undefined) => void
|
||||||
|
modelFilter?: (model: Model) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ model, resolve, modelFilter }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const { pinnedModels, togglePinnedModel, loading } = usePinnedModels()
|
const { pinnedModels, togglePinnedModel, loading } = usePinnedModels()
|
||||||
@ -156,7 +158,10 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
// 添加置顶模型分组(仅在无搜索文本时)
|
// 添加置顶模型分组(仅在无搜索文本时)
|
||||||
if (searchText.length === 0 && pinnedModels.length > 0) {
|
if (searchText.length === 0 && pinnedModels.length > 0) {
|
||||||
const pinnedItems = providers.flatMap((p) =>
|
const pinnedItems = providers.flatMap((p) =>
|
||||||
p.models.filter((m) => pinnedModels.includes(getModelUniqId(m))).map((m) => createModelItem(m, p, true))
|
p.models
|
||||||
|
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
.filter(modelFilter ? modelFilter : () => true)
|
||||||
|
.map((m) => createModelItem(m, p, true))
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pinnedItems.length > 0) {
|
if (pinnedItems.length > 0) {
|
||||||
@ -174,9 +179,9 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
|
|
||||||
// 添加常规模型分组
|
// 添加常规模型分组
|
||||||
providers.forEach((p) => {
|
providers.forEach((p) => {
|
||||||
const filteredModels = getFilteredModels(p).filter(
|
const filteredModels = getFilteredModels(p)
|
||||||
(m) => searchText.length > 0 || !pinnedModels.includes(getModelUniqId(m))
|
.filter((m) => searchText.length > 0 || !pinnedModels.includes(getModelUniqId(m)))
|
||||||
)
|
.filter(modelFilter ? modelFilter : () => true)
|
||||||
|
|
||||||
if (filteredModels.length === 0) return
|
if (filteredModels.length === 0) return
|
||||||
|
|
||||||
@ -199,7 +204,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
firstGroupRef.current = null
|
firstGroupRef.current = null
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}, [providers, getFilteredModels, pinnedModels, searchText, t, createModelItem])
|
}, [searchText.length, pinnedModels, providers, modelFilter, createModelItem, t, getFilteredModels])
|
||||||
|
|
||||||
// 获取可选择的模型项(过滤掉分组标题)
|
// 获取可选择的模型项(过滤掉分组标题)
|
||||||
const modelItems = useMemo(() => {
|
const modelItems = useMemo(() => {
|
||||||
|
|||||||
@ -2907,3 +2907,12 @@ export function isDoubaoThinkingAutoModel(model: Model): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$')
|
export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$')
|
||||||
|
|
||||||
|
// 模型集合功能测试
|
||||||
|
export const isVisionModels = (models: Model[]) => {
|
||||||
|
return models.every((model) => isVisionModel(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isGenerateImageModels = (models: Model[]) => {
|
||||||
|
return models.every((model) => isGenerateImageModel(model))
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
|
import { FileType } from '@renderer/types'
|
||||||
import { FileType, Model } from '@renderer/types'
|
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { Paperclip } from 'lucide-react'
|
import { Paperclip } from 'lucide-react'
|
||||||
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
import { FC, useCallback, useImperativeHandle } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface AttachmentButtonRef {
|
export interface AttachmentButtonRef {
|
||||||
@ -12,30 +10,25 @@ export interface AttachmentButtonRef {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ref?: React.RefObject<AttachmentButtonRef | null>
|
ref?: React.RefObject<AttachmentButtonRef | null>
|
||||||
model: Model
|
couldAddImageFile: boolean
|
||||||
|
extensions: string[]
|
||||||
files: FileType[]
|
files: FileType[]
|
||||||
setFiles: (files: FileType[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentButton: FC<Props> = ({ ref, model, files, setFiles, ToolbarButton, disabled }) => {
|
const AttachmentButton: FC<Props> = ({
|
||||||
|
ref,
|
||||||
|
couldAddImageFile,
|
||||||
|
extensions,
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
ToolbarButton,
|
||||||
|
disabled
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// const extensions = useMemo(
|
|
||||||
// () => (isVisionModel(model) ? [...imageExts, ...documentExts, ...textExts] : [...documentExts, ...textExts]),
|
|
||||||
// [model]
|
|
||||||
// )
|
|
||||||
const extensions = useMemo(() => {
|
|
||||||
if (isVisionModel(model)) {
|
|
||||||
return [...imageExts, ...documentExts, ...textExts]
|
|
||||||
} else if (isGenerateImageModel(model)) {
|
|
||||||
return [...imageExts]
|
|
||||||
} else {
|
|
||||||
return [...documentExts, ...textExts]
|
|
||||||
}
|
|
||||||
}, [model])
|
|
||||||
|
|
||||||
const onSelectFile = useCallback(async () => {
|
const onSelectFile = useCallback(async () => {
|
||||||
const _files = await window.api.file.select({
|
const _files = await window.api.file.select({
|
||||||
properties: ['openFile', 'multiSelections'],
|
properties: ['openFile', 'multiSelections'],
|
||||||
@ -61,12 +54,7 @@ const AttachmentButton: FC<Props> = ({ ref, model, files, setFiles, ToolbarButto
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip placement="top" title={couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document')} arrow>
|
||||||
placement="top"
|
|
||||||
title={
|
|
||||||
isVisionModel(model) || isGenerateImageModel(model) ? t('chat.input.upload') : t('chat.input.upload.document')
|
|
||||||
}
|
|
||||||
arrow>
|
|
||||||
<ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
|
<ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
|
||||||
<Paperclip size={18} style={{ color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }} />
|
<Paperclip size={18} style={{ color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
|||||||
@ -4,10 +4,12 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
|||||||
import Logger from '@renderer/config/logger'
|
import Logger from '@renderer/config/logger'
|
||||||
import {
|
import {
|
||||||
isGenerateImageModel,
|
isGenerateImageModel,
|
||||||
|
isGenerateImageModels,
|
||||||
isSupportedDisableGenerationModel,
|
isSupportedDisableGenerationModel,
|
||||||
isSupportedReasoningEffortModel,
|
isSupportedReasoningEffortModel,
|
||||||
isSupportedThinkingTokenModel,
|
isSupportedThinkingTokenModel,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
|
isVisionModels,
|
||||||
isWebSearchModel
|
isWebSearchModel
|
||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
@ -30,12 +32,11 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setSearching } from '@renderer/store/runtime'
|
import { setSearching } from '@renderer/store/runtime'
|
||||||
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
|
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
|
||||||
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
||||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||||
import { formatQuotedText } from '@renderer/utils/formats'
|
import { formatQuotedText } from '@renderer/utils/formats'
|
||||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
@ -95,17 +96,57 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
const [mentionedModels, setMentionedModels] = useState<Model[]>([])
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [isFileDragging, setIsFileDragging] = useState(false)
|
const [isFileDragging, setIsFileDragging] = useState(false)
|
||||||
const [textareaHeight, setTextareaHeight] = useState<number>()
|
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||||
const startDragY = useRef<number>(0)
|
const startDragY = useRef<number>(0)
|
||||||
const startHeight = useRef<number>(0)
|
const startHeight = useRef<number>(0)
|
||||||
const currentMessageId = useRef<string>('')
|
const currentMessageId = useRef<string>('')
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
|
||||||
const { bases: knowledgeBases } = useKnowledgeBases()
|
const { bases: knowledgeBases } = useKnowledgeBases()
|
||||||
const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode)
|
const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode)
|
||||||
|
const isVisionAssistant = useMemo(() => isVisionModel(model), [model])
|
||||||
|
const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model])
|
||||||
|
|
||||||
|
const isVisionSupported = useMemo(
|
||||||
|
() =>
|
||||||
|
(mentionedModels.length > 0 && isVisionModels(mentionedModels)) ||
|
||||||
|
(mentionedModels.length === 0 && isVisionAssistant),
|
||||||
|
[mentionedModels, isVisionAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isGenerateImageSupported = useMemo(
|
||||||
|
() =>
|
||||||
|
(mentionedModels.length > 0 && isGenerateImageModels(mentionedModels)) ||
|
||||||
|
(mentionedModels.length === 0 && isGenerateImageAssistant),
|
||||||
|
[mentionedModels, isGenerateImageAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 仅允许在不含图片文件时mention非视觉模型
|
||||||
|
const couldMentionNotVisionModel = useMemo(() => {
|
||||||
|
return !files.some((file) => file.type === FileTypes.IMAGE)
|
||||||
|
}, [files])
|
||||||
|
|
||||||
|
// 允许在支持视觉或生成图片时添加图片文件
|
||||||
|
const couldAddImageFile = useMemo(() => {
|
||||||
|
return isVisionSupported || isGenerateImageSupported
|
||||||
|
}, [isVisionSupported, isGenerateImageSupported])
|
||||||
|
|
||||||
|
const couldAddTextFile = useMemo(() => {
|
||||||
|
return isVisionSupported || (!isVisionSupported && !isGenerateImageSupported)
|
||||||
|
}, [isGenerateImageSupported, isVisionSupported])
|
||||||
|
|
||||||
|
const supportedExts = useMemo(() => {
|
||||||
|
if (couldAddImageFile && couldAddTextFile) {
|
||||||
|
return [...imageExts, ...documentExts, ...textExts]
|
||||||
|
} else if (couldAddImageFile) {
|
||||||
|
return [...imageExts]
|
||||||
|
} else if (couldAddTextFile) {
|
||||||
|
return [...documentExts, ...textExts]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}, [couldAddImageFile, couldAddTextFile])
|
||||||
|
|
||||||
const quickPanel = useQuickPanel()
|
const quickPanel = useQuickPanel()
|
||||||
|
|
||||||
@ -179,8 +220,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
baseUserMessage.files = uploadedFiles
|
baseUserMessage.files = uploadedFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mentionModels) {
|
if (mentionedModels) {
|
||||||
baseUserMessage.mentions = mentionModels
|
baseUserMessage.mentions = mentionedModels
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantWithTopicPrompt = topic.prompt
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
@ -203,7 +244,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error)
|
console.error('Failed to send message:', error)
|
||||||
}
|
}
|
||||||
}, [assistant, dispatch, files, inputEmpty, loading, mentionModels, resizeTextArea, text, topic])
|
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, text, topic])
|
||||||
|
|
||||||
const translate = useCallback(async () => {
|
const translate = useCallback(async () => {
|
||||||
if (isTranslating) {
|
if (isTranslating) {
|
||||||
@ -267,7 +308,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
description: '',
|
description: '',
|
||||||
icon: <Upload />,
|
icon: <Upload />,
|
||||||
action: () => {
|
action: () => {
|
||||||
inputbarToolsRef.current?.openQuickPanel()
|
inputbarToolsRef.current?.openAttachmentQuickPanel()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...knowledgeBases.map((base) => {
|
...knowledgeBases.map((base) => {
|
||||||
@ -378,8 +419,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) {
|
if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionedModels.length > 0) {
|
||||||
setMentionModels((prev) => prev.slice(0, -1))
|
setMentionedModels((prev) => prev.slice(0, -1))
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,36 +482,39 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
|
|
||||||
const onInput = () => !expended && resizeTextArea()
|
const onInput = () => !expended && resizeTextArea()
|
||||||
|
|
||||||
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = useCallback(
|
||||||
const newText = e.target.value
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setText(newText)
|
const newText = e.target.value
|
||||||
|
setText(newText)
|
||||||
|
|
||||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
const cursorPosition = textArea?.selectionStart ?? 0
|
const cursorPosition = textArea?.selectionStart ?? 0
|
||||||
const lastSymbol = newText[cursorPosition - 1]
|
const lastSymbol = newText[cursorPosition - 1]
|
||||||
|
|
||||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
||||||
const quickPanelMenu =
|
const quickPanelMenu =
|
||||||
inputbarToolsRef.current?.getQuickPanelMenu({
|
inputbarToolsRef.current?.getQuickPanelMenu({
|
||||||
t,
|
t,
|
||||||
files,
|
files,
|
||||||
model,
|
couldAddImageFile,
|
||||||
text: newText,
|
text: newText,
|
||||||
openSelectFileMenu,
|
openSelectFileMenu,
|
||||||
translate
|
translate
|
||||||
}) || []
|
}) || []
|
||||||
|
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
title: t('settings.quickPanel.title'),
|
title: t('settings.quickPanel.title'),
|
||||||
list: quickPanelMenu,
|
list: quickPanelMenu,
|
||||||
symbol: '/'
|
symbol: '/'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
||||||
inputbarToolsRef.current?.openMentionModelsPanel()
|
inputbarToolsRef.current?.openMentionModelsPanel()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[enableQuickPanelTriggers, quickPanel, t, files, couldAddImageFile, openSelectFileMenu, translate]
|
||||||
|
)
|
||||||
|
|
||||||
const onPaste = useCallback(
|
const onPaste = useCallback(
|
||||||
async (event: ClipboardEvent) => {
|
async (event: ClipboardEvent) => {
|
||||||
@ -478,7 +522,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
event,
|
event,
|
||||||
isVisionModel(model),
|
isVisionModel(model),
|
||||||
isGenerateImageModel(model),
|
isGenerateImageModel(model),
|
||||||
supportExts,
|
supportedExts,
|
||||||
setFiles,
|
setFiles,
|
||||||
setText,
|
setText,
|
||||||
pasteLongTextAsFile,
|
pasteLongTextAsFile,
|
||||||
@ -488,7 +532,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
t
|
t
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text]
|
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportedExts, t, text]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
@ -509,35 +553,38 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
setIsFileDragging(false)
|
setIsFileDragging(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDrop = useCallback(
|
||||||
e.preventDefault()
|
async (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.stopPropagation()
|
e.preventDefault()
|
||||||
setIsFileDragging(false)
|
e.stopPropagation()
|
||||||
|
setIsFileDragging(false)
|
||||||
|
|
||||||
const files = await getFilesFromDropEvent(e).catch((err) => {
|
const files = await getFilesFromDropEvent(e).catch((err) => {
|
||||||
Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] handleDrop:', err)
|
Logger.error('[Inputbar] handleDrop:', err)
|
||||||
return null
|
return null
|
||||||
})
|
|
||||||
|
|
||||||
if (files) {
|
|
||||||
let supportedFiles = 0
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (supportExts.includes(getFileExtension(file.path))) {
|
|
||||||
setFiles((prevFiles) => [...prevFiles, file])
|
|
||||||
supportedFiles++
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果有文件,但都不支持
|
if (files) {
|
||||||
if (files.length > 0 && supportedFiles === 0) {
|
let supportedFiles = 0
|
||||||
window.message.info({
|
|
||||||
key: 'file_not_supported',
|
files.forEach((file) => {
|
||||||
content: t('chat.input.file_not_supported')
|
if (supportedExts.includes(getFileExtension(file.path))) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, file])
|
||||||
|
supportedFiles++
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 如果有文件,但都不支持
|
||||||
|
if (files.length > 0 && supportedFiles === 0) {
|
||||||
|
window.message.info({
|
||||||
|
key: 'file_not_supported',
|
||||||
|
content: t('chat.input.file_not_supported')
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
[supportedExts, t]
|
||||||
|
)
|
||||||
|
|
||||||
const onTranslated = (translatedText: string) => {
|
const onTranslated = (translatedText: string) => {
|
||||||
setText(translatedText)
|
setText(translatedText)
|
||||||
@ -684,7 +731,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveModel = (model: Model) => {
|
const handleRemoveModel = (model: Model) => {
|
||||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
setMentionedModels(mentionedModels.filter((m) => m.id !== model.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => {
|
const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => {
|
||||||
@ -715,13 +762,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
}, [assistant, model, updateAssistant])
|
}, [assistant, model, updateAssistant])
|
||||||
|
|
||||||
const onMentionModel = useCallback((model: Model) => {
|
const onMentionModel = useCallback(
|
||||||
setMentionModels((prev) => {
|
(model: Model) => {
|
||||||
const modelId = getModelUniqId(model)
|
// 我想应该没有模型是只支持视觉而不支持文本的?
|
||||||
const exists = prev.some((m) => getModelUniqId(m) === modelId)
|
if (isVisionModel(model) || couldMentionNotVisionModel) {
|
||||||
return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model]
|
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 {
|
||||||
|
console.error('在已上传图片时,不能添加非视觉模型')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[couldMentionNotVisionModel]
|
||||||
|
)
|
||||||
|
|
||||||
const onToggleExpended = () => {
|
const onToggleExpended = () => {
|
||||||
const currentlyExpanded = expended || !!textareaHeight
|
const currentlyExpanded = expended || !!textareaHeight
|
||||||
@ -773,8 +828,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
|
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mentionModels.length > 0 && (
|
{mentionedModels.length > 0 && (
|
||||||
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
<MentionModelsInput selectedModels={mentionedModels} onRemoveModel={handleRemoveModel} />
|
||||||
)}
|
)}
|
||||||
<Textarea
|
<Textarea
|
||||||
value={text}
|
value={text}
|
||||||
@ -819,6 +874,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
assistant={assistant}
|
assistant={assistant}
|
||||||
model={model}
|
model={model}
|
||||||
files={files}
|
files={files}
|
||||||
|
extensions={supportedExts}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
showThinkingButton={showThinkingButton}
|
showThinkingButton={showThinkingButton}
|
||||||
showKnowledgeIcon={showKnowledgeIcon}
|
showKnowledgeIcon={showKnowledgeIcon}
|
||||||
@ -826,8 +882,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
handleKnowledgeBaseSelect={handleKnowledgeBaseSelect}
|
handleKnowledgeBaseSelect={handleKnowledgeBaseSelect}
|
||||||
setText={setText}
|
setText={setText}
|
||||||
resizeTextArea={resizeTextArea}
|
resizeTextArea={resizeTextArea}
|
||||||
mentionModels={mentionModels}
|
mentionModels={mentionedModels}
|
||||||
onMentionModel={onMentionModel}
|
onMentionModel={onMentionModel}
|
||||||
|
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||||
|
couldAddImageFile={couldAddImageFile}
|
||||||
onEnableGenerateImage={onEnableGenerateImage}
|
onEnableGenerateImage={onEnableGenerateImage}
|
||||||
isExpended={isExpended}
|
isExpended={isExpended}
|
||||||
onToggleExpended={onToggleExpended}
|
onToggleExpended={onToggleExpended}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||||
import { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
import { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||||
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
|
import { isGenerateImageModel } from '@renderer/config/models'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools'
|
import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools'
|
||||||
import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types'
|
import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types'
|
||||||
@ -42,21 +42,21 @@ export interface InputbarToolsRef {
|
|||||||
getQuickPanelMenu: (params: {
|
getQuickPanelMenu: (params: {
|
||||||
t: (key: string, options?: any) => string
|
t: (key: string, options?: any) => string
|
||||||
files: FileType[]
|
files: FileType[]
|
||||||
model: Model
|
couldAddImageFile: boolean
|
||||||
text: string
|
text: string
|
||||||
openSelectFileMenu: () => void
|
openSelectFileMenu: () => void
|
||||||
translate: () => void
|
translate: () => void
|
||||||
}) => QuickPanelListItem[]
|
}) => QuickPanelListItem[]
|
||||||
openMentionModelsPanel: () => void
|
openMentionModelsPanel: () => void
|
||||||
openQuickPanel: () => void
|
openAttachmentQuickPanel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputbarToolsProps {
|
export interface InputbarToolsProps {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
model: Model
|
model: Model
|
||||||
|
|
||||||
files: FileType[]
|
files: FileType[]
|
||||||
setFiles: (files: FileType[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
|
extensions: string[]
|
||||||
showThinkingButton: boolean
|
showThinkingButton: boolean
|
||||||
showKnowledgeIcon: boolean
|
showKnowledgeIcon: boolean
|
||||||
selectedKnowledgeBases: KnowledgeBase[]
|
selectedKnowledgeBases: KnowledgeBase[]
|
||||||
@ -65,6 +65,8 @@ export interface InputbarToolsProps {
|
|||||||
resizeTextArea: () => void
|
resizeTextArea: () => void
|
||||||
mentionModels: Model[]
|
mentionModels: Model[]
|
||||||
onMentionModel: (model: Model) => void
|
onMentionModel: (model: Model) => void
|
||||||
|
couldMentionNotVisionModel: boolean
|
||||||
|
couldAddImageFile: boolean
|
||||||
onEnableGenerateImage: () => void
|
onEnableGenerateImage: () => void
|
||||||
isExpended: boolean
|
isExpended: boolean
|
||||||
onToggleExpended: () => void
|
onToggleExpended: () => void
|
||||||
@ -104,6 +106,8 @@ const InputbarTools = ({
|
|||||||
resizeTextArea,
|
resizeTextArea,
|
||||||
mentionModels,
|
mentionModels,
|
||||||
onMentionModel,
|
onMentionModel,
|
||||||
|
couldMentionNotVisionModel,
|
||||||
|
couldAddImageFile,
|
||||||
onEnableGenerateImage,
|
onEnableGenerateImage,
|
||||||
isExpended,
|
isExpended,
|
||||||
onToggleExpended,
|
onToggleExpended,
|
||||||
@ -111,7 +115,8 @@ const InputbarTools = ({
|
|||||||
clearTopic,
|
clearTopic,
|
||||||
onNewContext,
|
onNewContext,
|
||||||
newTopicShortcut,
|
newTopicShortcut,
|
||||||
cleanTopicShortcut
|
cleanTopicShortcut,
|
||||||
|
extensions
|
||||||
}: InputbarToolsProps & { ref?: React.RefObject<InputbarToolsRef | null> }) => {
|
}: InputbarToolsProps & { ref?: React.RefObject<InputbarToolsRef | null> }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -153,12 +158,12 @@ const InputbarTools = ({
|
|||||||
const getQuickPanelMenuImpl = (params: {
|
const getQuickPanelMenuImpl = (params: {
|
||||||
t: (key: string, options?: any) => string
|
t: (key: string, options?: any) => string
|
||||||
files: FileType[]
|
files: FileType[]
|
||||||
model: Model
|
couldAddImageFile: boolean
|
||||||
text: string
|
text: string
|
||||||
openSelectFileMenu: () => void
|
openSelectFileMenu: () => void
|
||||||
translate: () => void
|
translate: () => void
|
||||||
}): QuickPanelListItem[] => {
|
}): QuickPanelListItem[] => {
|
||||||
const { t, files, model, text, openSelectFileMenu, translate } = params
|
const { t, files, couldAddImageFile, text, openSelectFileMenu, translate } = params
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -226,7 +231,7 @@ const InputbarTools = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
label: couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||||
description: '',
|
description: '',
|
||||||
icon: <Paperclip />,
|
icon: <Paperclip />,
|
||||||
isMenu: true,
|
isMenu: true,
|
||||||
@ -276,7 +281,7 @@ const InputbarTools = ({
|
|||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
getQuickPanelMenu: getQuickPanelMenuImpl,
|
getQuickPanelMenu: getQuickPanelMenuImpl,
|
||||||
openMentionModelsPanel: () => mentionModelsButtonRef.current?.openQuickPanel(),
|
openMentionModelsPanel: () => mentionModelsButtonRef.current?.openQuickPanel(),
|
||||||
openQuickPanel: () => attachmentButtonRef.current?.openQuickPanel()
|
openAttachmentQuickPanel: () => attachmentButtonRef.current?.openQuickPanel()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toolButtons = useMemo<ToolButtonConfig[]>(() => {
|
const toolButtons = useMemo<ToolButtonConfig[]>(() => {
|
||||||
@ -298,7 +303,8 @@ const InputbarTools = ({
|
|||||||
component: (
|
component: (
|
||||||
<AttachmentButton
|
<AttachmentButton
|
||||||
ref={attachmentButtonRef}
|
ref={attachmentButtonRef}
|
||||||
model={model}
|
couldAddImageFile={couldAddImageFile}
|
||||||
|
extensions={extensions}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
@ -364,9 +370,11 @@ const InputbarTools = ({
|
|||||||
component: (
|
component: (
|
||||||
<MentionModelsButton
|
<MentionModelsButton
|
||||||
ref={mentionModelsButtonRef}
|
ref={mentionModelsButtonRef}
|
||||||
mentionModels={mentionModels}
|
mentionedModels={mentionModels}
|
||||||
onMentionModel={onMentionModel}
|
onMentionModel={onMentionModel}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
|
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||||
|
files={files}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -416,6 +424,9 @@ const InputbarTools = ({
|
|||||||
assistant,
|
assistant,
|
||||||
cleanTopicShortcut,
|
cleanTopicShortcut,
|
||||||
clearTopic,
|
clearTopic,
|
||||||
|
couldAddImageFile,
|
||||||
|
couldMentionNotVisionModel,
|
||||||
|
extensions,
|
||||||
files,
|
files,
|
||||||
handleKnowledgeBaseSelect,
|
handleKnowledgeBaseSelect,
|
||||||
isExpended,
|
isExpended,
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||||
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
|
import { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
|
||||||
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
import { getModelLogo, isEmbeddingModel, isRerankModel, isVisionModel } from '@renderer/config/models'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { Model } from '@renderer/types'
|
import { FileType, Model } from '@renderer/types'
|
||||||
import { Avatar, Tooltip } from 'antd'
|
import { Avatar, Tooltip } from 'antd'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { first, sortBy } from 'lodash'
|
import { first, sortBy } from 'lodash'
|
||||||
import { AtSign, Plus } from 'lucide-react'
|
import { AtSign, Plus } from 'lucide-react'
|
||||||
import { FC, memo, useCallback, useImperativeHandle, useMemo } from 'react'
|
import { FC, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -21,12 +21,21 @@ export interface MentionModelsButtonRef {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ref?: React.RefObject<MentionModelsButtonRef | null>
|
ref?: React.RefObject<MentionModelsButtonRef | null>
|
||||||
mentionModels: Model[]
|
mentionedModels: Model[]
|
||||||
onMentionModel: (model: Model) => void
|
onMentionModel: (model: Model) => void
|
||||||
|
couldMentionNotVisionModel: boolean
|
||||||
|
files: FileType[]
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, ToolbarButton }) => {
|
const MentionModelsButton: FC<Props> = ({
|
||||||
|
ref,
|
||||||
|
mentionedModels,
|
||||||
|
onMentionModel,
|
||||||
|
couldMentionNotVisionModel,
|
||||||
|
files,
|
||||||
|
ToolbarButton
|
||||||
|
}) => {
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -49,6 +58,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
p.models
|
p.models
|
||||||
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||||
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
.filter((m) => couldMentionNotVisionModel || (!couldMentionNotVisionModel && isVisionModel(m)))
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
@ -64,7 +74,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
),
|
),
|
||||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||||
action: () => onMentionModel(m),
|
action: () => onMentionModel(m),
|
||||||
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,7 +87,8 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
const providerModels = sortBy(
|
const providerModels = sortBy(
|
||||||
p.models
|
p.models
|
||||||
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||||
.filter((m) => !pinnedModels.includes(getModelUniqId(m))),
|
.filter((m) => !pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
.filter((m) => couldMentionNotVisionModel || (!couldMentionNotVisionModel && isVisionModel(m))),
|
||||||
['group', 'name']
|
['group', 'name']
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,7 +107,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
),
|
),
|
||||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||||
action: () => onMentionModel(m),
|
action: () => onMentionModel(m),
|
||||||
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (providerModelItems.length > 0) {
|
if (providerModelItems.length > 0) {
|
||||||
@ -112,7 +123,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}, [providers, t, pinnedModels, mentionModels, onMentionModel, navigate])
|
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
|
||||||
|
|
||||||
const openQuickPanel = useCallback(() => {
|
const openQuickPanel = useCallback(() => {
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
@ -134,6 +145,18 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
}
|
}
|
||||||
}, [openQuickPanel, quickPanel])
|
}, [openQuickPanel, quickPanel])
|
||||||
|
|
||||||
|
const filesRef = useRef(files)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查files是否变化
|
||||||
|
if (filesRef.current !== files) {
|
||||||
|
if (quickPanel.isVisible && quickPanel.symbol === '@') {
|
||||||
|
quickPanel.close()
|
||||||
|
}
|
||||||
|
filesRef.current = files
|
||||||
|
}
|
||||||
|
}, [files, quickPanel])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openQuickPanel
|
openQuickPanel
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -147,6 +147,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
{isEditing && (
|
{isEditing && (
|
||||||
<MessageEditor
|
<MessageEditor
|
||||||
message={message}
|
message={message}
|
||||||
|
topicId={topic.id}
|
||||||
onSave={handleEditSave}
|
onSave={handleEditSave}
|
||||||
onResend={handleEditResend}
|
onResend={handleEditResend}
|
||||||
onCancel={handleEditCancel}
|
onCancel={handleEditCancel}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import PasteService from '@renderer/services/PasteService'
|
import PasteService from '@renderer/services/PasteService'
|
||||||
|
import { useAppSelector } from '@renderer/store'
|
||||||
|
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
import { FileType, FileTypes } from '@renderer/types'
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import { classNames, getFileExtension } from '@renderer/utils'
|
import { classNames, getFileExtension } from '@renderer/utils'
|
||||||
@ -25,12 +27,13 @@ import { ToolbarButton } from '../Inputbar/Inputbar'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
|
topicId: string
|
||||||
onSave: (blocks: MessageBlock[]) => void
|
onSave: (blocks: MessageBlock[]) => void
|
||||||
onResend: (blocks: MessageBlock[]) => void
|
onResend: (blocks: MessageBlock[]) => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel }) => {
|
const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onCancel }) => {
|
||||||
const allBlocks = findAllBlocks(message)
|
const allBlocks = findAllBlocks(message)
|
||||||
const [editedBlocks, setEditedBlocks] = useState<MessageBlock[]>(allBlocks)
|
const [editedBlocks, setEditedBlocks] = useState<MessageBlock[]>(allBlocks)
|
||||||
const [files, setFiles] = useState<FileType[]>([])
|
const [files, setFiles] = useState<FileType[]>([])
|
||||||
@ -44,6 +47,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const textareaRef = useRef<TextAreaRef>(null)
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||||
|
const isUserMessage = message.role === 'user'
|
||||||
|
|
||||||
const resizeTextArea = useCallback(() => {
|
const resizeTextArea = useCallback(() => {
|
||||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
@ -205,6 +209,52 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const topicMessages = useAppSelector((state) => selectMessagesForTopic(state, topicId))
|
||||||
|
|
||||||
|
const couldAddImageFile = useMemo(() => {
|
||||||
|
const relatedAssistantMessages = topicMessages.filter((m) => m.askId === message.id && m.role === 'assistant')
|
||||||
|
if (relatedAssistantMessages.length === 0) {
|
||||||
|
// 无关联消息时fallback到助手模型
|
||||||
|
return isVisionModel(model)
|
||||||
|
}
|
||||||
|
return relatedAssistantMessages.every((m) => {
|
||||||
|
if (m.model) {
|
||||||
|
return isVisionModel(m.model)
|
||||||
|
} else {
|
||||||
|
// 若消息关联不存在的模型,视为其支持视觉
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [message.id, model, topicMessages])
|
||||||
|
|
||||||
|
const couldAddTextFile = useMemo(() => {
|
||||||
|
const relatedAssistantMessages = topicMessages.filter((m) => m.askId === message.id && m.role === 'assistant')
|
||||||
|
if (relatedAssistantMessages.length === 0) {
|
||||||
|
// 无关联消息时fallback到助手模型
|
||||||
|
return isVisionModel(model) || (!isVisionModel(model) && !isGenerateImageModel(model))
|
||||||
|
}
|
||||||
|
return relatedAssistantMessages.every((m) => {
|
||||||
|
if (m.model) {
|
||||||
|
return isVisionModel(m.model) || (!isVisionModel(m.model) && !isGenerateImageModel(m.model))
|
||||||
|
} else {
|
||||||
|
// 若消息关联不存在的模型,视为其支持文本
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [message.id, model, topicMessages])
|
||||||
|
|
||||||
|
const extensions = useMemo(() => {
|
||||||
|
if (couldAddImageFile && couldAddTextFile) {
|
||||||
|
return [...imageExts, ...documentExts, ...textExts]
|
||||||
|
} else if (couldAddImageFile) {
|
||||||
|
return [...imageExts]
|
||||||
|
} else if (couldAddTextFile) {
|
||||||
|
return [...documentExts, ...textExts]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}, [couldAddImageFile, couldAddTextFile])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||||
{editedBlocks
|
{editedBlocks
|
||||||
@ -273,13 +323,16 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
|
|
||||||
<ActionBar>
|
<ActionBar>
|
||||||
<ActionBarLeft>
|
<ActionBarLeft>
|
||||||
<AttachmentButton
|
{isUserMessage && (
|
||||||
ref={attachmentButtonRef}
|
<AttachmentButton
|
||||||
model={model}
|
ref={attachmentButtonRef}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
ToolbarButton={ToolbarButton}
|
couldAddImageFile={couldAddImageFile}
|
||||||
/>
|
extensions={extensions}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ActionBarLeft>
|
</ActionBarLeft>
|
||||||
<ActionBarMiddle />
|
<ActionBarMiddle />
|
||||||
<ActionBarRight>
|
<ActionBarRight>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||||
@ -11,9 +12,9 @@ import { getMessageTitle } from '@renderer/services/MessagesService'
|
|||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import store, { RootState } from '@renderer/store'
|
import store, { RootState } from '@renderer/store'
|
||||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
import type { Model } from '@renderer/types'
|
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
import type { Assistant, Topic } from '@renderer/types'
|
import type { Assistant, Model, Topic } from '@renderer/types'
|
||||||
import type { Message } from '@renderer/types/newMessage'
|
import { type Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
||||||
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
||||||
import {
|
import {
|
||||||
@ -29,8 +30,20 @@ import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
|||||||
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||||
import { Dropdown, Popconfirm, Tooltip } from 'antd'
|
import { Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { AtSign, Copy, Languages, ListChecks, Menu, RefreshCw, Save, Share, Split, ThumbsUp, Trash } from 'lucide-react'
|
import {
|
||||||
import { FilePenLine } from 'lucide-react'
|
AtSign,
|
||||||
|
Copy,
|
||||||
|
FilePenLine,
|
||||||
|
Languages,
|
||||||
|
ListChecks,
|
||||||
|
Menu,
|
||||||
|
RefreshCw,
|
||||||
|
Save,
|
||||||
|
Share,
|
||||||
|
Split,
|
||||||
|
ThumbsUp,
|
||||||
|
Trash
|
||||||
|
} from 'lucide-react'
|
||||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
@ -327,13 +340,43 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
regenerateAssistantMessage(message, assistantWithTopicPrompt)
|
regenerateAssistantMessage(message, assistantWithTopicPrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = async (e: React.MouseEvent) => {
|
// 按条件筛选能够提及的模型,该函数仅在isAssistantMessage时会用到
|
||||||
e.stopPropagation()
|
const mentionModelFilter = useMemo(() => {
|
||||||
if (loading) return
|
if (!isAssistantMessage) {
|
||||||
const selectedModel = await SelectModelPopup.show({ model })
|
return () => true
|
||||||
if (!selectedModel) return
|
}
|
||||||
appendAssistantResponse(message, selectedModel, { ...assistant, model: selectedModel })
|
const state = store.getState()
|
||||||
}
|
const topicMessages = selectMessagesForTopic(state, topic.id)
|
||||||
|
// 理论上助手消息只会关联一条用户消息
|
||||||
|
const relatedUserMessage = topicMessages.find((msg) => {
|
||||||
|
return msg.role === 'user' && message.askId === msg.id
|
||||||
|
})
|
||||||
|
// 无关联用户消息时,默认返回所有模型
|
||||||
|
if (!relatedUserMessage) {
|
||||||
|
return () => true
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatedUserMessageBlocks = relatedUserMessage.blocks.map((msgBlockId) =>
|
||||||
|
messageBlocksSelectors.selectById(store.getState(), msgBlockId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relatedUserMessageBlocks.some((block) => block.type === MessageBlockType.IMAGE)) {
|
||||||
|
return (m: Model) => isVisionModel(m)
|
||||||
|
} else {
|
||||||
|
return () => true
|
||||||
|
}
|
||||||
|
}, [isAssistantMessage, message.askId, topic.id])
|
||||||
|
|
||||||
|
const onMentionModel = useCallback(
|
||||||
|
async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (loading) return
|
||||||
|
const selectedModel = await SelectModelPopup.show({ model, modelFilter: mentionModelFilter })
|
||||||
|
if (!selectedModel) return
|
||||||
|
appendAssistantResponse(message, selectedModel, { ...assistant, model: selectedModel })
|
||||||
|
},
|
||||||
|
[appendAssistantResponse, assistant, loading, mentionModelFilter, message, model]
|
||||||
|
)
|
||||||
|
|
||||||
const onUseful = useCallback(
|
const onUseful = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
|||||||
@ -17,8 +17,7 @@ import type {
|
|||||||
PlaceholderMessageBlock,
|
PlaceholderMessageBlock,
|
||||||
ToolMessageBlock
|
ToolMessageBlock
|
||||||
} from '@renderer/types/newMessage'
|
} from '@renderer/types/newMessage'
|
||||||
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType, Response } from '@renderer/types/newMessage'
|
||||||
import { Response } from '@renderer/types/newMessage'
|
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
|
import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
|
||||||
import {
|
import {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user