From f5a280383856f12ed2155d2ef95621e2f5b27619 Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Tue, 27 May 2025 12:42:16 +0800 Subject: [PATCH] feat: remove auto-detect language option and add bidirectional translation settings --- src/renderer/src/config/translate.ts | 23 +- src/renderer/src/i18n/locales/en-us.json | 12 +- src/renderer/src/i18n/locales/ja-jp.json | 12 +- src/renderer/src/i18n/locales/ru-ru.json | 12 +- src/renderer/src/i18n/locales/zh-cn.json | 12 +- src/renderer/src/i18n/locales/zh-tw.json | 12 +- .../src/pages/translate/TranslatePage.tsx | 620 +++++++++++++----- 7 files changed, 509 insertions(+), 194 deletions(-) diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index 3aebd51b60..fa35acea1a 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -1,18 +1,6 @@ import i18n from '@renderer/i18n' -type LanguageOption = { - value: string - label: string - emoji: string - style?: React.CSSProperties -} - -export const TranslateLanguageOptions: LanguageOption[] = [ - { - value: 'auto-detect', - label: i18n.t('languages.auto-detect'), - emoji: '🌐' - }, +export const TranslateLanguageOptions = [ { value: '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 { value: option.value, label: i18n.t(`languages.${option.value}`), - emoji: option.emoji, - style: { - display: 'flex', - alignItems: 'center', - gap: '8px' - } + emoji: option.emoji } }) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index e4531cac85..d9d124d0c6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})." }, "languages": { - "auto-detect": "Auto Detect", "arabic": "Arabic", "chinese": "Chinese", "chinese-traditional": "Traditional Chinese", @@ -1764,6 +1763,17 @@ "processing": "Translation in progress...", "scroll_sync.disable": "Disable 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", "tooltip.newline": "Newline", "menu": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 203aba979d..6cb36f8af1 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。" }, "languages": { - "auto-detect": "自動検出", "arabic": "アラビア語", "chinese": "中国語", "chinese-traditional": "繁体字中国語", @@ -1764,6 +1763,17 @@ "processing": "翻訳中...", "scroll_sync.disable": "關閉滾動同步", "scroll_sync.enable": "開啟滾動同步", + "bidirectional.disable": "双方向翻訳を無効にする", + "bidirectional.enable": "双方向翻訳を有効にする", + "language.same": "ソース言語と目標言語が同じです", + "language.not_pair": "ソース言語が設定された言語と異なります", + "settings": { + "title": "翻訳設定", + "model": "モデル設定", + "model_desc": "翻訳サービスで使用されるモデル", + "bidirectional": "双方向翻訳設定", + "scroll_sync": "スクロール同期設定" + }, "title": "翻訳", "tooltip.newline": "改行", "menu": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 80ae7eead2..85d2061e54 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})" }, "languages": { - "auto-detect": "Автоопределение", "arabic": "Арабский", "chinese": "Китайский", "chinese-traditional": "Китайский традиционный", @@ -1765,6 +1764,17 @@ "processing": "Перевод в процессе...", "scroll_sync.disable": "Отключить синхронизацию прокрутки", "scroll_sync.enable": "Включить синхронизацию прокрутки", + "bidirectional.disable": "Отключить двунаправленный перевод", + "bidirectional.enable": "Включить двунаправленный перевод", + "language.same": "Исходный и целевой языки совпадают", + "language.not_pair": "Исходный язык отличается от настроенного", + "settings": { + "title": "Настройки перевода", + "model": "Настройки модели", + "model_desc": "Модель, используемая для службы перевода", + "bidirectional": "Настройки двунаправленного перевода", + "scroll_sync": "Настройки синхронизации прокрутки" + }, "title": "Перевод", "tooltip.newline": "Перевести", "menu": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e2077e39fa..a9d1286431 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -558,7 +558,6 @@ "urls": "网址" }, "languages": { - "auto-detect": "自动检测", "arabic": "阿拉伯文", "chinese": "简体中文", "chinese-traditional": "繁体中文", @@ -1767,6 +1766,17 @@ "processing": "翻译中...", "scroll_sync.disable": "关闭滚动同步", "scroll_sync.enable": "开启滚动同步", + "bidirectional.disable": "关闭双向翻译", + "bidirectional.enable": "开启双向翻译", + "language.same": "源语言和目标语言相同", + "language.not_pair": "源语言与设置的语言不同", + "settings": { + "title": "翻译设置", + "model": "模型设置", + "model_desc": "翻译服务使用的模型", + "bidirectional": "双向翻译设置", + "scroll_sync": "滚动同步设置" + }, "title": "翻译", "tooltip.newline": "换行" }, diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 90b4e260c3..70b34c7d19 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})" }, "languages": { - "auto-detect": "自動偵測", "arabic": "阿拉伯文", "chinese": "簡體中文", "chinese-traditional": "繁體中文", @@ -1765,6 +1764,17 @@ "processing": "翻譯中...", "scroll_sync.disable": "關閉滾動同步", "scroll_sync.enable": "開啟滾動同步", + "bidirectional.disable": "關閉雙向翻譯", + "bidirectional.enable": "開啟雙向翻譯", + "language.same": "源語言和目標語言相同", + "language.not_pair": "源語言與設定的語言不同", + "settings": { + "title": "翻譯設定", + "model": "模型設定", + "model_desc": "翻譯服務使用的模型", + "bidirectional": "雙向翻譯設定", + "scroll_sync": "滾動同步設定" + }, "title": "翻譯", "tooltip.newline": "換行", "menu": { diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 698734a76b..fd160aad5f 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -1,51 +1,253 @@ import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' 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 db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' -import i18n from '@renderer/i18n' +import { useProviders } from '@renderer/hooks/useProvider' import { fetchTranslate } from '@renderer/services/ApiService' 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 { 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 dayjs from 'dayjs' import { useLiveQuery } from 'dexie-react-hooks' -import { isEmpty } from 'lodash' -import { Mouse, Settings2, TriangleAlert } from 'lucide-react' -import { FC, useEffect, useRef, useState } from 'react' +import { find, isEmpty, sortBy } from 'lodash' +import { Settings2, TriangleAlert } from 'lucide-react' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' import styled from 'styled-components' let _text = '' let _result = '' 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 ( + {t('translate.settings.title')}} + open={visible} + onCancel={onClose} + footer={[ + + {t('common.cancel')} + , + + {t('common.save')} + + ]} + width={420}> + + + {t('translate.settings.model')} + + { + const selectedModel = find(allModels, JSON.parse(value)) as Model + if (selectedModel) { + onModelChange(selectedModel) + } + }} + options={selectOptions} + showSearch + /> + {translateModel && } type="text" onClick={handleRemoveModel} danger />} + + {!translateModel && ( + + + + {t('translate.settings.no_model_warning')} + + + )} + + {t('translate.settings.model_desc')} + + + + + + + {t('translate.settings.bidirectional')} + + + + {isBidirectional ? t('translate.bidirectional.disable') : t('translate.bidirectional.enable')} + + + + {isBidirectional && ( + + setLocalPair([value, localPair[1]])} + options={translateLanguageOptions().map((lang) => ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + /> + ⇆ + setLocalPair([localPair[0], value])} + options={translateLanguageOptions().map((lang) => ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + /> + + )} + + + + + + + {t('translate.settings.scroll_sync')} + + {isScrollSyncEnabled ? t('translate.scroll_sync.disable') : t('translate.scroll_sync.enable')} + + + + + + ) +} + const TranslatePage: FC = () => { const { t } = useTranslation() const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const [text, setText] = useState(_text) const [result, setResult] = useState(_result) - const { translateModel } = useDefaultModel() + const { translateModel, setTranslateModel } = useDefaultModel() const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) const [historyDrawerVisible, setHistoryDrawerVisible] = 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(null) const contentContainerRef = useRef(null) const textAreaRef = useRef(null) const outputTextRef = useRef(null) 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(), []) _text = text _result = result _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 ( sourceText: string, targetText: string, @@ -64,18 +266,79 @@ const TranslatePage: FC = () => { } const deleteHistory = async (id: string) => { - db.translate_history.delete(id) + await db.translate_history.delete(id) } const clearHistory = async () => { - db.translate_history.clear() + await db.translate_history.clear() + } + + const detectLanguage = async (inputText: string): Promise => { + 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 () => { - if (!text.trim()) { - return - } - + if (!text.trim()) return if (!translateModel) { window.message.error({ content: t('translate.error.not_configured'), @@ -84,44 +347,66 @@ const TranslatePage: FC = () => { 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) - let translatedText = '' 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({ content: text, assistant, - onResponse: (text) => { - translatedText = text.replace(/^\s*\n+/g, '') + onResponse: (responseText) => { + translatedText = responseText.replace(/^\s*\n+/g, '') setResult(translatedText) } }) + + await saveTranslateHistory(text, translatedText, sourceLanguage, actualTargetLanguage) } catch (error) { - console.error('Translation error:', error) + console.error('翻译错误:', error) window.message.error({ content: String(error), key: 'translate-message' }) + } finally { setLoading(false) - return } + } - await saveTranslateHistory(text, translatedText, 'any', targetLanguage) - setLoading(false) + const toggleBidirectional = (value: boolean) => { + setIsBidirectional(value) + db.settings.put({ id: 'translate:bidirectional:enabled', value }) + + if (!value && originalTargetLanguage) { + setTargetLanguage(originalTargetLanguage) + setOriginalTargetLanguage(null) + } } const onCopy = () => { @@ -144,6 +429,24 @@ const TranslatePage: FC = () => { runAsyncFunction(async () => { const targetLang = await db.settings.get({ id: 'translate:target:language' }) 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 ( - - - - ) - } - - return ( - - }> - {t('translate.error.not_configured')} - - - ) - } - // Handle input area scroll event const handleInputScroll = (e: React.UIEvent) => { if (!isScrollSyncEnabled || !outputTextRef.current || isProgrammaticScroll.current) return @@ -216,8 +494,39 @@ const TranslatePage: FC = () => { }) } - const toggleScrollSync = () => { - setIsScrollSyncEnabled(!isScrollSyncEnabled) + // 获取当前语言状态显示 + const getLanguageDisplay = () => { + if (isBidirectional) { + return ( + + + {`${t(`languages.${bidirectionalPair[0]}`)} ⇆ ${t(`languages.${bidirectionalPair[1]}`)}`} + + + ) + } + + return ( + { + setTargetLanguage(value) + db.settings.put({ id: 'translate:target:language', value }) + }} + options={translateLanguageOptions().map((lang) => ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + /> + ) } return ( @@ -239,94 +548,70 @@ const TranslatePage: FC = () => { {t('translate.history.title')} - {!isEmpty(translateHistory) && ( - - }> - {t('translate.history.clear')} - - - )} + + }> + {t('translate.history.clear')} + + - {translateHistory && translateHistory.length ? ( + {translateHistory && translateHistory.length > 0 ? ( - {translateHistory.map((item) => ( - , - danger: true, - onClick: () => deleteHistory(item.id) - } - ] - }}> - onHistoryItemClick(item)}> - - {item.sourceText} - {item.targetText} - {dayjs(item.createdAt).format('MM/DD HH:mm')} - - - + {translateHistory.map((history) => ( + onHistoryItemClick(history)}> + {history.sourceText} + + {dayjs(history.createdAt).format('MM-DD HH:mm')} + } + onClick={(e) => { + e.stopPropagation() + deleteHistory(history.id) + }} + /> + + ))} ) : ( - - - + )} - - + } + onClick={() => setSettingsVisible(true)} /> - - + styles={{ body: { fontSize: '12px' } }} + title={ + + Enter: {t('translate.button.translate')} + + Shift + Enter: {t('translate.tooltip.newline')} + + }> + }> + {t('translate.button.translate')} + - - - - Enter: {t('translate.button.translate')} - - Shift + Enter: {t('translate.tooltip.newline')} - - }> - }> - {t('translate.button.translate')} - - + { - { - setTargetLanguage(value) - db.settings.put({ id: 'translate:target:language', value }) - }} - optionRender={(option) => ( - - - {option.data.emoji} - - {option.label} - - )} - /> + + {getLanguageDisplay()} + { + + setSettingsVisible(false)} + isScrollSyncEnabled={isScrollSyncEnabled} + setIsScrollSyncEnabled={setIsScrollSyncEnabled} + isBidirectional={isBidirectional} + setIsBidirectional={toggleBidirectional} + bidirectionalPair={bidirectionalPair} + setBidirectionalPair={setBidirectionalPair} + translateModel={translateModel} + onModelChange={handleModelChange} + allModels={allModels} + selectOptions={selectOptions} + /> ) } @@ -463,6 +735,16 @@ const TranslateButton = 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 }>` width: ${({ $historyDrawerVisible }) => ($historyDrawerVisible ? '300px' : '0')}; height: calc(100vh - var(--navbar-height) - 40px);