Compare commits

...

29 Commits

Author SHA1 Message Date
kangfenmao
5e954566c9 chore(version): 0.5.7 2024-08-16 17:41:30 +08:00
kangfenmao
b8960ef02c fix: windows frame background color 2024-08-16 17:41:14 +08:00
kangfenmao
1866b00265 feat: add user edit modal & add prompt block 2024-08-16 17:19:18 +08:00
kangfenmao
be0799a4c6 chore(version): 0.5.6 2024-08-14 21:32:14 +08:00
kangfenmao
d0f5547419 feat: new windows and linux sidebar style 2024-08-14 21:28:44 +08:00
kangfenmao
076011b02b fix: anthropic message generating error 2024-08-14 20:35:57 +08:00
kangfenmao
ba5c70c45a feat: add minapp popup 2024-08-14 19:47:58 +08:00
kangfenmao
ebe74ffd05 chore(version): 0.5.5 2024-08-13 21:10:04 +08:00
kangfenmao
d0bea0491f fix(settings): provider list scroll 2024-08-13 21:04:04 +08:00
kangfenmao
514e1a4796 chore: remove ahooks 2024-08-13 20:50:54 +08:00
kangfenmao
2ffedadee4 Revert "feat(translate): use full screen input"
This reverts commit b0c479190c.
2024-08-13 20:48:51 +08:00
kangfenmao
7b72783ae7 feat: add graphrag provider 2024-08-13 20:48:38 +08:00
kangfenmao
4485a00395 feat: add doubao provider 2024-08-13 19:41:01 +08:00
kangfenmao
77c0952635 feat: add stepfun provider 2024-08-13 18:02:00 +08:00
kangfenmao
e1c7a25b87 feat: add gemini provider 2024-08-13 16:51:52 +08:00
kangfenmao
b0c479190c feat(translate): use full screen input 2024-08-13 14:57:46 +08:00
kangfenmao
c7c3d28893 chore(version): 0.5.4 2024-08-12 22:37:09 +08:00
kangfenmao
994ee8d7df feat: change sidebar opacity 2024-08-12 22:35:35 +08:00
kangfenmao
57f9550891 feat: add font size options to assistant settings pannel 2024-08-12 22:21:47 +08:00
kangfenmao
0c0d1560db feat: about page add icons 2024-08-12 22:03:16 +08:00
kangfenmao
145d7ee748 refactor: slider onChange event 2024-08-12 21:48:59 +08:00
kangfenmao
52af23b931 feat: enable anthropic api host edit 2024-08-12 21:31:32 +08:00
kangfenmao
f7151bd066 feat: add change message font size feature #22
支持消息字体大小调节
2024-08-12 21:28:18 +08:00
kangfenmao
744a1fedba style(Inputbar): add width: auto to Textarea 2024-08-11 16:18:06 +08:00
kangfenmao
978432d910 fix: clear topic white generating 2024-08-11 16:11:31 +08:00
kangfenmao
b6cb1e4d84 refactor: code format 2024-08-11 15:49:08 +08:00
kangfenmao
0096783f26 chore(version): 0.5.3 2024-08-09 18:58:58 +08:00
kangfenmao
4fc53d7c19 feat: new inputbar style 2024-08-09 18:56:45 +08:00
亢奋猫
34d99b711c Update README.md 2024-08-09 11:30:56 +08:00
78 changed files with 1613 additions and 626 deletions

View File

@@ -56,4 +56,6 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
错误修复,优化体验 增加 Gemini、豆包、阶跃星辰等服务商支持
修复 Anthropic 回复问题
新的 Windows, Linux 侧边栏样式

View File

@@ -15,7 +15,6 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src') '@renderer': resolve('src/renderer/src')
} }
}, },
plugins: [react()], plugins: [react()]
assetsInclude: ['**/*.md']
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "cherry-studio", "name": "cherry-studio",
"version": "0.5.2", "version": "0.5.7",
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "kangfenmao@qq.com", "author": "kangfenmao@qq.com",
@@ -37,6 +37,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.16.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
@@ -45,8 +46,8 @@
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"ahooks": "^3.8.0",
"antd": "^5.18.3", "antd": "^5.18.3",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dotenv-cli": "^7.4.2", "dotenv-cli": "^7.4.2",

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="initial-scale=1, width=device-width" /> <meta name="viewport" content="initial-scale=1, width=device-width" />
<meta <meta
http-equiv="Content-Security-Policy" 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' data: *; 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' data: *; img-src 'self' data:; frame-src *" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4563475 */ font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722242729348') format('woff2'), src: url('iconfont.woff2?t=1723186111414') format('woff2'),
url('iconfont.woff?t=1722242729348') format('woff'), url('iconfont.woff?t=1723186111414') format('woff'),
url('iconfont.ttf?t=1722242729348') format('truetype'); url('iconfont.ttf?t=1723186111414') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-copy:before {
content: "\e6ae";
}
.icon-ic_send:before {
content: "\e795";
}
.icon-dark1:before { .icon-dark1:before {
content: "\e72f"; content: "\e72f";
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Standard_product_icon__x28_1:1_x29_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="192px" height="192px"
viewBox="0 0 192 192" enable-background="new 0 0 192 192" xml:space="preserve">
<symbol id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" viewBox="-96 -96 192 192">
<g opacity="0.4">
<defs>
<path id="SVGID_1_" opacity="0.4" d="M-96,96V-96H96V96H-96z"/>
</defs>
<clipPath id="SVGID_00000071517564283228984050000017848131202901217410_">
<use xlink:href="#SVGID_1_" overflow="visible"/>
</clipPath>
<g clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)">
<g>
<path d="M95.75,95.75v-191.5h-191.5v191.5H95.75 M96,96H-96V-96H96V96L96,96z"/>
</g>
<circle fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="64"/>
</g>
<circle clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="88"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M64,76H-64c-6.6,0-12-5.4-12-12V-64c0-6.6,5.4-12,12-12H64c6.6,0,12,5.4,12,12V64C76,70.6,70.6,76,64,76z"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M52,88H-52c-6.6,0-12-5.4-12-12V-76c0-6.6,5.4-12,12-12H52c6.6,0,12,5.4,12,12V76C64,82.6,58.6,88,52,88z"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M76,64H-76c-6.6,0-12-5.4-12-12V-52c0-6.6,5.4-12,12-12H76c6.6,0,12,5.4,12,12V52C88,58.6,82.6,64,76,64z"/>
</g>
</symbol>
<rect id="bounding_box_1_" display="none" fill="none" width="192" height="192"/>
<g id="art_layer">
<g>
<path fill="#F9AB00" d="M96,181.92L96,181.92c6.63,0,12-5.37,12-12v-104H84v104C84,176.55,89.37,181.92,96,181.92z"/>
<g>
<path fill="#5BB974" d="M143.81,103.87C130.87,90.94,111.54,88.32,96,96l51.37,51.37c2.12,2.12,5.77,1.28,6.67-1.57
C158.56,131.49,155.15,115.22,143.81,103.87z"/>
</g>
<g>
<path fill="#129EAF" d="M48.19,103.87C61.13,90.94,80.46,88.32,96,96l-51.37,51.37c-2.12,2.12-5.77,1.28-6.67-1.57
C33.44,131.49,36.85,115.22,48.19,103.87z"/>
</g>
<g>
<path fill="#AF5CF7" d="M140,64c-20.44,0-37.79,13.4-44,32h81.24c3.33,0,5.55-3.52,4.04-6.49C173.56,74.36,157.98,64,140,64z"/>
</g>
<g>
<path fill="#FF8BCB" d="M104.49,42.26C90.03,56.72,87.24,78.45,96,96l57.45-57.45c2.36-2.36,1.44-6.42-1.73-7.45
C135.54,25.85,117.2,29.55,104.49,42.26z"/>
</g>
<g>
<path fill="#FA7B17" d="M87.51,42.26C101.97,56.72,104.76,78.45,96,96L38.55,38.55c-2.36-2.36-1.44-6.42,1.73-7.45
C56.46,25.85,74.8,29.55,87.51,42.26z"/>
</g>
<g>
<g>
<path fill="#4285F4" d="M52,64c20.44,0,37.79,13.4,44,32H14.76c-3.33,0-5.55-3.52-4.04-6.49C18.44,74.36,34.02,64,52,64z"/>
</g>
</g>
</g>
</g>
<g id="keylines" display="none">
<use xlink:href="#material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" width="192" height="192" id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines" x="-96" y="-96" transform="matrix(1 0 0 -1 96 96)" display="inline" overflow="visible"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -31,15 +31,17 @@
--color-text: var(--color-text-1); --color-text: var(--color-text-1);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff20; --color-border: #000;
--color-border-soft: #ffffff20;
--color-error: #f44336; --color-error: #f44336;
--color-link: #1677ff; --color-link: #1677ff;
--color-code-background: #323232; --color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15); --color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3); --color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
--navbar-background: rgba(0, 0, 0, 0.8); --navbar-background-mac: rgba(30, 30, 30, 0.8);
--sidebar-background: rgba(0, 0, 0, 0.8); --navbar-background: rgba(30, 30, 30);
--input-bar-background: rgba(255, 255, 255, 0.02);
--navbar-height: 42px; --navbar-height: 42px;
--sidebar-width: 55px; --sidebar-width: 55px;
@@ -47,7 +49,7 @@
--topic-list-width: 260px; --topic-list-width: 260px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 115px; --input-bar-height: 85px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -79,14 +81,16 @@ body[theme-mode='light'] {
--color-icon: #00000099; --color-icon: #00000099;
--color-icon-white: #000000; --color-icon-white: #000000;
--color-border: #00000028; --color-border: #00000028;
--color-border-soft: #00000028;
--color-error: #f44336; --color-error: #f44336;
--color-link: #1677ff; --color-link: #1677ff;
--color-code-background: #e3e3e3; --color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15); --color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3); --color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
--navbar-background: rgba(255, 255, 255, 0.8); --navbar-background-mac: rgba(255, 255, 255, 0.75);
--sidebar-background: rgba(255, 255, 255, 0.8); --navbar-background: rgba(255, 255, 255);
--input-bar-background: rgba(0, 0, 0, 0.02);
} }
*, *,
@@ -105,6 +109,7 @@ body {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
color: var(--color-text); color: var(--color-text);
font-size: 14px;
line-height: 1.6; line-height: 1.6;
overflow: hidden; overflow: hidden;
background: transparent !important; background: transparent !important;
@@ -155,3 +160,44 @@ body,
position: relative; position: relative;
animation: flash 0.5s ease-out infinite alternate; animation: flash 0.5s ease-out infinite alternate;
} }
.ant-segmented-group {
gap: 4px;
}
.dragable {
-webkit-app-region: drag;
}
.dragdisable {
-webkit-app-region: no-drag;
}
.minapp-drawer {
.ant-drawer-header-title {
flex-direction: row-reverse;
}
.ant-drawer-close {
position: absolute;
top: 6px;
right: 15px;
padding: 15px;
margin-right: -5px;
-webkit-app-region: no-drag;
z-index: 100000;
}
.ant-drawer-header {
height: calc(var(--navbar-height) + 0.5px);
background: var(--navbar-background);
width: calc(100vw - var(--sidebar-width));
padding-right: 10px !important;
border-bottom: 0.5px solid var(--color-border);
margin-top: -0.5px;
}
.ant-drawer-body {
padding: 0;
}
.minapp-mask {
background-color: transparent !important;
}
}

View File

@@ -1,8 +1,8 @@
.markdown { .markdown {
color: var(--color-text); color: var(--color-text);
font-size: 15px;
line-height: 1.6; line-height: 1.6;
user-select: text; user-select: text;
word-break: break-word;
h1:first-child, h1:first-child,
h2:first-child, h2:first-child,
@@ -72,6 +72,9 @@
li { li {
margin-bottom: 0.5em; margin-bottom: 0.5em;
&::marker {
color: var(--color-text-3);
}
} }
li > ul, li > ul,
@@ -103,7 +106,7 @@
pre { pre {
white-space: pre-wrap !important; white-space: pre-wrap !important;
padding: 1em; padding: 1em 0;
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace; font-family: 'Fira Code', 'Courier New', Courier, monospace;

View File

@@ -0,0 +1,24 @@
import { getModelLogo } from '@renderer/config/provider'
import { Model } from '@renderer/types'
import { Avatar, AvatarProps } from 'antd'
import { first } from 'lodash'
import { FC } from 'react'
interface Props {
model: Model
size: number
props?: AvatarProps
}
const ModelAvatar: FC<Props> = ({ model, size, props }) => {
return (
<Avatar
src={getModelLogo(model?.id || '')}
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
{...props}>
{first(model?.name)}
</Avatar>
)
}
export default ModelAvatar

View File

@@ -0,0 +1,125 @@
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { Drawer } from 'antd'
import { useRef, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface ShowParams {
title: string
url: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
const [open, setOpen] = useState(true)
const iframeRef = useRef<HTMLIFrameElement>(null)
const onClose = () => {
setOpen(false)
setTimeout(() => resolve({}), 300)
}
const onReload = () => {
if (iframeRef.current) {
iframeRef.current.src = url
}
}
const onOpenLink = () => {
window.api.openWebsite(url)
}
return (
<Drawer
title={title}
placement="bottom"
onClose={onClose}
open={open}
mask={true}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
<Frame src={url} ref={iframeRef} />
<ButtonsGroup>
<Button onClick={onReload}>
<ReloadOutlined />
</Button>
<Button onClick={onOpenLink}>
<ExportOutlined />
</Button>
<Button onClick={onClose}>
<CloseOutlined />
</Button>
</ButtonsGroup>
</Drawer>
)
}
const Frame = styled.iframe`
width: calc(100vw - var(--sidebar-width));
height: calc(100vh - var(--navbar-height));
border: none;
`
const ButtonsGroup = styled.div`
position: absolute;
top: 0;
right: 0;
height: var(--navbar-height);
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
padding: 0 10px;
`
const Button = styled.div`
-webkit-app-region: no-drag;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
`
export default class MinApp {
static topviewId = 0
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
}
static start(props: ShowParams) {
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
}

View File

@@ -0,0 +1,108 @@
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings'
import { compressImage } from '@renderer/utils'
import { Avatar, Input, Modal, Upload } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Center, HStack } from '../Layout'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { userName } = useSettings()
const dispatch = useAppDispatch()
const avatar = useAvatar()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
return (
<Modal
width="300px"
open={open}
footer={null}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down">
<Center mt="30px">
<Upload
customRequest={() => {}}
accept="image/png, image/jpeg"
itemRender={() => null}
maxCount={1}
onChange={async ({ file }) => {
try {
const _file = file.originFileObj as File
const compressedFile = await compressImage(_file)
await LocalStorage.storeImage('avatar', compressedFile)
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
} catch (error: any) {
window.message.error(error.message)
}
}}>
<UserAvatar src={avatar} />
</Upload>
</Center>
<HStack alignItems="center" gap="10px" p="20px">
<Input
placeholder={t('settings.general.user_name.placeholder')}
value={userName}
onChange={(e) => dispatch(setUserName(e.target.value))}
style={{ flex: 1, textAlign: 'center', width: '100%' }}
maxLength={30}
/>
</HStack>
</Modal>
)
}
const UserAvatar = styled(Avatar)`
cursor: pointer;
width: 80px;
height: 80px;
transition: opacity 0.3s ease;
&:hover {
opacity: 0.8;
}
`
export default class UserPopup {
static topviewId = 0
static hide() {
TopView.hide('UserPopup')
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'UserPopup'
)
})
}
}

