Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd1bb5804 | ||
|
|
4dd6c46035 | ||
|
|
4036c36753 | ||
|
|
764aadd234 | ||
|
|
3d801f1552 | ||
|
|
bd865f0270 | ||
|
|
93505a4bc6 | ||
|
|
c43be11d20 | ||
|
|
8535edbdd1 |
@@ -56,5 +56,6 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
支持配置网络代理
|
||||
支持隐藏左侧智能体
|
||||
Windows 版本界面优化
|
||||
新增 mermaid 图表支持
|
||||
消息样式优化
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 |
BIN
src/renderer/src/assets/images/models/qwen.png
Normal file
BIN
src/renderer/src/assets/images/models/qwen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
1
src/renderer/src/env.d.ts
vendored
1
src/renderer/src/env.d.ts
vendored
@@ -9,5 +9,6 @@ declare global {
|
||||
message: MessageInstance
|
||||
modal: HookAPI
|
||||
keyv: KeyvStorage
|
||||
mermaid: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': '点击这里获取密钥',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
15
src/renderer/src/pages/home/components/Mermaid.tsx
Normal file
15
src/renderer/src/pages/home/components/Mermaid.tsx
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 14,
|
||||
version: 15,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user