mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
* fix(QuickPanel): auto-close panel when no commands match (#7824) Fixes the issue where QuickPanel remains visible when user types invalid slash commands. Now the panel intelligently closes after 300ms when no matching commands are found. - Add smart delayed closing mechanism for unmatched searches - Optimize memory management with proper timer cleanup - Preserve existing trigger behavior for / and @ symbols * feat(inputbar): intelligent @ symbol handling on model selection panel close - Add smart @ character deletion when user selects models and closes panel with ESC/Backspace - Preserve @ character when user closes panel without selecting any models - Implement action tracking using useRef to detect user model interactions - Support both ESC key and Backspace key for consistent behavior - Use React setState instead of DOM manipulation for proper state management Resolves user experience issue where @ symbol always remained after closing model selection panel * perf(QuickPanel): optimize timer management and fix React anti-patterns - Move side effects from useMemo to useEffect for proper React lifecycle - Add automatic timer cleanup on component unmount and dependency changes - Remove unnecessary timer creation/destruction on each search input - Improve memory management and prevent potential memory leaks - Maintain existing smart auto-close functionality with better performance Fixes React anti-pattern where side effects were executed in useMemo, which should be a pure function. This improves performance especially when users type quickly in the search input. * refactor(QuickPanel): remove redundant timer cleanup useEffect Remove duplicate timer cleanup logic as the existing useEffect at line 141-164 already handles component unmount cleanup properly. * refactor(QuickPanel): optimize useEffect dependencies and timer cleanup logic - Replace overly broad `ctx` dependency with precise `[ctx.isVisible, searchText, list.length, ctx.close]` - Move timer cleanup before visibility check to ensure proper cleanup on panel hide - Add early return when panel is invisible to prevent unnecessary timer creation - Improve performance by avoiding redundant effect executions - Fix edge case where timers might not be cleared when panel becomes invisible Addresses review feedback about dependency array optimization while maintaining existing auto-close functionality and improving memory management. * feat(QuickPanel): implement smart re-opening with dependency optimization Features: - Implement smart re-opening during deletion with real-time matching - Only reopen panel when actual matches exist to avoid unnecessary interactions - Add intelligent @ symbol handling on model selection panel close - Optimize search text length limits (≤10 chars) for performance Fixes: - Fix useMemo dependency from overly broad [ctx, searchText] to precise [ctx.isVisible, ctx.symbol, ctx.list, searchText] - Resolve trailing whitespace formatting issues - Eliminate ESLint exhaustive-deps warnings while maintaining stability - Prevent unnecessary re-renders when unrelated ctx properties change Performance improvements ensure optimal QuickPanel responsiveness while maintaining existing auto-close functionality and improving user experience. * fix(ci): add eslint-disable comment for exhaustive-deps warning The useEffect dependency array [ctx.isVisible, searchText, list.length, ctx.close] is intentionally precise to avoid unnecessary re-renders when unrelated ctx properties change. Adding ctx object would cause performance degradation. * refactor(QuickPanel): remove smart re-opening logic during deletion - Remove 62 lines of complex deletion detection logic from Inputbar component - Eliminates performance overhead from frequent string matching during typing - Reduces code complexity and potential edge cases - Maintains simple and predictable QuickPanel behavior - Improves maintainability by removing unnecessary "smart" features The deletion-triggered smart reopening feature added unnecessary complexity without significant user benefit. Users can simply type / or @ again to reopen panels when needed.
This commit is contained in:
parent
a4c61bcd66
commit
1bf380a921
@ -66,6 +66,9 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
const prevSearchTextRef = useRef('')
|
||||
const prevSymbolRef = useRef('')
|
||||
|
||||
// 无匹配项自动关闭的定时器
|
||||
const noMatchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// 处理搜索,过滤列表
|
||||
const list = useMemo(() => {
|
||||
if (!ctx.isVisible && !ctx.symbol) return []
|
||||
@ -128,12 +131,44 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
prevSymbolRef.current = ctx.symbol
|
||||
|
||||
return newList
|
||||
}, [ctx.isVisible, ctx.list, ctx.symbol, searchText])
|
||||
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
|
||||
|
||||
const canForwardAndBackward = useMemo(() => {
|
||||
return list.some((item) => item.isMenu) || historyPanel.length > 0
|
||||
}, [list, historyPanel])
|
||||
|
||||
// 智能关闭逻辑:当有搜索文本但无匹配项时,延迟关闭面板
|
||||
useEffect(() => {
|
||||
const _searchText = searchText.replace(/^[/@]/, '')
|
||||
|
||||
// 清除之前的定时器(无论面板是否可见都要清理)
|
||||
if (noMatchTimeoutRef.current) {
|
||||
clearTimeout(noMatchTimeoutRef.current)
|
||||
noMatchTimeoutRef.current = null
|
||||
}
|
||||
|
||||
// 面板不可见时不设置新定时器
|
||||
if (!ctx.isVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
// 只有在有搜索文本但无匹配项时才设置延迟关闭
|
||||
if (_searchText && _searchText.length > 0 && list.length === 0) {
|
||||
noMatchTimeoutRef.current = setTimeout(() => {
|
||||
ctx.close('no-matches')
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (noMatchTimeoutRef.current) {
|
||||
clearTimeout(noMatchTimeoutRef.current)
|
||||
noMatchTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ctx对象引用不稳定,使用具体属性避免过度重渲染
|
||||
}, [ctx.isVisible, searchText, list.length, ctx.close])
|
||||
|
||||
const clearSearchText = useCallback(
|
||||
(includeSymbol = false) => {
|
||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||
@ -275,7 +310,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
const newSearchText = textBeforeCursor.slice(lastSymbolIndex)
|
||||
setSearchText(newSearchText)
|
||||
} else {
|
||||
handleClose('delete-symbol')
|
||||
ctx.close('delete-symbol')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -397,6 +397,7 @@ const InputbarTools = ({
|
||||
ToolbarButton={ToolbarButton}
|
||||
couldMentionNotVisionModel={couldMentionNotVisionModel}
|
||||
files={files}
|
||||
setText={setText}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
@ -27,6 +27,7 @@ interface Props {
|
||||
couldMentionNotVisionModel: boolean
|
||||
files: FileType[]
|
||||
ToolbarButton: any
|
||||
setText: React.Dispatch<React.SetStateAction<string>>
|
||||
}
|
||||
|
||||
const MentionModelsButton: FC<Props> = ({
|
||||
@ -35,13 +36,17 @@ const MentionModelsButton: FC<Props> = ({
|
||||
onMentionModel,
|
||||
couldMentionNotVisionModel,
|
||||
files,
|
||||
ToolbarButton
|
||||
ToolbarButton,
|
||||
setText
|
||||
}) => {
|
||||
const { providers } = useProviders()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const quickPanel = useQuickPanel()
|
||||
|
||||
// 记录是否有模型被选择的动作发生
|
||||
const hasModelActionRef = useRef<boolean>(false)
|
||||
|
||||
const pinnedModels = useLiveQuery(
|
||||
async () => {
|
||||
const setting = await db.settings.get('pinned:models')
|
||||
@ -74,7 +79,10 @@ const MentionModelsButton: FC<Props> = ({
|
||||
</Avatar>
|
||||
),
|
||||
filterText: getFancyProviderName(p) + m.name,
|
||||
action: () => onMentionModel(m),
|
||||
action: () => {
|
||||
hasModelActionRef.current = true // 标记有模型动作发生
|
||||
onMentionModel(m)
|
||||
},
|
||||
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
}))
|
||||
)
|
||||
@ -107,7 +115,10 @@ const MentionModelsButton: FC<Props> = ({
|
||||
</Avatar>
|
||||
),
|
||||
filterText: getFancyProviderName(p) + m.name,
|
||||
action: () => onMentionModel(m),
|
||||
action: () => {
|
||||
hasModelActionRef.current = true // 标记有模型动作发生
|
||||
onMentionModel(m)
|
||||
},
|
||||
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
}))
|
||||
|
||||
@ -127,6 +138,9 @@ const MentionModelsButton: FC<Props> = ({
|
||||
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
|
||||
|
||||
const openQuickPanel = useCallback(() => {
|
||||
// 重置模型动作标记
|
||||
hasModelActionRef.current = false
|
||||
|
||||
quickPanel.open({
|
||||
title: t('agents.edit.model.select.title'),
|
||||
list: modelItems,
|
||||
@ -134,9 +148,25 @@ const MentionModelsButton: FC<Props> = ({
|
||||
multiple: true,
|
||||
afterAction({ item }) {
|
||||
item.isSelected = !item.isSelected
|
||||
},
|
||||
onClose({ action }) {
|
||||
// ESC或Backspace关闭时的特殊处理
|
||||
if (action === 'esc' || action === 'delete-symbol') {
|
||||
// 如果有模型选择动作发生,删除@字符
|
||||
if (hasModelActionRef.current) {
|
||||
// 使用React的setText来更新状态,而不是直接操作DOM
|
||||
setText((currentText) => {
|
||||
const lastAtIndex = currentText.lastIndexOf('@')
|
||||
if (lastAtIndex !== -1) {
|
||||
return currentText.slice(0, lastAtIndex) + currentText.slice(lastAtIndex + 1)
|
||||
}
|
||||
return currentText
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [modelItems, quickPanel, t])
|
||||
}, [modelItems, quickPanel, t, setText])
|
||||
|
||||
const handleOpenQuickPanel = useCallback(() => {
|
||||
if (quickPanel.isVisible && quickPanel.symbol === '@') {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user