diff --git a/src/renderer/src/hooks/useInPlaceEdit.ts b/src/renderer/src/hooks/useInPlaceEdit.ts new file mode 100644 index 0000000000..3af085f99a --- /dev/null +++ b/src/renderer/src/hooks/useInPlaceEdit.ts @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +export interface UseInPlaceEditOptions { + onSave: (value: string) => void + onCancel?: () => void + autoSelectOnStart?: boolean + trimOnSave?: boolean +} + +export interface UseInPlaceEditReturn { + isEditing: boolean + editValue: string + inputRef: React.RefObject + startEdit: (initialValue: string) => void + saveEdit: () => void + cancelEdit: () => void + handleKeyDown: (e: React.KeyboardEvent) => void + handleInputChange: (e: React.ChangeEvent) => void +} + +export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditReturn { + const { onSave, onCancel, autoSelectOnStart = true, trimOnSave = true } = options + + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState('') + const [originalValue, setOriginalValue] = useState('') + const inputRef = useRef(null) + + const startEdit = useCallback( + (initialValue: string) => { + setIsEditing(true) + setEditValue(initialValue) + setOriginalValue(initialValue) + + setTimeout(() => { + inputRef.current?.focus() + if (autoSelectOnStart) { + inputRef.current?.select() + } + }, 0) + }, + [autoSelectOnStart] + ) + + const saveEdit = useCallback(() => { + const finalValue = trimOnSave ? editValue.trim() : editValue + if (finalValue !== originalValue) { + onSave(finalValue) + } + setIsEditing(false) + setEditValue('') + setOriginalValue('') + }, [editValue, originalValue, onSave, trimOnSave]) + + const cancelEdit = useCallback(() => { + setIsEditing(false) + setEditValue('') + setOriginalValue('') + onCancel?.() + }, [onCancel]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + saveEdit() + } else if (e.key === 'Escape') { + e.preventDefault() + cancelEdit() + } + }, + [saveEdit, cancelEdit] + ) + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setEditValue(e.target.value) + }, []) + + // Handle clicks outside the input to save + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (isEditing && inputRef.current && !inputRef.current.contains(event.target as Node)) { + saveEdit() + } + } + + if (isEditing) { + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + } + return + }, [isEditing, saveEdit]) + + return { + isEditing, + editValue, + inputRef, + startEdit, + saveEdit, + cancelEdit, + handleKeyDown, + handleInputChange + } +} diff --git a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx index 7f4b094886..8c32b81f41 100644 --- a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx +++ b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx @@ -98,6 +98,7 @@ const PopupContainer: React.FC = ({ resolve }) => { title={t('agents.import.title')} open={open} onCancel={onCancel} + maskClosable={false} footer={ diff --git a/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx b/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx index d6e9621fd5..0354032fa1 100644 --- a/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/QuickPhrasesButton.tsx @@ -158,6 +158,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton, title={t('settings.quickPhrase.add')} open={isModalOpen} onOk={handleModalOk} + maskClosable={false} onCancel={() => { setIsModalOpen(false) setFormData({ title: '', content: '', location: 'global' }) diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index d5f3346a06..bfbfee7eea 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -4,6 +4,7 @@ import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup import PromptPopup from '@renderer/components/Popups/PromptPopup' import { isMac } from '@renderer/config/constant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' +import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' import { modelGenerating } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic' @@ -70,6 +71,22 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, 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.message.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]) @@ -203,16 +220,9 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, key: 'rename', icon: , disabled: isRenaming(topic.id), - async onClick() { - const name = await PromptPopup.show({ - title: t('chat.topics.edit.title'), - message: '', - defaultValue: topic?.name || '' - }) - if (name && topic?.name !== name) { - const updatedTopic = { ...topic, name, isNameManuallyEdited: true } - updateTopic(updatedTopic) - } + onClick() { + setEditingTopicId(topic.id) + topicEdit.startEdit(topic.name) } }, { @@ -415,6 +425,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, assistants, assistant, updateTopic, + topicEdit, activeTopic.id, setActiveTopic, onPinTopic, @@ -468,14 +479,27 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, setTargetTopic(topic)} className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} - onClick={() => onSwitchTopic(topic)} - style={{ borderRadius }}> + 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 && } - - {topicName} - + {editingTopicId === topic.id && topicEdit.isEditing ? ( + e.stopPropagation()} + /> + ) : ( + + {topicName} + + )} {!topic.pinned && ( = ({ size = 60 }) => { setIsModalVisible(false) setFileList([]) }} + maskClosable={false} footer={null} transitionName="animation-move-down" centered> diff --git a/src/renderer/src/pages/settings/AssistantSettings/index.tsx b/src/renderer/src/pages/settings/AssistantSettings/index.tsx index 042bf9bf22..3b7140512d 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/index.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/index.tsx @@ -94,6 +94,7 @@ const AssistantSettingPopupContainer: React.FC = ({ resolve, tab, ...prop onOk={onOk} onCancel={onCancel} afterClose={afterClose} + maskClosable={false} footer={null} title={assistant.name} transitionName="animation-move-down" diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx index fc7105b5c5..829190eb95 100644 --- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -115,6 +115,7 @@ const PopupContainer: React.FC = ({ resolve }) => { onOk={onOk} onCancel={onCancel} afterClose={onClose} + maskClosable={false} width={800} height="80vh" loading={jsonSaving} diff --git a/src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx b/src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx index 3e28bd3239..950f808059 100644 --- a/src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx @@ -46,6 +46,7 @@ const PopupContainer: React.FC = ({ resolve }) => { onOk={onOk} onCancel={onCancel} afterClose={onClose} + maskClosable={false} transitionName="animation-move-down" footer={null} centered> diff --git a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx index 54a3b7c822..7f74c457f1 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx @@ -68,6 +68,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { onOk={onOk} onCancel={onCancel} afterClose={onClose} + maskClosable={false} transitionName="animation-move-down" centered> diff --git a/src/renderer/src/pages/settings/ToolSettings/QuickPhraseSettings.tsx b/src/renderer/src/pages/settings/ToolSettings/QuickPhraseSettings.tsx index 6bc11e5e53..8cddc8aa4b 100644 --- a/src/renderer/src/pages/settings/ToolSettings/QuickPhraseSettings.tsx +++ b/src/renderer/src/pages/settings/ToolSettings/QuickPhraseSettings.tsx @@ -133,7 +133,8 @@ const QuickPhraseSettings: FC = () => { onCancel={() => setIsModalOpen(false)} width={520} transitionName="animation-move-down" - centered> + centered + maskClosable={false}>