Compare commits

...

22 Commits

Author SHA1 Message Date
icarus
ca44133e90 Merge branch 'main' of github.com:CherryHQ/cherry-studio into fix/react-hooks 2025-10-19 03:54:39 +08:00
icarus
51dcdf94fb refactor(TraceTree): replace useEffect with useMemo for usedTime calculation
Use useMemo to optimize performance by avoiding unnecessary recalculations and state updates

fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-17 00:05:07 +08:00
icarus
254051cf62 fix(InputBar): move focus logic into useEffect to prevent side effects
fix: Error: Cannot access refs during render
2025-10-16 23:59:45 +08:00
icarus
24d2e6e6ce refactor(translate): simplify translation component by removing unused state
Remove unused assistant state and refactor topic initialization
Clean up unused imports and simplify logic for translation flow

fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-16 23:57:09 +08:00
icarus
cf9bfce43c refactor(action): simplify assistant and topic initialization
Move assistant and topic initialization to useState hooks and sync with refs
Remove redundant initialization code from useEffect

fix: Error: Cannot access refs during render

fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-16 23:18:32 +08:00
icarus
76bf78b810 fix(Footer): move hotkey handler after function definition
fix: Error: Cannot access variable before it is declared
2025-10-14 18:16:50 +08:00
icarus
f4441e2a55 fix(MinAppPage): use startTransition to avoid synchronously setState in useEffect
fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-14 18:15:43 +08:00
icarus
84f590ec7b fix(HeaderNavbar): use startTransition for title update in useEffect to avoid synchronously setState
fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-14 18:14:15 +08:00
icarus
a5865cfd01 fix(HeaderNavbar): replace useState with useMemo for breadcrumbItems
fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-14 18:11:18 +08:00
icarus
4e7c714ea2 fix(MinAppPage): replace useRef with useState for initial navbar state
fix: Error: Cannot access refs during render
2025-10-14 18:04:33 +08:00
icarus
d2c4231458 fix(minapps): replace webview ref with state
fix: Error: Cannot access refs during render
2025-10-14 17:53:50 +08:00
icarus
b5004e2a51 fix(AgentSettingsPopup): inline ModalContent component for better readability
fix: Error: Cannot create components during render
2025-10-14 17:41:42 +08:00
icarus
e0c334b5ed fix(translate): use state instead of ref as hook parameter
fix: Error: Cannot access refs during render
2025-10-14 17:34:15 +08:00
icarus
d482e661fb fix(trace): simplify node selection state management
Replace direct node state with node ID tracking and memoized selection
Remove redundant useEffect for node updates

fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-14 17:31:43 +08:00
icarus
3ac1caca69 refactor(trace): simplify showList state management with useMemo
Replace manual state updates for showList with derived state using useMemo
2025-10-14 17:16:13 +08:00
icarus
94c112c066 fix(trace): extract trace utility functions to separate module
Move mergeTraceModals, updatePercentAndStart and findNodeById functions from trace page component to a dedicated utils module to fix react-hooks error
2025-10-14 17:00:20 +08:00
icarus
2e694a87f8 fix(SessionSettingsPopup): inline ModalContent component for better readability
fix: Error: Cannot create components during render
2025-10-14 16:25:49 +08:00
icarus
4ae30db53a fix(AssistantSettings): replace useEffect with useMemo for prompts list
fix: Error: Calling setState synchronously within an effect can trigger cascading renders
2025-10-14 16:19:45 +08:00
icarus
f4a6dd91cf fix(ocr): move ProviderSettings component outside main component
Extract ProviderSettings component to fix react-hooks error
2025-10-14 16:16:46 +08:00
icarus
c08a570c27 fix(WindowFooter): avoid earlier access 2025-10-14 16:11:56 +08:00
icarus
9c318c9526 fix(SelectionToolbar): avoid earlier access 2025-10-14 16:10:07 +08:00
icarus
4cee09870a build(deps): upgrade eslint-plugin-react-hooks to v7.0.0
Update react-hooks eslint plugin to latest version and adjust config to use flat recommended rules. This brings improved linting rules and compatibility with newer React versions.
2025-10-14 16:08:31 +08:00
20 changed files with 422 additions and 326 deletions

View File

