mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +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 {
|
||||
model?: Model
|
||||
modelFilter?: (model: Model) => boolean
|
||||
}
|
||||
|
||||
interface Props extends PopupParams {
|
||||
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 { providers } = useProviders()
|
||||
const { pinnedModels, togglePinnedModel, loading } = usePinnedModels()
|
||||
@ -156,7 +158,10 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
||||
// 添加置顶模型分组(仅在无搜索文本时)
|
||||
if (searchText.length === 0 && pinnedModels.length > 0) {
|
||||
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) {
|
||||
@ -174,9 +179,9 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
||||
|
||||
// 添加常规模型分组
|
||||
providers.forEach((p) => {
|
||||
const filteredModels = getFilteredModels(p).filter(
|
||||
(m) => searchText.length > 0 || !pinnedModels.includes(getModelUniqId(m))
|
||||
)
|
||||
const filteredModels = getFilteredModels(p)
|
||||
.filter((m) => searchText.length > 0 || !pinnedModels.includes(getModelUniqId(m)))
|
||||
.filter(modelFilter ? modelFilter : () => true)
|
||||
|
||||
if (filteredModels.length === 0) return
|
||||
|
||||
@ -199,7 +204,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
||||
firstGroupRef.current = null
|
||||
}
|
||||
return items
|
||||
}, [providers, getFilteredModels, pinnedModels, searchText, t, createModelItem])
|
||||
}, [searchText.length, pinnedModels, providers, modelFilter, createModelItem, t, getFilteredModels])
|
||||
|
||||
// 获取可选择的模型项(过滤掉分组标题)
|
||||
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 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, Model } from '@renderer/types'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Paperclip } from 'lucide-react'
|
||||
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||
import { FC, useCallback, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AttachmentButtonRef {
|
||||
@ -12,30 +10,25 @@ export interface AttachmentButtonRef {
|
||||
|
||||
interface Props {
|
||||
ref?: React.RefObject<AttachmentButtonRef | null>
|
||||
model: Model
|
||||
couldAddImageFile: boolean
|
||||
extensions: string[]
|
||||
files: FileType[]
|
||||
setFiles: (files: FileType[]) => void
|
||||
ToolbarButton: any
|
||||
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 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 _files = await window.api.file.select({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
@ -61,12 +54,7 @@ const AttachmentButton: FC<Props> = ({ ref, model, files, setFiles, ToolbarButto
|
||||
}))
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={
|
||||
isVisionModel(model) || isGenerateImageModel(model) ? t('chat.input.upload') : t('chat.input.upload.document')
|
||||
}
|
||||
arrow>
|
||||
<Tooltip placement="top" title={couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document')} arrow>
|
||||
<ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
|
||||
<Paperclip size={18} style={{ color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }} />
|
||||
</ToolbarButton>
|
||||
|
||||
@ -4,10 +4,12 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import Logger from '@renderer/config/logger'
|
||||
import {
|
||||
isGenerateImageModel,
|
||||
isGenerateImageModels,
|
||||
isSupportedDisableGenerationModel,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedThinkingTokenModel,
|
||||
isVisionModel,
|
||||
isVisionModels,
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
@ -30,12 +32,11 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setSearching } from '@renderer/store/runtime'
|
||||
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, FileType, 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 { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||
import { formatQuotedText } from '@renderer/utils/formats'
|
||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
@ -95,17 +96,57 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||
const [mentionedModels, setMentionedModels] = useState<Model[]>([])
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isFileDragging, setIsFileDragging] = useState(false)
|
||||
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||
const startDragY = useRef<number>(0)
|
||||
const startHeight = useRef<number>(0)
|
||||
const currentMessageId = useRef<string>('')
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
const { bases: knowledgeBases } = useKnowledgeBases()
|
||||
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()
|
||||
|
||||
@ -179,8 +220,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
baseUserMessage.files = uploadedFiles
|
||||
}
|
||||
|
||||
if (mentionModels) {
|
||||
baseUserMessage.mentions = mentionModels
|
||||
if (mentionedModels) {
|
||||
baseUserMessage.mentions = mentionedModels
|
||||
}
|
||||
|
||||
const assistantWithTopicPrompt = topic.prompt
|
||||
@ -203,7 +244,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
} catch (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 () => {
|
||||
if (isTranslating) {
|
||||
@ -267,7 +308,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
description: '',
|
||||
icon: <Upload />,
|
||||
action: () => {
|
||||
inputbarToolsRef.current?.openQuickPanel()
|
||||
inputbarToolsRef.current?.openAttachmentQuickPanel()
|
||||
}
|
||||
},
|
||||
...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) {
|
||||
setMentionModels((prev) => prev.slice(0, -1))
|
||||
if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionedModels.length > 0) {
|
||||
setMentionedModels((prev) => prev.slice(0, -1))
|
||||
return event.preventDefault()
|
||||
}
|
||||
|
||||
@ -441,36 +482,39 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
|
||||
const onInput = () => !expended && resizeTextArea()
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newText = e.target.value
|
||||
setText(newText)
|
||||
const onChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newText = e.target.value
|
||||
setText(newText)
|
||||
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
const cursorPosition = textArea?.selectionStart ?? 0
|
||||
const lastSymbol = newText[cursorPosition - 1]
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
const cursorPosition = textArea?.selectionStart ?? 0
|
||||
const lastSymbol = newText[cursorPosition - 1]
|
||||
|
||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
||||
const quickPanelMenu =
|
||||
inputbarToolsRef.current?.getQuickPanelMenu({
|
||||
t,
|
||||
files,
|
||||
model,
|
||||
text: newText,
|
||||
openSelectFileMenu,
|
||||
translate
|
||||
}) || []
|
||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
|
||||
const quickPanelMenu =
|
||||
inputbarToolsRef.current?.getQuickPanelMenu({
|
||||
t,
|
||||
files,
|
||||
couldAddImageFile,
|
||||
text: newText,
|
||||
openSelectFileMenu,
|
||||
translate
|
||||
}) || []
|
||||
|
||||
quickPanel.open({
|
||||
title: t('settings.quickPanel.title'),
|
||||
list: quickPanelMenu,
|
||||
symbol: '/'
|
||||
})
|
||||
}
|
||||
quickPanel.open({
|
||||
title: t('settings.quickPanel.title'),
|
||||
list: quickPanelMenu,
|
||||
symbol: '/'
|
||||
})
|
||||
}
|
||||
|
||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
||||
inputbarToolsRef.current?.openMentionModelsPanel()
|
||||
}
|
||||
}
|
||||
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
|
||||
inputbarToolsRef.current?.openMentionModelsPanel()
|
||||
}
|
||||
},
|
||||
[enableQuickPanelTriggers, quickPanel, t, files, couldAddImageFile, openSelectFileMenu, translate]
|
||||
)
|
||||
|
||||
const onPaste = useCallback(
|
||||
async (event: ClipboardEvent) => {
|
||||
@ -478,7 +522,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
event,
|
||||
isVisionModel(model),
|
||||
isGenerateImageModel(model),
|
||||
supportExts,
|
||||
supportedExts,
|
||||
setFiles,
|
||||
setText,
|
||||
pasteLongTextAsFile,
|
||||
@ -488,7 +532,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
t
|
||||
)
|
||||
},
|
||||
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text]
|
||||
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportedExts, t, text]
|
||||
)
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -509,35 +553,38 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setIsFileDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsFileDragging(false)
|
||||
const handleDrop = useCallback(
|
||||
async (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsFileDragging(false)
|
||||
|
||||
const files = await getFilesFromDropEvent(e).catch((err) => {
|
||||
Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] handleDrop:', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (files) {
|
||||
let supportedFiles = 0
|
||||
|
||||
files.forEach((file) => {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
setFiles((prevFiles) => [...prevFiles, file])
|
||||
supportedFiles++
|
||||
}
|
||||
const files = await getFilesFromDropEvent(e).catch((err) => {
|
||||
Logger.error('[Inputbar] handleDrop:', err)
|
||||
return null
|
||||
})
|
||||
|
||||
// 如果有文件,但都不支持
|
||||
if (files.length > 0 && supportedFiles === 0) {
|
||||
window.message.info({
|
||||
key: 'file_not_supported',
|
||||
content: t('chat.input.file_not_supported')
|
||||
if (files) {
|
||||
let supportedFiles = 0
|
||||
|
||||
files.forEach((file) => {
|
||||
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) => {
|
||||
setText(translatedText)
|
||||
@ -684,7 +731,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
|
||||
const handleRemoveModel = (model: Model) => {
|
||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
||||
setMentionedModels(mentionedModels.filter((m) => m.id !== model.id))
|
||||
}
|
||||
|
||||
const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => {
|
||||
@ -715,13 +762,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
}, [assistant, model, updateAssistant])
|
||||
|
||||
const onMentionModel = useCallback((model: Model) => {
|
||||
setMentionModels((prev) => {
|
||||
const modelId = getModelUniqId(model)
|
||||
const exists = prev.some((m) => getModelUniqId(m) === modelId)
|
||||
return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model]
|
||||
})
|
||||
}, [])
|
||||
const onMentionModel = useCallback(
|
||||
(model: Model) => {
|
||||
// 我想应该没有模型是只支持视觉而不支持文本的?
|
||||
if (isVisionModel(model) || couldMentionNotVisionModel) {
|
||||
setMentionedModels((prev) => {
|
||||
const modelId = getModelUniqId(model)
|
||||
const exists = prev.some((m) => getModelUniqId(m) === modelId)
|
||||
return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model]
|
||||
})
|
||||
} else {
|
||||
console.error('在已上传图片时,不能添加非视觉模型')
|
||||
}
|
||||
},
|
||||
[couldMentionNotVisionModel]
|
||||
)
|
||||
|
||||
const onToggleExpended = () => {
|
||||
const currentlyExpanded = expended || !!textareaHeight
|
||||
@ -773,8 +828,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
|
||||
/>
|
||||
)}
|
||||
{mentionModels.length > 0 && (
|
||||
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
||||
{mentionedModels.length > 0 && (
|
||||
<MentionModelsInput selectedModels={mentionedModels} onRemoveModel={handleRemoveModel} />
|
||||
)}
|
||||
<Textarea
|
||||
value={text}
|
||||
@ -819,6 +874,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
files={files}
|
||||
extensions={supportedExts}
|
||||
setFiles={setFiles}
|
||||
showThinkingButton={showThinkingButton}
|
||||
showKnowledgeIcon={showKnowledgeIcon}
|
||||
@ -826,8 +882,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
handleKnowledgeBaseSelect={handleKnowledgeBaseSelect}
|
||||
setText={setText}
|
||||
resizeTextArea={resizeTextArea}
|
||||
mentionModels={mentionModels}
|
||||
mentionModels={mentionedModels}
|
||||
onMentionModel={onMentionModel}
|
||||
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||
couldAddImageFile={couldAddImageFile}
|
||||
onEnableGenerateImage={onEnableGenerateImage}
|
||||
isExpended={isExpended}
|
||||
onToggleExpended={onToggleExpended}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
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 { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools'
|
||||
import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types'
|
||||
@ -42,21 +42,21 @@ export interface InputbarToolsRef {
|
||||
getQuickPanelMenu: (params: {
|
||||
t: (key: string, options?: any) => string
|
||||
files: FileType[]
|
||||
model: Model
|
||||
couldAddImageFile: boolean
|
||||
text: string
|
||||
openSelectFileMenu: () => void
|
||||
translate: () => void
|
||||
}) => QuickPanelListItem[]
|
||||
openMentionModelsPanel: () => void
|
||||
openQuickPanel: () => void
|
||||
openAttachmentQuickPanel: () => void
|
||||
}
|
||||
|
||||
export interface InputbarToolsProps {
|
||||
assistant: Assistant
|
||||
model: Model
|
||||
|
||||
files: FileType[]
|
||||
setFiles: (files: FileType[]) => void
|
||||
extensions: string[]
|
||||
showThinkingButton: boolean
|
||||
showKnowledgeIcon: boolean
|
||||
selectedKnowledgeBases: KnowledgeBase[]
|
||||
@ -65,6 +65,8 @@ export interface InputbarToolsProps {
|
||||
resizeTextArea: () => void
|
||||
mentionModels: Model[]
|
||||
onMentionModel: (model: Model) => void
|
||||
couldMentionNotVisionModel: boolean
|
||||
couldAddImageFile: boolean
|
||||
onEnableGenerateImage: () => void
|
||||
isExpended: boolean
|
||||
onToggleExpended: () => void
|
||||
@ -104,6 +106,8 @@ const InputbarTools = ({
|
||||
resizeTextArea,
|
||||
mentionModels,
|
||||
onMentionModel,
|
||||
couldMentionNotVisionModel,
|
||||
couldAddImageFile,
|
||||
onEnableGenerateImage,
|
||||
isExpended,
|
||||
onToggleExpended,
|
||||
@ -111,7 +115,8 @@ const InputbarTools = ({
|
||||
clearTopic,
|
||||
onNewContext,
|
||||
newTopicShortcut,
|
||||
cleanTopicShortcut
|
||||
cleanTopicShortcut,
|
||||
extensions
|
||||
}: InputbarToolsProps & { ref?: React.RefObject<InputbarToolsRef | null> }) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
@ -153,12 +158,12 @@ const InputbarTools = ({
|
||||
const getQuickPanelMenuImpl = (params: {
|
||||
t: (key: string, options?: any) => string
|
||||
files: FileType[]
|
||||
model: Model
|
||||
couldAddImageFile: boolean
|
||||
text: string
|
||||
openSelectFileMenu: () => void
|
||||
translate: () => void
|
||||
}): QuickPanelListItem[] => {
|
||||
const { t, files, model, text, openSelectFileMenu, translate } = params
|
||||
const { t, files, couldAddImageFile, text, openSelectFileMenu, translate } = params
|
||||
|
||||
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: '',
|
||||
icon: <Paperclip />,
|
||||
isMenu: true,
|
||||
@ -276,7 +281,7 @@ const InputbarTools = ({
|
||||
useImperativeHandle(ref, () => ({
|
||||
getQuickPanelMenu: getQuickPanelMenuImpl,
|
||||
openMentionModelsPanel: () => mentionModelsButtonRef.current?.openQuickPanel(),
|
||||
openQuickPanel: () => attachmentButtonRef.current?.openQuickPanel()
|
||||
openAttachmentQuickPanel: () => attachmentButtonRef.current?.openQuickPanel()
|
||||
}))
|
||||
|
||||
const toolButtons = useMemo<ToolButtonConfig[]>(() => {
|
||||
@ -298,7 +303,8 @@ const InputbarTools = ({
|
||||
component: (
|
||||
<AttachmentButton
|
||||
ref={attachmentButtonRef}
|
||||
model={model}
|
||||
couldAddImageFile={couldAddImageFile}
|
||||
extensions={extensions}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
@ -364,9 +370,11 @@ const InputbarTools = ({
|
||||
component: (
|
||||
<MentionModelsButton
|
||||
ref={mentionModelsButtonRef}
|
||||
mentionModels={mentionModels}
|
||||
mentionedModels={mentionModels}
|
||||
onMentionModel={onMentionModel}
|
||||
ToolbarButton={ToolbarButton}
|
||||
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||
files={files}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@ -416,6 +424,9 @@ const InputbarTools = ({
|
||||
assistant,
|
||||
cleanTopicShortcut,
|
||||
clearTopic,
|
||||
couldAddImageFile,
|
||||
couldMentionNotVisionModel,
|
||||
extensions,
|
||||
files,
|
||||
handleKnowledgeBaseSelect,
|
||||
isExpended,
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
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 { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { FileType, Model } from '@renderer/types'
|
||||
import { Avatar, Tooltip } from 'antd'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { first, sortBy } from 'lodash'
|
||||
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 { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
@ -21,12 +21,21 @@ export interface MentionModelsButtonRef {
|
||||
|
||||
interface Props {
|
||||
ref?: React.RefObject<MentionModelsButtonRef | null>
|
||||
mentionModels: Model[]
|
||||
mentionedModels: Model[]
|
||||
onMentionModel: (model: Model) => void
|
||||
couldMentionNotVisionModel: boolean
|
||||
files: FileType[]
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, ToolbarButton }) => {
|
||||
const MentionModelsButton: FC<Props> = ({
|
||||
ref,
|
||||
mentionedModels,
|
||||
onMentionModel,
|
||||
couldMentionNotVisionModel,
|
||||
files,
|
||||
ToolbarButton
|
||||
}) => {
|
||||
const { providers } = useProviders()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
@ -49,6 +58,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
p.models
|
||||
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||
.filter((m) => couldMentionNotVisionModel || (!couldMentionNotVisionModel && isVisionModel(m)))
|
||||
.map((m) => ({
|
||||
label: (
|
||||
<>
|
||||
@ -64,7 +74,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
),
|
||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||
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(
|
||||
p.models
|
||||
.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']
|
||||
)
|
||||
|
||||
@ -96,7 +107,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
),
|
||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||
action: () => onMentionModel(m),
|
||||
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
}))
|
||||
|
||||
if (providerModelItems.length > 0) {
|
||||
@ -112,7 +123,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
})
|
||||
|
||||
return items
|
||||
}, [providers, t, pinnedModels, mentionModels, onMentionModel, navigate])
|
||||
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
|
||||
|
||||
const openQuickPanel = useCallback(() => {
|
||||
quickPanel.open({
|
||||
@ -134,6 +145,18 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
}
|
||||
}, [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, () => ({
|
||||
openQuickPanel
|
||||
}))
|
||||
|
||||
@ -147,6 +147,7 @@ const MessageItem: FC<Props> = ({
|
||||
{isEditing && (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
topicId={topic.id}
|
||||
onSave={handleEditSave}
|
||||
onResend={handleEditResend}
|
||||
onCancel={handleEditCancel}
|
||||
|
||||
@ -5,6 +5,8 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { classNames, getFileExtension } from '@renderer/utils'
|
||||
@ -25,12 +27,13 @@ import { ToolbarButton } from '../Inputbar/Inputbar'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
topicId: string
|
||||
onSave: (blocks: MessageBlock[]) => void
|
||||
onResend: (blocks: MessageBlock[]) => 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 [editedBlocks, setEditedBlocks] = useState<MessageBlock[]>(allBlocks)
|
||||
const [files, setFiles] = useState<FileType[]>([])
|
||||
@ -44,6 +47,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
||||
const { t } = useTranslation()
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
const resizeTextArea = useCallback(() => {
|
||||
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 (
|
||||
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||
{editedBlocks
|
||||
@ -273,13 +323,16 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
||||
|
||||
<ActionBar>
|
||||
<ActionBarLeft>
|
||||
<AttachmentButton
|
||||
ref={attachmentButtonRef}
|
||||
model={model}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
{isUserMessage && (
|
||||
<AttachmentButton
|
||||
ref={attachmentButtonRef}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
couldAddImageFile={couldAddImageFile}
|
||||
extensions={extensions}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
)}
|
||||
</ActionBarLeft>
|
||||
<ActionBarMiddle />
|
||||
<ActionBarRight>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
@ -11,9 +12,9 @@ import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import type { Model } from '@renderer/types'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import type { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { type Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
||||
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
||||
import {
|
||||
@ -29,8 +30,20 @@ import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
||||
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import { Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { AtSign, Copy, Languages, ListChecks, Menu, RefreshCw, Save, Share, Split, ThumbsUp, Trash } from 'lucide-react'
|
||||
import { FilePenLine } from 'lucide-react'
|
||||
import {
|
||||
AtSign,
|
||||
Copy,
|
||||
FilePenLine,
|
||||
Languages,
|
||||
ListChecks,
|
||||
Menu,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Share,
|
||||
Split,
|
||||
ThumbsUp,
|
||||
Trash
|
||||
} from 'lucide-react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -327,13 +340,43 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
regenerateAssistantMessage(message, assistantWithTopicPrompt)
|
||||
}
|
||||
|
||||
const onMentionModel = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (loading) return
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
if (!selectedModel) return
|
||||
appendAssistantResponse(message, selectedModel, { ...assistant, model: selectedModel })
|
||||
}
|
||||
// 按条件筛选能够提及的模型,该函数仅在isAssistantMessage时会用到
|
||||
const mentionModelFilter = useMemo(() => {
|
||||
if (!isAssistantMessage) {
|
||||
return () => true
|
||||
}
|
||||
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(
|
||||
(e: React.MouseEvent) => {
|
||||
|
||||
@ -17,8 +17,7 @@ import type {
|
||||
PlaceholderMessageBlock,
|
||||
ToolMessageBlock
|
||||
} from '@renderer/types/newMessage'
|
||||
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { Response } from '@renderer/types/newMessage'
|
||||
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType, Response } from '@renderer/types/newMessage'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
|
||||
import {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user