Merge branch 'develop' into mutiple-select

This commit is contained in:
Pleasurecruise 2025-05-18 15:28:59 +08:00
commit ada130b907
No known key found for this signature in database
GPG Key ID: E6385136096279B6
6 changed files with 607 additions and 559 deletions

View File

@ -764,13 +764,13 @@
"type": {
"embedding": "嵌入",
"free": "免费",
"function_calling": "工具",
"reasoning": "推理",
"rerank": "重排",
"select": "选择模型类型",
"text": "文本",
"vision": "图像",
"function_calling": "函数调用",
"websearch": "[to be translated]:WebSearch"
"vision": "视觉",
"websearch": "联网"
}
},
"navbar": {

View File

@ -1,5 +1,6 @@
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
@ -7,7 +8,7 @@ import { setFoldDisplayMode } from '@renderer/store/settings'
import type { Model } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
import { FC } from 'react'
import { FC, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -25,39 +26,54 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
const { foldDisplayMode } = useSettings()
const isCompact = foldDisplayMode === 'compact'
const renderLabel = useCallback(
(message: Message) => {
const modelTip = message.model?.name
if (isCompact) {
return (
<Tooltip key={message.id} title={modelTip} mouseEnterDelay={0.5}>
<AvatarWrapper
className="avatar-wrapper"
$isSelected={message.id === selectMessageId}
onClick={() => {
setSelectedMessage(message)
}}>
<ModelAvatar model={message.model as Model} size={22} />
</AvatarWrapper>
</Tooltip>
)
}
return (
<SegmentedLabel>
<ModelAvatar model={message.model as Model} size={20} />
<ModelName>{message.model?.name}</ModelName>
</SegmentedLabel>
)
},
[isCompact, selectMessageId, setSelectedMessage]
)
return (
<ModelsWrapper>
<Container>
<DisplayModeToggle
displayMode={foldDisplayMode}
onClick={() => dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}>
<Tooltip
title={
foldDisplayMode === 'compact'
isCompact
? t(`message.message.multi_model_style.fold.expand`)
: t('message.message.multi_model_style.fold.compress')
}
placement="top">
{foldDisplayMode === 'compact' ? <ArrowsAltOutlined /> : <ShrinkOutlined />}
{isCompact ? <ArrowsAltOutlined /> : <ShrinkOutlined />}
</Tooltip>
</DisplayModeToggle>
<ModelsContainer $displayMode={foldDisplayMode}>
{foldDisplayMode === 'compact' ? (
{isCompact ? (
/* Compact style display */
<Avatar.Group className="avatar-group">
{messages.map((message, index) => (
<Tooltip key={index} title={message.model?.name} placement="top" mouseEnterDelay={0.2}>
<AvatarWrapper
className="avatar-wrapper"
isSelected={message.id === selectMessageId}
onClick={() => {
setSelectedMessage(message)
}}>
<ModelAvatar model={message.model as Model} size={28} />
</AvatarWrapper>
</Tooltip>
))}
</Avatar.Group>
<Avatar.Group className="avatar-group">{messages.map((message) => renderLabel(message))}</Avatar.Group>
) : (
/* Expanded style display */
<Segmented
@ -67,45 +83,32 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
setSelectedMessage(message)
}}
options={messages.map((message) => ({
label: (
<SegmentedLabel>
<ModelAvatar model={message.model as Model} size={20} />
<ModelName>{message.model?.name}</ModelName>
</SegmentedLabel>
),
label: renderLabel(message),
value: message.id
}))}
size="small"
/>
)}
</ModelsContainer>
</ModelsWrapper>
</Container>
)
}
const ModelsWrapper = styled.div`
position: relative;
display: flex;
const Container = styled(HStack)`
flex: 1;
overflow: hidden;
align-items: center;
margin-left: 4px;
`
const DisplayModeToggle = styled.div<{ displayMode: DisplayMode }>`
position: absolute;
left: 4px; /* Add more space on the left */
top: 50%;
transform: translateY(-50%);
z-index: 5;
width: 28px; /* Increase width */
height: 28px; /* Add height */
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
padding: 2px 6px 3px 6px;
border-radius: 4px;
padding: 2px;
width: 26px;
height: 26px;
/* Add hover effect */
&:hover {
background-color: var(--color-hover);
}
@ -119,9 +122,7 @@ const ModelsContainer = styled(Scrollbar)<{ $displayMode: DisplayMode }>`
overflow-x: auto;
flex: 1;
padding: 0 8px;
margin-left: 24px; /* Space for toggle button */
/* Hide scrollbar to match original code */
&::-webkit-scrollbar {
display: none;
}
@ -131,27 +132,23 @@ const ModelsContainer = styled(Scrollbar)<{ $displayMode: DisplayMode }>`
display: flex;
align-items: center;
flex-wrap: nowrap;
position: relative;
padding: 6px 4px;
/* Base style - default overlapping effect */
& > * {
margin-left: -6px !important;
/* Separate transition properties to avoid conflicts */
transition:
transform 0.18s ease-out,
margin 0.18s ease-out !important;
position: relative;
/* Only use will-change for transform to reduce rendering overhead */
will-change: transform;
}
/* First element has no left margin */
& > *:first-child {
margin-left: 0 !important;
}
/* Using :has() selector to handle the element before the hovered one */
/* Element before the hovered one */
& > *:has(+ *:hover) {
margin-right: 2px !important;
/* Use transform instead of margin to reduce layout recalculations */
@ -171,52 +168,24 @@ const ModelsContainer = styled(Scrollbar)<{ $displayMode: DisplayMode }>`
}
`
const AvatarWrapper = styled.div<{ isSelected: boolean }>`
const AvatarWrapper = styled.div<{ $isSelected: boolean }>`
cursor: pointer;
display: inline-flex;
border-radius: 50%;
/* Keep z-index separate from transitions to avoid rendering issues */
z-index: ${(props) => (props.isSelected ? 2 : 0)};
background: var(--color-background);
/* Simplify transitions to reduce jittering */
transition:
transform 0.18s ease-out,
margin 0.18s ease-out,
box-shadow 0.18s ease-out,
filter 0.18s ease-out;
box-shadow: 0 0 0 1px var(--color-background);
/* Use CSS variables to define animation parameters for easy adjustment */
--hover-scale: 1.15;
--hover-x-offset: 6px;
--hover-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
z-index: ${(props) => (props.$isSelected ? 1 : 0)};
border: ${(props) => (props.$isSelected ? '2px solid var(--color-primary)' : 'none')};
&:hover {
/* z-index is applied immediately, not part of the transition */
z-index: 10;
transform: translateX(var(--hover-x-offset)) scale(var(--hover-scale));
box-shadow: var(--hover-shadow);
transform: translateX(6px) scale(1.15);
filter: brightness(1.02);
margin-left: 8px !important;
margin-right: 4px !important;
}
${(props) =>
props.isSelected &&
`
border: 2px solid var(--color-primary);
z-index: 2;
&:hover {
/* z-index is applied immediately, not part of the transition */
z-index: 10;
border: 2px solid var(--color-primary);
filter: brightness(1.02);
transform: translateX(var(--hover-x-offset)) scale(var(--hover-scale));
margin-left: 8px !important;
margin-right: 4px !important;
}
`}
`
const Segmented = styled(AntdSegmented)`
@ -224,21 +193,15 @@ const Segmented = styled(AntdSegmented)`
background-color: transparent !important;
.ant-segmented-item {
background-color: transparent !important;
transition: none !important;
border-radius: var(--list-item-border-radius) !important;
box-shadow: none !important;
&:hover {
background: transparent !important;
}
}
.ant-segmented-thumb,
.ant-segmented-item-selected {
background-color: transparent !important;
border: 0.5px solid var(--color-border);
transition: none !important;
border-radius: var(--list-item-border-radius) !important;
box-shadow: none !important;
}
`
@ -254,4 +217,4 @@ const ModelName = styled.span`
font-size: 12px;
`
export default MessageGroupModelList
export default memo(MessageGroupModelList)

View File

@ -11,8 +11,9 @@ import {
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useAssistant } from '@renderer/hooks/useAssistant'
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 { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { useAppDispatch } from '@renderer/store'
import {
SendMessageShortcut,
@ -181,478 +182,488 @@ const SettingsTab: FC<Props> = (props) => {
return (
<Container className="settings-tab">
<SettingGroup style={{ marginTop: 10 }}>
<SettingSubtitle style={{ marginTop: 0, display: 'flex', justifyContent: 'space-between' }}>
<CollapsibleSettingGroup
title={t('assistants.settings.title')}
defaultExpanded={true}
extra={
<HStack alignItems="center">
{t('assistants.settings.title')}{' '}
<Tooltip title={t('chat.settings.reset')}>
<RotateCcw size={20} onClick={onReset} style={{ cursor: 'pointer', padding: '0 3px' }} />
</Tooltip>
<Button
type="text"
size="small"
icon={<Settings2 size={16} />}
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
/>
</HStack>
<Button
type="text"
size="small"
icon={<Settings2 size={16} />}
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
/>
</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')}>
}>
<SettingGroup style={{ marginTop: 10 }}>
<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>
</HStack>
<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>
<Row align="middle" gutter={10}>
<Col span={24}>
<InputNumber
disabled={!enableMaxTokens}
<Slider
min={0}
max={10000000}
step={100}
value={typeof maxTokens === 'number' ? maxTokens : 0}
changeOnBlur
onChange={(value) => value && setMaxTokens(value)}
onBlur={() => onMaxTokensChange(maxTokens)}
style={{ width: '100%' }}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
)}
</SettingGroup>
<SettingGroup>
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
<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)" />
<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>
</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}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
</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>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</Col>
</Row>
</SettingGroup>
<SettingGroup>
<SettingSubtitle style={{ marginTop: 0 }}>{t('chat.settings.code.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<StyledSelect
value={codeStyle}
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
style={{ width: 135 }}
size="small">
{themeNames.map((theme) => (
<Select.Option key={theme} value={theme}>
{theme}
</Select.Option>
))}
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
/>
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
size="small"
min={1}
max={60}
</SettingRow>
<SettingDivider />
<SettingRow>
<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)" />
</Tooltip>
</HStack>
<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 })
}}
/>
</SettingRow>
{enableMaxTokens && (
<Row align="middle" gutter={10}>
<Col span={24}>
<InputNumber
disabled={!enableMaxTokens}
min={0}
max={10000000}
step={100}
value={typeof maxTokens === 'number' ? maxTokens : 0}
changeOnBlur
onChange={(value) => value && setMaxTokens(value)}
onBlur={() => onMaxTokensChange(maxTokens)}
style={{ width: '100%' }}
/>
</Col>
</Row>
)}
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<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}
value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
style={{ width: 80 }}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</SettingRow>
</>
)}
</Col>
</Row>
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
<SettingGroup>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<StyledSelect
value={codeStyle}
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
style={{ width: 135 }}
size="small">
{themeNames.map((theme) => (
<Select.Option key={theme} value={theme}>
{theme}
</Select.Option>
))}
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
/>
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
size="small"
min={1}
max={60}
step={1}
value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
/>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
</SettingRow>
</SettingGroup>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
/>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
</SettingRow>
</SettingGroup>
<SettingGroup>
<SettingSubtitle style={{ marginTop: 10 }}>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<Switch
size="small"
checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
/>
</SettingRow>
{pasteLongTextAsFile && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<InputNumber
size="small"
min={500}
max={10000}
step={100}
value={pasteLongTextThreshold}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
{!language.startsWith('en') && (
<>
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<Switch
size="small"
checked={autoTranslateWithSpace}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
/>
</SettingRow>
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showTranslateConfirm}
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
/>
</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>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}>
<SettingGroup>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<Switch
size="small"
checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
/>
</SettingRow>
{pasteLongTextAsFile && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<InputNumber
size="small"
min={500}
max={10000}
step={100}
value={pasteLongTextThreshold}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
{!language.startsWith('en') && (
<>
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<Switch
size="small"
checked={autoTranslateWithSpace}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
/>
</SettingRow>
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showTranslateConfirm}
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
/>
</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>
)
}
@ -677,7 +688,7 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px;
width: 100%;
margin-top: 0;

View File

@ -0,0 +1,61 @@
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;
`
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;
`

View File

@ -215,10 +215,9 @@ export async function getMessageTitle(message: Message, length = 30): Promise<st
try {
window.message.loading({ content: t('chat.topics.export.wait_for_title_naming'), key: 'message-title-naming' })
const tempTextBlock = createMainTextBlock(message.id, content, { status: MessageBlockStatus.SUCCESS })
const tempMessage = resetMessage(message, {
status: AssistantMessageStatus.SUCCESS,
blocks: [tempTextBlock.id]
blocks: message.blocks,
})
const title = await fetchMessagesSummary({ messages: [tempMessage], assistant: {} as Assistant })

View File

@ -805,21 +805,19 @@ export const deleteMessageGroupThunk =
const currentState = getState()
const topicMessageIds = currentState.messages.messageIdsByTopic[topicId] || []
const messagesToDelete: Message[] = []
const idsToDelete: string[] = []
topicMessageIds.forEach((id) => {
const msg = currentState.messages.entities[id]
if (msg && msg.askId === askId) {
messagesToDelete.push(msg)
idsToDelete.push(id)
}
})
const userQuery = currentState.messages.entities[askId]
if (userQuery && userQuery.topicId === topicId && !idsToDelete.includes(askId)) {
messagesToDelete.push(userQuery)
idsToDelete.push(askId)
}
// const userQuery = currentState.messages.entities[askId]
// if (userQuery && userQuery.topicId === topicId && !idsToDelete.includes(askId)) {
// messagesToDelete.push(userQuery)
// idsToDelete.push(askId)
// }
if (messagesToDelete.length === 0) {
console.warn(`[deleteMessageGroup] No messages found with askId ${askId} in topic ${topicId}.`)
@ -894,13 +892,29 @@ export const resendMessageThunk =
const resetDataList: Message[] = []
if (assistantMessagesToReset.length === 0) {
// 没有用户消息,就创建一个
const assistantMessage = createAssistantMessage(assistant.id, topicId, {
askId: userMessageToResend.id,
model: assistant.model
// 没有用户消息,就创建一个或多个
if (userMessageToResend?.mentions?.length) {
console.log('userMessageToResend.mentions', userMessageToResend.mentions)
for (const mention of userMessageToResend.mentions) {
const assistantMessage = createAssistantMessage(assistant.id, topicId, {
askId: userMessageToResend.id,
model: mention,
modelId: mention.id
})
resetDataList.push(assistantMessage)
}
} else {
const assistantMessage = createAssistantMessage(assistant.id, topicId, {
askId: userMessageToResend.id,
model: assistant.model
})
resetDataList.push(assistantMessage)
}
resetDataList.forEach((message) => {
dispatch(newMessagesActions.addMessage({ topicId, message }))
})
resetDataList.push(assistantMessage)
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
}
const allBlockIdsToDelete: string[] = []