25531ecd76
* fix(MinappPopupContainer): 修复内存泄漏问题,清理未使用的定时器 在组件卸载时清理setTimeout定时器,避免潜在的内存泄漏 * fix(SelectModelButton): 修复模型选择后更新导致的卡顿问题 使用useRef存储定时器并在组件卸载时清理,避免内存泄漏 * fix(QuickPanel): 修复定时器未清理导致的内存泄漏问题 添加 clearSearchTimerRef 和 focusTimerRef 来管理定时器 在组件清理和状态变化时清理所有定时器 * fix(useInPlaceEdit): 修复编辑模式下定时器未清理的问题 添加清理定时器的逻辑,避免组件卸载时内存泄漏 * refactor(useKnowledge): 使用ref管理定时器并统一检查知识库逻辑 将分散的setTimeout调用统一为checkAllBases方法 使用useRef管理定时器并在组件卸载时清理 * fix(useScrollPosition): 修复滚动位置恢复时的内存泄漏问题 添加清理函数以清除未完成的定时器,防止组件卸载时内存泄漏 * fix(WebSearchProviderSetting): 清理定时器防止内存泄漏 在组件卸载时清理检查API有效性的定时器,避免潜在的内存泄漏问题 * fix(selection-toolbar): 修复选中文本时定时器未清理的问题 * fix(translate): 修复复制文本时定时器未清理的问题 添加 copyTimerRef 来管理复制操作的定时器,并在组件卸载时清理定时器 * fix(WebSearchSettings): 使用useRef管理订阅验证定时器以避免内存泄漏 * fix(MCPSettings): 修复定时器未清理导致的内存泄漏问题 添加 useRef 来存储定时器引用,并在组件卸载时清理定时器 * refactor(ThinkingBlock): 使用 useTemporaryValue 替换手动 setTimeout 移除手动设置的 setTimeout 来重置 copied 状态,改用 useTemporaryValue hook 自动处理 * refactor(ChatNavigation): 使用 useRef 替代 useState 管理定时器 简化定时器管理逻辑,避免不必要的状态更新 * fix(AddAssistantPopup): 清理创建助手时的定时器以避免内存泄漏 添加useEffect清理定时器,防止组件卸载时内存泄漏 * feat(hooks): 添加useTimer钩子管理定时器 实现一个自定义hook来集中管理setTimeout和setInterval定时器 自动在组件卸载时清理所有定时器防止内存泄漏 * refactor(Inputbar): 使用 useTimer 替换 setTimeout 实现延迟更新 将 setTimeout 替换为 useTimer 的自定义 setTimeoutTimer 方法,提高代码可维护性并统一计时器管理 * refactor(WindowFooter): 使用 useTimer 替换 setTimeout 以管理定时器 * docs(useTimer): 更新定时器hook的注释格式和描述 * feat(hooks): 为useTimer添加返回清理函数的功能 允许从setTimeoutTimer和setIntervalTimer返回清理函数,便于手动清除定时器 * refactor(ImportAgentPopup): 使用useTimer替换setTimeout以管理定时器 * refactor: 使用useTimer替代setTimeout以优化定时器管理 * refactor(SearchResults): 使用useTimer替换setTimeout以管理定时器 * refactor(消息组件): 使用useTimer替换setTimeout以管理定时器 * refactor: 使用useTimer替换setTimeout以优化定时器管理 * refactor(AssistantsDrawer): 使用useTimer替换setTimeout以优化定时器管理 * refactor(Inputbar): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MCPToolsButton): 使用useTimer优化定时器管理 * refactor(QuickPhrasesButton): 使用useTimer替换setTimeout以优化定时器管理 * refactor(ChatFlowHistory): 使用useTimer替换setTimeout以管理定时器 * refactor(Message): 使用useTimer替换setTimeout以管理定时器 * refactor(MessageAnchorLine): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MessageGroup): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MessageMenubar): 使用 useTemporaryValue 替换手动 setTimeout 逻辑 * refactor(Messages): 使用 useTimer 优化定时器管理 * refactor(MessageTools): 使用useTimer替换setTimeout以管理定时器 * fix(SelectionBox): 修复鼠标移动时未清除定时器导致的内存泄漏 在鼠标移动事件处理中增加定时器清理逻辑,避免组件卸载时未清除定时器导致的内存泄漏问题 * refactor(ErrorBlock): 使用自定义hook替换setTimeout 使用useTimer中的setTimeoutTimer替代原生setTimeout,便于统一管理定时器 * refactor(GeneralSettings): 使用useTimer替换setTimeout以实现更好的定时器管理 * refactor(ShortcutSettings): 使用useTimer替换setTimeout以优化定时器管理 * refactor(AssistantModelSettings): 使用useTimer替换setTimeout以优化定时器管理 * refactor(DataSettings): 使用useTimer替换setTimeout以增强定时器管理 统一使用useTimer hook管理所有定时器操作,提高代码可维护性 * refactor(NutstoreSettings): 使用useTimer优化setTimeout管理 替换直接使用setTimeout为useTimer hook的setTimeoutTimer方法,提升定时器管理的可维护性 * refactor(MCPSettings): 使用useTimer替换setTimeout以提升代码可维护性 * refactor(ProviderSetting): 使用useTimer优化setTimeout管理 * refactor(ProviderSettings): 使用 useTimer 替换 setTimeout 以优化定时器管理 * refactor(InputBar): 使用useTimer替换setTimeout以实现更好的定时器管理 * refactor(MessageEditor): 使用useTimer替换setTimeout以管理定时器 使用自定义hook useTimer来替代原生setTimeout,便于统一管理和清理定时器 * docs(useTimer): 添加 useTimer hook 的使用示例和详细说明 * refactor(MinappPopupContainer): 使用useTimer替换setTimeout实现 替换直接使用setTimeout为自定义hook useTimer,简化组件清理逻辑 * refactor(AddAssistantPopup): 使用useTimer替换手动定时器管理 用useTimer钩子替代手动管理定时器,简化代码并提高可维护性 * refactor(WebSearchSettings): 使用 useTimer 替换手动定时器管理 移除手动管理的定时器逻辑,改用 useTimer hook 统一处理 * refactor(WebSearchProviderSetting): 使用自定义hook替代原生定时器 用useTimer hook替换原有的setTimeout和clearTimeout逻辑,提高代码可维护性 * refactor(translate): 使用 useTemporaryValue 替换手动实现的复制状态定时器 * refactor(SelectionToolbar): 使用 useTimer 钩子替换 setTimeout 和 clearTimeout 重构 SelectionToolbar 组件,使用自定义的 useTimer 钩子来管理定时器,提升代码可维护性 清理隐藏时的定时器逻辑,避免内存泄漏
271 lines
8.1 KiB
TypeScript
271 lines
8.1 KiB
TypeScript
import { TopView } from '@renderer/components/TopView'
|
|
import { useAgents } from '@renderer/hooks/useAgents'
|
|
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
|
import { useTimer } from '@renderer/hooks/useTimer'
|
|
import { useSystemAgents } from '@renderer/pages/agents'
|
|
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
import { Agent, Assistant } from '@renderer/types'
|
|
import { uuid } from '@renderer/utils'
|
|
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
|
|
import { take } from 'lodash'
|
|
import { Search } from 'lucide-react'
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import styled from 'styled-components'
|
|
|
|
import EmojiIcon from '../EmojiIcon'
|
|
import { HStack } from '../Layout'
|
|
import Scrollbar from '../Scrollbar'
|
|
|
|
interface Props {
|
|
resolve: (value: Assistant | undefined) => void
|
|
}
|
|
|
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|
const [open, setOpen] = useState(true)
|
|
const { t } = useTranslation()
|
|
const { agents: userAgents } = useAgents()
|
|
const [searchText, setSearchText] = useState('')
|
|
const { defaultAssistant } = useDefaultAssistant()
|
|
const { assistants, addAssistant } = useAssistants()
|
|
const inputRef = useRef<InputRef>(null)
|
|
const systemAgents = useSystemAgents()
|
|
const loadingRef = useRef(false)
|
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const { setTimeoutTimer } = useTimer()
|
|
|
|
const agents = useMemo(() => {
|
|
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
|
const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
|
|
const filtered = searchText
|
|
? list.filter(
|
|
(agent) =>
|
|
agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()) ||
|
|
agent.description?.toLowerCase().includes(searchText.trim().toLocaleLowerCase())
|
|
)
|
|
: list
|
|
|
|
if (searchText.trim()) {
|
|
const newAgent: Agent = {
|
|
id: 'new',
|
|
name: searchText.trim(),
|
|
prompt: '',
|
|
topics: [],
|
|
type: 'assistant',
|
|
emoji: '⭐️'
|
|
}
|
|
return [newAgent, ...filtered]
|
|
}
|
|
return filtered
|
|
}, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
|
|
|
|
// 重置选中索引当搜索或列表内容变更时
|
|
useEffect(() => {
|
|
setSelectedIndex(0)
|
|
}, [agents.length, searchText])
|
|
|
|
const onCreateAssistant = useCallback(
|
|
async (agent: Agent) => {
|
|
if (loadingRef.current) {
|
|
return
|
|
}
|
|
|
|
loadingRef.current = true
|
|
let assistant: Assistant
|
|
|
|
if (agent.id === 'default') {
|
|
assistant = { ...agent, id: uuid() }
|
|
addAssistant(assistant)
|
|
} else {
|
|
assistant = await createAssistantFromAgent(agent)
|
|
}
|
|
|
|
setTimeoutTimer('onCreateAssistant', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
|
resolve(assistant)
|
|
setOpen(false)
|
|
},
|
|
[setTimeoutTimer, resolve, addAssistant]
|
|
) // 添加函数内使用的依赖项
|
|
// 键盘导航处理
|
|
useEffect(() => {
|
|
if (!open) return
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
const displayedAgents = take(agents, 100)
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
setSelectedIndex((prev) => (prev >= displayedAgents.length - 1 ? 0 : prev + 1))
|
|
break
|
|
case 'ArrowUp':
|
|
e.preventDefault()
|
|
setSelectedIndex((prev) => (prev <= 0 ? displayedAgents.length - 1 : prev - 1))
|
|
break
|
|
case 'Enter':
|
|
case 'NumpadEnter':
|
|
// 如果焦点在输入框且有搜索内容,则默认选择第一项
|
|
if (document.activeElement === inputRef.current?.input && searchText.trim()) {
|
|
e.preventDefault()
|
|
onCreateAssistant(displayedAgents[selectedIndex])
|
|
}
|
|
// 否则选择当前选中项
|
|
else if (selectedIndex >= 0 && selectedIndex < displayedAgents.length) {
|
|
e.preventDefault()
|
|
onCreateAssistant(displayedAgents[selectedIndex])
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [open, selectedIndex, agents, searchText, onCreateAssistant])
|
|
|
|
// 确保选中项在可视区域
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
const agentItems = containerRef.current.querySelectorAll('.agent-item')
|
|
if (agentItems[selectedIndex]) {
|
|
agentItems[selectedIndex].scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest'
|
|
})
|
|
}
|
|
}
|
|
}, [selectedIndex])
|
|
|
|
const onCancel = () => {
|
|
setOpen(false)
|
|
}
|
|
|
|
const onClose = async () => {
|
|
resolve(undefined)
|
|
AddAssistantPopup.hide()
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
|
|
const timer = setTimeout(() => inputRef.current?.focus(), 0)
|
|
return () => clearTimeout(timer)
|
|
}, [open])
|
|
|
|
return (
|
|
<Modal
|
|
centered
|
|
open={open}
|
|
onCancel={onCancel}
|
|
afterClose={onClose}
|
|
transitionName="animation-move-down"
|
|
styles={{
|
|
content: {
|
|
borderRadius: 20,
|
|
padding: 0,
|
|
overflow: 'hidden',
|
|
paddingBottom: 20
|
|
},
|
|
body: {
|
|
padding: 0
|
|
}
|
|
}}
|
|
closeIcon={null}
|
|
footer={null}>
|
|
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
|
|
<Input
|
|
prefix={
|
|
<SearchIcon>
|
|
<Search size={14} />
|
|
</SearchIcon>
|
|
}
|
|
ref={inputRef}
|
|
placeholder={t('assistants.search')}
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
allowClear
|
|
autoFocus
|
|
style={{ paddingLeft: 0 }}
|
|
variant="borderless"
|
|
size="middle"
|
|
/>
|
|
</HStack>
|
|
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
|
<Container ref={containerRef}>
|
|
{take(agents, 100).map((agent, index) => (
|
|
<AgentItem
|
|
key={agent.id}
|
|
onClick={() => onCreateAssistant(agent)}
|
|
className={`agent-item ${agent.id === 'default' ? 'default' : ''} ${index === selectedIndex ? 'keyboard-selected' : ''}`}
|
|
onMouseEnter={() => setSelectedIndex(index)}>
|
|
<HStack alignItems="center" gap={5} style={{ overflow: 'hidden', maxWidth: '100%' }}>
|
|
<EmojiIcon emoji={agent.emoji || ''} />
|
|
<span className="text-nowrap">{agent.name}</span>
|
|
</HStack>
|
|
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
|
|
{agent.type === 'agent' && <Tag color="orange">{t('agents.tag.agent')}</Tag>}
|
|
{agent.id === 'new' && <Tag color="green">{t('agents.tag.new')}</Tag>}
|
|
</AgentItem>
|
|
))}
|
|
</Container>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
const Container = styled(Scrollbar)`
|
|
padding: 0 12px;
|
|
height: 50vh;
|
|
margin-top: 10px;
|
|
`
|
|
|
|
const AgentItem = styled.div`
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 15px;
|
|
border-radius: 8px;
|
|
user-select: none;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
&.default {
|
|
background-color: var(--color-background-mute);
|
|
}
|
|
&.keyboard-selected {
|
|
background-color: var(--color-background-mute);
|
|
}
|
|
.anticon {
|
|
font-size: 16px;
|
|
color: var(--color-icon);
|
|
}
|
|
&:hover {
|
|
background-color: var(--color-background-mute);
|
|
}
|
|
`
|
|
|
|
const SearchIcon = styled.div`
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: var(--color-background-mute);
|
|
margin-right: 2px;
|
|
`
|
|
|
|
export default class AddAssistantPopup {
|
|
static topviewId = 0
|
|
static hide() {
|
|
TopView.hide('AddAssistantPopup')
|
|
}
|
|
static show() {
|
|
return new Promise<Assistant | undefined>((resolve) => {
|
|
TopView.show(<PopupContainer resolve={resolve} />, 'AddAssistantPopup')
|
|
})
|
|
}
|
|
}
|