diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index f15e893797..5b8bd93bfd 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,11 +1,12 @@ import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { useAssistants } from '@renderer/hooks/useAssistant' +import { useRuntime } from '@renderer/hooks/useRuntime' import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useActiveTopic } from '@renderer/hooks/useTopic' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import NavigationService from '@renderer/services/NavigationService' import { newMessagesActions } from '@renderer/store/newMessage' -import { setActiveTopicOrSessionAction } from '@renderer/store/runtime' +import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant' import { AnimatePresence, motion } from 'motion/react' @@ -32,6 +33,8 @@ const HomePage: FC = () => { const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic) const { showAssistants, showTopics, topicPosition } = useSettings() const dispatch = useDispatch() + const { chat } = useRuntime() + const { activeTopicOrSession } = chat _activeAssistant = activeAssistant @@ -91,6 +94,29 @@ const HomePage: FC = () => { } }, [showAssistants, showTopics, topicPosition]) + useEffect(() => { + if (activeTopicOrSession === 'session') { + setActiveAssistant({ + id: 'fake', + name: '', + prompt: '', + topics: [ + { + id: 'fake', + assistantId: 'fake', + name: 'fake', + createdAt: '', + updatedAt: '', + messages: [] + } + ], + type: '' + }) + } else if (activeTopicOrSession === 'topic') { + dispatch(setActiveAgentId('fake')) + } + }, [activeTopicOrSession, dispatch, setActiveAssistant]) + return ( {isLeftNavbar && ( diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index f0593acf7a..9299a75e2f 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -3,8 +3,8 @@ import { Assistant } from '@renderer/types' import { FC, useRef } from 'react' import styled from 'styled-components' -import { Agents } from './Agents' -import Assistants from './Assistants' +import { Agents } from './components/Agents' +import Assistants from './components/Assistants' interface AssistantsTabProps { activeAssistant: Assistant diff --git a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx b/src/renderer/src/pages/home/Tabs/SessionsTab.tsx index b31750a819..eb0d994d6c 100644 --- a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SessionsTab.tsx @@ -28,7 +28,7 @@ const SessionsTab: FC = () => { initial={{ opacity: 0, y: 5 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2, duration: 0.3 }} - className="text-sm text-foreground-500"> + className="text-foreground-500 text-sm"> {t('common.loading')}... diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index f0cdda7e6e..ce413aaca7 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -1,56 +1,9 @@ -import { DraggableVirtualList } from '@renderer/components/DraggableList' -import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' -import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' -import PromptPopup from '@renderer/components/Popups/PromptPopup' -import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup' -import { isMac } from '@renderer/config/constant' -import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' -import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' -import { useNotesSettings } from '@renderer/hooks/useNotesSettings' -import { modelGenerating } from '@renderer/hooks/useRuntime' -import { useSettings } from '@renderer/hooks/useSettings' -import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic' -import { fetchMessagesSummary } from '@renderer/services/ApiService' -import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import store from '@renderer/store' -import { RootState } from '@renderer/store' -import { newMessagesActions } from '@renderer/store/newMessage' -import { setGenerating } from '@renderer/store/runtime' +import { useRuntime } from '@renderer/hooks/useRuntime' import { Assistant, Topic } from '@renderer/types' -import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils' -import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy' -import { - exportMarkdownToJoplin, - exportMarkdownToSiyuan, - exportMarkdownToYuque, - exportTopicAsMarkdown, - exportTopicToNotes, - exportTopicToNotion, - topicToMarkdown -} from '@renderer/utils/export' -import { Dropdown, MenuProps, Tooltip } from 'antd' -import { ItemType, MenuItemType } from 'antd/es/menu/interface' -import dayjs from 'dayjs' -import { findIndex } from 'lodash' -import { - BrushCleaning, - FolderOpen, - HelpCircle, - MenuIcon, - NotebookPen, - PackagePlus, - PinIcon, - PinOffIcon, - PlusIcon, - Save, - Sparkles, - UploadIcon, - XIcon -} from 'lucide-react' -import { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' -import styled from 'styled-components' +import { FC } from 'react' + +import { Topics } from './components/Topics' +import SessionsTab from './SessionsTab' // const logger = loggerService.withContext('TopicsTab') @@ -61,739 +14,16 @@ interface Props { position: 'left' | 'right' } -const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, position }) => { - const { t } = useTranslation() - const { notesPath } = useNotesSettings() - const { assistants } = useAssistants() - const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) - const { showTopicTime, pinTopicsToTop, setTopicPosition, topicPosition } = useSettings() - - const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics) - const topicLoadingQuery = useSelector((state: RootState) => state.messages.loadingByTopic) - const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic) - const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) - - const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' - - const [deletingTopicId, setDeletingTopicId] = useState(null) - const deleteTimerRef = useRef(null) - const [editingTopicId, setEditingTopicId] = useState(null) - - const topicEdit = useInPlaceEdit({ - onSave: (name: string) => { - const topic = assistant.topics.find((t) => t.id === editingTopicId) - if (topic && name !== topic.name) { - const updatedTopic = { ...topic, name, isNameManuallyEdited: true } - updateTopic(updatedTopic) - window.toast.success(t('common.saved')) - } - setEditingTopicId(null) - }, - onCancel: () => { - setEditingTopicId(null) - } - }) - - const isPending = useCallback((topicId: string) => topicLoadingQuery[topicId], [topicLoadingQuery]) - const isFulfilled = useCallback((topicId: string) => topicFulfilledQuery[topicId], [topicFulfilledQuery]) - const dispatch = useDispatch() - - useEffect(() => { - dispatch(newMessagesActions.setTopicFulfilled({ topicId: activeTopic.id, fulfilled: false })) - }, [activeTopic.id, dispatch, topicFulfilledQuery]) - - const isRenaming = useCallback( - (topicId: string) => { - return renamingTopics.includes(topicId) - }, - [renamingTopics] - ) - - const isNewlyRenamed = useCallback( - (topicId: string) => { - return newlyRenamedTopics.includes(topicId) - }, - [newlyRenamedTopics] - ) - - const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => { - e.stopPropagation() - - if (deleteTimerRef.current) { - clearTimeout(deleteTimerRef.current) - } - - setDeletingTopicId(topicId) - - deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000) - }, []) - - const onClearMessages = useCallback((topic: Topic) => { - // window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) - store.dispatch(setGenerating(false)) - EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic) - }, []) - - const handleConfirmDelete = useCallback( - async (topic: Topic, e: React.MouseEvent) => { - e.stopPropagation() - if (assistant.topics.length === 1) { - return onClearMessages(topic) - } - await modelGenerating() - const index = findIndex(assistant.topics, (t) => t.id === topic.id) - if (topic.id === activeTopic.id) { - setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) - } - removeTopic(topic) - setDeletingTopicId(null) - }, - [activeTopic.id, assistant.topics, onClearMessages, removeTopic, setActiveTopic] - ) - - const onPinTopic = useCallback( - (topic: Topic) => { - const updatedTopic = { ...topic, pinned: !topic.pinned } - updateTopic(updatedTopic) - }, - [updateTopic] - ) - - const onDeleteTopic = useCallback( - async (topic: Topic) => { - await modelGenerating() - if (topic.id === activeTopic?.id) { - const index = findIndex(assistant.topics, (t) => t.id === topic.id) - setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) - } - removeTopic(topic) - }, - [assistant.topics, removeTopic, setActiveTopic, activeTopic] - ) - - const onMoveTopic = useCallback( - async (topic: Topic, toAssistant: Assistant) => { - await modelGenerating() - const index = findIndex(assistant.topics, (t) => t.id === topic.id) - setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1]) - moveTopic(topic, toAssistant) - }, - [assistant.topics, moveTopic, setActiveTopic] - ) - - const onSwitchTopic = useCallback( - async (topic: Topic) => { - // await modelGenerating() - setActiveTopic(topic) - }, - [setActiveTopic] - ) - - const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) - - const [_targetTopic, setTargetTopic] = useState(null) - const targetTopic = useDeferredValue(_targetTopic) - const getTopicMenuItems = useMemo(() => { - const topic = targetTopic - if (!topic) return [] - - const menus: MenuProps['items'] = [ - { - label: t('chat.topics.auto_rename'), - key: 'auto-rename', - icon: , - disabled: isRenaming(topic.id), - async onClick() { - const messages = await TopicManager.getTopicMessages(topic.id) - if (messages.length >= 2) { - startTopicRenaming(topic.id) - try { - const summaryText = await fetchMessagesSummary({ messages, assistant }) - if (summaryText) { - const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false } - updateTopic(updatedTopic) - } else { - window.toast?.error(t('message.error.fetchTopicName')) - } - } finally { - finishTopicRenaming(topic.id) - } - } - } - }, - { - label: t('chat.topics.edit.title'), - key: 'rename', - icon: , - disabled: isRenaming(topic.id), - async onClick() { - const name = await PromptPopup.show({ - title: t('chat.topics.edit.title'), - message: '', - defaultValue: topic?.name || '', - extraNode: ( -
{t('chat.topics.edit.title_tip')}
- ) - }) - if (name && topic?.name !== name) { - const updatedTopic = { ...topic, name, isNameManuallyEdited: true } - updateTopic(updatedTopic) - } - } - }, - { - label: t('chat.topics.prompt.label'), - key: 'topic-prompt', - icon: , - extra: ( - - - - ), - async onClick() { - const prompt = await PromptPopup.show({ - title: t('chat.topics.prompt.edit.title'), - message: '', - defaultValue: topic?.prompt || '', - inputProps: { - rows: 8, - allowClear: true - } - }) - - prompt !== null && - (() => { - const updatedTopic = { ...topic, prompt: prompt.trim() } - updateTopic(updatedTopic) - topic.id === activeTopic.id && setActiveTopic(updatedTopic) - })() - } - }, - { - label: topic.pinned ? t('chat.topics.unpin') : t('chat.topics.pin'), - key: 'pin', - icon: topic.pinned ? : , - onClick() { - onPinTopic(topic) - } - }, - { - label: t('notes.save'), - key: 'notes', - icon: , - onClick: async () => { - exportTopicToNotes(topic, notesPath) - } - }, - { - label: t('chat.topics.clear.title'), - key: 'clear-messages', - icon: , - async onClick() { - window.modal.confirm({ - title: t('chat.input.clear.content'), - centered: true, - onOk: () => onClearMessages(topic) - }) - } - }, - { - label: t('settings.topic.position.label'), - key: 'topic-position', - icon: , - children: [ - { - label: t('settings.topic.position.left'), - key: 'left', - onClick: () => setTopicPosition('left') - }, - { - label: t('settings.topic.position.right'), - key: 'right', - onClick: () => setTopicPosition('right') - } - ] - }, - { - label: t('chat.topics.copy.title'), - key: 'copy', - icon: , - children: [ - { - label: t('chat.topics.copy.image'), - key: 'img', - onClick: () => EventEmitter.emit(EVENT_NAMES.COPY_TOPIC_IMAGE, topic) - }, - { - label: t('chat.topics.copy.md'), - key: 'md', - onClick: () => copyTopicAsMarkdown(topic) - }, - { - label: t('chat.topics.copy.plain_text'), - key: 'plain_text', - onClick: () => copyTopicAsPlainText(topic) - } - ] - }, - { - label: t('chat.save.label'), - key: 'save', - icon: , - children: [ - { - label: t('chat.save.topic.knowledge.title'), - key: 'knowledge', - onClick: async () => { - try { - const result = await SaveToKnowledgePopup.showForTopic(topic) - if (result?.success) { - window.toast.success(t('chat.save.topic.knowledge.success', { count: result.savedCount })) - } - } catch { - window.toast.error(t('chat.save.topic.knowledge.error.save_failed')) - } - } - } - ] - }, - { - label: t('chat.topics.export.title'), - key: 'export', - icon: , - children: [ - exportMenuOptions.image && { - label: t('chat.topics.export.image'), - key: 'image', - onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic) - }, - exportMenuOptions.markdown && { - label: t('chat.topics.export.md.label'), - key: 'markdown', - onClick: () => exportTopicAsMarkdown(topic) - }, - exportMenuOptions.markdown_reason && { - label: t('chat.topics.export.md.reason'), - key: 'markdown_reason', - onClick: () => exportTopicAsMarkdown(topic, true) - }, - exportMenuOptions.docx && { - label: t('chat.topics.export.word'), - key: 'word', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name)) - } - }, - exportMenuOptions.notion && { - label: t('chat.topics.export.notion'), - key: 'notion', - onClick: async () => { - exportTopicToNotion(topic) - } - }, - exportMenuOptions.yuque && { - label: t('chat.topics.export.yuque'), - key: 'yuque', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToYuque(topic.name, markdown) - } - }, - exportMenuOptions.obsidian && { - label: t('chat.topics.export.obsidian'), - key: 'obsidian', - onClick: async () => { - await ObsidianExportPopup.show({ title: topic.name, topic, processingMethod: '3' }) - } - }, - exportMenuOptions.joplin && { - label: t('chat.topics.export.joplin'), - key: 'joplin', - onClick: async () => { - const topicMessages = await TopicManager.getTopicMessages(topic.id) - exportMarkdownToJoplin(topic.name, topicMessages) - } - }, - exportMenuOptions.siyuan && { - label: t('chat.topics.export.siyuan'), - key: 'siyuan', - onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToSiyuan(topic.name, markdown) - } - } - ].filter(Boolean) as ItemType[] - } - ] - - if (assistants.length > 1 && assistant.topics.length > 1) { - menus.push({ - label: t('chat.topics.move_to'), - key: 'move', - icon: , - children: assistants - .filter((a) => a.id !== assistant.id) - .map((a) => ({ - label: a.name, - key: a.id, - onClick: () => onMoveTopic(topic, a) - })) - }) - } - - if (assistant.topics.length > 1 && !topic.pinned) { - menus.push({ type: 'divider' }) - menus.push({ - label: t('common.delete'), - danger: true, - key: 'delete', - icon: , - onClick: () => onDeleteTopic(topic) - }) - } - - return menus - }, [ - targetTopic, - t, - isRenaming, - exportMenuOptions.image, - exportMenuOptions.markdown, - exportMenuOptions.markdown_reason, - exportMenuOptions.docx, - exportMenuOptions.notion, - exportMenuOptions.yuque, - exportMenuOptions.obsidian, - exportMenuOptions.joplin, - exportMenuOptions.siyuan, - assistants, - notesPath, - assistant, - updateTopic, - activeTopic.id, - setActiveTopic, - onPinTopic, - onClearMessages, - setTopicPosition, - onMoveTopic, - onDeleteTopic - ]) - - // Sort topics based on pinned status if pinTopicsToTop is enabled - const sortedTopics = useMemo(() => { - if (pinTopicsToTop) { - return [...assistant.topics].sort((a, b) => { - if (a.pinned && !b.pinned) return -1 - if (!a.pinned && b.pinned) return 1 - return 0 - }) - } - return assistant.topics - }, [assistant.topics, pinTopicsToTop]) - - const singlealone = topicPosition === 'right' && position === 'right' - - return ( - EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> - - {t('chat.add.topic.title')} - - }> - {(topic) => { - const isActive = topic.id === activeTopic?.id - const topicName = topic.name.replace('`', '') - const topicPrompt = topic.prompt - const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt - - const getTopicNameClassName = () => { - if (isRenaming(topic.id)) return 'shimmer' - if (isNewlyRenamed(topic.id)) return 'typing' - return '' - } - - return ( - - setTargetTopic(topic)} - className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} - onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)} - style={{ - borderRadius, - cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer' - }}> - {isPending(topic.id) && !isActive && } - {isFulfilled(topic.id) && !isActive && } - - {editingTopicId === topic.id && topicEdit.isEditing ? ( - e.stopPropagation()} - /> - ) : ( - { - setEditingTopicId(topic.id) - topicEdit.startEdit(topic.name) - }}> - {topicName} - - )} - {!topic.pinned && ( - - {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} - - }> - { - if (e.ctrlKey || e.metaKey) { - handleConfirmDelete(topic, e) - } else if (deletingTopicId === topic.id) { - handleConfirmDelete(topic, e) - } else { - handleDeleteClick(topic.id, e) - } - }}> - {deletingTopicId === topic.id ? ( - - ) : ( - - )} - - - )} - {topic.pinned && ( - - - - )} - - {topicPrompt && ( - - {fullTopicPrompt} - - )} - {showTopicTime && {dayjs(topic.createdAt).format('MM/DD HH:mm')}} - - - ) - }} - - ) +const TopicsTab: FC = (props) => { + const { chat } = useRuntime() + const { activeTopicOrSession } = chat + if (activeTopicOrSession === 'topic') { + return + } + if (activeTopicOrSession === 'session') { + return + } + return 'Not a valid state.' } -const TopicListItem = styled.div` - padding: 7px 12px; - border-radius: var(--list-item-border-radius); - font-size: 13px; - display: flex; - flex-direction: column; - justify-content: space-between; - cursor: pointer; - width: calc(var(--assistants-width) - 20px); - - .menu { - opacity: 0; - color: var(--color-text-3); - } - - &:hover { - background-color: var(--color-list-item-hover); - transition: background-color 0.1s; - - .menu { - opacity: 1; - } - } - - &.active { - background-color: var(--color-list-item); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - .menu { - opacity: 1; - - &:hover { - color: var(--color-text-2); - } - } - } - &.singlealone { - border-radius: 0 !important; - &:hover { - background-color: var(--color-background-soft); - } - &.active { - border-left: 2px solid var(--color-primary); - box-shadow: none; - } - } -` - -const TopicNameContainer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: 4px; - height: 20px; - justify-content: space-between; -` - -const TopicName = styled.div` - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - font-size: 13px; - position: relative; - will-change: background-position, width; - - --color-shimmer-mid: var(--color-text-1); - --color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent); - - &.shimmer { - background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end)); - background-size: 200% 100%; - background-clip: text; - color: transparent; - animation: shimmer 3s linear infinite; - } - - &.typing { - display: block; - -webkit-line-clamp: unset; - -webkit-box-orient: unset; - white-space: nowrap; - overflow: hidden; - animation: typewriter 0.5s steps(40, end); - } - - @keyframes shimmer { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } - } - - @keyframes typewriter { - from { - width: 0; - } - to { - width: 100%; - } - } -` - -const TopicEditInput = styled.input` - background: var(--color-background); - border: none; - color: var(--color-text-1); - font-size: 13px; - font-family: inherit; - padding: 2px 6px; - width: 100%; - outline: none; - padding: 0; -` - -const PendingIndicator = styled.div.attrs({ - className: 'animation-pulse' -})` - --pulse-size: 5px; - width: 5px; - height: 5px; - position: absolute; - left: 3px; - top: 15px; - border-radius: 50%; - background-color: var(--color-status-warning); -` - -const FulfilledIndicator = styled.div.attrs({ - className: 'animation-pulse' -})` - --pulse-size: 5px; - width: 5px; - height: 5px; - position: absolute; - left: 3px; - top: 15px; - border-radius: 50%; - background-color: var(--color-status-success); -` - -const AddTopicButton = styled.div` - display: flex; - align-items: center; - gap: 6px; - width: calc(100% - 10px); - padding: 7px 12px; - margin-bottom: 8px; - background: transparent; - color: var(--color-text-2); - font-size: 13px; - border-radius: var(--list-item-border-radius); - cursor: pointer; - transition: all 0.2s; - margin-top: -5px; - - &:hover { - background-color: var(--color-list-item-hover); - color: var(--color-text-1); - } - - .anticon { - font-size: 12px; - } -` - -const TopicPromptText = styled.div` - color: var(--color-text-2); - font-size: 12px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - ~ .prompt-text { - margin-top: 10px; - } -` - -const TopicTime = styled.div` - color: var(--color-text-3); - font-size: 11px; -` - -const MenuButton = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - min-width: 20px; - min-height: 20px; - .anticon { - font-size: 12px; - } -` - -export default Topics +export default TopicsTab diff --git a/src/renderer/src/pages/home/Tabs/Agents.tsx b/src/renderer/src/pages/home/Tabs/components/Agents.tsx similarity index 84% rename from src/renderer/src/pages/home/Tabs/Agents.tsx rename to src/renderer/src/pages/home/Tabs/components/Agents.tsx index b1a37c06e2..54c882cd4f 100644 --- a/src/renderer/src/pages/home/Tabs/Agents.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Agents.tsx @@ -3,13 +3,13 @@ import { AgentModal } from '@renderer/components/Popups/agent/AgentModal' import { useAgents } from '@renderer/hooks/agents/useAgents' import { useRuntime } from '@renderer/hooks/useRuntime' import { useAppDispatch } from '@renderer/store' -import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime' +import { setActiveAgentId as setActiveAgentIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime' import { Plus } from 'lucide-react' import { FC, useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import AgentItem from './components/AgentItem' -import { SectionName } from './components/SectionName' +import AgentItem from './AgentItem' +import { SectionName } from './SectionName' interface AssistantsTabProps {} @@ -45,7 +45,10 @@ export const Agents: FC = () => { agent={agent} isActive={agent.id === activeAgentId} onDelete={() => deleteAgent(agent.id)} - onPress={() => setActiveAgentId(agent.id)} + onPress={() => { + setActiveAgentId(agent.id) + dispatch(setActiveTopicOrSessionAction('session')) + }} /> ))} = ({ const { assistants, updateAssistants } = useAssistants() const [isPending, setIsPending] = useState(false) + const dispatch = useAppDispatch() useEffect(() => { if (isActive) { @@ -128,7 +131,8 @@ const AssistantItem: FC = ({ } } onSwitch(assistant) - }, [clickAssistantToShowTopic, onSwitch, assistant, topicPosition]) + dispatch(setActiveTopicOrSessionAction('topic')) + }, [clickAssistantToShowTopic, onSwitch, assistant, dispatch, topicPosition]) const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t]) const fullAssistantName = useMemo( diff --git a/src/renderer/src/pages/home/Tabs/Assistants.tsx b/src/renderer/src/pages/home/Tabs/components/Assistants.tsx similarity index 98% rename from src/renderer/src/pages/home/Tabs/Assistants.tsx rename to src/renderer/src/pages/home/Tabs/components/Assistants.tsx index ecd0f6d78f..b16a179f99 100644 --- a/src/renderer/src/pages/home/Tabs/Assistants.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Assistants.tsx @@ -12,8 +12,8 @@ import { FC, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import AssistantItem from './components/AssistantItem' -import { SectionName } from './components/SectionName' +import AssistantItem from './AssistantItem' +import { SectionName } from './SectionName' interface AssistantsProps { activeAssistant: Assistant diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx new file mode 100644 index 0000000000..dc699395b7 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -0,0 +1,795 @@ +import { DraggableVirtualList } from '@renderer/components/DraggableList' +import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' +import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' +import PromptPopup from '@renderer/components/Popups/PromptPopup' +import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup' +import { isMac } from '@renderer/config/constant' +import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' +import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' +import { useNotesSettings } from '@renderer/hooks/useNotesSettings' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic' +import { fetchMessagesSummary } from '@renderer/services/ApiService' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import store from '@renderer/store' +import { RootState } from '@renderer/store' +import { newMessagesActions } from '@renderer/store/newMessage' +import { setGenerating } from '@renderer/store/runtime' +import { Assistant, Topic } from '@renderer/types' +import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils' +import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy' +import { + exportMarkdownToJoplin, + exportMarkdownToSiyuan, + exportMarkdownToYuque, + exportTopicAsMarkdown, + exportTopicToNotes, + exportTopicToNotion, + topicToMarkdown +} from '@renderer/utils/export' +import { Dropdown, MenuProps, Tooltip } from 'antd' +import { ItemType, MenuItemType } from 'antd/es/menu/interface' +import dayjs from 'dayjs' +import { findIndex } from 'lodash' +import { + BrushCleaning, + FolderOpen, + HelpCircle, + MenuIcon, + NotebookPen, + PackagePlus, + PinIcon, + PinOffIcon, + PlusIcon, + Save, + Sparkles, + UploadIcon, + XIcon +} from 'lucide-react' +import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components' + +interface Props { + assistant: Assistant + activeTopic: Topic + setActiveTopic: (topic: Topic) => void + position: 'left' | 'right' +} + +export const Topics: React.FC = ({ assistant: _assistant, activeTopic, setActiveTopic, position }) => { + const { t } = useTranslation() + const { notesPath } = useNotesSettings() + const { assistants } = useAssistants() + const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) + const { showTopicTime, pinTopicsToTop, setTopicPosition, topicPosition } = useSettings() + + const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics) + const topicLoadingQuery = useSelector((state: RootState) => state.messages.loadingByTopic) + const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic) + const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) + + const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' + + const [deletingTopicId, setDeletingTopicId] = useState(null) + const deleteTimerRef = useRef(null) + const [editingTopicId, setEditingTopicId] = useState(null) + + const topicEdit = useInPlaceEdit({ + onSave: (name: string) => { + const topic = assistant.topics.find((t) => t.id === editingTopicId) + if (topic && name !== topic.name) { + const updatedTopic = { ...topic, name, isNameManuallyEdited: true } + updateTopic(updatedTopic) + window.toast.success(t('common.saved')) + } + setEditingTopicId(null) + }, + onCancel: () => { + setEditingTopicId(null) + } + }) + + const isPending = useCallback((topicId: string) => topicLoadingQuery[topicId], [topicLoadingQuery]) + const isFulfilled = useCallback((topicId: string) => topicFulfilledQuery[topicId], [topicFulfilledQuery]) + const dispatch = useDispatch() + + useEffect(() => { + dispatch(newMessagesActions.setTopicFulfilled({ topicId: activeTopic.id, fulfilled: false })) + }, [activeTopic.id, dispatch, topicFulfilledQuery]) + + const isRenaming = useCallback( + (topicId: string) => { + return renamingTopics.includes(topicId) + }, + [renamingTopics] + ) + + const isNewlyRenamed = useCallback( + (topicId: string) => { + return newlyRenamedTopics.includes(topicId) + }, + [newlyRenamedTopics] + ) + + const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => { + e.stopPropagation() + + if (deleteTimerRef.current) { + clearTimeout(deleteTimerRef.current) + } + + setDeletingTopicId(topicId) + + deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000) + }, []) + + const onClearMessages = useCallback((topic: Topic) => { + // window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) + store.dispatch(setGenerating(false)) + EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic) + }, []) + + const handleConfirmDelete = useCallback( + async (topic: Topic, e: React.MouseEvent) => { + e.stopPropagation() + if (assistant.topics.length === 1) { + return onClearMessages(topic) + } + await modelGenerating() + const index = findIndex(assistant.topics, (t) => t.id === topic.id) + if (topic.id === activeTopic.id) { + setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) + } + removeTopic(topic) + setDeletingTopicId(null) + }, + [activeTopic.id, assistant.topics, onClearMessages, removeTopic, setActiveTopic] + ) + + const onPinTopic = useCallback( + (topic: Topic) => { + const updatedTopic = { ...topic, pinned: !topic.pinned } + updateTopic(updatedTopic) + }, + [updateTopic] + ) + + const onDeleteTopic = useCallback( + async (topic: Topic) => { + await modelGenerating() + if (topic.id === activeTopic?.id) { + const index = findIndex(assistant.topics, (t) => t.id === topic.id) + setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) + } + removeTopic(topic) + }, + [assistant.topics, removeTopic, setActiveTopic, activeTopic] + ) + + const onMoveTopic = useCallback( + async (topic: Topic, toAssistant: Assistant) => { + await modelGenerating() + const index = findIndex(assistant.topics, (t) => t.id === topic.id) + setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1]) + moveTopic(topic, toAssistant) + }, + [assistant.topics, moveTopic, setActiveTopic] + ) + + const onSwitchTopic = useCallback( + async (topic: Topic) => { + // await modelGenerating() + setActiveTopic(topic) + }, + [setActiveTopic] + ) + + const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) + + const [_targetTopic, setTargetTopic] = useState(null) + const targetTopic = useDeferredValue(_targetTopic) + const getTopicMenuItems = useMemo(() => { + const topic = targetTopic + if (!topic) return [] + + const menus: MenuProps['items'] = [ + { + label: t('chat.topics.auto_rename'), + key: 'auto-rename', + icon: , + disabled: isRenaming(topic.id), + async onClick() { + const messages = await TopicManager.getTopicMessages(topic.id) + if (messages.length >= 2) { + startTopicRenaming(topic.id) + try { + const summaryText = await fetchMessagesSummary({ messages, assistant }) + if (summaryText) { + const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false } + updateTopic(updatedTopic) + } else { + window.toast?.error(t('message.error.fetchTopicName')) + } + } finally { + finishTopicRenaming(topic.id) + } + } + } + }, + { + label: t('chat.topics.edit.title'), + key: 'rename', + icon: , + disabled: isRenaming(topic.id), + async onClick() { + const name = await PromptPopup.show({ + title: t('chat.topics.edit.title'), + message: '', + defaultValue: topic?.name || '', + extraNode: ( +
{t('chat.topics.edit.title_tip')}
+ ) + }) + if (name && topic?.name !== name) { + const updatedTopic = { ...topic, name, isNameManuallyEdited: true } + updateTopic(updatedTopic) + } + } + }, + { + label: t('chat.topics.prompt.label'), + key: 'topic-prompt', + icon: , + extra: ( + + + + ), + async onClick() { + const prompt = await PromptPopup.show({ + title: t('chat.topics.prompt.edit.title'), + message: '', + defaultValue: topic?.prompt || '', + inputProps: { + rows: 8, + allowClear: true + } + }) + + prompt !== null && + (() => { + const updatedTopic = { ...topic, prompt: prompt.trim() } + updateTopic(updatedTopic) + topic.id === activeTopic.id && setActiveTopic(updatedTopic) + })() + } + }, + { + label: topic.pinned ? t('chat.topics.unpin') : t('chat.topics.pin'), + key: 'pin', + icon: topic.pinned ? : , + onClick() { + onPinTopic(topic) + } + }, + { + label: t('notes.save'), + key: 'notes', + icon: , + onClick: async () => { + exportTopicToNotes(topic, notesPath) + } + }, + { + label: t('chat.topics.clear.title'), + key: 'clear-messages', + icon: , + async onClick() { + window.modal.confirm({ + title: t('chat.input.clear.content'), + centered: true, + onOk: () => onClearMessages(topic) + }) + } + }, + { + label: t('settings.topic.position.label'), + key: 'topic-position', + icon: , + children: [ + { + label: t('settings.topic.position.left'), + key: 'left', + onClick: () => setTopicPosition('left') + }, + { + label: t('settings.topic.position.right'), + key: 'right', + onClick: () => setTopicPosition('right') + } + ] + }, + { + label: t('chat.topics.copy.title'), + key: 'copy', + icon: , + children: [ + { + label: t('chat.topics.copy.image'), + key: 'img', + onClick: () => EventEmitter.emit(EVENT_NAMES.COPY_TOPIC_IMAGE, topic) + }, + { + label: t('chat.topics.copy.md'), + key: 'md', + onClick: () => copyTopicAsMarkdown(topic) + }, + { + label: t('chat.topics.copy.plain_text'), + key: 'plain_text', + onClick: () => copyTopicAsPlainText(topic) + } + ] + }, + { + label: t('chat.save.label'), + key: 'save', + icon: , + children: [ + { + label: t('chat.save.topic.knowledge.title'), + key: 'knowledge', + onClick: async () => { + try { + const result = await SaveToKnowledgePopup.showForTopic(topic) + if (result?.success) { + window.toast.success(t('chat.save.topic.knowledge.success', { count: result.savedCount })) + } + } catch { + window.toast.error(t('chat.save.topic.knowledge.error.save_failed')) + } + } + } + ] + }, + { + label: t('chat.topics.export.title'), + key: 'export', + icon: , + children: [ + exportMenuOptions.image && { + label: t('chat.topics.export.image'), + key: 'image', + onClick: () => EventEmitter.emit(EVENT_NAMES.EXPORT_TOPIC_IMAGE, topic) + }, + exportMenuOptions.markdown && { + label: t('chat.topics.export.md.label'), + key: 'markdown', + onClick: () => exportTopicAsMarkdown(topic) + }, + exportMenuOptions.markdown_reason && { + label: t('chat.topics.export.md.reason'), + key: 'markdown_reason', + onClick: () => exportTopicAsMarkdown(topic, true) + }, + exportMenuOptions.docx && { + label: t('chat.topics.export.word'), + key: 'word', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + window.api.export.toWord(markdown, removeSpecialCharactersForFileName(topic.name)) + } + }, + exportMenuOptions.notion && { + label: t('chat.topics.export.notion'), + key: 'notion', + onClick: async () => { + exportTopicToNotion(topic) + } + }, + exportMenuOptions.yuque && { + label: t('chat.topics.export.yuque'), + key: 'yuque', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToYuque(topic.name, markdown) + } + }, + exportMenuOptions.obsidian && { + label: t('chat.topics.export.obsidian'), + key: 'obsidian', + onClick: async () => { + await ObsidianExportPopup.show({ title: topic.name, topic, processingMethod: '3' }) + } + }, + exportMenuOptions.joplin && { + label: t('chat.topics.export.joplin'), + key: 'joplin', + onClick: async () => { + const topicMessages = await TopicManager.getTopicMessages(topic.id) + exportMarkdownToJoplin(topic.name, topicMessages) + } + }, + exportMenuOptions.siyuan && { + label: t('chat.topics.export.siyuan'), + key: 'siyuan', + onClick: async () => { + const markdown = await topicToMarkdown(topic) + exportMarkdownToSiyuan(topic.name, markdown) + } + } + ].filter(Boolean) as ItemType[] + } + ] + + if (assistants.length > 1 && assistant.topics.length > 1) { + menus.push({ + label: t('chat.topics.move_to'), + key: 'move', + icon: , + children: assistants + .filter((a) => a.id !== assistant.id) + .map((a) => ({ + label: a.name, + key: a.id, + onClick: () => onMoveTopic(topic, a) + })) + }) + } + + if (assistant.topics.length > 1 && !topic.pinned) { + menus.push({ type: 'divider' }) + menus.push({ + label: t('common.delete'), + danger: true, + key: 'delete', + icon: , + onClick: () => onDeleteTopic(topic) + }) + } + + return menus + }, [ + targetTopic, + t, + isRenaming, + exportMenuOptions.image, + exportMenuOptions.markdown, + exportMenuOptions.markdown_reason, + exportMenuOptions.docx, + exportMenuOptions.notion, + exportMenuOptions.yuque, + exportMenuOptions.obsidian, + exportMenuOptions.joplin, + exportMenuOptions.siyuan, + assistants, + notesPath, + assistant, + updateTopic, + activeTopic.id, + setActiveTopic, + onPinTopic, + onClearMessages, + setTopicPosition, + onMoveTopic, + onDeleteTopic + ]) + + // Sort topics based on pinned status if pinTopicsToTop is enabled + const sortedTopics = useMemo(() => { + if (pinTopicsToTop) { + return [...assistant.topics].sort((a, b) => { + if (a.pinned && !b.pinned) return -1 + if (!a.pinned && b.pinned) return 1 + return 0 + }) + } + return assistant.topics + }, [assistant.topics, pinTopicsToTop]) + + const singlealone = topicPosition === 'right' && position === 'right' + + return ( + EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> + + {t('chat.add.topic.title')} + + }> + {(topic) => { + const isActive = topic.id === activeTopic?.id + const topicName = topic.name.replace('`', '') + const topicPrompt = topic.prompt + const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt + + const getTopicNameClassName = () => { + if (isRenaming(topic.id)) return 'shimmer' + if (isNewlyRenamed(topic.id)) return 'typing' + return '' + } + + return ( + + setTargetTopic(topic)} + className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} + onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)} + style={{ + borderRadius, + cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer' + }}> + {isPending(topic.id) && !isActive && } + {isFulfilled(topic.id) && !isActive && } + + {editingTopicId === topic.id && topicEdit.isEditing ? ( + e.stopPropagation()} + /> + ) : ( + { + setEditingTopicId(topic.id) + topicEdit.startEdit(topic.name) + }}> + {topicName} + + )} + {!topic.pinned && ( + + {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} + + }> + { + if (e.ctrlKey || e.metaKey) { + handleConfirmDelete(topic, e) + } else if (deletingTopicId === topic.id) { + handleConfirmDelete(topic, e) + } else { + handleDeleteClick(topic.id, e) + } + }}> + {deletingTopicId === topic.id ? ( + + ) : ( + + )} + + + )} + {topic.pinned && ( + + + + )} + + {topicPrompt && ( + + {fullTopicPrompt} + + )} + {showTopicTime && {dayjs(topic.createdAt).format('MM/DD HH:mm')}} + + + ) + }} + + ) +} + +const TopicListItem = styled.div` + padding: 7px 12px; + border-radius: var(--list-item-border-radius); + font-size: 13px; + display: flex; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + width: calc(var(--assistants-width) - 20px); + + .menu { + opacity: 0; + color: var(--color-text-3); + } + + &:hover { + background-color: var(--color-list-item-hover); + transition: background-color 0.1s; + + .menu { + opacity: 1; + } + } + + &.active { + background-color: var(--color-list-item); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + .menu { + opacity: 1; + + &:hover { + color: var(--color-text-2); + } + } + } + &.singlealone { + border-radius: 0 !important; + &:hover { + background-color: var(--color-background-soft); + } + &.active { + border-left: 2px solid var(--color-primary); + box-shadow: none; + } + } +` + +const TopicNameContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + height: 20px; + justify-content: space-between; +` + +const TopicName = styled.div` + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 13px; + position: relative; + will-change: background-position, width; + + --color-shimmer-mid: var(--color-text-1); + --color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent); + + &.shimmer { + background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end)); + background-size: 200% 100%; + background-clip: text; + color: transparent; + animation: shimmer 3s linear infinite; + } + + &.typing { + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + white-space: nowrap; + overflow: hidden; + animation: typewriter 0.5s steps(40, end); + } + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } + + @keyframes typewriter { + from { + width: 0; + } + to { + width: 100%; + } + } +` + +const TopicEditInput = styled.input` + background: var(--color-background); + border: none; + color: var(--color-text-1); + font-size: 13px; + font-family: inherit; + padding: 2px 6px; + width: 100%; + outline: none; + padding: 0; +` + +const PendingIndicator = styled.div.attrs({ + className: 'animation-pulse' +})` + --pulse-size: 5px; + width: 5px; + height: 5px; + position: absolute; + left: 3px; + top: 15px; + border-radius: 50%; + background-color: var(--color-status-warning); +` + +const FulfilledIndicator = styled.div.attrs({ + className: 'animation-pulse' +})` + --pulse-size: 5px; + width: 5px; + height: 5px; + position: absolute; + left: 3px; + top: 15px; + border-radius: 50%; + background-color: var(--color-status-success); +` + +const AddTopicButton = styled.div` + display: flex; + align-items: center; + gap: 6px; + width: calc(100% - 10px); + padding: 7px 12px; + margin-bottom: 8px; + background: transparent; + color: var(--color-text-2); + font-size: 13px; + border-radius: var(--list-item-border-radius); + cursor: pointer; + transition: all 0.2s; + margin-top: -5px; + + &:hover { + background-color: var(--color-list-item-hover); + color: var(--color-text-1); + } + + .anticon { + font-size: 12px; + } +` + +const TopicPromptText = styled.div` + color: var(--color-text-2); + font-size: 12px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + ~ .prompt-text { + margin-top: 10px; + } +` + +const TopicTime = styled.div` + color: var(--color-text-3); + font-size: 11px; +` + +const MenuButton = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + min-width: 20px; + min-height: 20px; + .anticon { + font-size: 12px; + } +` diff --git a/src/renderer/src/pages/home/Tabs/index.tsx b/src/renderer/src/pages/home/Tabs/index.tsx index a8b8cdeedc..fb29766528 100644 --- a/src/renderer/src/pages/home/Tabs/index.tsx +++ b/src/renderer/src/pages/home/Tabs/index.tsx @@ -47,7 +47,7 @@ const HomeTabs: FC = ({ const { t } = useTranslation() const { chat } = useRuntime() - const { activeTabId: tab } = chat + const { activeTabId: tab, activeTopicOrSession } = chat const dispatch = useAppDispatch() const setTab = useCallback( @@ -126,7 +126,7 @@ const HomeTabs: FC = ({ {t('assistants.abbr')} setTab('topic')}> - {t('common.topics')} + {activeTopicOrSession === 'topic' ? t('common.topics') : t('agent.session.label_other')} setTab('settings')}> {t('settings.title')} diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index 41e9a71b90..00c0e43229 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -13,6 +13,7 @@ export interface ChatState { /** UI state. Map agent id to active session id. * null represents no active session */ activeSessionId: Record + /** meanwhile active Assistants or Agents */ activeTopicOrSession: 'topic' | 'session' activeTabId: Tab /** topic ids that are currently being renamed */