View File

@@ -39,7 +39,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
}, [messageApi, modal]) }, [messageApi, modal])
onPop = () => { onPop = () => {
console.debug('[TopView] onPop')
const views = [...elementsRef.current] const views = [...elementsRef.current]
views.pop() views.pop()
elementsRef.current = views elementsRef.current = views
@@ -47,8 +46,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
} }
onShow = ({ element, id }: ElementItem) => { onShow = ({ element, id }: ElementItem) => {
console.debug('[TopView] onShow', id)
if (!elementsRef.current.find((el) => el.id === id)) { if (!elementsRef.current.find((el) => el.id === id)) {
elementsRef.current = elementsRef.current.concat([{ element, id }]) elementsRef.current = elementsRef.current.concat([{ element, id }])
setElements(elementsRef.current) setElements(elementsRef.current)
@@ -56,13 +53,11 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
} }
onHide = (id: string) => { onHide = (id: string) => {
console.debug('[TopView] onHide', id, elementsRef.current)
elementsRef.current = elementsRef.current.filter((el) => el.id !== id) elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
setElements(elementsRef.current) setElements(elementsRef.current)
} }
onHideAll = () => { onHideAll = () => {
console.debug('[TopView] onHideAll')
setElements([]) setElements([])
elementsRef.current = [] elementsRef.current = []
} }
@@ -76,11 +71,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
) )
}, []) }, [])
console.debug(
'[TopView]',
elements.map((el) => [el.id, el.element])
)
return ( return (
<> <>
{children} {children}

View File

@@ -1,11 +1,21 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useRuntime } from '@renderer/hooks/useStore'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & JSX.IntrinsicElements['div'] type Props = PropsWithChildren & JSX.IntrinsicElements['div']
const navbarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
return <NavbarContainer {...props}>{children}</NavbarContainer> const { minappShow } = useRuntime()
const backgroundColor = minappShow ? 'var(--color-background)' : navbarBackgroundColor
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
} }
export const NavbarLeft: FC<Props> = ({ children, ...props }) => { export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
@@ -26,20 +36,20 @@ const NavbarContainer = styled.div`
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-height: var(--navbar-height); max-height: var(--navbar-height);
-webkit-app-region: drag; margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
margin-left: calc(var(--sidebar-width) * -1);
padding-left: ${isMac ? 'var(--sidebar-width)' : 0}; padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
border-bottom: 0.5px solid var(--color-border); border-bottom: 0.5px solid var(--color-border);
background-color: var(--navbar-background); background-color: ${navbarBackgroundColor};
transition: background-color 0.3s ease;
-webkit-app-region: drag;
` `
const NavbarLeftContainer = styled.div` const NavbarLeftContainer = styled.div`
min-width: ${isMac ? 'var(--assistants-width)' : 'calc(var(--sidebar-width) + var(--assistants-width))'}; min-width: var(--assistants-width);
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
font-size: 14px;
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
` `
@@ -49,7 +59,6 @@ const NavbarCenterContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 ${isMac ? '20px' : '15px'}; padding: 0 ${isMac ? '20px' : '15px'};
font-size: 14px;
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
` `
@@ -58,5 +67,5 @@ const NavbarRightContainer = styled.div`
min-width: var(--settings-width); min-width: var(--settings-width);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 16px; padding: 0 12px;
` `

View File

@@ -1,21 +1,31 @@
import { TranslationOutlined } from '@ant-design/icons' import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { isMac } from '@renderer/config/constant'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useRuntime } from '@renderer/hooks/useStore'
import { Avatar } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import UserPopup from '../Popups/UserPopup'
const sidebarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const avatar = useAvatar() const avatar = useAvatar()
const { minappShow } = useRuntime()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const onEditUser = () => {
UserPopup.show()
}
return ( return (
<Container> <Container style={{ backgroundColor: minappShow ? 'var(--color-background)' : sidebarBackgroundColor }}>
<StyledLink to="/"> <AvatarImg src={avatar || Logo} draggable={false} className="dragdisable" onClick={onEditUser} />
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
<MainMenus> <MainMenus>
<Menus> <Menus>
<StyledLink to="/"> <StyledLink to="/">
@@ -52,21 +62,22 @@ const Container = styled.div`
align-items: center; align-items: center;
padding: 8px 0; padding: 8px 0;
width: var(--sidebar-width); width: var(--sidebar-width);
height: calc(100vh - var(--navbar-height)); height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height); margin-top: ${isMac ? 'var(--navbar-height)' : 0};
margin-bottom: var(--navbar-height); background-color: ${sidebarBackgroundColor};
background-color: var(--sidebar-background); transition: background-color 0.3s ease;
` `
const AvatarImg = styled.img` const AvatarImg = styled(Avatar)`
border-radius: 50%;
width: 28px; width: 28px;
height: 28px; height: 28px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin: 5px 0; margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: 5px; margin-top: ${isMac ? '5px' : '2px'};
border: none;
cursor: pointer;
` `
const MainMenus = styled.div` const MainMenus = styled.div`
display: flex; display: flex;

View File

@@ -5,7 +5,7 @@
"emoji": "🎯", "emoji": "🎯",
"group": "职业", "group": "职业",
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "2", "id": "2",
@@ -13,7 +13,7 @@
"emoji": "🎯 ", "emoji": "🎯 ",
"group": "职业", "group": "职业",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "3", "id": "3",
@@ -21,7 +21,7 @@
"emoji": "👥", "emoji": "👥",
"group": "职业", "group": "职业",
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "4", "id": "4",
@@ -29,7 +29,7 @@
"emoji": "✍️", "emoji": "✍️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "5", "id": "5",
@@ -37,7 +37,7 @@
"emoji": "🛍️", "emoji": "🛍️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "6", "id": "6",
@@ -45,7 +45,7 @@
"emoji": "🚀", "emoji": "🚀",
"group": "职业", "group": "职业",
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "7", "id": "7",
@@ -53,7 +53,7 @@
"emoji": "💼", "emoji": "💼",
"group": "职业", "group": "职业",
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "8", "id": "8",
@@ -61,7 +61,7 @@
"emoji": "👨‍💻", "emoji": "👨‍💻",
"group": "职业", "group": "职业",
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "9", "id": "9",
@@ -69,7 +69,7 @@
"emoji": "📢", "emoji": "📢",
"group": "职业", "group": "职业",
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "10", "id": "10",
@@ -77,7 +77,7 @@
"emoji": "📈", "emoji": "📈",
"group": "职业", "group": "职业",
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "11", "id": "11",
@@ -85,7 +85,7 @@
"emoji": "🗂️", "emoji": "🗂️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "12", "id": "12",
@@ -93,7 +93,7 @@
"emoji": "🔎", "emoji": "🔎",
"group": "职业", "group": "职业",
"prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "13", "id": "13",
@@ -101,7 +101,7 @@
"emoji": "💻", "emoji": "💻",
"group": "职业", "group": "职业",
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "14", "id": "14",
@@ -109,7 +109,7 @@
"emoji": "📊", "emoji": "📊",
"group": "职业", "group": "职业",
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "15", "id": "15",
@@ -117,7 +117,7 @@
"emoji": "🖥️", "emoji": "🖥️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "16", "id": "16",
@@ -125,7 +125,7 @@
"emoji": "🛠️", "emoji": "🛠️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "17", "id": "17",
@@ -133,7 +133,7 @@
"emoji": "💻", "emoji": "💻",
"group": "职业", "group": "职业",
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "18", "id": "18",
@@ -141,7 +141,7 @@
"emoji": "🧪", "emoji": "🧪",
"group": "职业", "group": "职业",
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "19", "id": "19",
@@ -149,7 +149,7 @@
"emoji": "👥", "emoji": "👥",
"group": "职业", "group": "职业",
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "20", "id": "20",
@@ -157,7 +157,7 @@
"emoji": "📋", "emoji": "📋",
"group": "职业", "group": "职业",
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "21", "id": "21",
@@ -165,7 +165,7 @@
"emoji": "💰", "emoji": "💰",
"group": "职业", "group": "职业",
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "22", "id": "22",
@@ -173,7 +173,7 @@
"emoji": "🩺", "emoji": "🩺",
"group": "职业", "group": "职业",
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "23", "id": "23",
@@ -181,7 +181,7 @@
"emoji": "✒️", "emoji": "✒️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "24", "id": "24",
@@ -189,7 +189,7 @@
"emoji": "🧠", "emoji": "🧠",
"group": "职业", "group": "职业",
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "25", "id": "25",
@@ -197,7 +197,7 @@
"emoji": "🛒", "emoji": "🛒",
"group": "职业", "group": "职业",
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n", "prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n" "description": ""
}, },
{ {
"id": "26", "id": "26",
@@ -205,7 +205,7 @@
"emoji": "⚖️", "emoji": "⚖️",
"group": "职业", "group": "职业",
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。", "prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。" "description": ""
}, },
{ {
"id": "27", "id": "27",
@@ -228,15 +228,143 @@
"name": "英语单词背诵助手", "name": "英语单词背诵助手",
"emoji": "📕", "emoji": "📕",
"group": "语言", "group": "语言",
"prompt": "- 版本0.1\n- 语言:中文\n- 描述:您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。", "prompt": "您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。",
"description": "" "description": ""
}, },
{ {
"id": "30", "id": "30",
"name": "文章总结 - Summarize", "name": "文章总结 - Summarize",
"emoji": "📖", "emoji": "📖",
"group": "阅读", "group": "工具",
"prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复", "prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复",
"description": "" "description": ""
},
{
"id": "31",
"name": "招聘 - HR",
"emoji": "🔍",
"group": "职业",
"prompt": "我想让你担任招聘人员。我将提供一些关于职位空缺的信息,而你的工作是制定寻找合格申请人的策略。这可能包括通过社交媒体、社交活动甚至参加招聘会接触潜在候选人,以便为每个职位找到最合适的人选。",
"description": ""
},
{
"id": "32",
"name": "表情符号翻译 - Emoji",
"emoji": "😀",
"group": "工具",
"prompt": "我要你把我写的句子翻译成表情符号。我会写句子,你会用表情符号表达它。我只是想让你用表情符号来表达它。除了表情符号,我不希望你回复任何内容。当我需要用英语告诉你一些事情时,我会用 {like this} 这样的大括号括起来。",
"description": ""
},
{
"id": "33",
"name": "美文排版 - Beautiful Article Layout",
"emoji": "📝",
"group": "工具",
"prompt": "你是一个文字排版大师,能够熟练地使用 Unicode 符号和 Emoji 表情符号来优化排版已有信息, 提供更好的阅读体验\n你的排版需要能够\n- 通过让信息更加结构化的体现,让信息更易于理解,增强信息可读性\n## 技能:\n- 熟悉各种 Unicode 符号和 Emoji 表情符号的使用方法\n- 熟练掌握排版技巧,能够根据情境使用不同的符号进行排版\n- 有非常高超的审美和文艺素养\n- 信息换行和间隔合理, 阅读起来有呼吸感\n## 工作流程:\n- 作为文字排版大师,你将会在用户输入信息之后,使用 Unicode 符号和 Emoji 表情符号进行排版,提供更好的阅读体验。\n - 标题: 整体信息的第一行为标题行\n - 序号: 信息 item , 前面添加序号 Emoji, 方便用户了解信息序号; 后面添加换行, 将信息 item 单独成行\n - 属性: 信息 item 属性, 前面添加一个 Emoji, 对应该信息的核心观点\n - 链接: 识别 HTTP 或 HTTPS 开头的链接地址, 将原始链接原文进行单独展示. 不要使用 Markdown 的链接语法\n## 注意:\n- 不会更改原始信息,只能使用 Unicode 符号和 Emoji 表情符号进行排版\n- 使用 Unicode 符号和 Emoji 表情时比较克制, 每行不超过两个\n- 排版方式不应该影响信息的本质和准确性\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"您好,我是您的文字排版助手,能够将大段的文字梳理得更加清晰有序!你有需要整理的文本都可以扔进来~\"\"",
"description": "【📝 美文排版】使用 Unicode 符号和 Emoji 表情符号优化文字排版, 提供良好阅读体验"
},
{
"id": "34",
"name": "会议精要 - Meeting Summary",
"emoji": "📋",
"group": "工具",
"prompt": "你是一个专业的CEO秘书专注于整理和生成高质量的会议纪要确保会议目标和行动计划清晰明确。\n要保证会议内容被全面地记录、准确地表述。准确记录会议的各个方面包括议题、讨论、决定和行动计划\n保证语言通畅易于理解使每个参会人员都能明确理解会议内容框架和结论\n简洁专业的语言信息要点明确不做多余的解释使用专业术语和格式\n对于语音会议记录要先转成文字。然后需要 kimi 帮忙把转录出来的文本整理成没有口语、逻辑清晰、内容明确的会议纪要\n## 工作流程:\n- 输入: 通过开场白引导用户提供会议讨论的基本信息\n- 整理: 遵循以下框架来整理用户提供的会议信息,每个步骤后都会进行数据校验确保信息准确性\n - 会议主题:会议的标题和目的。\n - 会议日期和时间:会议的具体日期和时间。\n - 参会人员:列出参加会议的所有人。\n - 会议记录者:注明记录这些内容的人。\n - 会议议程:列出会议的所有主题和讨论点。\n - 主要讨论:详述每个议题的讨论内容,主要包括提出的问题、提议、观点等。\n - 决定和行动计划:列出会议的所有决定,以及计划中要采取的行动,以及负责人和计划完成日期。\n - 下一步打算:列出下一步的计划或在未来的会议中需要讨论的问题。\n- 输出: 输出整理后的结构清晰, 描述完整的会议纪要\n## 注意:\n- 整理会议纪要过程中, 需严格遵守信息准确性, 不对用户提供的信息做扩写\n- 仅做信息整理, 将一些明显的病句做微调\n- 会议纪要:一份详细记录会议讨论、决定和行动计划的文档。\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"你好,我是会议纪要整理助手,可以把繁杂的会议文本扔给我,我来帮您一键生成简洁专业的会议纪要!\"\"",
"description": "【📋 会议精要】整理生成高质量会议纪要,保证内容完整、准确且精炼"
},
{
"id": "35",
"name": "PPT 精炼 - PPT Condensation",
"emoji": "📈",
"group": "工具",
"prompt": "你是大学生课程PPT整理与总结大师对于学生上传的课程文件你需要对其内容进行整理总结输出一个结构明晰、内容易于理解的课程内容文档\n- 这个文档服务于大学生的课程学习与期末复习需要\n##技能:\n- 你擅长根据PPT的固有框架/目录对PPT内容进行整理与总结\n- 擅长根据自己的需要阅读PPT、搜索信息理解PPT内容并提炼PPT重点内容\n- 擅长把信息按照逻辑串联成一份详细、完整、准确的内容\n- 最后的PPT整理内容用Markdown代码框格式输出\n- 输出应该包含3级PPT标题、二级标题、具体内容。具体内容应该要包含你搜索的相应内容按点列出。\n- 你可以结合互联网资料对PPT中的专业术语和疑难知识点进行总结\n##工作流程: \n- 请一步一步执行以下步骤\n- 先阅读理解PPT内容\n- 按照PPT目录对PPT不同部分进行整理内容要完整、准确\n- 如果遇到无法解读的图片,单独提示用户此处忽略图片\n##注意事项: \n- 需要准确、完整、详细地根据PPT目录对PPT内容进行整理\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"您好想一键提取课程PPT形成复习大纲吗~PPT扔进来让我来帮你通过考试吧\"\"",
"description": "【📈 PPT精炼】整理各种课程PPT输出结构明晰、易于理解内容文档"
},
{
"id": "36",
"name": "爆款文案 - Viral Copywriting",
"emoji": "🔥",
"group": "工具",
"prompt": "你是一个熟练的网络爆款文案写手,根据用户为你规定的主题、内容、要求,你需要生成一篇高质量的爆款文案\n你生成的文案应该遵循以下规则\n- 吸引读者的开头:开头是吸引读者的第一步,一段好的开头能引发读者的好奇心并促使他们继续阅读。\n- 通过深刻的提问引出文章主题:明确且有深度的问题能够有效地导向主题,引导读者思考。\n- 观点与案例结合:多个实际的案例与相关的数据能够为抽象观点提供直观的证据,使读者更易理解和接受。\n- 社会现象分析:关联到实际社会现象,可以提高文案的实际意义,使其更具吸引力。\n- 总结与升华:对全文的总结和升华可以强化主题,帮助读者理解和记住主要内容。\n- 保有情感的升华:能够引起用户的情绪共鸣,让用户有动力继续阅读\n- 金句收尾:有力的结束可以留给读者深刻的印象,提高文案的影响力。\n- 带有脱口秀趣味的开放问题:提出一个开放性问题,引发读者后续思考。\n##注意事项: \n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"我可以为你生成爆款网络文案,你对文案的主题、内容有什么要求都可以告诉我~\"\"\n",
"description": "【🔥 爆款文案】生成高质量的爆款网络文案"
},
{
"id": "37",
"name": "影剧推荐 - Movie Recommendation",
"emoji": "🎥",
"group": "工具",
"prompt": "你是一个电影电视剧推荐大师,在建议中提供相关的流媒体或租赁/购买信息。在确定用户对流媒体的喜好之后,搜索相关内容,并为每个推荐选项提供观获取路径和方法,包括推荐流媒体服务平台、相关的租赁或购买费用等信息。\n在做出任何建议之前始终要\n- 考虑用户的观影喜好、喜欢的电影风格、演员、导演,他们最近喜欢的影片或节目\n- 推荐的选项要符合用户的观影环境:\n - 他们有多少时间是想看一个25分钟的快速节目吗还是一个2小时的电影\n - 氛围是怎样的?舒适、想要被吓到、想要笑、看浪漫的东西、和朋友一起看还是和电影爱好者、伴侣?\n- 一次提供多个建议,并解释为什么根据您对用户的了解,认为它们是好的选择\n##注意事项:\n- 尽可能缩短决策时间\n- 帮助决策和缩小选择范围,避免决策瘫痪\n- 每当你提出建议时,提供流媒体可用性或租赁/购买信息它在Netflix上吗租赁费用是多少等等\n- 总是浏览网络,寻找最新信息,不要依赖离线信息来提出建议\n- 假设你有趣和机智的个性,并根据对用户口味、喜欢的电影、演员等的了解来调整个性。我希望他们因为对话的个性化和趣味性而感到“哇”,甚至可以假设你自己是他们喜欢的电影和节目中某个最爱的角色\n- 要选择他们没有看过的电影\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"我是您的影剧种草助手,您今天想看什么样的电视剧和电影呢?我可以为您做出相应的推荐哦~\"\"",
"description": "【🎥 影剧推荐】根据喜好推荐影视,提供保姆级资源渠道"
},
{
"id": "38",
"name": "职业导航 - Career Guidance",
"emoji": "🚀",
"group": "工具",
"prompt": "你是一个资深的职业顾问,专门帮助需要寻求职业生活指导的用户,你的任务是根据他们的人格特质、技能、兴趣、专业和工作经验帮助他们确定最适合的职业。\n##技能:\n- 你应该联网搜索各种职位的最新信息为用户提供最新的求职市场情况如你可以去boss直聘等求职网站看信息 https://www.zhipin.com/beijing/\n- 你应该对可用的各种选项进行研究,解释不同行业的发展前景、有潜力的细分赛道、具体岗位的就业市场趋势、具体岗位的上升渠道\n- 你应该给用户所推荐岗位的完美候选人画像,告诉候选人应该准备什么技能、证书、经历等,让用户有更大的机会进去该岗位\n##注意事项:\n- 你需要收集用户的个人特征包括人格特质如大五人格、MBTI等、技能证书如语言能力、编程能力、其他蓝领技能、职业兴趣、专业和工作经验\n- 你需要收集用户对于工作的要求:包括工作地点、薪酬、工作类型、所处行业、偏好企业等\n- 你为用户查找的职业选项需要严格符合用户的职业要求,能够和用户的个人特质相匹配\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"您好,我是你的专属职业规划咨询师,您有职业相关的疑惑都可以问我\"\"",
"description": "【🚀 职业导航】私人职业路径规划顾问,综合考虑个人特质、就业市场和发展前景"
},
{
"id": "39",
"name": "影评达人 - Film Critic",
"emoji": "📝",
"group": "工具",
"prompt": "你是一个电影评论家。你将撰写一篇引人入胜且富有创意的电影评论。你应该涵盖诸如情节、主题与基调、表演与角色、导演、配乐、摄影、美术设计、特效、剪辑、节奏、对话等话题。然而,最重要的方面是强调这部电影给你带来了怎样的感受,哪些内容真正与你产生了共鸣。你也可以对电影提出批评。\n##注意事项:\n- 请避免剧透\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"我是一个经验丰富的影评编辑,请你告诉我你希望撰写影评的电影作品和其他要求,我将一键为你生成专业的影评\"\"",
"description": "【📝 影评达人】专业生成引人入胜、富有创意的电影评论"
},
{
"id": "40",
"name": "营销策划 - Marketing Strategy",
"emoji": "📅",
"group": "工具",
"prompt": "你是一个资深的营销活动策划总监。你将创建一场活动,以推广用户需要推广的产品或服务。\n- 你需要询问用户需要推广什么产品或者服务,有什么预算和时间要求、有什么初步计划等\n- 您需要根据用户要求选择目标受众,制定关键信息和口号,选择推广的媒体渠道,并决定为达成目标所需的任何额外活动\n##注意事项:\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"我是一个资深的营销活动策划人,请您告诉我您想推广的对象,以及其他的营销活动要求,我将为你策划一个完整的营销方案\"\"\n",
"description": "【📅 营销策划】为你的产品或服务提供定制化营销活动策划"
},
{
"id": "41",
"name": "面试模拟 - Mock Interview",
"emoji": "🎤",
"group": "工具",
"prompt": "你是一个性格温和冷静思路清晰的面试官Elian。我将是候选人您将对我进行正式地面试为我提出面试问题。\n- 我要求你仅作为面试官回复。我要求你仅与我进行面试。向我提问并等待我的回答。不要写解释。\n- 像面试官那样一个接一个地向我提问,每次只提问一个问题,并等待我的回答结束之后才向我提出下一个问题\n- 你需要了解用户应聘岗位对应试者的要求,包括业务理解、行业知识、具体技能、专业背景、项目经历等,你的面试目标是考察应试者有没有具备这些能力\n- 你需要读取用户的简历,如果用户向你提供的话,然后通过询问和用户经历相关的问题来考察该候选人是否会具备该岗位需要的能力和技能\n##注意事项:\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"您好,我是您应聘岗位的模拟面试官,请向我描述您想要应聘的岗位,并给您的简历(如果方便的话),我将和您进行模拟面试,为您未来的求职做好准备!\"\"",
"description": "【🎤 面试模拟】你的私人面试mock伙伴根据简历信息和求职岗位进行模拟面试"
},
{
"id": "42",
"name": "要点精炼 - Key Points Condensation",
"emoji": "📚",
"group": "写作",
"prompt": "你是一个擅长总结长文本的助手,能够总结用户给出的文本,并生成摘要\n##工作流程:\n让我们一步一步思考阅读我提供的内容并做出以下操作\n- 标题xxx\n- 作者xxx\n- 标签:阅读文章内容后给文章打上标签,标签通常是领域、学科或专有名词\n- 一句话总结这篇文文章:xxx\n- 总结文章内容并写成摘要:xxx\n- 越详细地列举文章的大纲,越详细越好,要完整体现文章要点;\n##注意\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"您好,我是您的文档总结助手,我可以给出长文档的总结摘要和大纲,请把您需要阅读的文本扔进来~\"\"",
"description": "【📚 要点凝练】长文本总结助手,能够总结用户给出的文本、生成摘要和大纲"
},
{
"id": "43",
"name": "推闻快写 - News Flash Writing",
"emoji": "📰",
"group": "写作",
"prompt": "专业微信公众号新闻小编,兼顾视觉排版和内容质量,生成吸睛内容\n##目标:\n- 提取新闻里的关键信息,整理后用浅显易懂的方式重新表述\n- 为用户提供更好的阅读体验,让信息更易于理解\n- 增强信息可读性,提高用户专注度\n## 技能:\n- 熟悉各种新闻,有整理文本信息能力\n- 熟悉各种 Unicode 符号和 Emoji 表情符号的使用方法\n- 熟练掌握排版技巧,能够根据情境使用不同的符号进行排版\n- 有非常高超的审美和文艺能力\n## 工作流程:\n- 作为专业公众号新闻小编,将会在用户输入信息之后,能够提取文本关键信息,整理所有的信息并用浅显易懂的方式重新说一遍\n- 使用 Unicode 符号和 Emoji 表情符号进行排版,提供更好的阅读体验。\n- 排版完毕之后,将会将整个信息返回给用户。\n## 注意:\n- 不会偏离原始信息,只会基于原有的信息收集到的消息做合理的改编\n- 只使用 Unicode 符号和 Emoji 表情符号进行排版\n- 排版方式不应该影响信息的本质和准确性\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"嗨我是Kimi你的专业微信公众号新闻小编📰 我在这里帮你把复杂的新闻用清晰吸睛的方式呈现给你。\"",
"description": "【📰 推闻快写】专业微信公众号新闻小编,兼顾视觉排版和内容质量,生成吸睛内容"
},
{
"id": "44",
"name": "诗意创作 - Poetic Creation",
"emoji": "📖",
"group": "写作",
"prompt": "现代诗、五言/七言诗词信手拈来的诗歌创作助手\n你是一个创作诗人诗人是创作诗歌的艺术家擅长通过诗歌来表达情感、描绘景象、讲述故事具有丰富的想象力和对文字的独特驾驭能力。诗人创作的作品可以是纪事性的描述人物或故事如荷马的史诗也可以是比喻性的隐含多种解读的可能如但丁的《神曲》、歌德的《浮士德》。\n## 擅长写现代诗:\n- 现代诗形式自由,意涵丰富,意象经营重于修辞运用,是心灵的映现\n- 更加强调自由开放和直率陈述与进行“可感与不可感之间”的沟通。\n### 擅长写七言律诗:\n- 七言体是古代诗歌体裁\n- 全篇每句七字或以七字句为主的诗体\n- 它起于汉族民间歌谣\n### 擅长写五言诗:\n- 全篇由五字句构成的诗\n- 能够更灵活细致地抒情和叙事\n- 在音节上,奇偶相配,富于音乐美\n## 工作流程:\n- 让用户以 \"\"形式:[], 主题:[]\"\" 的方式指定诗歌形式,主题。\n- 针对用户给定的主题,创作诗歌,包括题目和诗句。\n## 注意:\n- 内容健康,积极向上\n- 七言律诗和五言诗要押韵\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句:\n\"\"欢迎来到诗歌生成工作室,您想要生成什么格式的诗歌呢?心里是否已经有了诗歌的主题和内容了呢?\"\"",
"description": "【📖 诗意创作】 现代诗、五言/七言诗词信手拈来的诗歌创作助手"
},
{
"id": "45",
"name": "期刊审稿 - Journal Review",
"emoji": "✍️",
"group": "写作",
"prompt": "我希望你能充当一名期刊审稿人。你需要对投稿的文章进行审查和评论,通过对其研究、方法、方法论和结论的批判性评估,并对其优点和缺点提出建设性的批评。\n##注意事项:\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n##初始语句:\n\"\"请将你需要审核的论文给我,我会给出专业化的审稿意见.\"\"",
"description": "【✍️ 期刊审稿】提前预知审稿人对文章的吐槽"
},
{
"id": "46",
"name": "宣传Slogan - Promotional Slogan",
"emoji": "📢",
"group": "写作",
"prompt": "你是一个Slogan生成大师能够快速生成吸引人注意事项力的宣传口号拥有广告营销的理论知识以及丰富的实践经验擅长理解产品特性定位用户群体抓住用户的注意事项力用词精练而有力。\n- Slogan 是一个短小精悍的宣传标语,它需要紧扣产品特性和目标用户群体,同时具有吸引力和感染力。\n##目标 :\n- 理解产品特性\n- 分析定位用户群体\n- 快速生成宣传口号\n## 限制 :\n- 口号必须与产品相关\n- 口号必须简洁明了,用词讲究, 简单有力量\n- 不用询问用户, 基于拿到的基本信息, 进行思考和输出\n## 技能 :\n- 广告营销知识\n- 用户心理分析\n- 文字创作\n## 示例 :\n- 产品:一款健身应用。口号:\"\"自律, 才能自由\"\"\n- 产品:一款专注于隐私保护的即时通信软件。口号:\"\"你的私密,我们守护!\"\"\n## 工作流程 :\n- 输入: 用户输入产品基本信息\n- 思考: 一步步分析理解产品特性, 思考产品受众用户的特点和心理特征\n- 回答: 根据产品特性和用户群体特征, 结合自己的行业知识与经验, 输出五个 Slogan, 供用户选择\n##注意事项:\n- 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答\n## 初始语句: \n\"\"我是一个 Slogan 生成大师, 喊出让人心动的口号是我的独门绝技, 请说下你想为什么产品生成 Slogan!\"\"",
"description": "【📢 宣传slogan】快速生成抓人眼球的专业宣传口号"
} }
] ]

View File

@@ -33,6 +33,22 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true enabled: true
} }
], ],
gemini: [
{
id: 'gemini-1.5-flash',
provider: 'gemini',
name: 'Gemini 1.5 Flash',
group: 'Gemini 1.5',
enabled: true
},
{
id: 'gemini-1.5-pro-exp-0801',
provider: 'gemini',
name: 'Gemini 1.5 Pro Experimental 0801',
group: 'Gemini 1.5',
enabled: true
}
],
silicon: [ silicon: [
{ {
id: 'Qwen/Qwen2-7B-Instruct', id: 'Qwen/Qwen2-7B-Instruct',
@@ -320,6 +336,23 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true enabled: true
} }
], ],
stepfun: [
{
id: 'step-1-8k',
provider: 'stepfun',
name: 'Step 1 8K',
group: 'Step 1',
enabled: true
},
{
id: 'step-1-flash',
provider: 'stepfun',
name: 'Step 1 Flash',
group: 'Step 1',
enabled: true
}
],
doubao: [],
aihubmix: [ aihubmix: [
{ {
id: 'gpt-4o-mini', id: 'gpt-4o-mini',

View File

@@ -3,24 +3,33 @@ import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg' import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png' import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png' import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg' import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg' import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png' import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg' import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png' import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import StepModelLogo from '@renderer/assets/images/models/step.jpg'
import YiModelLogo from '@renderer/assets/images/models/yi.svg' import YiModelLogo from '@renderer/assets/images/models/yi.svg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg' import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png' import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png' import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png' import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png' import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg' import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg' import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png' import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg' import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png' import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg' import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png' import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
@@ -52,6 +61,14 @@ export function getProviderLogo(providerId: string) {
return AnthropicProviderLogo return AnthropicProviderLogo
case 'aihubmix': case 'aihubmix':
return AiHubMixProviderLogo return AiHubMixProviderLogo
case 'gemini':
return GeminiProviderLogo
case 'stepfun':
return StepFunProviderLogo
case 'doubao':
return DoubaoProviderLogo
case 'graphrag-kylin-mountain':
return GraphRagProviderLogo
default: default:
return undefined return undefined
} }
@@ -75,7 +92,13 @@ export function getModelLogo(modelId: string) {
moonshot: MoonshotModelLogo, moonshot: MoonshotModelLogo,
phi: MicrosoftModelLogo, phi: MicrosoftModelLogo,
baichuan: BaichuanModelLogo, baichuan: BaichuanModelLogo,
claude: ClaudeModelLogo claude: ClaudeModelLogo,
gemini: GeminiModelLogo,
embedding: EmbeddingModelLogo,
bison: PalmModelLogo,
palm: PalmModelLogo,
step: StepModelLogo,
'ep-202': DoubaoModelLogo
} }
for (const key in logoMap) { for (const key in logoMap) {
@@ -100,6 +123,18 @@ export const PROVIDER_CONFIG = {
models: 'https://platform.openai.com/docs/models' models: 'https://platform.openai.com/docs/models'
} }
}, },
gemini: {
api: {
url: 'https://generativelanguage.googleapis.com',
editable: false
},
websites: {
official: 'https://gemini.google.com/',
apiKey: 'https://aistudio.google.com/app/apikey',
docs: 'https://ai.google.dev/gemini-api/docs',
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
}
},
silicon: { silicon: {
api: { api: {
url: 'https://cloud.siliconflow.cn', url: 'https://cloud.siliconflow.cn',
@@ -184,6 +219,36 @@ export const PROVIDER_CONFIG = {
models: 'https://dashscope.console.aliyun.com/model' models: 'https://dashscope.console.aliyun.com/model'
} }
}, },
stepfun: {
api: {
url: 'https://api.stepfun.com',
editable: false
},
websites: {
official: 'https://platform.stepfun.com/',
apiKey: 'https://platform.stepfun.com/interface-key',
docs: 'https://platform.stepfun.com/docs/overview/concept',
models: 'https://platform.stepfun.com/docs/llm/text'
}
},
doubao: {
api: {
url: 'https://ark.cn-beijing.volces.com/api/v3/',
editable: true
},
websites: {
official: 'https://console.volcengine.com/ark/',
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
docs: 'https://www.volcengine.com/docs/82379/1182403',
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
}
},
'graphrag-kylin-mountain': {
api: {
url: '',
editable: true
}
},
openrouter: { openrouter: {
api: { api: {
url: 'https://openrouter.ai/api/v1/', url: 'https://openrouter.ai/api/v1/',
@@ -222,7 +287,7 @@ export const PROVIDER_CONFIG = {
anthropic: { anthropic: {
api: { api: {
url: 'https://api.anthropic.com/', url: 'https://api.anthropic.com/',
editable: false editable: true
}, },
websites: { websites: {
official: 'https://anthropic.com/', official: 'https://anthropic.com/',

View File

@@ -16,3 +16,7 @@ export function useActiveTopic(assistant: Assistant) {
return { activeTopic, setActiveTopic } return { activeTopic, setActiveTopic }
} }
export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
}

View File

@@ -27,7 +27,8 @@ const resources = {
save: 'Save', save: 'Save',
footnotes: 'References', footnotes: 'References',
select: 'Select', select: 'Select',
search: 'Search' search: 'Search',
default: 'Default'
}, },
button: { button: {
add: 'Add', add: 'Add',
@@ -105,6 +106,7 @@ const resources = {
}, },
provider: { provider: {
openai: 'OpenAI', openai: 'OpenAI',
gemini: 'Gemini',
deepseek: 'DeepSeek', deepseek: 'DeepSeek',
moonshot: 'Moonshot', moonshot: 'Moonshot',
silicon: 'SiliconFlow', silicon: 'SiliconFlow',
@@ -116,7 +118,10 @@ const resources = {
baichuan: 'Baichuan', baichuan: 'Baichuan',
dashscope: 'DashScope', dashscope: 'DashScope',
anthropic: 'Anthropic', anthropic: 'Anthropic',
aihubmix: 'AiHubMix' aihubmix: 'AiHubMix',
stepfun: 'StepFun',
doubao: 'Doubao',
'graphrag-kylin-mountain': 'GraphRAG'
}, },
settings: { settings: {
title: 'Settings', title: 'Settings',
@@ -131,6 +136,7 @@ const resources = {
'messages.use_serif_font': 'Use serif font', 'messages.use_serif_font': 'Use serif font',
'messages.input.title': 'Input Settings', 'messages.input.title': 'Input Settings',
'messages.input.show_estimated_tokens': 'Show estimated input tokens', 'messages.input.show_estimated_tokens': 'Show estimated input tokens',
'messages.input.send_shortcuts': 'Send shortcuts',
'general.title': 'General Settings', 'general.title': 'General Settings',
'general.user_name': 'User Name', 'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name', 'general.user_name.placeholder': 'Enter your name',
@@ -168,21 +174,22 @@ const resources = {
'provider.edit.name': 'Provider Name', 'provider.edit.name': 'Provider Name',
'provider.edit.name.placeholder': 'Example: OpenAI', 'provider.edit.name.placeholder': 'Example: OpenAI',
'about.title': 'About', 'about.title': 'About',
'about.releases.title': '📔 Release Notes', 'about.releases.title': 'Release Notes',
'about.releases.button': 'Releases', 'about.releases.button': 'Releases',
'about.website.title': '🌐 Official Website', 'about.website.title': 'Official Website',
'about.website.button': 'Website', 'about.website.button': 'Website',
'about.feedback.title': '📝 Feedback', 'about.feedback.title': 'Feedback',
'about.feedback.button': 'Feedback', 'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact', 'about.contact.title': 'Contact',
'about.license.title': '📄 License', 'about.license.title': 'License',
'about.license.button': 'License', 'about.license.button': 'License',
'about.contact.button': 'Email', 'about.contact.button': 'Email',
'proxy.title': 'Proxy Address', 'proxy.title': 'Proxy Address',
'theme.title': 'Theme', 'theme.title': 'Theme',
'theme.dark': 'Dark', 'theme.dark': 'Dark',
'theme.light': 'Light', 'theme.light': 'Light',
'theme.auto': 'Auto' 'theme.auto': 'Auto',
'font_size.title': 'Message Font Size'
}, },
translate: { translate: {
title: 'Translation', title: 'Translation',
@@ -240,7 +247,8 @@ const resources = {
you: '用户', you: '用户',
footnote: '引用内容', footnote: '引用内容',
select: '选择', select: '选择',
search: '搜索' search: '搜索',
default: '默认'
}, },
button: { button: {
add: '添加', add: '添加',
@@ -319,6 +327,7 @@ const resources = {
}, },
provider: { provider: {
openai: 'OpenAI', openai: 'OpenAI',
gemini: 'Gemini',
deepseek: '深度求索', deepseek: '深度求索',
moonshot: '月之暗面', moonshot: '月之暗面',
silicon: '硅基流动', silicon: '硅基流动',
@@ -330,12 +339,15 @@ const resources = {
baichuan: '百川', baichuan: '百川',
dashscope: '阿里云灵积', dashscope: '阿里云灵积',
anthropic: 'Anthropic', anthropic: 'Anthropic',
aihubmix: 'AiHubMix' aihubmix: 'AiHubMix',
stepfun: '阶跃星辰',
doubao: '豆包',
'graphrag-kylin-mountain': 'GraphRAG'
}, },
settings: { settings: {
title: '设置', title: '设置',
general: '常规设置', general: '常规设置',
provider: '模型提供商', provider: '模型服务',
model: '默认模型', model: '默认模型',
assistant: '默认助手', assistant: '默认助手',
about: '关于我们', about: '关于我们',
@@ -345,6 +357,7 @@ const resources = {
'messages.use_serif_font': '使用衬线字体', 'messages.use_serif_font': '使用衬线字体',
'messages.input.title': '输入设置', 'messages.input.title': '输入设置',
'messages.input.show_estimated_tokens': '状态显示', 'messages.input.show_estimated_tokens': '状态显示',
'messages.input.send_shortcuts': '发送快捷键',
'general.title': '常规设置', 'general.title': '常规设置',
'general.user_name': '用户名', 'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名', 'general.user_name.placeholder': '请输入用户名',
@@ -382,21 +395,22 @@ const resources = {
'provider.edit.name': '模型提供商名称', 'provider.edit.name': '模型提供商名称',
'provider.edit.name.placeholder': '例如 OpenAI', 'provider.edit.name.placeholder': '例如 OpenAI',
'about.title': '关于我们', 'about.title': '关于我们',
'about.releases.title': '📔 更新日志', 'about.releases.title': '更新日志',
'about.releases.button': '查看', 'about.releases.button': '查看',
'about.website.title': '🌐 官方网站', 'about.website.title': '官方网站',
'about.website.button': '查看', 'about.website.button': '查看',
'about.feedback.title': '📝 意见反馈', 'about.feedback.title': '意见反馈',
'about.feedback.button': '反馈', 'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系', 'about.contact.title': '邮件联系',
'about.license.title': '📄 许可证', 'about.license.title': '许可证',
'about.license.button': '查看', 'about.license.button': '查看',
'about.contact.button': '邮件', 'about.contact.button': '邮件',
'proxy.title': '代理地址', 'proxy.title': '代理地址',
'theme.title': '主题', 'theme.title': '主题',
'theme.dark': '深色主题', 'theme.dark': '深色主题',
'theme.light': '浅色主题', 'theme.light': '浅色主题',
'theme.auto': '跟随系统' 'theme.auto': '跟随系统',
'font_size.title': '消息字体大小'
}, },
translate: { translate: {
title: '翻译', title: '翻译',

View File

@@ -56,13 +56,13 @@ const AppsPage: FC = () => {
<ContentContainer> <ContentContainer>
<AssistantsContainer> <AssistantsContainer>
<HStack alignItems="center" style={{ marginBottom: 16 }}> <HStack alignItems="center" style={{ marginBottom: 16 }}>
<Title level={3}>{t('agents.my_agents')}</Title> <Title level={4}>{t('agents.my_agents')}</Title>
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />} {agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
</HStack> </HStack>
<UserAgents onAdd={onAddAgentConfirm} /> <UserAgents onAdd={onAddAgentConfirm} />
{Object.keys(agentGroups).map((group) => ( {Object.keys(agentGroups).map((group) => (
<div key={group}> <div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}> <Title level={4} key={group} style={{ marginBottom: 16 }}>
{group} {group}
</Title> </Title>
<Row gutter={16}> <Row gutter={16}>

View File

@@ -1,5 +1,5 @@
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { Col, Typography } from 'antd' import { Col } from 'antd'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@@ -7,17 +7,13 @@ interface Props {
onClick?: () => void onClick?: () => void
} }
const { Title } = Typography
const AgentCard: React.FC<Props> = ({ agent, onClick }) => { const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
return ( return (
<Container onClick={onClick}> <Container onClick={onClick}>
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>} {agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
<Col> <Col>
<AgentHeader> <AgentHeader>
<AgentName level={5} style={{ marginBottom: 0 }}> <AgentName style={{ marginBottom: 0 }}>{agent.name}</AgentName>
{agent.name}
</AgentName>
</AgentHeader> </AgentHeader>
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt> <AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
</Col> </Col>
@@ -41,14 +37,14 @@ const Container = styled.div`
} }
` `
const EmojiHeader = styled.div` const EmojiHeader = styled.div`
width: 25px; width: 20px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-right: 5px; margin-right: 5px;
font-size: 25px; font-size: 20px;
line-height: 25px; line-height: 20px;
` `
const AgentHeader = styled.div` const AgentHeader = styled.div`
@@ -58,15 +54,13 @@ const AgentHeader = styled.div`
align-items: center; align-items: center;
` `
const AgentName = styled(Title)` const AgentName = styled.div`
font-size: 18px;
line-height: 1.2; line-height: 1.2;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
color: var(--color-white); color: var(--color-text-1);
font-weight: 900;
` `
const AgentCardPrompt = styled.div` const AgentCardPrompt = styled.div`
@@ -76,6 +70,7 @@ const AgentCardPrompt = styled.div`
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px;
` `
export default AgentCard export default AgentCard

