Merge branch 'feat/ocr' into feat/ocr-translate

This commit is contained in:
icarus 2025-08-24 18:58:21 +08:00
commit 222b3bacab
32 changed files with 280 additions and 98 deletions

View File

@ -228,6 +228,7 @@
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
"react-hotkeys-hook": "^4.6.1", "react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",

View File

@ -68,7 +68,11 @@ class CopilotService {
constructor() { constructor() {
this.tokenFilePath = path.join(app.getPath('userData'), '.copilot_token') this.tokenFilePath = path.join(app.getPath('userData'), '.copilot_token')
this.headers = { ...CONFIG.DEFAULT_HEADERS } this.headers = {
...CONFIG.DEFAULT_HEADERS,
accept: 'application/json',
'user-agent': 'Visual Studio Code (desktop)'
}
} }
/** /**
@ -93,6 +97,7 @@ class CopilotService {
'Sec-Fetch-Site': 'none', 'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Dest': 'empty',
accept: 'application/json',
authorization: `token ${token}` authorization: `token ${token}`
} }
}) })

View File

@ -204,7 +204,7 @@ export function registerShortcuts(window: BrowserWindow) {
selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut) selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
break break
//the following ZOOMs will register shortcuts seperately, so will return //the following ZOOMs will register shortcuts separately, so will return
case 'zoom_in': case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window)) globalShortcut.register('CommandOrControl+=', () => handler(window))
globalShortcut.register('CommandOrControl+numadd', () => handler(window)) globalShortcut.register('CommandOrControl+numadd', () => handler(window))

View File

@ -555,9 +555,9 @@ export class WindowService {
// [Windows] hacky fix // [Windows] hacky fix
// the window is minimized only when in Windows platform // the window is minimized only when in Windows platform
// because it's a workround for Windows, see `hideMiniWindow()` // because it's a workaround for Windows, see `hideMiniWindow()`
if (this.miniWindow?.isMinimized()) { if (this.miniWindow?.isMinimized()) {
// don't let the window being seen before we finish adusting the position across screens // don't let the window being seen before we finish adjusting the position across screens
this.miniWindow?.setOpacity(0) this.miniWindow?.setOpacity(0)
// DO NOT use `restore()` here, Electron has the bug with screens of different scale factor // DO NOT use `restore()` here, Electron has the bug with screens of different scale factor
// We have to use `show()` here, then set the position and bounds // We have to use `show()` here, then set the position and bounds

View File

@ -4,6 +4,7 @@ import { FC, useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter, Route, Routes } from 'react-router-dom'
import Sidebar from './components/app/Sidebar' import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import TabsContainer from './components/Tab/TabContainer' import TabsContainer from './components/Tab/TabContainer'
import NavigationHandler from './handler/NavigationHandler' import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useSettings' import { useNavbarPosition } from './hooks/useSettings'
@ -23,18 +24,20 @@ const Router: FC = () => {
const routes = useMemo(() => { const routes = useMemo(() => {
return ( return (
<Routes> <ErrorBoundary>
<Route path="/" element={<HomePage />} /> <Routes>
<Route path="/agents" element={<AgentsPage />} /> <Route path="/" element={<HomePage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} /> <Route path="/agents" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} /> <Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/files" element={<FilesPage />} /> <Route path="/translate" element={<TranslatePage />} />
<Route path="/knowledge" element={<KnowledgePage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/apps" element={<MinAppsPage />} /> <Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/code" element={<CodeToolsPage />} /> <Route path="/apps" element={<MinAppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} /> <Route path="/code" element={<CodeToolsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} /> <Route path="/settings/*" element={<SettingsPage />} />
</Routes> <Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
</ErrorBoundary>
) )
}, []) }, [])

View File

@ -157,6 +157,7 @@ const IconWrapper = styled.div<{ $isStreaming: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
width: 44px; width: 44px;
height: 44px; height: 44px;
background: ${(props) => background: ${(props) =>
@ -177,13 +178,16 @@ const TitleSection = styled.div`
gap: 6px; gap: 6px;
` `
const Title = styled.h3` const Title = styled.span`
margin: 0 !important; font-size: 14px;
font-size: 14px !important; font-weight: bold;
font-weight: 600; color: var(--color-text-1);
color: var(--color-text);
line-height: 1.4; line-height: 1.4;
font-family: 'Ubuntu'; font-family: 'Ubuntu';
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
` `
const TypeBadge = styled.div` const TypeBadge = styled.div`

View File

@ -1,7 +1,7 @@
import CodeEditor, { CodeEditorHandles } from '@renderer/components/CodeEditor' import CodeEditor, { CodeEditorHandles } from '@renderer/components/CodeEditor'
import { isLinux, isMac, isWin } from '@renderer/config/constant' import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import { Button, Modal, Splitter, Tooltip } from 'antd' import { Button, Modal, Splitter, Tooltip, Typography } from 'antd'
import { Code, Eye, Maximize2, Minimize2, SaveIcon, SquareSplitHorizontal, X } from 'lucide-react' import { Code, Eye, Maximize2, Minimize2, SaveIcon, SquareSplitHorizontal, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -43,7 +43,7 @@ const HtmlArtifactsPopup: React.FC<HtmlArtifactsPopupProps> = ({ open, title, ht
const renderHeader = () => ( const renderHeader = () => (
<ModalHeader onDoubleClick={() => setIsFullscreen(!isFullscreen)} className={classNames({ drag: isFullscreen })}> <ModalHeader onDoubleClick={() => setIsFullscreen(!isFullscreen)} className={classNames({ drag: isFullscreen })}>
<HeaderLeft $isFullscreen={isFullscreen}> <HeaderLeft $isFullscreen={isFullscreen}>
<TitleText>{title}</TitleText> <TitleText ellipsis={{ tooltip: true }}>{title}</TitleText>
</HeaderLeft> </HeaderLeft>
<HeaderCenter> <HeaderCenter>
@ -266,13 +266,13 @@ const HeaderRight = styled.div<{ $isFullscreen?: boolean }>`
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? (isWin ? '136px' : isLinux ? '120px' : '12px') : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? (isWin ? '136px' : isLinux ? '120px' : '12px') : '12px')};
` `
const TitleText = styled.span` const TitleText = styled(Typography.Text)`
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: bold;
color: var(--color-text); color: var(--color-text);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; width: 50%;
` `
const ViewControls = styled.div` const ViewControls = styled.div`

View File

@ -0,0 +1,57 @@
import { formatErrorMessage } from '@renderer/utils/error'
import { Alert, Button, Space } from 'antd'
import { ComponentType, ReactNode } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): ReactNode => {
const { t } = useTranslation()
const { error } = props
const debug = async () => {
await window.api.devTools.toggle()
}
const reload = async () => {
await window.api.reload()
}
return (
<ErrorContainer>
<Alert
message={t('error.boundary.default.message')}
showIcon
description={formatErrorMessage(error)}
type="error"
action={
<Space>
<Button size="small" danger onClick={debug}>
{t('error.boundary.default.devtools')}
</Button>
<Button size="small" danger onClick={reload}>
{t('error.boundary.default.reload')}
</Button>
</Space>
}
/>
</ErrorContainer>
)
}
const ErrorBoundaryCustomized = ({
children,
fallbackComponent
}: {
children: ReactNode
fallbackComponent?: ComponentType<FallbackProps>
}) => {
return <ErrorBoundary FallbackComponent={fallbackComponent ?? DefaultFallback}>{children}</ErrorBoundary>
}
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px;
`
export { ErrorBoundaryCustomized as ErrorBoundary }

View File

@ -166,7 +166,7 @@ export const SEARCH_SUMMARY_PROMPT = `
</knowledge> </knowledge>
\` \`
7. Follow up question: Based on knowledge, Fomula of Scaled Dot-Product Attention and Multi-Head Attention? 7. Follow up question: Based on knowledge, Formula of Scaled Dot-Product Attention and Multi-Head Attention?
Rephrased question: \` Rephrased question: \`
<websearch> <websearch>
<question> <question>
@ -279,7 +279,7 @@ export const SEARCH_SUMMARY_PROMPT_WEB_ONLY = `
</websearch> </websearch>
\` \`
7. Follow up question: Based on knowledge, Fomula of Scaled Dot-Product Attention and Multi-Head Attention? 7. Follow up question: Based on knowledge, Formula of Scaled Dot-Product Attention and Multi-Head Attention?
Rephrased question: \` Rephrased question: \`
<websearch> <websearch>
<question> <question>
@ -374,7 +374,7 @@ export const SEARCH_SUMMARY_PROMPT_KNOWLEDGE_ONLY = `
</knowledge> </knowledge>
\` \`
7. Follow up question: Based on knowledge, Fomula of Scaled Dot-Product Attention and Multi-Head Attention? 7. Follow up question: Based on knowledge, Formula of Scaled Dot-Product Attention and Multi-Head Attention?
Rephrased question: \` Rephrased question: \`
<knowledge> <knowledge>
<rewrite> <rewrite>

View File

@ -66,7 +66,7 @@ db.version(6).stores({
// --- NEW VERSION 7 --- // --- NEW VERSION 7 ---
db.version(7) db.version(7)
.stores({ .stores({
// Re-declare all tables for the new version // Redeclare all tables for the new version
files: 'id, name, origin_name, path, size, ext, type, created_at, count', files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id', // Correct index for topics topics: '&id', // Correct index for topics
settings: '&id, value', settings: '&id, value',
@ -79,7 +79,7 @@ db.version(7)
db.version(8) db.version(8)
.stores({ .stores({
// Re-declare all tables for the new version // Redeclare all tables for the new version
files: 'id, name, origin_name, path, size, ext, type, created_at, count', files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id', // Correct index for topics topics: '&id', // Correct index for topics
settings: '&id, value', settings: '&id, value',
@ -91,7 +91,7 @@ db.version(8)
.upgrade((tx) => upgradeToV8(tx)) .upgrade((tx) => upgradeToV8(tx))
db.version(9).stores({ db.version(9).stores({
// Re-declare all tables for the new version // Redeclare all tables for the new version
files: 'id, name, origin_name, path, size, ext, type, created_at, count', files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id', // Correct index for topics topics: '&id', // Correct index for topics
settings: '&id, value', settings: '&id, value',

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Backup file format error" "file_format": "Backup file format error"
}, },
"boundary": {
"default": {
"devtools": "Open debug panel",
"message": "It seems that something went wrong...",
"reload": "Reload"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "Returned an invalid data format" "non_json": "Returned an invalid data format"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "バックアップファイルの形式エラー" "file_format": "バックアップファイルの形式エラー"
}, },
"boundary": {
"default": {
"devtools": "デバッグパネルを開く",
"message": "何か問題が発生したようです...",
"reload": "再読み込み"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "無効なデータ形式が返されました" "non_json": "無効なデータ形式が返されました"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Ошибка формата файла резервной копии" "file_format": "Ошибка формата файла резервной копии"
}, },
"boundary": {
"default": {
"devtools": "Открыть панель отладки",
"message": "Похоже, возникла какая-то проблема...",
"reload": "Перезагрузить"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "Вернулся недопустимый формат данных" "non_json": "Вернулся недопустимый формат данных"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "备份文件格式错误" "file_format": "备份文件格式错误"
}, },
"boundary": {
"default": {
"devtools": "打开调试面板",
"message": "似乎出现了一些问题...",
"reload": "重新加载"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "返回了无效的数据格式" "non_json": "返回了无效的数据格式"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "備份檔案格式錯誤" "file_format": "備份檔案格式錯誤"
}, },
"boundary": {
"default": {
"devtools": "打開除錯面板",
"message": "似乎出現了一些問題...",
"reload": "重新載入"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "返回了無效的資料格式" "non_json": "返回了無效的資料格式"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Λάθος μορφή αρχείου που επιστρέφεται" "file_format": "Λάθος μορφή αρχείου που επιστρέφεται"
}, },
"boundary": {
"default": {
"devtools": "Άνοιγμα πίνακα αποσφαλμάτωσης",
"message": "Φαίνεται ότι προέκυψε κάποιο πρόβλημα...",
"reload": "Επαναφόρτωση"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων" "non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Formato de archivo de copia de seguridad incorrecto" "file_format": "Formato de archivo de copia de seguridad incorrecto"
}, },
"boundary": {
"default": {
"devtools": "Abrir el panel de depuración",
"message": "Parece que ha surgido un problema...",
"reload": "Recargar"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "Devuelve un formato de datos no válido" "non_json": "Devuelve un formato de datos no válido"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Le format du fichier de sauvegarde est incorrect" "file_format": "Le format du fichier de sauvegarde est incorrect"
}, },
"boundary": {
"default": {
"devtools": "Ouvrir le panneau de débogage",
"message": "Il semble que quelques problèmes soient survenus...",
"reload": "Recharger"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "a renvoyé un format de données invalide" "non_json": "a renvoyé un format de données invalide"

View File

@ -808,6 +808,13 @@
"backup": { "backup": {
"file_format": "Formato do arquivo de backup está incorreto" "file_format": "Formato do arquivo de backup está incorreto"
}, },
"boundary": {
"default": {
"devtools": "Abrir o painel de depuração",
"message": "Parece que ocorreu um problema...",
"reload": "Recarregar"
}
},
"chat": { "chat": {
"chunk": { "chunk": {
"non_json": "Devolveu um formato de dados inválido" "non_json": "Devolveu um formato de dados inválido"

View File

@ -1,3 +1,4 @@
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
@ -100,20 +101,24 @@ const HomePage: FC = () => {
)} )}
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}> <ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
{showAssistants && ( {showAssistants && (
<HomeTabs <ErrorBoundary>
activeAssistant={activeAssistant} <HomeTabs
activeTopic={activeTopic} activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant} activeTopic={activeTopic}
setActiveTopic={setActiveTopic} setActiveAssistant={setActiveAssistant}
position="left" setActiveTopic={setActiveTopic}
/> position="left"
/>
</ErrorBoundary>
)} )}
<Chat <ErrorBoundary>
assistant={activeAssistant} <Chat
activeTopic={activeTopic} assistant={activeAssistant}
setActiveTopic={setActiveTopic} activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant} setActiveTopic={setActiveTopic}
/> setActiveAssistant={setActiveAssistant}
/>
</ErrorBoundary>
</ContentContainer> </ContentContainer>
</Container> </Container>
) )

View File

@ -83,7 +83,7 @@ export const getFileIcon = (type?: string) => {
export const FileNameRender: FC<{ file: FileMetadata }> = ({ file }) => { export const FileNameRender: FC<{ file: FileMetadata }> = ({ file }) => {
const [visible, setVisible] = useState<boolean>(false) const [visible, setVisible] = useState<boolean>(false)
const isImage = (ext: string) => { const isImage = (ext: string) => {
return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext) return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext.toLocaleLowerCase())
} }
const fullName = FileManager.formatFileName(file) const fullName = FileManager.formatFileName(file)

View File

@ -28,48 +28,62 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const { providers } = useWebSearchProviders() const { providers } = useWebSearchProviders()
const { updateAssistant } = useAssistant(assistant.id) const { updateAssistant } = useAssistant(assistant.id)
// 注意assistant.enableWebSearch 有不同的语义
/** 表示是否启用网络搜索 */
const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch
const WebSearchIcon = useCallback( const WebSearchIcon = useCallback(
({ pid, size = 18 }: { pid?: WebSearchProviderId; size?: number }) => { ({ pid, size = 18, color }: { pid?: WebSearchProviderId; size?: number; color?: string }) => {
const iconColor = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
switch (pid) { switch (pid) {
case 'bocha': case 'bocha':
return <BochaLogo width={size} height={size} color={iconColor} /> return <BochaLogo width={size} height={size} color={color} />
case 'exa': case 'exa':
// size微调视觉上和其他图标平衡一些 // size微调视觉上和其他图标平衡一些
return <ExaLogo width={size - 2} height={size} color={iconColor} /> return <ExaLogo width={size - 2} height={size} color={color} />
case 'tavily': case 'tavily':
return <TavilyLogo width={size} height={size} color={iconColor} /> return <TavilyLogo width={size} height={size} color={color} />
case 'searxng': case 'searxng':
return <SearXNGLogo width={size} height={size} color={iconColor} /> return <SearXNGLogo width={size} height={size} color={color} />
case 'local-baidu': case 'local-baidu':
return <BaiduOutlined size={size} style={{ color: iconColor, fontSize: size }} /> return <BaiduOutlined size={size} style={{ color, fontSize: size }} />
case 'local-bing': case 'local-bing':
return <BingLogo width={size} height={size} color={iconColor} /> return <BingLogo width={size} height={size} color={color} />
case 'local-google': case 'local-google':
return <GoogleOutlined size={size} style={{ color: iconColor, fontSize: size }} /> return <GoogleOutlined size={size} style={{ color, fontSize: size }} />
default: default:
return <Globe size={size} style={{ color: iconColor, fontSize: size }} /> return <Globe size={size} style={{ color, fontSize: size }} />
} }
}, },
[enableWebSearch] [enableWebSearch]
) )
const updateSelectedWebSearchProvider = useCallback( const updateWebSearchProvider = useCallback(
async (providerId?: WebSearchProvider['id']) => { async (providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿 // TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
const currentWebSearchProviderId = assistant.webSearchProviderId
const newWebSearchProviderId = currentWebSearchProviderId === providerId ? undefined : providerId
startTransition(() => { startTransition(() => {
updateAssistant({ ...assistant, webSearchProviderId: newWebSearchProviderId, enableWebSearch: false }) updateAssistant({
...assistant,
webSearchProviderId: providerId,
enableWebSearch: false
})
}) })
}, },
[assistant, updateAssistant] [assistant, updateAssistant]
) )
const updateSelectedWebSearchBuiltin = useCallback(async () => { const updateQuickPanelItem = useCallback(
async (providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
if (providerId === assistant.webSearchProviderId) {
updateWebSearchProvider(undefined)
} else {
updateWebSearchProvider(providerId)
}
},
[assistant.webSearchProviderId, updateWebSearchProvider]
)
const updateToModelBuiltinWebSearch = useCallback(async () => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿 // TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
startTransition(() => { startTransition(() => {
updateAssistant({ ...assistant, webSearchProviderId: undefined, enableWebSearch: !assistant.enableWebSearch }) updateAssistant({ ...assistant, webSearchProviderId: undefined, enableWebSearch: !assistant.enableWebSearch })
@ -90,7 +104,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
icon: <WebSearchIcon size={13} pid={p.id} />, icon: <WebSearchIcon size={13} pid={p.id} />,
isSelected: p.id === assistant?.webSearchProviderId, isSelected: p.id === assistant?.webSearchProviderId,
disabled: !WebSearchService.isWebSearchEnabled(p.id), disabled: !WebSearchService.isWebSearchEnabled(p.id),
action: () => updateSelectedWebSearchProvider(p.id) action: () => updateQuickPanelItem(p.id)
})) }))
.filter((o) => !o.disabled) .filter((o) => !o.disabled)
@ -103,7 +117,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
icon: <Globe />, icon: <Globe />,
isSelected: assistant.enableWebSearch, isSelected: assistant.enableWebSearch,
disabled: !isWebSearchModelEnabled, disabled: !isWebSearchModelEnabled,
action: () => updateSelectedWebSearchBuiltin() action: () => updateToModelBuiltinWebSearch()
}) })
} }
@ -115,36 +129,18 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
assistant?.webSearchProviderId, assistant?.webSearchProviderId,
providers, providers,
t, t,
updateSelectedWebSearchBuiltin, updateQuickPanelItem,
updateSelectedWebSearchProvider updateToModelBuiltinWebSearch
]) ])
const openQuickPanel = useCallback(() => { const openQuickPanel = useCallback(() => {
if (assistant.webSearchProviderId) {
updateSelectedWebSearchProvider(undefined)
return
}
if (assistant.enableWebSearch) {
updateSelectedWebSearchBuiltin()
return
}
quickPanel.open({ quickPanel.open({
title: t('chat.input.web_search.label'), title: t('chat.input.web_search.label'),
list: providerItems, list: providerItems,
symbol: '?', symbol: '?',
pageSize: 9 pageSize: 9
}) })
}, [ }, [quickPanel, t, providerItems])
assistant.webSearchProviderId,
assistant.enableWebSearch,
quickPanel,
t,
providerItems,
updateSelectedWebSearchProvider,
updateSelectedWebSearchBuiltin
])
const handleOpenQuickPanel = useCallback(() => { const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '?') { if (quickPanel.isVisible && quickPanel.symbol === '?') {
@ -154,18 +150,28 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
} }
}, [openQuickPanel, quickPanel]) }, [openQuickPanel, quickPanel])
const onClick = useCallback(() => {
if (enableWebSearch) {
updateWebSearchProvider(undefined)
} else {
handleOpenQuickPanel()
}
}, [enableWebSearch, handleOpenQuickPanel, updateWebSearchProvider])
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
openQuickPanel openQuickPanel
})) }))
const color = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
return ( return (
<Tooltip <Tooltip
placement="top" placement="top"
title={enableWebSearch ? t('common.close') : t('chat.input.web_search.label')} title={enableWebSearch ? t('common.close') : t('chat.input.web_search.label')}
mouseLeaveDelay={0} mouseLeaveDelay={0}
arrow> arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}> <ToolbarButton type="text" onClick={onClick}>
<WebSearchIcon pid={assistant.webSearchProviderId} /> <WebSearchIcon color={color} pid={assistant.webSearchProviderId} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

@ -1,4 +1,5 @@
// import { loggerService } from '@logger' // import { loggerService } from '@logger'
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { isBuiltinOcrProvider, OcrProvider } from '@renderer/types' import { isBuiltinOcrProvider, OcrProvider } from '@renderer/types'
import { getOcrProviderLogo } from '@renderer/utils/ocr' import { getOcrProviderLogo } from '@renderer/utils/ocr'
import { Avatar, Divider, Flex } from 'antd' import { Avatar, Divider, Flex } from 'antd'
@ -35,7 +36,7 @@ const OcrProviderSettings = ({ provider }: Props) => {
</Flex> </Flex>
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <Divider style={{ width: '100%', margin: '10px 0' }} />
{getProviderSettings()} <ErrorBoundary>{getProviderSettings()}</ErrorBoundary>
</> </>
) )
} }

