Compare commits

...

18 Commits

Author SHA1 Message Date
kangfenmao
f7ef895ce6 chore(version): 0.5.0 2024-08-07 21:55:51 +08:00
kangfenmao
beb40f5baf feat: fix add assistant search keywords format 2024-08-07 20:57:31 +08:00
kangfenmao
07613e65f5 feat: add max token limit #18 2024-08-07 20:49:21 +08:00
kangfenmao
6185068353 feat: use ubuntu font as default 2024-08-07 14:28:29 +08:00
kangfenmao
61934cd65c feat add agent popup #14 2024-08-07 13:23:29 +08:00
kangfenmao
41f65b66ba chore(version): 0.4.9 2024-08-06 20:41:34 +08:00
kangfenmao
5edb53ef7d feat: add ollama settings 2024-08-06 20:38:01 +08:00
kangfenmao
167988927b feat: add custom agent #14 2024-08-06 19:18:17 +08:00
kangfenmao
a39beb3841 fix(AboutSettings.tsx): handle errors in update check by setting loading state 2024-08-05 16:15:58 +08:00
kangfenmao
8719d5c330 chore(version): 0.4.8 2024-08-05 13:20:55 +08:00
kangfenmao
a7427d6cb6 feat(i18n): new topic 2024-08-05 13:14:57 +08:00
kangfenmao
8759a50727 fix: estimate history token count 2024-08-05 13:09:13 +08:00
kangfenmao
7ffa42caa0 feat: input status use tag 2024-08-05 13:00:18 +08:00
kangfenmao
b0a3d705ff feat: @model regenerate message 2024-08-05 12:39:37 +08:00
kangfenmao
de41199f7e feat: quick regenerate with new model 2024-08-04 14:04:11 +08:00
kangfenmao
cbd9f60cfc fix: markdown link color 2024-08-04 13:30:15 +08:00
kangfenmao
8a0e2890dd fix: math code format 2024-08-04 13:23:35 +08:00
kangfenmao
a8f3e2be6b chore(release.yml): add CSC_LINK and CSC_KEY_PASSWORD environment variables for windows build to enable code signing 2024-08-02 15:17:21 +08:00
82 changed files with 1729 additions and 621 deletions

View File

@@ -52,6 +52,8 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Replace spaces in filenames

View File

@@ -56,5 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
新增发送按钮
输入区域展开可以全屏显示
支持保存自定义智能体
修复话题重命名的问题

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.4.7",
"version": "0.5.0",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -55,6 +55,7 @@
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.0.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",

View File

@@ -26,7 +26,7 @@ function createWindow() {
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 500,
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: process.platform === 'darwin',

View File

@@ -5,7 +5,7 @@ import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AppsPage from './pages/apps/AppsPage'
import AgentsPage from './pages/agents/AgentsPage'
import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
@@ -23,7 +23,7 @@ function App(): JSX.Element {
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/apps" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>

View File

@@ -1,47 +0,0 @@
@font-face {
font-family: 'Poppins';
src: url(Poppins-Thin.ttf) format('truetype');
font-weight: 100;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-ExtraLight.ttf) format('truetype');
font-weight: 200;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Light.ttf) format('truetype');
font-weight: 300;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Regular.ttf) format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Medium.ttf) format('truetype');
font-weight: 500;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-SemiBold.ttf) format('truetype');
font-weight: 600;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Bold.ttf) format('truetype');
font-weight: 700;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-ExtraBold.ttf) format('truetype');
font-weight: 800;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Italic.ttf') format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-BoldItalic.ttf') format('truetype');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-LightItalic.ttf') format('truetype');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-MediumItalic.ttf') format('truetype');
font-weight: 500;
font-style: italic;
}

View File

@@ -1,7 +1,7 @@
@import './markdown.scss';
@import './scrollbar.scss';
@import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/Poppins/Poppins.css';
@import '../fonts/Ubuntu/Ubuntu.css';
:root {
--color-white: #ffffff;
@@ -33,6 +33,7 @@
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
@@ -79,6 +80,7 @@ body[theme-mode='light'] {
--color-icon-white: #000000;
--color-border: #00000028;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
@@ -106,18 +108,8 @@ body {
line-height: 1.6;
overflow: hidden;
background: transparent !important;
font-family:
-apple-system,
BlinkMacSystemFont,
'Microsoft YaHei',
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue' sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

@@ -4,14 +4,6 @@
line-height: 1.6;
user-select: text;
p:last-child {
margin-bottom: 5px;
}
p:first-child {
margin-top: 0;
}
h1:first-child,
h2:first-child,
h3:first-child,
@@ -29,6 +21,8 @@
h6 {
margin: 1em 0 1em 0;
font-weight: 800;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
h1 {
@@ -61,6 +55,13 @@
p {
margin: 1em 0;
&:last-child {
margin-bottom: 5px;
}
&:first-child {
margin-top: 0;
}
}
ul,
@@ -105,6 +106,7 @@
padding: 1em;
border-radius: 5px;
overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace;
pre {
margin: 0 !important;
}
@@ -120,6 +122,7 @@
padding-left: 1em;
color: var(--color-text-light);
border-left: 4px solid var(--color-border);
font-family: Georgia, 'Times New Roman', Times, serif;
}
table {
@@ -137,6 +140,8 @@
th {
background-color: var(--color-background-mute);
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
img {
@@ -144,9 +149,11 @@
height: auto;
}
a {
color: var(--color-primary);
a,
.link {
color: var(--color-link);
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
@@ -228,3 +235,7 @@
}
}
}
emoji-picker {
--border-size: 0;
}

View File

@@ -0,0 +1,49 @@
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { FC } from 'react'
interface Props<T> {
list: T[]
children: (item: T, index: number) => React.ReactNode
onUpdate: (list: T[]) => void
onDragStart?: () => void
onDragEnd?: () => void
}
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
const _onDragEnd = (result: DropResult) => {
onDragEnd?.()
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
onUpdate(reorderAgents)
}
}
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{list.map((item, index) => (
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
{children(item, index)}
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default DragableList

View File

@@ -0,0 +1,25 @@
import { useTheme } from '@renderer/providers/ThemeProvider'
import { FC, useEffect, useRef } from 'react'
interface Props {
onEmojiClick: (emoji: string) => void
}
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
const { theme } = useTheme()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
ref.current.addEventListener('emoji-click', (event: any) => {
event.stopPropagation()
onEmojiClick(event.detail.emoji.unicode)
})
}
}, [onEmojiClick])
// @ts-ignore next-line
return <emoji-picker ref={ref} class={theme === 'dark' ? 'dark' : 'light'} style={{ border: 'none' }} />
}
export default EmojiPicker

View File

@@ -150,11 +150,12 @@ export const BaseTypography = styled(Box)<{
`
export const TypographyNormal = styled(BaseTypography)`
font-family: 'Poppins';
font-family: 'Ubuntu';
`
export const TypographyBold = styled(BaseTypography)`
font-family: 'Poppins Bold';
font-family: 'Ubuntu';
font-weight: bold;
`
export const Container = styled.main<ContainerProps>`

View File

@@ -57,18 +57,19 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'AssistantSettingPopup'
)
})
}

View File

@@ -58,18 +58,19 @@ const PromptPopupContainer: React.FC<Props> = ({
export default class PromptPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('PromptPopup')
}
static show(props: PromptPopupShowParams) {
return new Promise<string>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PromptPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'PromptPopup'
)
})
}

View File

@@ -37,18 +37,19 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
export default class TemplatePopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('TemplatePopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'TemplatePopup'
)
})
}

View File