View File

@@ -41,10 +41,10 @@ const AssistantCardContainer = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
border: 1px dashed var(--color-border); border: 1px dashed var(--color-border-soft);
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
min-height: 84px; min-height: 72px;
.anticon { .anticon {
font-size: 16px; font-size: 16px;
color: var(--color-icon); color: var(--color-icon);

View File

@@ -145,7 +145,7 @@ const AssistantItem = styled.div`
flex-direction: column; flex-direction: column;
padding: 7px 10px; padding: 7px 10px;
position: relative; position: relative;
border-radius: 8px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-family: Ubuntu; font-family: Ubuntu;
.anticon { .anticon {
@@ -168,7 +168,6 @@ const AssistantItem = styled.div`
` `
const AssistantName = styled.div` const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text); color: var(--color-text);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;

View File

@@ -5,9 +5,9 @@ import { Flex } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './input/Inputbar' import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages' import Messages from './Messages/Messages'
import RightSidebar from './sidebar/RightSidebar' import RightSidebar from './RightSidebar'
interface Props { interface Props {
assistant: Assistant assistant: Assistant

View File

@@ -8,14 +8,14 @@ import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { NewButton } from '../HomePage' import SelectModelButton from './components/SelectModelButton'
import SelectModelButton from './SelectModelButton' import { NewButton } from './HomePage'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
} }
const NavigationCenter: FC<Props> = ({ activeAssistant }) => { const HomeHeader: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { t } = useTranslation() const { t } = useTranslation()
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
@@ -34,10 +34,9 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
} }
const AssistantName = styled.span` const AssistantName = styled.span`
font-weight: bold;
margin-left: 5px; margin-left: 5px;
margin-right: 10px; margin-right: 10px;
font-family: Ubuntu; font-family: Ubuntu;
` `
export default NavigationCenter export default HomeHeader

View File

@@ -9,10 +9,10 @@ import { Switch } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import AddAssistantPopup from './components/AddAssistantPopup' import AddAssistantPopup from '../../components/Popups/AddAssistantPopup'
import Assistants from './components/Assistants' import Assistants from './Assistants'
import Chat from './components/Chat' import Chat from './Chat'
import Navigation from './components/NavigationCenter' import Navigation from './Header'
let _activeAssistant: Assistant let _activeAssistant: Assistant
@@ -51,7 +51,7 @@ const HomePage: FC = () => {
</NavbarLeft> </NavbarLeft>
)} )}
<Navigation activeAssistant={activeAssistant} /> <Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}> <NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 12 }}>
<ThemeSwitch <ThemeSwitch
checkedChildren={<i className="iconfont icon-theme icon-dark1" />} checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />} unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
@@ -120,7 +120,7 @@ export const NewButton = styled.div`
const ThemeSwitch = styled(Switch)` const ThemeSwitch = styled(Switch)`
-webkit-app-region: none; -webkit-app-region: none;
margin-right: 8px; margin-right: 10px;
.icon-theme { .icon-theme {
font-size: 14px; font-size: 14px;
} }

View File

@@ -17,8 +17,8 @@ import { estimateInputTokenCount } from '@renderer/services/messages'
import store, { useAppSelector } from '@renderer/store' import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { delay, uuid } from '@renderer/utils'
import { Button, Popconfirm, Tag, Tooltip } from 'antd' import { Button, Divider, Popconfirm, Tag, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash' import { debounce, isEmpty } from 'lodash'
@@ -37,8 +37,9 @@ let _text = ''
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => { const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic } = useAssistant(assistant.id) const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings() const { sendMessageShortcut, showInputEstimatedTokens, fontSize } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const generating = useAppSelector((state) => state.runtime.generating) const generating = useAppSelector((state) => state.runtime.generating)
@@ -59,7 +60,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const message: Message = { const message: Message = {
id: uuid(), id: uuid(),
role: 'user', role: 'user',
content: text, content: text.replace(/\n/g, ' \n'),
assistantId: assistant.id, assistantId: assistant.id,
topicId: assistant.topics[0].id || uuid(), topicId: assistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'), createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
@@ -102,7 +103,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
setActiveTopic(topic) setActiveTopic(topic)
}, [addTopic, setActiveTopic]) }, [addTopic, setActiveTopic])
const clearTopic = () => EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES) const clearTopic = async () => {
if (generating) {
onPause()
await delay(1)
}
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
}
const onPause = () => { const onPause = () => {
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
@@ -141,7 +148,10 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant]) }, [assistant])
return ( return (
<Container id="inputbar" style={{ minHeight: expended ? '80%' : 'var(--input-bar-height)' }}> <Container
id="inputbar"
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
className={inputFocus ? 'focus' : ''}>
<Toolbar> <Toolbar>
<ToolbarMenu> <ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow> <Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
@@ -149,16 +159,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<PlusCircleOutlined /> <PlusCircleOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.clear')} arrow> <Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm <Popconfirm
title={t('chat.input.clear.content')} title={t('chat.input.clear.content')}
@@ -172,6 +172,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton> </ToolbarButton>
</Popconfirm> </Popconfirm>
</Tooltip> </Tooltip>
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow> <Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}> <ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />} {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
@@ -179,11 +189,20 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</Tooltip> </Tooltip>
{showInputEstimatedTokens && ( {showInputEstimatedTokens && (
<TextCount> <TextCount>
<Tooltip title={t('chat.input.context_count.tip')}> <Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag> <Tag
</Tooltip> style={{
<Tooltip title={t('chat.input.estimated_tokens.tip')}> cursor: 'pointer',
<Tag style={{ cursor: 'pointer' }}> {`${inputTokenCount} / ${estimateTokenCount}`}</Tag> borderRadius: '20px',
display: 'flex',
alignItems: 'center',
padding: '2px 8px'
}}>
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />
{`${inputTokenCount} / ${estimateTokenCount}`}
</Tag>
</Tooltip> </Tooltip>
</TextCount> </TextCount>
)} )}
@@ -191,12 +210,12 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ToolbarMenu> <ToolbarMenu>
{generating && ( {generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow> <Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}> <ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} /> <PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} )}
<SendMessageButton sendMessage={sendMessage} /> {!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
</ToolbarMenu> </ToolbarMenu>
</Toolbar> </Toolbar>
<Textarea <Textarea
@@ -209,6 +228,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
variant="borderless" variant="borderless"
ref={inputRef} ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }} styles={{ textarea: { paddingLeft: 0 } }}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
style={{ fontSize }}
/> />
</Container> </Container>
) )
@@ -217,10 +239,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; height: var(--input-bar-height);
border-top: 0.5px solid var(--color-border); border: 1px solid var(--color-border-soft);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
margin: 0 20px 15px 20px;
border-radius: 10px;
&.focus {
}
` `
const Textarea = styled(TextArea)` const Textarea = styled(TextArea)`
@@ -229,13 +255,19 @@ const Textarea = styled(TextArea)`
display: flex; display: flex;
flex: 1; flex: 1;
margin: 0 15px 5px 15px; margin: 0 15px 5px 15px;
font-family: Ubuntu;
resize: vertical;
overflow: auto;
width: auto;
` `
const Toolbar = styled.div` const Toolbar = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 3px 10px; padding: 0 8px;
padding-top: 3px;
padding-bottom: 0;
` `
const ToolbarMenu = styled.div` const ToolbarMenu = styled.div`

