mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 18:10:26 +08:00
parent
0634baf780
commit
ceef19e55b
@ -2960,6 +2960,7 @@
|
|||||||
"none": "None"
|
"none": "None"
|
||||||
},
|
},
|
||||||
"prompt": "Show prompt",
|
"prompt": "Show prompt",
|
||||||
|
"show_message_outline": "Show message outline",
|
||||||
"title": "Message Settings",
|
"title": "Message Settings",
|
||||||
"use_serif_font": "Use serif font"
|
"use_serif_font": "Use serif font"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2960,6 +2960,7 @@
|
|||||||
"none": "表示しない"
|
"none": "表示しない"
|
||||||
},
|
},
|
||||||
"prompt": "プロンプト表示",
|
"prompt": "プロンプト表示",
|
||||||
|
"show_message_outline": "メッセージの概要を表示します",
|
||||||
"title": "メッセージ設定",
|
"title": "メッセージ設定",
|
||||||
"use_serif_font": "セリフフォントを使用"
|
"use_serif_font": "セリフフォントを使用"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2960,6 +2960,7 @@
|
|||||||
"none": "Не показывать"
|
"none": "Не показывать"
|
||||||
},
|
},
|
||||||
"prompt": "Показывать подсказки",
|
"prompt": "Показывать подсказки",
|
||||||
|
"show_message_outline": "Показать наброски сообщения",
|
||||||
"title": "Настройки сообщений",
|
"title": "Настройки сообщений",
|
||||||
"use_serif_font": "Использовать serif шрифт"
|
"use_serif_font": "Использовать serif шрифт"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2960,6 +2960,7 @@
|
|||||||
"none": "不显示"
|
"none": "不显示"
|
||||||
},
|
},
|
||||||
"prompt": "显示提示词",
|
"prompt": "显示提示词",
|
||||||
|
"show_message_outline": "显示消息大纲",
|
||||||
"title": "消息设置",
|
"title": "消息设置",
|
||||||
"use_serif_font": "使用衬线字体"
|
"use_serif_font": "使用衬线字体"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2960,6 +2960,7 @@
|
|||||||
"none": "不顯示"
|
"none": "不顯示"
|
||||||
},
|
},
|
||||||
"prompt": "提示詞顯示",
|
"prompt": "提示詞顯示",
|
||||||
|
"show_message_outline": "顯示消息大綱",
|
||||||
"title": "訊息設定",
|
"title": "訊息設定",
|
||||||
"use_serif_font": "使用襯線字型"
|
"use_serif_font": "使用襯線字型"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { Pluggable } from 'unified'
|
|||||||
|
|
||||||
import CodeBlock from './CodeBlock'
|
import CodeBlock from './CodeBlock'
|
||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
|
import rehypeHeadingIds from './plugins/rehypeHeadingIds'
|
||||||
import remarkDisableConstructs from './plugins/remarkDisableConstructs'
|
import remarkDisableConstructs from './plugins/remarkDisableConstructs'
|
||||||
import Table from './Table'
|
import Table from './Table'
|
||||||
|
|
||||||
@ -110,17 +111,18 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
|||||||
}, [block, displayedContent, t])
|
}, [block, displayedContent, t])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
const plugins: any[] = []
|
const plugins: Pluggable[] = []
|
||||||
if (ALLOWED_ELEMENTS.test(messageContent)) {
|
if (ALLOWED_ELEMENTS.test(messageContent)) {
|
||||||
plugins.push(rehypeRaw)
|
plugins.push(rehypeRaw)
|
||||||
}
|
}
|
||||||
|
plugins.push([rehypeHeadingIds, { prefix: `heading-${block.id}` }])
|
||||||
if (mathEngine === 'KaTeX') {
|
if (mathEngine === 'KaTeX') {
|
||||||
plugins.push(rehypeKatex as any)
|
plugins.push(rehypeKatex)
|
||||||
} else if (mathEngine === 'MathJax') {
|
} else if (mathEngine === 'MathJax') {
|
||||||
plugins.push(rehypeMathjax as any)
|
plugins.push(rehypeMathjax)
|
||||||
}
|
}
|
||||||
return plugins
|
return plugins
|
||||||
}, [mathEngine, messageContent])
|
}, [mathEngine, messageContent, block.id])
|
||||||
|
|
||||||
const onSaveCodeBlock = useCallback(
|
const onSaveCodeBlock = useCallback(
|
||||||
(id: string, newContent: string) => {
|
(id: string, newContent: string) => {
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
import type { Root, Node, Element, Text } from 'hast'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 GitHub 风格的标题 slug 生成器(去重逻辑)
|
||||||
|
* - 小写
|
||||||
|
* - 去除前后空白
|
||||||
|
* - 移除部分标点
|
||||||
|
* - 将空白与非字母数字字符合并为单个 '-'
|
||||||
|
* - 多次出现的相同 slug 加上递增后缀(-1, -2...)
|
||||||
|
*/
|
||||||
|
export function createSlugger() {
|
||||||
|
const seen = new Map<string, number>()
|
||||||
|
const normalize = (text: string): string => {
|
||||||
|
const slug = (text || 'section')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
// 移除常见分隔符和标点
|
||||||
|
.replace(/[\u200B-\u200D\uFEFF]/g, '') // 零宽字符
|
||||||
|
.replace(/["'`(){}[\]:;!?.,]/g, '')
|
||||||
|
// 将空白和非字母数字字符转换为 '-'
|
||||||
|
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||||
|
// 合并多余的 '-'
|
||||||
|
.replace(/-{2,}/g, '-')
|
||||||
|
// 去除首尾 '-'
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
|
||||||
|
return slug
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = (text: string): string => {
|
||||||
|
const base = normalize(text)
|
||||||
|
const count = seen.get(base) || 0
|
||||||
|
seen.set(base, count + 1)
|
||||||
|
return `${base}-${count}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return { slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTextFromNode(node: Node | Text | Element | null | undefined): string {
|
||||||
|
if (!node) return ''
|
||||||
|
|
||||||
|
if (typeof (node as Text).value === 'string') {
|
||||||
|
return (node as Text).value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((node as Element).children?.length) {
|
||||||
|
return (node as Element).children.map(extractTextFromNode).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function rehypeHeadingIds(options?: { prefix?: string }) {
|
||||||
|
return (tree: Root) => {
|
||||||
|
const slugger = createSlugger()
|
||||||
|
const prefix = options?.prefix ? `${options.prefix}--` : ''
|
||||||
|
visit(tree, 'element', (node) => {
|
||||||
|
if (!node || typeof node.tagName !== 'string') return
|
||||||
|
const tag = node.tagName.toLowerCase()
|
||||||
|
if (!/^h[1-6]$/.test(tag)) return
|
||||||
|
|
||||||
|
const text = extractTextFromNode(node)
|
||||||
|
const id = prefix + slugger.slug(text)
|
||||||
|
node.properties = node.properties || {}
|
||||||
|
if (!node.properties.id) node.properties.id = id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import MessageEditor from './MessageEditor'
|
|||||||
import MessageErrorBoundary from './MessageErrorBoundary'
|
import MessageErrorBoundary from './MessageErrorBoundary'
|
||||||
import MessageHeader from './MessageHeader'
|
import MessageHeader from './MessageHeader'
|
||||||
import MessageMenubar from './MessageMenubar'
|
import MessageMenubar from './MessageMenubar'
|
||||||
|
import MessageOutline from './MessageOutline'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -66,7 +67,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const { isMultiSelectMode } = useChatContext(topic)
|
const { isMultiSelectMode } = useChatContext(topic)
|
||||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { messageFont, fontSize, messageStyle } = useSettings()
|
const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings()
|
||||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||||
@ -183,6 +184,9 @@ const MessageItem: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<>
|
<>
|
||||||
|
{!isMultiSelectMode && message.role === 'assistant' && showMessageOutline && (
|
||||||
|
<MessageOutline message={message} />
|
||||||
|
)}
|
||||||
<MessageContentContainer
|
<MessageContentContainer
|
||||||
className="message-content-container"
|
className="message-content-container"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -378,7 +378,7 @@ interface MessageWrapperProps {
|
|||||||
|
|
||||||
const MessageWrapper = styled.div<MessageWrapperProps>`
|
const MessageWrapper = styled.div<MessageWrapperProps>`
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
padding-right: 1px;
|
padding: 1px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
.message {
|
.message {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
180
src/renderer/src/pages/home/Messages/MessageOutline.tsx
Normal file
180
src/renderer/src/pages/home/Messages/MessageOutline.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { RootState } from '@renderer/store'
|
||||||
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
|
import { Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
|
import React, { FC, useMemo, useRef } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import remarkParse from 'remark-parse'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { unified } from 'unified'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
|
import { createSlugger, extractTextFromNode } from '../Markdown/plugins/rehypeHeadingIds'
|
||||||
|
|
||||||
|
interface MessageOutlineProps {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeadingItem {
|
||||||
|
id: string
|
||||||
|
level: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageOutline: FC<MessageOutlineProps> = ({ message }) => {
|
||||||
|
const blockEntities = useSelector((state: RootState) => messageBlocksSelectors.selectEntities(state))
|
||||||
|
|
||||||
|
const headings: HeadingItem[] = useMemo(() => {
|
||||||
|
const mainTextBlocks = message.blocks
|
||||||
|
.map((blockId) => blockEntities[blockId])
|
||||||
|
.filter((b) => b?.type === MessageBlockType.MAIN_TEXT)
|
||||||
|
|
||||||
|
if (!mainTextBlocks?.length) return []
|
||||||
|
|
||||||
|
const result: HeadingItem[] = []
|
||||||
|
mainTextBlocks.forEach((mainTextBlock) => {
|
||||||
|
const tree = unified().use(remarkParse).parse(mainTextBlock?.content)
|
||||||
|
const slugger = createSlugger()
|
||||||
|
visit(tree, ['heading', 'html'], (node) => {
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
const level = node.depth ?? 0
|
||||||
|
if (!level || level < 1 || level > 6) return
|
||||||
|
const text = extractTextFromNode(node)
|
||||||
|
if (!text) return
|
||||||
|
const id = `heading-${mainTextBlock.id}--` + slugger.slug(text || '')
|
||||||
|
result.push({ id, level, text: text })
|
||||||
|
} else if (node.type === 'html') {
|
||||||
|
// 匹配 <h1>...</h1> 到 <h6>...</h6>
|
||||||
|
const match = node.value.match(/<h([1-6])[^>]*>(.*?)<\/h\1>/i)
|
||||||
|
if (match) {
|
||||||
|
const level = parseInt(match[1], 10)
|
||||||
|
const text = match[2].replace(/<[^>]*>/g, '').trim() // 移除内部的HTML标签
|
||||||
|
if (text) {
|
||||||
|
const id = `heading-${mainTextBlock.id}--${slugger.slug(text || '')}`
|
||||||
|
result.push({ id, level, text })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [message.blocks, blockEntities])
|
||||||
|
|
||||||
|
const miniLevel = useMemo(() => {
|
||||||
|
return headings.length ? Math.min(...headings.map((heading) => heading.level)) : 1
|
||||||
|
}, [headings])
|
||||||
|
|
||||||
|
const messageOutlineContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const scrollToHeading = (id: string) => {
|
||||||
|
const parent = messageOutlineContainerRef.current?.parentElement
|
||||||
|
const messageContentContainer = parent?.querySelector('.message-content-container')
|
||||||
|
if (messageContentContainer) {
|
||||||
|
const headingElement = messageContentContainer.querySelector(`#${id}`)
|
||||||
|
if (headingElement) {
|
||||||
|
const scrollBlock = ['horizontal', 'grid'].includes(message.multiModelMessageStyle ?? '') ? 'nearest' : 'start'
|
||||||
|
headingElement.scrollIntoView({ behavior: 'smooth', block: scrollBlock })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂时不支持 grid,因为在锚点滚动时会导致渲染错位
|
||||||
|
if (message.multiModelMessageStyle === 'grid' || !headings.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageOutlineContainer ref={messageOutlineContainerRef}>
|
||||||
|
<MessageOutlineBody $count={headings.length}>
|
||||||
|
{headings.map((heading, index) => (
|
||||||
|
<MessageOutlineItem key={index} onClick={() => scrollToHeading(heading.id)}>
|
||||||
|
<MessageOutlineItemDot $level={heading.level} />
|
||||||
|
<MessageOutlineItemText $level={heading.level} $miniLevel={miniLevel}>
|
||||||
|
{heading.text}
|
||||||
|
</MessageOutlineItemText>
|
||||||
|
</MessageOutlineItem>
|
||||||
|
))}
|
||||||
|
</MessageOutlineBody>
|
||||||
|
</MessageOutlineContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageOutlineContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
inset: 63px 0 36px 10px;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: none;
|
||||||
|
& ~ .message-content-container {
|
||||||
|
padding-left: 46px !important;
|
||||||
|
}
|
||||||
|
& ~ .MessageFooter {
|
||||||
|
margin-left: 46px !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItemDot = styled.div<{ $level: number }>`
|
||||||
|
width: ${({ $level }) => 16 - $level * 2}px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItemText = styled.div<{ $level: number; $miniLevel: number }>`
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
padding: 2px 8px;
|
||||||
|
padding-left: ${({ $level, $miniLevel }) => ($level - $miniLevel) * 8}px;
|
||||||
|
font-size: ${({ $level }) => 16 - $level}px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineItem = styled.div`
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
&:hover {
|
||||||
|
${MessageOutlineItemText} {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
${MessageOutlineItemDot} {
|
||||||
|
background: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MessageOutlineBody = styled(Scrollbar)<{ $count: number }>`
|
||||||
|
max-width: 50%;
|
||||||
|
max-height: min(100%, 70vh);
|
||||||
|
position: sticky;
|
||||||
|
top: max(calc(50% - ${({ $count }) => ($count * 24) / 2 + 10}px), 20px);
|
||||||
|
bottom: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
pointer-events: auto;
|
||||||
|
&:hover {
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 0 10px 0 rgba(128, 128, 128, 0.2);
|
||||||
|
${MessageOutlineItemText} {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default React.memo(MessageOutline)
|
||||||
@ -38,6 +38,7 @@ import {
|
|||||||
setPasteLongTextThreshold,
|
setPasteLongTextThreshold,
|
||||||
setRenderInputMessageAsMarkdown,
|
setRenderInputMessageAsMarkdown,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
|
setShowMessageOutline,
|
||||||
setShowPrompt,
|
setShowPrompt,
|
||||||
setShowTranslateConfirm,
|
setShowTranslateConfirm,
|
||||||
setThoughtAutoCollapse
|
setThoughtAutoCollapse
|
||||||
@ -103,7 +104,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
messageNavigation,
|
messageNavigation,
|
||||||
enableQuickPanelTriggers,
|
enableQuickPanelTriggers,
|
||||||
enableBackspaceDeleteModel,
|
enableBackspaceDeleteModel,
|
||||||
showTranslateConfirm
|
showTranslateConfirm,
|
||||||
|
showMessageOutline
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
@ -332,6 +334,15 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.show_message_outline')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={showMessageOutline}
|
||||||
|
onChange={(checked) => dispatch(setShowMessageOutline(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
|
||||||
<Selector
|
<Selector
|
||||||
|
|||||||
@ -217,6 +217,7 @@ export interface SettingsState {
|
|||||||
navbarPosition: 'left' | 'top'
|
navbarPosition: 'left' | 'top'
|
||||||
// API Server
|
// API Server
|
||||||
apiServer: ApiServerConfig
|
apiServer: ApiServerConfig
|
||||||
|
showMessageOutline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
@ -404,7 +405,8 @@ export const initialState: SettingsState = {
|
|||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 23333,
|
port: 23333,
|
||||||
apiKey: `cs-sk-${uuid()}`
|
apiKey: `cs-sk-${uuid()}`
|
||||||
}
|
},
|
||||||
|
showMessageOutline: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -833,6 +835,9 @@ const settingsSlice = createSlice({
|
|||||||
...state.apiServer,
|
...state.apiServer,
|
||||||
apiKey: action.payload
|
apiKey: action.payload
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setShowMessageOutline: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.showMessageOutline = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -958,6 +963,7 @@ export const {
|
|||||||
setS3Partial,
|
setS3Partial,
|
||||||
setEnableDeveloperMode,
|
setEnableDeveloperMode,
|
||||||
setNavbarPosition,
|
setNavbarPosition,
|
||||||
|
setShowMessageOutline,
|
||||||
// API Server actions
|
// API Server actions
|
||||||
setApiServerEnabled,
|
setApiServerEnabled,
|
||||||
setApiServerPort,
|
setApiServerPort,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user