feat: make sidebar setting group collapsible (#6019)

* feat: code tools, editor, executor

CodeEditor & Preview
- CodeEditor: CodeMirror 6
  - Switch to CodeEditor in the settings
  - Support edit&save with a accurate diff&lookup strategy
  - Use CodeEditor for editing MCP json configuration
- CodePreview: Original Shiki syntax highlighting
  - Implemented using a custom Shiki stream tokenizer
  - Remov code caching as it is incompatible with the current streaming code highlighting
  - Add a webworker for shiki
- Other preview components
  - Merge MermaidPopup and Mermaid to MermaidPreview, use local mermaidjs
  - Show mermaid syntax error message on demand
  - Rename PlantUML to PlantUmlPreview
- Rename SyntaxHighlighterProvider to CodeStyleProvider for clarity
- Both light and dark themes are preserved for convenience

CodeToolbar
- Top sticky toolbar provides quick tools (left) and core tools (right)
- Quick tools are hidden under the `More` button to avoid clutter, while core tools are always visible
- View&edit mode
  - Allow switching between preview and edit modes
  - Add a split view

Code execution
- Pyodide for executing Python scripts
- Add a webworker for Pyodide

* fix: migrate version and lint error

* refactor: use constants for defining tool specs

* feat: make setting group collapsible

* fix: yarn.lock

* fix: conflict error

---------

Co-authored-by: one <wangan.cs@gmail.com>
This commit is contained in:
自由的世界人 2025-05-18 10:11:14 +08:00 committed by GitHub
parent 9d0b5d2c8f
commit efd11457db
2 changed files with 527 additions and 457 deletions

View File

@ -11,8 +11,9 @@ import {
import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings' import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
SendMessageShortcut, SendMessageShortcut,
@ -49,7 +50,7 @@ import {
TranslateLanguageVarious TranslateLanguageVarious
} from '@renderer/types' } from '@renderer/types'
import { modalConfirm } from '@renderer/utils' import { modalConfirm } from '@renderer/utils'
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd' import { Button, Col, Divider, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react' import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -181,478 +182,485 @@ const SettingsTab: FC<Props> = (props) => {
return ( return (
<Container className="settings-tab"> <Container className="settings-tab">
<SettingGroup style={{ marginTop: 10 }}> <CollapsibleSettingGroup
<SettingSubtitle style={{ marginTop: 0, display: 'flex', justifyContent: 'space-between' }}> title={t('assistants.settings.title')}
defaultExpanded={true}
extra={
<HStack alignItems="center"> <HStack alignItems="center">
{t('assistants.settings.title')}{' '}
<Tooltip title={t('chat.settings.reset')}> <Tooltip title={t('chat.settings.reset')}>
<RotateCcw size={20} onClick={onReset} style={{ cursor: 'pointer', padding: '0 3px' }} /> <RotateCcw size={20} onClick={onReset} style={{ cursor: 'pointer', padding: '0 3px' }} />
</Tooltip> </Tooltip>
<Button
type="text"
size="small"
icon={<Settings2 size={16} />}
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
/>
</HStack> </HStack>
<Button }>
type="text" <SettingGroup style={{ marginTop: 10 }}>
size="small" <SettingDivider />
icon={<Settings2 size={16} />} <Row align="middle">
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })} <Label>{t('chat.settings.temperature')}</Label>
/> <Tooltip title={t('chat.settings.temperature.tip')}>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<CircleHelp size={14} color="var(--color-text-2)" />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.context_count')}</Label>
<Tooltip title={t('chat.settings.context_count.tip')}>
<CircleHelp size={14} color="var(--color-text-2)" />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={maxContextCount}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<SettingDivider />
<Row align="middle" justify="space-between" style={{ marginBottom: 10 }}>
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<CircleHelp size={14} color="var(--color-text-2)" /> <CircleHelp size={14} color="var(--color-text-2)" />
</Tooltip> </Tooltip>
</HStack> </Row>
<Switch
size="small"
checked={enableMaxTokens}
onChange={async (enabled) => {
if (enabled) {
const confirmed = await modalConfirm({
title: t('chat.settings.max_tokens.confirm'),
content: t('chat.settings.max_tokens.confirm_content'),
okButtonProps: {
danger: true
}
})
if (!confirmed) return
}
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
{enableMaxTokens && (
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={24}> <Col span={24}>
<InputNumber <Slider
disabled={!enableMaxTokens}
min={0} min={0}
max={10000000} max={2}
step={100} onChange={setTemperature}
value={typeof maxTokens === 'number' ? maxTokens : 0} onChangeComplete={onTemperatureChange}
changeOnBlur value={typeof temperature === 'number' ? temperature : 0}
onChange={(value) => value && setMaxTokens(value)} step={0.1}
onBlur={() => onMaxTokensChange(maxTokens)}
style={{ width: '100%' }}
/> />
</Col> </Col>
</Row> </Row>
)} <Row align="middle">
</SettingGroup> <Label>{t('chat.settings.context_count')}</Label>
<SettingGroup> <Tooltip title={t('chat.settings.context_count.tip')}>
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle> <CircleHelp size={14} color="var(--color-text-2)" />
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall>
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip> </Tooltip>
</SettingRowTitleSmall> </Row>
<Switch <Row align="middle" gutter={10}>
size="small" <Col span={24}>
checked={thoughtAutoCollapse} <Slider
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))} min={0}
/> max={maxContextCount}
</SettingRow> onChange={setContextCount}
<SettingDivider /> onChangeComplete={onContextCountChange}
<SettingRow> value={typeof contextCount === 'number' ? contextCount : 0}
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall> step={1}
<StyledSelect />
value={messageStyle} </Col>
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))} </Row>
style={{ width: 135 }} <Divider style={{ margin: '10px 0' }} />
size="small"> <SettingRow>
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option> <SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option> <Switch
</StyledSelect> size="small"
</SettingRow> checked={streamOutput}
<SettingDivider /> onChange={(checked) => {
<SettingRow> setStreamOutput(checked)
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall> onUpdateAssistantSettings({ streamOutput: checked })
<StyledSelect
size="small"
value={multiModelMessageStyle}
onChange={(value) =>
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
}
style={{ width: 135 }}>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
style={{ width: 135 }}>
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<StyledSelect
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
style={{ width: 135 }}
size="small">
<Select.Option value="KaTeX">KaTeX</Select.Option>
<Select.Option value="MathJax">MathJax</Select.Option>
<Select.Option value="none">{t('settings.messages.math_engine.none')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={22}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}} }}
/> />
</Col> </SettingRow>
</Row> <SettingDivider />
</SettingGroup> <Row align="middle" justify="space-between" style={{ marginBottom: 10 }}>
<SettingGroup> <HStack alignItems="center">
<SettingSubtitle style={{ marginTop: 0 }}>{t('chat.settings.code.title')}</SettingSubtitle> <Label>{t('chat.settings.max_tokens')}</Label>
<SettingDivider /> <Tooltip title={t('chat.settings.max_tokens.tip')}>
<SettingRow> <CircleHelp size={14} color="var(--color-text-2)" />
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall> </Tooltip>
<StyledSelect </HStack>
value={codeStyle} <Switch
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)} size="small"
style={{ width: 135 }} checked={enableMaxTokens}
size="small"> onChange={async (enabled) => {
{themeNames.map((theme) => ( if (enabled) {
<Select.Option key={theme} value={theme}> const confirmed = await modalConfirm({
{theme} title: t('chat.settings.max_tokens.confirm'),
</Select.Option> content: t('chat.settings.max_tokens.confirm_content'),
))} okButtonProps: {
</StyledSelect> danger: true
</SettingRow> }
<SettingDivider /> })
<SettingRow> if (!confirmed) return
<SettingRowTitleSmall> }
{t('chat.settings.code_execution.title')} setEnableMaxTokens(enabled)
<Tooltip title={t('chat.settings.code_execution.tip')}> onUpdateAssistantSettings({ enableMaxTokens: enabled })
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" /> }}
</Tooltip> />
</SettingRowTitleSmall> </Row>
<Switch {enableMaxTokens && (
size="small" <Row align="middle" gutter={10}>
checked={codeExecution.enabled} <Col span={24}>
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))} <InputNumber
/> disabled={!enableMaxTokens}
</SettingRow> min={0}
{codeExecution.enabled && ( max={10000000}
<> step={100}
<SettingDivider /> value={typeof maxTokens === 'number' ? maxTokens : 0}
<SettingRow style={{ paddingLeft: 8 }}> changeOnBlur
<SettingRowTitleSmall> onChange={(value) => value && setMaxTokens(value)}
{t('chat.settings.code_execution.timeout_minutes')} onBlur={() => onMaxTokensChange(maxTokens)}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}> style={{ width: '100%' }}
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" /> />
</Tooltip> </Col>
</SettingRowTitleSmall> </Row>
<InputNumber )}
size="small" </SettingGroup>
min={1} </CollapsibleSettingGroup>
max={60} <CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
<SettingGroup>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall>
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={thoughtAutoCollapse}
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<StyledSelect
value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))}
style={{ width: 135 }}
size="small">
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option>
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={multiModelMessageStyle}
onChange={(value) =>
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
}
style={{ width: 135 }}>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
style={{ width: 135 }}>
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<StyledSelect
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
style={{ width: 135 }}
size="small">
<Select.Option value="KaTeX">KaTeX</Select.Option>
<Select.Option value="MathJax">MathJax</Select.Option>
<Select.Option value="none">{t('settings.messages.math_engine.none')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={22}
step={1} step={1}
value={codeExecution.timeoutMinutes} marks={{
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))} 12: <span style={{ fontSize: '12px' }}>A</span>,
style={{ width: 80 }} 14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/> />
</SettingRow> </Col>
</> </Row>
)} </SettingGroup>
<SettingDivider /> </CollapsibleSettingGroup>
<SettingRow> <CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall> <SettingGroup>
<Switch <SettingDivider />
size="small" <SettingRow>
checked={codeEditor.enabled} <SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))} <StyledSelect
/> value={codeStyle}
</SettingRow> onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
{codeEditor.enabled && ( style={{ width: 135 }}
<> size="small">
<SettingDivider /> {themeNames.map((theme) => (
<SettingRow style={{ paddingLeft: 8 }}> <Select.Option key={theme} value={theme}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall> {theme}
<Switch </Select.Option>
size="small" ))}
checked={codeEditor.highlightActiveLine} </StyledSelect>
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))} </SettingRow>
/> <SettingDivider />
</SettingRow> <SettingRow>
<SettingDivider /> <SettingRowTitleSmall>
<SettingRow style={{ paddingLeft: 8 }}> {t('chat.settings.code_execution.title')}
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall> <Tooltip title={t('chat.settings.code_execution.tip')}>
<Switch <CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
size="small" </Tooltip>
checked={codeEditor.foldGutter} </SettingRowTitleSmall>
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))} <Switch
/> size="small"
</SettingRow> checked={codeExecution.enabled}
<SettingDivider /> onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
<SettingRow style={{ paddingLeft: 8 }}> />
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall> </SettingRow>
<Switch {codeExecution.enabled && (
size="small" <>
checked={codeEditor.autocompletion} <SettingDivider />
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))} <SettingRow style={{ paddingLeft: 8 }}>
/> <SettingRowTitleSmall>
</SettingRow> {t('chat.settings.code_execution.timeout_minutes')}
<SettingDivider /> <Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<SettingRow style={{ paddingLeft: 8 }}> <CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall> </Tooltip>
<Switch </SettingRowTitleSmall>
size="small" <InputNumber
checked={codeEditor.keymap} size="small"
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))} min={1}
/> max={60}
</SettingRow> step={1}
</> value={codeExecution.timeoutMinutes}
)} onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
<SettingDivider /> style={{ width: 80 }}
<SettingRow> />
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall> </SettingRow>
<Switch </>
size="small" )}
checked={codeShowLineNumbers} <SettingDivider />
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))} <SettingRow>
/> <SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
</SettingRow> <Switch
<SettingDivider /> size="small"
<SettingRow> checked={codeEditor.enabled}
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall> onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
<Switch />
size="small" </SettingRow>
checked={codeCollapsible} {codeEditor.enabled && (
onChange={(checked) => dispatch(setCodeCollapsible(checked))} <>
/> <SettingDivider />
</SettingRow> <SettingRow style={{ paddingLeft: 8 }}>
<SettingDivider /> <SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<SettingRow> <Switch
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall> size="small"
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} /> checked={codeEditor.highlightActiveLine}
</SettingRow> onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
</SettingGroup> />
<SettingGroup> </SettingRow>
<SettingSubtitle style={{ marginTop: 10 }}>{t('settings.messages.input.title')}</SettingSubtitle> <SettingDivider />
<SettingDivider /> <SettingRow style={{ paddingLeft: 8 }}>
<SettingRow> <SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall> <Switch
<Switch size="small"
size="small" checked={codeEditor.foldGutter}
checked={showInputEstimatedTokens} onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))} />
/> </SettingRow>
</SettingRow> <SettingDivider />
<SettingDivider /> <SettingRow style={{ paddingLeft: 8 }}>
<SettingRow> <SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall> <Switch
<Switch size="small"
size="small" checked={codeEditor.autocompletion}
checked={pasteLongTextAsFile} onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))} />
/> </SettingRow>
</SettingRow> <SettingDivider />
{pasteLongTextAsFile && ( <SettingRow style={{ paddingLeft: 8 }}>
<> <SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<SettingDivider /> <Switch
<SettingRow> size="small"
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall> checked={codeEditor.keymap}
<InputNumber onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
size="small" />
min={500} </SettingRow>
max={10000} </>
step={100} )}
value={pasteLongTextThreshold} <SettingDivider />
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))} <SettingRow>
style={{ width: 80 }} <SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
/> <Switch
</SettingRow> size="small"
</> checked={codeShowLineNumbers}
)} onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
<SettingDivider /> />
<SettingRow> </SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall> <SettingDivider />
<Switch <SettingRow>
size="small" <SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
checked={renderInputMessageAsMarkdown} <Switch
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))} size="small"
/> checked={codeCollapsible}
</SettingRow> onChange={(checked) => dispatch(setCodeCollapsible(checked))}
<SettingDivider /> />
{!language.startsWith('en') && ( </SettingRow>
<> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch <Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
size="small" </SettingRow>
checked={autoTranslateWithSpace} </SettingGroup>
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))} </CollapsibleSettingGroup>
/> <CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}>
</SettingRow> <SettingGroup>
<SettingDivider /> <SettingDivider />
</> <SettingRow>
)} <SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<SettingRow> <Switch
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall> size="small"
<Switch checked={showInputEstimatedTokens}
size="small" onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
checked={showTranslateConfirm} />
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))} </SettingRow>
/> <SettingDivider />
</SettingRow> <SettingRow>
<SettingDivider /> <SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<SettingRow> <Switch
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall> size="small"
<Switch checked={pasteLongTextAsFile}
size="small" onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
checked={enableQuickPanelTriggers} />
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))} </SettingRow>
/> {pasteLongTextAsFile && (
</SettingRow> <>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<Switch <InputNumber
size="small" size="small"
checked={enableBackspaceDeleteModel} min={500}
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))} max={10000}
/> step={100}
</SettingRow> value={pasteLongTextThreshold}
<SettingDivider /> onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
<SettingRow> style={{ width: 80 }}
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall> />
<StyledSelect </SettingRow>
defaultValue={'english' as TranslateLanguageVarious} </>
size="small" )}
value={targetLanguage} <SettingDivider />
menuItemSelectedIcon={<CheckOutlined />} <SettingRow>
options={[ <SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
{ value: 'chinese', label: t('settings.input.target_language.chinese') }, <Switch
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') }, size="small"
{ value: 'english', label: t('settings.input.target_language.english') }, checked={renderInputMessageAsMarkdown}
{ value: 'japanese', label: t('settings.input.target_language.japanese') }, onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
{ value: 'russian', label: t('settings.input.target_language.russian') } />
]} </SettingRow>
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)} <SettingDivider />
style={{ width: 135 }} {!language.startsWith('en') && (
/> <>
</SettingRow> <SettingRow>
<SettingDivider /> <SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<SettingRow> <Switch
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall> size="small"
<StyledSelect checked={autoTranslateWithSpace}
size="small" onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
value={sendMessageShortcut} />
menuItemSelectedIcon={<CheckOutlined />} </SettingRow>
options={[ <SettingDivider />
{ value: 'Enter', label: 'Enter' }, </>
{ value: 'Shift+Enter', label: 'Shift + Enter' }, )}
{ value: 'Ctrl+Enter', label: 'Ctrl + Enter' }, <SettingRow>
{ value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` } <SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
]} <Switch
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)} size="small"
style={{ width: 135 }} checked={showTranslateConfirm}
/> onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
</SettingRow> />
</SettingGroup> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableQuickPanelTriggers}
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableBackspaceDeleteModel}
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<StyledSelect
defaultValue={'english' as TranslateLanguageVarious}
size="small"
value={targetLanguage}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'chinese', label: t('settings.input.target_language.chinese') },
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') },
{ value: 'english', label: t('settings.input.target_language.english') },
{ value: 'japanese', label: t('settings.input.target_language.japanese') },
{ value: 'russian', label: t('settings.input.target_language.russian') }
]}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)}
style={{ width: 135 }}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: 'Enter' },
{ value: 'Shift+Enter', label: 'Shift + Enter' },
{ value: 'Ctrl+Enter', label: 'Ctrl + Enter' },
{ value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` }
]}
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
style={{ width: 135 }}
/>
</SettingRow>
</SettingGroup>
</CollapsibleSettingGroup>
</Container> </Container>
) )
} }
@ -677,7 +685,7 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px; font-size: 13px;
` `
export const SettingGroup = styled.div<{ theme?: ThemeMode }>` const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px; padding: 0 5px;
width: 100%; width: 100%;
margin-top: 0; margin-top: 0;

View File

@ -0,0 +1,62 @@
import { ThemeMode } from '@renderer/types'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { useState } from 'react'
import styled from 'styled-components'
export const CollapsibleSettingGroup = styled(({ title, children, defaultExpanded = true, extra, ...rest }) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
return (
<SettingGroup {...rest}>
<GroupHeader>
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{ display: 'flex', alignItems: 'center', flex: 1, cursor: 'pointer' }}>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
<GroupTitle>{title}</GroupTitle>
</div>
{extra && <div>{extra}</div>}
</GroupHeader>
<AnimatePresence initial={false}>
{isExpanded && (
<ContentContainer
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}>
<div>{children}</div>
</ContentContainer>
)}
</AnimatePresence>
</SettingGroup>
)
})`
margin-bottom: 4px;
`
const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px;
width: 100%;
margin-top: 0;
border-radius: 8px;
margin-bottom: 10px;
`
const GroupHeader = styled.div`
display: flex;
align-items: center;
cursor: pointer;
padding: 6px 0;
user-select: none;
`
const GroupTitle = styled.div`
font-weight: 500;
margin-left: 4px;
font-size: 14px;
`
const ContentContainer = styled(motion.div)`
overflow: hidden;
`