Compare commits

...

6 Commits

Author SHA1 Message Date
kangfenmao
d1cd453ca4 chore: release v1.5.7 2025-08-22 12:47:47 +08:00
Chen Tao
ca3c9927d2 fix: knowledge encrypted (#9385) 2025-08-22 12:44:57 +08:00
Phantom
8e82f54c2b fix(translate): fix translating state management (#9387)
* fix(translate): 修复翻译状态管理逻辑

调整翻译状态设置的位置,确保在翻译开始和结束时正确更新状态

* fix(translate): 添加缺失的setTranslating属性

* fix(translate): 去除检测语言结果中的空格

检测语言返回的结果可能包含多余空格,导致后续处理出现问题。通过trim()去除前后空格确保结果干净
2025-08-22 12:44:51 +08:00
kangfenmao
ea797ac88a feat(DraggableList): add listProps support for custom list configurations
- Enhanced DraggableList component to accept listProps, allowing for customization of the Ant Design List component.
- Updated MCPSettings to utilize the new listProps feature, providing a custom empty state message when no servers are available.
2025-08-22 12:44:41 +08:00
Phantom
fe097a937c feat: search translate history (#9342)
* feat(翻译历史): 添加搜索翻译历史UI

在翻译历史页面添加搜索框

* feat(翻译历史): 优化搜索功能并添加延迟渲染

- 将搜索逻辑提取为独立函数并使用useDeferredValue优化性能
- 重构类型命名和状态管理
- 格式化日期显示并移入memo计算

* feat(i18n): 为翻译历史添加搜索框占位文本

* refactor(translate): 移除未使用的InputRef引用和inputRef变量
2025-08-22 12:44:17 +08:00
亢奋猫
1ac32bad14 refactor(CodeToolsPage): streamline CLI tool management and enhance p… (#9386)
* refactor(CodeToolsPage): streamline CLI tool management and enhance provider filtering logic

- Removed hardcoded CLI tool options and supported providers, replacing them with imported constants for better maintainability.
- Optimized provider filtering to include additional providers for Claude and Gemini tools.
- Updated environment variable handling for CLI tools to utilize a centralized API base URL function.

* refactor(CodeToolsPage): enhance CLI tool management and environment variable handling

- Updated provider filtering logic to utilize a centralized mapping for CLI tools, improving maintainability and extensibility.
- Refactored environment variable generation and parsing to streamline the launch process for different CLI tools.
- Simplified state management for tool selection and directory handling, enhancing code clarity.
2025-08-22 12:43:53 +08:00
19 changed files with 317 additions and 160 deletions

View File

@@ -118,6 +118,7 @@ releaseInfo:
releaseNotes: |
输入框快捷菜单增加清除按钮
侧边栏增加代码工具入口,代码工具增加环境变量设置
翻译列表增加搜索功能
小程序增加多语言显示
优化 MCP 服务器列表
新增 Web 搜索图标

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.5.7-rc.2",
"version": "1.5.7",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",

View File

@@ -91,7 +91,7 @@ export default abstract class BasePreprocessProvider {
}
public async readPdf(buffer: Buffer) {
const pdfDoc = await PDFDocument.load(buffer)
const pdfDoc = await PDFDocument.load(buffer, { ignoreEncryption: true })
return {
numPages: pdfDoc.getPageCount()
}

View File

@@ -9,13 +9,14 @@ import {
ResponderProvided
} from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { List } from 'antd'
import { List, ListProps } from 'antd'
import { FC } from 'react'
interface Props<T> {
list: T[]
style?: React.CSSProperties
listStyle?: React.CSSProperties
listProps?: ListProps<T>
children: (item: T, index: number) => React.ReactNode
onUpdate: (list: T[]) => void
onDragStart?: OnDragStartResponder
@@ -28,6 +29,7 @@ const DraggableList: FC<Props<any>> = ({
list,
style,
listStyle,
listProps,
droppableProps,
onDragStart,
onUpdate,
@@ -51,6 +53,7 @@ const DraggableList: FC<Props<any>> = ({
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
<List
{...listProps}
dataSource={list}
renderItem={(item, index) => {
const id = item.id || item

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Failed to save translation history"
},
"search": {
"placeholder": "Search translation history"
},
"title": "Translation History"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "保存翻訳履歴に失敗しました"
},
"search": {
"placeholder": "翻訳履歴を検索する"
},
"title": "翻訳履歴"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Не удалось сохранить историю переводов"
},
"search": {
"placeholder": "Поиск истории переводов"
},
"title": "История переводов"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "保存翻译历史失败"
},
"search": {
"placeholder": "搜索翻译历史"
},
"title": "翻译历史"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "保存翻譯歷史失敗"
},
"search": {
"placeholder": "搜索翻譯歷史"
},
"title": "翻譯歷史"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων"
},
"search": {
"placeholder": "Αναζήτηση ιστορικού μεταφράσεων"
},
"title": "Ιστορικό μετάφρασης"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Error al guardar el historial de traducciones"
},
"search": {
"placeholder": "Historial de búsqueda de traducción"
},
"title": "Historial de traducciones"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Échec de la sauvegarde de l'historique des traductions"
},
"search": {
"placeholder": "Rechercher l'historique des traductions"
},
"title": "Historique des traductions"
},
"input": {

View File

@@ -3723,6 +3723,9 @@
"error": {
"save": "Falha ao guardar o histórico de traduções"
},
"search": {
"placeholder": "Pesquisar histórico de tradução"
},
"title": "Histórico de Tradução"
},
"input": {

View File

@@ -11,22 +11,19 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled } from '@renderer/store/mcp'
import { Model } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
import { Alert, Button, Checkbox, Input, Select, Space } from 'antd'
import { Download, Terminal, X } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
// CLI 工具选项
const CLI_TOOLS = [
{ value: codeTools.qwenCode, label: 'Qwen Code' },
{ value: codeTools.claudeCode, label: 'Claude Code' },
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
]
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
import {
CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS,
CLI_TOOL_PROVIDER_MAP,
CLI_TOOLS,
generateToolEnvironment,
parseEnvironmentVariables
} from '.'
const logger = loggerService.withContext('CodeToolsPage')
@@ -51,25 +48,17 @@ const CodeToolsPage: FC = () => {
} = useCodeTools()
const { setTimeoutTimer } = useTimer()
// 状态管理
const [isLaunching, setIsLaunching] = useState(false)
const [isInstallingBun, setIsInstallingBun] = useState(false)
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
const handleCliToolChange = (value: codeTools) => setCliTool(value)
const openAiCompatibleProviders = providers.filter((p) => p.type.includes('openai'))
const openAiProviders = providers.filter((p) => p.id === 'openai')
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
const modelPredicate = useCallback(
(m: Model) => {
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
return false
}
if (selectedCliTool === 'claude-code') {
return m.id.includes('claude')
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
}
if (selectedCliTool === 'gemini-cli') {
return m.id.includes('gemini')
@@ -80,20 +69,9 @@ const CodeToolsPage: FC = () => {
)
const availableProviders = useMemo(() => {
if (selectedCliTool === codeTools.claudeCode) {
return claudeProviders
}
if (selectedCliTool === codeTools.geminiCli) {
return geminiProviders
}
if (selectedCliTool === codeTools.qwenCode) {
return openAiCompatibleProviders
}
if (selectedCliTool === codeTools.openaiCodex) {
return openAiProviders
}
return []
}, [claudeProviders, geminiProviders, openAiCompatibleProviders, openAiProviders, selectedCliTool])
const filterFn = CLI_TOOL_PROVIDER_MAP[selectedCliTool]
return filterFn ? filterFn(providers) : []
}, [providers, selectedCliTool])
const handleModelChange = (value: string) => {
if (!value) {
@@ -111,25 +89,6 @@ const CodeToolsPage: FC = () => {
}
}
// 处理环境变量更改
const handleEnvVarsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEnvVars(e.target.value)
}
// 处理文件夹选择
const handleFolderSelect = async () => {
try {
await selectFolder()
} catch (error) {
logger.error('选择文件夹失败:', error as Error)
}
}
// 处理目录选择
const handleDirectoryChange = (value: string) => {
setCurrentDir(value)
}
// 处理删除目录
const handleRemoveDirectory = (directory: string, e: React.MouseEvent) => {
e.stopPropagation()
@@ -170,104 +129,74 @@ const CodeToolsPage: FC = () => {
}
}
// 处理启动
const handleLaunch = async () => {
// 验证启动条件
const validateLaunch = (): { isValid: boolean; message?: string } => {
if (!canLaunch || !isBunInstalled) {
if (!isBunInstalled) {
window.message.warning({
content: t('code.launch.bun_required'),
key: 'code-launch-message'
})
} else {
window.message.warning({
content: t('code.launch.validation_error'),
key: 'code-launch-message'
})
return {
isValid: false,
message: !isBunInstalled ? t('code.launch.bun_required') : t('code.launch.validation_error')
}
return
}
setIsLaunching(true)
if (!selectedModel) {
window.message.error({
content: t('code.model_required'),
key: 'code-launch-message'
})
return
return { isValid: false, message: t('code.model_required') }
}
return { isValid: true }
}
// 准备启动环境
const prepareLaunchEnvironment = async (): Promise<Record<string, string> | null> => {
if (!selectedModel) return null
const modelProvider = getProviderByModel(selectedModel)
const aiProvider = new AiProvider(modelProvider)
const baseUrl = await aiProvider.getBaseURL()
const apiKey = await aiProvider.getApiKey()
let env: Record<string, string> = {}
if (selectedCliTool === codeTools.claudeCode) {
env = {
ANTHROPIC_API_KEY: apiKey,
ANTHROPIC_BASE_URL: modelProvider.apiHost,
ANTHROPIC_MODEL: selectedModel.id
}
// 生成工具特定的环境变量
const toolEnv = generateToolEnvironment({
tool: selectedCliTool,
model: selectedModel,
modelProvider,
apiKey,
baseUrl
})
// 合并用户自定义的环境变量
const userEnv = parseEnvironmentVariables(environmentVariables)
return { ...toolEnv, ...userEnv }
}
if (selectedCliTool === codeTools.geminiCli) {
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
const apiBaseUrl = modelProvider.apiHost + apiSuffix
env = {
GEMINI_API_KEY: apiKey,
GEMINI_BASE_URL: apiBaseUrl,
GOOGLE_GEMINI_BASE_URL: apiBaseUrl,
GEMINI_MODEL: selectedModel.id
}
// 执行启动操作
const executeLaunch = async (env: Record<string, string>) => {
window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { autoUpdateToLatest })
window.message.success({ content: t('code.launch.success'), key: 'code-launch-message' })
}
if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.openaiCodex) {
env = {
OPENAI_API_KEY: apiKey,
OPENAI_BASE_URL: baseUrl,
OPENAI_MODEL: selectedModel.id
}
// 处理启动
const handleLaunch = async () => {
const validation = validateLaunch()
if (!validation.isValid) {
window.message.warning({ content: validation.message, key: 'code-launch-message' })
return
}
// 解析用户自定义的环境变量
if (environmentVariables) {
const lines = environmentVariables.split('\n')
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine && trimmedLine.includes('=')) {
const [key, ...valueParts] = trimmedLine.split('=')
const trimmedKey = key.trim()
const value = valueParts.join('=').trim()
if (trimmedKey) {
env[trimmedKey] = value
}
}
}
}
setIsLaunching(true)
try {
// 这里可以添加实际的启动逻辑
logger.info('启动配置:', {
cliTool: selectedCliTool,
model: selectedModel,
folder: currentDirectory
})
const env = await prepareLaunchEnvironment()
if (!env) {
window.message.error({ content: t('code.model_required'), key: 'code-launch-message' })
return
}
window.api.codeTools.run(selectedCliTool, selectedModel?.id, currentDirectory, env, {
autoUpdateToLatest
})
window.message.success({
content: t('code.launch.success'),
key: 'code-launch-message'
})
await executeLaunch(env)
} catch (error) {
logger.error('启动失败:', error as Error)
window.message.error({
content: t('code.launch.error'),
key: 'code-launch-message'
})
window.message.error({ content: t('code.launch.error'), key: 'code-launch-message' })
} finally {
setIsLaunching(false)
}
@@ -320,7 +249,7 @@ const CodeToolsPage: FC = () => {
style={{ width: '100%' }}
placeholder={t('code.cli_tool_placeholder')}
value={selectedCliTool}
onChange={handleCliToolChange}
onChange={setCliTool}
options={CLI_TOOLS}
/>
</SettingsItem>
@@ -345,7 +274,7 @@ const CodeToolsPage: FC = () => {
style={{ flex: 1, width: 480 }}
placeholder={t('code.folder_placeholder')}
value={currentDirectory || undefined}
onChange={handleDirectoryChange}
onChange={setCurrentDir}
allowClear
showSearch
filterOption={(input, option) => {
@@ -366,7 +295,7 @@ const CodeToolsPage: FC = () => {
)
}))}
/>
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
<Button onClick={selectFolder} style={{ width: 120 }}>
{t('code.select_folder')}
</Button>
</Space.Compact>
@@ -377,7 +306,7 @@ const CodeToolsPage: FC = () => {
<Input.TextArea
placeholder={`KEY1=value1\nKEY2=value2`}
value={environmentVariables}
onChange={handleEnvVarsChange}
onChange={(e) => setEnvVars(e.target.value)}
rows={2}
style={{ fontFamily: 'monospace' }}
/>

View File

@@ -1 +1,135 @@
import { EndpointType, Model, Provider } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
export interface LaunchValidationResult {
isValid: boolean
message?: string
}
export interface ToolEnvironmentConfig {
tool: codeTools
model: Model
modelProvider: Provider
apiKey: string
baseUrl: string
}
// CLI 工具选项
export const CLI_TOOLS = [
{ value: codeTools.qwenCode, label: 'Qwen Code' },
{ value: codeTools.claudeCode, label: 'Claude Code' },
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
]
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu']
export const CLAUDE_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', ...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS]
// Provider 过滤映射
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
[codeTools.claudeCode]: (providers) =>
providers.filter((p) => p.type === 'anthropic' || CLAUDE_SUPPORTED_PROVIDERS.includes(p.id)),
[codeTools.geminiCli]: (providers) =>
providers.filter((p) => p.type === 'gemini' || GEMINI_SUPPORTED_PROVIDERS.includes(p.id)),
[codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')),
[codeTools.openaiCodex]: (providers) => providers.filter((p) => p.id === 'openai')
}
export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
const CODE_TOOLS_API_ENDPOINTS = {
aihubmix: {
gemini: {
api_base_url: 'https://api.aihubmix.com/gemini'
}
},
deepseek: {
anthropic: {
api_base_url: 'https://api.deepseek.com/anthropic'
}
},
moonshot: {
anthropic: {
api_base_url: 'https://api.moonshot.cn/anthropic'
}
},
zhipu: {
anthropic: {
api_base_url: 'https://open.bigmodel.cn/api/anthropic'
}
}
}
const provider = model.provider
return CODE_TOOLS_API_ENDPOINTS[provider]?.[type]?.api_base_url
}
// 解析环境变量字符串为对象
export const parseEnvironmentVariables = (envVars: string): Record<string, string> => {
const env: Record<string, string> = {}
if (!envVars) return env
const lines = envVars.split('\n')
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine && trimmedLine.includes('=')) {
const [key, ...valueParts] = trimmedLine.split('=')
const trimmedKey = key.trim()
const value = valueParts.join('=').trim()
if (trimmedKey) {
env[trimmedKey] = value
}
}
}
return env
}
// 为不同 CLI 工具生成环境变量配置
export const generateToolEnvironment = ({
tool,
model,
modelProvider,
apiKey,
baseUrl
}: {
tool: codeTools
model: Model
modelProvider: Provider
apiKey: string
baseUrl: string
}): Record<string, string> => {
const env: Record<string, string> = {}
switch (tool) {
case codeTools.claudeCode:
env.ANTHROPIC_BASE_URL = getCodeToolsApiBaseUrl(model, 'anthropic') || modelProvider.apiHost
env.ANTHROPIC_MODEL = model.id
if (modelProvider.type === 'anthropic') {
env.ANTHROPIC_API_KEY = apiKey
} else {
env.ANTHROPIC_AUTH_TOKEN = apiKey
}
break
case codeTools.geminiCli: {
const apiBaseUrl = getCodeToolsApiBaseUrl(model, 'gemini') || modelProvider.apiHost
env.GEMINI_API_KEY = apiKey
env.GEMINI_BASE_URL = apiBaseUrl
env.GOOGLE_GEMINI_BASE_URL = apiBaseUrl
env.GEMINI_MODEL = model.id
break
}
case codeTools.qwenCode:
case codeTools.openaiCodex:
env.OPENAI_API_KEY = apiKey
env.OPENAI_BASE_URL = baseUrl
env.OPENAI_MODEL = model.id
break
}
return env
}
export { default } from './CodeToolsPage'

View File

@@ -210,7 +210,21 @@ const McpServersList: FC = () => {
</Button>
</ButtonGroup>
</ListHeader>
<DraggableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
<DraggableList
style={{ width: '100%' }}
list={mcpServers}
onUpdate={updateMcpServers}
listProps={{
locale: {
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
style={{ marginTop: 20 }}
/>
)
}
}}>
{(server: MCPServer) => (
<div onClick={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}>
<McpServerCard
@@ -225,13 +239,6 @@ const McpServersList: FC = () => {
</div>
)}
</DraggableList>
{mcpServers.length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
style={{ marginTop: 20 }}
/>
)}
<McpMarketList />
<BuiltinMCPServerList />

View File

@@ -1,28 +1,32 @@
import { DeleteOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import db from '@renderer/databases'
import useTranslate from '@renderer/hooks/useTranslate'
import { clearHistory, deleteHistory } from '@renderer/services/TranslateService'
import { TranslateHistory, TranslateLanguage } from '@renderer/types'
import { Button, Drawer, Dropdown, Empty, Flex, Popconfirm } from 'antd'
import { Button, Drawer, Dropdown, Empty, Flex, Input, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react'
import { SearchIcon } from 'lucide-react'
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
type DisplayedTranslateHistory = TranslateHistory & {
type DisplayedTranslateHistoryItem = TranslateHistory & {
_sourceLanguage: TranslateLanguage
_targetLanguage: TranslateLanguage
}
type TranslateHistoryProps = {
isOpen: boolean
onHistoryItemClick: (history: DisplayedTranslateHistory) => void
onHistoryItemClick: (history: DisplayedTranslateHistoryItem) => void
onClose: () => void
}
// const logger = loggerService.withContext('TranslateHistory')
// px
const ITEM_HEIGHT = 140
@@ -30,17 +34,35 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
const { t } = useTranslation()
const { getLanguageByLangcode } = useTranslate()
const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
const [search, setSearch] = useState('')
const [displayedHistory, setDisplayedHistory] = useState<DisplayedTranslateHistoryItem[]>([])
const translateHistory: DisplayedTranslateHistory[] = useMemo(() => {
const translateHistory: DisplayedTranslateHistoryItem[] = useMemo(() => {
if (!_translateHistory) return []
return _translateHistory.map((item) => ({
...item,
_sourceLanguage: getLanguageByLangcode(item.sourceLanguage),
_targetLanguage: getLanguageByLangcode(item.targetLanguage)
_targetLanguage: getLanguageByLangcode(item.targetLanguage),
createdAt: dayjs(item.createdAt).format('MM/DD HH:mm')
}))
}, [_translateHistory, getLanguageByLangcode])
const searchFilter = useCallback(
(item: DisplayedTranslateHistoryItem) => {
if (isEmpty(search)) return true
const content = `${item._sourceLanguage.label()} ${item._targetLanguage.label()} ${item.sourceText} ${item.targetText} ${item.createdAt}`
return content.includes(search)
},
[search]
)
useEffect(() => {
setDisplayedHistory(translateHistory.filter(searchFilter))
}, [searchFilter, translateHistory])
const deferredHistory = useDeferredValue(displayedHistory)
return (
<Drawer
title={t('translate.history.title')}
@@ -71,9 +93,32 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
}
}}>
<HistoryContainer>
{translateHistory && translateHistory.length ? (
{/* Search Bar */}
<HStack style={{ padding: '0 12px', borderBottom: '1px solid var(--ant-color-split)' }}>
<Input
prefix={
<IconWrapper>
<SearchIcon size={18} />
</IconWrapper>
}
placeholder={t('translate.history.search.placeholder')}
value={search}
onChange={(e) => {
setSearch(e.target.value)
}}
allowClear
autoFocus
spellCheck={false}
style={{ paddingLeft: 0, height: '3em' }}
variant="borderless"
size="middle"
/>
</HStack>
{/* Virtual List */}
{deferredHistory.length > 0 ? (
<HistoryList>
<DynamicVirtualList list={translateHistory} estimateSize={() => ITEM_HEIGHT}>
<DynamicVirtualList list={deferredHistory} estimateSize={() => ITEM_HEIGHT}>
{(item) => {
return (
<Dropdown
@@ -98,7 +143,7 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
<HistoryListItemLanguage>{item._sourceLanguage.label()} </HistoryListItemLanguage>
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
</Flex>
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
<HistoryListItemDate>{item.createdAt}</HistoryListItemDate>
</Flex>
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
<HistoryListItemTitle style={{ color: 'var(--color-text-2)' }}>
@@ -192,4 +237,14 @@ const HistoryListItemLanguage = styled.div`
color: var(--color-text-3);
`
const IconWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 30px;
border-radius: 15px;
background-color: var(--color-background-soft);
`
export default TranslateHistoryList

View File

@@ -122,8 +122,6 @@ const TranslatePage: FC = () => {
return
}
setTranslating(true)
let translated: string
try {
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100))
@@ -145,8 +143,6 @@ const TranslatePage: FC = () => {
} catch (e) {
logger.error('Failed to translate', e as Error)
window.message.error(t('translate.error.unknown') + ': ' + (e as Error).message)
} finally {
setTranslating(false)
}
},
[setTranslatedContent, setTranslating, t, translating]
@@ -163,6 +159,8 @@ const TranslatePage: FC = () => {
return
}
setTranslating(true)
try {
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
let actualSourceLanguage: TranslateLanguage
@@ -202,11 +200,14 @@ const TranslatePage: FC = () => {
key: 'translate-message'
})
return
} finally {
setTranslating(false)
}
}, [
bidirectionalPair,
getLanguageByLangcode,
isBidirectional,
setTranslating,
sourceLanguage,
t,
targetLanguage,

View File

@@ -41,7 +41,7 @@ export const detectLanguage = async (inputText: string): Promise<TranslateLangua
throw new Error('Invalid detection method.')
}
logger.info(`Detected Language: ${result}`)
return result
return result.trim()
}
const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguageCode> => {