mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
* feat: 🎸 使用@呼出模型选择列表 输入第一个字符为@符号的时候可以呼出选择模型的列表 * feat: 🎸 Only one can be chosen at a time 一次只能选择一个模型,选择后自动关闭。选择过的模型不在出现在列表,避免删除模型的时候显示异常。 * fix: 🐛 When choosing the model, Enter will send a message * feat: 🎸 选中的模型显示供应商 * feat: 🎸 pinned module show privoder * feat: 🎸 only selected modle show provider * feat: 🎸 删除@符号以后自动关闭 * feat: 🎸 增加模糊搜索 --------- Co-authored-by: duanyongcheng77 <duanyongcheng77@gmail.com>
This commit is contained in:
parent
ceb97e80ff
commit
5e8d7682f5
@ -85,6 +85,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
||||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||||
|
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@ -165,6 +166,24 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isEnterPressed = event.keyCode == 13
|
const isEnterPressed = event.keyCode == 13
|
||||||
|
|
||||||
|
if (event.key === '@') {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
const cursorPosition = textArea.selectionStart
|
||||||
|
const textBeforeCursor = text.substring(0, cursorPosition)
|
||||||
|
if (cursorPosition === 0 || textBeforeCursor.endsWith(' ')) {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.SHOW_MODEL_SELECTOR)
|
||||||
|
setIsMentionPopupOpen(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape' && isMentionPopupOpen) {
|
||||||
|
setIsMentionPopupOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (autoTranslateWithSpace) {
|
if (autoTranslateWithSpace) {
|
||||||
if (event.key === ' ') {
|
if (event.key === ' ') {
|
||||||
setSpaceClickCount((prev) => prev + 1)
|
setSpaceClickCount((prev) => prev + 1)
|
||||||
@ -193,25 +212,34 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Enter' && isEnterPressed) {
|
if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') {
|
||||||
if (event.shiftKey) {
|
if (isMentionPopupOpen) {
|
||||||
return
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
||||||
|
if (isMentionPopupOpen) {
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
||||||
|
if (isMentionPopupOpen) {
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
||||||
|
if (isMentionPopupOpen) {
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
@ -280,6 +308,23 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
|
|
||||||
const onInput = () => !expended && resizeTextArea()
|
const onInput = () => !expended && resizeTextArea()
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newText = e.target.value
|
||||||
|
setText(newText)
|
||||||
|
|
||||||
|
// Check if @ was deleted
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
const cursorPosition = textArea.selectionStart
|
||||||
|
const textBeforeCursor = newText.substring(0, cursorPosition)
|
||||||
|
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
|
||||||
|
|
||||||
|
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
|
||||||
|
setIsMentionPopupOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onPaste = useCallback(
|
const onPaste = useCallback(
|
||||||
async (event: ClipboardEvent) => {
|
async (event: ClipboardEvent) => {
|
||||||
const clipboardText = event.clipboardData?.getData('text')
|
const clipboardText = event.clipboardData?.getData('text')
|
||||||
@ -420,17 +465,22 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
setSelectedKnowledgeBase(base)
|
setSelectedKnowledgeBase(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = useCallback(
|
const onMentionModel = (model: Model) => {
|
||||||
(model: Model) => {
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
const isSelected = mentionModels.some((m) => m.id === model.id)
|
if (textArea) {
|
||||||
if (isSelected) {
|
const cursorPosition = textArea.selectionStart
|
||||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
const textBeforeCursor = text.substring(0, cursorPosition)
|
||||||
} else {
|
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
|
||||||
setMentionModels([...mentionModels, model])
|
|
||||||
|
if (lastAtIndex !== -1) {
|
||||||
|
const newText = text.substring(0, lastAtIndex) + text.substring(cursorPosition)
|
||||||
|
setText(newText)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[mentionModels]
|
setMentionModels((prev) => [...prev, model])
|
||||||
)
|
setIsMentionPopupOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleRemoveModel = (model: Model) => {
|
const handleRemoveModel = (model: Model) => {
|
||||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
||||||
@ -447,7 +497,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
||||||
<Textarea
|
<Textarea
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|||||||
@ -3,11 +3,12 @@ import ModelTags from '@renderer/components/ModelTags'
|
|||||||
import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
|
import { getModelLogo, isEmbeddingModel } 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 { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { Model } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||||
import { first, sortBy } from 'lodash'
|
import { first, sortBy } from 'lodash'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled, { createGlobalStyle } from 'styled-components'
|
import styled, { createGlobalStyle } from 'styled-components'
|
||||||
|
|
||||||
@ -17,18 +18,15 @@ interface Props {
|
|||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const MentionModelsButton: FC<Props> = ({ onMentionModel: onSelect, ToolbarButton }) => {
|
const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelect, ToolbarButton }) => {
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const dropdownRef = useRef<any>(null)
|
||||||
useEffect(() => {
|
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||||
const loadPinnedModels = async () => {
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const setting = await db.settings.get('pinned:models')
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
setPinnedModels(setting?.value || [])
|
const [searchText, setSearchText] = useState('')
|
||||||
}
|
|
||||||
loadPinnedModels()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const togglePin = async (modelId: string) => {
|
const togglePin = async (modelId: string) => {
|
||||||
const newPinnedModels = pinnedModels.includes(modelId)
|
const newPinnedModels = pinnedModels.includes(modelId)
|
||||||
@ -39,72 +37,246 @@ const MentionModelsButton: FC<Props> = ({ onMentionModel: onSelect, ToolbarButto
|
|||||||
setPinnedModels(newPinnedModels)
|
setPinnedModels(newPinnedModels)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelMenuItems = providers
|
const handleModelSelect = (model: Model) => {
|
||||||
.filter((p) => p.models && p.models.length > 0)
|
// Check if model is already selected
|
||||||
.map((p) => {
|
if (mentionModels.some((selected) => selected.id === model.id)) {
|
||||||
const filteredModels = sortBy(p.models, ['group', 'name'])
|
return
|
||||||
.filter((m) => !isEmbeddingModel(m))
|
}
|
||||||
|
onSelect(model)
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelMenuItems = useMemo(() => {
|
||||||
|
const items = providers
|
||||||
|
.filter((p) => p.models && p.models.length > 0)
|
||||||
|
.map((p) => {
|
||||||
|
const filteredModels = sortBy(p.models, ['group', 'name'])
|
||||||
|
.filter((m) => !isEmbeddingModel(m))
|
||||||
|
// Filter out pinned models from regular groups
|
||||||
|
.filter((m) => !pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
// Filter by search text
|
||||||
|
.filter((m) => {
|
||||||
|
if (!searchText) return true
|
||||||
|
return (
|
||||||
|
m.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
m.id.toLowerCase().includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map((m) => ({
|
||||||
|
key: getModelUniqId(m),
|
||||||
|
model: m,
|
||||||
|
label: (
|
||||||
|
<ModelItem>
|
||||||
|
<ModelNameRow>
|
||||||
|
<span>{m?.name}</span> <ModelTags model={m} />
|
||||||
|
</ModelNameRow>
|
||||||
|
<PinIcon
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
togglePin(getModelUniqId(m))
|
||||||
|
}}
|
||||||
|
$isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
||||||
|
<PushpinOutlined />
|
||||||
|
</PinIcon>
|
||||||
|
</ModelItem>
|
||||||
|
),
|
||||||
|
icon: (
|
||||||
|
<Avatar src={getModelLogo(m.id)} size={24}>
|
||||||
|
{first(m.name)}
|
||||||
|
</Avatar>
|
||||||
|
),
|
||||||
|
onClick: () => handleModelSelect(m)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return filteredModels.length > 0
|
||||||
|
? {
|
||||||
|
key: p.id,
|
||||||
|
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||||
|
type: 'group' as const,
|
||||||
|
children: filteredModels
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
.filter((group): group is NonNullable<typeof group> => group !== null)
|
||||||
|
|
||||||
|
if (pinnedModels.length > 0) {
|
||||||
|
const pinnedItems = providers
|
||||||
|
.filter((p): p is Provider => p.models && p.models.length > 0)
|
||||||
|
.flatMap((p) =>
|
||||||
|
p.models
|
||||||
|
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
.map((m) => ({
|
||||||
|
key: getModelUniqId(m),
|
||||||
|
model: m,
|
||||||
|
provider: p
|
||||||
|
}))
|
||||||
|
)
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
key: getModelUniqId(m),
|
...m,
|
||||||
|
key: m.key + 'pinned',
|
||||||
label: (
|
label: (
|
||||||
<ModelItem>
|
<ModelItem>
|
||||||
<ModelNameRow>
|
<ModelNameRow>
|
||||||
<span>{m?.name}</span> <ModelTags model={m} />
|
<span>
|
||||||
|
{m.model?.name} | {m.provider.isSystem ? t(`provider.${m.provider.id}`) : m.provider.name}
|
||||||
|
</span>{' '}
|
||||||
|
<ModelTags model={m.model} />
|
||||||
</ModelNameRow>
|
</ModelNameRow>
|
||||||
{/* <Checkbox checked={selectedModels.some((sm) => sm.id === m.id)} /> */}
|
|
||||||
<PinIcon
|
<PinIcon
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
togglePin(getModelUniqId(m))
|
togglePin(getModelUniqId(m.model))
|
||||||
}}
|
}}
|
||||||
$isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
$isPinned={true}>
|
||||||
<PushpinOutlined />
|
<PushpinOutlined />
|
||||||
</PinIcon>
|
</PinIcon>
|
||||||
</ModelItem>
|
</ModelItem>
|
||||||
),
|
),
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar src={getModelLogo(m.id)} size={24}>
|
<Avatar src={getModelLogo(m.model.id)} size={24}>
|
||||||
{first(m.name)}
|
{first(m.model.name)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => handleModelSelect(m.model)
|
||||||
onSelect(m)
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return filteredModels.length > 0
|
if (pinnedItems.length > 0) {
|
||||||
? {
|
items.unshift({
|
||||||
key: p.id,
|
key: 'pinned',
|
||||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
label: t('models.pinned'),
|
||||||
type: 'group' as const,
|
type: 'group' as const,
|
||||||
children: filteredModels
|
children: pinnedItems
|
||||||
}
|
})
|
||||||
: null
|
}
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
if (pinnedModels.length > 0) {
|
|
||||||
const pinnedItems = modelMenuItems
|
|
||||||
.flatMap((p) => p?.children || [])
|
|
||||||
.filter((m) => pinnedModels.includes(m.key))
|
|
||||||
.map((m) => ({ ...m, key: m.key + 'pinned' }))
|
|
||||||
|
|
||||||
if (pinnedItems.length > 0) {
|
|
||||||
modelMenuItems.unshift({
|
|
||||||
key: 'pinned',
|
|
||||||
label: t('models.pinned'),
|
|
||||||
type: 'group' as const,
|
|
||||||
children: pinnedItems
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove empty groups
|
||||||
|
return items.filter((group) => group.children.length > 0)
|
||||||
|
}, [providers, pinnedModels, t, onSelect, mentionModels, searchText])
|
||||||
|
|
||||||
|
// Get flattened list of all model items
|
||||||
|
const flatModelItems = useMemo(() => {
|
||||||
|
return modelMenuItems.flatMap((group) => group?.children || [])
|
||||||
|
}, [modelMenuItems])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPinnedModels = async () => {
|
||||||
|
const setting = await db.settings.get('pinned:models')
|
||||||
|
setPinnedModels(setting?.value || [])
|
||||||
|
}
|
||||||
|
loadPinnedModels()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const showModelSelector = () => {
|
||||||
|
dropdownRef.current?.click()
|
||||||
|
setIsOpen(true)
|
||||||
|
setSelectedIndex(0)
|
||||||
|
setSearchText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!isOpen) return
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev < flatModelItems.length - 1 ? prev + 1 : prev))
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
|
||||||
|
const selectedModel = flatModelItems[selectedIndex].model
|
||||||
|
if (!mentionModels.some((selected) => selected.id === selectedModel.id)) {
|
||||||
|
flatModelItems[selectedIndex].onClick()
|
||||||
|
}
|
||||||
|
setIsOpen(false)
|
||||||
|
setSearchText('')
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsOpen(false)
|
||||||
|
setSearchText('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextChange = (e: Event) => {
|
||||||
|
const textArea = e.target as HTMLTextAreaElement
|
||||||
|
const cursorPosition = textArea.selectionStart
|
||||||
|
const textBeforeCursor = textArea.value.substring(0, cursorPosition)
|
||||||
|
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
|
||||||
|
|
||||||
|
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
|
||||||
|
setIsOpen(false)
|
||||||
|
setSearchText('')
|
||||||
|
} else if (lastAtIndex !== -1) {
|
||||||
|
// Get the text after @ for search
|
||||||
|
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
|
||||||
|
setSearchText(searchStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||||
|
if (textArea) {
|
||||||
|
textArea.addEventListener('input', handleTextChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.on(EVENT_NAMES.SHOW_MODEL_SELECTOR, showModelSelector)
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
EventEmitter.off(EVENT_NAMES.SHOW_MODEL_SELECTOR, showModelSelector)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
if (textArea) {
|
||||||
|
textArea.removeEventListener('input', handleTextChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, selectedIndex, flatModelItems, mentionModels])
|
||||||
|
|
||||||
|
// Hide dropdown if no models available
|
||||||
|
if (flatModelItems.length === 0) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<div ref={menuRef} className="ant-dropdown-menu">
|
||||||
|
{modelMenuItems.map((group, groupIndex) => {
|
||||||
|
if (!group) return null
|
||||||
|
|
||||||
|
// Calculate the starting index for this group's items
|
||||||
|
const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={group.key} className="ant-dropdown-menu-item-group">
|
||||||
|
<div className="ant-dropdown-menu-item-group-title">{group.label}</div>
|
||||||
|
<div>
|
||||||
|
{group.children.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className={`ant-dropdown-menu-item ${selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''}`}
|
||||||
|
onClick={item.onClick}>
|
||||||
|
<span className="ant-dropdown-menu-item-icon">{item.icon}</span>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuStyle />
|
<DropdownMenuStyle />
|
||||||
<Dropdown menu={{ items: modelMenuItems }} trigger={['click']} overlayClassName="mention-models-dropdown">
|
<Dropdown
|
||||||
|
dropdownRender={() => menu}
|
||||||
|
trigger={['click']}
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
overlayClassName="mention-models-dropdown">
|
||||||
<Tooltip placement="top" title={t('agents.edit.model.select.title')} arrow>
|
<Tooltip placement="top" title={t('agents.edit.model.select.title')} arrow>
|
||||||
<ToolbarButton type="text">
|
<ToolbarButton type="text" ref={dropdownRef}>
|
||||||
<i className="iconfont icon-at" style={{ fontSize: 18 }}></i>
|
<i className="iconfont icon-at" style={{ fontSize: 18 }}></i>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -117,6 +289,54 @@ const DropdownMenuStyle = createGlobalStyle`
|
|||||||
.mention-models-dropdown {
|
.mention-models-dropdown {
|
||||||
.ant-dropdown-menu {
|
.ant-dropdown-menu {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-scrollbar);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-item-group {
|
||||||
|
.ant-dropdown-menu-item-group-title {
|
||||||
|
padding: 5px 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-item {
|
||||||
|
padding: 5px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-dropdown-menu-item-selected {
|
||||||
|
background-color: var(--color-primary-bg);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-item-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -127,6 +347,7 @@ const ModelItem = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@ -1,17 +1,27 @@
|
|||||||
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Flex, Tag } from 'antd'
|
import { Flex, Tag } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const MentionModelsInput: FC<{
|
const MentionModelsInput: FC<{
|
||||||
selectedModels: Model[]
|
selectedModels: Model[]
|
||||||
onRemoveModel: (model: Model) => void
|
onRemoveModel: (model: Model) => void
|
||||||
}> = ({ selectedModels, onRemoveModel }) => {
|
}> = ({ selectedModels, onRemoveModel }) => {
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getProviderName = (model: Model) => {
|
||||||
|
const provider = providers.find((p) => p.models?.some((m) => m.id === model.id))
|
||||||
|
return provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container gap="4px 0" wrap>
|
<Container gap="4px 0" wrap>
|
||||||
{selectedModels.map((model) => (
|
{selectedModels.map((model) => (
|
||||||
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
|
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
|
||||||
@{model.name}
|
@{model.name} ({getProviderName(model)})
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -22,5 +22,6 @@ export const EVENT_NAMES = {
|
|||||||
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
|
||||||
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
||||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
||||||
RESEND_MESSAGE: 'RESEND_MESSAGE'
|
RESEND_MESSAGE: 'RESEND_MESSAGE',
|
||||||
|
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR'
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user