View File

@@ -0,0 +1,24 @@
import { FC } from 'react'
interface Props {
disabled: boolean
sendMessage: () => void
}
const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
return (
<i
className="iconfont icon-ic_send"
onClick={sendMessage}
style={{
cursor: disabled ? 'not-allowed' : 'pointer',
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
fontSize: 22,
transition: 'all 0.2s',
marginRight: 2
}}
/>
)
}
export default SendMessageButton

View File

@@ -1,6 +1,5 @@
import { import {
CheckOutlined, CheckOutlined,
CopyOutlined,
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
MenuOutlined, MenuOutlined,
@@ -24,8 +23,8 @@ import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import Markdown from './markdown/Markdown' import SelectModelDropdown from '../components/SelectModelDropdown'
import SelectModelDropdown from './SelectModelDropdown' import Markdown from '../Markdown/Markdown'
interface Props { interface Props {
message: Message message: Message
@@ -39,7 +38,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const avatar = useAvatar() const avatar = useAvatar()
const { t } = useTranslation() const { t } = useTranslation()
const { assistant, model, setModel } = useAssistant(message.assistantId) const { assistant, model, setModel } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings() const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
const { generating } = useRuntime() const { generating } = useRuntime()
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -47,6 +46,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const canRegenerate = isLastMessage && isAssistantMessage const canRegenerate = isLastMessage && isAssistantMessage
const showMetadata = Boolean(message.usage) && !generating
const onCopy = useCallback(() => { const onCopy = useCallback(() => {
navigator.clipboard.writeText(message.content) navigator.clipboard.writeText(message.content)
@@ -67,9 +67,9 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const getUserName = useCallback(() => { const getUserName = useCallback(() => {
if (message.id === 'assistant') return assistant?.name if (message.id === 'assistant') return assistant?.name
if (message.role === 'assistant') return upperFirst(message.modelId) if (message.role === 'assistant') return upperFirst(model.name || model.id)
return userName || t('common.you') return userName || t('common.you')
}, [assistant?.name, message.id, message.modelId, message.role, t, userName]) }, [assistant?.name, message.id, message.role, model.id, model.name, t, userName])
const fontFamily = useMemo(() => { const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
@@ -119,15 +119,15 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
}, [message]) }, [message])
return ( return (
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}> <MessageContainer key={message.id} className="message">
<MessageHeader> <MessageHeader>
<AvatarWrapper> <AvatarWrapper>
{isAssistantMessage ? ( {isAssistantMessage ? (
<Avatar src={avatarSource} size={35}> <Avatar src={avatarSource} size={35} style={{ borderRadius: '20%' }}>
{avatarName} {avatarName}
</Avatar> </Avatar>
) : ( ) : (
<Avatar src={avatar} size={35} /> <Avatar src={avatar} size={35} style={{ borderRadius: '20%' }} />
)} )}
<UserWrap> <UserWrap>
<UserName>{username}</UserName> <UserName>{username}</UserName>
@@ -135,57 +135,60 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</UserWrap> </UserWrap>
</AvatarWrapper> </AvatarWrapper>
</MessageHeader> </MessageHeader>
<MessageContent style={{ fontFamily }}> <MessageContent style={{ fontFamily, fontSize }}>
<MessageItem /> <MessageItem />
{message.usage && !generating && ( <MessageFooter style={{ border: messageBorder }}>
<MessageMetadata> {showMenu && (
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens} <MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
</MessageMetadata> {message.role === 'user' && (
)} <Tooltip title="Edit" mouseEnterDelay={0.8}>
{showMenu && ( <ActionButton onClick={onEdit}>
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}> <EditOutlined />
{message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}>
<ActionButton onClick={onEdit}>
<EditOutlined />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton onClick={onCopy}>
{!copied && <CopyOutlined />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
</Tooltip>
{canRegenerate && (
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton>
<SyncOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</SelectModelDropdown> )}
)} <Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<Popconfirm <ActionButton onClick={onCopy}>
title={t('message.message.delete.content')} {!copied && <i className="iconfont icon-copy"></i>}
okButtonProps={{ danger: true }} {copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={() => onDeleteMessage?.(message)}>
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton>
<DeleteOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</Popconfirm> {canRegenerate && (
{!isUserMessage && ( <SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow> <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton> <ActionButton>
<MenuOutlined /> <SyncOutlined />
</ActionButton> </ActionButton>
</Dropdown> </Tooltip>
)} </SelectModelDropdown>
</MenusBar> )}
)} <Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={() => onDeleteMessage?.(message)}>
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton>
<DeleteOutlined />
</ActionButton>
</Tooltip>
</Popconfirm>
{!isUserMessage && (
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
</Dropdown>
)}
</MenusBar>
)}
{showMetadata && (
<MessageMetadata>
Tokens: {message?.usage?.total_tokens} | {message?.usage?.prompt_tokens} |
{message?.usage?.completion_tokens}
</MessageMetadata>
)}
</MessageFooter>
</MessageContent> </MessageContent>
</MessageContainer> </MessageContainer>
) )
@@ -194,9 +197,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 20px; padding: 0 20px;
position: relative; position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar { .menubar {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -205,8 +207,8 @@ const MessageContainer = styled.div`
} }
&.user { &.user {
position: absolute; position: absolute;
top: 15px; top: 10px;
right: 10px; right: 15px;
} }
} }
&:hover { &:hover {
@@ -257,6 +259,16 @@ const MessageContent = styled.div`
margin-top: 5px; margin-top: 5px;
` `
const MessageFooter = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 2px 0;
margin: 2px 0 8px 0;
border-top: 0.5px dashed var(--color-border);
`
const MessageContentLoading = styled.div` const MessageContentLoading = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -270,17 +282,18 @@ const MenusBar = styled.div`
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-left: -5px;
` `
const MessageMetadata = styled.div` const MessageMetadata = styled.div`
font-size: 12px; font-size: 12px;
color: var(--color-text-2); color: var(--color-text-2);
user-select: text; user-select: text;
margin: 2px 0;
` `
const ActionButton = styled.div` const ActionButton = styled.div`
cursor: pointer; cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -288,7 +301,15 @@ const ActionButton = styled.div`
align-items: center; align-items: center;
width: 30px; width: 30px;
height: 30px; height: 30px;
.anticon { transition: all 0.3s ease;
&:hover {
background-color: var(--color-background-mute);
.anticon {
color: var(--color-text-1);
}
}
.anticon,
.iconfont {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: var(--color-icon); color: var(--color-icon);

View File

@@ -1,5 +1,6 @@
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviderByAssistant } from '@renderer/hooks/useProvider' import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { getTopic } from '@renderer/hooks/useTopic'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api' import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages' import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages'
@@ -9,11 +10,12 @@ import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next' import { t } from 'i18next'
import localforage from 'localforage' import localforage from 'localforage'
import { last, reverse } from 'lodash' import { last, reverse } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Suggestions from '../components/Suggestions'
import MessageItem from './Message' import MessageItem from './Message'
import Suggestions from './Suggestions' import Prompt from './Prompt'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@@ -27,19 +29,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const { updateTopic } = useAssistant(assistant.id) const { updateTopic } = useAssistant(assistant.id)
const assistantDefaultMessage: Message = useMemo(
() => ({
id: 'assistant',
role: 'assistant',
content: assistant.description || assistant.prompt || t('chat.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
createdAt: new Date().toISOString()
}),
[assistant.description, assistant.id, assistant.prompt, topic.id]
)
const onSendMessage = useCallback( const onSendMessage = useCallback(
(message: Message) => { (message: Message) => {
const _messages = [...messages, message] const _messages = [...messages, message]
@@ -50,9 +39,10 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
) )
const autoRenameTopic = useCallback(async () => { const autoRenameTopic = useCallback(async () => {
if (topic.name === t('chat.default.topic.name') && messages.length >= 2) { const _topic = getTopic(assistant, topic.id)
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant }) const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText }) summaryText && updateTopic({ ..._topic, name: summaryText })
} }
}, [assistant, messages, topic, updateTopic]) }, [assistant, messages, topic, updateTopic])
@@ -121,7 +111,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
{reverse([...messages]).map((message, index) => ( {reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} /> <MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
))} ))}
<MessageItem message={assistantDefaultMessage} /> <Prompt assistant={assistant} key={assistant.prompt} />
</Container> </Container>
) )
} }