@@ -12,7 +12,7 @@ export default defineConfig([
eslint.configs.recommended,
tseslint.configs.recommended,
eslintReact.configs['recommended-typescript'],
reactHooks.configs['recommended-latest'],
reactHooks.configs.flat.recommended,
{
plugins: {
'simple-import-sort': simpleImportSort,

View File

@@ -261,7 +261,7 @@
"eslint": "^9.22.0",
"eslint-plugin-import-zod": "^1.2.0",
"eslint-plugin-oxlint": "^1.15.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"express-validator": "^7.2.1",

View File

@@ -7,7 +7,7 @@ import TabsService from '@renderer/services/TabsService'
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import { Avatar } from 'antd'
import { WebviewTag } from 'electron'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FC, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
@@ -28,7 +28,9 @@ const MinAppPage: FC = () => {
const navigate = useNavigate()
// Remember the initial navbar position when component mounts
const initialIsTopNavbar = useRef<boolean>(isTopNavbar)
// It's immutable state
const [initialIsTopNavbar] = useState<boolean>(isTopNavbar)
const initialIsTopNavbarRef = useRef<boolean>(initialIsTopNavbar)
const hasRedirected = useRef<boolean>(false)
// Initialize TabsService with cache reference
@@ -40,8 +42,8 @@ const MinAppPage: FC = () => {
// Debug: track navbar position changes
useEffect(() => {
if (initialIsTopNavbar.current !== isTopNavbar) {
logger.debug(`NavBar position changed from ${initialIsTopNavbar.current} to ${isTopNavbar}`)
if (initialIsTopNavbarRef.current !== isTopNavbar) {
logger.debug(`NavBar position changed from ${initialIsTopNavbarRef.current} to ${isTopNavbar}`)
}
}, [isTopNavbar])
@@ -69,7 +71,7 @@ const MinAppPage: FC = () => {
// For sidebar navigation, redirect to apps list and open popup
// Only check once and only if we haven't already redirected
if (!initialIsTopNavbar.current && !hasRedirected.current) {
if (!initialIsTopNavbarRef.current && !hasRedirected.current) {
hasRedirected.current = true
navigate('/apps')
// Open popup after navigation
@@ -80,15 +82,20 @@ const MinAppPage: FC = () => {
}
// For top navbar mode, integrate with cache system
if (initialIsTopNavbar.current) {
if (initialIsTopNavbarRef.current) {
// 无论是否已在缓存,都调用以确保 currentMinappId 同步到路由切换的新 appId
openMinappKeepAlive(app)
}
}, [app, navigate, openMinappKeepAlive, initialIsTopNavbar])
}, [app, navigate, openMinappKeepAlive])
// -------------- 新的 Tab Shell 逻辑 --------------
// 注意Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空
const webviewRef = useRef<WebviewTag | null>(null)
const [webview, setWebview] = useState<WebviewTag | null>(null)
const webviewRef = useRef<WebviewTag | null>(webview)
useEffect(() => {
webviewRef.current = webview
}, [webview])
const [isReady, setIsReady] = useState<boolean>(() => (app ? getWebviewLoaded(app.id) : false))
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
@@ -103,7 +110,7 @@ const MinAppPage: FC = () => {
if (webviewRef.current === el) return true // 已附着
webviewRef.current = el
setWebview(el)
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
el.addEventListener('did-navigate-in-page', handleInPageNav)
webviewCleanupRef.current = () => {
@@ -137,7 +144,10 @@ const MinAppPage: FC = () => {
if (!app) return
if (getWebviewLoaded(app.id)) {
// 已经加载
if (!isReady) setIsReady(true)
if (!isReady)
startTransition(() => {
setIsReady(true)
})
return
}
let mounted = true
@@ -155,7 +165,7 @@ const MinAppPage: FC = () => {
}, [app, isReady])
// 如果条件不满足,提前返回(所有 hooks 已调用)
if (!app || !initialIsTopNavbar.current) {
if (!app || !initialIsTopNavbar) {
return null
}
@@ -185,7 +195,7 @@ const MinAppPage: FC = () => {
onOpenDevTools={handleOpenDevTools}
/>
</ToolbarWrapper>
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
<WebviewSearch activeWebview={webview} isWebviewReady={isReady} appId={app.id} />
{!isReady && (
<LoadingMask>
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />

View File

@@ -8,14 +8,14 @@ import { useTranslation } from 'react-i18next'
type FoundInPageResult = Electron.FoundInPageResult
interface WebviewSearchProps {
webviewRef: React.RefObject<WebviewTag | null>
activeWebview: WebviewTag | null
isWebviewReady: boolean
appId: string
}
const logger = loggerService.withContext('WebviewSearch')
const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, appId }) => {
const WebviewSearch: FC<WebviewSearchProps> = ({ activeWebview, isWebviewReady, appId }) => {
const { t } = useTranslation()
const [isVisible, setIsVisible] = useState(false)
const [query, setQuery] = useState('')
@@ -25,7 +25,6 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
const focusFrameRef = useRef<number | null>(null)
const lastAppIdRef = useRef<string>(appId)
const attachedWebviewRef = useRef<WebviewTag | null>(null)
const activeWebview = webviewRef.current ?? null
const focusInput = useCallback(() => {
if (focusFrameRef.current !== null) {
@@ -81,7 +80,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
)
const getUsableWebview = useCallback(() => {
const candidates = [webviewRef.current, attachedWebviewRef.current]
const candidates = [activeWebview, attachedWebviewRef.current]
for (const candidate of candidates) {
const usable = ensureWebviewReady(candidate)
@@ -91,7 +90,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
}
return null
}, [ensureWebviewReady, webviewRef])
}, [ensureWebviewReady, activeWebview])
const stopSearch = useCallback(() => {
const target = getUsableWebview()

View File

@@ -136,9 +136,8 @@ describe('WebviewSearch', () => {
it('opens the search overlay with keyboard shortcut', async () => {
const { webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
@@ -149,9 +148,8 @@ describe('WebviewSearch', () => {
it('opens the search overlay when webview shortcut is forwarded', async () => {
const { webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(onFindShortcutMock).toHaveBeenCalled()
@@ -170,13 +168,12 @@ describe('WebviewSearch', () => {
;(webview as any).getWebContentsId = vi.fn(() => {
throw error
})
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const getWebContentsIdMock = vi.fn(() => {
throw error
})
;(webview as any).getWebContentsId = getWebContentsIdMock
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(getWebContentsIdMock).toHaveBeenCalled()
@@ -185,8 +182,8 @@ describe('WebviewSearch', () => {
;(webview as any).getWebContentsId = vi.fn(() => 1)
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(onFindShortcutMock).toHaveBeenCalled()
@@ -200,9 +197,8 @@ describe('WebviewSearch', () => {
throw error
})
;(webview as any).getWebContentsId = getWebContentsIdMock
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const { rerender, unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
const { rerender, unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(getWebContentsIdMock).toHaveBeenCalled()
@@ -212,7 +208,7 @@ describe('WebviewSearch', () => {
throw new Error('should not be called')
})
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
expect(stopFindInPageMock).not.toHaveBeenCalled()
unmount()
@@ -221,9 +217,8 @@ describe('WebviewSearch', () => {
it('closes the search overlay when escape is forwarded from the webview', async () => {
const { webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(onFindShortcutMock).toHaveBeenCalled()
@@ -245,10 +240,9 @@ describe('WebviewSearch', () => {
it('performs searches and navigates between results', async () => {
const { emit, findInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
@@ -286,10 +280,9 @@ describe('WebviewSearch', () => {
it('navigates results when enter is forwarded from the webview', async () => {
const { findInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await waitFor(() => {
expect(onFindShortcutMock).toHaveBeenCalled()
@@ -325,10 +318,9 @@ describe('WebviewSearch', () => {
it('clears search state when appId changes', async () => {
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
@@ -338,7 +330,7 @@ describe('WebviewSearch', () => {
})
await act(async () => {
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-2" />)
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-2" />)
})
await waitFor(() => {
@@ -352,10 +344,9 @@ describe('WebviewSearch', () => {
findInPageMock.mockImplementation(() => {
throw new Error('findInPage failed')
})
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
@@ -368,9 +359,8 @@ describe('WebviewSearch', () => {
it('stops search when component unmounts', async () => {
const { stopFindInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const { unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
const { unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
await openSearchOverlay()
stopFindInPageMock.mockClear()
@@ -382,9 +372,8 @@ describe('WebviewSearch', () => {
it('ignores keyboard shortcut when webview is not ready', async () => {
const { findInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
render(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
await act(async () => {
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })

View File

@@ -9,7 +9,7 @@ import { findNode } from '@renderer/services/NotesTreeService'
import { Dropdown, Input, Tooltip } from 'antd'
import { t } from 'i18next'
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import { menuItems } from './MenuConfig'
@@ -19,9 +19,6 @@ const logger = loggerService.withContext('HeaderNavbar')
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpandPath, onRenameNode }) => {
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
const { activeNode } = useActiveNode(notesTree)
const [breadcrumbItems, setBreadcrumbItems] = useState<
Array<{ key: string; title: string; treePath: string; isFolder: boolean }>
>([])
const [titleValue, setTitleValue] = useState('')
const titleInputRef = useRef<any>(null)
const { settings, updateSettings } = useNotesSettings()
@@ -141,18 +138,17 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
// 同步标题值
useEffect(() => {
if (activeNode?.type === 'file') {
setTitleValue(activeNode.name.replace('.md', ''))
startTransition(() => setTitleValue(activeNode.name.replace('.md', '')))
}
}, [activeNode])
// 构建面包屑路径
useEffect(() => {
const breadcrumbItems = useMemo(() => {
if (!activeNode || !notesTree) {
setBreadcrumbItems([])
return
return []
}
const node = findNode(notesTree, activeNode.id)
if (!node) return
if (!node) return []
const pathParts = node.treePath.split('/').filter(Boolean)
const items = pathParts.map((part, index) => {
@@ -166,7 +162,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
}
})
setBreadcrumbItems(items)
return items
}, [activeNode, notesTree])
return (

View File

@@ -63,39 +63,6 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
).filter(Boolean)
const ModalContent = () => {
if (isLoading) {
// TODO: use skeleton for better ux
return <Spinner />
}
if (error) {
return (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
)
}
return (
<div className="flex w-full flex-1">
<LeftMenu>
<StyledMenu
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
mode="vertical"
selectedKeys={[menu]}
items={items}
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
</Settings>
</div>
)
}
return (
<StyledModal
open={open}
@@ -129,7 +96,31 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
}}
width="min(800px, 70vw)"
centered>
<ModalContent />
{isLoading && <Spinner />}
{!isLoading && error && (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
)}
{!isLoading && !error && (
<div className="flex w-full flex-1">
<LeftMenu>
<StyledMenu
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
mode="vertical"
selectedKeys={[menu]}
items={items}
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
</Settings>
</div>
)}
</StyledModal>
)
}

View File

@@ -65,39 +65,6 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
).filter(Boolean)
const ModalContent = () => {
if (isLoading) {
// TODO: use skeleton for better ux
return <Spinner />
}
if (error) {
return (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
)
}
return (
<div className="flex w-full flex-1">
<LeftMenu>
<StyledMenu
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
mode="vertical"
selectedKeys={[menu]}
items={items}
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <EssentialSettings agentBase={session} update={updateSession} />}
{menu === 'prompt' && <PromptSettings agentBase={session} update={updateSession} />}
{menu === 'tooling' && <ToolingSettings agentBase={session} update={updateSession} />}
{menu === 'advanced' && <AdvancedSettings agentBase={session} update={updateSession} />}
</Settings>
</div>
)
}
return (
<StyledModal
open={open}
@@ -125,7 +92,31 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
}}
width="min(800px, 70vw)"
centered>
<ModalContent />
{isLoading && <Spinner />}
{!isLoading && error && (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
)}
{!isLoading && !error && (
<div className="flex w-full flex-1">
<LeftMenu>
<StyledMenu
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
mode="vertical"
selectedKeys={[menu]}
items={items}
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <EssentialSettings agentBase={session} update={updateSession} />}
{menu === 'prompt' && <PromptSettings agentBase={session} update={updateSession} />}
{menu === 'tooling' && <ToolingSettings agentBase={session} update={updateSession} />}
{menu === 'advanced' && <AdvancedSettings agentBase={session} update={updateSession} />}
</Settings>
</div>
)}
</StyledModal>
)
}

View File

@@ -5,7 +5,7 @@ import FileItem from '@renderer/pages/files/FileItem'
import { Assistant, QuickPhrase } from '@renderer/types'
import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd'
import { PlusIcon } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { v4 as uuidv4 } from 'uuid'
@@ -21,15 +21,12 @@ interface AssistantRegularPromptsSettingsProps {
const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps> = ({ assistant, updateAssistant }) => {
const { t } = useTranslation()
const [promptsList, setPromptsList] = useState<QuickPhrase[]>([])
const [isModalOpen, setIsModalOpen] = useState(false)
const [editingPrompt, setEditingPrompt] = useState<QuickPhrase | null>(null)
const [formData, setFormData] = useState({ title: '', content: '' })
const [dragging, setDragging] = useState(false)
useEffect(() => {
setPromptsList(assistant.regularPhrases || [])
}, [assistant.regularPhrases])
const promptsList: QuickPhrase[] = useMemo(() => assistant.regularPhrases || [], [assistant.regularPhrases])
const handleAdd = () => {
setEditingPrompt(null)
@@ -45,7 +42,6 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
const handleDelete = async (id: string) => {
const updatedPrompts = promptsList.filter((prompt) => prompt.id !== id)
setPromptsList(updatedPrompts)
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
}
@@ -68,13 +64,11 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
}
updatedPrompts = [...promptsList, newPrompt]
}
setPromptsList(updatedPrompts)
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
setIsModalOpen(false)
}
const handleUpdateOrder = async (newPrompts: QuickPhrase[]) => {
setPromptsList(newPrompts)
updateAssistant({ ...assistant, regularPhrases: newPrompts })
}

View File

@@ -27,25 +27,6 @@ const OcrProviderSettings = ({ provider }: Props) => {
return null
}
const ProviderSettings = () => {
if (isBuiltinOcrProvider(provider)) {
switch (provider.id) {
case 'tesseract':
return <OcrTesseractSettings />
case 'system':
return <OcrSystemSettings />
case 'paddleocr':
return <OcrPpocrSettings />
case 'ovocr':
return <OcrOVSettings />
default:
return null
}
} else {
throw new Error('Not supported OCR provider')
}
}
return (
<SettingGroup theme={themeMode}>
<SettingTitle>
@@ -56,7 +37,7 @@ const OcrProviderSettings = ({ provider }: Props) => {
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
<ErrorBoundary>
<ProviderSettings />
<ProviderSettings provider={provider} />
</ErrorBoundary>
</SettingGroup>
)
@@ -67,4 +48,23 @@ const ProviderName = styled.span`
font-weight: 500;
`
const ProviderSettings = ({ provider }: { provider: OcrProvider }) => {
if (isBuiltinOcrProvider(provider)) {
switch (provider.id) {
case 'tesseract':
return <OcrTesseractSettings />
case 'system':
return <OcrSystemSettings />
case 'paddleocr':
return <OcrPpocrSettings />
case 'ovocr':
return <OcrOVSettings />
default:
return null
}
} else {
throw new Error('Not supported OCR provider')
}
}
export default OcrProviderSettings

View File

@@ -1,7 +1,8 @@
import { TraceModal } from '@renderer/trace/pages/TraceModel'
import { Divider } from 'antd/lib'
import dayjs from 'dayjs'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useMemo, useState } from 'react'
import { Box, GridItem, HStack, IconButton, SimpleGrid, Text } from './Component'
import { ProgressBar } from './ProgressBar'
@@ -38,12 +39,10 @@ export const convertTime = (time: number | null): string => {
const TreeNode: React.FC<TreeNodeProps> = ({ node, handleClick, treeData, paddingLeft = 2 }) => {
const [isOpen, setIsOpen] = useState(true)
const hasChildren = node.children && node.children.length > 0
const [usedTime, setUsedTime] = useState('--')
// 只在 endTime 或 node 变化时更新 usedTime
useEffect(() => {
const endTime = node.endTime || Date.now()
setUsedTime(convertTime(endTime - node.startTime))
const usedTime = useMemo(() => {
const endTime = node.endTime || dayjs().valueOf()
return convertTime(endTime - node.startTime)
}, [node])
return (

View File

@@ -3,9 +3,10 @@ import './Trace.css'
import { SpanEntity } from '@mcp-trace/trace-core'
import { TraceModal } from '@renderer/trace/pages/TraceModel'
import { Divider } from 'antd/lib'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { findNodeById, mergeTraceModals, updatePercentAndStart } from '../utils'
import { Box, GridItem, SimpleGrid, Text, VStack } from './Component'
import SpanDetail from './SpanDetail'
import TraceTree from './TraceTree'
@@ -19,44 +20,12 @@ export interface TracePageProp {
export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName, reload = false }) => {
const [spans, setSpans] = useState<TraceModal[]>([])
const [selectNode, setSelectNode] = useState<TraceModal | null>(null)
const [showList, setShowList] = useState(true)
const [selectNodeId, setSelectNodeId] = useState<string | null>(null)
const selectNode = useMemo(() => (selectNodeId ? findNodeById(spans, selectNodeId) : null), [selectNodeId, spans])
const showList = useMemo(() => selectNodeId === null || !selectNode, [selectNode, selectNodeId])
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const { t } = useTranslation()
const mergeTraceModals = useCallback((oldNodes: TraceModal[], newNodes: TraceModal[]): TraceModal[] => {
const oldMap = new Map(oldNodes.map((n) => [n.id, n]))
return newNodes.map((newNode) => {
const oldNode = oldMap.get(newNode.id)
if (oldNode) {
// 如果旧节点已经结束,则直接返回旧节点
if (oldNode.endTime) {
return oldNode
}
oldNode.children = mergeTraceModals(oldNode.children, newNode.children)
Object.assign(oldNode, newNode)
return oldNode
} else {
return newNode
}
})
}, [])
const updatePercentAndStart = useCallback((nodes: TraceModal[], rootStart?: number, rootEnd?: number) => {
nodes.forEach((node) => {
const _rootStart = rootStart || node.startTime
const _rootEnd = rootEnd || node.endTime || Date.now()
const endTime = node.endTime || _rootEnd
const usedTime = endTime - node.startTime
const duration = _rootEnd - _rootStart
node.start = ((node.startTime - _rootStart) * 100) / duration
node.percent = duration === 0 ? 0 : (usedTime * 100) / duration
if (node.children) {
updatePercentAndStart(node.children, _rootStart, _rootEnd)
}
})
}, [])
const getRootSpan = (spans: SpanEntity[]): TraceModal[] => {
const map: Map<string, TraceModal> = new Map()
@@ -78,17 +47,6 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
)
}
const findNodeById = useCallback((nodes: TraceModal[], id: string): TraceModal | null => {
for (const n of nodes) {
if (n.id === id) return n
if (n.children) {
const found = findNodeById(n.children, id)
if (found) return found
}
}
return null
}, [])
const getTraceData = useCallback(async (): Promise<boolean> => {
const datas = topicId && traceId ? await window.api.trace.getData(topicId, traceId, modelName) : []
const matchedSpans = getRootSpan(datas)
@@ -96,19 +54,14 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
setSpans((prev) => mergeTraceModals(prev, matchedSpans))
const isEnded = !matchedSpans.find((e) => !e.endTime || e.endTime <= 0)
return isEnded
}, [topicId, traceId, modelName, updatePercentAndStart, mergeTraceModals])
}, [topicId, traceId, modelName])
const handleNodeClick = (nodeId: string) => {
const latestNode = findNodeById(spans, nodeId)
if (latestNode) {
setSelectNode(latestNode)
setShowList(false)
}
setSelectNodeId(nodeId)
}
const handleShowList = () => {
setShowList(true)
setSelectNode(null)
setSelectNodeId(null)
}
useEffect(() => {
@@ -138,18 +91,6 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
}
}, [getTraceData, traceId, topicId, reload])
useEffect(() => {
if (selectNode) {
const latest = findNodeById(spans, selectNode.id)
if (!latest) {
setShowList(true)
setSelectNode(null)
} else if (latest !== selectNode) {
setSelectNode(latest)
}
}
}, [spans, selectNode, findNodeById])
return (
<div className="trace-window">
<div className="tab-container_trace">

View File

@@ -0,0 +1,45 @@
import { TraceModal } from '../pages/TraceModel'
export const updatePercentAndStart = (nodes: TraceModal[], rootStart?: number, rootEnd?: number) => {
nodes.forEach((node) => {
const _rootStart = rootStart || node.startTime
const _rootEnd = rootEnd || node.endTime || Date.now()
const endTime = node.endTime || _rootEnd
const usedTime = endTime - node.startTime
const duration = _rootEnd - _rootStart
node.start = ((node.startTime - _rootStart) * 100) / duration
node.percent = duration === 0 ? 0 : (usedTime * 100) / duration
if (node.children) {
updatePercentAndStart(node.children, _rootStart, _rootEnd)
}
})
}
export const findNodeById = (nodes: TraceModal[], id: string): TraceModal | null => {
for (const n of nodes) {
if (n.id === id) return n
if (n.children) {
const found = findNodeById(n.children, id)
if (found) return found
}
}
return null
}
export const mergeTraceModals = (oldNodes: TraceModal[], newNodes: TraceModal[]): TraceModal[] => {
const oldMap = new Map(oldNodes.map((n) => [n.id, n]))
return newNodes.map((newNode) => {
const oldNode = oldMap.get(newNode.id)
if (oldNode) {
// 如果旧节点已经结束,则直接返回旧节点
if (oldNode.endTime) {
return oldNode
}
oldNode.children = mergeTraceModals(oldNode.children, newNode.children)
Object.assign(oldNode, newNode)
return oldNode
} else {
return newNode
}
})
}

View File

@@ -33,15 +33,15 @@ const Footer: FC<FooterProps> = ({
onEsc()
})
useHotkeys('c', () => {
handleCopy()
})
const handleCopy = () => {
if (loading || !onCopy) return
onCopy()
}
useHotkeys('c', () => {
handleCopy()
})
return (
<WindowFooter className="drag">
<FooterText>

View File

@@ -3,7 +3,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
import { Assistant } from '@renderer/types'
import { Input as AntdInput } from 'antd'
import { InputRef } from 'rc-input/lib/interface'
import React, { useRef } from 'react'
import React, { useEffect, useRef } from 'react'
import styled from 'styled-components'
interface InputBarProps {
@@ -27,9 +27,13 @@ const InputBar = ({
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
const inputRef = useRef<InputRef>(null)
const { setTimeoutTimer } = useTimer()
if (!loading) {
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
}
useEffect(() => {
if (!loading) {
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
}
})
return (
<InputWrapper ref={ref}>
{assistant.model && <ModelAvatar model={assistant.model} size={30} />}

View File

@@ -36,32 +36,38 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
const [isContented, setIsContented] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [contentToCopy, setContentToCopy] = useState('')
const [assistant] = useState<Assistant>(() => {
const assistant = action.assistantId
? getAssistantById(action.assistantId) || getDefaultAssistant()
: getDefaultAssistant()
if (!assistant.model) {
return { ...assistant, model: getDefaultModel() }
} else {
return assistant
}
})
const [topic] = useState<Topic | null>(getDefaultTopic(assistant.id))
const initialized = useRef(false)
// Use useRef for values that shouldn't trigger re-renders
const assistantRef = useRef<Assistant | null>(null)
const topicRef = useRef<Topic | null>(null)
const topicRef = useRef<Topic | null>(topic)
const promptContentRef = useRef('')
const askId = useRef('')
// Sync refs
useEffect(() => {
assistantRef.current = assistant
}, [assistant])
useEffect(() => {
topicRef.current = topic
}, [assistant, topic])
// Initialize values only once when action changes
useEffect(() => {
if (initialized.current) return
initialized.current = true
// Initialize assistant
const currentAssistant = action.assistantId
? getAssistantById(action.assistantId) || getDefaultAssistant()
: getDefaultAssistant()
assistantRef.current = {
...currentAssistant,
model: currentAssistant.model || getDefaultModel()
}
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
// Initialize prompt content
let userContent = ''
switch (action.id) {
@@ -128,7 +134,7 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
fetchResult()
}, [fetchResult])
const allMessages = useTopicMessages(topicRef.current?.id || '')
const allMessages = useTopicMessages(topic?.id || '')
// Memoize the messages to prevent unnecessary re-renders
const messageContent = useMemo(() => {

View File

@@ -9,7 +9,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import useTranslate from '@renderer/hooks/useTranslate'
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
import { Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
import type { ActionItem } from '@renderer/types/selectionTypes'
import { runAsyncFunction } from '@renderer/utils'
import { abortCompletion } from '@renderer/utils/abortController'
@@ -31,7 +31,7 @@ const logger = loggerService.withContext('ActionTranslate')
const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const { t } = useTranslation()
const { translateModelPrompt, language } = useSettings()
const { language } = useSettings()
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
@@ -41,14 +41,20 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const [isContented, setIsContented] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [contentToCopy, setContentToCopy] = useState('')
// use default assistant.
// FIXME: this component create a new topic every time, but related data would not be cleared.
const [topic] = useState<Topic>(getDefaultTopic('default'))
const { getLanguageByLangcode } = useTranslate()
// Use useRef for values that shouldn't trigger re-renders
const initialized = useRef(false)
const assistantRef = useRef<Assistant | null>(null)
const topicRef = useRef<Topic | null>(null)
const topicRef = useRef<Topic | null>(topic)
const askId = useRef('')
// Sync ref
useEffect(() => {
topicRef.current = topic
}, [topic])
useEffect(() => {
runAsyncFunction(async () => {
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
@@ -79,22 +85,8 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
})
}, [getLanguageByLangcode, language])
// Initialize values only once when action changes
useEffect(() => {
if (initialized.current || !action.selectedText) return
initialized.current = true
// Initialize assistant
const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText)
assistantRef.current = currentAssistant
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
}, [action, targetLanguage, translateModelPrompt])
const fetchResult = useCallback(async () => {
if (!assistantRef.current || !topicRef.current || !action.selectedText) return
if (!topicRef.current || !action.selectedText) return
const setAskId = (id: string) => {
askId.current = id
@@ -112,8 +104,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
setError(error.message)
}
setIsLoading(true)
let sourceLanguageCode: TranslateLanguageCode
try {
@@ -139,7 +129,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
}
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
assistantRef.current = assistant
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom])
@@ -147,7 +136,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
fetchResult()
}, [fetchResult])
const allMessages = useTopicMessages(topicRef.current?.id || '')
const allMessages = useTopicMessages(topic?.id || '')
const messageContent = useMemo(() => {
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')

View File

@@ -30,19 +30,6 @@ const WindowFooter: FC<FooterProps> = ({
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
const { setTimeoutTimer } = useTimer()
useEffect(() => {
window.addEventListener('focus', handleWindowFocus)
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('focus', handleWindowFocus)
window.removeEventListener('blur', handleWindowBlur)
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current)
}
}
}, [])
useEffect(() => {
hideTimerRef.current = setTimeout(() => {
setIsShowMe(false)
@@ -68,21 +55,6 @@ const WindowFooter: FC<FooterProps> = ({
}, 2000)
}
useHotkeys('c', () => {
showMePeriod()
handleCopy()
})
useHotkeys('r', () => {
showMePeriod()
handleRegenerate()
})
useHotkeys('esc', () => {
showMePeriod()
handleEsc()
})
const handleEsc = () => {
setIsEscHovered(true)
setTimeoutTimer(
@@ -100,6 +72,11 @@ const WindowFooter: FC<FooterProps> = ({
}
}
useHotkeys('esc', () => {
showMePeriod()
handleEsc()
})
const handleRegenerate = () => {
setIsRegenerateHovered(true)
setTimeoutTimer(
@@ -126,6 +103,11 @@ const WindowFooter: FC<FooterProps> = ({
}
}
useHotkeys('r', () => {
showMePeriod()
handleRegenerate()
})
const handleCopy = () => {
if (!content || loading) return
@@ -147,6 +129,11 @@ const WindowFooter: FC<FooterProps> = ({
})
}
useHotkeys('c', () => {
showMePeriod()
handleCopy()
})
const handleWindowFocus = () => {
setIsWindowFocus(true)
}
@@ -155,6 +142,19 @@ const WindowFooter: FC<FooterProps> = ({
setIsWindowFocus(false)
}
useEffect(() => {
window.addEventListener('focus', handleWindowFocus)
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('focus', handleWindowFocus)
window.removeEventListener('blur', handleWindowBlur)
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current)
}
}
}, [])
return (
<Container
onMouseEnter={() => setIsContainerHovered(true)}

View File

@@ -204,31 +204,6 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
}
}, [setTimeoutTimer])
const handleAction = useCallback(
(action: ActionItem) => {
if (demo) return
/** avoid mutating the original action, it will cause syncing issue */
const newAction = { ...action, selectedText: selectedText.current }
switch (action.id) {
case 'copy':
handleCopy()
break
case 'search':
handleSearch(newAction)
break
case 'quote':
handleQuote(newAction)
break
default:
handleDefaultAction(newAction)
break
}
},
[demo, handleCopy]
)
const handleSearch = (action: ActionItem) => {
if (!action.searchEngine) return
@@ -256,6 +231,31 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
window.api?.selection.hideToolbar()
}
const handleAction = useCallback(
(action: ActionItem) => {
if (demo) return
/** avoid mutating the original action, it will cause syncing issue */
const newAction = { ...action, selectedText: selectedText.current }
switch (action.id) {
case 'copy':
handleCopy()
break
case 'search':
handleSearch(newAction)
break
case 'quote':
handleQuote(newAction)
break
default:
handleDefaultAction(newAction)
break
}
},
[demo, handleCopy]
)
return (
<Container>
<LogoWrapper $draggable={!demo}>

146
yarn.lock
View File

@@ -2098,6 +2098,29 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:^7.24.4":
version: 7.28.4
resolution: "@babel/core@npm:7.28.4"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helpers": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.4"
"@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.28.4"
"@babel/types": "npm:^7.28.4"
"@jridgewell/remapping": "npm:^2.3.5"
convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
checksum: 10c0/ef5a6c3c6bf40d3589b5593f8118cfe2602ce737412629fb6e26d595be2fcbaae0807b43027a5c42ec4fba5b895ff65891f2503b5918c8a3ea3542ab44d4c278
languageName: node
linkType: hard
"@babel/core@npm:^7.27.7":
version: 7.28.0
resolution: "@babel/core@npm:7.28.0"
@@ -2134,6 +2157,19 @@ __metadata:
languageName: node
linkType: hard
"@babel/generator@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/generator@npm:7.28.3"
dependencies:
"@babel/parser": "npm:^7.28.3"
"@babel/types": "npm:^7.28.2"
"@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2"
checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc
languageName: node
linkType: hard
"@babel/helper-compilation-targets@npm:^7.27.2":
version: 7.27.2
resolution: "@babel/helper-compilation-targets@npm:7.27.2"
@@ -2177,6 +2213,19 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-transforms@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/helper-module-transforms@npm:7.28.3"
dependencies:
"@babel/helper-module-imports": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
"@babel/traverse": "npm:^7.28.3"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
@@ -2215,6 +2264,27 @@ __metadata:
languageName: node
linkType: hard
"@babel/helpers@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/helpers@npm:7.28.4"
dependencies:
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.4"
checksum: 10c0/aaa5fb8098926dfed5f223adf2c5e4c7fbba4b911b73dfec2d7d3083f8ba694d201a206db673da2d9b3ae8c01793e795767654558c450c8c14b4c2175b4fcb44
languageName: node
linkType: hard
"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/parser@npm:7.28.4"
dependencies:
"@babel/types": "npm:^7.28.4"
bin:
parser: ./bin/babel-parser.js
checksum: 10c0/58b239a5b1477ac7ed7e29d86d675cc81075ca055424eba6485872626db2dc556ce63c45043e5a679cd925e999471dba8a3ed4864e7ab1dbf64306ab72c52707
languageName: node
linkType: hard
"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.27.7, @babel/parser@npm:^7.28.0":
version: 7.28.0
resolution: "@babel/parser@npm:7.28.0"
@@ -2284,6 +2354,21 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/traverse@npm:7.28.4"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.4"
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.4"
debug: "npm:^4.3.1"
checksum: 10c0/ee678fdd49c9f54a32e07e8455242390d43ce44887cea6567b233fe13907b89240c377e7633478a32c6cf1be0e17c2f7f3b0c59f0666e39c5074cc47b968489c
languageName: node
linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0":
version: 7.28.1
resolution: "@babel/types@npm:7.28.1"
@@ -2304,6 +2389,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/types@npm:7.28.4"
dependencies:
"@babel/helper-string-parser": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
checksum: 10c0/ac6f909d6191319e08c80efbfac7bd9a25f80cc83b43cd6d82e7233f7a6b9d6e7b90236f3af7400a3f83b576895bcab9188a22b584eb0f224e80e6d4e95f4517
languageName: node
linkType: hard
"@bcoe/v8-coverage@npm:^1.0.2":
version: 1.0.2
resolution: "@bcoe/v8-coverage@npm:1.0.2"
@@ -5998,7 +6093,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/remapping@npm:^2.3.4":
"@jridgewell/remapping@npm:^2.3.4, @jridgewell/remapping@npm:^2.3.5":
version: 2.3.5
resolution: "@jridgewell/remapping@npm:2.3.5"
dependencies:
@@ -14113,7 +14208,7 @@ __metadata:
eslint: "npm:^9.22.0"
eslint-plugin-import-zod: "npm:^1.2.0"
eslint-plugin-oxlint: "npm:^1.15.0"
eslint-plugin-react-hooks: "npm:^5.2.0"
eslint-plugin-react-hooks: "npm:^7.0.0"
eslint-plugin-simple-import-sort: "npm:^12.1.1"
eslint-plugin-unused-imports: "npm:^4.1.4"
express: "npm:^5.1.0"
@@ -18225,6 +18320,21 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-react-hooks@npm:^7.0.0":
version: 7.0.0
resolution: "eslint-plugin-react-hooks@npm:7.0.0"
dependencies:
"@babel/core": "npm:^7.24.4"
"@babel/parser": "npm:^7.24.4"
hermes-parser: "npm:^0.25.1"
zod: "npm:^3.22.4 || ^4.0.0"
zod-validation-error: "npm:^3.0.3 || ^4.0.0"
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
checksum: 10c0/911c9efdd9b102ce2eabac247dff8c217ecb8d6972aaf3b7eecfb1cfc293d4d902766355993ff7a37a33c0abde3e76971f43bc1c8ff36d6c123310e5680d0423
languageName: node
linkType: hard
"eslint-plugin-react-naming-convention@npm:1.48.1":
version: 1.48.1
resolution: "eslint-plugin-react-naming-convention@npm:1.48.1"
@@ -20005,6 +20115,22 @@ __metadata:
languageName: node
linkType: hard
"hermes-estree@npm:0.25.1":
version: 0.25.1
resolution: "hermes-estree@npm:0.25.1"
checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac
languageName: node
linkType: hard
"hermes-parser@npm:^0.25.1":
version: 0.25.1
resolution: "hermes-parser@npm:0.25.1"
dependencies:
hermes-estree: "npm:0.25.1"
checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c
languageName: node
linkType: hard
"hls-video-element@npm:^1.5.6":
version: 1.5.7
resolution: "hls-video-element@npm:1.5.7"
@@ -30251,6 +30377,15 @@ __metadata:
languageName: node
linkType: hard
"zod-validation-error@npm:^3.0.3 || ^4.0.0":
version: 4.0.2
resolution: "zod-validation-error@npm:4.0.2"
peerDependencies:
zod: ^3.25.0 || ^4.0.0
checksum: 10c0/0ccfec48c46de1be440b719cd02044d4abb89ed0e14c13e637cd55bf29102f67ccdba373f25def0fc7130e5f15025be4d557a7edcc95d5a3811599aade689e1b
languageName: node
linkType: hard
"zod-validation-error@npm:^3.4.0":
version: 3.4.0
resolution: "zod-validation-error@npm:3.4.0"
@@ -30260,6 +30395,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.22.4 || ^4.0.0":
version: 4.1.12
resolution: "zod@npm:4.1.12"
checksum: 10c0/b64c1feb19e99d77075261eaf613e0b2be4dfcd3551eff65ad8b4f2a079b61e379854d066f7d447491fcf193f45babd8095551a9d47973d30b46b6d8e2c46774
languageName: node
linkType: hard
"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.1":
version: 3.25.56
resolution: "zod@npm:3.25.56"