@@ -1,87 +1,104 @@
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import { findIndex, pullAt } from 'lodash'
import React, { useEffect, useState } from 'react'
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
import { Box } from '../Layout'
let id = 0
let onPop = () => {}
let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
element
key
id
}
let onHide = ({ key }: { key: number }) => {
key
let onHide = (id: string) => {
id
}
let onHideAll = () => {}
interface Props {
children?: React.ReactNode
}
type ElementItem = {
key: number
id: string
element: React.FC | React.ReactNode
}
const TopViewContainer: React.FC<Props> = ({ children }) => {
const [elements, setElements] = useState<ElementItem[]>([])
const elementsRef = useRef<ElementItem[]>([])
elementsRef.current = elements
const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal()
useAppInit()
onPop = () => {
const views = [...elements]
views.pop()
setElements(views)
}
onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
setElements(elements.concat([{ element, key }]))
}
onHide = ({ key }: { key: number }) => {
const views = [...elements]
pullAt(views, findIndex(views, { key }))
setElements(views)
}
useEffect(() => {
window.message = messageApi
window.modal = modal
}, [messageApi, modal])
onPop = () => {
console.debug('[TopView] onPop')
const views = [...elementsRef.current]
views.pop()
elementsRef.current = views
setElements(elementsRef.current)
}
onShow = ({ element, id }: ElementItem) => {
console.debug('[TopView] onShow', id)
if (!elementsRef.current.find((el) => el.id === id)) {
elementsRef.current = elementsRef.current.concat([{ element, id }])
setElements(elementsRef.current)
}
}
onHide = (id: string) => {
console.debug('[TopView] onHide', id, elementsRef.current)
elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
setElements(elementsRef.current)
}
onHideAll = () => {
console.debug('[TopView] onHideAll')
setElements([])
elementsRef.current = []
}
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
return (
<Box flex={1} position="absolute" w="100%" h="100%">
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
{children}
</Box>
)
}, [])
console.debug(
'[TopView]',
elements.map((el) => [el.id, el.element])
)
return (
<>
{children}
{messageContextHolder}
{modalContextHolder}
{elements.length > 0 && (
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
{elements.map(({ element: Element, key }) =>
typeof Element === 'function' ? (
<Element key={`TOPVIEW_${key}`} />
) : (
<div key={`TOPVIEW_${key}`}>{Element}</div>
)
)}
</div>
)}
{elements.map(({ element: Element, id }) => (
<FullScreenContainer key={`TOPVIEW_${id}`}>
{typeof Element === 'function' ? <Element /> : Element}
</FullScreenContainer>
))}
</>
)
}
export const TopView = {
show: (element: React.FC | React.ReactNode) => {
id = id + 1
onShow({ element, key: id })
return id
},
hide: (key: number) => {
onHide({ key })
},
show: (element: React.FC | React.ReactNode, id: string) => onShow({ element, id }),
hide: (id: string) => onHide(id),
clear: () => onHideAll(),
pop: onPop
}

View File

@@ -1,6 +1,5 @@
import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png'
import { isWindows } from '@renderer/config/constant'
import useAvatar from '@renderer/hooks/useAvatar'
import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom'
@@ -13,7 +12,7 @@ const Sidebar: FC = () => {
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container style={isWindows ? { paddingTop: 0 } : {}}>
<Container>
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>

View File

@@ -1,239 +1,239 @@
[
{
"id": 1,
"name": "🎯 产品经理 - Product Manager",
"id": "1",
"name": "产品经理 - Product Manager",
"emoji": "🎯",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。"
},
{
"id": 2,
"name": "🎯 策略产品经理 - Strategy Product Manager",
"emoji": "🎯",
"id": "2",
"name": "策略产品经理 - Strategy Product Manager",
"emoji": "🎯 ",
"group": "职业",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
},
{
"id": 3,
"name": "👥 社群运营 - Community Operations",
"id": "3",
"name": "社群运营 - Community Operations",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。"
},
{
"id": 4,
"name": "✍️ 内容运营 - Content Operations",
"id": "4",
"name": "内容运营 - Content Operations",
"emoji": "✍️",
"group": "职业",
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。"
},
{
"id": 5,
"name": "🛍️ 商家运营 - Merchant Operations",
"id": "5",
"name": "商家运营 - Merchant Operations",
"emoji": "🛍️",
"group": "职业",
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。"
},
{
"id": 6,
"name": "🚀 产品运营 - Product Operations",
"id": "6",
"name": "产品运营 - Product Operations",
"emoji": "🚀",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n"
},
{
"id": 7,
"name": "💼 销售运营 - Sales Operations",
"emoji": "🎓",
"id": "7",
"name": "销售运营 - Sales Operations",
"emoji": "💼",
"group": "职业",
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。"
},
{
"id": 8,
"name": "👨‍💻 用户运营 - User Operations",
"id": "8",
"name": "用户运营 - User Operations",
"emoji": "👨‍💻",
"group": "职业",
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n"
},
{
"id": 9,
"name": "📢 市场营销 - Marketing",
"id": "9",
"name": "市场营销 - Marketing",
"emoji": "📢",
"group": "职业",
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。"
},
{
"id": 10,
"name": "📈 商业数据分析 - Business Data Analysis",
"id": "10",
"name": "商业数据分析 - Business Data Analysis",
"emoji": "📈",
"group": "职业",
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。"
},
{
"id": 11,
"name": "🗂️ 项目管理 - Project Management",
"id": "11",
"name": "项目管理 - Project Management",
"emoji": "🗂️",
"group": "职业",
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。"
},
{
"id": 12,
"name": "🔎 SEO专家 - SEO Expert",
"id": "12",
"name": "SEO专家 - SEO Expert",
"emoji": "🔎",
"group": "职业",
"prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。"
},
{
"id": 13,
"name": "💻 网站运营数据分析 - Website Operations Data Analysis",
"id": "13",
"name": "网站运营数据分析 - Website Operations Data Analysis",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n"
},
{
"id": 14,
"name": "📊 数据分析师 - Data Analyst",
"id": "14",
"name": "数据分析师 - Data Analyst",
"emoji": "📊",
"group": "职业",
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。"
},
{
"id": 15,
"name": "🖥️ 前端工程师 - Frontend Engineer",
"id": "15",
"name": "前端工程师 - Frontend Engineer",
"emoji": "🖥️",
"group": "职业",
"prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n"
},
{
"id": 16,
"name": "🛠️ 运维工程师 - Operations Engineer",
"id": "16",
"name": "运维工程师 - Operations Engineer",
"emoji": "🛠️",
"group": "职业",
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。"
},
{
"id": 17,
"name": "💻 开发工程师 - Software Engineer",
"id": "17",
"name": "开发工程师 - Software Engineer",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。"
},
{
"id": 18,
"name": "🧪 测试工程师 - Test Engineer",
"id": "18",
"name": "测试工程师 - Test Engineer",
"emoji": "🧪",
"group": "职业",
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。"
},
{
"id": 19,
"name": "👥 HR人力资源管理 - Human Resources Management",
"id": "19",
"name": "HR人力资源管理 - Human Resources Management",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。"
},
{
"id": 20,
"name": "📋 行政 - Administration",
"id": "20",
"name": "行政 - Administration",
"emoji": "📋",
"group": "职业",
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。"
},
{
"id": 21,
"name": "💰 财务顾问 - Financial Advisor",
"id": "21",
"name": "财务顾问 - Financial Advisor",
"emoji": "💰",
"group": "职业",
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。"
},
{
"id": 22,
"name": "🩺 医生 - Doctor",
"id": "22",
"name": "医生 - Doctor",
"emoji": "🩺",
"group": "职业",
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。"
},
{
"id": 23,
"name": "✒️ 编辑 - Editor",
"id": "23",
"name": "编辑 - Editor",
"emoji": "✒️",
"group": "职业",
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n"
},
{
"id": 24,
"name": "🧠 哲学家 - Philosopher",
"id": "24",
"name": "哲学家 - Philosopher",
"emoji": "🧠",
"group": "职业",
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n"
},
{
"id": 25,
"name": "🛒 采购 - Procurement",
"id": "25",
"name": "采购 - Procurement",
"emoji": "🛒",
"group": "职业",
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n"
},
{
"id": 26,
"name": "⚖️ 法务 - Legal Affairs",
"id": "26",
"name": "法务 - Legal Affairs",
"emoji": "⚖️",
"group": "职业",
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。"
},
{
"id": 27,
"name": "🇨🇳 翻译成中文 - Chinese",
"id": "27",
"name": "翻译成中文 - Chinese",
"emoji": "🇨🇳",
"group": "语言",
"prompt": "你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。",
"description": ""
},
{
"id": 28,
"name": "🌐 翻译成英文 - English",
"id": "28",
"name": "翻译成英文 - English",
"emoji": "🌐",
"group": "语言",
"prompt": "你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。",
"description": ""
},
{
"id": 29,
"name": "📕 英语单词背诵助手",
"id": "29",
"name": "英语单词背诵助手",
"emoji": "📕",
"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作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。",
"description": ""
},
{
"id": 30,
"name": "📖 文章总结 - Summarize",
"id": "30",
"name": "文章总结 - Summarize",
"emoji": "📖",
"group": "阅读",
"prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复",

View File

@@ -1,5 +1,8 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 5
export const DEFAULT_MAX_TOKENS = 4096
export const FONT_FAMILY =
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
export const platform = window.electron?.process?.platform === 'darwin' ? 'macos' : 'windows'
export const isMac = platform === 'macos'
export const isWindows = platform === 'windows'

View File

@@ -0,0 +1,17 @@
import { RootState } from '@renderer/store'
import { addAgent, removeAgent, updateAgent, updateAgents } from '@renderer/store/agents'
import { Agent } from '@renderer/types'
import { useDispatch, useSelector } from 'react-redux'
export function useAgents() {
const agents = useSelector((state: RootState) => state.agents.agents)
const dispatch = useDispatch()
return {
agents,
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (agent: Agent) => dispatch(removeAgent(agent)),
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents))
}
}

View File

@@ -0,0 +1,18 @@
import store, { useAppSelector } from '@renderer/store'
import { setOllamaKeepAliveTime } from '@renderer/store/llm'
import { useDispatch } from 'react-redux'
export function useOllamaSettings() {
const settings = useAppSelector((state) => state.llm.settings.ollama)
const dispatch = useDispatch()
return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) }
}
export function getOllamaSettings() {
return store.getState().llm.settings.ollama
}
export function getOllamaKeepAliveTime() {
return store.getState().llm.settings.ollama.keepAliveTime + 'm'
}

