From 6d76a23d06b6796b3db898d3064f08cb0363c210 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 29 May 2025 16:40:20 +0800 Subject: [PATCH] refactor: standardize variable naming and improve tag calculation logic - Renamed variables for consistency, changing `AssistantsTabSortType` to `assistantsTabSortType`. - Refactored tag calculation in `useTags` to utilize `uniq` and `flatMap` for better performance and readability. - Updated localization files to remove unnecessary characters in quick trigger messages. - Enhanced the `AssistantItem` component by extracting menu item creation logic and sorting functions for better maintainability. --- src/renderer/src/assets/styles/scrollbar.scss | 2 - .../src/components/Popups/PromptPopup.tsx | 16 +- .../src/components/Popups/TagsPopup.tsx | 70 --- .../src/components/Popups/TextEditPopup.tsx | 24 +- src/renderer/src/hooks/useStore.ts | 4 +- src/renderer/src/hooks/useTags.ts | 63 +-- src/renderer/src/i18n/locales/en-us.json | 8 +- src/renderer/src/i18n/locales/ja-jp.json | 10 +- src/renderer/src/i18n/locales/ru-ru.json | 11 +- src/renderer/src/i18n/locales/zh-cn.json | 10 +- src/renderer/src/i18n/locales/zh-tw.json | 10 +- src/renderer/src/pages/home/Chat.tsx | 9 +- .../src/pages/home/Tabs/AssistantsTab.tsx | 70 +-- .../home/Tabs/components/AssistantItem.tsx | 456 ++++++++++-------- .../Tabs/components/AssistantTagsPopup.tsx | 123 +++++ .../AssistantTagsSettings.tsx | 171 ------- .../settings/AssistantSettings/index.tsx | 8 +- 17 files changed, 487 insertions(+), 578 deletions(-) delete mode 100644 src/renderer/src/components/Popups/TagsPopup.tsx create mode 100644 src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx delete mode 100644 src/renderer/src/pages/settings/AssistantSettings/AssistantTagsSettings.tsx diff --git a/src/renderer/src/assets/styles/scrollbar.scss b/src/renderer/src/assets/styles/scrollbar.scss index cfe3497a18..8e73054c38 100644 --- a/src/renderer/src/assets/styles/scrollbar.scss +++ b/src/renderer/src/assets/styles/scrollbar.scss @@ -24,7 +24,6 @@ body[theme-mode='light'] { } ::-webkit-scrollbar-thumb { - cursor: pointer; border-radius: 10px; background: var(--color-scrollbar-thumb); &:hover { @@ -33,7 +32,6 @@ body[theme-mode='light'] { } pre:not(.shiki)::-webkit-scrollbar-thumb { - cursor: pointer; border-radius: 0; background: rgba(0, 0, 0, 0.08); &:hover { diff --git a/src/renderer/src/components/Popups/PromptPopup.tsx b/src/renderer/src/components/Popups/PromptPopup.tsx index 413aeff1e5..603304c00c 100644 --- a/src/renderer/src/components/Popups/PromptPopup.tsx +++ b/src/renderer/src/components/Popups/PromptPopup.tsx @@ -38,8 +38,9 @@ const PromptPopupContainer: React.FC = ({ setOpen(false) } - const onClose = () => { + const onAfterClose = () => { resolve(null) + TopView.hide(TopViewKey) } const handleAfterOpenChange = (visible: boolean) => { @@ -61,7 +62,7 @@ const PromptPopupContainer: React.FC = ({ open={open} onOk={onOk} onCancel={onCancel} - afterClose={onClose} + afterClose={onAfterClose} afterOpenChange={handleAfterOpenChange} transitionName="animation-move-down" centered> @@ -95,16 +96,7 @@ export default class PromptPopup { } static show(props: PromptPopupShowParams) { return new Promise((resolve) => { - TopView.show( - { - resolve(v) - TopView.hide(TopViewKey) - }} - />, - 'PromptPopup' - ) + TopView.show(, 'PromptPopup') }) } } diff --git a/src/renderer/src/components/Popups/TagsPopup.tsx b/src/renderer/src/components/Popups/TagsPopup.tsx deleted file mode 100644 index 7a4c14a3e6..0000000000 --- a/src/renderer/src/components/Popups/TagsPopup.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import AssistantTagsSettings from '@renderer/pages/settings/AssistantSettings/AssistantTagsSettings' -import { Assistant } from '@renderer/types' -import { Modal } from 'antd' -import { useState } from 'react' - -import { TopView } from '../TopView' - -interface Props { - assistant: Assistant - updateAssistant: (assistant: Assistant) => void - resolve: (data: any) => void - mode?: 'add' | 'manage' -} - -const PopupContainer: React.FC = ({ assistant, updateAssistant, resolve, mode }) => { - const [open, setOpen] = useState(true) - - const onCancel = () => { - setOpen(false) - } - - const onClose = () => { - resolve({}) - } - - TagsPopup.hide = onCancel - - return ( - - - - ) -} - -export default class TagsPopup { - static topviewId = 0 - static hide() { - TopView.hide('TagsPopup') - } - static show(assistant: Assistant, updateAssistant: (assistant: Assistant) => void, mode?: 'add' | 'manage') { - return new Promise((resolve) => { - TopView.show( - { - resolve(v) - TopView.hide('TagsPopup') - }} - mode={mode} - />, - 'TagsPopup' - ) - }) - } -} diff --git a/src/renderer/src/components/Popups/TextEditPopup.tsx b/src/renderer/src/components/Popups/TextEditPopup.tsx index f2ecae4fc6..46bca109fc 100644 --- a/src/renderer/src/components/Popups/TextEditPopup.tsx +++ b/src/renderer/src/components/Popups/TextEditPopup.tsx @@ -18,6 +18,7 @@ interface ShowParams { text: string textareaProps?: TextAreaProps modalProps?: ModalProps + showTranslate?: boolean children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode } @@ -25,7 +26,14 @@ interface Props extends ShowParams { resolve: (data: any) => void } -const PopupContainer: React.FC = ({ text, textareaProps, modalProps, resolve, children }) => { +const PopupContainer: React.FC = ({ + text, + textareaProps, + modalProps, + resolve, + children, + showTranslate = true +}) => { const [open, setOpen] = useState(true) const { t } = useTranslation() const [textValue, setTextValue] = useState(text) @@ -148,12 +156,14 @@ const PopupContainer: React.FC = ({ text, textareaProps, modalProps, reso onInput={resizeTextArea} onChange={(e) => setTextValue(e.target.value)} /> - - {isTranslating ? : } - + {showTranslate && ( + + {isTranslating ? : } + + )} {children && children({ onOk, onCancel })} diff --git a/src/renderer/src/hooks/useStore.ts b/src/renderer/src/hooks/useStore.ts index c426de3f55..1b731e74c7 100644 --- a/src/renderer/src/hooks/useStore.ts +++ b/src/renderer/src/hooks/useStore.ts @@ -31,11 +31,11 @@ export function useShowTopics() { } export function useAssistantsTabSortType() { - const AssistantsTabSortType = useAppSelector((state) => state.settings.assistantsTabSortType) + const assistantsTabSortType = useAppSelector((state) => state.settings.assistantsTabSortType) const dispatch = useAppDispatch() return { - AssistantsTabSortType, + assistantsTabSortType, setAssistantsTabSortType: (sortType: AssistantsSortType) => dispatch(setAssistantsTabSortType(sortType)) } } diff --git a/src/renderer/src/hooks/useTags.ts b/src/renderer/src/hooks/useTags.ts index 2e1a22f069..50b3f2a782 100644 --- a/src/renderer/src/hooks/useTags.ts +++ b/src/renderer/src/hooks/useTags.ts @@ -1,5 +1,5 @@ -import { Assistant } from '@renderer/types' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { flatMap, groupBy, uniq } from 'lodash' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useAssistants } from './useAssistant' @@ -10,63 +10,44 @@ import { useAssistants } from './useAssistant' export const useTags = () => { const { assistants } = useAssistants() - const [allTags, setAllTags] = useState([]) const { t } = useTranslation() // 计算所有标签 - const calculateTags = useCallback(() => { - const tags = new Set() - assistants.forEach((assistant) => { - assistant.tags?.forEach((tag) => tags.add(tag)) - }) - return Array.from(tags) + const allTags = useMemo(() => { + return uniq(flatMap(assistants, (assistant) => assistant.tags || [])) }, [assistants]) - // 当assistants变化时重新计算标签 - useEffect(() => { - setAllTags(calculateTags()) - }, [assistants, calculateTags]) - const getAssistantsByTag = useCallback( - (tag: string) => { - return assistants.filter((assistant) => assistant.tags?.includes(tag)) - }, + (tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)), [assistants] ) - const addTag = useCallback((tag: string) => { - setAllTags((prev) => [...prev, tag]) - }, []) - const getGroupedAssistants = useMemo(() => { - const grouped: { tag: string; assistants: Assistant[] }[] = [] - - allTags.forEach((tag) => { - const taggedAssistants = assistants.filter((a) => a.tags?.includes(tag)) - if (taggedAssistants.length > 0) { - grouped.push({ - tag, - assistants: taggedAssistants.sort((a, b) => a.name.localeCompare(b.name)) - }) - } + // 按标签分组,处理多标签的情况 + const assistantsByTags = flatMap(assistants, (assistant) => { + const tags = assistant.tags?.length ? assistant.tags : [t('assistants.tags.untagged')] + return tags.map((tag) => ({ tag, assistant })) }) - grouped.sort((a, b) => a.tag.localeCompare(b.tag)) + // 按标签分组并构建结果 + const grouped = Object.entries(groupBy(assistantsByTags, 'tag')).map(([tag, group]) => ({ + tag, + assistants: group.map((g) => g.assistant) + })) - const untagged = assistants.filter((a) => !a.tags?.length) - if (untagged.length > 0) { - grouped.unshift({ - tag: t('assistants.tags.untagged'), - assistants: untagged - }) + // 将未标记的组移到最前面 + const untaggedIndex = grouped.findIndex((g) => g.tag === t('assistants.tags.untagged')) + if (untaggedIndex > -1) { + const [untagged] = grouped.splice(untaggedIndex, 1) + grouped.unshift(untagged) } + return grouped - }, [allTags, assistants, t]) + }, [assistants, t]) return { allTags, getAssistantsByTag, - getGroupedAssistants, - addTag + getGroupedAssistants } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a025bf5728..c7e72d5a10 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -114,13 +114,9 @@ "modify": "Modify Tag", "add": "Add Tag", "delete": "Delete Tag", + "deleteConfirm": "Are you sure to delete this tag?", "settings": { - "title": "Tag Settings", - "current": "Current Tags", - "searchTagsPlaceholder": "Enter tag name to filter tags", - "addTagsPlaceholder": "Enter tag name to add", - "tagsLsitTitle": "Tag List", - "tagsLsitTitleTips": "Click tag to toggle" + "title": "Tag Settings" } } }, diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index accac204cb..f4b213aa71 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -111,13 +111,9 @@ "add": "タグ追加", "modify": "タグ修正", "delete": "タグ削除", + "deleteConfirm": "このタグを削除してもよろしいですか?", "settings": { - "title": "タグ設定", - "current": "現在のタグ", - "searchTagsPlaceholder": "タグ名を入力してフィルタリング", - "addTagsPlaceholder": "追加するタグ名を入力", - "tagsLsitTitle": "タグ一覧", - "tagsLsitTitleTips": "タグをクリックで切り替え" + "title": "タグ設定" } }, "settings.tool_use_mode": "工具調用方式", @@ -1497,7 +1493,7 @@ "messages.input.send_shortcuts": "送信ショートカット", "messages.input.show_estimated_tokens": "推定トークン数を表示", "messages.input.title": "入力設定", - "messages.input.enable_quick_triggers": "'/' と '@' を有効にしてクイックメニューを表示します。", + "messages.input.enable_quick_triggers": "/ と @ を有効にしてクイックメニューを表示します。", "messages.input.enable_delete_model": "バックスペースキーでモデル/添付ファイルを削除します。", "messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング", "messages.math_engine": "数式エンジン", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 7581cef4e8..29b2be684c 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -114,14 +114,9 @@ "add": "Добавить тег", "modify": "Изменить тег", "delete": "Удалить тег", - + "deleteConfirm": "Вы уверены, что хотите удалить этот тег?", "settings": { - "title": "Настройки тегов", - "current": "Текущие теги", - "searchTagsPlaceholder": "Введите название тега для фильтрации", - "addTagsPlaceholder": "Введите название тега для добавления", - "tagsLsitTitle": "Список тегов", - "tagsLsitTitleTips": "Нажмите на тег для переключения" + "title": "Настройки тегов" } } }, @@ -1498,7 +1493,7 @@ "messages.input.send_shortcuts": "Горячие клавиши для отправки", "messages.input.show_estimated_tokens": "Показывать затраты токенов", "messages.input.title": "Настройки ввода", - "messages.input.enable_quick_triggers": "Включите '/' и '@', чтобы вызвать быстрое меню.", + "messages.input.enable_quick_triggers": "Включите / и @, чтобы вызвать быстрое меню.", "messages.input.enable_delete_model": "Включите удаление модели/вложения с помощью клавиши Backspace", "messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown", "messages.math_engine": "Математический движок", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c85eb7ce86..538ea8e8aa 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -114,13 +114,9 @@ "untagged": "未分组", "modify": "修改标签", "delete": "删除标签", + "deleteConfirm": "确定要删除这个标签吗?", "settings": { - "title": "标签设置", - "current": "当前标签", - "searchTagsPlaceholder": "请输入标签名称以过滤标签", - "addTagsPlaceholder": "请输入标签名称以添加", - "tagsLsitTitle": "标签列表", - "tagsLsitTitleTips": "点击标签可切换" + "title": "标签设置" } } }, @@ -1501,7 +1497,7 @@ "messages.input.send_shortcuts": "发送快捷键", "messages.input.show_estimated_tokens": "显示预估 Token 数", "messages.input.title": "输入设置", - "messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单", + "messages.input.enable_quick_triggers": "启用 / 和 @ 触发快捷菜单", "messages.input.enable_delete_model": "启用删除键删除输入的模型/附件", "messages.markdown_rendering_input_message": "Markdown 渲染输入消息", "messages.math_engine": "数学公式引擎", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fa8a4704c3..3540b8192b 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -111,13 +111,9 @@ "add": "添加標籤", "modify": "修改標籤", "delete": "刪除標籤", + "deleteConfirm": "確定要刪除這個標籤嗎?", "settings": { - "title": "標籤設定", - "current": "當前標籤", - "searchTagsPlaceholder": "請輸入標籤名稱以過濾標籤", - "addTagsPlaceholder": "請輸入標籤名稱以新增", - "tagsLsitTitle": "標籤列表", - "tagsLsitTitleTips": "點擊標籤可切換" + "title": "標籤設定" } }, "settings.tool_use_mode": "工具調用方式", @@ -1500,7 +1496,7 @@ "messages.input.send_shortcuts": "傳送快捷鍵", "messages.input.show_estimated_tokens": "顯示預估 Token 數", "messages.input.title": "輸入設定", - "messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單", + "messages.input.enable_quick_triggers": "啟用 / 和 @ 觸發快捷選單", "messages.input.enable_delete_model": "啟用刪除鍵刪除模型/附件", "messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息", "messages.math_engine": "數學公式引擎", diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 7b1c5168df..e2fdbb740c 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -26,7 +26,7 @@ interface Props { const Chat: FC = (props) => { const { assistant } = useAssistant(props.assistant.id) - const { topicPosition, messageStyle } = useSettings() + const { topicPosition, messageStyle, showAssistants } = useSettings() const { showTopics } = useShowTopics() const { isMultiSelectMode } = useChatContext(props.activeTopic) @@ -36,9 +36,10 @@ const Chat: FC = (props) => { const maxWidth = useMemo(() => { const showRightTopics = showTopics && topicPosition === 'right' - const minusRightTopicsWidth = showRightTopics ? `- var(--assistants-width)` : '' - return `calc(100vw - var(--sidebar-width) - var(--assistants-width) ${minusRightTopicsWidth})` - }, [showTopics, topicPosition]) + const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' + const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' + return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})` + }, [showAssistants, showTopics, topicPosition]) useHotkeys('esc', () => { contentSearchRef.current?.disable() diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index c8cc26a570..16048a0f1e 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -30,7 +30,7 @@ const Assistants: FC = ({ const { addAgent } = useAgents() const { t } = useTranslation() const { getGroupedAssistants } = useTags() - const { AssistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType() + const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType() const containerRef = useRef(null) const onDelete = useCallback( @@ -51,9 +51,10 @@ const Assistants: FC = ({ }, [setAssistantsTabSortType] ) - return ( - - {AssistantsTabSortType === 'tags' && ( + + if (assistantsTabSortType === 'tags') { + return ( +
{getGroupedAssistants.map((group) => ( @@ -68,7 +69,7 @@ const Assistants: FC = ({ key={assistant.id} assistant={assistant} isActive={assistant.id === activeAssistant.id} - sortBy={AssistantsTabSortType} + sortBy={assistantsTabSortType} onSwitch={setActiveAssistant} onDelete={onDelete} addAgent={addAgent} @@ -80,30 +81,39 @@ const Assistants: FC = ({ ))}
- )} - {AssistantsTabSortType === 'list' && ( - setDragging(true)} - onDragEnd={() => setDragging(false)}> - {(assistant) => ( - - )} - - )} + + + + {t('chat.add.assistant.title')} + + +
+ ) + } + + return ( + + setDragging(true)} + onDragEnd={() => setDragging(false)}> + {(assistant) => ( + + )} + {!dragging && ( @@ -128,7 +138,6 @@ const TagsContainer = styled.div` display: flex; flex-direction: column; gap: 8px; - margin-bottom: px; ` const AssistantAddItem = styled.div` @@ -158,6 +167,7 @@ const GroupTitle = styled.div` color: var(--color-text-2); font-size: 12px; font-weight: 500; + margin-bottom: -8px; ` const GroupTitleName = styled.div` diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index cebc7c0daa..79882263de 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -1,21 +1,18 @@ import { DeleteOutlined, EditOutlined, - MenuOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined, SmileOutlined, SortAscendingOutlined, - SortDescendingOutlined, - TagsOutlined + SortDescendingOutlined } from '@ant-design/icons' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import EmojiIcon from '@renderer/components/EmojiIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon' -import TagsPopup from '@renderer/components/Popups/TagsPopup' +import PromptPopup from '@renderer/components/Popups/PromptPopup' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' -import { modelGenerating } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { useTags } from '@renderer/hooks/useTags' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' @@ -24,14 +21,16 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Assistant, AssistantsSortType } from '@renderer/types' import { uuid } from '@renderer/utils' import { hasTopicPendingRequests } from '@renderer/utils/queue' -import { Dropdown } from 'antd' -import { ItemType } from 'antd/es/menu/interface' +import { Dropdown, MenuProps } from 'antd' import { omit } from 'lodash' -import { FC, startTransition, useCallback, useEffect, useState } from 'react' +import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react' +import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import * as tinyPinyin from 'tiny-pinyin' +import AssistantTagsPopup from './AssistantTagsPopup' + interface AssistantItemProps { assistant: Assistant isActive: boolean @@ -57,219 +56,70 @@ const AssistantItem: FC = ({ }) => { const { t } = useTranslation() const { allTags } = useTags() - const { removeAllTopics } = useAssistant(assistant.id) // 使用当前助手的ID + const { removeAllTopics } = useAssistant(assistant.id) const { clickAssistantToShowTopic, topicPosition, assistantIconType, setAssistantIconType } = useSettings() const defaultModel = getDefaultModel() const { assistants, updateAssistants } = useAssistants() const [isPending, setIsPending] = useState(false) + useEffect(() => { if (isActive) { setIsPending(false) + return } + const hasPending = assistant.topics.some((topic) => hasTopicPendingRequests(topic.id)) - if (hasPending) { - setIsPending(true) - } + setIsPending(hasPending) }, [isActive, assistant.topics]) const sortByPinyinAsc = useCallback(() => { - const sorted = [...assistants].sort((a, b) => { - const pinyinA = tinyPinyin.convertToPinyin(a.name, '', true) - const pinyinB = tinyPinyin.convertToPinyin(b.name, '', true) - return pinyinA.localeCompare(pinyinB) - }) - updateAssistants(sorted) + updateAssistants(sortAssistantsByPinyin(assistants, true)) }, [assistants, updateAssistants]) const sortByPinyinDesc = useCallback(() => { - const sorted = [...assistants].sort((a, b) => { - const pinyinA = tinyPinyin.convertToPinyin(a.name, '', true) - const pinyinB = tinyPinyin.convertToPinyin(b.name, '', true) - return pinyinB.localeCompare(pinyinA) - }) - updateAssistants(sorted) + updateAssistants(sortAssistantsByPinyin(assistants, false)) }, [assistants, updateAssistants]) - const getMenuItems = useCallback( - (assistant: Assistant): ItemType[] => [ - { - label: t('assistants.edit.title'), - key: 'edit', - icon: , - onClick: () => AssistantSettingsPopup.show({ assistant }) - }, - { - label: t('assistants.copy.title'), - key: 'duplicate', - icon: , - onClick: async () => { - const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } - addAssistant(_assistant) - onSwitch(_assistant) - } - }, - { - label: t('assistants.clear.title'), - key: 'clear', - icon: , - onClick: () => { - window.modal.confirm({ - title: t('assistants.clear.title'), - content: t('assistants.clear.content'), - centered: true, - okButtonProps: { danger: true }, - onOk: () => removeAllTopics() // 使用当前助手的removeAllTopics - }) - } - }, - { - label: t('assistants.save.title'), - key: 'save-to-agent', - icon: , - onClick: async () => { - const agent = omit(assistant, ['model', 'emoji']) - agent.id = uuid() - agent.type = 'agent' - addAgent(agent) - window.message.success({ - content: t('assistants.save.success'), - key: 'save-to-agent' - }) - } - }, - { - label: t('assistants.icon.type'), - key: 'icon-type', - icon: , - children: [ - { - label: t('settings.assistant.icon.type.model'), - key: 'model', - onClick: () => setAssistantIconType('model') - }, - { - label: t('settings.assistant.icon.type.emoji'), - key: 'emoji', - onClick: () => setAssistantIconType('emoji') - }, - { - label: t('settings.assistant.icon.type.none'), - key: 'none', - onClick: () => setAssistantIconType('none') - } - ] - }, - { type: 'divider' }, - { - label: t('assistants.tags.manage'), - key: 'all-tags', - icon: , - children: [ - ...allTags.map((tag) => ({ - label: tag, - icon: assistant.tags?.includes(tag) ? : , - danger: assistant.tags?.includes(tag) ? true : false, - key: `all-tag-${tag}`, - onClick: () => { - if (assistant.tags?.includes(tag)) { - // 如果已有该标签,则移除 - updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [] } : a))) - } else { - // 如果没有该标签,则切换到该标签分类 - updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a))) - } - } - })), - allTags.length > 0 ? { type: 'divider' } : null, - { - label: t('assistants.tags.add'), - key: 'new-tag', - onClick: () => { - TagsPopup.show( - assistant, - (updated) => { - updateAssistants(assistants.map((a) => (a.id === assistant.id ? updated : a))) - }, - 'add' - ) - } - }, - allTags.length > 0 - ? { - label: t('assistants.tags.manage'), - key: 'manage-tags', - onClick: () => { - TagsPopup.show( - assistant, - (updated) => { - updateAssistants(assistants.map((a) => (a.id === assistant.id ? updated : a))) - }, - 'manage' - ) - } - } - : null - ] - }, - { - label: sortBy === 'list' ? t('assistants.list.showByTags') : t('assistants.list.showByList'), - key: 'switch-view', - icon: sortBy === 'list' ? : , - onClick: () => { - sortBy === 'list' ? handleSortByChange?.('tags') : handleSortByChange?.('list') - } - }, - { - label: t('common.sort.pinyin.asc'), - key: 'sort-asc', - icon: , - onClick: () => sortByPinyinAsc() - }, - { - label: t('common.sort.pinyin.desc'), - key: 'sort-desc', - icon: , - onClick: () => sortByPinyinDesc() - }, - { type: 'divider' }, - { - label: t('common.delete'), - key: 'delete', - icon: , - danger: true, - onClick: () => { - window.modal.confirm({ - title: t('assistants.delete.title'), - content: t('assistants.delete.content'), - centered: true, - okButtonProps: { danger: true }, - onOk: () => onDelete(assistant) - }) - } - } - ], + const menuItems = useMemo( + () => + getMenuItems({ + assistant, + t, + allTags, + assistants, + updateAssistants, + addAgent, + addAssistant, + onSwitch, + onDelete, + removeAllTopics, + setAssistantIconType, + sortBy, + handleSortByChange, + sortByPinyinAsc, + sortByPinyinDesc + }), [ - addAgent, - addAssistant, + assistant, + t, allTags, assistants, - handleSortByChange, - onDelete, + updateAssistants, + addAgent, + addAssistant, onSwitch, + onDelete, removeAllTopics, setAssistantIconType, sortBy, + handleSortByChange, sortByPinyinAsc, - sortByPinyinDesc, - t, - updateAssistants + sortByPinyinDesc ] ) const handleSwitch = useCallback(async () => { - await modelGenerating() - if (clickAssistantToShowTopic) { if (topicPosition === 'left') { EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR) @@ -282,11 +132,14 @@ const AssistantItem: FC = ({ } }, [clickAssistantToShowTopic, onSwitch, assistant, topicPosition]) - const assistantName = assistant.name || t('chat.default.name') - const fullAssistantName = assistant.emoji ? `${assistant.emoji} ${assistantName}` : assistantName + const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t]) + const fullAssistantName = useMemo( + () => (assistant.emoji ? `${assistant.emoji} ${assistantName}` : assistantName), + [assistant.emoji, assistantName] + ) return ( - + {assistantIconType === 'model' ? ( @@ -315,6 +168,217 @@ const AssistantItem: FC = ({ ) } +// 提取排序相关的工具函数 +const sortAssistantsByPinyin = (assistants: Assistant[], isAscending: boolean) => { + return [...assistants].sort((a, b) => { + const pinyinA = tinyPinyin.convertToPinyin(a.name, '', true) + const pinyinB = tinyPinyin.convertToPinyin(b.name, '', true) + return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA) + }) +} + +// 提取标签相关的操作函数 +const handleTagOperation = ( + tag: string, + assistant: Assistant, + assistants: Assistant[], + updateAssistants: (assistants: Assistant[]) => void +) => { + if (assistant.tags?.includes(tag)) { + updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [] } : a))) + } else { + updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a))) + } +} + +// 提取创建菜单项的函数 +const createTagMenuItems = ( + allTags: string[], + assistant: Assistant, + assistants: Assistant[], + updateAssistants: (assistants: Assistant[]) => void, + t: (key: string) => string +): MenuProps['items'] => { + const items: MenuProps['items'] = [ + ...allTags.map((tag) => ({ + label: tag, + icon: assistant.tags?.includes(tag) ? : , + danger: assistant.tags?.includes(tag), + key: `all-tag-${tag}`, + onClick: () => handleTagOperation(tag, assistant, assistants, updateAssistants) + })) + ] + + if (allTags.length > 0) { + items.push({ type: 'divider' }) + } + + items.push({ + label: t('assistants.tags.add'), + key: 'new-tag', + icon: , + onClick: async () => { + const tagName = await PromptPopup.show({ + title: t('assistants.tags.add'), + message: '' + }) + + if (tagName && tagName.trim()) { + updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tagName.trim()] } : a))) + } + } + }) + + if (allTags.length > 0) { + items.push({ + label: t('assistants.tags.manage'), + key: 'manage-tags', + icon: , + onClick: () => { + AssistantTagsPopup.show({ title: t('assistants.tags.manage') }) + } + }) + } + + return items +} + +// 提取创建菜单配置的函数 +function getMenuItems({ + assistant, + t, + allTags, + assistants, + updateAssistants, + addAgent, + addAssistant, + onSwitch, + onDelete, + removeAllTopics, + setAssistantIconType, + sortBy, + handleSortByChange, + sortByPinyinAsc, + sortByPinyinDesc +}): MenuProps['items'] { + return [ + { + label: t('assistants.edit.title'), + key: 'edit', + icon: , + onClick: () => AssistantSettingsPopup.show({ assistant }) + }, + { + label: t('assistants.copy.title'), + key: 'duplicate', + icon: , + onClick: async () => { + const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } + addAssistant(_assistant) + onSwitch(_assistant) + } + }, + { + label: t('assistants.clear.title'), + key: 'clear', + icon: , + onClick: () => { + window.modal.confirm({ + title: t('assistants.clear.title'), + content: t('assistants.clear.content'), + centered: true, + okButtonProps: { danger: true }, + onOk: removeAllTopics + }) + } + }, + { + label: t('assistants.save.title'), + key: 'save-to-agent', + icon: , + onClick: async () => { + const agent = omit(assistant, ['model', 'emoji']) + agent.id = uuid() + agent.type = 'agent' + addAgent(agent) + window.message.success({ + content: t('assistants.save.success'), + key: 'save-to-agent' + }) + } + }, + { + label: t('assistants.icon.type'), + key: 'icon-type', + icon: , + children: [ + { + label: t('settings.assistant.icon.type.model'), + key: 'model', + onClick: () => setAssistantIconType('model') + }, + { + label: t('settings.assistant.icon.type.emoji'), + key: 'emoji', + onClick: () => setAssistantIconType('emoji') + }, + { + label: t('settings.assistant.icon.type.none'), + key: 'none', + onClick: () => setAssistantIconType('none') + } + ] + }, + { + type: 'divider' + }, + { + label: t('assistants.tags.manage'), + key: 'all-tags', + icon: , + children: createTagMenuItems(allTags, assistant, assistants, updateAssistants, t) + }, + { + label: sortBy === 'list' ? t('assistants.list.showByTags') : t('assistants.list.showByList'), + key: 'switch-view', + icon: sortBy === 'list' ? : , + onClick: () => { + sortBy === 'list' ? handleSortByChange?.('tags') : handleSortByChange?.('list') + } + }, + { + label: t('common.sort.pinyin.asc'), + key: 'sort-asc', + icon: , + onClick: sortByPinyinAsc + }, + { + label: t('common.sort.pinyin.desc'), + key: 'sort-desc', + icon: , + onClick: sortByPinyinDesc + }, + { + type: 'divider' + }, + { + label: t('common.delete'), + key: 'delete', + icon: , + danger: true, + onClick: () => { + window.modal.confirm({ + title: t('assistants.delete.title'), + content: t('assistants.delete.content'), + centered: true, + okButtonProps: { danger: true }, + onOk: () => onDelete(assistant) + }) + } + } + ] +} + const Container = styled.div` display: flex; flex-direction: row; @@ -336,8 +400,6 @@ const Container = styled.div` &.active { background-color: var(--color-background-soft); border: 0.5px solid var(--color-border); - .name { - } } ` @@ -382,4 +444,4 @@ const TopicCount = styled.div` align-items: center; ` -export default AssistantItem +export default memo(AssistantItem) diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx new file mode 100644 index 0000000000..978aca0212 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/AssistantTagsPopup.tsx @@ -0,0 +1,123 @@ +import { Box } from '@renderer/components/Layout' +import { TopView } from '@renderer/components/TopView' +import { useAssistants } from '@renderer/hooks/useAssistant' +import { useTags } from '@renderer/hooks/useTags' +import { Button, Empty, Modal } from 'antd' +import { isEmpty } from 'lodash' +import { Trash } from 'lucide-react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface ShowParams { + title: string +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ title, resolve }) => { + const [open, setOpen] = useState(true) + const { allTags, getAssistantsByTag } = useTags() + const { assistants, updateAssistants } = useAssistants() + const { t } = useTranslation() + + const onOk = () => { + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + const onDelete = (removedTag: string) => { + window.modal.confirm({ + title: t('assistants.tags.deleteConfirm'), + centered: true, + onOk: () => { + const relatedAssistants = getAssistantsByTag(removedTag) + if (!isEmpty(relatedAssistants)) { + updateAssistants( + assistants.map((assistant) => { + const findedAssitant = relatedAssistants.find((_assistant) => _assistant.id === assistant.id) + return findedAssitant ? { ...findedAssitant, tags: [] } : assistant + }) + ) + } + } + }) + } + + AssistantTagsPopup.hide = onCancel + + return ( + + + {allTags.map((tag) => ( + + {tag} +