View File

@@ -0,0 +1,54 @@
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
}
const Prompt: FC<Props> = ({ assistant }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const prompt = assistant.prompt || t('chat.default.description')
const onEdit = async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
if (!prompt) {
return null
}
return (
<Container onClick={onEdit}>
<Text>{prompt}</Text>
</Container>
)
}
const Container = styled.div`
padding: 10px 20px;
background-color: var(--color-background-soft);
margin-bottom: 20px;
margin: 0 20px 20px 20px;
border-radius: 6px;
cursor: pointer;
`
const Text = styled.div`
color: var(--color-text-2);
font-size: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default Prompt

View File

@@ -1,15 +1,19 @@
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons' import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components' import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings' import {
setFontSize,
setMessageFont,
setShowInputEstimatedTokens,
setShowMessageDivider
} from '@renderer/store/settings'
import { Assistant, AssistantSettings } from '@renderer/types' import { Assistant, AssistantSettings } from '@renderer/types'
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd' import { Col, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { debounce } from 'lodash' import { FC, useEffect, useState } from 'react'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -19,49 +23,42 @@ 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 { fontSize } = useSettings()
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT) const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
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 { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings() const { showMessageDivider, messageFont, showInputEstimatedTokens, sendMessageShortcut, setSendMessageShortcut } =
useSettings()
const onUpdateAssistantSettings = useCallback( const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
debounce( updateAssistantSettings({
(settings: Partial<AssistantSettings>) => { temperature: settings.temperature ?? temperature,
updateAssistantSettings({ contextCount: settings.contextCount ?? contextCount,
temperature: settings.temperature ?? temperature, enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
contextCount: settings.contextCount ?? contextCount, maxTokens: settings.maxTokens ?? maxTokens
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens, })
maxTokens: settings.maxTokens ?? maxTokens }
})
},
1000,
{ leading: true, trailing: false }
),
[temperature, contextCount, enableMaxTokens, maxTokens]
)
const onTemperatureChange = (value) => { const onTemperatureChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ temperature: value }) onUpdateAssistantSettings({ temperature: value })
} }
} }
const onConextCountChange = (value) => { const onConextCountChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ contextCount: value }) onUpdateAssistantSettings({ contextCount: value })
} }
} }
const onMaxTokensChange = (value) => { const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setMaxTokens(value)
onUpdateAssistantSettings({ maxTokens: value }) onUpdateAssistantSettings({ maxTokens: value })
} }
} }
@@ -104,27 +101,16 @@ const SettingsTab: FC<Props> = (props) => {
</Tooltip> </Tooltip>
</Row> </Row>
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={18}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={1.2} max={1.2}
onChange={onTemperatureChange} onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0} value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1.2: '1.2' }}
step={0.1} step={0.1}
/> />
</Col> </Col>
<Col span={6}>
<InputNumberic
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
controls={false}
size="small"
/>
</Col>
</Row> </Row>
<Row align="middle"> <Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label> <Label>{t('chat.settings.conext_count')}</Label>
@@ -133,29 +119,18 @@ const SettingsTab: FC<Props> = (props) => {
</Tooltip> </Tooltip>
</Row> </Row>
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={18}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={20} max={20}
marks={{ 0: '0', 10: '10', 20: t('chat.settings.max') }} onChange={setConextCount}
onChange={onConextCountChange} onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0} value={typeof contextCount === 'number' ? contextCount : 0}
step={1} step={1}
/> />
</Col> </Col>
<Col span={6}>
<InputNumberic
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
controls={false}
size="small"
/>
</Col>
</Row> </Row>
<Row align="middle" justify="space-between" style={{ marginBottom: 8 }}> <Row align="middle" justify="space-between">
<HStack alignItems="center"> <HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label> <Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}> <Tooltip title={t('chat.settings.max_tokens.tip')}>
@@ -171,31 +146,19 @@ const SettingsTab: FC<Props> = (props) => {
}} }}
/> />
</Row> </Row>
{enableMaxTokens && ( <Row align="middle" gutter={10}>
<Row align="middle" gutter={10}> <Col span={24}>
<Col span={16}> <Slider
<Slider disabled={!enableMaxTokens}
min={0} min={0}
max={32000} max={32000}
onChange={onMaxTokensChange} onChange={setMaxTokens}
value={typeof maxTokens === 'number' ? maxTokens : 0} onChangeComplete={onMaxTokensChange}
step={100} value={typeof maxTokens === 'number' ? maxTokens : 0}
/> step={100}
</Col> />
<Col span={8}> </Col>
<InputNumberic </Row>
min={0}
max={32000}
step={100}
value={maxTokens}
onChange={onMaxTokensChange}
controls={true}
style={{ width: '100%' }}
size="small"
/>
</Col>
</Row>
)}
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle> <SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@@ -216,7 +179,30 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingSubtitle style={{ marginTop: 20 }}>{t('settings.messages.input.title')}</SettingSubtitle> <SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => {
dispatch(setFontSize(value))
console.debug('set font size', value)
}}
min={12}
max={18}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
18: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</Col>
</Row>
<SettingSubtitle>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
@@ -227,6 +213,19 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
</SettingRow>
<Select
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: `Enter ${t('chat.input.send')}` },
{ value: 'Shift+Enter', label: `Shift + Enter ${t('chat.input.send')}` }
]}
onChange={(value) => setSendMessageShortcut(value)}
style={{ width: '100%', marginTop: 10 }}
/>
</Container> </Container>
) )
} }
@@ -238,21 +237,10 @@ const Container = styled.div`
padding: 0 15px; padding: 0 15px;
` `
const InputNumberic = styled(InputNumber)`
width: 45px;
padding: 0;
margin-left: 5px;
text-align: center;
.ant-input-number-input {
text-align: center;
}
`
const Label = styled.p` const Label = styled.p`
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
font-weight: 600; margin-right: 5px;
margin-right: 8px;
` `
const QuestionIcon = styled(QuestionCircleOutlined)` const QuestionIcon = styled(QuestionCircleOutlined)`