View File

@@ -2,12 +2,10 @@ import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect, useState } from 'react'
const activeTopicsMap = new Map<string, Topic>()
let _activeTopic: Topic
export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0])
activeTopicsMap.set(assistant.id, activeTopic)
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
useEffect(() => {
// activeTopic not in assistant.topics

View File

@@ -25,7 +25,9 @@ const resources = {
provider: 'Provider',
you: 'You',
save: 'Save',
footnotes: 'References'
footnotes: 'References',
select: 'Select',
search: 'Search'
},
button: {
add: 'Add',
@@ -48,9 +50,7 @@ const resources = {
'switch.disabled': 'Switching is disabled while the assistant is generating'
},
chat: {
save: 'Save'
},
assistant: {
save: 'Save',
'default.name': '😀 Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
'default.topic.name': 'Default Topic',
@@ -60,7 +60,7 @@ const resources = {
'topics.edit.placeholder': 'Enter new name',
'topics.delete.all.title': 'Delete all topics',
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
'input.new_chat': ' New Chat ',
'input.new_topic': 'New Topic',
'input.topics': ' Topics ',
'input.clear': 'Clear',
'input.expand': 'Expand',
@@ -71,18 +71,37 @@ const resources = {
'input.send': 'Send',
'input.pause': 'Pause',
'input.settings': 'Settings',
'input.context_count.tip': 'Context Count',
'input.estimated_tokens.tip': 'Estimated tokens',
'settings.temperature': 'Temperature',
'settings.temperature.tip':
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
'settings.conext_count': 'Context',
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
'settings.max_tokens': 'Enable Max Tokens Limit',
'settings.max_tokens.tip':
'The maximum number of tokens the model can generate. Normal chat suggests 500-800. Short text generation suggests 800-2000. Code generation suggests 2000-3600. Long text generation suggests above 4000.',
'settings.reset': 'Reset',
'settings.set_as_default': 'Apply to default assistant',
'settings.max': 'Max',
'suggestions.title': 'Suggested Questions'
'suggestions.title': 'Suggested Questions',
'add.assistant.title': 'Add Assistant'
},
apps: {
title: 'Agents'
agents: {
title: 'Agents',
my_agents: 'My Agents',
'add.title': 'Add Agent',
'edit.title': 'Edit Agent',
'add.name': 'Name',
'add.name.placeholder': 'Enter name',
'add.prompt': 'Prompt',
'add.prompt.placeholder': 'Enter prompt',
'add.button': 'Add',
'manage.title': 'Manage Agents',
'delete.popup.content': 'Are you sure you want to delete this agent?',
'tag.default': 'Default',
'tag.system': 'System',
'tag.user': 'Mine'
},
provider: {
openai: 'OpenAI',
@@ -185,6 +204,12 @@ const resources = {
italian: 'Italian',
portuguese: 'Portuguese',
arabic: 'Arabic'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': 'Keep Alive Time',
'keep_alive_time.placeholder': 'Minutes',
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
}
}
},
@@ -210,7 +235,9 @@ const resources = {
regenerate: '重新生成',
provider: '提供商',
you: '用户',
footnote: '引用内容'
footnote: '引用内容',
select: '选择',
search: '搜索'
},
button: {
add: '添加',
@@ -233,9 +260,7 @@ const resources = {
'switch.disabled': '模型回复完成后才能切换'
},
chat: {
save: '保存'
},
assistant: {
save: '保存',
'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
@@ -245,7 +270,7 @@ const resources = {
'topics.edit.placeholder': '输入新名称',
'topics.delete.all.title': '删除所有话题',
'topics.delete.all.content': '确定要删除所有话题吗?',
'input.new_chat': ' 新聊天 ',
'input.new_topic': '新话题',
'input.topics': ' 话题 ',
'input.clear': '清除',
'input.expand': '展开',
@@ -256,19 +281,38 @@ const resources = {
'input.send': '发送',
'input.pause': '暂停',
'input.settings': '设置',
'input.context_count.tip': '上下文数',
'input.estimated_tokens.tip': '预估 token 数',
'settings.temperature': '模型温度',
'settings.temperature.tip':
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
'settings.conext_count': '上下文数',
'settings.conext_count.tip':
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10',
'settings.max_tokens': '开启消息长度限制',
'settings.max_tokens.tip':
'单次交互所用的最大 Token 数, 会影响返回结果的长度。普通聊天建议 500-800短文生成建议 800-2000代码生成建议 2000-3600长文生成建议切换模型到 4000 左右',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限',
'suggestions.title': '建议的问题'
'suggestions.title': '建议的问题',
'add.assistant.title': '添加智能体'
},
apps: {
title: '智能体'
agents: {
title: '智能体',
my_agents: '我的智能体',
'add.title': '添加智能体',
'edit.title': '编辑智能体',
'add.name': '名称',
'add.name.placeholder': '输入名称',
'add.prompt': '提示词',
'add.prompt.placeholder': '输入提示词',
'add.button': '添加',
'manage.title': '管理智能体',
'delete.popup.content': '确定要删除此智能体吗?',
'tag.default': '默认',
'tag.system': '系统',
'tag.user': '我的'
},
provider: {
openai: 'OpenAI',
@@ -371,6 +415,12 @@ const resources = {
italian: '意大利文',
portuguese: '葡萄牙文',
arabic: '阿拉伯文'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': '保持活跃时间',
'keep_alive_time.placeholder': '分钟',
'keep_alive_time.description': '对话后模型在内存中保持的时间默认5分钟'
}
}
}

View File

@@ -0,0 +1,120 @@
import { UnorderedListOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import Agents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { Col, Row, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AgentCard from './components/AgentCard'
import ManageAgentsPopup from './components/ManageAgentsPopup'
import UserAgents from './components/UserAgents'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const { agents } = useAgents()
const agentGroups = groupBy(Agents, 'group')
const { t } = useTranslation()
const onAddAgentConfirm = (agent: Agent) => {
const added = find(assistants, { id: agent.id })
window.modal.confirm({
title: agent.emoji + ' ' + agent.name,
content: agent.description || agent.prompt,
icon: null,
closable: true,
maskClosable: true,
okButtonProps: { type: 'primary', disabled: Boolean(added) },
okText: added ? t('button.added') : t('button.add'),
onOk: () => onAddAgent(agent)
})
}
const onAddAgent = (agent: Agent) => {
addAssistant(covertAgentToAssistant(agent))
window.message.success({
content: t('message.assistant.added.content'),
key: 'agent-added',
style: { marginTop: '5vh' }
})
}
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<AssistantsContainer>
<HStack alignItems="center" style={{ marginBottom: 16 }}>
<Title level={3}>{t('agents.my_agents')}</Title>
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
</HStack>
<UserAgents onAdd={onAddAgentConfirm} />
{Object.keys(agentGroups).map((group) => (
<div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</AssistantsContainer>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: scroll;
background-color: var(--color-background);
`
const AssistantsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
padding: 20px;
max-width: 1000px;
`
const ManageIcon = styled(UnorderedListOutlined)`
font-size: 18px;
color: var(--color-icon);
cursor: pointer;
margin-bottom: 0.5em;
margin-left: 0.5em;
`
export default AppsPage

View File

@@ -0,0 +1,137 @@
import 'emoji-picker-element'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { syncAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
agent?: Agent
resolve: (data: Agent | null) => void
}
type FieldType = {
id: string
name: string
prompt: string
}
const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { t } = useTranslation()
const { addAgent, updateAgent } = useAgents()
const formRef = useRef<FormInstance>(null)
const [emoji, setEmoji] = useState(agent?.emoji)
const onFinish = (values: FieldType) => {
const _emoji = emoji || getLeadingEmoji(values.name)
if (values.name.trim() === '' || values.prompt.trim() === '') {
return
}
if (agent) {
const _agent = {
...agent,
name: values.name,
emoji: _emoji,
prompt: values.prompt
}
updateAgent(_agent)
syncAgentToAssistant(_agent)
resolve(_agent)
setOpen(false)
return
}
const _agent = {
id: uuid(),
name: values.name,
emoji: _emoji,
prompt: values.prompt,
group: 'user'
}
addAgent(_agent)
resolve(_agent)
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(null)
}
useEffect(() => {
if (agent) {
form.setFieldsValue({
name: agent.name,
prompt: agent.prompt
})
}
}, [agent, form])
return (
<Modal
style={{ marginTop: '10vh' }}
title={agent ? t('agents.edit.title') : t('agents.add.title')}
open={open}
onOk={() => formRef.current?.submit()}
onCancel={onCancel}
maskClosable={false}
afterClose={onClose}
okText={agent ? t('common.save') : t('agents.add.button')}>
<Form
ref={formRef}
form={form}
labelCol={{ flex: '80px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 25 }}
onFinish={onFinish}>
<Form.Item name="name" label="Emoji">
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} trigger="click" arrow>
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
</Popover>
</Form.Item>
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
</Form.Item>
<Form.Item name="prompt" label={t('agents.add.prompt')} rules={[{ required: true }]}>
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={4} />
</Form.Item>
</Form>
</Modal>
)
}
export default class AddAgentPopup {
static topviewId = 0
static hide() {
TopView.hide('AddAgentPopup')
}
static show(agent?: Agent) {
return new Promise<Agent | null>((resolve) => {
TopView.show(
<PopupContainer
agent={agent}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AddAgentPopup'
)
})
}
}

View File

@@ -0,0 +1,81 @@
import { Agent } from '@renderer/types'
import { Col, Typography } from 'antd'
import styled from 'styled-components'
interface Props {
agent: Agent
onClick?: () => void
}
const { Title } = Typography
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
return (
<Container onClick={onClick}>
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
<Col>
<AgentHeader>
<AgentName level={5} style={{ marginBottom: 0 }}>
{agent.name}
</AgentName>
</AgentHeader>
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
</Col>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 16px;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
border-radius: 10px;
padding: 15px;
position: relative;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--color-background-mute);
}
`
const EmojiHeader = styled.div`
width: 25px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-right: 5px;
font-size: 25px;
line-height: 25px;
`
const AgentHeader = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const AgentName = styled(Title)`
font-size: 18px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-white);
font-weight: 900;
`
const AgentCardPrompt = styled.div`
color: #666;
margin-top: 6px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default AgentCard

View File

@@ -0,0 +1,109 @@
import { DeleteOutlined, EditOutlined, MenuOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import { Box, HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { Empty, Modal, Popconfirm } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AddAgentPopup from './AddAgentPopup'
const PopupContainer: React.FC = () => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
ManageAgentsPopup.hide()
}
useEffect(() => {
if (agents.length === 0) {
setOpen(false)
}
}, [agents])
return (
<Modal
style={{ marginTop: '10vh' }}
title={t('agents.manage.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
footer={null}>
<Container>
{agents.length > 0 && (
<DragableList list={agents} onUpdate={updateAgents}>
{(item) => (
<AgentItem>
<Box mr={8}>
{item.emoji} {item.name}
</Box>
<HStack gap="15px">
<Popconfirm
title={t('agents.delete.popup.content')}
okButtonProps={{ danger: true }}
onConfirm={() => removeAgent(item)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => AddAgentPopup.show(item)} />
<MenuOutlined style={{ cursor: 'move' }} />
</HStack>
</AgentItem>
)}
</DragableList>
)}
{agents.length === 0 && <Empty description="" />}
</Container>
</Modal>
)
}
const Container = styled.div`
padding: 12px 0;
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class ManageAgentsPopup {
static topviewId = 0
static hide() {
TopView.hide('ManageAgentsPopup')
}
static show() {
TopView.show(<PopupContainer />, 'ManageAgentsPopup')
}
}

View File

@@ -0,0 +1,57 @@
import { PlusOutlined } from '@ant-design/icons'
import { useAgents } from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Col, Row } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import AddAssistantPopup from './AddAgentPopup'
import AgentCard from './AgentCard'
interface Props {
onAdd: (agent: Agent) => void
}
const UserAgents: FC<Props> = ({ onAdd }) => {
const { agents } = useAgents()
const onAddMyAgentClick = () => {
AddAssistantPopup.show()
}
return (
<Row gutter={16} style={{ marginBottom: 16 }}>
{agents.map((agent) => (
<Col span={8} key={agent.id}>
<AgentCard agent={agent} onClick={() => onAdd(agent)} />
</Col>
))}
<Col span={8}>
<AssistantCardContainer style={{ borderStyle: 'dashed' }} onClick={onAddMyAgentClick}>
<PlusOutlined />
</AssistantCardContainer>
</Col>
</Row>
)
}
const AssistantCardContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px dashed var(--color-border);
border-radius: 10px;
cursor: pointer;
min-height: 84px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
}
`
export default UserAgents

View File

@@ -1,169 +0,0 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { SystemAssistant } from '@renderer/types'
import { Col, Row, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const assistantGroups = groupBy(
SYSTEM_ASSISTANTS.map((a) => ({ ...a, id: String(a.id) })),
'group'
)
const { t } = useTranslation()
const onAddAssistantConfirm = (assistant: SystemAssistant) => {
const added = find(assistants, { id: assistant.id })
window.modal.confirm({
title: assistant.name,
content: assistant.description || assistant.prompt,
icon: null,
closable: true,
maskClosable: true,
okButtonProps: { type: 'primary', disabled: Boolean(added) },
okText: added ? t('button.added') : t('button.add'),
onOk: () => onAddAssistant(assistant)
})
}
const onAddAssistant = (assistant: SystemAssistant) => {
addAssistant({
...getDefaultAssistant(),
...assistant,
id: String(assistant.id)
})
window.message.success({
content: t('message.assistant.added.content'),
key: 'assistant-added',
style: { marginTop: '5vh' }
})
}
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('apps.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<AssistantsContainer>
{Object.keys(assistantGroups).map((group) => (
<div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{assistantGroups[group].map((assistant, index) => {
return (
<Col span={8} key={group + index}>
<AssistantCard onClick={() => onAddAssistantConfirm(assistant)}>
<EmojiHeader>{assistant.emoji}</EmojiHeader>
<Col>
<AssistantHeader>
<AssistantName level={5} style={{ marginBottom: 0 }}>
{assistant.name.replace(assistant.emoji + ' ', '')}
</AssistantName>
</AssistantHeader>
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
</Col>
</AssistantCard>
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</AssistantsContainer>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: scroll;
background-color: var(--color-background);
`
const AssistantsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
padding: 20px;
max-width: 1000px;
`
const AssistantCard = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 16px;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
border-radius: 10px;
padding: 15px;
position: relative;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--color-background-mute);
}
`
const EmojiHeader = styled.div`
width: 25px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-right: 5px;
font-size: 25px;
line-height: 25px;
`
const AssistantHeader = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const AssistantName = styled(Title)`
font-size: 18px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-white);
font-weight: 900;
`
const AssistantCardPrompt = styled.div`
color: #666;
margin-top: 6px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default AppsPage

View File

@@ -9,6 +9,7 @@ import { Switch } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import AddAssistantPopup from './components/AddAssistantPopup'
import Assistants from './components/Assistants'
import Chat from './components/Chat'
import Navigation from './components/NavigationCenter'
@@ -25,12 +26,17 @@ const HomePage: FC = () => {
_activeAssistant = activeAssistant
const onCreateAssistant = () => {
const onCreateDefaultAssistant = () => {
const assistant = { ...defaultAssistant, id: uuid() }
addAssistant(assistant)
setActiveAssistant(assistant)
}
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
assistant && setActiveAssistant(assistant)
}
return (
<Container>
<Navbar>
@@ -62,7 +68,7 @@ const HomePage: FC = () => {
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
onCreateAssistant={onCreateDefaultAssistant}
/>
)}
<Chat assistant={activeAssistant} />

View File

@@ -0,0 +1,129 @@
import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent, Assistant } from '@renderer/types'
import { Input, Modal, Tag } from 'antd'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
resolve: (value: Assistant | undefined) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents: userAgents } = useAgents()
const [searchText, setSearchText] = useState('')
const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants()
const defaultAgent: Agent = useMemo(
() => ({
id: defaultAssistant.id,
name: defaultAssistant.name,
emoji: '',
prompt: defaultAssistant.prompt,
group: 'system'
}),
[defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
)
const agents = useMemo(() => {
const allAgents = [defaultAgent, ...userAgents, ...systemAgents] as Agent[]
const list = allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))
return searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list
}, [assistants, defaultAgent, searchText, userAgents])
const onCreateAssistant = (agent: Agent) => {
if (assistants.map((a) => a.id).includes(String(agent.id))) return
const assistant = covertAgentToAssistant(agent)
addAssistant(assistant)
resolve(assistant)
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
resolve(undefined)
AddAssistantPopup.hide()
}
return (
<Modal
style={{ marginTop: '5vh' }}
title={t('chat.add.assistant.title')}
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName=""
maskTransitionName=""
footer={null}>
<Input
placeholder={t('common.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
autoFocus
style={{ marginBottom: 16 }}
/>
<Container>
{agents.map((agent) => (
<AgentItem key={agent.id} onClick={() => onCreateAssistant(agent)}>
{agent.emoji} {agent.name}
{agent.group === 'system' && <Tag color="orange">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="green">{t('agents.tag.user')}</Tag>}
</AgentItem>
))}
</Container>
</Modal>
)
}
const Container = styled.div`
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
cursor: pointer;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class AddAssistantPopup {
static topviewId = 0
static hide() {
TopView.hide('AddAssistantPopup')
}
static show() {
return new Promise<Assistant | undefined>((resolve) => {
TopView.show(<PopupContainer resolve={resolve} />, 'AddAssistantPopup')
})
}
}

View File

@@ -2,7 +2,7 @@ import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types'
@@ -45,6 +45,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
async onClick() {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
},
{
@@ -113,9 +114,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
<AssistantItem
onClick={() => onSwitchAssistant(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<AssistantName className="name">
{assistant.name || t('assistant.default.name')}
</AssistantName>
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
</AssistantItem>
</Dropdown>
</div>
@@ -148,7 +147,7 @@ const AssistantItem = styled.div`
position: relative;
border-radius: 8px;
cursor: pointer;
font-family: Poppins;
font-family: Ubuntu;
.anticon {
display: none;
}

View File

@@ -8,13 +8,14 @@ import {
SaveOutlined,
SyncOutlined
} from '@ant-design/icons'
import { FONT_FAMILY } from '@renderer/config/constant'
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types'
import { Message, Model } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
@@ -24,6 +25,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Markdown from './markdown/Markdown'
import SelectModelDropdown from './SelectModelDropdown'
interface Props {
message: Message
@@ -36,7 +38,7 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const { assistant } = useAssistant(message.assistantId)
const { assistant, model, setModel } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings()
const { generating } = useRuntime()
const [copied, setCopied] = useState(false)
@@ -55,10 +57,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
const onRegenerate = useCallback(() => {
onDeleteMessage?.(message)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
}, [message, onDeleteMessage])
const onRegenerate = useCallback(
(model: Model) => {
setModel(model)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
},
[setModel]
)
const getUserName = useCallback(() => {
if (message.id === 'assistant') return assistant?.name
@@ -66,8 +71,10 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
return userName || t('common.you')
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
const serifFonts = "Georgia, Cambria, 'Times New Roman', Times, serif"
const fontFamily = messageFont === 'serif' ? serifFonts : 'Poppins, -apple-system, sans-serif'
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
}, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none'
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
@@ -132,6 +139,15 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
{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>
</Tooltip>
</SelectModelDropdown>
)}
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
@@ -143,13 +159,6 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</ActionButton>
</Tooltip>
</Popconfirm>
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton onClick={onRegenerate}>
<SyncOutlined />
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
<ActionButton>

View File

@@ -2,12 +2,13 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateHistoryTokenCount, filterAtMessages } from '@renderer/services/messages'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import { Assistant, Message, Model, Topic } from '@renderer/types'
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next'
import localforage from 'localforage'
import { debounce, reverse } from 'lodash'
import { debounce, last, reverse } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
@@ -30,7 +31,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
() => ({
id: 'assistant',
role: 'assistant',
content: assistant.description || assistant.prompt || t('assistant.default.description'),
content: assistant.description || assistant.prompt || t('chat.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
@@ -49,7 +50,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
)
const autoRenameTopic = useCallback(async () => {
if (topic.name === t('assistant.default.topic.name') && messages.length >= 2) {
if (topic.name === t('chat.default.topic.name') && messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText })
}
@@ -75,8 +76,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
onSendMessage(msg)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
}),
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async () => {
fetchChatCompletion({ assistant, messages: messages, topic, onResponse: setLastMessage })
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
const lastUserMessage = last(filterAtMessages(messages).filter((m) => m.role === 'user'))
if (lastUserMessage) {
const content = `[@${model.name}](#) ${getBriefInfo(lastUserMessage.content)}`
onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', content })
fetchChatCompletion({
assistant,
topic,
messages: [...messages, lastUserMessage],
onResponse: setLastMessage
})
}
}),
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
@@ -89,14 +100,14 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
useEffect(() => {
runAsyncFunction(async () => setMessages((await LocalStorage.getTopicMessages(topic.id)) || []))
runAsyncFunction(async () => {
const messages = (await LocalStorage.getTopicMessages(topic.id)) || []
setMessages(messages)
})
}, [topic.id])
const scrollTop = useCallback(
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
leading: true,
trailing: false
}),
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500),
[]
)

View File

@@ -1,19 +1,15 @@
import { CodeSandboxOutlined } from '@ant-design/icons'
import { NavbarCenter } from '@renderer/components/app/Navbar'
import { isMac } from '@renderer/config/constant'
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { Assistant } from '@renderer/types'
import { removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Button, Dropdown, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
import SelectModelButton from './SelectModelButton'
interface Props {
activeAssistant: Assistant
@@ -21,30 +17,9 @@ interface Props {
const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { model, setModel } = useAssistant(activeAssistant.id)
const { providers } = useProviders()
const { t } = useTranslation()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: p.models.map((m) => ({
key: m?.id,
label: upperFirst(m?.name),
style: m?.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => m && setModel(m)
}))
}))
return (
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
{!showAssistants && (
@@ -52,39 +27,17 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
<i className="iconfont icon-showsidebarhoriz" />
</NewButton>
)}
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('assistant.default.name')}</AssistantName>
<DropdownMenu
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
trigger={['click']}
overlayClassName="chat-nav-dropdown">
<DropdownButton size="small" type="primary" ghost>
<CodeSandboxOutlined />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton>
</DropdownMenu>
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('chat.default.name')}</AssistantName>
<SelectModelButton assistant={assistant} />
</NavbarCenter>
)
}
const DropdownMenu = styled(Dropdown)`
-webkit-app-region: none;
margin-left: 10px;
`
const AssistantName = styled.span`
font-weight: bold;
margin-left: 5px;
`
const DropdownButton = styled(Button)`
font-size: 11px;
border-radius: 15px;
padding: 0 8px;
`
const ModelName = styled.span`
margin-left: -2px;
font-weight: bolder;
margin-right: 10px;
font-family: Ubuntu;
`
export default NavigationCenter

View File

@@ -0,0 +1,41 @@
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Avatar, Button } from 'antd'
import { upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SelectModelDropdown from './SelectModelDropdown'
interface Props {
assistant: Assistant
}
const SelectModelButton: FC<Props> = ({ assistant }) => {
const { model, setModel } = useAssistant(assistant.id)
const { t } = useTranslation()
return (
<SelectModelDropdown model={model} onSelect={setModel}>
<DropdownButton size="small" type="default">
<Avatar src={getModelLogo(model?.id || '')} style={{ width: 20, height: 20 }} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton>
</SelectModelDropdown>
)
}
const DropdownButton = styled(Button)`
font-size: 11px;
border-radius: 15px;
padding: 12px 8px 12px 3px;
`
const ModelName = styled.span`
margin-left: -2px;
font-weight: bolder;
`
export default SelectModelButton

View File

@@ -0,0 +1,55 @@
import { getModelLogo } from '@renderer/config/provider'
import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types'
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props extends DropdownProps {
model: Model
onSelect: (model: Model) => void
}
const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, onSelect, ...props }) => {
const { t } = useTranslation()
const { providers } = useProviders()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: p.models.map((m) => ({
key: m?.id,
label: upperFirst(m?.name),
style: m?.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => m && onSelect(m)
}))
}))
return (
<DropdownMenu
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
trigger={['click']}
arrow
placement="bottom"
overlayClassName="chat-nav-dropdown"
{...props}>
{children}
</DropdownMenu>
)
}
const DropdownMenu = styled(Dropdown)`
-webkit-app-region: none;
`
export default SelectModelDropdown

View File

@@ -13,11 +13,12 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateInputTokenCount } from '@renderer/services/messages'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateInputTokenCount, uuid } from '@renderer/utils'
import { Button, Popconfirm, Tooltip } from 'antd'
import { uuid } from '@renderer/utils'
import { Button, Popconfirm, Tag, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash'
@@ -32,8 +33,10 @@ interface Props {
setActiveTopic: (topic: Topic) => void
}
let _text = ''
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState('')
const [text, setText] = useState(_text)
const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false)
@@ -42,6 +45,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
_text = text
const sendMessage = useCallback(() => {
if (generating) {
return
@@ -143,49 +148,53 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
<Toolbar onDoubleClick={() => setExpend(!expended)}>
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<PlusCircleOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
<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>
<Popconfirm
title={t('assistant.input.clear.content')}
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('assistant.input.clear')}>
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.settings')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
{showInputEstimatedTokens && (
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
{`${inputTokenCount}/${estimateTokenCount}`}
<Tooltip title={t('chat.input.context_count.tip')}>
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag>
</Tooltip>
<Tooltip title={t('chat.input.estimated_tokens.tip')}>
<Tag style={{ cursor: 'pointer' }}> {`${inputTokenCount} / ${estimateTokenCount}`}</Tag>
</Tooltip>
</TextCount>
)}
</ToolbarMenu>
<ToolbarMenu>
{generating && (
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}>
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
</ToolbarButton>
@@ -198,7 +207,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('assistant.input.placeholder')}
placeholder={t('chat.input.placeholder')}
autoFocus
contextMenu="true"
variant="borderless"

View File

@@ -15,13 +15,13 @@ const SendMessageButton: FC<Props> = ({ sendMessage }) => {
const sendSettingItems: MenuProps['items'] = [
{
label: `Enter ${t('assistant.input.send')}`,
label: `Enter ${t('chat.input.send')}`,
key: 'Enter',
icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter')
},
{
label: `Shift+Enter ${t('assistant.input.send')}`,
label: `Shift+Enter ${t('chat.input.send')}`,
key: 'Shift+Enter',
icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter')
@@ -36,7 +36,7 @@ const SendMessageButton: FC<Props> = ({ sendMessage }) => {
arrow
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
style={{ width: 'auto' }}>
{t('assistant.input.send')}
{t('chat.input.send')}
<SendOutlined />
</Dropdown.Button>
)

View File

@@ -1,7 +1,11 @@
import { omit } from 'lodash'
import React from 'react'
const Link: React.FC = (props) => {
const Link: React.FC = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (props.href?.startsWith('#')) {
return <span className="link">{props.children}</span>
}
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
}

View File

@@ -1,8 +1,9 @@
import 'katex/dist/katex.min.css'
import { Message } from '@renderer/types'
import { convertMathFormula } from '@renderer/utils'
import { isEmpty } from 'lodash'
import { FC, useCallback, useMemo } from 'react'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
@@ -19,31 +20,29 @@ interface Props {
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const getMessageContent = useCallback(
(message: Message) => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
return empty && paused ? t('message.chat.completion.paused') : message.content
},
[t]
)
const messageContent = useMemo(() => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : message.content
return convertMathFormula(content)
}, [message.content, message.status, t])
return useMemo(() => {
return (
<ReactMarkdown
className="markdown"
rehypePlugins={[rehypeKatex]}
remarkPlugins={[[remarkMath, { singleDollarTextMath: false }], remarkGfm]}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',
footnoteBackContent: ' '
}}
rehypePlugins={[rehypeKatex]}
components={{ code: CodeBlock as any, a: Link as any }}>
{getMessageContent(message)}
{messageContent}
</ReactMarkdown>
)
}, [getMessageContent, message, t])
}, [messageContent, t])
}
export default Markdown

View File

@@ -1,11 +1,12 @@
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
import { Assistant } from '@renderer/types'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { FC, useCallback, useEffect, useState } from 'react'
@@ -20,6 +21,8 @@ const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const { t } = useTranslation()
const dispatch = useAppDispatch()
@@ -28,33 +31,38 @@ const SettingsTab: FC<Props> = (props) => {
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
(settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
...assistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens
})
},
1000,
{
leading: false,
trailing: true
}
{ leading: true, trailing: false }
),
[]
[temperature, contextCount, enableMaxTokens, maxTokens]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
onUpdateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
setMaxTokens(value)
onUpdateAssistantSettings({ maxTokens: value })
}
}
@@ -66,7 +74,9 @@ const SettingsTab: FC<Props> = (props) => {
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS
}
})
}
@@ -74,20 +84,22 @@ const SettingsTab: FC<Props> = (props) => {
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
}, [assistant])
return (
<Container>
<SettingSubtitle>
{t('settings.messages.model.title')}{' '}
<Tooltip title={t('assistant.settings.reset')}>
<Tooltip title={t('chat.settings.reset')}>
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
</Tooltip>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
@@ -110,12 +122,13 @@ const SettingsTab: FC<Props> = (props) => {
value={temperature}
onChange={onTemperatureChange}
controls={false}
size="small"
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
@@ -124,7 +137,7 @@ const SettingsTab: FC<Props> = (props) => {
<Slider
min={0}
max={20}
marks={{ 0: '0', 10: '10', 20: t('assistant.settings.max') }}
marks={{ 0: '0', 10: '10', 20: t('chat.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
@@ -138,9 +151,51 @@ const SettingsTab: FC<Props> = (props) => {
value={contextCount}
onChange={onConextCountChange}
controls={false}
size="small"
/>
</Col>
</Row>
<Row align="middle" justify="space-between" style={{ marginBottom: 8 }}>
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
size="small"
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
{enableMaxTokens && (
<Row align="middle" gutter={10}>
<Col span={16}>
<Slider
min={0}
max={32000}
onChange={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
<Col span={8}>
<InputNumberic
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>
<SettingDivider />
<SettingRow>
@@ -196,7 +251,7 @@ const InputNumberic = styled(InputNumber)`
const Label = styled.p`
margin: 0;
font-size: 12px;
font-weight: bold;
font-weight: 600;
margin-right: 8px;
`

View File

@@ -27,7 +27,7 @@ const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTop
(topic: Topic) => {
const menus: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
label: t('chat.topics.auto_rename'),
key: 'auto-rename',
icon: <OpenAIOutlined />,
async onClick() {
@@ -41,12 +41,12 @@ const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTop
}
},
{
label: t('assistant.topics.edit.title'),
label: t('chat.topics.edit.title'),
key: 'rename',
icon: <EditOutlined />,
async onClick() {
const name = await PromptPopup.show({
title: t('assistant.topics.edit.title'),
title: t('chat.topics.edit.title'),
message: '',
defaultValue: topic?.name || ''
})
@@ -147,7 +147,8 @@ const TopicListItem = styled.div`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: Poppins;
font-family: Ubuntu;
transition: all 0.3s;
&:hover {
background-color: var(--color-background-soft);
}

View File

@@ -20,7 +20,13 @@ const AboutSettings: FC = () => {
async () => {
if (checkUpdateLoading || downloading) return
setCheckUpdateLoading(true)
await window.api.checkForUpdate()
try {
await window.api.checkForUpdate()
} catch (error) {
setCheckUpdateLoading(false)
}
setCheckUpdateLoading(false)
},
2000,

View File

@@ -1,7 +1,9 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
import { AssistantSettings as AssistantSettingsType } from '@renderer/types'
import { Button, Col, Input, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { debounce } from 'lodash'
import { FC, useCallback, useState } from 'react'
@@ -14,38 +16,49 @@ const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0)
const { t } = useTranslation()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
(settings: Partial<AssistantSettingsType>) => {
updateDefaultAssistant({
...defaultAssistant,
settings: {
...defaultAssistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens
}
})
},
1000,
{ leading: false, trailing: true }
),
[]
[temperature, contextCount, enableMaxTokens, maxTokens]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
onUpdateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
setMaxTokens(value)
onUpdateAssistantSettings({ maxTokens: value })
}
}
@@ -57,7 +70,9 @@ const AssistantSettings: FC = () => {
settings: {
...defaultAssistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS
}
})
}
@@ -80,10 +95,22 @@ const AssistantSettings: FC = () => {
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
/>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.assistant.model_params')}</SettingSubtitle>
<SettingSubtitle
style={{
marginTop: 0,
marginBottom: 20,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
}}>
<span>{t('settings.assistant.model_params')}</span>
<Button onClick={onReset} style={{ width: 90 }}>
{t('chat.settings.reset')}
</Button>
</SettingSubtitle>
<Row align="middle">
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
@@ -110,8 +137,8 @@ const AssistantSettings: FC = () => {
</Col>
</Row>
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
@@ -120,7 +147,7 @@ const AssistantSettings: FC = () => {
<Slider
min={0}
max={20}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('chat.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
@@ -137,9 +164,46 @@ const AssistantSettings: FC = () => {
/>
</Col>
</Row>
<Button onClick={onReset} style={{ width: 100 }}>
{t('assistant.settings.reset')}
</Button>
<Row align="middle">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
style={{ marginLeft: 10 }}
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
{enableMaxTokens && (
<Row align="middle" gutter={20}>
<Col span={22}>
<Slider
min={0}
max={32000}
onChange={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
<Col span={2}>
<InputNumber
min={0}
max={32000}
step={100}
value={maxTokens}
onChange={onMaxTokensChange}
controls={true}
style={{ width: '100%' }}
/>
</Col>
</Row>
)}
</SettingContainer>
)
}

View File

@@ -1,5 +1,6 @@
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
@@ -23,6 +24,7 @@ const GeneralSettings: FC = () => {
const onSelectLanguage = (value: string) => {
dispatch(setLanguage(value))
localStorage.setItem('language', value)
i18n.changeLanguage(value)
}
const onSetProxyUrl = () => {

View File

@@ -192,6 +192,7 @@ const ProviderItemName = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
font-family: Ubuntu;
`
const AddButtonWrapper = styled.div`

View File

@@ -115,18 +115,19 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
export default class AddModelPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('AddModelPopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'AddModelPopup'
)
})
}

View File

@@ -54,18 +54,19 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
export default class AddProviderPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('AddProviderPopup')
}
static show(provider?: Provider) {
return new Promise<string>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
provider={provider}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'AddProviderPopup'
)
})
}

View File

@@ -224,18 +224,19 @@ const Question = styled(QuestionCircleOutlined)`
export default class EditModelsPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('EditModelsPopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'EditModelsPopup'
)
})
}

View File

@@ -19,6 +19,7 @@ import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import OllamSettings from '../providers/OllamaSettings'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup'
@@ -126,6 +127,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
/>
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
</Space.Compact>
{provider.id === 'ollama' && <OllamSettings />}
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
@@ -182,14 +184,14 @@ const ModelListHeader = styled.div`
align-items: center;
`
const HelpTextRow = styled.div`
export const HelpTextRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 0;
`
const HelpText = styled.div`
export const HelpText = styled.div`
font-size: 11px;
color: var(--color-text);
opacity: 0.4;

View File

@@ -8,6 +8,7 @@ export const SettingContainer = styled.div`
height: calc(100vh - var(--navbar-height));
padding: 15px;
overflow-y: scroll;
font-family: Ubuntu;
&::-webkit-scrollbar {
display: none;

View File

@@ -0,0 +1,35 @@
import { useOllamaSettings } from '@renderer/hooks/useOllama'
import { InputNumber } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingSubtitle } from '../components'
import { HelpText, HelpTextRow } from '../components/ProviderSetting'
const OllamSettings: FC = () => {
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime)
const { t } = useTranslation()
return (
<Container>
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
<InputNumber
style={{ width: '100%' }}
value={keepAliveMinutes}
onChange={(e) => setKeepAliveMinutes(Number(e))}
onBlur={() => setKeepAliveTime(keepAliveMinutes)}
suffix={t('ollama.keep_alive_time.placeholder')}
step={5}
/>
<HelpTextRow>
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText>
</HelpTextRow>
</Container>
)
}
const Container = styled.div``
export default OllamSettings

View File

@@ -1,12 +1,14 @@
import Anthropic from '@anthropic-ai/sdk'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
import { removeQuotes } from '@renderer/utils'
import { sum, takeRight } from 'lodash'
import OpenAI from 'openai'
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
import { getDefaultModel, getTopNamingModel } from './assistant'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from './assistant'
import { EVENT_NAMES } from './event'
export default class ProviderSDK {
@@ -26,6 +28,10 @@ export default class ProviderSDK {
return this.provider.id === 'anthropic'
}
private get keepAliveTime() {
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
}
public async completions(
messages: Message[],
assistant: Assistant,
@@ -33,7 +39,7 @@ export default class ProviderSDK {
) {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const { contextCount } = getAssistantSettings(assistant)
const { contextCount, maxTokens } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
@@ -47,7 +53,7 @@ export default class ProviderSDK {
.stream({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
max_tokens: 4096,
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
temperature: assistant?.settings?.temperature
})
.on('text', (text) => onChunk({ text: text || '' }))
@@ -61,11 +67,14 @@ export default class ProviderSDK {
})
)
} else {
// @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
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
@@ -92,10 +101,12 @@ export default class ProviderSDK {
})
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
stream: false,
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
}
@@ -124,11 +135,13 @@ export default class ProviderSDK {
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({
model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
stream: false,
max_tokens: 50
max_tokens: 50,
keep_alive: this.keepAliveTime
})
return removeQuotes(response.choices[0].message?.content || '')

View File

@@ -14,6 +14,7 @@ import {
getTranslateModel
} from './assistant'
import { EVENT_NAMES, EventEmitter } from './event'
import { filterAtMessages } from './messages'
import ProviderSDK from './ProviderSDK'
export async function fetchChatCompletion({
@@ -50,7 +51,7 @@ export async function fetchChatCompletion({
onResponse({ ...message })
try {
await providerSdk.completions(messages, assistant, ({ text, usage }) => {
await providerSdk.completions(filterAtMessages(messages), assistant, ({ text, usage }) => {
message.content = message.content + text || ''
message.usage = usage
onResponse({ ...message, status: 'pending' })

View File

@@ -1,12 +1,15 @@
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { Assistant, Model, Provider, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { updateAgent } from '@renderer/store/agents'
import { updateAssistant } from '@renderer/store/assistants'
import { Agent, Assistant, AssistantSettings, Model, Provider, Topic } from '@renderer/types'
import { getLeadingEmoji, removeLeadingEmoji, uuid } from '@renderer/utils'
export function getDefaultAssistant(): Assistant {
return {
id: 'default',
name: i18n.t('assistant.default.name'),
name: i18n.t('chat.default.name'),
prompt: '',
topics: [getDefaultTopic()]
}
@@ -15,7 +18,7 @@ export function getDefaultAssistant(): Assistant {
export function getDefaultTopic(): Topic {
return {
id: uuid(),
name: i18n.t('assistant.default.topic.name'),
name: i18n.t('chat.default.topic.name'),
messages: []
}
}
@@ -53,3 +56,68 @@ export function getProviderByModelId(modelId?: string) {
const _modelId = modelId || getDefaultModel().id
return providers.find((p) => p.models.find((m) => m.id === _modelId)) as Provider
}
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
const getAssistantMaxTokens = () => {
if (assistant.settings?.enableMaxTokens) {
const maxTokens = assistant.settings.maxTokens
if (typeof maxTokens === 'number') {
return maxTokens > 100 ? maxTokens : DEFAULT_MAX_TOKENS
}
return DEFAULT_MAX_TOKENS
}
return undefined
}
return {
contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE,
enableMaxTokens: assistant?.settings?.enableMaxTokens ?? false,
maxTokens: getAssistantMaxTokens()
}
}
export function covertAgentToAssistant(agent: Agent): Assistant {
return {
...getDefaultAssistant(),
...agent,
name: getAssistantNameWithAgent(agent),
id: agent.group === 'system' ? uuid() : String(agent.id)
}
}
export function getAssistantNameWithAgent(agent: Agent) {
return agent.emoji ? agent.emoji + ' ' + agent.name : agent.name
}
export function syncAsistantToAgent(assistant: Assistant) {
const agents = store.getState().agents.agents
const agent = agents.find((a) => a.id === assistant.id)
if (agent) {
store.dispatch(
updateAgent({
...agent,
emoji: getLeadingEmoji(assistant.name),
name: removeLeadingEmoji(assistant.name),
prompt: assistant.prompt
})
)
}
}
export function syncAgentToAssistant(agent: Agent) {
const assistants = store.getState().assistants.assistants
const assistant = assistants.find((a) => a.id === agent.id)
if (assistant) {
store.dispatch(
updateAssistant({
...assistant,
name: getAssistantNameWithAgent(agent),
prompt: agent.prompt
})
)
}
}

View File

@@ -0,0 +1,35 @@
import { Assistant, Message } from '@renderer/types'
import { GPTTokens } from 'gpt-tokens'
import { takeRight } from 'lodash'
import { getAssistantSettings } from './assistant'
export const filterAtMessages = (messages: Message[]) => {
return messages.filter((message) => message.type !== '@')
}
export function estimateInputTokenCount(text: string) {
const input = new GPTTokens({
model: 'gpt-4o',
messages: [{ role: 'user', content: text }]
})
return input.usedTokens - 7
}
export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[]) {
const { contextCount } = getAssistantSettings(assistant)
const all = new GPTTokens({
model: 'gpt-4o',
messages: [
{ role: 'system', content: assistant.prompt },
...filterAtMessages(takeRight(msgs, contextCount)).map((message) => ({
role: message.role,
content: message.content
}))
]
})
return all.usedTokens - 7
}

View File

@@ -0,0 +1,33 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Agent } from '@renderer/types'
export interface AgentsState {
agents: Agent[]
}
const initialState: AgentsState = {
agents: []
}
const runtimeSlice = createSlice({
name: 'agents',
initialState,
reducers: {
addAgent: (state, action: PayloadAction<Agent>) => {
state.agents.push(action.payload)
},
removeAgent: (state, action: PayloadAction<Agent>) => {
state.agents = state.agents.filter((a) => a.id !== action.payload.id)
},
updateAgent: (state, action: PayloadAction<Agent>) => {
state.agents = state.agents.map((a) => (a.id === action.payload.id ? action.payload : a))
},
updateAgents: (state, action: PayloadAction<Agent[]>) => {
state.agents = action.payload
}
}
})
export const { addAgent, removeAgent, updateAgent, updateAgents } = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@@ -3,6 +3,7 @@ import { useDispatch, useSelector, useStore } from 'react-redux'
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import agents from './agents'
import assistants from './assistants'
import llm from './llm'
import migrate from './migrate'
@@ -13,6 +14,7 @@ const rootReducer = combineReducers({
assistants,
settings,
llm,
agents,
runtime
})
@@ -20,7 +22,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 17,
version: 19,
blacklist: ['runtime'],
migrate
},

View File

@@ -3,11 +3,18 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
import { Model, Provider } from '@renderer/types'
import { uniqBy } from 'lodash'
type LlmSettings = {
ollama: {
keepAliveTime: number
}
}
export interface LlmState {
providers: Provider[]
defaultModel: Model
topicNamingModel: Model
translateModel: Model
settings: LlmSettings
}
const initialState: LlmState = {
@@ -132,11 +139,16 @@ const initialState: LlmState = {
isSystem: true,
enabled: false
}
]
],
settings: {
ollama: {
keepAliveTime: 0
}
}
}
const settingsSlice = createSlice({
name: 'settings',
name: 'llm',
initialState,
reducers: {
updateProvider: (state, action: PayloadAction<Provider>) => {
@@ -179,6 +191,9 @@ const settingsSlice = createSlice({
},
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
state.translateModel = action.payload.model
},
setOllamaKeepAliveTime: (state, action: PayloadAction<number>) => {
state.settings.ollama.keepAliveTime = action.payload
}
}
})
@@ -192,7 +207,8 @@ export const {
removeModel,
setDefaultModel,
setTopicNamingModel,
setTranslateModel
setTranslateModel,
setOllamaKeepAliveTime
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -271,6 +271,22 @@ const migrateConfig = {
theme: 'auto'
}
}
},
'19': (state: RootState) => {
return {
...state,
agents: {
agents: []
},
llm: {
...state.llm,
settings: {
ollama: {
keepAliveTime: 5
}
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ const initialState: RuntimeState = {
}
const runtimeSlice = createSlice({
name: 'settings',
name: 'runtime',
initialState,
reducers: {
setAvatar: (state, action: PayloadAction<string | null>) => {

View File

@@ -3,9 +3,10 @@ import OpenAI from 'openai'
export type Assistant = {
id: string
name: string
description?: string
prompt: string
topics: Topic[]
emoji?: string
description?: string
model?: Model
settings?: AssistantSettings
}
@@ -13,6 +14,8 @@ export type Assistant = {
export type AssistantSettings = {
contextCount: number
temperature: number
maxTokens: number | undefined
enableMaxTokens: boolean
}
export type Message = {
@@ -25,6 +28,7 @@ export type Message = {
createdAt: string
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
usage?: OpenAI.Completions.CompletionUsage
type?: 'text' | '@'
}
export type Topic = {
@@ -59,7 +63,7 @@ export type Model = {
description?: string
}
export type SystemAssistant = {
export type Agent = {
id: string
name: string
emoji: string

View File

@@ -1,8 +1,5 @@
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
import { Model } from '@renderer/types'
import imageCompression from 'browser-image-compression'
import { GPTTokens } from 'gpt-tokens'
import { takeRight } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export const runAsyncFunction = async (fn: () => void) => {
@@ -100,7 +97,13 @@ export function firstLetter(str: string): string {
export function removeLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
return str.replace(emojiRegex, '')
return str.replace(emojiRegex, '').trim()
}
export function getLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
const match = str.match(emojiRegex)
return match ? match[0] : ''
}
export function isFreeModel(model: Model) {
@@ -173,37 +176,6 @@ export function getFirstCharacter(str) {
}
}
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
return {
contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE
}
}
export function estimateInputTokenCount(text: string) {
const input = new GPTTokens({
model: 'gpt-4o',
messages: [{ role: 'user', content: text }]
})
return input.usedTokens - 7
}
export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[]) {
const { contextCount } = getAssistantSettings(assistant)
const all = new GPTTokens({
model: 'gpt-4o',
messages: [
{ role: 'system', content: assistant.prompt },
...takeRight(msgs, contextCount).map((message) => ({ role: message.role, content: message.content }))
]
})
return all.usedTokens - 7
}
/**
* is valid proxy url
* @param url proxy url
@@ -225,3 +197,29 @@ export function loadScript(url: string) {
document.head.appendChild(script)
})
}
export function convertMathFormula(input) {
// 使用正则表达式匹配并替换公式格式
return input.replaceAll(/\\\[/g, '$$$$').replaceAll(/\\\]/g, '$$$$')
}
export function getBriefInfo(text: string, maxLength: number = 50): string {
// 去除空行
const noEmptyLinesText = text.replace(/\n\s*\n/g, '\n')
// 检查文本是否超过最大长度
if (noEmptyLinesText.length <= maxLength) {
return noEmptyLinesText
}
// 找到最近的单词边界
let truncatedText = noEmptyLinesText.slice(0, maxLength)
const lastSpaceIndex = truncatedText.lastIndexOf(' ')
if (lastSpaceIndex !== -1) {
truncatedText = truncatedText.slice(0, lastSpaceIndex)
}
// 截取前面的内容,并在末尾添加 "..."
return truncatedText + '...'
}

View File

@@ -3469,6 +3469,7 @@ __metadata:
electron-vite: "npm:^2.0.0"
electron-window-state: "npm:^5.0.3"
emittery: "npm:^1.0.3"
emoji-picker-element: "npm:^1.22.1"
eslint: "npm:^8.56.0"
eslint-plugin-react: "npm:^7.34.3"
eslint-plugin-react-hooks: "npm:^4.6.2"
@@ -4251,6 +4252,13 @@ __metadata:
languageName: node
linkType: hard
"emoji-picker-element@npm:^1.22.1":
version: 1.22.1
resolution: "emoji-picker-element@npm:1.22.1"
checksum: 10c0/3fbd6b5498796b4d46cc641a0276e934c683f2c0a63a00aef0082e7b2acc6711b4acab49c0cf38aaa12bf3bcd126aad1355afbf4580917f7aad5f2fb2ffa0d9c
languageName: node
linkType: hard
"emoji-regex@npm:^8.0.0":
version: 8.0.0
resolution: "emoji-regex@npm:8.0.0"