diff --git a/src/renderer/src/components/Icons/UnWrapIcon.tsx b/src/renderer/src/components/Icons/UnWrapIcon.tsx new file mode 100644 index 0000000000..c20056f9ec --- /dev/null +++ b/src/renderer/src/components/Icons/UnWrapIcon.tsx @@ -0,0 +1,17 @@ +const UnWrapIcon = (props: React.SVGProps) => ( + + + +) +export default UnWrapIcon diff --git a/src/renderer/src/components/Icons/WrapIcon.tsx b/src/renderer/src/components/Icons/WrapIcon.tsx new file mode 100644 index 0000000000..8708d977bf --- /dev/null +++ b/src/renderer/src/components/Icons/WrapIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +const WrapIcon = (props: React.SVGProps) => ( + + + + +) +export default WrapIcon diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 5356248d13..a126857b19 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -114,6 +114,7 @@ "resend": "Resend", "save": "Save", "settings.code_collapsible": "Code block collapsible", + "settings.code_wrappable": "Code block wrappable", "settings.context_count": "Context", "settings.context_count.tip": "The number of previous messages to keep in the context.", "settings.max": "Max", @@ -890,6 +891,10 @@ "show_window": "Show Window", "visualization": "Visualization" }, + "code_block": { + "enable_wrap": "Wrap", + "disable_wrap": "Unwrap" + }, "backup": { "title": "Data Backup", "confirm": "Are you sure you want to backup data?", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 67f642ea12..374a2204dc 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -113,7 +113,8 @@ "message.quote": "引用", "resend": "再送信", "save": "保存", - "settings.code_collapsible": "コードブロックを折りたたむ", + "settings.code_collapsible": "コードブロック折り畳み", + "settings.code_wrappable": "コードブロック折り返し", "settings.context_count": "コンテキスト", "settings.context_count.tip": "コンテキストに保持する以前のメッセージの数", "settings.max": "最大", @@ -890,6 +891,10 @@ "show_window": "ウィンドウを表示", "visualization": "可視化" }, + "code_block": { + "enable_wrap": "改行", + "disable_wrap": "改行解除" + }, "backup": { "title": "データバックアップ", "confirm": "データをバックアップしますか?", @@ -920,3 +925,4 @@ } } } + diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 8ea108795e..397fb84a7f 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -114,6 +114,7 @@ "resend": "Переотправить", "save": "Сохранить", "settings.code_collapsible": "Блок кода свернут", + "settings.code_wrappable": "Блок кода можно переносить", "settings.context_count": "Контекст", "settings.context_count.tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте.", "settings.max": "Максимум", @@ -890,6 +891,10 @@ "show_window": "Показать окно", "visualization": "Визуализация" }, + "code_block": { + "enable_wrap": "Перенос строки", + "disable_wrap": "Отменить перенос строки" + }, "backup": { "title": "Резервное копирование данных", "confirm": "Вы уверены, что хотите создать резервную копию?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1637681180..469eee85d6 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -114,6 +114,7 @@ "resend": "重新发送", "save": "保存", "settings.code_collapsible": "代码块可折叠", + "settings.code_wrappable": "代码块可换行", "settings.context_count": "上下文数", "settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10", "settings.max": "不限", @@ -890,6 +891,10 @@ "show_window": "显示窗口", "visualization": "可视化" }, + "code_block": { + "enable_wrap": "换行", + "disable_wrap": "取消换行" + }, "backup": { "title": "数据备份", "confirm": "确定要备份数据吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 15def1ebeb..94d2251167 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -113,7 +113,8 @@ "message.quote": "引用", "resend": "重新發送", "save": "保存", - "settings.code_collapsible": "代码块可折叠", + "settings.code_collapsible": "代碼區塊可折疊", + "settings.code_wrappable": "代碼區塊可換行", "settings.context_count": "上下文", "settings.context_count.tip": "在上下文中保留的前幾則訊息。", "settings.max": "最大", @@ -890,6 +891,10 @@ "show_window": "顯示視窗", "visualization": "可視化" }, + "code_block": { + "enable_wrap": "換行", + "disable_wrap": "取消換行" + }, "backup": { "title": "資料備份", "confirm": "確定要備份資料嗎?", diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index 2c2ee520c4..9199cd7b87 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -1,8 +1,11 @@ import { CheckOutlined, DownloadOutlined, DownOutlined, RightOutlined } from '@ant-design/icons' import CopyIcon from '@renderer/components/Icons/CopyIcon' +import UnWrapIcon from '@renderer/components/Icons/UnWrapIcon' +import WrapIcon from '@renderer/components/Icons/WrapIcon' import { HStack } from '@renderer/components/Layout' import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider' import { useSettings } from '@renderer/hooks/useSettings' +import { Tooltip } from 'antd' import dayjs from 'dayjs' import React, { memo, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,12 +23,13 @@ interface CodeBlockProps { } const CodeBlock: React.FC = ({ children, className }) => { - const match = /language-(\w+)/.exec(className || '') - const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings() + const match = /language-(\w+)/.exec(className || '') || children?.includes('\n') + const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings() const language = match?.[1] ?? 'text' const [html, setHtml] = useState('') const { codeToHtml } = useSyntaxHighlighter() const [isExpanded, setIsExpanded] = useState(!codeCollapsible) + const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable) const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false) const codeContentRef = useRef(null) @@ -59,6 +63,15 @@ const CodeBlock: React.FC = ({ children, className }) => { } }, [codeCollapsible]) + useEffect(() => { + if (!codeWrappable) { + // 如果未启动代码块换行功能 + setIsUnwrapped(true) + } else { + setIsUnwrapped(!codeWrappable) // 被换行 + } + }, [codeWrappable]) + if (language === 'mermaid') { return } @@ -86,16 +99,19 @@ const CodeBlock: React.FC = ({ children, className }) => { {codeCollapsible && shouldShowExpandButton && ( setIsExpanded(!isExpanded)} /> )} - {'<' + match[1].toUpperCase() + '>'} + {'<' + language.toUpperCase() + '>'} {showDownloadButton && } + {codeWrappable && setIsUnwrapped(!isUnwrapped)} />} void }> = ({ unwrapped, onClick }) => { + const { t } = useTranslation() + const unwrapLabel = unwrapped ? t('code_block.enable_wrap') : t('code_block.disable_wrap') + return ( + + + {unwrapped ? ( + + ) : ( + + )} + + + ) +} + const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => { const [copied, setCopied] = useState(false) const { t } = useTranslation() @@ -183,9 +215,19 @@ const DownloadButton = ({ language, data }: { language: string; data: string }) const CodeBlockWrapper = styled.div`` -const CodeContent = styled.div<{ isShowLineNumbers: boolean }>` +const CodeContent = styled.div<{ isShowLineNumbers: boolean; isUnwrapped: boolean; isCodeWrappable: boolean }>` .shiki { padding: 1em; + + code { + display: table; + width: 100%; + + .line { + display: table-row; + height: 1.3rem; + } + } } ${(props) => @@ -200,14 +242,23 @@ const CodeContent = styled.div<{ isShowLineNumbers: boolean }>` content: counter(step); counter-increment: step; width: 1rem; - margin-right: 1rem; - display: inline-block; + padding-right: 1rem; + display: table-cell; text-align: right; opacity: 0.35; } `} -` + ${(props) => + props.isCodeWrappable && + !props.isUnwrapped && + ` + code .line * { + word-wrap: break-word; + white-space: pre-wrap; + } + `} +` const CodeHeader = styled.div` display: flex; align-items: center; @@ -290,6 +341,23 @@ const CollapseIconWrapper = styled.div` } ` +const UnwrapButtonWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 4px; + cursor: pointer; + color: var(--color-text-3); + transition: all 0.2s ease; + + &:hover { + background-color: var(--color-background-soft); + color: var(--color-text-1); + } +` + const DownloadWrapper = styled.div` display: flex; align-items: center; diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 97cacbfc3c..cb5e78e18a 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -19,6 +19,7 @@ import { setCodeCollapsible, setCodeShowLineNumbers, setCodeStyle, + setCodeWrappable, setFontSize, setMathEngine, setMessageFont, @@ -69,6 +70,7 @@ const SettingsTab: FC = (props) => { renderInputMessageAsMarkdown, codeShowLineNumbers, codeCollapsible, + codeWrappable, mathEngine, autoTranslateWithSpace, pasteLongTextThreshold, @@ -315,6 +317,11 @@ const SettingsTab: FC = (props) => { /> + + {t('chat.settings.code_wrappable')} + dispatch(setCodeWrappable(checked))} /> + + {t('chat.settings.thought_auto_collapse')} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index a0aab93d26..d269144a6b 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -42,6 +42,7 @@ export interface SettingsState { renderInputMessageAsMarkdown: boolean codeShowLineNumbers: boolean codeCollapsible: boolean + codeWrappable: boolean mathEngine: 'MathJax' | 'KaTeX' messageStyle: 'plain' | 'bubble' codeStyle: CodeStyleVarious @@ -107,6 +108,7 @@ const initialState: SettingsState = { renderInputMessageAsMarkdown: false, codeShowLineNumbers: false, codeCollapsible: false, + codeWrappable: false, mathEngine: 'KaTeX', messageStyle: 'plain', codeStyle: 'auto', @@ -243,6 +245,9 @@ const settingsSlice = createSlice({ setCodeCollapsible: (state, action: PayloadAction) => { state.codeCollapsible = action.payload }, + setCodeWrappable: (state, action: PayloadAction) => { + state.codeWrappable = action.payload + }, setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => { state.mathEngine = action.payload }, @@ -359,6 +364,7 @@ export const { setWebdavSyncInterval, setCodeShowLineNumbers, setCodeCollapsible, + setCodeWrappable, setMathEngine, setGridColumns, setGridPopoverTrigger,