View File

@ -1,4 +1,5 @@
import { PictureOutlined } from '@ant-design/icons' import { PictureOutlined } from '@ant-design/icons'
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { OcrProvider } from '@renderer/types' import { OcrProvider } from '@renderer/types'
@ -26,7 +27,7 @@ const OcrSettings: FC = () => {
] ]
return ( return (
<> <ErrorBoundary>
<SettingGroup theme={themeMode}> <SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.tool.ocr.title')}</SettingTitle> <SettingTitle>{t('settings.tool.ocr.title')}</SettingTitle>
<SettingDivider /> <SettingDivider />
@ -35,7 +36,7 @@ const OcrSettings: FC = () => {
<SettingGroup theme={themeMode}> <SettingGroup theme={themeMode}>
<OcrProviderSettings provider={provider} /> <OcrProviderSettings provider={provider} />
</SettingGroup> </SettingGroup>
</> </ErrorBoundary>
) )
} }
export default OcrSettings export default OcrSettings

View File

@ -13,7 +13,6 @@ export const OcrTesseractSettings = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { provider } = useOcrProvider(BuiltinOcrProviderIds.tesseract) const { provider } = useOcrProvider(BuiltinOcrProviderIds.tesseract)
// TODO: use error boundary
if (!isOcrTesseractProvider(provider)) { if (!isOcrTesseractProvider(provider)) {
throw new Error('Not tesseract provider.') throw new Error('Not tesseract provider.')
} }

