mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 14:29:15 +08:00
feat: remove auto-detect language option and add bidirectional translation settings
This commit is contained in:
parent
2a2e77632f
commit
f5a2803838
@ -1,18 +1,6 @@
|
|||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
|
||||||
type LanguageOption = {
|
export const TranslateLanguageOptions = [
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
emoji: string
|
|
||||||
style?: React.CSSProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TranslateLanguageOptions: LanguageOption[] = [
|
|
||||||
{
|
|
||||||
value: 'auto-detect',
|
|
||||||
label: i18n.t('languages.auto-detect'),
|
|
||||||
emoji: '🌐'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: 'english',
|
value: 'english',
|
||||||
label: i18n.t('languages.english'),
|
label: i18n.t('languages.english'),
|
||||||
@ -75,17 +63,12 @@ export const TranslateLanguageOptions: LanguageOption[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const translateLanguageOptions = (): LanguageOption[] => {
|
export const translateLanguageOptions = (): typeof TranslateLanguageOptions => {
|
||||||
return TranslateLanguageOptions.map((option) => {
|
return TranslateLanguageOptions.map((option) => {
|
||||||
return {
|
return {
|
||||||
value: option.value,
|
value: option.value,
|
||||||
label: i18n.t(`languages.${option.value}`),
|
label: i18n.t(`languages.${option.value}`),
|
||||||
emoji: option.emoji,
|
emoji: option.emoji
|
||||||
style: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -558,7 +558,6 @@
|
|||||||
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})."
|
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})."
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"auto-detect": "Auto Detect",
|
|
||||||
"arabic": "Arabic",
|
"arabic": "Arabic",
|
||||||
"chinese": "Chinese",
|
"chinese": "Chinese",
|
||||||
"chinese-traditional": "Traditional Chinese",
|
"chinese-traditional": "Traditional Chinese",
|
||||||
@ -1764,6 +1763,17 @@
|
|||||||
"processing": "Translation in progress...",
|
"processing": "Translation in progress...",
|
||||||
"scroll_sync.disable": "Disable synced scroll",
|
"scroll_sync.disable": "Disable synced scroll",
|
||||||
"scroll_sync.enable": "Enable synced scroll",
|
"scroll_sync.enable": "Enable synced scroll",
|
||||||
|
"bidirectional.disable": "Disable bidirectional translation",
|
||||||
|
"bidirectional.enable": "Enable bidirectional translation",
|
||||||
|
"language.same": "Source and target languages are the same",
|
||||||
|
"language.not_pair": "Source language is different from the set language",
|
||||||
|
"settings": {
|
||||||
|
"title": "Translation Settings",
|
||||||
|
"model": "Model Settings",
|
||||||
|
"model_desc": "Model used for translation service",
|
||||||
|
"bidirectional": "Bidirectional Translation Settings",
|
||||||
|
"scroll_sync": "Scroll Sync Settings"
|
||||||
|
},
|
||||||
"title": "Translation",
|
"title": "Translation",
|
||||||
"tooltip.newline": "Newline",
|
"tooltip.newline": "Newline",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -558,7 +558,6 @@
|
|||||||
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。"
|
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"auto-detect": "自動検出",
|
|
||||||
"arabic": "アラビア語",
|
"arabic": "アラビア語",
|
||||||
"chinese": "中国語",
|
"chinese": "中国語",
|
||||||
"chinese-traditional": "繁体字中国語",
|
"chinese-traditional": "繁体字中国語",
|
||||||
@ -1764,6 +1763,17 @@
|
|||||||
"processing": "翻訳中...",
|
"processing": "翻訳中...",
|
||||||
"scroll_sync.disable": "關閉滾動同步",
|
"scroll_sync.disable": "關閉滾動同步",
|
||||||
"scroll_sync.enable": "開啟滾動同步",
|
"scroll_sync.enable": "開啟滾動同步",
|
||||||
|
"bidirectional.disable": "双方向翻訳を無効にする",
|
||||||
|
"bidirectional.enable": "双方向翻訳を有効にする",
|
||||||
|
"language.same": "ソース言語と目標言語が同じです",
|
||||||
|
"language.not_pair": "ソース言語が設定された言語と異なります",
|
||||||
|
"settings": {
|
||||||
|
"title": "翻訳設定",
|
||||||
|
"model": "モデル設定",
|
||||||
|
"model_desc": "翻訳サービスで使用されるモデル",
|
||||||
|
"bidirectional": "双方向翻訳設定",
|
||||||
|
"scroll_sync": "スクロール同期設定"
|
||||||
|
},
|
||||||
"title": "翻訳",
|
"title": "翻訳",
|
||||||
"tooltip.newline": "改行",
|
"tooltip.newline": "改行",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -558,7 +558,6 @@
|
|||||||
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})"
|
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"auto-detect": "Автоопределение",
|
|
||||||
"arabic": "Арабский",
|
"arabic": "Арабский",
|
||||||
"chinese": "Китайский",
|
"chinese": "Китайский",
|
||||||
"chinese-traditional": "Китайский традиционный",
|
"chinese-traditional": "Китайский традиционный",
|
||||||
@ -1765,6 +1764,17 @@
|
|||||||
"processing": "Перевод в процессе...",
|
"processing": "Перевод в процессе...",
|
||||||
"scroll_sync.disable": "Отключить синхронизацию прокрутки",
|
"scroll_sync.disable": "Отключить синхронизацию прокрутки",
|
||||||
"scroll_sync.enable": "Включить синхронизацию прокрутки",
|
"scroll_sync.enable": "Включить синхронизацию прокрутки",
|
||||||
|
"bidirectional.disable": "Отключить двунаправленный перевод",
|
||||||
|
"bidirectional.enable": "Включить двунаправленный перевод",
|
||||||
|
"language.same": "Исходный и целевой языки совпадают",
|
||||||
|
"language.not_pair": "Исходный язык отличается от настроенного",
|
||||||
|
"settings": {
|
||||||
|
"title": "Настройки перевода",
|
||||||
|
"model": "Настройки модели",
|
||||||
|
"model_desc": "Модель, используемая для службы перевода",
|
||||||
|
"bidirectional": "Настройки двунаправленного перевода",
|
||||||
|
"scroll_sync": "Настройки синхронизации прокрутки"
|
||||||
|
},
|
||||||
"title": "Перевод",
|
"title": "Перевод",
|
||||||
"tooltip.newline": "Перевести",
|
"tooltip.newline": "Перевести",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -558,7 +558,6 @@
|
|||||||
"urls": "网址"
|
"urls": "网址"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"auto-detect": "自动检测",
|
|
||||||
"arabic": "阿拉伯文",
|
"arabic": "阿拉伯文",
|
||||||
"chinese": "简体中文",
|
"chinese": "简体中文",
|
||||||
"chinese-traditional": "繁体中文",
|
"chinese-traditional": "繁体中文",
|
||||||
@ -1767,6 +1766,17 @@
|
|||||||
"processing": "翻译中...",
|
"processing": "翻译中...",
|
||||||
"scroll_sync.disable": "关闭滚动同步",
|
"scroll_sync.disable": "关闭滚动同步",
|
||||||
"scroll_sync.enable": "开启滚动同步",
|
"scroll_sync.enable": "开启滚动同步",
|
||||||
|
"bidirectional.disable": "关闭双向翻译",
|
||||||
|
"bidirectional.enable": "开启双向翻译",
|
||||||
|
"language.same": "源语言和目标语言相同",
|
||||||
|
"language.not_pair": "源语言与设置的语言不同",
|
||||||
|
"settings": {
|
||||||
|
"title": "翻译设置",
|
||||||
|
"model": "模型设置",
|
||||||
|
"model_desc": "翻译服务使用的模型",
|
||||||
|
"bidirectional": "双向翻译设置",
|
||||||
|
"scroll_sync": "滚动同步设置"
|
||||||
|
},
|
||||||
"title": "翻译",
|
"title": "翻译",
|
||||||
"tooltip.newline": "换行"
|
"tooltip.newline": "换行"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -558,7 +558,6 @@
|
|||||||
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})"
|
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"auto-detect": "自動偵測",
|
|
||||||
"arabic": "阿拉伯文",
|
"arabic": "阿拉伯文",
|
||||||
"chinese": "簡體中文",
|
"chinese": "簡體中文",
|
||||||
"chinese-traditional": "繁體中文",
|
"chinese-traditional": "繁體中文",
|
||||||
@ -1765,6 +1764,17 @@
|
|||||||
"processing": "翻譯中...",
|
"processing": "翻譯中...",
|
||||||
"scroll_sync.disable": "關閉滾動同步",
|
"scroll_sync.disable": "關閉滾動同步",
|
||||||
"scroll_sync.enable": "開啟滾動同步",
|
"scroll_sync.enable": "開啟滾動同步",
|
||||||
|
"bidirectional.disable": "關閉雙向翻譯",
|
||||||
|
"bidirectional.enable": "開啟雙向翻譯",
|
||||||
|
"language.same": "源語言和目標語言相同",
|
||||||
|
"language.not_pair": "源語言與設定的語言不同",
|
||||||
|
"settings": {
|
||||||
|
"title": "翻譯設定",
|
||||||
|
"model": "模型設定",
|
||||||
|
"model_desc": "翻譯服務使用的模型",
|
||||||
|
"bidirectional": "雙向翻譯設定",
|
||||||
|
"scroll_sync": "滾動同步設定"
|
||||||
|
},
|
||||||
"title": "翻譯",
|
"title": "翻譯",
|
||||||
"tooltip.newline": "換行",
|
"tooltip.newline": "換行",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -1,51 +1,253 @@
|
|||||||
import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons'
|
import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { isEmbeddingModel } from '@renderer/config/models'
|
||||||
import { translateLanguageOptions } from '@renderer/config/translate'
|
import { translateLanguageOptions } from '@renderer/config/translate'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import i18n from '@renderer/i18n'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import type { Assistant, TranslateHistory } from '@renderer/types'
|
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
|
||||||
|
import type { Model, TranslateHistory } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { Button, Dropdown, Empty, Flex, Popconfirm, Select, Space, Tooltip } from 'antd'
|
import { Button, Divider, Empty, Flex, Modal, Popconfirm, Select, Space, Switch, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { isEmpty } from 'lodash'
|
import { find, isEmpty, sortBy } from 'lodash'
|
||||||
import { Mouse, Settings2, TriangleAlert } from 'lucide-react'
|
import { Settings2, TriangleAlert } from 'lucide-react'
|
||||||
import { FC, useEffect, useRef, useState } from 'react'
|
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _result = ''
|
let _result = ''
|
||||||
let _targetLanguage = 'english'
|
let _targetLanguage = 'english'
|
||||||
|
|
||||||
|
const TranslateSettings: FC<{
|
||||||
|
visible: boolean
|
||||||
|
onClose: () => void
|
||||||
|
isScrollSyncEnabled: boolean
|
||||||
|
setIsScrollSyncEnabled: (value: boolean) => void
|
||||||
|
isBidirectional: boolean
|
||||||
|
setIsBidirectional: (value: boolean) => void
|
||||||
|
bidirectionalPair: [string, string]
|
||||||
|
setBidirectionalPair: (value: [string, string]) => void
|
||||||
|
translateModel: Model | undefined
|
||||||
|
onModelChange: (model: Model) => void
|
||||||
|
allModels: Model[]
|
||||||
|
selectOptions: any[]
|
||||||
|
}> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
isScrollSyncEnabled,
|
||||||
|
setIsScrollSyncEnabled,
|
||||||
|
isBidirectional,
|
||||||
|
setIsBidirectional,
|
||||||
|
bidirectionalPair,
|
||||||
|
setBidirectionalPair,
|
||||||
|
translateModel,
|
||||||
|
onModelChange,
|
||||||
|
allModels,
|
||||||
|
selectOptions
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [localPair, setLocalPair] = useState<[string, string]>(bidirectionalPair)
|
||||||
|
|
||||||
|
const defaultTranslateModel = useMemo(
|
||||||
|
() => (hasModel(translateModel) ? getModelUniqId(translateModel) : undefined),
|
||||||
|
[translateModel]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalPair(bidirectionalPair)
|
||||||
|
}, [bidirectionalPair, visible])
|
||||||
|
|
||||||
|
const handleRemoveModel = () => {
|
||||||
|
db.settings.put({ id: 'translate:model', value: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (localPair[0] === localPair[1]) {
|
||||||
|
window.message.warning({
|
||||||
|
content: t('translate.language.same'),
|
||||||
|
key: 'translate-message'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBidirectionalPair(localPair)
|
||||||
|
db.settings.put({ id: 'translate:bidirectional:pair', value: localPair })
|
||||||
|
db.settings.put({ id: 'translate:scroll:sync', value: isScrollSyncEnabled })
|
||||||
|
window.message.success({
|
||||||
|
content: t('message.save.success.title'),
|
||||||
|
key: 'translate-settings-save'
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<div style={{ fontSize: 16 }}>{t('translate.settings.title')}</div>}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key="save" type="primary" onClick={handleSave}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={420}>
|
||||||
|
<Flex vertical gap={16} style={{ marginTop: 16 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('translate.settings.model')}</div>
|
||||||
|
<HStack alignItems="center" gap={5}>
|
||||||
|
<Select
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={t('translate.settings.model_placeholder')}
|
||||||
|
value={defaultTranslateModel}
|
||||||
|
onChange={(value) => {
|
||||||
|
const selectedModel = find(allModels, JSON.parse(value)) as Model
|
||||||
|
if (selectedModel) {
|
||||||
|
onModelChange(selectedModel)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={selectOptions}
|
||||||
|
showSearch
|
||||||
|
/>
|
||||||
|
{translateModel && <Button icon={<DeleteOutlined />} type="text" onClick={handleRemoveModel} danger />}
|
||||||
|
</HStack>
|
||||||
|
{!translateModel && (
|
||||||
|
<div style={{ marginTop: 8, color: 'var(--color-warning)' }}>
|
||||||
|
<HStack alignItems="center" gap={5}>
|
||||||
|
<TriangleAlert size={14} />
|
||||||
|
<span style={{ fontSize: 12 }}>{t('translate.settings.no_model_warning')}</span>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-3)' }}>
|
||||||
|
{t('translate.settings.model_desc')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('translate.settings.bidirectional')}</div>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<span>
|
||||||
|
{isBidirectional ? t('translate.bidirectional.disable') : t('translate.bidirectional.enable')}
|
||||||
|
</span>
|
||||||
|
<Switch checked={isBidirectional} onChange={setIsBidirectional} />
|
||||||
|
</Flex>
|
||||||
|
{isBidirectional && (
|
||||||
|
<Flex align="center" justify="space-between" gap={10}>
|
||||||
|
<Select
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
value={localPair[0]}
|
||||||
|
onChange={(value) => setLocalPair([value, localPair[1]])}
|
||||||
|
options={translateLanguageOptions().map((lang) => ({
|
||||||
|
value: lang.value,
|
||||||
|
label: (
|
||||||
|
<Space.Compact direction="horizontal" block>
|
||||||
|
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
|
||||||
|
{lang.emoji}
|
||||||
|
</span>
|
||||||
|
<Space.Compact block>{lang.label}</Space.Compact>
|
||||||
|
</Space.Compact>
|
||||||
|
)
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span>⇆</span>
|
||||||
|
<Select
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
value={localPair[1]}
|
||||||
|
onChange={(value) => setLocalPair([localPair[0], value])}
|
||||||
|
options={translateLanguageOptions().map((lang) => ({
|
||||||
|
value: lang.value,
|
||||||
|
label: (
|
||||||
|
<Space.Compact direction="horizontal" block>
|
||||||
|
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
|
||||||
|
{lang.emoji}
|
||||||
|
</span>
|
||||||
|
<div style={{ textAlign: 'left', flex: 1 }}>{lang.label}</div>
|
||||||
|
</Space.Compact>
|
||||||
|
)
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('translate.settings.scroll_sync')}</div>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<span>{isScrollSyncEnabled ? t('translate.scroll_sync.disable') : t('translate.scroll_sync.enable')}</span>
|
||||||
|
<Switch checked={isScrollSyncEnabled} onChange={setIsScrollSyncEnabled} />
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TranslatePage: FC = () => {
|
const TranslatePage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [result, setResult] = useState(_result)
|
const [result, setResult] = useState(_result)
|
||||||
const { translateModel } = useDefaultModel()
|
const { translateModel, setTranslateModel } = useDefaultModel()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||||
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false)
|
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false)
|
||||||
|
const [isBidirectional, setIsBidirectional] = useState(false)
|
||||||
|
const [bidirectionalPair, setBidirectionalPair] = useState<[string, string]>(['english', 'chinese'])
|
||||||
|
const [settingsVisible, setSettingsVisible] = useState(false)
|
||||||
|
const [originalTargetLanguage, setOriginalTargetLanguage] = useState<string | null>(null)
|
||||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const textAreaRef = useRef<TextAreaRef>(null)
|
const textAreaRef = useRef<TextAreaRef>(null)
|
||||||
const outputTextRef = useRef<HTMLDivElement>(null)
|
const outputTextRef = useRef<HTMLDivElement>(null)
|
||||||
const isProgrammaticScroll = useRef(false)
|
const isProgrammaticScroll = useRef(false)
|
||||||
|
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const allModels = useMemo(() => providers.map((p) => p.models).flat(), [providers])
|
||||||
|
|
||||||
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_result = result
|
_result = result
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
|
const selectOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
providers
|
||||||
|
.filter((p) => p.models.length > 0)
|
||||||
|
.map((p) => ({
|
||||||
|
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||||
|
title: p.name,
|
||||||
|
options: sortBy(p.models, 'name')
|
||||||
|
.filter((m) => !isEmbeddingModel(m))
|
||||||
|
.map((m) => ({
|
||||||
|
label: `${m.name} | ${p.isSystem ? t(`provider.${p.id}`) : p.name}`,
|
||||||
|
value: getModelUniqId(m)
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
[providers, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleModelChange = (model: Model) => {
|
||||||
|
setTranslateModel(model)
|
||||||
|
db.settings.put({ id: 'translate:model', value: model.id })
|
||||||
|
}
|
||||||
|
|
||||||
const saveTranslateHistory = async (
|
const saveTranslateHistory = async (
|
||||||
sourceText: string,
|
sourceText: string,
|
||||||
targetText: string,
|
targetText: string,
|
||||||
@ -64,18 +266,79 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteHistory = async (id: string) => {
|
const deleteHistory = async (id: string) => {
|
||||||
db.translate_history.delete(id)
|
await db.translate_history.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearHistory = async () => {
|
const clearHistory = async () => {
|
||||||
db.translate_history.clear()
|
await db.translate_history.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectLanguage = async (inputText: string): Promise<string> => {
|
||||||
|
if (!inputText.trim()) return 'any'
|
||||||
|
|
||||||
|
const langPatterns = {
|
||||||
|
chinese: /[\u4e00-\u9fa5]/,
|
||||||
|
japanese: /[\u3040-\u30ff\u3400-\u4dbf]/,
|
||||||
|
korean: /[\uAC00-\uD7AF]/,
|
||||||
|
russian: /[\u0400-\u04FF]/,
|
||||||
|
arabic: /[\u0600-\u06FF]/
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [lang, pattern] of Object.entries(langPatterns)) {
|
||||||
|
if (pattern.test(inputText)) return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prompt = `Identify language: "${inputText.substring(0, 50)}". Reply with one word only: english, chinese, japanese, korean, russian, spanish, french, german, italian, portuguese, arabic.`
|
||||||
|
|
||||||
|
let detectedCode = ''
|
||||||
|
await fetchTranslate({
|
||||||
|
content: inputText.substring(0, 50),
|
||||||
|
assistant: {
|
||||||
|
id: 'lang-detector',
|
||||||
|
name: 'Language Detector',
|
||||||
|
prompt,
|
||||||
|
topics: [],
|
||||||
|
type: 'translator'
|
||||||
|
},
|
||||||
|
onResponse: (response) => {
|
||||||
|
detectedCode = response.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const validCodes = [
|
||||||
|
'english',
|
||||||
|
'chinese',
|
||||||
|
'japanese',
|
||||||
|
'korean',
|
||||||
|
'russian',
|
||||||
|
'spanish',
|
||||||
|
'french',
|
||||||
|
'italian',
|
||||||
|
'portuguese',
|
||||||
|
'arabic',
|
||||||
|
'german'
|
||||||
|
]
|
||||||
|
|
||||||
|
return validCodes.find((code) => detectedCode.includes(code)) || 'english'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('语言检测错误:', error)
|
||||||
|
return 'english'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTargetLanguageForBidirectional = (sourceLanguage: string): string => {
|
||||||
|
if (sourceLanguage === bidirectionalPair[0]) {
|
||||||
|
return bidirectionalPair[1]
|
||||||
|
} else if (sourceLanguage === bidirectionalPair[1]) {
|
||||||
|
return bidirectionalPair[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return bidirectionalPair[0] === sourceLanguage ? bidirectionalPair[1] : bidirectionalPair[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTranslate = async () => {
|
const onTranslate = async () => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!translateModel) {
|
if (!translateModel) {
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: t('translate.error.not_configured'),
|
content: t('translate.error.not_configured'),
|
||||||
@ -84,44 +347,66 @@ const TranslatePage: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let assistant: Assistant
|
|
||||||
|
|
||||||
if (targetLanguage === 'auto-detect') {
|
|
||||||
const currentLanguage = i18n.language
|
|
||||||
const targetLang = currentLanguage === 'en' ? 'Chinese' : 'English'
|
|
||||||
|
|
||||||
assistant = {
|
|
||||||
...getDefaultTranslateAssistant(currentLanguage === 'en' ? 'chinese' : 'english', text),
|
|
||||||
name: 'Auto Translator',
|
|
||||||
prompt: `You are a translator. If input is in ${targetLang}, translate to ${currentLanguage === 'en' ? 'English' : 'Chinese'}. Otherwise translate to ${targetLang}. Output translation only. Text: ${text}`
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
let translatedText = ''
|
|
||||||
try {
|
try {
|
||||||
|
const sourceLanguage = await detectLanguage(text)
|
||||||
|
console.log('检测到的源语言:', sourceLanguage)
|
||||||
|
|
||||||
|
let actualTargetLanguage = targetLanguage
|
||||||
|
|
||||||
|
if (isBidirectional) {
|
||||||
|
if (![bidirectionalPair[0], bidirectionalPair[1]].includes(sourceLanguage)) {
|
||||||
|
window.message.warning({
|
||||||
|
content: t('translate.language.not_pair'),
|
||||||
|
key: 'translate-message'
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actualTargetLanguage = getTargetLanguageForBidirectional(sourceLanguage)
|
||||||
|
setTargetLanguage(actualTargetLanguage)
|
||||||
|
} else {
|
||||||
|
if (sourceLanguage === targetLanguage) {
|
||||||
|
window.message.warning({
|
||||||
|
content: t('translate.language.same'),
|
||||||
|
key: 'translate-message'
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistant = getDefaultTranslateAssistant(actualTargetLanguage, text)
|
||||||
|
let translatedText = ''
|
||||||
await fetchTranslate({
|
await fetchTranslate({
|
||||||
content: text,
|
content: text,
|
||||||
assistant,
|
assistant,
|
||||||
onResponse: (text) => {
|
onResponse: (responseText) => {
|
||||||
translatedText = text.replace(/^\s*\n+/g, '')
|
translatedText = responseText.replace(/^\s*\n+/g, '')
|
||||||
setResult(translatedText)
|
setResult(translatedText)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await saveTranslateHistory(text, translatedText, sourceLanguage, actualTargetLanguage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Translation error:', error)
|
console.error('翻译错误:', error)
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: String(error),
|
content: String(error),
|
||||||
key: 'translate-message'
|
key: 'translate-message'
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await saveTranslateHistory(text, translatedText, 'any', targetLanguage)
|
const toggleBidirectional = (value: boolean) => {
|
||||||
setLoading(false)
|
setIsBidirectional(value)
|
||||||
|
db.settings.put({ id: 'translate:bidirectional:enabled', value })
|
||||||
|
|
||||||
|
if (!value && originalTargetLanguage) {
|
||||||
|
setTargetLanguage(originalTargetLanguage)
|
||||||
|
setOriginalTargetLanguage(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
@ -144,6 +429,24 @@ const TranslatePage: FC = () => {
|
|||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
const targetLang = await db.settings.get({ id: 'translate:target:language' })
|
const targetLang = await db.settings.get({ id: 'translate:target:language' })
|
||||||
targetLang && setTargetLanguage(targetLang.value)
|
targetLang && setTargetLanguage(targetLang.value)
|
||||||
|
|
||||||
|
const bidirectionalPairSetting = await db.settings.get({ id: 'translate:bidirectional:pair' })
|
||||||
|
if (bidirectionalPairSetting) {
|
||||||
|
const langPair = bidirectionalPairSetting.value
|
||||||
|
if (Array.isArray(langPair) && langPair.length === 2 && langPair[0] !== langPair[1]) {
|
||||||
|
setBidirectionalPair(langPair as [string, string])
|
||||||
|
} else {
|
||||||
|
const defaultPair: [string, string] = ['english', 'chinese']
|
||||||
|
setBidirectionalPair(defaultPair)
|
||||||
|
db.settings.put({ id: 'translate:bidirectional:pair', value: defaultPair })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bidirectionalSetting = await db.settings.get({ id: 'translate:bidirectional:enabled' })
|
||||||
|
setIsBidirectional(bidirectionalSetting ? bidirectionalSetting.value : false)
|
||||||
|
|
||||||
|
const scrollSyncSetting = await db.settings.get({ id: 'translate:scroll:sync' })
|
||||||
|
setIsScrollSyncEnabled(scrollSyncSetting ? scrollSyncSetting.value : false)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -155,31 +458,6 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingButton = () => {
|
|
||||||
if (isLocalAi) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (translateModel) {
|
|
||||||
return (
|
|
||||||
<Link to="/settings/model" style={{ color: 'var(--color-text-2)', display: 'flex' }}>
|
|
||||||
<Settings2 size={18} />
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to="/settings/model" style={{ marginLeft: -10, display: 'flex' }}>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
style={{ color: 'var(--color-error)', textDecoration: 'underline' }}
|
|
||||||
icon={<TriangleAlert size={16} />}>
|
|
||||||
{t('translate.error.not_configured')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle input area scroll event
|
// Handle input area scroll event
|
||||||
const handleInputScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
|
const handleInputScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isScrollSyncEnabled || !outputTextRef.current || isProgrammaticScroll.current) return
|
if (!isScrollSyncEnabled || !outputTextRef.current || isProgrammaticScroll.current) return
|
||||||
@ -216,8 +494,39 @@ const TranslatePage: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleScrollSync = () => {
|
// 获取当前语言状态显示
|
||||||
setIsScrollSyncEnabled(!isScrollSyncEnabled)
|
const getLanguageDisplay = () => {
|
||||||
|
if (isBidirectional) {
|
||||||
|
return (
|
||||||
|
<Flex align="center" style={{ width: 160 }}>
|
||||||
|
<BidirectionalLanguageDisplay>
|
||||||
|
{`${t(`languages.${bidirectionalPair[0]}`)} ⇆ ${t(`languages.${bidirectionalPair[1]}`)}`}
|
||||||
|
</BidirectionalLanguageDisplay>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
style={{ width: 160 }}
|
||||||
|
value={targetLanguage}
|
||||||
|
onChange={(value) => {
|
||||||
|
setTargetLanguage(value)
|
||||||
|
db.settings.put({ id: 'translate:target:language', value })
|
||||||
|
}}
|
||||||
|
options={translateLanguageOptions().map((lang) => ({
|
||||||
|
value: lang.value,
|
||||||
|
label: (
|
||||||
|
<Space.Compact direction="horizontal" block>
|
||||||
|
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
|
||||||
|
{lang.emoji}
|
||||||
|
</span>
|
||||||
|
<Space.Compact block>{lang.label}</Space.Compact>
|
||||||
|
</Space.Compact>
|
||||||
|
)
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -239,94 +548,70 @@ const TranslatePage: FC = () => {
|
|||||||
<HistoryContainner $historyDrawerVisible={historyDrawerVisible}>
|
<HistoryContainner $historyDrawerVisible={historyDrawerVisible}>
|
||||||
<OperationBar>
|
<OperationBar>
|
||||||
<span style={{ fontSize: 16 }}>{t('translate.history.title')}</span>
|
<span style={{ fontSize: 16 }}>{t('translate.history.title')}</span>
|
||||||
{!isEmpty(translateHistory) && (
|
<Popconfirm
|
||||||
<Popconfirm
|
title={t('translate.history.clear')}
|
||||||
title={t('translate.history.clear')}
|
description={t('translate.history.clear_description')}
|
||||||
description={t('translate.history.clear_description')}
|
onConfirm={clearHistory}
|
||||||
onConfirm={clearHistory}>
|
okText={t('settings.data.nutstore.new_folder.button.confirm')}
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />}>
|
cancelText={t('settings.data.nutstore.new_folder.button.cancel')}>
|
||||||
{t('translate.history.clear')}
|
<Button type="text" size="small" danger icon={<DeleteOutlined />}>
|
||||||
</Button>
|
{t('translate.history.clear')}
|
||||||
</Popconfirm>
|
</Button>
|
||||||
)}
|
</Popconfirm>
|
||||||
</OperationBar>
|
</OperationBar>
|
||||||
{translateHistory && translateHistory.length ? (
|
{translateHistory && translateHistory.length > 0 ? (
|
||||||
<HistoryList>
|
<HistoryList>
|
||||||
{translateHistory.map((item) => (
|
{translateHistory.map((history) => (
|
||||||
<Dropdown
|
<HistoryListItem key={history.id} onClick={() => onHistoryItemClick(history)}>
|
||||||
key={item.id}
|
<HistoryListItemTitle>{history.sourceText}</HistoryListItemTitle>
|
||||||
trigger={['contextMenu']}
|
<Flex justify="space-between" style={{ marginTop: 5 }}>
|
||||||
menu={{
|
<HistoryListItemDate>{dayjs(history.createdAt).format('MM-DD HH:mm')}</HistoryListItemDate>
|
||||||
items: [
|
<Button
|
||||||
{
|
size="small"
|
||||||
key: 'delete',
|
type="text"
|
||||||
label: t('translate.history.delete'),
|
icon={<DeleteOutlined />}
|
||||||
icon: <DeleteOutlined />,
|
onClick={(e) => {
|
||||||
danger: true,
|
e.stopPropagation()
|
||||||
onClick: () => deleteHistory(item.id)
|
deleteHistory(history.id)
|
||||||
}
|
}}
|
||||||
]
|
/>
|
||||||
}}>
|
</Flex>
|
||||||
<HistoryListItem onClick={() => onHistoryItemClick(item)}>
|
</HistoryListItem>
|
||||||
<Flex justify="space-between" vertical gap={4} style={{ width: '100%' }}>
|
|
||||||
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
|
|
||||||
<HistoryListItemTitle>{item.targetText}</HistoryListItemTitle>
|
|
||||||
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
|
|
||||||
</Flex>
|
|
||||||
</HistoryListItem>
|
|
||||||
</Dropdown>
|
|
||||||
))}
|
))}
|
||||||
</HistoryList>
|
</HistoryList>
|
||||||
) : (
|
) : (
|
||||||
<Flex justify="center" align="center" style={{ flex: 1 }}>
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('translate.history.empty')} />
|
||||||
<Empty description={t('translate.history.empty')} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
</HistoryContainner>
|
</HistoryContainner>
|
||||||
|
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<OperationBar>
|
<OperationBar>
|
||||||
<Flex align="center" gap={20}>
|
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
|
||||||
<Select
|
<Button
|
||||||
showSearch
|
type="text"
|
||||||
value="any"
|
icon={<Settings2 size={18} color="var(--color-icon)" />}
|
||||||
style={{ width: 180 }}
|
onClick={() => setSettingsVisible(true)}
|
||||||
optionFilterProp="label"
|
|
||||||
disabled
|
|
||||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
|
||||||
/>
|
/>
|
||||||
<SettingButton />
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
mouseEnterDelay={0.5}
|
mouseEnterDelay={0.5}
|
||||||
title={isScrollSyncEnabled ? t('translate.scroll_sync.disable') : t('translate.scroll_sync.enable')}>
|
styles={{ body: { fontSize: '12px' } }}
|
||||||
<Mouse
|
title={
|
||||||
size={16}
|
<div style={{ textAlign: 'center' }}>
|
||||||
onClick={toggleScrollSync}
|
Enter: {t('translate.button.translate')}
|
||||||
style={{ cursor: 'pointer' }}
|
<br />
|
||||||
color={isScrollSyncEnabled ? 'var(--color-primary)' : 'var(--color-icon)'}
|
Shift + Enter: {t('translate.tooltip.newline')}
|
||||||
/>
|
</div>
|
||||||
|
}>
|
||||||
|
<TranslateButton
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={onTranslate}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
icon={<SendOutlined />}>
|
||||||
|
{t('translate.button.translate')}
|
||||||
|
</TranslateButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</div>
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
mouseEnterDelay={0.5}
|
|
||||||
styles={{ body: { fontSize: '12px' } }}
|
|
||||||
title={
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
Enter: {t('translate.button.translate')}
|
|
||||||
<br />
|
|
||||||
Shift + Enter: {t('translate.tooltip.newline')}
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<TranslateButton
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={onTranslate}
|
|
||||||
disabled={!text.trim()}
|
|
||||||
icon={<SendOutlined />}>
|
|
||||||
{t('translate.button.translate')}
|
|
||||||
</TranslateButton>
|
|
||||||
</Tooltip>
|
|
||||||
</OperationBar>
|
</OperationBar>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
@ -345,37 +630,9 @@ const TranslatePage: FC = () => {
|
|||||||
|
|
||||||
<OutputContainer>
|
<OutputContainer>
|
||||||
<OperationBar>
|
<OperationBar>
|
||||||
<Select
|
<HStack alignItems="center" gap={5}>
|
||||||
showSearch
|
{getLanguageDisplay()}
|
||||||
value={targetLanguage}
|
</HStack>
|
||||||
style={{ width: 180 }}
|
|
||||||
optionFilterProp="label"
|
|
||||||
options={translateLanguageOptions()}
|
|
||||||
onChange={(value) => {
|
|
||||||
setTargetLanguage(value)
|
|
||||||
db.settings.put({ id: 'translate:target:language', value })
|
|
||||||
}}
|
|
||||||
optionRender={(option) => (
|
|
||||||
<Space align="center">
|
|
||||||
<span
|
|
||||||
role="img"
|
|
||||||
aria-label={option.data.label}
|
|
||||||
style={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
fontSize: '16px',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
lineHeight: 1
|
|
||||||
}}>
|
|
||||||
{option.data.emoji}
|
|
||||||
</span>
|
|
||||||
{option.label}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<CopyButton
|
<CopyButton
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!result}
|
disabled={!result}
|
||||||
@ -388,6 +645,21 @@ const TranslatePage: FC = () => {
|
|||||||
</OutputText>
|
</OutputText>
|
||||||
</OutputContainer>
|
</OutputContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
|
|
||||||
|
<TranslateSettings
|
||||||
|
visible={settingsVisible}
|
||||||
|
onClose={() => setSettingsVisible(false)}
|
||||||
|
isScrollSyncEnabled={isScrollSyncEnabled}
|
||||||
|
setIsScrollSyncEnabled={setIsScrollSyncEnabled}
|
||||||
|
isBidirectional={isBidirectional}
|
||||||
|
setIsBidirectional={toggleBidirectional}
|
||||||
|
bidirectionalPair={bidirectionalPair}
|
||||||
|
setBidirectionalPair={setBidirectionalPair}
|
||||||
|
translateModel={translateModel}
|
||||||
|
onModelChange={handleModelChange}
|
||||||
|
allModels={allModels}
|
||||||
|
selectOptions={selectOptions}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -463,6 +735,16 @@ const TranslateButton = styled(Button)``
|
|||||||
|
|
||||||
const CopyButton = styled(Button)``
|
const CopyButton = styled(Button)``
|
||||||
|
|
||||||
|
const BidirectionalLanguageDisplay = styled.div`
|
||||||
|
padding: 4px 11px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
const HistoryContainner = styled.div<{ $historyDrawerVisible: boolean }>`
|
const HistoryContainner = styled.div<{ $historyDrawerVisible: boolean }>`
|
||||||
width: ${({ $historyDrawerVisible }) => ($historyDrawerVisible ? '300px' : '0')};
|
width: ${({ $historyDrawerVisible }) => ($historyDrawerVisible ? '300px' : '0')};
|
||||||
height: calc(100vh - var(--navbar-height) - 40px);
|
height: calc(100vh - var(--navbar-height) - 40px);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user