Compare commits
22 Commits
feat/route
...
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,
|
||||
tseslint.configs.recommended,
|
||||
eslintReact.configs['recommended-typescript'],
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactHooks.configs.flat.recommended,
|
||||
{
|
||||
plugins: {
|
||||
'simple-import-sort': simpleImportSort,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)' }} />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
useHotkeys('c', () => {
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
const handleCopy = () => {
|
||||
if (loading || !onCopy) return
|
||||
onCopy()
|
||||
}
|
||||
|
||||
useHotkeys('c', () => {
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
return (
|
||||
<WindowFooter className="drag">
|
||||
<FooterText>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
146
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user