feat: Add a new grid mode for message display. (#1626)

* chore(version): 0.9.23

* feat(renderer): 新增网格模式的消息展示方式

* feat(message): 新增消息网格展示相关设置

* 根据 gridPopoverTrigger 属性动态设置消息分组的样式

* 在 MessageMenubar 组件中,各个按钮 click 事件阻止事件冒泡,避免打开 popover

* 多模型回答样式添加网格模式并优化消息样式

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
cl1107 2025-02-17 16:36:01 +08:00 committed by GitHub
parent e3115d00bf
commit bad2f15c1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 221 additions and 51 deletions

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "Fold", "message.multi_model_style.fold": "Fold",
"message.multi_model_style.horizontal": "Horizontal", "message.multi_model_style.horizontal": "Horizontal",
"message.multi_model_style.vertical": "Vertical", "message.multi_model_style.vertical": "Vertical",
"message.multi_model_style.grid": "Grid",
"message.style": "Message style", "message.style": "Message style",
"message.style.bubble": "Bubble", "message.style.bubble": "Bubble",
"message.style.plain": "Plain", "message.style.plain": "Plain",
@ -636,6 +637,8 @@
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input message", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math engine", "messages.math_engine": "Math engine",
"messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.model.title": "Model Settings", "messages.model.title": "Model Settings",
"messages.title": "Message Settings", "messages.title": "Message Settings",

View File

@ -363,6 +363,7 @@
"message.multi_model_style.fold": "折りたたむ", "message.multi_model_style.fold": "折りたたむ",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "グリッド",
"message.style": "メッセージスタイル", "message.style": "メッセージスタイル",
"message.style.bubble": "バブル", "message.style.bubble": "バブル",
"message.style.plain": "プレーン", "message.style.plain": "プレーン",
@ -636,6 +637,8 @@
"messages.input.title": "入力設定", "messages.input.title": "入力設定",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング", "messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン", "messages.math_engine": "数式エンジン",
"messages.grid_columns": "メッセージグリッドの表示列数",
"messages.grid_popover_trigger": "グリッド詳細トリガー",
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec", "messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
"messages.model.title": "モデル設定", "messages.model.title": "モデル設定",
"messages.title": "メッセージ設定", "messages.title": "メッセージ設定",

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "Свернуть", "message.multi_model_style.fold": "Свернуть",
"message.multi_model_style.horizontal": "Горизонтальный", "message.multi_model_style.horizontal": "Горизонтальный",
"message.multi_model_style.vertical": "Вертикальный", "message.multi_model_style.vertical": "Вертикальный",
"message.multi_model_style.grid": "клетчатый вид",
"message.style": "Стиль сообщения", "message.style": "Стиль сообщения",
"message.style.bubble": "Пузырь", "message.style.bubble": "Пузырь",
"message.style.plain": "Простой", "message.style.plain": "Простой",
@ -637,6 +638,8 @@
"messages.math_engine": "Математический движок", "messages.math_engine": "Математический движок",
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
"messages.model.title": "Настройки модели", "messages.model.title": "Настройки модели",
"messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.title": "Настройки сообщений", "messages.title": "Настройки сообщений",
"messages.use_serif_font": "Использовать serif шрифт", "messages.use_serif_font": "Использовать serif шрифт",
"model": "Модель по умолчанию", "model": "Модель по умолчанию",

View File

@ -366,6 +366,7 @@
"message.multi_model_style.fold": "折叠", "message.multi_model_style.fold": "折叠",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "网格",
"message.style": "消息样式", "message.style": "消息样式",
"message.style.bubble": "气泡", "message.style.bubble": "气泡",
"message.style.plain": "简洁", "message.style.plain": "简洁",
@ -636,6 +637,8 @@
"messages.input.title": "输入设置", "messages.input.title": "输入设置",
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息", "messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
"messages.math_engine": "数学公式引擎", "messages.math_engine": "数学公式引擎",
"messages.grid_columns": "消息网格展示列数",
"messages.grid_popover_trigger": "网格详情触发",
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", "messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.model.title": "模型设置", "messages.model.title": "模型设置",
"messages.title": "消息设置", "messages.title": "消息设置",

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "折疊", "message.multi_model_style.fold": "折疊",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "网格",
"message.style": "消息樣式", "message.style": "消息樣式",
"message.style.bubble": "氣泡", "message.style.bubble": "氣泡",
"message.style.plain": "簡潔", "message.style.plain": "簡潔",
@ -635,6 +636,8 @@
"messages.input.show_estimated_tokens": "顯示預估 Token 數", "messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.input.title": "輸入設定", "messages.input.title": "輸入設定",
"messages.math_engine": "Markdown 渲染輸入訊息", "messages.math_engine": "Markdown 渲染輸入訊息",
"messages.grid_columns": "消息網格展示列數",
"messages.grid_popover_trigger": "網格詳情觸發",
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", "messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.model.title": "模型設定", "messages.model.title": "模型設定",
"messages.title": "訊息設定", "messages.title": "訊息設定",

View File

@ -1,4 +1,10 @@
import { ColumnHeightOutlined, ColumnWidthOutlined, DeleteOutlined, FolderOutlined } from '@ant-design/icons' import {
ColumnHeightOutlined,
ColumnWidthOutlined,
DeleteOutlined,
FolderOutlined,
NumberOutlined
} from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -6,7 +12,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings' import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model, Topic } from '@renderer/types' import { Message, Model, Topic } from '@renderer/types'
import { Button, Segmented as AntdSegmented } from 'antd' import { Button, Popover, Segmented as AntdSegmented } from 'antd'
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
@ -32,7 +38,7 @@ const MessageGroup: FC<Props> = ({
onGetMessages, onGetMessages,
onDeleteGroupMessages onDeleteGroupMessages
}) => { }) => {
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings() const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const [multiModelMessageStyle, setMultiModelMessageStyle] = const [multiModelMessageStyle, setMultiModelMessageStyle] =
@ -67,33 +73,89 @@ const MessageGroup: FC<Props> = ({
return ( return (
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}> <GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}> <GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
{messages.map((message, index) => ( {messages.map((message, index) =>
<MessageWrapper multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped ? (
$layout={multiModelMessageStyle} <Popover
$selected={index === selectedIndex} content={
$isGrouped={isGrouped} <MessageWrapper
key={message.id} $layout={multiModelMessageStyle}
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}> $selected={index === selectedIndex}
<MessageItem $isGrouped={isGrouped}
isGrouped={isGrouped} $isInPopover={true}
message={message} key={message.id}>
topic={topic} <MessageItem
index={message.index} isGrouped={isGrouped}
hidePresetMessages={hidePresetMessages} message={message}
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }} topic={topic}
onSetMessages={onSetMessages} index={message.index}
onDeleteMessage={onDeleteMessage} hidePresetMessages={hidePresetMessages}
onGetMessages={onGetMessages} style={{
/> paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
</MessageWrapper> }}
))} onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
}
trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
key={message.id}>
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}>
<MessageItem
isGrouped={isGrouped}
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={
gridPopoverTrigger === 'hover' && isGrouped
? {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
overflow: isGrouped ? 'hidden' : 'auto',
maxHeight: isGrouped ? '280px' : 'unset'
}
: undefined
}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
</Popover>
) : (
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
<MessageItem
isGrouped={isGrouped}
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={{ paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15 }}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
)
)}
</GridContainer> </GridContainer>
{isGrouped && ( {isGrouped && (
<GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}> <GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}>
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}> <HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
<LayoutContainer> <LayoutContainer>
{['fold', 'vertical', 'horizontal'].map((layout) => ( {['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
<LayoutOption <LayoutOption
key={layout} key={layout}
active={multiModelMessageStyle === layout} active={multiModelMessageStyle === layout}
@ -102,8 +164,10 @@ const MessageGroup: FC<Props> = ({
<FolderOutlined /> <FolderOutlined />
) : layout === 'horizontal' ? ( ) : layout === 'horizontal' ? (
<ColumnWidthOutlined /> <ColumnWidthOutlined />
) : ( ) : layout === 'vertical' ? (
<ColumnHeightOutlined /> <ColumnHeightOutlined />
) : (
<NumberOutlined />
)} )}
</LayoutOption> </LayoutOption>
))} ))}
@ -143,30 +207,38 @@ const MessageGroup: FC<Props> = ({
} }
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>` const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && $layout === 'horizontal' ? '15px' : '0')}; padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
` `
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle }>` const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
width: 100%; width: 100%;
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)}, ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(550px, 1fr) minmax(550px, 1fr)
); );
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')}; gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
@media (max-width: 800px) { @media (max-width: 800px) {
grid-template-columns: repeat( grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)}, ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(400px, 1fr) minmax(400px, 1fr)
); );
} }
overflow-y: auto; overflow-y: auto;
${({ $gridColumns, $layout, $count }) =>
$layout === 'grid' &&
css`
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
grid-template-rows: auto;
gap: 16px;
`}
` `
interface MessageWrapperProps { interface MessageWrapperProps {
$layout: 'fold' | 'horizontal' | 'vertical' $layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
$selected: boolean $selected: boolean
$isGrouped: boolean $isGrouped: boolean
$isInPopover?: boolean
} }
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>` const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
@ -193,6 +265,22 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
} }
return '' return ''
}} }}
${({ $layout, $isInPopover, $isGrouped }) =>
$layout === 'grid' && $isGrouped
? css`
max-height: ${$isInPopover ? '50vh' : '300px'};
overflow-y: auto;
border: 0.5px solid var(--color-border);
padding: 10px;
border-radius: 6px;
`
: css`
overflow-y: auto;
border: 0.5px solid transparent;
padding: 0 10px;
border-radius: 6px;
`}
` `
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>` const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`

View File

@ -60,12 +60,16 @@ const MessageMenubar: FC<Props> = (props) => {
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const onCopy = useCallback(() => { const onCopy = useCallback(
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content)) (e: React.MouseEvent) => {
window.message.success({ content: t('message.copied'), key: 'copy-message' }) e.stopPropagation()
setCopied(true) navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
setTimeout(() => setCopied(false), 2000) window.message.success({ content: t('message.copied'), key: 'copy-message' })
}, [message.content, t]) setCopied(true)
setTimeout(() => setCopied(false), 2000)
},
[message.content, t]
)
const onNewBranch = useCallback(async () => { const onNewBranch = useCallback(async () => {
await modelGenerating() await modelGenerating()
@ -195,14 +199,16 @@ const MessageMenubar: FC<Props> = (props) => {
[message, onEdit, onNewBranch, t] [message, onEdit, onNewBranch, t]
) )
const onRegenerate = async () => { const onRegenerate = async (e: React.MouseEvent | undefined) => {
e?.stopPropagation?.()
await modelGenerating() await modelGenerating()
const selectedModel = isGrouped ? model : assistantModel const selectedModel = isGrouped ? model : assistantModel
const _message = resetAssistantMessage(message, selectedModel) const _message = resetAssistantMessage(message, selectedModel)
onEditMessage?.(_message) onEditMessage?.(_message)
} }
const onMentionModel = async () => { const onMentionModel = async (e: React.MouseEvent) => {
e.stopPropagation()
await modelGenerating() await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model }) const selectedModel = await SelectModelPopup.show({ model })
if (!selectedModel) return if (!selectedModel) return
@ -216,9 +222,13 @@ const MessageMenubar: FC<Props> = (props) => {
onEditMessage?.(_message) onEditMessage?.(_message)
} }
const onUseful = useCallback(() => { const onUseful = useCallback(
onEditMessage?.({ ...message, useful: !message.useful }) (e: React.MouseEvent) => {
}, [message, onEditMessage]) e.stopPropagation()
onEditMessage?.({ ...message, useful: !message.useful })
},
[message, onEditMessage]
)
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
@ -270,13 +280,14 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'translate-close', key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined }) onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
} }
] ],
onClick: (e) => e.domEvent.stopPropagation()
}} }}
trigger={['click']} trigger={['click']}
placement="topRight" placement="topRight"
arrow> arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}> <Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button"> <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<TranslationOutlined /> <TranslationOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -298,14 +309,25 @@ const MessageMenubar: FC<Props> = (props) => {
<Tooltip title={t('common.delete')} mouseEnterDelay={1}> <Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton <ActionButton
className="message-action-button" className="message-action-button"
onClick={isGrouped ? () => onDeleteMessage?.(message) : undefined}> onClick={
isGrouped
? (e) => {
e.stopPropagation()
onDeleteMessage?.(message)
}
: (e) => e.stopPropagation()
}>
<DeleteOutlined /> <DeleteOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
{!isUserMessage && ( {!isUserMessage && (
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow> <Dropdown
<ActionButton className="message-action-button"> menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight"
arrow>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<MenuOutlined /> <MenuOutlined />
</ActionButton> </ActionButton>
</Dropdown> </Dropdown>

View File

@ -19,6 +19,8 @@ import {
setCodeShowLineNumbers, setCodeShowLineNumbers,
setCodeStyle, setCodeStyle,
setFontSize, setFontSize,
setGridColumns,
setGridPopoverTrigger,
setMathEngine, setMathEngine,
setMessageFont, setMessageFont,
setMessageStyle, setMessageStyle,
@ -42,13 +44,14 @@ interface Props {
const SettingsTab: FC<Props> = (props) => { const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id) const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const { messageStyle, codeStyle, fontSize, language } = useSettings() const { messageStyle, codeStyle, fontSize, language, gridColumns } = useSettings()
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [fontSizeValue, setFontSizeValue] = useState(fontSize) const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const [gridColumnsValue, setGridColumnsValue] = useState(gridColumns)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const { t } = useTranslation() const { t } = useTranslation()
@ -69,7 +72,8 @@ const SettingsTab: FC<Props> = (props) => {
mathEngine, mathEngine,
autoTranslateWithSpace, autoTranslateWithSpace,
pasteLongTextThreshold, pasteLongTextThreshold,
multiModelMessageStyle multiModelMessageStyle,
gridPopoverTrigger
} = useSettings() } = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => { const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -283,6 +287,7 @@ const SettingsTab: FC<Props> = (props) => {
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option> <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="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="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -313,6 +318,34 @@ const SettingsTab: FC<Props> = (props) => {
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.grid_popover_trigger')}</SettingRowTitleSmall>
<Select
value={gridPopoverTrigger || 'hover'}
onChange={(value) => dispatch(setGridPopoverTrigger(value))}
style={{ width: 135 }}
size="small">
<Select.Option value="hover">hover</Select.Option>
<Select.Option value="click">click</Select.Option>
</Select>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.grid_columns')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={gridColumnsValue}
onChange={(value) => setGridColumnsValue(value)}
onChangeComplete={(value) => dispatch(setGridColumns(value))}
min={2}
max={9}
step={1}
/>
</Col>
</Row>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow> </SettingRow>

View File

@ -44,6 +44,8 @@ export interface SettingsState {
mathEngine: 'MathJax' | 'KaTeX' mathEngine: 'MathJax' | 'KaTeX'
messageStyle: 'plain' | 'bubble' messageStyle: 'plain' | 'bubble'
codeStyle: CodeStyleVarious codeStyle: CodeStyleVarious
gridColumns: number
gridPopoverTrigger: 'hover' | 'click'
// webdav 配置 host, user, pass, path // webdav 配置 host, user, pass, path
webdavHost: string webdavHost: string
webdavUser: string webdavUser: string
@ -69,7 +71,7 @@ export interface SettingsState {
notionApiKey: string | null notionApiKey: string | null
} }
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
const initialState: SettingsState = { const initialState: SettingsState = {
showAssistants: true, showAssistants: true,
@ -99,6 +101,8 @@ const initialState: SettingsState = {
mathEngine: 'KaTeX', mathEngine: 'KaTeX',
messageStyle: 'plain', messageStyle: 'plain',
codeStyle: 'auto', codeStyle: 'auto',
gridColumns: 2,
gridPopoverTrigger: 'hover',
webdavHost: '', webdavHost: '',
webdavUser: '', webdavUser: '',
webdavPass: '', webdavPass: '',
@ -224,6 +228,12 @@ const settingsSlice = createSlice({
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => { setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
state.mathEngine = action.payload state.mathEngine = action.payload
}, },
setGridColumns: (state, action: PayloadAction<number>) => {
state.gridColumns = action.payload
},
setGridPopoverTrigger: (state, action: PayloadAction<'hover' | 'click'>) => {
state.gridPopoverTrigger = action.payload
},
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => { setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
state.messageStyle = action.payload state.messageStyle = action.payload
}, },
@ -265,7 +275,7 @@ const settingsSlice = createSlice({
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => { setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
state.enableQuickAssistant = action.payload state.enableQuickAssistant = action.payload
}, },
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => { setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold' | 'grid'>) => {
state.multiModelMessageStyle = action.payload state.multiModelMessageStyle = action.payload
}, },
setNotionDatabaseID: (state, action: PayloadAction<string>) => { setNotionDatabaseID: (state, action: PayloadAction<string>) => {
@ -310,6 +320,8 @@ export const {
setCodeShowLineNumbers, setCodeShowLineNumbers,
setCodeCollapsible, setCodeCollapsible,
setMathEngine, setMathEngine,
setGridColumns,
setGridPopoverTrigger,
setMessageStyle, setMessageStyle,
setCodeStyle, setCodeStyle,
setTranslateModelPrompt, setTranslateModelPrompt,