mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 06:49:02 +08:00
feat(translate): migrate translate state to cache system
- Replace Redux state management with cache system for translate functionality - Add new CacheTranslating type to track translation state - Update TranslatePage to use cache hooks for input, output and state - Simplify translate function and improve error handling
This commit is contained in:
parent
c258035f6a
commit
9ebe4801f4
12
packages/shared/data/cache/cacheSchemas.ts
vendored
12
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -27,6 +27,12 @@ export type UseCacheSchema = {
|
|||||||
'topic.renaming': string[]
|
'topic.renaming': string[]
|
||||||
'topic.newly_renamed': string[]
|
'topic.newly_renamed': string[]
|
||||||
|
|
||||||
|
// Translate state
|
||||||
|
'translate.input': string
|
||||||
|
'translate.output': string
|
||||||
|
'translate.detecting': boolean
|
||||||
|
'translate.translating': CacheValueTypes.CacheTranslating
|
||||||
|
|
||||||
// Test keys (for dataRefactorTest window)
|
// Test keys (for dataRefactorTest window)
|
||||||
// TODO: remove after testing
|
// TODO: remove after testing
|
||||||
'test-hook-memory-1': string
|
'test-hook-memory-1': string
|
||||||
@ -72,6 +78,12 @@ export const DefaultUseCache: UseCacheSchema = {
|
|||||||
'topic.renaming': [],
|
'topic.renaming': [],
|
||||||
'topic.newly_renamed': [],
|
'topic.newly_renamed': [],
|
||||||
|
|
||||||
|
// Translate state
|
||||||
|
'translate.input': '',
|
||||||
|
'translate.output': '',
|
||||||
|
'translate.detecting': false,
|
||||||
|
'translate.translating': { isTranslating: false, abortKey: null },
|
||||||
|
|
||||||
// Test keys (for dataRefactorTest window)
|
// Test keys (for dataRefactorTest window)
|
||||||
// TODO: remove after testing
|
// TODO: remove after testing
|
||||||
'test-hook-memory-1': 'default-memory-value',
|
'test-hook-memory-1': 'default-memory-value',
|
||||||
|
|||||||
@ -16,3 +16,12 @@ export type CacheActiveSearches = Record<string, WebSearchStatus>
|
|||||||
// The actual type checking will be done at runtime by the cache system
|
// The actual type checking will be done at runtime by the cache system
|
||||||
export type CacheMinAppType = MinAppType
|
export type CacheMinAppType = MinAppType
|
||||||
export type CacheTopic = Topic
|
export type CacheTopic = Topic
|
||||||
|
export type CacheTranslating =
|
||||||
|
| {
|
||||||
|
isTranslating: true
|
||||||
|
abortKey: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isTranslating: false
|
||||||
|
abortKey: null
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { PlusOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons'
|
import { PlusOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons'
|
||||||
import { Button, Flex, Tooltip } from '@cherrystudio/ui'
|
import { Button, Flex, Tooltip } from '@cherrystudio/ui'
|
||||||
|
import { useCache } from '@data/hooks/useCache'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { CopyIcon } from '@renderer/components/Icons'
|
import { CopyIcon } from '@renderer/components/Icons'
|
||||||
@ -18,9 +19,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
|||||||
import useTranslate from '@renderer/hooks/useTranslate'
|
import useTranslate from '@renderer/hooks/useTranslate'
|
||||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||||
import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService'
|
import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
|
||||||
// import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
// import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
||||||
import { setTranslatedContent as setTranslatedContentAction, setTranslateInput } from '@renderer/store/translate'
|
|
||||||
import type { FileMetadata, SupportedOcrFile } from '@renderer/types'
|
import type { FileMetadata, SupportedOcrFile } from '@renderer/types'
|
||||||
import {
|
import {
|
||||||
type AutoDetectionMethod,
|
type AutoDetectionMethod,
|
||||||
@ -29,7 +28,7 @@ import {
|
|||||||
type TranslateHistory,
|
type TranslateHistory,
|
||||||
type TranslateLanguage
|
type TranslateLanguage
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { getFileExtension, isTextFile, runAsyncFunction, uuid } from '@renderer/utils'
|
import { getFileExtension, isTextFile, runAsyncFunction } from '@renderer/utils'
|
||||||
import { abortCompletion } from '@renderer/utils/abortController'
|
import { abortCompletion } from '@renderer/utils/abortController'
|
||||||
import { isAbortError } from '@renderer/utils/error'
|
import { isAbortError } from '@renderer/utils/error'
|
||||||
import { formatErrorMessage } from '@renderer/utils/error'
|
import { formatErrorMessage } from '@renderer/utils/error'
|
||||||
@ -70,9 +69,13 @@ const TranslatePage: FC = () => {
|
|||||||
const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts] })
|
const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts] })
|
||||||
const { ocr } = useOcr()
|
const { ocr } = useOcr()
|
||||||
const { setTimeoutTimer } = useTimer()
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
const [text, setText] = useCache('translate.input')
|
||||||
|
const [output, setOutput] = useCache('translate.output')
|
||||||
|
const [isDetecting, setIsDetecting] = useCache('translate.detecting')
|
||||||
|
const [translatingState, setTranslatingState] = useCache('translate.translating')
|
||||||
|
const { isTranslating, abortKey } = translatingState
|
||||||
|
|
||||||
// states
|
// states
|
||||||
// const [text, setText] = useState(_text)
|
|
||||||
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
|
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
|
||||||
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||||
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||||
@ -90,22 +93,12 @@ const TranslatePage: FC = () => {
|
|||||||
const [autoDetectionMethod, setAutoDetectionMethod] = useState<AutoDetectionMethod>('franc')
|
const [autoDetectionMethod, setAutoDetectionMethod] = useState<AutoDetectionMethod>('franc')
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
|
|
||||||
const [translating, setTranslating] = useState(false)
|
|
||||||
const [abortKey, setTranslateAbortKey] = useState<string>('')
|
|
||||||
// redux states
|
|
||||||
const text = useAppSelector((state) => state.translate.translateInput)
|
|
||||||
const translatedContent = useAppSelector((state) => state.translate.translatedContent)
|
|
||||||
// const translating = useAppSelector((state) => state.runtime.translating)
|
|
||||||
// const abortKey = useAppSelector((state) => state.runtime.translateAbortKey)
|
|
||||||
|
|
||||||
// ref
|
// ref
|
||||||
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 dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
_sourceLanguage = sourceLanguage
|
_sourceLanguage = sourceLanguage
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
@ -115,96 +108,45 @@ const TranslatePage: FC = () => {
|
|||||||
db.settings.put({ id: 'translate:model', value: model.id })
|
db.settings.put({ id: 'translate:model', value: model.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 控制翻译状态
|
|
||||||
const setText = useCallback(
|
|
||||||
(input: string) => {
|
|
||||||
dispatch(setTranslateInput(input))
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setTranslatedContent = useCallback(
|
|
||||||
(content: string) => {
|
|
||||||
dispatch(setTranslatedContentAction(content))
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
// const setTranslating = useCallback(
|
|
||||||
// (translating: boolean) => {
|
|
||||||
// dispatch(setTranslatingAction(translating))
|
|
||||||
// },
|
|
||||||
// [dispatch]
|
|
||||||
// )
|
|
||||||
|
|
||||||
// 控制复制行为
|
// 控制复制行为
|
||||||
const onCopy = useCallback(async () => {
|
const onCopy = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(translatedContent)
|
await navigator.clipboard.writeText(output)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to copy text to clipboard:', error as Error)
|
logger.error('Failed to copy text to clipboard:', error as Error)
|
||||||
window.toast.error(t('common.copy_failed'))
|
window.toast.error(t('common.copy_failed'))
|
||||||
}
|
}
|
||||||
}, [setCopied, t, translatedContent])
|
}, [setCopied, t, output])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译文本并保存历史记录,包含完整的异常处理,不会抛出异常
|
* Translate text and save history with full exception handling; never throws.
|
||||||
* @param text - 需要翻译的文本
|
* This function is responsible for managing the translating state.
|
||||||
* @param actualSourceLanguage - 源语言
|
* No other part of the code should directly write to the translating state.
|
||||||
* @param actualTargetLanguage - 目标语言
|
* @param text - Text to be translated
|
||||||
|
* @param actualSourceLanguage - Source language
|
||||||
|
* @param actualTargetLanguage - Target language
|
||||||
*/
|
*/
|
||||||
const translate = useCallback(
|
const translate = useCallback(
|
||||||
async (
|
async (text: string, targetLanguage: TranslateLanguage): Promise<string | null> => {
|
||||||
text: string,
|
|
||||||
actualSourceLanguage: TranslateLanguage,
|
|
||||||
actualTargetLanguage: TranslateLanguage
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
if (translating) {
|
const abortKey = crypto.randomUUID()
|
||||||
return
|
setTranslatingState({ isTranslating: true, abortKey })
|
||||||
}
|
// This await is necessary. Finally must be done after the promise is settled.
|
||||||
|
return await translateText(text, targetLanguage, throttle(setOutput, 100), abortKey)
|
||||||
let translated: string
|
|
||||||
const abortKey = uuid()
|
|
||||||
setTranslateAbortKey(abortKey)
|
|
||||||
|
|
||||||
try {
|
|
||||||
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey)
|
|
||||||
} catch (e) {
|
|
||||||
if (isAbortError(e)) {
|
|
||||||
window.toast.info(t('translate.info.aborted'))
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to translate text', e as Error)
|
|
||||||
window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(e))
|
|
||||||
}
|
|
||||||
setTranslating(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
window.toast.success(t('translate.complete'))
|
|
||||||
if (autoCopy) {
|
|
||||||
setTimeoutTimer(
|
|
||||||
'auto-copy',
|
|
||||||
async () => {
|
|
||||||
await onCopy()
|
|
||||||
},
|
|
||||||
100
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await saveTranslateHistory(text, translated, actualSourceLanguage.langCode, actualTargetLanguage.langCode)
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to save translate history', e as Error)
|
|
||||||
window.toast.error(t('translate.history.error.save') + ': ' + formatErrorMessage(e))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to translate', e as Error)
|
if (isAbortError(e)) {
|
||||||
window.toast.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e))
|
window.toast.info(t('translate.info.aborted'))
|
||||||
|
} else {
|
||||||
|
logger.error('Failed to translate text', e as Error)
|
||||||
|
window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(e))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
setTranslatingState({ isTranslating: false, abortKey: null })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[autoCopy, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating]
|
[t, setOutput, setTranslatingState]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 控制翻译按钮是否可用
|
// 控制翻译按钮是否可用
|
||||||
@ -215,9 +157,10 @@ const TranslatePage: FC = () => {
|
|||||||
targetLanguage.langCode === UNKNOWN.langCode ||
|
targetLanguage.langCode === UNKNOWN.langCode ||
|
||||||
(isBidirectional &&
|
(isBidirectional &&
|
||||||
(bidirectionalPair[0].langCode === UNKNOWN.langCode || bidirectionalPair[1].langCode === UNKNOWN.langCode)) ||
|
(bidirectionalPair[0].langCode === UNKNOWN.langCode || bidirectionalPair[1].langCode === UNKNOWN.langCode)) ||
|
||||||
isProcessing
|
isProcessing ||
|
||||||
|
isDetecting
|
||||||
)
|
)
|
||||||
}, [bidirectionalPair, isBidirectional, isProcessing, sourceLanguage, targetLanguage.langCode, text])
|
}, [bidirectionalPair, isBidirectional, isDetecting, isProcessing, sourceLanguage, targetLanguage.langCode, text])
|
||||||
|
|
||||||
// 控制翻译按钮,翻译前进行校验
|
// 控制翻译按钮,翻译前进行校验
|
||||||
const onTranslate = useCallback(async () => {
|
const onTranslate = useCallback(async () => {
|
||||||
@ -228,18 +171,25 @@ const TranslatePage: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTranslating(true)
|
let actualSourceLanguage: TranslateLanguage
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsDetecting(true)
|
||||||
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
||||||
let actualSourceLanguage: TranslateLanguage
|
|
||||||
if (sourceLanguage === 'auto') {
|
if (sourceLanguage === 'auto') {
|
||||||
actualSourceLanguage = getLanguageByLangcode(await detectLanguage(text))
|
actualSourceLanguage = getLanguageByLangcode(await detectLanguage(text))
|
||||||
setDetectedLanguage(actualSourceLanguage)
|
setDetectedLanguage(actualSourceLanguage)
|
||||||
} else {
|
} else {
|
||||||
actualSourceLanguage = sourceLanguage
|
actualSourceLanguage = sourceLanguage
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Language detecting error:', error as Error)
|
||||||
|
window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(error))
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
setIsDetecting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const result = determineTargetLanguage(actualSourceLanguage, targetLanguage, isBidirectional, bidirectionalPair)
|
const result = determineTargetLanguage(actualSourceLanguage, targetLanguage, isBidirectional, bidirectionalPair)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
let errorMessage = ''
|
let errorMessage = ''
|
||||||
@ -257,21 +207,42 @@ const TranslatePage: FC = () => {
|
|||||||
if (isBidirectional) {
|
if (isBidirectional) {
|
||||||
setTargetLanguage(actualTargetLanguage)
|
setTargetLanguage(actualTargetLanguage)
|
||||||
}
|
}
|
||||||
|
const translated = await translate(text, actualTargetLanguage)
|
||||||
|
if (translated === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await translate(text, actualSourceLanguage, actualTargetLanguage)
|
if (autoCopy) {
|
||||||
|
setTimeoutTimer(
|
||||||
|
'auto-copy',
|
||||||
|
async () => {
|
||||||
|
await onCopy()
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveTranslateHistory(text, translated, actualSourceLanguage.langCode, actualTargetLanguage.langCode)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to save translate history', e as Error)
|
||||||
|
window.toast.error(t('translate.history.error.save') + ': ' + formatErrorMessage(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toast.success(t('translate.complete'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Translation error:', error as Error)
|
logger.error('Language detecting error:', error as Error)
|
||||||
window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(error))
|
window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(error))
|
||||||
return
|
|
||||||
} finally {
|
|
||||||
setTranslating(false)
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
autoCopy,
|
||||||
bidirectionalPair,
|
bidirectionalPair,
|
||||||
couldTranslate,
|
couldTranslate,
|
||||||
getLanguageByLangcode,
|
getLanguageByLangcode,
|
||||||
isBidirectional,
|
isBidirectional,
|
||||||
setTranslating,
|
onCopy,
|
||||||
|
setIsDetecting,
|
||||||
|
setTimeoutTimer,
|
||||||
sourceLanguage,
|
sourceLanguage,
|
||||||
t,
|
t,
|
||||||
targetLanguage,
|
targetLanguage,
|
||||||
@ -300,7 +271,7 @@ const TranslatePage: FC = () => {
|
|||||||
history: TranslateHistory & { _sourceLanguage: TranslateLanguage; _targetLanguage: TranslateLanguage }
|
history: TranslateHistory & { _sourceLanguage: TranslateLanguage; _targetLanguage: TranslateLanguage }
|
||||||
) => {
|
) => {
|
||||||
setText(history.sourceText)
|
setText(history.sourceText)
|
||||||
setTranslatedContent(history.targetText)
|
setOutput(history.targetText)
|
||||||
if (history._sourceLanguage === UNKNOWN) {
|
if (history._sourceLanguage === UNKNOWN) {
|
||||||
setSourceLanguage('auto')
|
setSourceLanguage('auto')
|
||||||
} else {
|
} else {
|
||||||
@ -339,15 +310,15 @@ const TranslatePage: FC = () => {
|
|||||||
}, [couldExchangeAuto, detectedLanguage, sourceLanguage, t, targetLanguage])
|
}, [couldExchangeAuto, detectedLanguage, sourceLanguage, t, targetLanguage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isEmpty(text) && setTranslatedContent('')
|
isEmpty(text) && setOutput('')
|
||||||
}, [setTranslatedContent, text])
|
}, [setOutput, text])
|
||||||
|
|
||||||
// Render markdown content when result or enableMarkdown changes
|
// Render markdown content when result or enableMarkdown changes
|
||||||
// 控制Markdown渲染
|
// 控制Markdown渲染
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enableMarkdown && translatedContent) {
|
if (enableMarkdown && output) {
|
||||||
let isMounted = true
|
let isMounted = true
|
||||||
shikiMarkdownIt(translatedContent).then((rendered) => {
|
shikiMarkdownIt(output).then((rendered) => {
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setRenderedMarkdown(rendered)
|
setRenderedMarkdown(rendered)
|
||||||
}
|
}
|
||||||
@ -359,7 +330,7 @@ const TranslatePage: FC = () => {
|
|||||||
setRenderedMarkdown('')
|
setRenderedMarkdown('')
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}, [enableMarkdown, shikiMarkdownIt, translatedContent])
|
}, [enableMarkdown, shikiMarkdownIt, output])
|
||||||
|
|
||||||
// 控制设置加载
|
// 控制设置加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -728,7 +699,7 @@ const TranslatePage: FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{getLanguageDisplay()}
|
{getLanguageDisplay()}
|
||||||
<TranslateButton
|
<TranslateButton
|
||||||
translating={translating}
|
translating={isTranslating}
|
||||||
onTranslate={onTranslate}
|
onTranslate={onTranslate}
|
||||||
couldTranslate={couldTranslate}
|
couldTranslate={couldTranslate}
|
||||||
onAbort={onAbort}
|
onAbort={onAbort}
|
||||||
@ -780,7 +751,7 @@ const TranslatePage: FC = () => {
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onScroll={handleInputScroll}
|
onScroll={handleInputScroll}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
disabled={translating}
|
disabled={isTranslating}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
@ -799,19 +770,19 @@ const TranslatePage: FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="copy-button"
|
className="copy-button"
|
||||||
onPress={onCopy}
|
onPress={onCopy}
|
||||||
isDisabled={!translatedContent}
|
isDisabled={!output}
|
||||||
startContent={copied ? <Check size={16} color="var(--color-primary)" /> : <CopyIcon size={16} />}
|
startContent={copied ? <Check size={16} color="var(--color-primary)" /> : <CopyIcon size={16} />}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
/>
|
/>
|
||||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
||||||
{!translatedContent ? (
|
{!output ? (
|
||||||
<div style={{ color: 'var(--color-text-3)', userSelect: 'none' }}>
|
<div style={{ color: 'var(--color-text-3)', userSelect: 'none' }}>
|
||||||
{t('translate.output.placeholder')}
|
{t('translate.output.placeholder')}
|
||||||
</div>
|
</div>
|
||||||
) : enableMarkdown ? (
|
) : enableMarkdown ? (
|
||||||
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
|
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
|
||||||
) : (
|
) : (
|
||||||
<div className="plain">{translatedContent}</div>
|
<div className="plain">{output}</div>
|
||||||
)}
|
)}
|
||||||
</OutputText>
|
</OutputText>
|
||||||
</OutputContainer>
|
</OutputContainer>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user