Compare commits

...

9 Commits

Author SHA1 Message Date
kangfenmao
1dd1bb5804 0.3.4 2024-07-23 18:10:33 +08:00
kangfenmao
4dd6c46035 fix: message style 2024-07-23 18:10:25 +08:00
kangfenmao
4036c36753 feat: add Mermaid render 2024-07-23 18:05:14 +08:00
kangfenmao
764aadd234 feat: change message font 2024-07-23 17:42:52 +08:00
kangfenmao
3d801f1552 feat: optimize message style 2024-07-23 17:32:06 +08:00
kangfenmao
bd865f0270 fix: windows title style 2024-07-23 16:55:32 +08:00
kangfenmao
93505a4bc6 feat: hide window title 2024-07-23 16:40:06 +08:00
kangfenmao
c43be11d20 feat: add username and message divider line settings 2024-07-23 15:16:34 +08:00
kangfenmao
8535edbdd1 feat: messages styles optimization 2024-07-23 14:59:09 +08:00
28 changed files with 286 additions and 90 deletions

View File

@@ -56,5 +56,6 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
支持配置网络代理
支持隐藏左侧智能体
Windows 版本界面优化
新增 mermaid 图表支持
消息样式优化

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.3.3",
"version": "0.3.4",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",

View File

@@ -24,7 +24,12 @@ function createWindow() {
minHeight: 500,
show: true,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
titleBarStyle: 'hidden',
titleBarOverlay: {
height: 41,
color: '#1f1f1f',
symbolColor: '#eee'
},
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {

View File

@@ -7,7 +7,7 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
</head>
<body theme-mode="dark">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -31,7 +31,9 @@
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-error: #f44336;
--navbar-background: #1f1f1f;
--navbar-height: 42px;
--sidebar-width: 55px;
--assistants-width: 250px;

View File

@@ -1,9 +1,13 @@
.markdown {
color: #fff;
font-size: 15px;
color: #f1f1f1;
font-family: Georgia, Cambria, 'Times New Roman', Times, serif;
font-size: 16px;
line-height: 1.6;
user-select: text;
margin-top: 4px;
p:last-child {
margin-bottom: 5px;
}
p:first-of-type {
margin-top: 0;

View File

@@ -26,11 +26,10 @@ const NavbarContainer = styled.div`
min-height: var(--navbar-height);
max-height: var(--navbar-height);
-webkit-app-region: drag;
background-color: #1f1f1f;
background-color: var(--navbar-background);
margin-left: calc(var(--sidebar-width) * -1);
padding-left: var(--sidebar-width);
border-bottom: 0.5px solid var(--color-border);
border-top: 0.5px solid var(--color-border);
`
const NavbarLeftContainer = styled.div`

View File

@@ -13,7 +13,7 @@ const Sidebar: FC = () => {
return (
<Container style={isWindows ? { paddingTop: 0 } : {}}>
{isMac && <PlaceholderBorder />}
{isMac ? <PlaceholderBorderMac /> : <PlaceholderBorderWin />}
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
@@ -52,7 +52,6 @@ const Container = styled.div`
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
border-top: 0.5px solid var(--color-border);
padding-top: var(--navbar-height);
position: relative;
`
@@ -115,7 +114,7 @@ const StyledLink = styled(Link)`
}
`
const PlaceholderBorder = styled.div`
const PlaceholderBorderMac = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
border-right: 1px solid #1f1f1f;
@@ -125,4 +124,13 @@ const PlaceholderBorder = styled.div`
left: 0.5px;
`
const PlaceholderBorderWin = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
position: absolute;
border-right: 1px solid #1f1f1f;
top: -1px;
right: -1px;
`
export default Sidebar

View File

@@ -15,7 +15,7 @@ import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.jpeg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'

View File

@@ -9,5 +9,6 @@ declare global {
message: MessageInstance
modal: HookAPI
keyv: KeyvStorage
mermaid: any
}
}

View File

@@ -23,7 +23,8 @@ const resources = {
duplicate: 'Duplicate',
copy: 'Copy',
regenerate: 'Regenerate',
provider: 'Provider'
provider: 'Provider',
you: 'You'
},
button: {
add: 'Add',
@@ -104,6 +105,9 @@ const resources = {
assistant: 'Default Assistant',
about: 'About & Feedback',
'general.title': 'General Settings',
'general.message.divider': 'Show divider between messages',
'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name',
'provider.api_key': 'API Key',
'provider.check': 'Check',
'provider.get_api_key': 'Get API Key',
@@ -168,7 +172,8 @@ const resources = {
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商'
provider: '提供商',
you: '用户'
},
button: {
add: '添加',
@@ -250,6 +255,9 @@ const resources = {
assistant: '默认助手',
about: '关于我们',
'general.title': '常规设置',
'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名',
'general.message.divider': '消息分割线',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥',

View File

@@ -1,7 +1,7 @@
import localforage from 'localforage'
import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer'
import { isProduction } from './utils'
import { isProduction, loadScript } from './utils'
async function initSentry() {
if (await isProduction()) {
@@ -21,6 +21,15 @@ async function initSentry() {
}
}
async function initMermaid() {
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
window.mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose'
})
}
function init() {
localforage.config({
driver: localforage.INDEXEDDB,
@@ -34,6 +43,7 @@ function init() {
window.keyv.init()
initSentry()
initMermaid()
}
init()

View File

@@ -9,8 +9,7 @@ import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore
import { Tooltip } from 'antd'
import Navigation from './components/NavigationCenter'
import { useTranslation } from 'react-i18next'
import { PlusCircleOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isMac, isWindows } from '@renderer/config/constant'
const HomePage: FC = () => {
const { assistants, addAssistant } = useAssistants()
@@ -35,12 +34,12 @@ const HomePage: FC = () => {
<i className="iconfont icon-hidesidebarhoriz" />
</NewButton>
<NewButton onClick={onCreateAssistant}>
<PlusCircleOutlined />
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
)}
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', padding: '0 7px' }}>
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
<Tooltip
placement="left"
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
@@ -88,6 +87,9 @@ export const NewButton = styled.div`
align-items: center;
transition: all 0.2s ease-in-out;
color: var(--color-icon);
.icon-a-addchat {
font-size: 20px;
}
.anticon {
font-size: 19px;
}

View File

@@ -147,7 +147,7 @@ const AssistantItem = styled.div`
const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: bold;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

View File

@@ -4,6 +4,7 @@ import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import Mermaid from './Mermaid'
interface CodeBlockProps {
children: string
@@ -21,6 +22,10 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
window.message.success({ content: t('message.copied'), key: 'copy-code' })
}
if (match && match[1] === 'mermaid') {
return <Mermaid chart={children} />
}
return match ? (
<div>
<CodeHeader>

View File

@@ -4,9 +4,9 @@ import {
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MessageOutlined,
MoreOutlined,
PauseCircleOutlined
PauseCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@@ -134,7 +134,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<MessageOutlined />
<PlusCircleOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
@@ -170,7 +170,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
{generating && (
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}>
<PauseCircleOutlined />
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
</ToolbarButton>
</Tooltip>
)}

View File

@@ -0,0 +1,15 @@
import React, { useEffect } from 'react'
interface Props {
chart: string
}
const Mermaid: React.FC<Props> = ({ chart }) => {
useEffect(() => {
window?.mermaid?.contentLoaded()
}, [])
return <div className="mermaid">{chart}</div>
}
export default Mermaid

View File

@@ -3,7 +3,7 @@ import { Avatar, Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import useAvatar from '@renderer/hooks/useAvatar'
import { DeleteOutlined, EditOutlined, SwitcherOutlined } from '@ant-design/icons'
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
@@ -12,7 +12,10 @@ import Logo from '@renderer/assets/images/logo.png'
import { SyncOutlined } from '@ant-design/icons'
import { firstLetter } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import { isEmpty } from 'lodash'
import { isEmpty, upperFirst } from 'lodash'
import dayjs from 'dayjs'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
interface Props {
message: Message
@@ -25,8 +28,11 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const { assistant } = useAssistant(message.assistantId)
const { userName, showMessageDivider } = useSettings()
const isLastMessage = index === 0
const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && message.role === 'assistant'
const onCopy = () => {
@@ -61,17 +67,40 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
return message.content
}
const getUserName = () => {
if (message.id === 'assistant') {
return assistant.name
}
if (message.role === 'assistant') {
return upperFirst(message.modelId)
}
return userName || t('common.you')
}
return (
<MessageContainer key={message.id}>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
{firstLetter(message.modelId).toUpperCase()}
</Avatar>
) : (
<Avatar src={avatar} />
<MessageContainer key={message.id} className="message" style={{ border: showMessageDivider ? undefined : 'none' }}>
<MessageHeader>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo} size={35}>
{firstLetter(message.modelId).toUpperCase()}
</Avatar>
) : (
<Avatar src={avatar} size={35} />
)}
<UserWrap>
<UserName>{getUserName()}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
{message.usage && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
)}
</AvatarWrapper>
</MessageHeader>
<MessageContent>
{message.status === 'sending' && (
<MessageContentLoading>
@@ -84,32 +113,31 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</Markdown>
)}
{showMenu && (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
<MenusBar className={`menubar ${isLastMessage && 'show'} ${isUserMessage && 'user'}`}>
{message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}>
<EditOutlined onClick={onEdit} />
<ActionButton>
<EditOutlined onClick={onEdit} />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<SwitcherOutlined onClick={onCopy} />
<ActionButton>
<CopyOutlined onClick={onCopy} />
</ActionButton>
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<DeleteOutlined onClick={onDelete} />
<ActionButton>
<DeleteOutlined onClick={onDelete} />
</ActionButton>
</Tooltip>
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<SyncOutlined onClick={onRegenerate} />
<ActionButton>
<SyncOutlined onClick={onRegenerate} />
</ActionButton>
</Tooltip>
)}
<MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && (
<>
<MessageMetadata>
tokens: {message.usage.total_tokens} (in:{message.usage.prompt_tokens}/out:
{message.usage.completion_tokens})
</MessageMetadata>
</>
)}
</MenusBar>
)}
</MessageContent>
@@ -119,26 +147,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
padding: 10px;
position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar {
opacity: 0;
transition: opacity 0.2s ease;
&.show {
opacity: 1;
}
&.user {
position: absolute;
top: 15px;
right: 10px;
}
}
&:hover {
.menubar {
@@ -147,6 +170,47 @@ const MessageContent = styled.div`
}
`
const MessageHeader = styled.div`
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-left: 46px;
margin-top: 5px;
`
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
@@ -157,17 +221,9 @@ const MessageContentLoading = styled.div`
const MenusBar = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
justify-content: flex-end;
align-items: center;
gap: 6px;
.anticon {
cursor: pointer;
margin-right: 8px;
font-size: 15px;
color: var(--color-icon);
&:hover {
color: var(--color-text-1);
}
}
`
const MessageMetadata = styled.div`
@@ -176,4 +232,24 @@ const MessageMetadata = styled.div`
user-select: text;
`
const ActionButton = styled.div`
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
.anticon {
cursor: pointer;
font-size: 14px;
color: var(--color-icon);
}
&:hover {
color: var(--color-text-1);
}
`
export default MessageItem

View File

@@ -119,7 +119,10 @@ const Container = styled.div`
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding-top: 10px;
padding-bottom: 20px;
padding-bottom: 10px;
.message:first-child {
border: none;
}
`
export default Messages

View File

@@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { capitalizeFirstLetter } from '@renderer/utils'
import { isMac } from '@renderer/config/constant'
import { upperFirst } from 'lodash'
interface Props {
activeAssistant: Assistant
@@ -47,7 +47,7 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
<AssistantName>{assistant?.name}</AssistantName>
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<DropdownButton size="small" type="primary" ghost>
{model ? capitalizeFirstLetter(model.name) : t('button.select_model')}
{model ? upperFirst(model.name) : t('button.select_model')}
</DropdownButton>
</DropdownMenu>
</NavbarCenter>

View File

@@ -1,6 +1,6 @@
import { FC, useState } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, Input, Select, Upload } from 'antd'
import { Avatar, Input, Select, Switch, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage, isValidProxyUrl } from '@renderer/utils'
@@ -8,14 +8,14 @@ import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage } from '@renderer/store/settings'
import { setLanguage, setShowMessageDivider, setUserName } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => {
const avatar = useAvatar()
const { language, proxyUrl: storeProxyUrl } = useSettings()
const { language, proxyUrl: storeProxyUrl, userName, showMessageDivider } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch()
const { t } = useTranslation()
@@ -74,6 +74,17 @@ const GeneralSettings: FC = () => {
</Upload>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.user_name')}</SettingRowTitle>
<Input
placeholder={t('settings.general.user_name.placeholder')}
value={userName}
onChange={(e) => dispatch(setUserName(e.target.value))}
style={{ width: 150 }}
maxLength={30}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
@@ -86,6 +97,11 @@ const GeneralSettings: FC = () => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.message.divider')}</SettingRowTitle>
<Switch checked={showMessageDivider} onChange={(checked) => dispatch(setShowMessageDivider(checked))} />
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}

View File

@@ -39,6 +39,7 @@ export const SettingRow = styled.div`
flex-direction: row;
justify-content: space-between;
align-items: center;
min-height: 40px;
`
export const SettingRowTitle = styled.div`

View File

@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 14,
version: 15,
blacklist: ['runtime'],
migrate
},

View File

@@ -254,6 +254,17 @@ const migrate = createMigrate({
proxyUrl: undefined
}
}
},
// @ts-ignore store type is unknown
'15': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
userName: '',
showMessageDivider: true
}
}
}
})

View File

@@ -8,6 +8,8 @@ export interface SettingsState {
sendMessageShortcut: SendMessageShortcut
language: string
proxyUrl?: string
userName: string
showMessageDivider: boolean
}
const initialState: SettingsState = {
@@ -15,7 +17,9 @@ const initialState: SettingsState = {
showAssistants: true,
sendMessageShortcut: 'Enter',
language: navigator.language,
proxyUrl: undefined
proxyUrl: undefined,
userName: '',
showMessageDivider: true
}
const settingsSlice = createSlice({
@@ -36,11 +40,24 @@ const settingsSlice = createSlice({
},
setProxyUrl: (state, action: PayloadAction<string | undefined>) => {
state.proxyUrl = action.payload
},
setUserName: (state, action: PayloadAction<string>) => {
state.userName = action.payload
},
setShowMessageDivider: (state, action: PayloadAction<boolean>) => {
state.showMessageDivider = action.payload
}
}
})
export const { toggleRightSidebar, toggleShowAssistants, setSendMessageShortcut, setLanguage, setProxyUrl } =
settingsSlice.actions
export const {
toggleRightSidebar,
toggleShowAssistants,
setSendMessageShortcut,
setLanguage,
setProxyUrl,
setUserName,
setShowMessageDivider
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -199,12 +199,24 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
return all.usedTokens - 7
}
// 首字母大写
export const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// is valid proxy url
/**
* is valid proxy url
* @param url proxy url
* @returns boolean
*/
export const isValidProxyUrl = (url: string) => {
return url.includes('://')
}
export function loadScript(url: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}