mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-02 10:29:02 +08:00
fix: timeout memory leak (#9312)
* 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 钩子来管理定时器,提升代码可维护性 清理隐藏时的定时器逻辑,避免内存泄漏
This commit is contained in:
parent
332ba5d678
commit
25531ecd76
@ -19,6 +19,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
@ -170,6 +171,8 @@ const MinappPopupContainer: React.FC = () => {
|
||||
|
||||
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useBridge()
|
||||
|
||||
/** set the popup display status */
|
||||
@ -295,7 +298,7 @@ const MinappPopupContainer: React.FC = () => {
|
||||
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
||||
}
|
||||
if (appid == currentMinappId) {
|
||||
setTimeout(() => setIsReady(true), 200)
|
||||
setTimeoutTimer('handleWebviewLoaded', () => setIsReady(true), 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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'
|
||||
@ -33,6 +34,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
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[]
|
||||
@ -80,11 +82,11 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
assistant = await createAssistantFromAgent(agent)
|
||||
}
|
||||
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
setTimeoutTimer('onCreateAssistant', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
resolve(assistant)
|
||||
setOpen(false)
|
||||
},
|
||||
[resolve, addAssistant, setOpen]
|
||||
[setTimeoutTimer, resolve, addAssistant]
|
||||
) // 添加函数内使用的依赖项
|
||||
// 键盘导航处理
|
||||
useEffect(() => {
|
||||
|
||||
@ -68,6 +68,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
|
||||
// 无匹配项自动关闭的定时器
|
||||
const noMatchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const clearSearchTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
const focusTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
// 处理搜索,过滤列表
|
||||
const list = useMemo(() => {
|
||||
@ -145,6 +147,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
if (noMatchTimeoutRef.current) {
|
||||
clearTimeout(noMatchTimeoutRef.current)
|
||||
noMatchTimeoutRef.current = null
|
||||
clearTimeout(clearSearchTimerRef.current)
|
||||
clearTimeout(focusTimerRef.current)
|
||||
}
|
||||
|
||||
// 面板不可见时不设置新定时器
|
||||
@ -165,6 +169,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
clearTimeout(noMatchTimeoutRef.current)
|
||||
noMatchTimeoutRef.current = null
|
||||
}
|
||||
clearTimeout(clearSearchTimerRef.current)
|
||||
clearTimeout(focusTimerRef.current)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ctx对象引用不稳定,使用具体属性避免过度重渲染
|
||||
}, [ctx.isVisible, searchText, list.length, ctx.close])
|
||||
@ -192,7 +198,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
newText = inputText.slice(0, start) + inputText.slice(end)
|
||||
setInputText(newText)
|
||||
|
||||
setTimeout(() => {
|
||||
clearTimeout(focusTimerRef.current)
|
||||
focusTimerRef.current = setTimeout(() => {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(start, start)
|
||||
}, 0)
|
||||
@ -333,7 +340,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
textArea.removeEventListener('input', handleInput)
|
||||
textArea.removeEventListener('compositionupdate', handleCompositionUpdate)
|
||||
textArea.removeEventListener('compositionend', handleCompositionEnd)
|
||||
setTimeout(() => {
|
||||
clearTimeout(clearSearchTimerRef.current)
|
||||
clearSearchTimerRef.current = setTimeout(() => {
|
||||
setSearchText('')
|
||||
}, 200) // 等待面板关闭动画结束后,再清空搜索词
|
||||
}
|
||||
|
||||
@ -26,13 +26,22 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
||||
const [originalValue, setOriginalValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const editTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(editTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startEdit = useCallback(
|
||||
(initialValue: string) => {
|
||||
setIsEditing(true)
|
||||
setEditValue(initialValue)
|
||||
setOriginalValue(initialValue)
|
||||
|
||||
setTimeout(() => {
|
||||
clearTimeout(editTimerRef.current)
|
||||
editTimerRef.current = setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
if (autoSelectOnStart) {
|
||||
inputRef.current?.select()
|
||||
|
||||
@ -20,7 +20,7 @@ import { FileMetadata, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@r
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { useAgents } from './useAgents'
|
||||
@ -29,6 +29,7 @@ import { useAssistants } from './useAssistant'
|
||||
export const useKnowledge = (baseId: string) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
|
||||
const checkTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
// 重命名知识库
|
||||
const renameKnowledgeBase = (name: string) => {
|
||||
@ -40,34 +41,46 @@ export const useKnowledge = (baseId: string) => {
|
||||
dispatch(updateBase(base))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(checkTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 检查知识库
|
||||
const checkAllBases = () => {
|
||||
clearTimeout(checkTimerRef.current)
|
||||
checkTimerRef.current = setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
}
|
||||
|
||||
// 批量添加文件
|
||||
const addFiles = (files: FileMetadata[]) => {
|
||||
dispatch(addFilesThunk(baseId, files))
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
|
||||
// 添加笔记
|
||||
const addNote = async (content: string) => {
|
||||
await dispatch(addNoteThunk(baseId, content))
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
|
||||
// 添加URL
|
||||
const addUrl = (url: string) => {
|
||||
dispatch(addItemThunk(baseId, 'url', url))
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
|
||||
// 添加 Sitemap
|
||||
const addSitemap = (url: string) => {
|
||||
dispatch(addItemThunk(baseId, 'sitemap', url))
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
|
||||
// Add directory support
|
||||
const addDirectory = (path: string) => {
|
||||
dispatch(addItemThunk(baseId, 'directory', path))
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
// 更新笔记内容
|
||||
const updateNoteContent = async (noteId: string, content: string) => {
|
||||
@ -133,7 +146,7 @@ export const useKnowledge = (baseId: string) => {
|
||||
uniqueId: undefined,
|
||||
updated_at: Date.now()
|
||||
})
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,7 +242,7 @@ export const useKnowledge = (baseId: string) => {
|
||||
throw new Error(`Failed to migrate files ${files}: ${error}`)
|
||||
}
|
||||
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
checkAllBases()
|
||||
}
|
||||
|
||||
const fileItems = base?.items.filter((item) => item.type === 'file') || []
|
||||
|
||||
@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react'
|
||||
export default function useScrollPosition(key: string) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollKey = `scroll:${key}`
|
||||
const scrollTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const position = containerRef.current?.scrollTop ?? 0
|
||||
@ -15,8 +16,15 @@ export default function useScrollPosition(key: string) {
|
||||
useEffect(() => {
|
||||
const scroll = () => containerRef.current?.scrollTo({ top: window.keyv.get(scrollKey) || 0 })
|
||||
scroll()
|
||||
setTimeout(scroll, 50)
|
||||
clearTimeout(scrollTimerRef.current)
|
||||
scrollTimerRef.current = setTimeout(scroll, 50)
|
||||
}, [scrollKey])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(scrollTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { containerRef, handleScroll }
|
||||
}
|
||||
|
||||
152
src/renderer/src/hooks/useTimer.ts
Normal file
152
src/renderer/src/hooks/useTimer.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 定时器管理 Hook,用于管理 setTimeout 和 setInterval 定时器,支持通过 key 来标识不同的定时器
|
||||
*
|
||||
* - 在设置定时器时以前会自动清理相同key的定时器
|
||||
* - 组件卸载时会自动清理所有定时器,避免内存泄漏
|
||||
*
|
||||
* 通常在 `useEffect` 中使用的定时器,可以通过清理函数处理。但是,在函数中使用的定时器则相对难以管理。
|
||||
* 这个 Hook 主要解决需要在函数中设置定时器的场景。然而,`setTimeoutTimer` 和 `setIntervalTimer` 同样也返回清理函数,因此可以用于 `useEffect` 中。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* function MyComponent() {
|
||||
* const {
|
||||
* setTimeoutTimer,
|
||||
* setIntervalTimer,
|
||||
* clearTimeoutTimer,
|
||||
* clearAllTimers
|
||||
* } = useTimer();
|
||||
*
|
||||
* useEffect(() => {
|
||||
* // 设置一个3秒后执行的定时器
|
||||
* setTimeoutTimer('notify', () => {
|
||||
* console.log('3秒后执行');
|
||||
* }, 3000);
|
||||
*
|
||||
* // 设置一个每5秒执行一次的定时器
|
||||
* const cleanup = setIntervalTimer('poll', () => {
|
||||
* console.log('每5秒执行一次');
|
||||
* }, 5000);
|
||||
*
|
||||
* // 手动清理指定的定时器
|
||||
* clearTimeoutTimer('notify');
|
||||
*
|
||||
* // 返回清理函数来停止轮询
|
||||
* return cleanup;
|
||||
* }, []);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const useTimer = () => {
|
||||
const timeoutMapRef = useRef(new Map<string, NodeJS.Timeout>())
|
||||
const intervalMapRef = useRef(new Map<string, NodeJS.Timeout>())
|
||||
|
||||
// 组件卸载时自动清理所有定时器
|
||||
useEffect(() => {
|
||||
return clearAllTimers
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 设置一个 setTimeout 定时器
|
||||
* @param key - 定时器标识符,用于标识和管理不同的定时器实例
|
||||
* @param args - setTimeout 的参数列表,包含回调函数和延迟时间(毫秒)
|
||||
* @returns 返回一个清理函数,可以用来手动清除该定时器
|
||||
* @example
|
||||
* ```ts
|
||||
* const { setTimeoutTimer } = useTimer();
|
||||
* // 设置一个3秒后执行的定时器
|
||||
* const cleanup = setTimeoutTimer('myTimer', () => {
|
||||
* console.log('Timer executed');
|
||||
* }, 3000);
|
||||
*
|
||||
* // 需要时可以提前清理定时器
|
||||
* cleanup();
|
||||
* ```
|
||||
*/
|
||||
const setTimeoutTimer = (key: string, ...args: Parameters<typeof setTimeout>) => {
|
||||
clearTimeout(timeoutMapRef.current.get(key))
|
||||
const timer = setTimeout(...args)
|
||||
timeoutMapRef.current.set(key, timer)
|
||||
return () => clearTimeoutTimer(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个 setInterval 定时器
|
||||
* @param key - 定时器标识符,用于标识和管理不同的定时器实例
|
||||
* @param args - setInterval 的参数列表,包含回调函数和时间间隔(毫秒)
|
||||
* @returns 返回一个清理函数,可以用来手动清除该定时器
|
||||
* @example
|
||||
* ```ts
|
||||
* const { setIntervalTimer } = useTimer();
|
||||
* // 设置一个每3秒执行一次的定时器
|
||||
* const cleanup = setIntervalTimer('myTimer', () => {
|
||||
* console.log('Timer executed');
|
||||
* }, 3000);
|
||||
*
|
||||
* // 需要时可以停止定时器
|
||||
* cleanup();
|
||||
* ```
|
||||
*/
|
||||
const setIntervalTimer = (key: string, ...args: Parameters<typeof setInterval>) => {
|
||||
clearInterval(intervalMapRef.current.get(key))
|
||||
const timer = setInterval(...args)
|
||||
intervalMapRef.current.set(key, timer)
|
||||
return () => clearIntervalTimer(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定 key 的 setTimeout 定时器
|
||||
* @param key - 定时器标识符
|
||||
*/
|
||||
const clearTimeoutTimer = (key: string) => {
|
||||
clearTimeout(timeoutMapRef.current.get(key))
|
||||
timeoutMapRef.current.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定 key 的 setInterval 定时器
|
||||
* @param key - 定时器标识符
|
||||
*/
|
||||
const clearIntervalTimer = (key: string) => {
|
||||
clearInterval(intervalMapRef.current.get(key))
|
||||
intervalMapRef.current.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有 setTimeout 定时器
|
||||
*/
|
||||
const clearAllTimeoutTimers = () => {
|
||||
timeoutMapRef.current.forEach((timer) => clearTimeout(timer))
|
||||
timeoutMapRef.current.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有 setInterval 定时器
|
||||
*/
|
||||
const clearAllIntervalTimers = () => {
|
||||
intervalMapRef.current.forEach((timer) => clearInterval(timer))
|
||||
intervalMapRef.current.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有定时器,包括 setTimeout 和 setInterval
|
||||
*/
|
||||
const clearAllTimers = () => {
|
||||
timeoutMapRef.current.forEach((timer) => clearTimeout(timer))
|
||||
intervalMapRef.current.forEach((timer) => clearInterval(timer))
|
||||
timeoutMapRef.current.clear()
|
||||
intervalMapRef.current.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
setTimeoutTimer,
|
||||
setIntervalTimer,
|
||||
clearTimeoutTimer,
|
||||
clearIntervalTimer,
|
||||
clearAllTimeoutTimers,
|
||||
clearAllIntervalTimers,
|
||||
clearAllTimers
|
||||
} as const
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Agent } from '@renderer/types'
|
||||
@ -19,6 +20,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const { addAgent } = useAgents()
|
||||
const [importType, setImportType] = useState<'url' | 'file'>('url')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const onFinish = async (values: { url?: string }) => {
|
||||
setLoading(true)
|
||||
@ -77,7 +79,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
key: 'agents-imported'
|
||||
})
|
||||
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
setOpen(false)
|
||||
resolve(agents)
|
||||
} catch (error) {
|
||||
|
||||
@ -4,6 +4,7 @@ import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@ -48,6 +49,7 @@ const CodeToolsPage: FC = () => {
|
||||
removeDir,
|
||||
selectFolder
|
||||
} = useCodeTools()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
// 状态管理
|
||||
const [isLaunching, setIsLaunching] = useState(false)
|
||||
@ -159,7 +161,7 @@ const CodeToolsPage: FC = () => {
|
||||
} finally {
|
||||
setIsInstallingBun(false)
|
||||
// 重新检查安装状态
|
||||
setTimeout(checkBunInstallation, 1000)
|
||||
setTimeoutTimer('handleInstallBun', checkBunInstallation, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import db from '@renderer/databases'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { type Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||
@ -18,6 +19,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
const SearchResults: FC<Props> = ({ keywords, onMessageClick, onTopicClick, ...props }) => {
|
||||
const { handleScroll, containerRef } = useScrollPosition('SearchResults')
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const [searchTerms, setSearchTerms] = useState<string[]>(
|
||||
keywords
|
||||
@ -112,7 +114,7 @@ const SearchResults: FC<Props> = ({ keywords, onMessageClick, onTopicClick, ...p
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
onChange: () => {
|
||||
setTimeout(() => containerRef.current?.scrollTo({ top: 0 }), 0)
|
||||
setTimeoutTimer('scroll', () => containerRef.current?.scrollTo({ top: 0 }), 0)
|
||||
}
|
||||
}}
|
||||
renderItem={({ message, topic, content }) => (
|
||||
|
||||
@ -4,6 +4,7 @@ import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
||||
@ -29,6 +30,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
||||
const dispatch = useAppDispatch()
|
||||
const { messageStyle } = useSettings()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useEffect(() => {
|
||||
topic && dispatch(loadTopicMessagesThunk(topic.id))
|
||||
@ -45,7 +47,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||
SearchPopup.hide()
|
||||
const assistant = getAssistantById(topic.assistantId)
|
||||
navigate('/', { state: { assistant, topic } })
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
|
||||
setTimeoutTimer('onContinueChat', () => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -8,6 +8,7 @@ import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Flex } from 'antd'
|
||||
@ -43,6 +44,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
|
||||
|
||||
const maxWidth = useChatMaxWidth()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
contentSearchRef.current?.disable()
|
||||
@ -79,10 +81,14 @@ const Chat: FC<Props> = (props) => {
|
||||
setFilterIncludeUser(!filterIncludeUser)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
contentSearchRef.current?.search()
|
||||
contentSearchRef.current?.focus()
|
||||
}, 0)
|
||||
setTimeoutTimer(
|
||||
'userOutlinedItemClickHandler',
|
||||
() => {
|
||||
contentSearchRef.current?.search()
|
||||
contentSearchRef.current?.focus()
|
||||
},
|
||||
0
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -99,7 +105,7 @@ const Chat: FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const messagesComponentFirstUpdateHandler = () => {
|
||||
setTimeout(() => (firstUpdateCompleted = true), 300)
|
||||
setTimeoutTimer('messagesComponentFirstUpdateHandler', () => (firstUpdateCompleted = true), 300)
|
||||
firstUpdateOrNoFirstUpdateHandler()
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@ -114,6 +115,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode)
|
||||
const isVisionAssistant = useMemo(() => isVisionModel(model), [model])
|
||||
const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model])
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const isVisionSupported = useMemo(
|
||||
() =>
|
||||
@ -254,14 +256,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
// Clear input
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(true), 0)
|
||||
setTimeoutTimer('sendMessage_1', () => setText(''), 500)
|
||||
setTimeoutTimer('sendMessage_2', () => resizeTextArea(true), 0)
|
||||
setExpand(false)
|
||||
} catch (error) {
|
||||
logger.warn('Failed to send message:', error as Error)
|
||||
parent?.recordException(error as Error)
|
||||
}
|
||||
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, text, topic])
|
||||
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
|
||||
|
||||
const translate = useCallback(async () => {
|
||||
if (isTranslating) {
|
||||
@ -272,13 +274,13 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setIsTranslating(true)
|
||||
const translatedText = await translateText(text, getLanguageByLangcode(targetLanguage))
|
||||
translatedText && setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
setTimeoutTimer('translate', () => resizeTextArea(), 0)
|
||||
} catch (error) {
|
||||
logger.warn('Translation failed:', error as Error)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
}, [isTranslating, text, getLanguageByLangcode, targetLanguage, resizeTextArea])
|
||||
}, [isTranslating, text, getLanguageByLangcode, targetLanguage, setTimeoutTimer, resizeTextArea])
|
||||
|
||||
const openKnowledgeFileList = useCallback(
|
||||
(base: KnowledgeBase) => {
|
||||
@ -428,10 +430,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setText(newText)
|
||||
|
||||
// set cursor position in the next render cycle
|
||||
setTimeout(() => {
|
||||
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||
onInput() // trigger resizeTextArea
|
||||
}, 0)
|
||||
setTimeoutTimer(
|
||||
'handleKeyDown',
|
||||
() => {
|
||||
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||
onInput() // trigger resizeTextArea
|
||||
},
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -457,20 +463,20 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
addTopic(topic)
|
||||
setActiveTopic(topic)
|
||||
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant, setActiveTopic, setModel])
|
||||
setTimeoutTimer('addNewTopic', () => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant.defaultModel, assistant.id, setActiveTopic, setModel, setTimeoutTimer])
|
||||
|
||||
const onQuote = useCallback(
|
||||
(text: string) => {
|
||||
const quotedText = formatQuotedText(text)
|
||||
setText((prevText) => {
|
||||
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
setTimeoutTimer('onQuote', () => resizeTextArea(), 0)
|
||||
return newText
|
||||
})
|
||||
focusTextarea()
|
||||
},
|
||||
[resizeTextArea, focusTextarea]
|
||||
[focusTextarea, setTimeoutTimer, resizeTextArea]
|
||||
)
|
||||
|
||||
const onPause = async () => {
|
||||
@ -608,7 +614,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
|
||||
const onTranslated = (translatedText: string) => {
|
||||
setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
setTimeoutTimer('onTranslated', () => resizeTextArea(), 0)
|
||||
}
|
||||
|
||||
const handleDragStart = (e: React.MouseEvent) => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Assistant, MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
|
||||
import { Form, Input, Tooltip } from 'antd'
|
||||
@ -116,6 +117,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { updateAssistant, assistant } = useAssistant(props.assistant.id)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
// 使用 useRef 存储不需要触发重渲染的值
|
||||
const isMountedRef = useRef(true)
|
||||
@ -154,14 +156,18 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
|
||||
const updateMcpEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setTimeout(() => {
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
mcpServers: enabled ? assistant.mcpServers || [] : []
|
||||
})
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'updateMcpEnabled',
|
||||
() => {
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
mcpServers: enabled ? assistant.mcpServers || [] : []
|
||||
})
|
||||
},
|
||||
200
|
||||
)
|
||||
},
|
||||
[assistant, updateAssistant]
|
||||
[assistant, setTimeoutTimer, updateAssistant]
|
||||
)
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { QuickPanelListItem, QuickPanelOpenOptions } from '@renderer/components/QuickPanel/types'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import QuickPhraseService from '@renderer/services/QuickPhraseService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { QuickPhrase } from '@renderer/types'
|
||||
@ -34,6 +35,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
|
||||
state.assistants.assistants.find((a) => a.id === assistantObj.id)?.id || state.assistants.defaultAssistant.id
|
||||
)
|
||||
const { assistant, updateAssistant } = useAssistant(activeAssistantId)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const loadQuickListPhrases = useCallback(
|
||||
async (regularPhrases: QuickPhrase[] = []) => {
|
||||
@ -54,24 +56,32 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
|
||||
|
||||
const handlePhraseSelect = useCallback(
|
||||
(phrase: QuickPhrase) => {
|
||||
setTimeout(() => {
|
||||
setInputValue((prev) => {
|
||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||
const cursorPosition = textArea.selectionStart
|
||||
const selectionStart = cursorPosition
|
||||
const selectionEndPosition = cursorPosition + phrase.content.length
|
||||
const newText = prev.slice(0, cursorPosition) + phrase.content + prev.slice(cursorPosition)
|
||||
setTimeoutTimer(
|
||||
'handlePhraseSelect_1',
|
||||
() => {
|
||||
setInputValue((prev) => {
|
||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||
const cursorPosition = textArea.selectionStart
|
||||
const selectionStart = cursorPosition
|
||||
const selectionEndPosition = cursorPosition + phrase.content.length
|
||||
const newText = prev.slice(0, cursorPosition) + phrase.content + prev.slice(cursorPosition)
|
||||
|
||||
setTimeout(() => {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(selectionStart, selectionEndPosition)
|
||||
resizeTextArea()
|
||||
}, 10)
|
||||
return newText
|
||||
})
|
||||
}, 10)
|
||||
setTimeoutTimer(
|
||||
'handlePhraseSelect_2',
|
||||
() => {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(selectionStart, selectionEndPosition)
|
||||
resizeTextArea()
|
||||
},
|
||||
10
|
||||
)
|
||||
return newText
|
||||
})
|
||||
},
|
||||
10
|
||||
)
|
||||
},
|
||||
[setInputValue, resizeTextArea]
|
||||
[setTimeoutTimer, setInputValue, resizeTextArea]
|
||||
)
|
||||
|
||||
const handleModalOk = async () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Link } from 'lucide-react'
|
||||
@ -18,14 +19,19 @@ interface Props {
|
||||
const UrlContextButton: FC<Props> = ({ assistant, ToolbarButton }) => {
|
||||
const { t } = useTranslation()
|
||||
const { updateAssistant } = useAssistant(assistant.id)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const urlContentNewState = !assistant.enableUrlContext
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
updateAssistant({ ...assistant, enableUrlContext: urlContentNewState })
|
||||
}, 100)
|
||||
}, [assistant, urlContentNewState, updateAssistant])
|
||||
setTimeoutTimer(
|
||||
'handleToggle',
|
||||
() => {
|
||||
updateAssistant({ ...assistant, enableUrlContext: urlContentNewState })
|
||||
},
|
||||
100
|
||||
)
|
||||
}, [setTimeoutTimer, updateAssistant, assistant, urlContentNewState])
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getHttpMessageLabel } from '@renderer/i18n/label'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
|
||||
@ -19,11 +20,12 @@ const ErrorBlock: React.FC<Props> = ({ block, message }) => {
|
||||
const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }> = ({ block, message }) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
|
||||
|
||||
const onRemoveBlock = () => {
|
||||
setTimeout(() => dispatch(removeBlocksThunk(message.topicId, message.id, [block.id])), 350)
|
||||
setTimeoutTimer('onRemoveBlock', () => dispatch(removeBlocksThunk(message.topicId, message.id, [block.id])), 350)
|
||||
}
|
||||
|
||||
if (block.error && HTTP_ERROR_CODES.includes(block.error?.status)) {
|
||||
|
||||
@ -2,6 +2,7 @@ import { CheckOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import ThinkingEffect from '@renderer/components/ThinkingEffect'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
|
||||
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@ -16,7 +17,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||
const { t } = useTranslation()
|
||||
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
|
||||
const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought')
|
||||
@ -38,14 +39,13 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
||||
.then(() => {
|
||||
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Failed to copy text:', error)
|
||||
antdMessage.error({ content: t('message.copy.failed'), key: 'copy-message-error' })
|
||||
})
|
||||
}
|
||||
}, [block.content, t])
|
||||
}, [block.content, setCopied, t])
|
||||
|
||||
if (!block.content) {
|
||||
return null
|
||||
|
||||
@ -7,6 +7,7 @@ import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
@ -50,6 +51,8 @@ const TooltipFooter = styled.div`
|
||||
// 自定义节点组件
|
||||
const CustomNode: FC<{ data: any }> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const nodeType = data.type
|
||||
let borderColor = 'var(--color-border)'
|
||||
let title = ''
|
||||
@ -114,9 +117,13 @@ const CustomNode: FC<{ data: any }> = ({ data }) => {
|
||||
// 让监听器处理标签切换
|
||||
document.dispatchEvent(customEvent)
|
||||
|
||||
setTimeout(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + data.messageId)
|
||||
}, 250)
|
||||
setTimeoutTimer(
|
||||
'handleNodeClick',
|
||||
() => {
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + data.messageId)
|
||||
},
|
||||
250
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isNearButtons, setIsNearButtons] = useState(false)
|
||||
const [hideTimer, setHideTimer] = useState<NodeJS.Timeout | null>(null)
|
||||
const hideTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
const [showChatHistory, setShowChatHistory] = useState(false)
|
||||
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
|
||||
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
|
||||
@ -49,20 +49,16 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
|
||||
// Reset hide timer and make buttons visible
|
||||
const resetHideTimer = useCallback(() => {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
|
||||
setIsVisible(true)
|
||||
|
||||
// Only set a hide timer if cursor is not near the buttons
|
||||
if (!isNearButtons) {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(hideTimerRef.current)
|
||||
hideTimerRef.current = setTimeout(() => {
|
||||
setIsVisible(false)
|
||||
}, 1500)
|
||||
setHideTimer(timer)
|
||||
}
|
||||
}, [hideTimer, isNearButtons])
|
||||
}, [isNearButtons])
|
||||
|
||||
// Handle mouse entering button area
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
@ -74,21 +70,21 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
setIsVisible(true)
|
||||
|
||||
// Clear any existing hide timer
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
setHideTimer(null)
|
||||
}
|
||||
}, [hideTimer, manuallyClosedUntil])
|
||||
clearTimeout(hideTimerRef.current)
|
||||
}, [manuallyClosedUntil])
|
||||
|
||||
// Handle mouse leaving button area
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsNearButtons(false)
|
||||
|
||||
// Set a timer to hide the buttons
|
||||
const timer = setTimeout(() => {
|
||||
hideTimerRef.current = setTimeout(() => {
|
||||
setIsVisible(false)
|
||||
}, 500)
|
||||
setHideTimer(timer)
|
||||
|
||||
return () => {
|
||||
clearTimeout(hideTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleChatHistoryClick = () => {
|
||||
@ -322,13 +318,10 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
} else {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
clearTimeout(hideTimerRef.current)
|
||||
}
|
||||
}, [
|
||||
containerId,
|
||||
hideTimer,
|
||||
resetHideTimer,
|
||||
isNearButtons,
|
||||
handleMouseEnter,
|
||||
|
||||
@ -6,6 +6,7 @@ import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@ -71,6 +72,7 @@ const MessageItem: FC<Props> = ({
|
||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const isEditing = editingMessageId === message.id
|
||||
|
||||
useEffect(() => {
|
||||
@ -119,18 +121,25 @@ const MessageItem: FC<Props> = ({
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
|
||||
|
||||
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
|
||||
if (messageContainerRef.current) {
|
||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
if (highlight) {
|
||||
setTimeout(() => {
|
||||
const classList = messageContainerRef.current?.classList
|
||||
classList?.add('message-highlight')
|
||||
setTimeout(() => classList?.remove('message-highlight'), 2500)
|
||||
}, 500)
|
||||
const messageHighlightHandler = useCallback(
|
||||
(highlight: boolean = true) => {
|
||||
if (messageContainerRef.current) {
|
||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
if (highlight) {
|
||||
setTimeoutTimer(
|
||||
'messageHighlightHandler_1',
|
||||
() => {
|
||||
const classList = messageContainerRef.current?.classList
|
||||
classList?.add('message-highlight')
|
||||
setTimeoutTimer('messageHighlightHandler_2', () => classList?.remove('message-highlight'), 2500)
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
},
|
||||
[setTimeoutTimer]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
|
||||
|
||||
@ -4,6 +4,7 @@ import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelName } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
@ -32,13 +33,14 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { userName } = useSettings()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const messagesListRef = useRef<HTMLDivElement>(null)
|
||||
const messageItemsRef = useRef<Map<string, HTMLDivElement>>(new Map())
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [mouseY, setMouseY] = useState<number | null>(null)
|
||||
|
||||
const [mouseY, setMouseY] = useState<number | null>(null)
|
||||
const [listOffsetY, setListOffsetY] = useState(0)
|
||||
const [containerHeight, setContainerHeight] = useState<number | null>(null)
|
||||
|
||||
@ -112,15 +114,19 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
|
||||
)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'auto', block: 'start' })
|
||||
}
|
||||
}, 100)
|
||||
setTimeoutTimer(
|
||||
'setSelectedMessage',
|
||||
() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'auto', block: 'start' })
|
||||
}
|
||||
},
|
||||
100
|
||||
)
|
||||
}
|
||||
},
|
||||
[dispatch, messages]
|
||||
[dispatch, messages, setTimeoutTimer]
|
||||
)
|
||||
|
||||
const scrollToMessage = useCallback(
|
||||
|
||||
@ -4,6 +4,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
@ -51,6 +52,7 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
const topicMessages = useAppSelector((state) => selectMessagesForTopic(state, topicId))
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const couldAddImageFile = useMemo(() => {
|
||||
const relatedAssistantMessages = topicMessages.filter((m) => m.askId === message.id && m.role === 'assistant')
|
||||
@ -247,9 +249,13 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
|
||||
handleTextChange(blockId, newText)
|
||||
|
||||
// set cursor position in the next render cycle
|
||||
setTimeout(() => {
|
||||
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||
}, 0)
|
||||
setTimeoutTimer(
|
||||
'handleKeyDown',
|
||||
() => {
|
||||
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||
},
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import type { Topic } from '@renderer/types'
|
||||
@ -32,6 +33,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const { isMultiSelectMode } = useChatContext(topic)
|
||||
const maxWidth = useChatMaxWidth()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant')
|
||||
|
||||
@ -68,14 +70,18 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
// 当前选中的消息
|
||||
editMessage(message.id, { foldSelected: true })
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'setSelectedMessage',
|
||||
() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
},
|
||||
200
|
||||
)
|
||||
},
|
||||
[editMessage, selectedMessageId]
|
||||
[editMessage, selectedMessageId, setTimeoutTimer]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -9,6 +9,7 @@ import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { useEnableDeveloperMode, useMessageStyle } from '@renderer/hooks/useSettings'
|
||||
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
@ -78,7 +79,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const { toggleMultiSelectMode } = useChatContext(props.topic)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
|
||||
@ -136,9 +137,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
},
|
||||
[message, t] // message is needed for message.id and as a fallback. t is for translation.
|
||||
[message, setCopied, t] // message is needed for message.id and as a fallback. t is for translation.
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
|
||||
@ -3,6 +3,7 @@ import { CopyIcon, LoadingIcon } from '@renderer/components/Icons'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
|
||||
import { cancelToolAction, confirmToolAction } from '@renderer/utils/userConfirmation'
|
||||
@ -52,6 +53,7 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
const { mcpServers, updateMCPServer } = useMCPServers()
|
||||
const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null)
|
||||
const [progress, setProgress] = useState<number>(0)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const toolResponse = block.metadata?.rawMcpToolResponse
|
||||
|
||||
@ -130,7 +132,7 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopiedMap((prev) => ({ ...prev, [toolId]: true }))
|
||||
setTimeout(() => setCopiedMap((prev) => ({ ...prev, [toolId]: false })), 2000)
|
||||
setTimeoutTimer('copyContent', () => setCopiedMap((prev) => ({ ...prev, [toolId]: false })), 2000)
|
||||
}
|
||||
|
||||
const handleCollapseChange = (keys: string | string[]) => {
|
||||
|
||||
@ -9,6 +9,7 @@ import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessa
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
|
||||
import SelectionBox from '@renderer/pages/home/Messages/SelectionBox'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
@ -55,22 +56,24 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
const { containerRef: scrollContainerRef, handleScroll: handleScrollPosition } = useScrollPosition(
|
||||
`topic-${topic.id}`
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
const { showPrompt, messageNavigation } = useSettings()
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const dispatch = useAppDispatch()
|
||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const [isProcessingContext, setIsProcessingContext] = useState(false)
|
||||
|
||||
const messageElements = useRef<Map<string, HTMLElement>>(new Map())
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const { showPrompt, messageNavigation } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const messages = useTopicMessages(topic.id)
|
||||
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
|
||||
const messagesRef = useRef<Message[]>(messages)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
||||
|
||||
const messageElements = useRef<Map<string, HTMLElement>>(new Map())
|
||||
const messagesRef = useRef<Message[]>(messages)
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
}, [messages])
|
||||
@ -256,15 +259,19 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
if (!hasMore || isLoadingMore) return
|
||||
|
||||
setIsLoadingMore(true)
|
||||
setTimeout(() => {
|
||||
const currentLength = displayMessages.length
|
||||
const newMessages = computeDisplayMessages(messages, currentLength, LOAD_MORE_COUNT)
|
||||
setTimeoutTimer(
|
||||
'loadMoreMessages',
|
||||
() => {
|
||||
const currentLength = displayMessages.length
|
||||
const newMessages = computeDisplayMessages(messages, currentLength, LOAD_MORE_COUNT)
|
||||
|
||||
setDisplayMessages((prev) => [...prev, ...newMessages])
|
||||
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 300)
|
||||
}, [displayMessages.length, hasMore, isLoadingMore, messages])
|
||||
setDisplayMessages((prev) => [...prev, ...newMessages])
|
||||
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
||||
setIsLoadingMore(false)
|
||||
},
|
||||
300
|
||||
)
|
||||
}, [displayMessages.length, hasMore, isLoadingMore, messages, setTimeoutTimer])
|
||||
|
||||
useShortcut('copy_last_message', () => {
|
||||
const lastMessage = last(messages)
|
||||
|
||||
@ -26,6 +26,7 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
const DRAG_THRESHOLD = 5
|
||||
|
||||
useEffect(() => {
|
||||
let handleMouseMoveTimer: NodeJS.Timeout
|
||||
if (!isMultiSelectMode) return
|
||||
|
||||
const updateDragPos = (e: MouseEvent) => {
|
||||
@ -106,7 +107,7 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
dragSelectedIds.current.add(id)
|
||||
newSelectedIds.add(id)
|
||||
el.classList.add('selection-highlight')
|
||||
setTimeout(() => el.classList.remove('selection-highlight'), 300)
|
||||
handleMouseMoveTimer = setTimeout(() => el.classList.remove('selection-highlight'), 300)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -129,6 +130,7 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.classList.remove('no-select')
|
||||
clearTimeout(handleMouseMoveTimer)
|
||||
}
|
||||
}, [isMultiSelectMode, isDragging, isMouseDown, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Drawer } from 'antd'
|
||||
import { useState } from 'react'
|
||||
@ -25,10 +26,11 @@ const PopupContainer: React.FC<Props> = ({
|
||||
resolve
|
||||
}) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
setTimeout(resolve, 300)
|
||||
setTimeoutTimer('onClose', resolve, 300)
|
||||
}
|
||||
|
||||
AssistantsDrawer.hide = onClose
|
||||
|
||||
@ -7,7 +7,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -18,17 +18,15 @@ interface Props {
|
||||
const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
const { model, updateAssistant } = useAssistant(assistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLocalAi) {
|
||||
return null
|
||||
}
|
||||
const timerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
const onSelectModel = async (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.currentTarget.blur()
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
if (selectedModel) {
|
||||
// 避免更新数据造成关闭弹框的卡顿
|
||||
setTimeout(() => {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(() => {
|
||||
const enabledWebSearch = isWebSearchModel(selectedModel)
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
@ -39,6 +37,16 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLocalAi) {
|
||||
return null
|
||||
}
|
||||
|
||||
const providerName = getProviderName(model?.provider)
|
||||
|
||||
return (
|
||||
|
||||
@ -6,6 +6,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, MAX_CONTEXT_COUNT } from '@renderer/config/constant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
@ -42,6 +43,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
customParametersRef.current = customParameters
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
@ -191,13 +193,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
// TODO: 需要根据配置来设置默认值
|
||||
if (selectedModel.name.includes('kimi-k2')) {
|
||||
setTemperature(0.6)
|
||||
setTimeout(() => updateAssistantSettings({ temperature: 0.6 }), 500)
|
||||
setTimeoutTimer('onSelectModel_1', () => updateAssistantSettings({ temperature: 0.6 }), 500)
|
||||
} else if (selectedModel.name.includes('moonshot')) {
|
||||
setTemperature(0.3)
|
||||
setTimeout(() => updateAssistantSettings({ temperature: 0.3 }), 500)
|
||||
setTimeoutTimer('onSelectModel_2', () => updateAssistantSettings({ temperature: 0.3 }), 500)
|
||||
}
|
||||
}
|
||||
}, [assistant, defaultModel, updateAssistant, updateAssistantSettings])
|
||||
}, [assistant, defaultModel, setTimeoutTimer, updateAssistant, updateAssistantSettings])
|
||||
|
||||
useEffect(() => {
|
||||
return () => updateAssistantSettings({ customParameters: customParametersRef.current })
|
||||
@ -275,7 +277,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
onChange={(value) => {
|
||||
if (!isNull(value)) {
|
||||
setTemperature(value)
|
||||
setTimeout(() => updateAssistantSettings({ temperature: value }), 500)
|
||||
setTimeoutTimer('temperature_onChange', () => updateAssistantSettings({ temperature: value }), 500)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
@ -323,7 +325,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
onChange={(value) => {
|
||||
if (!isNull(value)) {
|
||||
setTopP(value)
|
||||
setTimeout(() => updateAssistantSettings({ topP: value }), 500)
|
||||
setTimeoutTimer('topP_onChange', () => updateAssistantSettings({ topP: value }), 500)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
@ -352,7 +354,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
onChange={(value) => {
|
||||
if (!isNull(value)) {
|
||||
setContextCount(value)
|
||||
setTimeout(() => updateAssistantSettings({ contextCount: value }), 500)
|
||||
setTimeoutTimer('contextCount_onChange', () => updateAssistantSettings({ contextCount: value }), 500)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
@ -413,7 +415,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
onChange={(value) => {
|
||||
if (!isNull(value)) {
|
||||
setMaxTokens(value)
|
||||
setTimeout(() => updateAssistantSettings({ maxTokens: value }), 1000)
|
||||
setTimeoutTimer('maxTokens_onChange', () => updateAssistantSettings({ maxTokens: value }), 1000)
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@ -13,6 +13,7 @@ import BackupPopup from '@renderer/components/Popups/BackupPopup'
|
||||
import RestorePopup from '@renderer/components/Popups/RestorePopup'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { reset } from '@renderer/services/BackupService'
|
||||
import store, { useAppDispatch } from '@renderer/store'
|
||||
import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/settings'
|
||||
@ -54,6 +55,7 @@ const DataSettings: FC = () => {
|
||||
const { size, removeAllFiles } = useKnowledgeFiles()
|
||||
const { theme } = useTheme()
|
||||
const [menu, setMenu] = useState<string>('data')
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const _skipBackupFile = store.getState().settings.skipBackupFile
|
||||
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(_skipBackupFile)
|
||||
@ -247,11 +249,15 @@ const DataSettings: FC = () => {
|
||||
content: t('settings.data.app_data.restart_notice'),
|
||||
duration: 2
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.api.relaunchApp({
|
||||
args: ['--new-data-path=' + newPath]
|
||||
})
|
||||
}, 500)
|
||||
setTimeoutTimer(
|
||||
'doubleConfirmModalBeforeCopyData',
|
||||
() => {
|
||||
window.api.relaunchApp({
|
||||
args: ['--new-data-path=' + newPath]
|
||||
})
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -333,11 +339,15 @@ const DataSettings: FC = () => {
|
||||
content: t('settings.data.app_data.restart_notice'),
|
||||
duration: 3
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.api.relaunchApp({
|
||||
args: ['--new-data-path=' + newPath]
|
||||
})
|
||||
}, 500)
|
||||
setTimeoutTimer(
|
||||
'showMigrationConfirmModal_1',
|
||||
() => {
|
||||
window.api.relaunchApp({
|
||||
args: ['--new-data-path=' + newPath]
|
||||
})
|
||||
},
|
||||
500
|
||||
)
|
||||
return
|
||||
}
|
||||
// 如果不复制数据,直接设置新的应用数据路径
|
||||
@ -348,11 +358,15 @@ const DataSettings: FC = () => {
|
||||
setAppInfo(await window.api.getAppInfo())
|
||||
|
||||
// 通知用户并重启应用
|
||||
setTimeout(() => {
|
||||
window.message.success(t('settings.data.app_data.select_success'))
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.api.relaunchApp()
|
||||
}, 500)
|
||||
setTimeoutTimer(
|
||||
'showMigrationConfirmModal_2',
|
||||
() => {
|
||||
window.message.success(t('settings.data.app_data.select_success'))
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.api.relaunchApp()
|
||||
},
|
||||
500
|
||||
)
|
||||
} catch (error) {
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.message.error({
|
||||
@ -456,7 +470,7 @@ const DataSettings: FC = () => {
|
||||
await window.api.flushAppData()
|
||||
|
||||
// wait 2 seconds to flush app data
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
await new Promise((resolve) => setTimeoutTimer('startMigration_1', resolve, 2000))
|
||||
|
||||
// 开始复制过程
|
||||
const copyResult = await window.api.copy(
|
||||
@ -476,15 +490,19 @@ const DataSettings: FC = () => {
|
||||
if (!copyResult.success) {
|
||||
// 延迟关闭加载模态框
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
loadingModal.destroy()
|
||||
window.message.error({
|
||||
content: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error,
|
||||
key: messageKey,
|
||||
duration: 5
|
||||
})
|
||||
resolve()
|
||||
}, 500)
|
||||
setTimeoutTimer(
|
||||
'startMigration_2',
|
||||
() => {
|
||||
loadingModal.destroy()
|
||||
window.message.error({
|
||||
content: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error,
|
||||
key: messageKey,
|
||||
duration: 5
|
||||
})
|
||||
resolve()
|
||||
},
|
||||
500
|
||||
)
|
||||
})
|
||||
|
||||
throw new Error(copyResult.error || 'Unknown error during copy')
|
||||
@ -494,7 +512,7 @@ const DataSettings: FC = () => {
|
||||
await window.api.setAppDataPath(newPath)
|
||||
|
||||
// 短暂延迟以显示100%完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
await new Promise((resolve) => setTimeoutTimer('startMigration_3', resolve, 500))
|
||||
|
||||
// 关闭加载模态框
|
||||
loadingModal.destroy()
|
||||
@ -529,13 +547,17 @@ const DataSettings: FC = () => {
|
||||
setAppInfo(await window.api.getAppInfo())
|
||||
|
||||
// 通知用户并重启应用
|
||||
setTimeout(() => {
|
||||
window.message.success(t('settings.data.app_data.select_success'))
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.api.relaunchApp({
|
||||
args: ['--user-data-dir=' + newDataPath]
|
||||
})
|
||||
}, 1000)
|
||||
setTimeoutTimer(
|
||||
'handleDataMigration',
|
||||
() => {
|
||||
window.message.success(t('settings.data.app_data.select_success'))
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.api.relaunchApp({
|
||||
args: ['--user-data-dir=' + newDataPath]
|
||||
})
|
||||
},
|
||||
1000
|
||||
)
|
||||
} catch (error) {
|
||||
window.api.setStopQuitApp(false, '')
|
||||
window.message.error({
|
||||
@ -552,7 +574,7 @@ const DataSettings: FC = () => {
|
||||
}
|
||||
|
||||
handleDataMigration()
|
||||
}, [t])
|
||||
}, [setTimeoutTimer, t])
|
||||
|
||||
const onSkipBackupFilesChange = (value: boolean) => {
|
||||
setSkipBackupFile(value)
|
||||
|
||||
@ -6,6 +6,7 @@ import { WebdavBackupManager } from '@renderer/components/WebdavBackupManager'
|
||||
import { useWebdavBackupModal, WebdavBackupModal } from '@renderer/components/WebdavModals'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useNutstoreSSO } from '@renderer/hooks/useNutstoreSSO'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import {
|
||||
backupToNutstore,
|
||||
checkConnection,
|
||||
@ -51,17 +52,14 @@ const NutstoreSettings: FC = () => {
|
||||
const [nutstoreUsername, setNutstoreUsername] = useState<string | undefined>(undefined)
|
||||
const [nutstorePass, setNutstorePass] = useState<string | undefined>(undefined)
|
||||
const [storagePath, setStoragePath] = useState<string | undefined>(nutstorePath)
|
||||
|
||||
const [checkConnectionLoading, setCheckConnectionLoading] = useState(false)
|
||||
const [nsConnected, setNsConnected] = useState<boolean>(false)
|
||||
|
||||
const [syncInterval, setSyncInterval] = useState<number>(nutstoreSyncInterval)
|
||||
|
||||
const [nutSkipBackupFile, setNutSkipBackupFile] = useState<boolean>(nutstoreSkipBackupFile)
|
||||
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
|
||||
|
||||
const nutstoreSSOHandler = useNutstoreSSO()
|
||||
|
||||
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const handleClickNutstoreSSO = useCallback(async () => {
|
||||
const ssoUrl = await window.api.nutstore.getSSOUrl()
|
||||
@ -120,7 +118,7 @@ const NutstoreSettings: FC = () => {
|
||||
setNsConnected(isConnectedToNutstore)
|
||||
setCheckConnectionLoading(false)
|
||||
|
||||
setTimeout(() => setNsConnected(false), 3000)
|
||||
setTimeoutTimer('handleCheckConnection', () => setNsConnected(false), 3000)
|
||||
}
|
||||
|
||||
const { isModalVisible, handleBackup, handleCancel, backuping, customFileName, setCustomFileName, showBackupModal } =
|
||||
|
||||
@ -4,6 +4,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useEnableDeveloperMode, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
@ -48,6 +49,7 @@ const GeneralSettings: FC = () => {
|
||||
const [proxyBypassRules, setProxyBypassRules] = useState<string | undefined>(storeProxyBypassRules)
|
||||
const { theme } = useTheme()
|
||||
const { enableDeveloperMode, setEnableDeveloperMode } = useEnableDeveloperMode()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const updateTray = (isShowTray: boolean) => {
|
||||
setTray(isShowTray)
|
||||
@ -171,9 +173,13 @@ const GeneralSettings: FC = () => {
|
||||
}
|
||||
|
||||
// 重启应用
|
||||
setTimeout(() => {
|
||||
window.api.relaunchApp()
|
||||
}, 500)
|
||||
setTimeoutTimer(
|
||||
'handleHardwareAccelerationChange',
|
||||
() => {
|
||||
window.api.relaunchApp()
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { UploadOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMCPServerActive } from '@renderer/store/mcp'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
@ -72,6 +73,7 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({
|
||||
const [importMethod, setImportMethod] = useState<'json' | 'dxt'>(initialImportMethod)
|
||||
const [dxtFile, setDxtFile] = useState<File | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
// Update import method when initialImportMethod changes
|
||||
useEffect(() => {
|
||||
@ -159,21 +161,25 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({
|
||||
onClose()
|
||||
|
||||
// Check server connectivity in background (with timeout)
|
||||
setTimeout(() => {
|
||||
window.api.mcp
|
||||
.checkMcpConnectivity(newServer)
|
||||
.then((isConnected) => {
|
||||
logger.debug(`Connectivity check for ${newServer.name}: ${isConnected}`)
|
||||
dispatch(setMCPServerActive({ id: newServer.id, isActive: isConnected }))
|
||||
})
|
||||
.catch((connError: any) => {
|
||||
logger.error(`Connectivity check failed for ${newServer.name}:`, connError)
|
||||
// Don't show error for DXT servers as they might need additional setup
|
||||
logger.warn(
|
||||
`DXT server ${newServer.name} connectivity check failed, this is normal for servers requiring additional configuration`
|
||||
)
|
||||
})
|
||||
}, 1000) // Delay to ensure server is properly added to store
|
||||
setTimeoutTimer(
|
||||
'handleOk',
|
||||
() => {
|
||||
window.api.mcp
|
||||
.checkMcpConnectivity(newServer)
|
||||
.then((isConnected) => {
|
||||
logger.debug(`Connectivity check for ${newServer.name}: ${isConnected}`)
|
||||
dispatch(setMCPServerActive({ id: newServer.id, isActive: isConnected }))
|
||||
})
|
||||
.catch((connError: any) => {
|
||||
logger.error(`Connectivity check failed for ${newServer.name}:`, connError)
|
||||
// Don't show error for DXT servers as they might need additional setup
|
||||
logger.warn(
|
||||
`DXT server ${newServer.name} connectivity check failed, this is normal for servers requiring additional configuration`
|
||||
)
|
||||
})
|
||||
},
|
||||
1000
|
||||
) // Delay to ensure server is properly added to store
|
||||
} catch (error) {
|
||||
logger.error('DXT processing error:', error as Error)
|
||||
window.message.error({
|
||||
|
||||
@ -3,7 +3,7 @@ import { Center, VStack } from '@renderer/components/Layout'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
|
||||
import { Alert, Button } from 'antd'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
@ -26,6 +26,15 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const checkBinariesTimerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(checkBinariesTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const checkBinaries = useCallback(async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
@ -48,7 +57,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
|
||||
setIsInstallingUv(false)
|
||||
}
|
||||
setTimeout(checkBinaries, 1000)
|
||||
clearTimeout(checkBinariesTimerRef.current)
|
||||
checkBinariesTimerRef.current = setTimeout(checkBinaries, 1000)
|
||||
}
|
||||
|
||||
const installBun = async () => {
|
||||
@ -64,7 +74,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
})
|
||||
setIsInstallingBun(false)
|
||||
}
|
||||
setTimeout(checkBinaries, 1000)
|
||||
clearTimeout(checkBinariesTimerRef.current)
|
||||
checkBinariesTimerRef.current = setTimeout(checkBinaries, 1000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -6,6 +6,7 @@ import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
||||
import { checkApi } from '@renderer/services/ApiService'
|
||||
@ -53,6 +54,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
|
||||
@ -170,9 +172,13 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
})
|
||||
|
||||
setApiKeyConnectivity((prev) => ({ ...prev, status: HealthStatus.SUCCESS }))
|
||||
setTimeout(() => {
|
||||
setApiKeyConnectivity((prev) => ({ ...prev, status: HealthStatus.NOT_CHECKED }))
|
||||
}, 3000)
|
||||
setTimeoutTimer(
|
||||
'onCheckApi',
|
||||
() => {
|
||||
setApiKeyConnectivity((prev) => ({ ...prev, status: HealthStatus.NOT_CHECKED }))
|
||||
},
|
||||
3000
|
||||
)
|
||||
} catch (error: any) {
|
||||
window.message.error({
|
||||
key: 'api-check',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { isRerankModel } from '@renderer/config/models'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
@ -19,6 +20,7 @@ interface Props extends ShowParams {
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider, resolve, reject }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
// Keep the natural order of models
|
||||
const models = useMemo(() => provider.models.filter((m) => !isRerankModel(m)), [provider])
|
||||
@ -42,7 +44,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve, reject }) => {
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
setTimeout(reject, 300)
|
||||
setTimeoutTimer('onCancel', reject, 300)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
|
||||
@ -3,6 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useShortcuts } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getShortcutLabel } from '@renderer/i18n/label'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { initialState, resetShortcuts, toggleShortcut, updateShortcut } from '@renderer/store/shortcuts'
|
||||
@ -22,6 +23,7 @@ const ShortcutSettings: FC = () => {
|
||||
const { shortcuts: originalShortcuts } = useShortcuts()
|
||||
const inputRefs = useRef<Record<string, InputRef>>({})
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
//if shortcut is not available on all the platforms, block the shortcut here
|
||||
let shortcuts = originalShortcuts
|
||||
@ -42,9 +44,13 @@ const ShortcutSettings: FC = () => {
|
||||
|
||||
const handleAddShortcut = (record: Shortcut) => {
|
||||
setEditingKey(record.key)
|
||||
setTimeout(() => {
|
||||
inputRefs.current[record.key]?.focus()
|
||||
}, 0)
|
||||
setTimeoutTimer(
|
||||
'handleAddShortcut',
|
||||
() => {
|
||||
inputRefs.current[record.key]?.focus()
|
||||
},
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
const isShortcutModified = (record: Shortcut) => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { CheckOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { useBlacklist } from '@renderer/hooks/useWebSearchProviders'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setExcludeDomains } from '@renderer/store/websearch'
|
||||
@ -47,6 +48,7 @@ const BlacklistSettings: FC = () => {
|
||||
name: source.name
|
||||
})) || []
|
||||
)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@ -148,7 +150,7 @@ const BlacklistSettings: FC = () => {
|
||||
content: t('settings.tool.websearch.subscribe_update_success'),
|
||||
duration: 2
|
||||
})
|
||||
setTimeout(() => setSubscribeValid(false), 3000)
|
||||
setTimeoutTimer('updateSubscribe', () => setSubscribeValid(false), 3000)
|
||||
} else {
|
||||
setSubscribeValid(false)
|
||||
throw new Error('No valid sources updated')
|
||||
@ -190,7 +192,7 @@ const BlacklistSettings: FC = () => {
|
||||
content: t('settings.tool.websearch.subscribe_add_success'),
|
||||
duration: 2
|
||||
})
|
||||
setTimeout(() => setSubscribeValid(false), 3000)
|
||||
setTimeoutTimer('handleAddSubscribe', () => setSubscribeValid(false), 3000)
|
||||
} catch (error) {
|
||||
setSubscribeValid(false)
|
||||
window.message.error({
|
||||
|
||||
@ -6,6 +6,7 @@ import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
|
||||
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
|
||||
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
|
||||
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { WebSearchProviderId } from '@renderer/types'
|
||||
@ -33,6 +34,7 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const [basicAuthUsername, setBasicAuthUsername] = useState(provider.basicAuthUsername || '')
|
||||
const [basicAuthPassword, setBasicAuthPassword] = useState(provider.basicAuthPassword || '')
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const webSearchProviderConfig = WEB_SEARCH_PROVIDER_CONFIG[provider.id]
|
||||
const apiKeyWebsite = webSearchProviderConfig?.websites?.apiKey
|
||||
@ -125,7 +127,7 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
})
|
||||
} finally {
|
||||
setApiChecking(false)
|
||||
setTimeout(() => setApiValid(false), 2500)
|
||||
setTimeoutTimer('checkSearch', () => setApiValid(false), 2500)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import { LanguagesEnum, UNKNOWN } from '@renderer/config/translate'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import db from '@renderer/databases'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService'
|
||||
@ -51,7 +52,7 @@ const TranslatePage: FC = () => {
|
||||
// states
|
||||
const [text, setText] = useState(_text)
|
||||
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false)
|
||||
const [isBidirectional, setIsBidirectional] = useState(false)
|
||||
@ -222,8 +223,7 @@ const TranslatePage: FC = () => {
|
||||
// 控制复制按钮
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(translatedContent)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
setCopied(false)
|
||||
}
|
||||
|
||||
// 控制历史记录点击
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import { InputRef } from 'rc-input/lib/interface'
|
||||
@ -25,8 +26,9 @@ const InputBar = ({
|
||||
handleChange
|
||||
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
if (!loading) {
|
||||
setTimeout(() => inputRef.current?.input?.focus(), 0)
|
||||
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
return (
|
||||
<InputWrapper ref={ref}>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { RefreshIcon } from '@renderer/components/Icons'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { CircleX, Copy, Pause } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -27,6 +28,7 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
const [isContainerHovered, setIsContainerHovered] = useState(false)
|
||||
const [isShowMe, setIsShowMe] = useState(true)
|
||||
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleWindowFocus)
|
||||
@ -83,9 +85,13 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
|
||||
const handleEsc = () => {
|
||||
setIsEscHovered(true)
|
||||
setTimeout(() => {
|
||||
setIsEscHovered(false)
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'handleEsc',
|
||||
() => {
|
||||
setIsEscHovered(false)
|
||||
},
|
||||
200
|
||||
)
|
||||
|
||||
if (loading && onPause) {
|
||||
onPause()
|
||||
@ -96,9 +102,13 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
|
||||
const handleRegenerate = () => {
|
||||
setIsRegenerateHovered(true)
|
||||
setTimeout(() => {
|
||||
setIsRegenerateHovered(false)
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'handleRegenerate_1',
|
||||
() => {
|
||||
setIsRegenerateHovered(false)
|
||||
},
|
||||
200
|
||||
)
|
||||
|
||||
if (loading && onPause) {
|
||||
onPause()
|
||||
@ -106,9 +116,13 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
|
||||
if (onRegenerate) {
|
||||
//wait for a little time
|
||||
setTimeout(() => {
|
||||
onRegenerate()
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'handleRegenerate_2',
|
||||
() => {
|
||||
onRegenerate()
|
||||
},
|
||||
200
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,9 +134,13 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
.then(() => {
|
||||
window.message.success(t('message.copy.success'))
|
||||
setIsCopyHovered(true)
|
||||
setTimeout(() => {
|
||||
setIsCopyHovered(false)
|
||||
}, 200)
|
||||
setTimeoutTimer(
|
||||
'handleCopy',
|
||||
() => {
|
||||
setIsCopyHovered(false)
|
||||
},
|
||||
200
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
window.message.error(t('message.copy.failed'))
|
||||
|
||||
@ -4,6 +4,7 @@ import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import i18n from '@renderer/i18n'
|
||||
import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
@ -103,7 +104,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
const [animateKey, setAnimateKey] = useState(0)
|
||||
const [copyIconStatus, setCopyIconStatus] = useState<'normal' | 'success' | 'fail'>('normal')
|
||||
const [copyIconAnimation, setCopyIconAnimation] = useState<'none' | 'enter' | 'exit'>('none')
|
||||
const copyIconTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
const { setTimeoutTimer, clearTimeoutTimer } = useTimer()
|
||||
|
||||
const realActionItems = useMemo(() => {
|
||||
return actionItems?.filter((item) => item.enabled)
|
||||
@ -113,18 +114,31 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
// [macOS] only macOS has the fullscreen mode
|
||||
const isFullScreen = useRef(false)
|
||||
|
||||
const onHideCleanUp = useCallback(() => {
|
||||
setCopyIconStatus('normal')
|
||||
setCopyIconAnimation('none')
|
||||
clearTimeoutTimer('textSelection')
|
||||
clearTimeoutTimer('copyIcon')
|
||||
}, [clearTimeoutTimer])
|
||||
|
||||
// listen to selectionService events
|
||||
useEffect(() => {
|
||||
const cleanups: (() => void)[] = []
|
||||
// TextSelection
|
||||
const textSelectionListenRemover = window.electron?.ipcRenderer.on(
|
||||
IpcChannel.Selection_TextSelected,
|
||||
(_, selectionData: TextSelectionData) => {
|
||||
selectedText.current = selectionData.text
|
||||
isFullScreen.current = selectionData.isFullscreen ?? false
|
||||
setTimeout(() => {
|
||||
//make sure the animation is active
|
||||
setAnimateKey((prev) => prev + 1)
|
||||
}, 400)
|
||||
const cleanup = setTimeoutTimer(
|
||||
'textSelection',
|
||||
() => {
|
||||
//make sure the animation is active
|
||||
setAnimateKey((prev) => prev + 1)
|
||||
},
|
||||
400
|
||||
)
|
||||
cleanups.push(cleanup)
|
||||
}
|
||||
)
|
||||
|
||||
@ -142,8 +156,9 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
return () => {
|
||||
textSelectionListenRemover()
|
||||
toolbarVisibilityChangeListenRemover()
|
||||
cleanups.forEach((cleanup) => cleanup())
|
||||
}
|
||||
}, [demo])
|
||||
}, [demo, onHideCleanUp, setTimeoutTimer])
|
||||
|
||||
//make sure the toolbar size is updated when the compact mode/actionItems is changed
|
||||
useEffect(() => {
|
||||
@ -172,11 +187,22 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
}
|
||||
}, [customCss, demo])
|
||||
|
||||
const onHideCleanUp = () => {
|
||||
setCopyIconStatus('normal')
|
||||
setCopyIconAnimation('none')
|
||||
clearTimeout(copyIconTimeoutRef.current)
|
||||
}
|
||||
// copy selected text to clipboard
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (selectedText.current) {
|
||||
const result = await window.api?.selection.writeToClipboard(selectedText.current)
|
||||
|
||||
setCopyIconStatus(result ? 'success' : 'fail')
|
||||
setCopyIconAnimation('enter')
|
||||
setTimeoutTimer(
|
||||
'copyIcon',
|
||||
() => {
|
||||
setCopyIconAnimation('exit')
|
||||
},
|
||||
2000
|
||||
)
|
||||
}
|
||||
}, [setTimeoutTimer])
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: ActionItem) => {
|
||||
@ -200,22 +226,9 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
break
|
||||
}
|
||||
},
|
||||
[demo]
|
||||
[demo, handleCopy]
|
||||
)
|
||||
|
||||
// copy selected text to clipboard
|
||||
const handleCopy = async () => {
|
||||
if (selectedText.current) {
|
||||
const result = await window.api?.selection.writeToClipboard(selectedText.current)
|
||||
|
||||
setCopyIconStatus(result ? 'success' : 'fail')
|
||||
setCopyIconAnimation('enter')
|
||||
copyIconTimeoutRef.current = setTimeout(() => {
|
||||
setCopyIconAnimation('exit')
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (action: ActionItem) => {
|
||||
if (!action.searchEngine) return
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user