View File

@ -477,8 +477,9 @@ export async function fetchChatCompletion({
assistant.settings?.reasoning_effort !== undefined) || assistant.settings?.reasoning_effort !== undefined) ||
(isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model))) (isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model)))
// NOTEassistant.enableWebSearch 的语义是是否启用模型内置搜索功能
const enableWebSearch = const enableWebSearch =
(assistant.enableWebSearch && isWebSearchModel(model)) || (assistant.webSearchProviderId && isWebSearchModel(model)) ||
isOpenRouterBuiltInWebSearchModel(model) || isOpenRouterBuiltInWebSearchModel(model) ||
model.id.includes('sonar') || model.id.includes('sonar') ||
false false

View File

@ -185,7 +185,7 @@ export function mcpToolsToAnthropicTools(mcpTools: MCPTool[]): Array<ToolUnion>
const t: ToolUnion = { const t: ToolUnion = {
name: tool.id, name: tool.id,
description: tool.description, description: tool.description,
// @ts-ignore ignore type as it it unknow // @ts-ignore ignore type as it it unknown
input_schema: tool.inputSchema input_schema: tool.inputSchema
} }
return t return t

View File

@ -53,7 +53,7 @@ export const detectLanguage = async (inputText: string): Promise<TranslateLangua
} }
const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguageCode> => { const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguageCode> => {
logger.info('Detect langugage by llm') logger.info('Detect language by llm')
let detectedLang = '' let detectedLang = ''
await fetchLanguageDetection({ await fetchLanguageDetection({
text: sliceByTokens(inputText, 0, 100), text: sliceByTokens(inputText, 0, 100),
@ -65,7 +65,7 @@ const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguage
} }
const detectLanguageByFranc = (inputText: string): TranslateLanguageCode => { const detectLanguageByFranc = (inputText: string): TranslateLanguageCode => {
logger.info('Detect langugage by franc') logger.info('Detect language by franc')
const iso3 = franc(inputText) const iso3 = franc(inputText)
const isoMap: Record<string, TranslateLanguage> = { const isoMap: Record<string, TranslateLanguage> = {

View File

@ -21,7 +21,7 @@ import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { defaultLanguage } from '@shared/config/constant' import { defaultLanguage } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Divider } from 'antd' import { Divider } from 'antd'
import { isEmpty } from 'lodash' import { cloneDeep, isEmpty } from 'lodash'
import { last } from 'lodash' import { last } from 'lodash'
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -256,9 +256,19 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
setIsFirstMessage(false) setIsFirstMessage(false)
setUserInputText('') setUserInputText('')
const newAssistant = cloneDeep(currentAssistant)
if (!newAssistant.settings) {
newAssistant.settings = {}
}
newAssistant.settings.streamOutput = true
// 显式关闭这些功能
// newAssistant.webSearchProviderId = undefined
newAssistant.mcpServers = undefined
// newAssistant.knowledge_bases = undefined
await fetchChatCompletion({ await fetchChatCompletion({
messages: messagesForContext, messages: messagesForContext,
assistant: { ...currentAssistant, settings: { streamOutput: true } }, assistant: newAssistant,
onChunkReceived: (chunk: Chunk) => { onChunkReceived: (chunk: Chunk) => {
switch (chunk.type) { switch (chunk.type) {
case ChunkType.THINKING_START: case ChunkType.THINKING_START:

View File

@ -1,4 +1,5 @@
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import CopyButton from '@renderer/components/CopyButton' import CopyButton from '@renderer/components/CopyButton'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
@ -21,6 +22,7 @@ import styled from 'styled-components'
import { processMessages } from './ActionUtils' import { processMessages } from './ActionUtils'
import WindowFooter from './WindowFooter' import WindowFooter from './WindowFooter'
const logger = loggerService.withContext('ActionGeneral')
interface Props { interface Props {
action: ActionItem action: ActionItem
scrollToBottom?: () => void scrollToBottom?: () => void
@ -112,6 +114,7 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
} }
if (!assistantRef.current || !topicRef.current) return if (!assistantRef.current || !topicRef.current) return
logger.debug('Before peocess message', { assistant: assistantRef.current })
processMessages( processMessages(
assistantRef.current, assistantRef.current,
topicRef.current, topicRef.current,

View File

@ -10,6 +10,7 @@ import { Chunk, ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage' import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import { formatErrorMessage, isAbortError } from '@renderer/utils/error' import { formatErrorMessage, isAbortError } from '@renderer/utils/error'
import { createErrorBlock, createMainTextBlock, createThinkingBlock } from '@renderer/utils/messageUtils/create' import { createErrorBlock, createMainTextBlock, createThinkingBlock } from '@renderer/utils/messageUtils/create'
import { cloneDeep } from 'lodash'
const logger = loggerService.withContext('ActionUtils') const logger = loggerService.withContext('ActionUtils')
@ -53,9 +54,19 @@ export const processMessages = async (
let finished = false let finished = false
const newAssistant = cloneDeep(assistant)
if (!newAssistant.settings) {
newAssistant.settings = {}
}
newAssistant.settings.streamOutput = true
// 显式关闭这些功能
newAssistant.webSearchProviderId = undefined
newAssistant.mcpServers = undefined
// newAssistant.knowledge_bases = undefined
await fetchChatCompletion({ await fetchChatCompletion({
messages: [userMessage], messages: [userMessage],
assistant: { ...assistant, settings: { streamOutput: true } }, assistant: newAssistant,
onChunkReceived: (chunk: Chunk) => { onChunkReceived: (chunk: Chunk) => {
if (finished) { if (finished) {
return return

View File

@ -8538,6 +8538,7 @@ __metadata:
proxy-agent: "npm:^6.5.0" proxy-agent: "npm:^6.5.0"
react: "npm:^19.0.0" react: "npm:^19.0.0"
react-dom: "npm:^19.0.0" react-dom: "npm:^19.0.0"
react-error-boundary: "npm:^6.0.0"
react-hotkeys-hook: "npm:^4.6.1" react-hotkeys-hook: "npm:^4.6.1"
react-i18next: "npm:^14.1.2" react-i18next: "npm:^14.1.2"
react-infinite-scroll-component: "npm:^6.1.0" react-infinite-scroll-component: "npm:^6.1.0"
@ -19057,6 +19058,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-error-boundary@npm:^6.0.0":
version: 6.0.0
resolution: "react-error-boundary@npm:6.0.0"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
react: ">=16.13.1"
checksum: 10c0/1914d600dee95a14f14af4afe9867b0d35c26c4f7826d23208800ba2a99728659029aad60a6ef95e13430b4d79c2c4c9b3585f50bf508450478760d2e4e732d8
languageName: node
linkType: hard
"react-hotkeys-hook@npm:^4.6.1": "react-hotkeys-hook@npm:^4.6.1":
version: 4.6.2 version: 4.6.2
resolution: "react-hotkeys-hook@npm:4.6.2" resolution: "react-hotkeys-hook@npm:4.6.2"