Compare commits
22 Commits
v1.7.0-bet
...
fix/react-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca44133e90 | ||
|
|
51dcdf94fb | ||
|
|
254051cf62 | ||
|
|
24d2e6e6ce | ||
|
|
cf9bfce43c | ||
|
|
76bf78b810 | ||
|
|
f4441e2a55 | ||
|
|
84f590ec7b | ||
|
|
a5865cfd01 | ||
|
|
4e7c714ea2 | ||
|
|
d2c4231458 | ||
|
|
b5004e2a51 | ||
|
|
e0c334b5ed | ||
|
|
d482e661fb | ||
|
|
3ac1caca69 | ||
|
|
94c112c066 | ||
|
|
2e694a87f8 | ||
|
|
4ae30db53a | ||
|
|
f4a6dd91cf | ||
|
|
c08a570c27 | ||
|
|
9c318c9526 | ||
|
|
4cee09870a |
@@ -12,7 +12,7 @@ export default defineConfig([
|
|||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
eslintReact.configs['recommended-typescript'],
|
eslintReact.configs['recommended-typescript'],
|
||||||
reactHooks.configs['recommended-latest'],
|
reactHooks.configs.flat.recommended,
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'simple-import-sort': simpleImportSort,
|
'simple-import-sort': simpleImportSort,
|
||||||
|
|||||||
@@ -261,7 +261,7 @@
|
|||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-import-zod": "^1.2.0",
|
"eslint-plugin-import-zod": "^1.2.0",
|
||||||
"eslint-plugin-oxlint": "^1.15.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-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import TabsService from '@renderer/services/TabsService'
|
|||||||
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
||||||
import { Avatar } from 'antd'
|
import { Avatar } from 'antd'
|
||||||
import { WebviewTag } from 'electron'
|
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 { useNavigate, useParams } from 'react-router-dom'
|
||||||
import BeatLoader from 'react-spinners/BeatLoader'
|
import BeatLoader from 'react-spinners/BeatLoader'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -28,7 +28,9 @@ const MinAppPage: FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// Remember the initial navbar position when component mounts
|
// 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)
|
const hasRedirected = useRef<boolean>(false)
|
||||||
|
|
||||||
// Initialize TabsService with cache reference
|
// Initialize TabsService with cache reference
|
||||||
@@ -40,8 +42,8 @@ const MinAppPage: FC = () => {
|
|||||||
|
|
||||||
// Debug: track navbar position changes
|
// Debug: track navbar position changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialIsTopNavbar.current !== isTopNavbar) {
|
if (initialIsTopNavbarRef.current !== isTopNavbar) {
|
||||||
logger.debug(`NavBar position changed from ${initialIsTopNavbar.current} to ${isTopNavbar}`)
|
logger.debug(`NavBar position changed from ${initialIsTopNavbarRef.current} to ${isTopNavbar}`)
|
||||||
}
|
}
|
||||||
}, [isTopNavbar])
|
}, [isTopNavbar])
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ const MinAppPage: FC = () => {
|
|||||||
|
|
||||||
// For sidebar navigation, redirect to apps list and open popup
|
// For sidebar navigation, redirect to apps list and open popup
|
||||||
// Only check once and only if we haven't already redirected
|
// Only check once and only if we haven't already redirected
|
||||||
if (!initialIsTopNavbar.current && !hasRedirected.current) {
|
if (!initialIsTopNavbarRef.current && !hasRedirected.current) {
|
||||||
hasRedirected.current = true
|
hasRedirected.current = true
|
||||||
navigate('/apps')
|
navigate('/apps')
|
||||||
// Open popup after navigation
|
// Open popup after navigation
|
||||||
@@ -80,15 +82,20 @@ const MinAppPage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For top navbar mode, integrate with cache system
|
// For top navbar mode, integrate with cache system
|
||||||
if (initialIsTopNavbar.current) {
|
if (initialIsTopNavbarRef.current) {
|
||||||
// 无论是否已在缓存,都调用以确保 currentMinappId 同步到路由切换的新 appId
|
// 无论是否已在缓存,都调用以确保 currentMinappId 同步到路由切换的新 appId
|
||||||
openMinappKeepAlive(app)
|
openMinappKeepAlive(app)
|
||||||
}
|
}
|
||||||
}, [app, navigate, openMinappKeepAlive, initialIsTopNavbar])
|
}, [app, navigate, openMinappKeepAlive])
|
||||||
|
|
||||||
// -------------- 新的 Tab Shell 逻辑 --------------
|
// -------------- 新的 Tab Shell 逻辑 --------------
|
||||||
// 注意:Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空
|
// 注意: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 [isReady, setIsReady] = useState<boolean>(() => (app ? getWebviewLoaded(app.id) : false))
|
||||||
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
|
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
|
||||||
|
|
||||||
@@ -103,7 +110,7 @@ const MinAppPage: FC = () => {
|
|||||||
|
|
||||||
if (webviewRef.current === el) return true // 已附着
|
if (webviewRef.current === el) return true // 已附着
|
||||||
|
|
||||||
webviewRef.current = el
|
setWebview(el)
|
||||||
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
|
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
|
||||||
el.addEventListener('did-navigate-in-page', handleInPageNav)
|
el.addEventListener('did-navigate-in-page', handleInPageNav)
|
||||||
webviewCleanupRef.current = () => {
|
webviewCleanupRef.current = () => {
|
||||||
@@ -137,7 +144,10 @@ const MinAppPage: FC = () => {
|
|||||||
if (!app) return
|
if (!app) return
|
||||||
if (getWebviewLoaded(app.id)) {
|
if (getWebviewLoaded(app.id)) {
|
||||||
// 已经加载
|
// 已经加载
|
||||||
if (!isReady) setIsReady(true)
|
if (!isReady)
|
||||||
|
startTransition(() => {
|
||||||
|
setIsReady(true)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let mounted = true
|
let mounted = true
|
||||||
@@ -155,7 +165,7 @@ const MinAppPage: FC = () => {
|
|||||||
}, [app, isReady])
|
}, [app, isReady])
|
||||||
|
|
||||||
// 如果条件不满足,提前返回(所有 hooks 已调用)
|
// 如果条件不满足,提前返回(所有 hooks 已调用)
|
||||||
if (!app || !initialIsTopNavbar.current) {
|
if (!app || !initialIsTopNavbar) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +195,7 @@ const MinAppPage: FC = () => {
|
|||||||
onOpenDevTools={handleOpenDevTools}
|
onOpenDevTools={handleOpenDevTools}
|
||||||
/>
|
/>
|
||||||
</ToolbarWrapper>
|
</ToolbarWrapper>
|
||||||
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
|
<WebviewSearch activeWebview={webview} isWebviewReady={isReady} appId={app.id} />
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<LoadingMask>
|
<LoadingMask>
|
||||||
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import { useTranslation } from 'react-i18next'
|
|||||||
type FoundInPageResult = Electron.FoundInPageResult
|
type FoundInPageResult = Electron.FoundInPageResult
|
||||||
|
|
||||||
interface WebviewSearchProps {
|
interface WebviewSearchProps {
|
||||||
webviewRef: React.RefObject<WebviewTag | null>
|
activeWebview: WebviewTag | null
|
||||||
isWebviewReady: boolean
|
isWebviewReady: boolean
|
||||||
appId: string
|
appId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = loggerService.withContext('WebviewSearch')
|
const logger = loggerService.withContext('WebviewSearch')
|
||||||
|
|
||||||
const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, appId }) => {
|
const WebviewSearch: FC<WebviewSearchProps> = ({ activeWebview, isWebviewReady, appId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
@@ -25,7 +25,6 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
const focusFrameRef = useRef<number | null>(null)
|
const focusFrameRef = useRef<number | null>(null)
|
||||||
const lastAppIdRef = useRef<string>(appId)
|
const lastAppIdRef = useRef<string>(appId)
|
||||||
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
||||||
const activeWebview = webviewRef.current ?? null
|
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
const focusInput = useCallback(() => {
|
||||||
if (focusFrameRef.current !== null) {
|
if (focusFrameRef.current !== null) {
|
||||||
@@ -81,7 +80,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getUsableWebview = useCallback(() => {
|
const getUsableWebview = useCallback(() => {
|
||||||
const candidates = [webviewRef.current, attachedWebviewRef.current]
|
const candidates = [activeWebview, attachedWebviewRef.current]
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const usable = ensureWebviewReady(candidate)
|
const usable = ensureWebviewReady(candidate)
|
||||||
@@ -91,7 +90,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}, [ensureWebviewReady, webviewRef])
|
}, [ensureWebviewReady, activeWebview])
|
||||||
|
|
||||||
const stopSearch = useCallback(() => {
|
const stopSearch = useCallback(() => {
|
||||||
const target = getUsableWebview()
|
const target = getUsableWebview()
|
||||||
|
|||||||
@@ -136,9 +136,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('opens the search overlay with keyboard shortcut', async () => {
|
it('opens the search overlay with keyboard shortcut', async () => {
|
||||||
const { webview } = createWebviewMock()
|
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()
|
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
|
||||||
|
|
||||||
@@ -149,9 +148,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
||||||
const { webview } = createWebviewMock()
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@@ -170,13 +168,12 @@ describe('WebviewSearch', () => {
|
|||||||
;(webview as any).getWebContentsId = vi.fn(() => {
|
;(webview as any).getWebContentsId = vi.fn(() => {
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
const getWebContentsIdMock = vi.fn(() => {
|
const getWebContentsIdMock = vi.fn(() => {
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
;(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(() => {
|
await waitFor(() => {
|
||||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
@@ -185,8 +182,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
||||||
|
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@@ -200,9 +197,8 @@ describe('WebviewSearch', () => {
|
|||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
;(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(() => {
|
await waitFor(() => {
|
||||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
@@ -212,7 +208,7 @@ describe('WebviewSearch', () => {
|
|||||||
throw new Error('should not be called')
|
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()
|
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
unmount()
|
unmount()
|
||||||
@@ -221,9 +217,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||||
const { webview } = createWebviewMock()
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@@ -245,10 +240,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('performs searches and navigates between results', async () => {
|
it('performs searches and navigates between results', async () => {
|
||||||
const { emit, findInPageMock, webview } = createWebviewMock()
|
const { emit, findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@@ -286,10 +280,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('navigates results when enter is forwarded from the webview', async () => {
|
it('navigates results when enter is forwarded from the webview', async () => {
|
||||||
const { findInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@@ -325,10 +318,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('clears search state when appId changes', async () => {
|
it('clears search state when appId changes', async () => {
|
||||||
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
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()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@@ -338,7 +330,7 @@ describe('WebviewSearch', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-2" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-2" />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -352,10 +344,9 @@ describe('WebviewSearch', () => {
|
|||||||
findInPageMock.mockImplementation(() => {
|
findInPageMock.mockImplementation(() => {
|
||||||
throw new Error('findInPage failed')
|
throw new Error('findInPage failed')
|
||||||
})
|
})
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@@ -368,9 +359,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('stops search when component unmounts', async () => {
|
it('stops search when component unmounts', async () => {
|
||||||
const { stopFindInPageMock, webview } = createWebviewMock()
|
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()
|
await openSearchOverlay()
|
||||||
|
|
||||||
stopFindInPageMock.mockClear()
|
stopFindInPageMock.mockClear()
|
||||||
@@ -382,9 +372,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('ignores keyboard shortcut when webview is not ready', async () => {
|
it('ignores keyboard shortcut when webview is not ready', async () => {
|
||||||
const { findInPageMock, webview } = createWebviewMock()
|
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 () => {
|
await act(async () => {
|
||||||
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
|
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { findNode } from '@renderer/services/NotesTreeService'
|
|||||||
import { Dropdown, Input, Tooltip } from 'antd'
|
import { Dropdown, Input, Tooltip } from 'antd'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
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 styled from 'styled-components'
|
||||||
|
|
||||||
import { menuItems } from './MenuConfig'
|
import { menuItems } from './MenuConfig'
|
||||||
@@ -19,9 +19,6 @@ const logger = loggerService.withContext('HeaderNavbar')
|
|||||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpandPath, onRenameNode }) => {
|
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpandPath, onRenameNode }) => {
|
||||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||||
const { activeNode } = useActiveNode(notesTree)
|
const { activeNode } = useActiveNode(notesTree)
|
||||||
const [breadcrumbItems, setBreadcrumbItems] = useState<
|
|
||||||
Array<{ key: string; title: string; treePath: string; isFolder: boolean }>
|
|
||||||
>([])
|
|
||||||
const [titleValue, setTitleValue] = useState('')
|
const [titleValue, setTitleValue] = useState('')
|
||||||
const titleInputRef = useRef<any>(null)
|
const titleInputRef = useRef<any>(null)
|
||||||
const { settings, updateSettings } = useNotesSettings()
|
const { settings, updateSettings } = useNotesSettings()
|
||||||
@@ -141,18 +138,17 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
|||||||
// 同步标题值
|
// 同步标题值
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeNode?.type === 'file') {
|
if (activeNode?.type === 'file') {
|
||||||
setTitleValue(activeNode.name.replace('.md', ''))
|
startTransition(() => setTitleValue(activeNode.name.replace('.md', '')))
|
||||||
}
|
}
|
||||||
}, [activeNode])
|
}, [activeNode])
|
||||||
|
|
||||||
// 构建面包屑路径
|
// 构建面包屑路径
|
||||||
useEffect(() => {
|
const breadcrumbItems = useMemo(() => {
|
||||||
if (!activeNode || !notesTree) {
|
if (!activeNode || !notesTree) {
|
||||||
setBreadcrumbItems([])
|
return []
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const node = findNode(notesTree, activeNode.id)
|
const node = findNode(notesTree, activeNode.id)
|
||||||
if (!node) return
|
if (!node) return []
|
||||||
|
|
||||||
const pathParts = node.treePath.split('/').filter(Boolean)
|
const pathParts = node.treePath.split('/').filter(Boolean)
|
||||||
const items = pathParts.map((part, index) => {
|
const items = pathParts.map((part, index) => {
|
||||||
@@ -166,7 +162,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setBreadcrumbItems(items)
|
return items
|
||||||
}, [activeNode, notesTree])
|
}, [activeNode, notesTree])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -63,39 +63,6 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||||
).filter(Boolean)
|
).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 (
|
return (
|
||||||
<StyledModal
|
<StyledModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -129,7 +96,31 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
|||||||
}}
|
}}
|
||||||
width="min(800px, 70vw)"
|
width="min(800px, 70vw)"
|
||||||
centered>
|
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>
|
</StyledModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,39 +65,6 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
|||||||
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||||
).filter(Boolean)
|
).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 (
|
return (
|
||||||
<StyledModal
|
<StyledModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -125,7 +92,31 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
|||||||
}}
|
}}
|
||||||
width="min(800px, 70vw)"
|
width="min(800px, 70vw)"
|
||||||
centered>
|
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>
|
</StyledModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import FileItem from '@renderer/pages/files/FileItem'
|
|||||||
import { Assistant, QuickPhrase } from '@renderer/types'
|
import { Assistant, QuickPhrase } from '@renderer/types'
|
||||||
import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd'
|
import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd'
|
||||||
import { PlusIcon } from 'lucide-react'
|
import { PlusIcon } from 'lucide-react'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@@ -21,15 +21,12 @@ interface AssistantRegularPromptsSettingsProps {
|
|||||||
|
|
||||||
const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps> = ({ assistant, updateAssistant }) => {
|
const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps> = ({ assistant, updateAssistant }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [promptsList, setPromptsList] = useState<QuickPhrase[]>([])
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [editingPrompt, setEditingPrompt] = useState<QuickPhrase | null>(null)
|
const [editingPrompt, setEditingPrompt] = useState<QuickPhrase | null>(null)
|
||||||
const [formData, setFormData] = useState({ title: '', content: '' })
|
const [formData, setFormData] = useState({ title: '', content: '' })
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const promptsList: QuickPhrase[] = useMemo(() => assistant.regularPhrases || [], [assistant.regularPhrases])
|
||||||
setPromptsList(assistant.regularPhrases || [])
|
|
||||||
}, [assistant.regularPhrases])
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setEditingPrompt(null)
|
setEditingPrompt(null)
|
||||||
@@ -45,7 +42,6 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
|||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
const updatedPrompts = promptsList.filter((prompt) => prompt.id !== id)
|
const updatedPrompts = promptsList.filter((prompt) => prompt.id !== id)
|
||||||
setPromptsList(updatedPrompts)
|
|
||||||
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +64,11 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
|||||||
}
|
}
|
||||||
updatedPrompts = [...promptsList, newPrompt]
|
updatedPrompts = [...promptsList, newPrompt]
|
||||||
}
|
}
|
||||||
setPromptsList(updatedPrompts)
|
|
||||||
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateOrder = async (newPrompts: QuickPhrase[]) => {
|
const handleUpdateOrder = async (newPrompts: QuickPhrase[]) => {
|
||||||
setPromptsList(newPrompts)
|
|
||||||
updateAssistant({ ...assistant, regularPhrases: newPrompts })
|
updateAssistant({ ...assistant, regularPhrases: newPrompts })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,28 @@ const OcrProviderSettings = ({ provider }: Props) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderSettings = () => {
|
return (
|
||||||
|
<SettingGroup theme={themeMode}>
|
||||||
|
<SettingTitle>
|
||||||
|
<Flex align="center" gap={8}>
|
||||||
|
<OcrProviderLogo provider={provider} />
|
||||||
|
<ProviderName> {getOcrProviderName(provider)}</ProviderName>
|
||||||
|
</Flex>
|
||||||
|
</SettingTitle>
|
||||||
|
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ProviderSettings provider={provider} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</SettingGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProviderName = styled.span`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProviderSettings = ({ provider }: { provider: OcrProvider }) => {
|
||||||
if (isBuiltinOcrProvider(provider)) {
|
if (isBuiltinOcrProvider(provider)) {
|
||||||
switch (provider.id) {
|
switch (provider.id) {
|
||||||
case 'tesseract':
|
case 'tesseract':
|
||||||
@@ -46,25 +67,4 @@ const OcrProviderSettings = ({ provider }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingGroup theme={themeMode}>
|
|
||||||
<SettingTitle>
|
|
||||||
<Flex align="center" gap={8}>
|
|
||||||
<OcrProviderLogo provider={provider} />
|
|
||||||
<ProviderName> {getOcrProviderName(provider)}</ProviderName>
|
|
||||||
</Flex>
|
|
||||||
</SettingTitle>
|
|
||||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
|
||||||
<ErrorBoundary>
|
|
||||||
<ProviderSettings />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</SettingGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProviderName = styled.span`
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default OcrProviderSettings
|
export default OcrProviderSettings
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||||
import { Divider } from 'antd/lib'
|
import { Divider } from 'antd/lib'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import * as React from 'react'
|
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 { Box, GridItem, HStack, IconButton, SimpleGrid, Text } from './Component'
|
||||||
import { ProgressBar } from './ProgressBar'
|
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 TreeNode: React.FC<TreeNodeProps> = ({ node, handleClick, treeData, paddingLeft = 2 }) => {
|
||||||
const [isOpen, setIsOpen] = useState(true)
|
const [isOpen, setIsOpen] = useState(true)
|
||||||
const hasChildren = node.children && node.children.length > 0
|
const hasChildren = node.children && node.children.length > 0
|
||||||
const [usedTime, setUsedTime] = useState('--')
|
|
||||||
|
|
||||||
// 只在 endTime 或 node 变化时更新 usedTime
|
// 只在 endTime 或 node 变化时更新 usedTime
|
||||||
useEffect(() => {
|
const usedTime = useMemo(() => {
|
||||||
const endTime = node.endTime || Date.now()
|
const endTime = node.endTime || dayjs().valueOf()
|
||||||
setUsedTime(convertTime(endTime - node.startTime))
|
return convertTime(endTime - node.startTime)
|
||||||
}, [node])
|
}, [node])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import './Trace.css'
|
|||||||
import { SpanEntity } from '@mcp-trace/trace-core'
|
import { SpanEntity } from '@mcp-trace/trace-core'
|
||||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||||
import { Divider } from 'antd/lib'
|
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 { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { findNodeById, mergeTraceModals, updatePercentAndStart } from '../utils'
|
||||||
import { Box, GridItem, SimpleGrid, Text, VStack } from './Component'
|
import { Box, GridItem, SimpleGrid, Text, VStack } from './Component'
|
||||||
import SpanDetail from './SpanDetail'
|
import SpanDetail from './SpanDetail'
|
||||||
import TraceTree from './TraceTree'
|
import TraceTree from './TraceTree'
|
||||||
@@ -19,44 +20,12 @@ export interface TracePageProp {
|
|||||||
|
|
||||||
export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName, reload = false }) => {
|
export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName, reload = false }) => {
|
||||||
const [spans, setSpans] = useState<TraceModal[]>([])
|
const [spans, setSpans] = useState<TraceModal[]>([])
|
||||||
const [selectNode, setSelectNode] = useState<TraceModal | null>(null)
|
const [selectNodeId, setSelectNodeId] = useState<string | null>(null)
|
||||||
const [showList, setShowList] = useState(true)
|
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 intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const { t } = useTranslation()
|
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 getRootSpan = (spans: SpanEntity[]): TraceModal[] => {
|
||||||
const map: Map<string, TraceModal> = new Map()
|
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 getTraceData = useCallback(async (): Promise<boolean> => {
|
||||||
const datas = topicId && traceId ? await window.api.trace.getData(topicId, traceId, modelName) : []
|
const datas = topicId && traceId ? await window.api.trace.getData(topicId, traceId, modelName) : []
|
||||||
const matchedSpans = getRootSpan(datas)
|
const matchedSpans = getRootSpan(datas)
|
||||||
@@ -96,19 +54,14 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
|
|||||||
setSpans((prev) => mergeTraceModals(prev, matchedSpans))
|
setSpans((prev) => mergeTraceModals(prev, matchedSpans))
|
||||||
const isEnded = !matchedSpans.find((e) => !e.endTime || e.endTime <= 0)
|
const isEnded = !matchedSpans.find((e) => !e.endTime || e.endTime <= 0)
|
||||||
return isEnded
|
return isEnded
|
||||||
}, [topicId, traceId, modelName, updatePercentAndStart, mergeTraceModals])
|
}, [topicId, traceId, modelName])
|
||||||
|
|
||||||
const handleNodeClick = (nodeId: string) => {
|
const handleNodeClick = (nodeId: string) => {
|
||||||
const latestNode = findNodeById(spans, nodeId)
|
setSelectNodeId(nodeId)
|
||||||
if (latestNode) {
|
|
||||||
setSelectNode(latestNode)
|
|
||||||
setShowList(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShowList = () => {
|
const handleShowList = () => {
|
||||||
setShowList(true)
|
setSelectNodeId(null)
|
||||||
setSelectNode(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -138,18 +91,6 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
|
|||||||
}
|
}
|
||||||
}, [getTraceData, traceId, topicId, reload])
|
}, [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 (
|
return (
|
||||||
<div className="trace-window">
|
<div className="trace-window">
|
||||||
<div className="tab-container_trace">
|
<div className="tab-container_trace">
|
||||||
|
|||||||
45
src/renderer/src/trace/utils/index.ts
Normal file
45
src/renderer/src/trace/utils/index.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -33,15 +33,15 @@ const Footer: FC<FooterProps> = ({
|
|||||||
onEsc()
|
onEsc()
|
||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys('c', () => {
|
|
||||||
handleCopy()
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
if (loading || !onCopy) return
|
if (loading || !onCopy) return
|
||||||
onCopy()
|
onCopy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('c', () => {
|
||||||
|
handleCopy()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WindowFooter className="drag">
|
<WindowFooter className="drag">
|
||||||
<FooterText>
|
<FooterText>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
|||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { Input as AntdInput } from 'antd'
|
import { Input as AntdInput } from 'antd'
|
||||||
import { InputRef } from 'rc-input/lib/interface'
|
import { InputRef } from 'rc-input/lib/interface'
|
||||||
import React, { useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface InputBarProps {
|
interface InputBarProps {
|
||||||
@@ -27,9 +27,13 @@ const InputBar = ({
|
|||||||
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||||
const inputRef = useRef<InputRef>(null)
|
const inputRef = useRef<InputRef>(null)
|
||||||
const { setTimeoutTimer } = useTimer()
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
|
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputWrapper ref={ref}>
|
<InputWrapper ref={ref}>
|
||||||
{assistant.model && <ModelAvatar model={assistant.model} size={30} />}
|
{assistant.model && <ModelAvatar model={assistant.model} size={30} />}
|
||||||
|
|||||||
@@ -36,32 +36,38 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
|||||||
const [isContented, setIsContented] = useState(false)
|
const [isContented, setIsContented] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [contentToCopy, setContentToCopy] = useState('')
|
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)
|
const initialized = useRef(false)
|
||||||
|
|
||||||
// Use useRef for values that shouldn't trigger re-renders
|
// Use useRef for values that shouldn't trigger re-renders
|
||||||
const assistantRef = useRef<Assistant | null>(null)
|
const assistantRef = useRef<Assistant | null>(null)
|
||||||
const topicRef = useRef<Topic | null>(null)
|
const topicRef = useRef<Topic | null>(topic)
|
||||||
const promptContentRef = useRef('')
|
const promptContentRef = useRef('')
|
||||||
const askId = useRef('')
|
const askId = useRef('')
|
||||||
|
|
||||||
|
// Sync refs
|
||||||
|
useEffect(() => {
|
||||||
|
assistantRef.current = assistant
|
||||||
|
}, [assistant])
|
||||||
|
useEffect(() => {
|
||||||
|
topicRef.current = topic
|
||||||
|
}, [assistant, topic])
|
||||||
|
|
||||||
// Initialize values only once when action changes
|
// Initialize values only once when action changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialized.current) return
|
if (initialized.current) return
|
||||||
initialized.current = true
|
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
|
// Initialize prompt content
|
||||||
let userContent = ''
|
let userContent = ''
|
||||||
switch (action.id) {
|
switch (action.id) {
|
||||||
@@ -128,7 +134,7 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
|||||||
fetchResult()
|
fetchResult()
|
||||||
}, [fetchResult])
|
}, [fetchResult])
|
||||||
|
|
||||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
const allMessages = useTopicMessages(topic?.id || '')
|
||||||
|
|
||||||
// Memoize the messages to prevent unnecessary re-renders
|
// Memoize the messages to prevent unnecessary re-renders
|
||||||
const messageContent = useMemo(() => {
|
const messageContent = useMemo(() => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import useTranslate from '@renderer/hooks/useTranslate'
|
import useTranslate from '@renderer/hooks/useTranslate'
|
||||||
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||||
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
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 type { ActionItem } from '@renderer/types/selectionTypes'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { abortCompletion } from '@renderer/utils/abortController'
|
import { abortCompletion } from '@renderer/utils/abortController'
|
||||||
@@ -31,7 +31,7 @@ const logger = loggerService.withContext('ActionTranslate')
|
|||||||
|
|
||||||
const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { translateModelPrompt, language } = useSettings()
|
const { language } = useSettings()
|
||||||
|
|
||||||
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
|
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
|
||||||
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
|
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
|
||||||
@@ -41,14 +41,20 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
|||||||
const [isContented, setIsContented] = useState(false)
|
const [isContented, setIsContented] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [contentToCopy, setContentToCopy] = useState('')
|
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()
|
const { getLanguageByLangcode } = useTranslate()
|
||||||
|
|
||||||
// Use useRef for values that shouldn't trigger re-renders
|
// Use useRef for values that shouldn't trigger re-renders
|
||||||
const initialized = useRef(false)
|
const topicRef = useRef<Topic | null>(topic)
|
||||||
const assistantRef = useRef<Assistant | null>(null)
|
|
||||||
const topicRef = useRef<Topic | null>(null)
|
|
||||||
const askId = useRef('')
|
const askId = useRef('')
|
||||||
|
|
||||||
|
// Sync ref
|
||||||
|
useEffect(() => {
|
||||||
|
topicRef.current = topic
|
||||||
|
}, [topic])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
|
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
|
||||||
@@ -79,22 +85,8 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
|||||||
})
|
})
|
||||||
}, [getLanguageByLangcode, language])
|
}, [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 () => {
|
const fetchResult = useCallback(async () => {
|
||||||
if (!assistantRef.current || !topicRef.current || !action.selectedText) return
|
if (!topicRef.current || !action.selectedText) return
|
||||||
|
|
||||||
const setAskId = (id: string) => {
|
const setAskId = (id: string) => {
|
||||||
askId.current = id
|
askId.current = id
|
||||||
@@ -112,8 +104,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
|||||||
setError(error.message)
|
setError(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
let sourceLanguageCode: TranslateLanguageCode
|
let sourceLanguageCode: TranslateLanguageCode
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -139,7 +129,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
|
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
|
||||||
assistantRef.current = assistant
|
|
||||||
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
|
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
|
||||||
}, [action, targetLanguage, alterLanguage, scrollToBottom])
|
}, [action, targetLanguage, alterLanguage, scrollToBottom])
|
||||||
|
|
||||||
@@ -147,7 +136,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
|||||||
fetchResult()
|
fetchResult()
|
||||||
}, [fetchResult])
|
}, [fetchResult])
|
||||||
|
|
||||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
const allMessages = useTopicMessages(topic?.id || '')
|
||||||
|
|
||||||
const messageContent = useMemo(() => {
|
const messageContent = useMemo(() => {
|
||||||
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')
|
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')
|
||||||
|
|||||||
@@ -30,19 +30,6 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
|
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const { setTimeoutTimer } = useTimer()
|
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(() => {
|
useEffect(() => {
|
||||||
hideTimerRef.current = setTimeout(() => {
|
hideTimerRef.current = setTimeout(() => {
|
||||||
setIsShowMe(false)
|
setIsShowMe(false)
|
||||||
@@ -68,21 +55,6 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys('c', () => {
|
|
||||||
showMePeriod()
|
|
||||||
handleCopy()
|
|
||||||
})
|
|
||||||
|
|
||||||
useHotkeys('r', () => {
|
|
||||||
showMePeriod()
|
|
||||||
handleRegenerate()
|
|
||||||
})
|
|
||||||
|
|
||||||
useHotkeys('esc', () => {
|
|
||||||
showMePeriod()
|
|
||||||
handleEsc()
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleEsc = () => {
|
const handleEsc = () => {
|
||||||
setIsEscHovered(true)
|
setIsEscHovered(true)
|
||||||
setTimeoutTimer(
|
setTimeoutTimer(
|
||||||
@@ -100,6 +72,11 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('esc', () => {
|
||||||
|
showMePeriod()
|
||||||
|
handleEsc()
|
||||||
|
})
|
||||||
|
|
||||||
const handleRegenerate = () => {
|
const handleRegenerate = () => {
|
||||||
setIsRegenerateHovered(true)
|
setIsRegenerateHovered(true)
|
||||||
setTimeoutTimer(
|
setTimeoutTimer(
|
||||||
@@ -126,6 +103,11 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('r', () => {
|
||||||
|
showMePeriod()
|
||||||
|
handleRegenerate()
|
||||||
|
})
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
if (!content || loading) return
|
if (!content || loading) return
|
||||||
|
|
||||||
@@ -147,6 +129,11 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('c', () => {
|
||||||
|
showMePeriod()
|
||||||
|
handleCopy()
|
||||||
|
})
|
||||||
|
|
||||||
const handleWindowFocus = () => {
|
const handleWindowFocus = () => {
|
||||||
setIsWindowFocus(true)
|
setIsWindowFocus(true)
|
||||||
}
|
}
|
||||||
@@ -155,6 +142,19 @@ const WindowFooter: FC<FooterProps> = ({
|
|||||||
setIsWindowFocus(false)
|
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 (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onMouseEnter={() => setIsContainerHovered(true)}
|
onMouseEnter={() => setIsContainerHovered(true)}
|
||||||
|
|||||||
@@ -204,31 +204,6 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
|||||||
}
|
}
|
||||||
}, [setTimeoutTimer])
|
}, [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) => {
|
const handleSearch = (action: ActionItem) => {
|
||||||
if (!action.searchEngine) return
|
if (!action.searchEngine) return
|
||||||
|
|
||||||
@@ -256,6 +231,31 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
|||||||
window.api?.selection.hideToolbar()
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<LogoWrapper $draggable={!demo}>
|
<LogoWrapper $draggable={!demo}>
|
||||||
|
|||||||
146
yarn.lock
146
yarn.lock
@@ -2098,6 +2098,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@babel/core@npm:^7.27.7":
|
||||||
version: 7.28.0
|
version: 7.28.0
|
||||||
resolution: "@babel/core@npm:7.28.0"
|
resolution: "@babel/core@npm:7.28.0"
|
||||||
@@ -2134,6 +2157,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@babel/helper-compilation-targets@npm:^7.27.2":
|
||||||
version: 7.27.2
|
version: 7.27.2
|
||||||
resolution: "@babel/helper-compilation-targets@npm:7.27.2"
|
resolution: "@babel/helper-compilation-targets@npm:7.27.2"
|
||||||
@@ -2177,6 +2213,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@babel/helper-plugin-utils@npm:^7.27.1":
|
||||||
version: 7.27.1
|
version: 7.27.1
|
||||||
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
|
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
|
||||||
@@ -2215,6 +2264,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@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
|
version: 7.28.0
|
||||||
resolution: "@babel/parser@npm:7.28.0"
|
resolution: "@babel/parser@npm:7.28.0"
|
||||||
@@ -2284,6 +2354,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@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
|
version: 7.28.1
|
||||||
resolution: "@babel/types@npm:7.28.1"
|
resolution: "@babel/types@npm:7.28.1"
|
||||||
@@ -2304,6 +2389,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@bcoe/v8-coverage@npm:^1.0.2":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "@bcoe/v8-coverage@npm:1.0.2"
|
resolution: "@bcoe/v8-coverage@npm:1.0.2"
|
||||||
@@ -5998,7 +6093,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@jridgewell/remapping@npm:^2.3.4":
|
"@jridgewell/remapping@npm:^2.3.4, @jridgewell/remapping@npm:^2.3.5":
|
||||||
version: 2.3.5
|
version: 2.3.5
|
||||||
resolution: "@jridgewell/remapping@npm:2.3.5"
|
resolution: "@jridgewell/remapping@npm:2.3.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14113,7 +14208,7 @@ __metadata:
|
|||||||
eslint: "npm:^9.22.0"
|
eslint: "npm:^9.22.0"
|
||||||
eslint-plugin-import-zod: "npm:^1.2.0"
|
eslint-plugin-import-zod: "npm:^1.2.0"
|
||||||
eslint-plugin-oxlint: "npm:^1.15.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-simple-import-sort: "npm:^12.1.1"
|
||||||
eslint-plugin-unused-imports: "npm:^4.1.4"
|
eslint-plugin-unused-imports: "npm:^4.1.4"
|
||||||
express: "npm:^5.1.0"
|
express: "npm:^5.1.0"
|
||||||
@@ -18225,6 +18320,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"eslint-plugin-react-naming-convention@npm:1.48.1":
|
||||||
version: 1.48.1
|
version: 1.48.1
|
||||||
resolution: "eslint-plugin-react-naming-convention@npm:1.48.1"
|
resolution: "eslint-plugin-react-naming-convention@npm:1.48.1"
|
||||||
@@ -20005,6 +20115,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"hls-video-element@npm:^1.5.6":
|
||||||
version: 1.5.7
|
version: 1.5.7
|
||||||
resolution: "hls-video-element@npm:1.5.7"
|
resolution: "hls-video-element@npm:1.5.7"
|
||||||
@@ -30251,6 +30377,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"zod-validation-error@npm:^3.4.0":
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
resolution: "zod-validation-error@npm:3.4.0"
|
resolution: "zod-validation-error@npm:3.4.0"
|
||||||
@@ -30260,6 +30395,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.1":
|
||||||
version: 3.25.56
|
version: 3.25.56
|
||||||
resolution: "zod@npm:3.25.56"
|
resolution: "zod@npm:3.25.56"
|
||||||
|
|||||||
Reference in New Issue
Block a user