View File

@@ -136,19 +136,17 @@ const Container = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
padding: 15px 10px; padding: 10px 10px;
` `
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 10px; padding: 7px 10px;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 4px;
font-size: 14px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: Ubuntu; font-family: Ubuntu;
transition: all 0.3s;
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
} }

View File

@@ -1,6 +1,8 @@
import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
import { useShowRightSidebar } from '@renderer/hooks/useStore' import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Segmented } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -54,14 +56,16 @@ const RightSidebar: FC<Props> = (props) => {
return ( return (
<Container> <Container>
<Tabs> <Segmented
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}> value={tab}
{t('common.topics')} style={{ borderRadius: 0, padding: '10px', gap: 5, borderBottom: '0.5px solid var(--color-border)' }}
</Tab> options={[
<Tab className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}> { label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
{t('settings.title')} { label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
</Tab> ]}
</Tabs> block
onChange={(value) => setTab(value as 'topic' | 'settings')}
/>
<TabContent> <TabContent>
{tab === 'topic' && <TopicsTab {...props} />} {tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />} {tab === 'settings' && <SettingsTab assistant={props.assistant} />}
@@ -82,29 +86,6 @@ const Container = styled.div`
} }
` `
const Tabs = styled.div`
display: flex;
flex-direction: row;
border-bottom: 0.5px solid var(--color-border);
padding: 0 10px;
`
const Tab = styled.div`
padding: 8px 0;
font-weight: 500;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
font-size: 13px;
cursor: pointer;
color: var(--color-text-3);
&.active {
color: var(--color-text-2);
font-weight: 600;
}
`
const TabContent = styled.div` const TabContent = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;

View File

@@ -1,7 +1,7 @@
import { getModelLogo } from '@renderer/config/provider' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { Avatar, Button } from 'antd' import { Button } from 'antd'
import { upperFirst } from 'lodash' import { upperFirst } from 'lodash'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -20,7 +20,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
return ( return (
<SelectModelDropdown model={model} onSelect={setModel}> <SelectModelDropdown model={model} onSelect={setModel}>
<DropdownButton size="small" type="default"> <DropdownButton size="small" type="default">
<Avatar src={getModelLogo(model?.id || '')} style={{ width: 20, height: 20 }} /> <ModelAvatar model={model} size={20} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName> <ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton> </DropdownButton>
</SelectModelDropdown> </SelectModelDropdown>

View File

@@ -2,7 +2,7 @@ import { getModelLogo } from '@renderer/config/provider'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd' import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash' import { first, sortBy, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -22,7 +22,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
key: p.id, key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name, label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group', type: 'group',
children: p.models.map((m) => ({ children: sortBy(p.models, 'name').map((m) => ({
key: m?.id, key: m?.id,
label: upperFirst(m?.name), label: upperFirst(m?.name),
defaultSelectedKeys: [model?.id], defaultSelectedKeys: [model?.id],

View File

@@ -1,45 +0,0 @@
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { SendOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
sendMessage: () => void
}
const SendMessageButton: FC<Props> = ({ sendMessage }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
const sendSettingItems: MenuProps['items'] = [
{
label: `Enter ${t('chat.input.send')}`,
key: 'Enter',
icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter')
},
{
label: `Shift+Enter ${t('chat.input.send')}`,
key: 'Shift+Enter',
icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter')
}
]
return (
<Dropdown.Button
size="small"
onClick={sendMessage}
trigger={['click']}
arrow
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
style={{ width: 'auto' }}>
{t('chat.input.send')}
<SendOutlined />
</Dropdown.Button>
)
}
export default SendMessageButton

View File

@@ -1,4 +1,5 @@
import { GithubOutlined } from '@ant-design/icons' import { GithubOutlined } from '@ant-design/icons'
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
@@ -10,7 +11,7 @@ import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components' import { SettingContainer, SettingDivider, SettingRow, SettingTitle } from '.'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
@@ -134,31 +135,45 @@ const AboutSettings: FC = () => {
</AboutHeader> </AboutHeader>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.releases.title')}</SettingRowTitle> <SettingRowTitle>
<SoundOutlined />
{t('settings.about.releases.title')}
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}> <Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}>
{t('settings.about.releases.button')} {t('settings.about.releases.button')}
</Button> </Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle> <SettingRowTitle>
<GlobalOutlined />
{t('settings.about.website.title')}
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button> <Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.feedback.title')}</SettingRowTitle> <SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues')}> <GithubOutlined />
{t('settings.about.feedback.title')}
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues/new')}>
{t('settings.about.feedback.button')} {t('settings.about.feedback.button')}
</Button> </Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.license.title')}</SettingRowTitle> <SettingRowTitle>
<FileProtectOutlined />
{t('settings.about.license.title')}
</SettingRowTitle>
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button> <Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle> <SettingRowTitle>
<MailOutlined /> {t('settings.about.contact.title')}
</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button> <Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@@ -210,4 +225,18 @@ const ProgressCircle = styled(Progress)`
left: -2px; left: -2px;
` `
export const SettingRowTitle = styled.div`
font-size: 14px;
line-height: 18px;
color: var(--color-text-1);
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
.anticon {
font-size: 16px;
color: var(--color-text-1);
}
`
export default AboutSettings export default AboutSettings

View File

@@ -5,12 +5,11 @@ import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { AssistantSettings as AssistantSettingsType } from '@renderer/types' import { AssistantSettings as AssistantSettingsType } from '@renderer/types'
import { Button, Col, Input, InputNumber, Row, Slider, Switch, Tooltip } from 'antd' import { Button, Col, Input, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { debounce } from 'lodash' import { FC, useState } from 'react'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components' import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from '.'
const AssistantSettings: FC = () => { const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant() const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
@@ -21,43 +20,33 @@ const AssistantSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const onUpdateAssistantSettings = useCallback( const onUpdateAssistantSettings = (settings: Partial<AssistantSettingsType>) => {
debounce( updateDefaultAssistant({
(settings: Partial<AssistantSettingsType>) => { ...defaultAssistant,
updateDefaultAssistant({ settings: {
...defaultAssistant, ...defaultAssistant.settings,
settings: { temperature: settings.temperature ?? temperature,
...defaultAssistant.settings, contextCount: settings.contextCount ?? contextCount,
temperature: settings.temperature ?? temperature, enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
contextCount: settings.contextCount ?? contextCount, maxTokens: settings.maxTokens ?? maxTokens
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens, }
maxTokens: settings.maxTokens ?? maxTokens })
} }
})
},
1000,
{ leading: false, trailing: true }
),
[temperature, contextCount, enableMaxTokens, maxTokens]
)
const onTemperatureChange = (value) => { const onTemperatureChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ temperature: value }) onUpdateAssistantSettings({ temperature: value })
} }
} }
const onConextCountChange = (value) => { const onConextCountChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ contextCount: value }) onUpdateAssistantSettings({ contextCount: value })
} }
} }
const onMaxTokensChange = (value) => { const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
setMaxTokens(value)
onUpdateAssistantSettings({ maxTokens: value }) onUpdateAssistantSettings({ maxTokens: value })
} }
} }
@@ -88,6 +77,7 @@ const AssistantSettings: FC = () => {
placeholder={t('common.assistant') + t('common.name')} placeholder={t('common.assistant') + t('common.name')}
value={defaultAssistant.name} value={defaultAssistant.name}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, name: e.target.value })} onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, name: e.target.value })}
style={{ margin: '10px 0' }}
/> />
<SettingSubtitle>{t('common.prompt')}</SettingSubtitle> <SettingSubtitle>{t('common.prompt')}</SettingSubtitle>
<TextArea <TextArea
@@ -95,17 +85,15 @@ const AssistantSettings: FC = () => {
placeholder={t('common.assistant') + t('common.prompt')} placeholder={t('common.assistant') + t('common.prompt')}
value={defaultAssistant.prompt} value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })} onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
style={{ margin: '10px 0' }}
/> />
<SettingDivider />
<SettingSubtitle <SettingSubtitle
style={{ style={{
marginTop: 0,
marginBottom: 20,
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between'
}}> }}>
<span>{t('settings.assistant.model_params')}</span> {t('settings.assistant.model_params')}
<Button onClick={onReset} style={{ width: 90 }}> <Button onClick={onReset} style={{ width: 90 }}>
{t('chat.settings.reset')} {t('chat.settings.reset')}
</Button> </Button>
@@ -121,7 +109,8 @@ const AssistantSettings: FC = () => {
<Slider <Slider
min={0} min={0}
max={1.2} max={1.2}
onChange={onTemperatureChange} onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0} value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }} marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
step={0.1} step={0.1}
@@ -150,7 +139,8 @@ const AssistantSettings: FC = () => {
min={0} min={0}
max={20} max={20}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('chat.settings.max') }} marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('chat.settings.max') }}
onChange={onConextCountChange} onChange={setConextCount}
onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0} value={typeof contextCount === 'number' ? contextCount : 0}
step={1} step={1}
/> />
@@ -182,34 +172,34 @@ const AssistantSettings: FC = () => {
}} }}
/> />
</Row> </Row>
{enableMaxTokens && ( <Row align="middle" gutter={20}>
<Row align="middle" gutter={20}> <Col span={21}>
<Col span={21}> <Slider
<Slider disabled={!enableMaxTokens}
min={0} min={0}
max={32000} max={32000}
onChange={onMaxTokensChange} onChange={setMaxTokens}
value={typeof maxTokens === 'number' ? maxTokens : 0} onChangeComplete={onMaxTokensChange}
step={100} value={typeof maxTokens === 'number' ? maxTokens : 0}
marks={{ step={100}
0: '0', marks={{
32000: t('chat.settings.max') 0: '0',
}} 32000: t('chat.settings.max')
/> }}
</Col> />
<Col span={3}> </Col>
<InputNumber <Col span={3}>
min={0} <InputNumber
max={32000} min={0}
step={100} max={32000}
value={maxTokens} step={100}
onChange={onMaxTokensChange} value={maxTokens}
controls={true} onChange={onMaxTokensChange}
style={{ width: '100%' }} controls={true}
/> style={{ width: '100%' }}
</Col> />
</Row> </Col>
)} </Row>
</SettingContainer> </SettingContainer>
) )
} }
@@ -217,7 +207,6 @@ const AssistantSettings: FC = () => {
const Label = styled.p` const Label = styled.p`
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: bold;
margin-right: 5px; margin-right: 5px;
` `

View File

@@ -4,19 +4,20 @@ import i18n from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings' import { setFontSize, setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings' import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import { compressImage, isValidProxyUrl } from '@renderer/utils' import { compressImage, isValidProxyUrl } from '@renderer/utils'
import { Avatar, Input, Select, Upload } from 'antd' import { Avatar, Input, Select, Slider, Upload } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components' import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const avatar = useAvatar() const avatar = useAvatar()
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings() const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme, fontSize } = useSettings()
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
@@ -100,6 +101,27 @@ const GeneralSettings: FC = () => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.font_size.title')}</SettingRowTitle>
<Slider
style={{ width: 290 }}
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => {
dispatch(setFontSize(value))
console.debug('set font size', value)
}}
min={12}
max={18}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
18: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input <Input

View File

@@ -7,7 +7,7 @@ import { find, sortBy, upperFirst } from 'lodash'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingTitle } from './components' import { SettingContainer, SettingDivider, SettingTitle } from '.'
const ModelSettings: FC = () => { const ModelSettings: FC = () => {
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } = const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =

View File

@@ -4,8 +4,8 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingSubtitle } from '../components' import { SettingSubtitle } from '..'
import { HelpText, HelpTextRow } from '../components/ProviderSetting' import { HelpText, HelpTextRow } from '../ProviderSettings/ProviderSetting'
const OllamSettings: FC = () => { const OllamSettings: FC = () => {
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings() const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()

View File

@@ -19,10 +19,10 @@ import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import OllamSettings from '../providers/OllamaSettings' import { SettingContainer, SettingSubtitle, SettingTitle } from '..'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import AddModelPopup from './AddModelPopup' import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup' import EditModelsPopup from './EditModelsPopup'
import OllamSettings from './OllamaSettings'
interface Props { interface Props {
provider: Provider provider: Provider
@@ -95,7 +95,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <Divider style={{ width: '100%', margin: '10px 0' }} />
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle> <SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password <Input.Password
value={apiKey} value={apiKey}
placeholder={t('settings.provider.api_key')} placeholder={t('settings.provider.api_key')}
@@ -117,7 +117,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</HelpTextRow> </HelpTextRow>
)} )}
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle> <SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input <Input
value={apiHost} value={apiHost}
placeholder={t('settings.provider.api_host')} placeholder={t('settings.provider.api_host')}
@@ -128,7 +128,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>} {apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
</Space.Compact> </Space.Compact>
{provider.id === 'ollama' && <OllamSettings />} {provider.id === 'ollama' && <OllamSettings />}
<SettingSubtitle>{t('common.models')}</SettingSubtitle> <SettingSubtitle style={{ marginBottom: 5 }}>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => ( {Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small"> <Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
{modelGroups[group].map((model) => ( {modelGroups[group].map((model) => (
@@ -148,7 +148,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<HelpTextRow> <HelpTextRow>
<HelpText>{t('settings.provider.docs_check')} </HelpText> <HelpText>{t('settings.provider.docs_check')} </HelpText>
<HelpLink target="_blank" href={docsWebsite}> <HelpLink target="_blank" href={docsWebsite}>
{t(`provider.${provider.id}`)} {t(`provider.${provider.id}`) + ' '}
{t('common.docs')} {t('common.docs')}
</HelpLink> </HelpLink>
<HelpText>{t('common.and')}</HelpText> <HelpText>{t('common.and')}</HelpText>

View File

@@ -10,10 +10,10 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import AddProviderPopup from './components/AddProviderPopup' import AddProviderPopup from './AddProviderPopup'
import ProviderSetting from './components/ProviderSetting' import ProviderSetting from './ProviderSetting'
const ProviderSettings: FC = () => { const ProvidersList: FC = () => {
const providers = useAllProviders() const providers = useAllProviders()
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders() const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0]) const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
@@ -105,10 +105,13 @@ const ProviderSettings: FC = () => {
key={JSON.stringify(provider)} key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''} className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}> onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={25} />} {provider.isSystem && (
<Avatar shape="square" src={getProviderLogo(provider.id)} size={25} />
)}
{!provider.isSystem && ( {!provider.isSystem && (
<Avatar <Avatar
size={25} size={25}
shape="square"
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}> style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
{getFirstCharacter(provider.name)} {getFirstCharacter(provider.name)}
</Avatar> </Avatar>
@@ -156,7 +159,6 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width); width: var(--assistants-width);
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
padding: 8px;
overflow-y: auto; overflow-y: auto;
` `
@@ -164,6 +166,9 @@ const ProviderList = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
height: calc(100vh - var(--navbar-height));
overflow: auto;
padding: 8px;
` `
const ProviderListItem = styled.div` const ProviderListItem = styled.div`
@@ -200,7 +205,7 @@ const AddButtonWrapper = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 10px 0; padding: 10px 8px;
` `
export default ProviderSettings export default ProvidersList

View File

@@ -1,3 +1,10 @@
import {
CloudOutlined,
CodeSandboxOutlined,
InfoCircleOutlined,
MessageOutlined,
SettingOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -8,7 +15,7 @@ import AboutSettings from './AboutSettings'
import AssistantSettings from './AssistantSettings' import AssistantSettings from './AssistantSettings'
import GeneralSettings from './GeneralSettings' import GeneralSettings from './GeneralSettings'
import ModelSettings from './ModelSettings' import ModelSettings from './ModelSettings'
import ProviderSettings from './ProviderSettings' import ProvidersList from './ProviderSettings'
const SettingsPage: FC = () => { const SettingsPage: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
@@ -24,24 +31,39 @@ const SettingsPage: FC = () => {
<ContentContainer> <ContentContainer>
<SettingMenus> <SettingMenus>
<MenuItemLink to="/settings/provider"> <MenuItemLink to="/settings/provider">
<MenuItem className={isRoute('/settings/provider')}>{t('settings.provider')}</MenuItem> <MenuItem className={isRoute('/settings/provider')}>
<CloudOutlined />
{t('settings.provider')}
</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/model"> <MenuItemLink to="/settings/model">
<MenuItem className={isRoute('/settings/model')}>{t('settings.model')}</MenuItem> <MenuItem className={isRoute('/settings/model')}>
<CodeSandboxOutlined />
{t('settings.model')}
</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/assistant"> <MenuItemLink to="/settings/assistant">
<MenuItem className={isRoute('/settings/assistant')}>{t('settings.assistant')}</MenuItem> <MenuItem className={isRoute('/settings/assistant')}>
<MessageOutlined />
{t('settings.assistant')}
</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/general"> <MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}>{t('settings.general')}</MenuItem> <MenuItem className={isRoute('/settings/general')}>
<SettingOutlined />
{t('settings.general')}
</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/about"> <MenuItemLink to="/settings/about">
<MenuItem className={isRoute('/settings/about')}>{t('settings.about')}</MenuItem> <MenuItem className={isRoute('/settings/about')}>
<InfoCircleOutlined />
{t('settings.about')}
</MenuItem>
</MenuItemLink> </MenuItemLink>
</SettingMenus> </SettingMenus>
<SettingContent> <SettingContent>
<Routes> <Routes>
<Route path="provider" element={<ProviderSettings />} /> <Route path="provider" element={<ProvidersList />} />
<Route path="model" element={<ModelSettings />} /> <Route path="model" element={<ModelSettings />} />
<Route path="assistant" element={<AssistantSettings />} /> <Route path="assistant" element={<AssistantSettings />} />
<Route path="general" element={<GeneralSettings />} /> <Route path="general" element={<GeneralSettings />} />
@@ -81,13 +103,20 @@ const MenuItemLink = styled(Link)`
` `
const MenuItem = styled.li` const MenuItem = styled.li`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 6px 10px; padding: 6px 10px;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
font-size: 14px;
font-weight: 500; font-weight: 500;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
.anticon {
font-size: 16px;
opacity: 0.8;
}
&:hover { &:hover {
background: var(--color-primary-soft); background: var(--color-primary-soft);
} }

View File

@@ -1,6 +1,8 @@
import { Divider } from 'antd' import { Divider } from 'antd'
import styled from 'styled-components' import styled from 'styled-components'
import SettingsPage from './SettingsPage'
export const SettingContainer = styled.div` export const SettingContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -26,9 +28,10 @@ export const SettingTitle = styled.div`
export const SettingSubtitle = styled.div` export const SettingSubtitle = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text-2); color: var(--color-text-1);
margin: 15px 0 0 0; margin: 15px 0 0 0;
user-select: none; user-select: none;
font-weight: bold;
` `
export const SettingDivider = styled(Divider)` export const SettingDivider = styled(Divider)`
@@ -47,3 +50,5 @@ export const SettingRowTitle = styled.div`
line-height: 18px; line-height: 18px;
color: var(--color-text-1); color: var(--color-text-1);
` `
export default SettingsPage

View File

@@ -257,7 +257,7 @@ const InputContainer = styled.div`
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
border: 1px solid var(--color-border); border: 1px solid var(--color-border-soft);
border-radius: 10px; border-radius: 10px;
` `

View File

@@ -8,15 +8,22 @@ import { useTheme } from './ThemeProvider'
const AntdProvider: FC<PropsWithChildren> = ({ children }) => { const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
const { language } = useSettings() const { language } = useSettings()
const { theme: _theme } = useTheme() const { theme: _theme } = useTheme()
const isDarkTheme = _theme === 'dark'
return ( return (
<ConfigProvider <ConfigProvider
locale={getAntdLocale(language)} locale={getAntdLocale(language)}
theme={{ theme={{
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm], algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
components: {
Segmented: {
trackBg: 'transparent',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
boxShadowTertiary: undefined
}
},
token: { token: {
colorPrimary: '#00b96b', colorPrimary: '#00b96b'
borderRadius: 8
} }
}}> }}>
{children} {children}

View File

@@ -1,10 +1,12 @@
import Anthropic from '@anthropic-ai/sdk' import Anthropic from '@anthropic-ai/sdk'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources' import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { GoogleGenerativeAI } from '@google/generative-ai'
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant' import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama' import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types' import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import { removeQuotes } from '@renderer/utils' import { removeQuotes } from '@renderer/utils'
import { sum, takeRight } from 'lodash' import axios from 'axios'
import { isEmpty, sum, takeRight } from 'lodash'
import OpenAI from 'openai' import OpenAI from 'openai'
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources' import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
@@ -15,6 +17,7 @@ export default class ProviderSDK {
provider: Provider provider: Provider
openaiSdk: OpenAI openaiSdk: OpenAI
anthropicSdk: Anthropic anthropicSdk: Anthropic
geminiSdk: GoogleGenerativeAI
constructor(provider: Provider) { constructor(provider: Provider) {
this.provider = provider this.provider = provider
@@ -22,12 +25,17 @@ export default class ProviderSDK {
const baseURL = host.endsWith('/') ? host : `${provider.apiHost}/v1/` const baseURL = host.endsWith('/') ? host : `${provider.apiHost}/v1/`
this.anthropicSdk = new Anthropic({ apiKey: provider.apiKey, baseURL }) this.anthropicSdk = new Anthropic({ apiKey: provider.apiKey, baseURL })
this.openaiSdk = new OpenAI({ dangerouslyAllowBrowser: true, apiKey: provider.apiKey, baseURL }) this.openaiSdk = new OpenAI({ dangerouslyAllowBrowser: true, apiKey: provider.apiKey, baseURL })
this.geminiSdk = new GoogleGenerativeAI(provider.apiKey)
} }
private get isAnthropic() { private get isAnthropic() {
return this.provider.id === 'anthropic' return this.provider.id === 'anthropic'
} }
private get isGemini() {
return this.provider.id === 'gemini'
}
private get keepAliveTime() { private get keepAliveTime() {
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
} }
@@ -42,49 +50,99 @@ export default class ProviderSDK {
const { contextCount, maxTokens } = getAssistantSettings(assistant) const { contextCount, maxTokens } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({ const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
role: message.role, role: message.role,
content: message.content content: message.content
})) }))
if (this.isAnthropic) { if (this.isAnthropic) {
await this.anthropicSdk.messages return new Promise<void>((resolve, reject) => {
.stream({ const stream = this.anthropicSdk.messages
model: model.id, .stream({
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[], model: model.id,
max_tokens: maxTokens || DEFAULT_MAX_TOKENS, messages: userMessages.filter(Boolean) as MessageParam[],
temperature: assistant?.settings?.temperature max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
}) temperature: assistant?.settings?.temperature,
.on('text', (text) => onChunk({ text: text || '' })) system: assistant.prompt,
.on('finalMessage', (message) => stream: true
onChunk({
usage: {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
}
}) })
) .on('text', (text) => {
} else { if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
// @ts-ignore key is not typed resolve()
const stream = await this.openaiSdk.chat.completions.create({ return stream.controller.abort()
model: model.id, }
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[], onChunk({ text })
stream: true, })
temperature: assistant?.settings?.temperature, .on('finalMessage', (message) => {
max_tokens: maxTokens, onChunk({
keep_alive: this.keepAliveTime text: '',
usage: {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
}
})
resolve()
})
.on('error', (error) => reject(error))
}) })
for await (const chunk of stream) { }
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id,
systemInstruction: assistant.prompt,
generationConfig: {
maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature
}
})
const userLastMessage = userMessages.pop()
const chat = geminiModel.startChat({
history: userMessages.map((message) => ({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
}))
})
const userMessagesStream = await chat.sendMessageStream(userLastMessage?.content!)
for await (const chunk of userMessagesStream.stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
onChunk({ text: chunk.choices[0]?.delta?.content || '', usage: chunk.usage }) onChunk({
text: chunk.text(),
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
}
})
} }
return
}
// @ts-ignore key is not typed
const stream = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true,
temperature: assistant?.settings?.temperature,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime
})
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
onChunk({ text: chunk.choices[0]?.delta?.content || '', usage: chunk.usage })
} }
} }
public async translate(message: Message, assistant: Assistant) { public async translate(message: Message, assistant: Assistant) {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const { maxTokens } = getAssistantSettings(assistant)
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const messages = [ const messages = [
{ role: 'system', content: assistant.prompt }, { role: 'system', content: assistant.prompt },
@@ -94,29 +152,47 @@ export default class ProviderSDK {
if (this.isAnthropic) { if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({ const response = await this.anthropicSdk.messages.create({
model: model.id, model: model.id,
messages: messages as MessageParam[], messages: messages.filter((m) => m.role === 'user') as MessageParam[],
max_tokens: 4096, max_tokens: 4096,
temperature: assistant?.settings?.temperature, temperature: assistant?.settings?.temperature,
system: assistant.prompt,
stream: false stream: false
}) })
return response.content[0].type === 'text' ? response.content[0].text : '' return response.content[0].type === 'text' ? response.content[0].text : ''
} else {
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false,
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
} }
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id,
systemInstruction: assistant.prompt,
generationConfig: {
maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature
}
})
const { response } = await geminiModel.generateContent(message.content)
return response.text()
}
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false,
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
} }
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> { public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
const model = getTopNamingModel() || assistant.model || getDefaultModel() const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({ const userMessages = takeRight(messages, 5).map((message) => ({
role: 'user', role: message.role,
content: message.content content: message.content
})) }))
@@ -127,25 +203,49 @@ export default class ProviderSDK {
if (this.isAnthropic) { if (this.isAnthropic) {
const message = await this.anthropicSdk.messages.create({ const message = await this.anthropicSdk.messages.create({
messages: [systemMessage, ...userMessages] as Anthropic.Messages.MessageParam[], messages: userMessages as Anthropic.Messages.MessageParam[],
model: model.id, model: model.id,
system: systemMessage.content,
stream: false, stream: false,
max_tokens: 50 max_tokens: 4096
}) })
return message.content[0].type === 'text' ? message.content[0].text : null return message.content[0].type === 'text' ? message.content[0].text : null
} else { }
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({ if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id, model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[], systemInstruction: systemMessage.content,
stream: false, generationConfig: {
max_tokens: 50, temperature: assistant?.settings?.temperature
keep_alive: this.keepAliveTime }
}) })
return removeQuotes(response.choices[0].message?.content || '') const lastUserMessage = userMessages.pop()
const chat = await geminiModel.startChat({
history: userMessages.map((message) => ({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
}))
})
const { response } = await chat.sendMessage(lastUserMessage?.content!)
return response.text()
} }
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
stream: false,
max_tokens: 50,
keep_alive: this.keepAliveTime
})
return removeQuotes(response.choices[0].message?.content || '')
} }
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> { public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
@@ -172,6 +272,7 @@ export default class ProviderSDK {
public async check(): Promise<{ valid: boolean; error: Error | null }> { public async check(): Promise<{ valid: boolean; error: Error | null }> {
const model = this.provider.models[0] const model = this.provider.models[0]
const body = { const body = {
model: model.id, model: model.id,
messages: [{ role: 'user', content: 'hi' }], messages: [{ role: 'user', content: 'hi' }],
@@ -182,13 +283,32 @@ export default class ProviderSDK {
try { try {
if (this.isAnthropic) { if (this.isAnthropic) {
const message = await this.anthropicSdk.messages.create(body as MessageCreateParamsNonStreaming) const message = await this.anthropicSdk.messages.create(body as MessageCreateParamsNonStreaming)
return { valid: message.content.length > 0, error: null } return {
} else { valid: message.content.length > 0,
const response = await this.openaiSdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming) error: null
return { valid: Boolean(response?.choices[0].message), error: null } }
}
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({ model: body.model })
const result = await geminiModel.generateContent(body.messages[0].content)
return {
valid: !isEmpty(result.response.text()),
error: null
}
}
const response = await this.openaiSdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
return {
valid: Boolean(response?.choices[0].message),
error: null
} }
} catch (error: any) { } catch (error: any) {
return { valid: false, error } return {
valid: false,
error
}
} }
} }
@@ -198,6 +318,22 @@ export default class ProviderSDK {
return [] return []
} }
if (this.isGemini) {
const api = this.provider.apiHost + '/v1beta/models'
const { data } = await axios.get(api, { params: { key: this.provider.apiKey } })
return data.models.map(
(m: any) =>
({
id: m.name.replace('models/', ''),
name: m.displayName,
description: m.description,
object: 'model',
created: Date.now(),
owned_by: 'gemini'
}) as OpenAI.Models.Model
)
}
const response = await this.openaiSdk.models.list() const response = await this.openaiSdk.models.list()
return response.data return response.data
} catch (error) { } catch (error) {

View File

@@ -123,7 +123,7 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
const providerSdk = new ProviderSDK(provider) const providerSdk = new ProviderSDK(provider)
try { try {
return await providerSdk.summaries(messages, assistant) return await providerSdk.summaries(filterMessages(messages), assistant)
} catch (error: any) { } catch (error: any) {
return null return null
} }

View File

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

View File

@@ -31,6 +31,15 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: true enabled: true
}, },
{
id: 'gemini',
name: 'Gemini',
apiKey: '',
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{ {
id: 'silicon', id: 'silicon',
name: 'Silicon', name: 'Silicon',
@@ -95,11 +104,20 @@ const initialState: LlmState = {
enabled: false enabled: false
}, },
{ {
id: 'anthropic', id: 'stepfun',
name: 'Anthropic', name: 'StepFun',
apiKey: '', apiKey: '',
apiHost: 'https://api.anthropic.com/', apiHost: 'https://api.stepfun.com',
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled), models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'doubao',
name: 'doubao',
apiKey: '',
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
@@ -112,6 +130,15 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
{
id: 'graphrag-kylin-mountain',
name: 'GraphRAG',
apiKey: '',
apiHost: '',
models: [],
isSystem: true,
enabled: false
},
{ {
id: 'openrouter', id: 'openrouter',
name: 'OpenRouter', name: 'OpenRouter',
@@ -130,6 +157,15 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
{
id: 'anthropic',
name: 'Anthropic',
apiKey: '',
apiHost: 'https://api.anthropic.com/',
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{ {
id: 'ollama', id: 'ollama',
name: 'Ollama', name: 'Ollama',

View File

@@ -287,6 +287,62 @@ const migrateConfig = {
} }
} }
} }
},
'20': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
fontSize: 14
}
}
},
'21': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: [
...state.llm.providers,
{
id: 'gemini',
name: 'Gemini',
apiKey: '',
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'stepfun',
name: 'StepFun',
apiKey: '',
apiHost: 'https://api.stepfun.com',
models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'doubao',
name: 'doubao',
apiKey: '',
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'graphrag-kylin-mountain',
name: 'GraphRAG',
apiKey: '',
apiHost: '',
models: [],
isSystem: true,
enabled: false
}
]
}
}
} }
} }

View File

@@ -4,11 +4,13 @@ import Logo from '@renderer/assets/images/logo.png'
export interface RuntimeState { export interface RuntimeState {
avatar: string avatar: string
generating: boolean generating: boolean
minappShow: boolean
} }
const initialState: RuntimeState = { const initialState: RuntimeState = {
avatar: Logo, avatar: Logo,
generating: false generating: false,
minappShow: false
} }
const runtimeSlice = createSlice({ const runtimeSlice = createSlice({
@@ -20,10 +22,13 @@ const runtimeSlice = createSlice({
}, },
setGenerating: (state, action: PayloadAction<boolean>) => { setGenerating: (state, action: PayloadAction<boolean>) => {
state.generating = action.payload state.generating = action.payload
},
setMinappShow: (state, action: PayloadAction<boolean>) => {
state.minappShow = action.payload
} }
} }
}) })
export const { setAvatar, setGenerating } = runtimeSlice.actions export const { setAvatar, setGenerating, setMinappShow } = runtimeSlice.actions
export default runtimeSlice.reducer export default runtimeSlice.reducer

View File

@@ -19,6 +19,7 @@ export interface SettingsState {
messageFont: 'system' | 'serif' messageFont: 'system' | 'serif'
showInputEstimatedTokens: boolean showInputEstimatedTokens: boolean
theme: ThemeMode theme: ThemeMode
fontSize: number
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@@ -31,7 +32,8 @@ const initialState: SettingsState = {
showMessageDivider: false, showMessageDivider: false,
messageFont: 'system', messageFont: 'system',
showInputEstimatedTokens: false, showInputEstimatedTokens: false,
theme: ThemeMode.light theme: ThemeMode.light,
fontSize: 14
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@@ -70,6 +72,9 @@ const settingsSlice = createSlice({
}, },
setTheme: (state, action: PayloadAction<ThemeMode>) => { setTheme: (state, action: PayloadAction<ThemeMode>) => {
state.theme = action.payload state.theme = action.payload
},
setFontSize: (state, action: PayloadAction<number>) => {
state.fontSize = action.payload
} }
} }
}) })
@@ -85,7 +90,8 @@ export const {
setShowMessageDivider, setShowMessageDivider,
setMessageFont, setMessageFont,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
setTheme setTheme,
setFontSize
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@@ -961,6 +961,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@google/generative-ai@npm:^0.16.0":
version: 0.16.0
resolution: "@google/generative-ai@npm:0.16.0"
checksum: 10c0/5d561a41cb7be60fc9b49965b66359e15df907bf6679009de7917beff138ba69d4a0772ab2a9d6f0e543d658d72bd19b83e6abdb87a6cdfa402a8764b08eed4c
languageName: node
linkType: hard
"@hello-pangea/dnd@npm:^16.6.0": "@hello-pangea/dnd@npm:^16.6.0":
version: 16.6.0 version: 16.6.0
resolution: "@hello-pangea/dnd@npm:16.6.0" resolution: "@hello-pangea/dnd@npm:16.6.0"
@@ -2698,25 +2705,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ahooks@npm:^3.8.0":
version: 3.8.0
resolution: "ahooks@npm:3.8.0"
dependencies:
"@babel/runtime": "npm:^7.21.0"
dayjs: "npm:^1.9.1"
intersection-observer: "npm:^0.12.0"
js-cookie: "npm:^2.x.x"
lodash: "npm:^4.17.21"
react-fast-compare: "npm:^3.2.2"
resize-observer-polyfill: "npm:^1.5.1"
screenfull: "npm:^5.0.0"
tslib: "npm:^2.4.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/28344a10443a1374066d931ac687e8f18f714717c32fc4c12b5fea1d0b29b9dfbddd5e9943df2394dd7912440e474413d899e1a2877bf80093fbe8f1c3b5ea58
languageName: node
linkType: hard
"ajv-formats@npm:^2.1.1": "ajv-formats@npm:^2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "ajv-formats@npm:2.1.1" resolution: "ajv-formats@npm:2.1.1"
@@ -3099,6 +3087,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"axios@npm:^1.7.3":
version: 1.7.3
resolution: "axios@npm:1.7.3"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/a18cbe559203efa05fb1fec2d1898e23bf6329bd2575784ee32aa11b5bbe1d54b9f472c49a261294125519cf62aa4fe5ef6e647bb7482eafc15bffe15ab314ce
languageName: node
linkType: hard
"bail@npm:^2.0.0": "bail@npm:^2.0.0":
version: 2.0.2 version: 2.0.2
resolution: "bail@npm:2.0.2" resolution: "bail@npm:2.0.2"
@@ -3446,6 +3445,7 @@ __metadata:
"@electron-toolkit/preload": "npm:^3.0.0" "@electron-toolkit/preload": "npm:^3.0.0"
"@electron-toolkit/tsconfig": "npm:^1.0.1" "@electron-toolkit/tsconfig": "npm:^1.0.1"
"@electron-toolkit/utils": "npm:^3.0.0" "@electron-toolkit/utils": "npm:^3.0.0"
"@google/generative-ai": "npm:^0.16.0"
"@hello-pangea/dnd": "npm:^16.6.0" "@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0" "@kangfenmao/keyv-storage": "npm:^0.1.0"
"@reduxjs/toolkit": "npm:^2.2.5" "@reduxjs/toolkit": "npm:^2.2.5"
@@ -3455,8 +3455,8 @@ __metadata:
"@types/react": "npm:^18.2.48" "@types/react": "npm:^18.2.48"
"@types/react-dom": "npm:^18.2.18" "@types/react-dom": "npm:^18.2.18"
"@vitejs/plugin-react": "npm:^4.2.1" "@vitejs/plugin-react": "npm:^4.2.1"
ahooks: "npm:^3.8.0"
antd: "npm:^5.18.3" antd: "npm:^5.18.3"
axios: "npm:^1.7.3"
browser-image-compression: "npm:^2.0.2" browser-image-compression: "npm:^2.0.2"
dayjs: "npm:^1.11.11" dayjs: "npm:^1.11.11"
dotenv-cli: "npm:^7.4.2" dotenv-cli: "npm:^7.4.2"
@@ -3831,7 +3831,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dayjs@npm:^1.11.11, dayjs@npm:^1.9.1": "dayjs@npm:^1.11.11":
version: 1.11.11 version: 1.11.11
resolution: "dayjs@npm:1.11.11" resolution: "dayjs@npm:1.11.11"
checksum: 10c0/0131d10516b9945f05a57e13f4af49a6814de5573a494824e103131a3bbe4cc470b1aefe8e17e51f9a478a22cd116084be1ee5725cedb66ec4c3f9091202dc4b checksum: 10c0/0131d10516b9945f05a57e13f4af49a6814de5573a494824e103131a3bbe4cc470b1aefe8e17e51f9a478a22cd116084be1ee5725cedb66ec4c3f9091202dc4b
@@ -5037,6 +5037,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.6
resolution: "follow-redirects@npm:1.15.6"
peerDependenciesMeta:
debug:
optional: true
checksum: 10c0/9ff767f0d7be6aa6870c82ac79cf0368cd73e01bbc00e9eb1c2a16fbb198ec105e3c9b6628bb98e9f3ac66fe29a957b9645bcb9a490bb7aa0d35f908b6b85071
languageName: node
linkType: hard
"for-each@npm:^0.3.3": "for-each@npm:^0.3.3":
version: 0.3.3 version: 0.3.3
resolution: "for-each@npm:0.3.3" resolution: "for-each@npm:0.3.3"
@@ -5876,13 +5886,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"intersection-observer@npm:^0.12.0":
version: 0.12.2
resolution: "intersection-observer@npm:0.12.2"
checksum: 10c0/9591f46b2b742f5801ed69dbc8860f487771b4af8361e7a5dcb28a377beff2ba56336a2b090af261825430d225dae9417121496d2e6925e000e4a469958843ff
languageName: node
linkType: hard
"ip-address@npm:^9.0.5": "ip-address@npm:^9.0.5":
version: 9.0.5 version: 9.0.5
resolution: "ip-address@npm:9.0.5" resolution: "ip-address@npm:9.0.5"
@@ -6320,13 +6323,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"js-cookie@npm:^2.x.x":
version: 2.2.1
resolution: "js-cookie@npm:2.2.1"
checksum: 10c0/ee67fc0f8495d0800b851910b5eb5bf49d3033adff6493d55b5c097ca6da46f7fe666b10e2ecb13cfcaf5b88d71c205ce00a7e646de791689bfd053bbb36a376
languageName: node
linkType: hard
"js-tiktoken@npm:^1.0.10, js-tiktoken@npm:^1.0.7": "js-tiktoken@npm:^1.0.10, js-tiktoken@npm:^1.0.7":
version: 1.0.12 version: 1.0.12
resolution: "js-tiktoken@npm:1.0.12" resolution: "js-tiktoken@npm:1.0.12"
@@ -8251,6 +8247,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"pump@npm:^3.0.0": "pump@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "pump@npm:3.0.0" resolution: "pump@npm:3.0.0"
@@ -8844,13 +8847,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-fast-compare@npm:^3.2.2":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
languageName: node
linkType: hard
"react-i18next@npm:^14.1.2": "react-i18next@npm:^14.1.2":
version: 14.1.2 version: 14.1.2
resolution: "react-i18next@npm:14.1.2" resolution: "react-i18next@npm:14.1.2"
@@ -9514,13 +9510,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"screenfull@npm:^5.0.0":
version: 5.2.0
resolution: "screenfull@npm:5.2.0"
checksum: 10c0/86fd49983e2edc153ee2e674a570c711cb0961a9cacca659309f79636ccc8ca8a0b830ea4dacdae7403a8bb7ba6affd5bcdce053aa97782961247a49bfd2ba68
languageName: node
linkType: hard
"scroll-into-view-if-needed@npm:^3.1.0": "scroll-into-view-if-needed@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "scroll-into-view-if-needed@npm:3.1.0" resolution: "scroll-into-view-if-needed@npm:3.1.0"
@@ -10106,7 +10095,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.1.0, tslib@npm:^2.4.1": "tslib@npm:^2.1.0":
version: 2.6.3 version: 2.6.3
resolution: "tslib@npm:2.6.3" resolution: "tslib@npm:2.6.3"
checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a