Compare commits

...

14 Commits

Author SHA1 Message Date
kangfenmao
e00c66e54a chore(version): 0.4.3 2024-07-31 13:08:19 +08:00
kangfenmao
62b0908dfa feat: add send message button 2024-07-31 13:07:02 +08:00
kangfenmao
cb0b9de1e9 feat: default enable new added provider 2024-07-31 12:21:46 +08:00
kangfenmao
d8d4afbc0d feat: add message suggestions 2024-07-31 12:13:03 +08:00
kangfenmao
c50ff4585a chore(version): 0.4.2 2024-07-30 17:53:45 +08:00
kangfenmao
a5ee8548f3 feat(AboutSettings): implement functionality to open license page from about settings 2024-07-30 16:33:58 +08:00
kangfenmao
15b286a095 doc: update LICENSE 2024-07-30 16:13:32 +08:00
kangfenmao
d47d4a158d docs: change offical website url 2024-07-30 15:31:17 +08:00
kangfenmao
cd85dcddf8 remove: website 2024-07-30 15:30:35 +08:00
kangfenmao
925a9fb8ec fix: delete provider crash 2024-07-30 15:30:09 +08:00
kangfenmao
17c3437e02 chore(version): 0.4.1 2024-07-29 18:18:03 +08:00
kangfenmao
69293846fc fix: model list text color 2024-07-29 18:17:50 +08:00
kangfenmao
20a7fbfc48 fix(ProviderSDK.ts): translation message 2024-07-29 17:45:08 +08:00
kangfenmao
64d4b8450a style(website): adjust border-radius of images to 20% 2024-07-29 17:36:27 +08:00
21 changed files with 402 additions and 287 deletions

114
LICENSE
View File

@@ -1,21 +1,101 @@
MIT License ### Cherry Studio 商业许可协议
Copyright (c) 2024 亢奋猫 ---
Permission is hereby granted, free of charge, to any person obtaining a copy #### 中文版
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all **Cherry Studio 商业许可协议**
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 本协议(以下简称“协议”)由以下双方签订:
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - 许可方王谦kangfenmao@qq.com
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - 被许可方:[被许可方名称]
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE **1. 定义**
SOFTWARE.
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
- “商业用途”指任何以盈利为目的的使用。
**2. 许可**
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
**3. 责任限制**
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
**4. 许可协议生效日期**
本许可协议自用户首次下载或使用本软件之日起生效。
**5. 许可终止**
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
**6. 其他**
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
**7. 联系信息**
- 许可方联系方式:
- 手机号18539907620
- 邮箱kangfenmao@qq.com
**许可方(签字):**
**日期:**
**被许可方(签字):**
**日期:**
---
#### English Version
**Cherry Studio Commercial License Agreement**
This Agreement ("Agreement") is entered into by and between:
- Licensor: Wang Qian (kangfenmao)
- Licensee: [Licensee Name]
**1. Definitions**
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
- "Commercial Use" refers to any use for profit.
**2. License**
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
**3. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**4. Effective Date of License Agreement**
This license agreement becomes effective from the date the user first downloads or uses the software.
**5. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**6. Miscellaneous**
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
**7. Contact Information**
- Licensor's Contact Details:
- Phone: 18539907620
- Email: kangfenmao@qq.com
**Licensor (Signature):**
**Date:**
**Licensee (Signature):**
**Date:**

View File

@@ -56,5 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
支持主题切换 新增发送按钮
修复一些问题 输入区域展开可以全屏显示

View File

@@ -1,6 +1,6 @@
{ {
"name": "cherry-studio", "name": "cherry-studio",
"version": "0.4.0", "version": "0.4.3",
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "kangfenmao@qq.com", "author": "kangfenmao@qq.com",
@@ -70,6 +70,7 @@
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "6",
"react-router-dom": "6", "react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"sass": "^1.77.2", "sass": "^1.77.2",

View File

@@ -45,7 +45,7 @@
--topic-list-width: 260px; --topic-list-width: 260px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 125px; --input-bar-height: 135px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -151,3 +151,15 @@ body,
padding-bottom: 12px; padding-bottom: 12px;
} }
} }
.loader {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #000;
box-shadow:
32px 0 #000,
-32px 0 #000;
position: relative;
animation: flash 0.5s ease-out infinite alternate;
}

View File

@@ -47,7 +47,7 @@ export function useProvider(id: string) {
return { return {
provider, provider,
models: provider.models, models: provider?.models || [],
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)), updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })), addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model })) removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))

View File

@@ -78,7 +78,8 @@ const resources = {
'settings.conext_count.tip': 'The number of previous messages to keep in the context.', 'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
'settings.reset': 'Reset', 'settings.reset': 'Reset',
'settings.set_as_default': 'Apply to default assistant', 'settings.set_as_default': 'Apply to default assistant',
'settings.max': 'Max' 'settings.max': 'Max',
'suggestions.title': 'Suggested Questions'
}, },
apps: { apps: {
title: 'Agents' title: 'Agents'
@@ -155,6 +156,8 @@ const resources = {
'about.feedback.title': '📝 Feedback', 'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback', 'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact', 'about.contact.title': '📧 Contact',
'about.license.title': '📄 License',
'about.license.button': 'License',
'about.contact.button': 'Email', 'about.contact.button': 'Email',
'proxy.title': 'Proxy Address', 'proxy.title': 'Proxy Address',
'theme.title': 'Theme', 'theme.title': 'Theme',
@@ -215,7 +218,7 @@ const resources = {
select_model: '选择模型' select_model: '选择模型'
}, },
message: { message: {
copied: '已复制!', copied: '已复制',
'assistant.added.content': '智能体添加成功', 'assistant.added.content': '智能体添加成功',
'message.delete.title': '删除消息', 'message.delete.title': '删除消息',
'message.delete.content': '确定要删除此消息吗?', 'message.delete.content': '确定要删除此消息吗?',
@@ -260,7 +263,8 @@ const resources = {
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10', '要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'settings.reset': '重置', 'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手', 'settings.set_as_default': '应用到默认助手',
'settings.max': '不限' 'settings.max': '不限',
'suggestions.title': '建议的问题'
}, },
apps: { apps: {
title: '智能体' title: '智能体'
@@ -337,6 +341,8 @@ const resources = {
'about.feedback.title': '📝 意见反馈', 'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈', 'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系', 'about.contact.title': '📧 邮件联系',
'about.license.title': '📄 许可证',
'about.license.button': '查看',
'about.contact.button': '邮件', 'about.contact.button': '邮件',
'proxy.title': '代理地址', 'proxy.title': '代理地址',
'theme.title': '主题', 'theme.title': '主题',

View File

@@ -4,10 +4,10 @@ import {
FullscreenExitOutlined, FullscreenExitOutlined,
FullscreenOutlined, FullscreenOutlined,
HistoryOutlined, HistoryOutlined,
MoreOutlined,
PauseCircleOutlined, PauseCircleOutlined,
PlusCircleOutlined PlusCircleOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic } from '@renderer/services/assistant' import { getDefaultTopic } from '@renderer/services/assistant'
@@ -23,8 +23,7 @@ import { debounce, isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import SendMessageSetting from './SendMessageSetting' import SendMessageButton from './SendMessageButton'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@@ -63,11 +62,25 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('') setText('')
setExpend(false)
} }
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text]) const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (expended) {
if (event.key === 'Escape') {
setExpend(false)
return
}
if (event.key === 'Enter' && event.shiftKey) {
sendMessage()
return
}
return
}
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') { if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) { if (event.shiftKey) {
return return
@@ -127,7 +140,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant]) }, [assistant])
return ( return (
<Container id="inputbar" style={{ minHeight: expended ? '35%' : 'var(--input-bar-height)' }}> <Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
<Toolbar> <Toolbar>
<ToolbarMenu> <ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow> <Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
@@ -158,11 +171,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ControlOutlined /> <ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
</ToolbarMenu> </ToolbarMenu>
<ToolbarMenu> <ToolbarMenu>
{generating && ( {generating && (
@@ -172,11 +180,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} )}
<SendMessageSetting> <Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" style={{ marginRight: 0 }}> <ToolbarButton type="text" onClick={() => setExpend(!expended)}>
<MoreOutlined /> {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton> </ToolbarButton>
</SendMessageSetting> </Tooltip>
</ToolbarMenu> </ToolbarMenu>
</Toolbar> </Toolbar>
<Textarea <Textarea
@@ -187,16 +195,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus autoFocus
contextMenu="true" contextMenu="true"
variant="borderless" variant="borderless"
showCount
ref={inputRef} ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }} styles={{ textarea: { paddingLeft: 0 } }}
/> />
{showInputEstimatedTokens && ( <Footer>
<TextCount> {showInputEstimatedTokens && (
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T <TextCount>
{`${inputTokenCount}/${estimateTokenCount}`} <HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
</TextCount> {`${inputTokenCount}/${estimateTokenCount}`}
)} </TextCount>
)}
<SendMessageButton sendMessage={sendMessage} />
</Footer>
</Container> </Container>
) )
} }
@@ -225,6 +235,7 @@ const Toolbar = styled.div`
justify-content: space-between; justify-content: space-between;
margin: 0 -5px; margin: 0 -5px;
margin-bottom: 5px; margin-bottom: 5px;
margin-right: -8px;
` `
const ToolbarMenu = styled.div` const ToolbarMenu = styled.div`
@@ -253,17 +264,22 @@ const ToolbarButton = styled(Button)`
} }
` `
const Footer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
margin-bottom: 8px;
`
const TextCount = styled.div` const TextCount = styled.div`
position: absolute;
right: 0;
bottom: 0;
font-size: 11px; font-size: 11px;
color: var(--color-text-3); color: var(--color-text-3);
z-index: 10; z-index: 10;
background-color: var(--color-background-soft);
padding: 2px 8px; padding: 2px 8px;
border-top-left-radius: 7px; border-top-left-radius: 7px;
user-select: none; user-select: none;
margin-right: 10px;
` `
export default Inputbar export default Inputbar

View File

@@ -4,13 +4,14 @@ import localforage from 'localforage'
import { FC, useCallback, useEffect, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import MessageItem from './Message' import MessageItem from './Message'
import { reverse } from 'lodash' import { debounce, reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api' import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils' import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider' import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { t } from 'i18next' import { t } from 'i18next'
import Suggestions from './Suggestions'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@@ -93,9 +94,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
}) })
}, [topic.id]) }, [topic.id])
const scrollTop = useCallback(
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
leading: true,
trailing: false
}),
[]
)
useEffect(() => { useEffect(() => {
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }) setTimeout(scrollTop, 100)
}, [messages]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages, lastMessage])
useEffect(() => { useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages)) EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
@@ -103,6 +113,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
return ( return (
<Container id="messages" key={assistant.id} ref={containerRef}> <Container id="messages" key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem message={lastMessage} />} {lastMessage && <MessageItem message={lastMessage} />}
{reverse([...messages]).map((message, index) => ( {reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} /> <MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
@@ -118,8 +129,6 @@ const Container = styled.div`
overflow-y: auto; overflow-y: auto;
flex-direction: column-reverse; flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height)); max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding-top: 10px;
padding-bottom: 10px;
.message:first-child { .message:first-child {
border: none; border: none;
} }

View File

@@ -1,12 +1,15 @@
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd' import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react' import { FC } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons' import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DownOutlined } from '@ant-design/icons'
interface Props extends PropsWithChildren {} interface Props {
sendMessage: () => void
}
const SendMessageSetting: FC<Props> = ({ children }) => { const SendMessageButton: FC<Props> = ({ sendMessage }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings() const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
@@ -26,14 +29,15 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
] ]
return ( return (
<Dropdown <Dropdown.Button
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }} onClick={sendMessage}
placement="topRight"
trigger={['click']} trigger={['click']}
arrow> menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
{children} icon={<DownOutlined />}
</Dropdown> style={{ width: 'auto' }}>
{t('assistant.input.send')}
</Dropdown.Button>
) )
} }
export default SendMessageSetting export default SendMessageButton

View File

@@ -0,0 +1,119 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Suggestion } from '@renderer/types'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import BeatLoader from 'react-spinners/BeatLoader'
import { fetchSuggestions } from '@renderer/services/api'
interface Props {
assistant: Assistant
messages: Message[]
lastMessage: Message | null
}
const suggestionsMap = new Map<string, Suggestion[]>()
const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
const [suggestions, setSuggestions] = useState<Suggestion[]>(
suggestionsMap.get(messages[messages.length - 1]?.id) || []
)
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const onClick = (s: Suggestion) => {
const message: Message = {
id: uuid(),
role: 'user',
content: s.content,
assistantId: assistant.id,
topicId: assistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
setLoadingSuggestions(true)
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
if (_suggestions.length) {
setSuggestions(_suggestions)
suggestionsMap.set(msg.id, _suggestions)
}
setLoadingSuggestions(false)
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant, messages])
useEffect(() => {
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
}, [messages])
if (lastMessage) {
return null
}
if (loadingSuggestions) {
return (
<Container>
<BeatLoader color="var(--color-text-2)" size="10" />
</Container>
)
}
if (suggestions.length === 0) {
return null
}
return (
<Container>
<SuggestionsContainer>
{suggestions.map((s, i) => (
<SuggestionItem key={i} onClick={() => onClick(s)}>
{s.content}
</SuggestionItem>
))}
</SuggestionsContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
padding: 20px;
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
padding-left: 55px;
`
const SuggestionsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`
const SuggestionItem = styled.div`
display: flex;
align-items: center;
width: fit-content;
padding: 7px 15px;
border-radius: 12px;
font-size: 13px;
color: var(--color-text);
background: var(--color-background-mute);
cursor: pointer;
&:hover {
opacity: 0.9;
}
`
export default Suggestions

View File

@@ -39,6 +39,10 @@ const AboutSettings: FC = () => {
onOpenWebsite(url) onOpenWebsite(url)
} }
const showLicense = () => {
window.api.openWebsite('https://raw.githubusercontent.com/kangfenmao/cherry-studio/main/LICENSE')
}
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const appInfo = await window.api.getAppInfo() const appInfo = await window.api.getAppInfo()
@@ -121,9 +125,7 @@ const AboutSettings: FC = () => {
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}> <Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button>
{t('settings.about.website.button')}
</Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@@ -133,6 +135,11 @@ const AboutSettings: FC = () => {
</Button> </Button>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.license.title')}</SettingRowTitle>
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button> <Button onClick={mailto}>{t('settings.about.contact.button')}</Button>

View File

@@ -42,7 +42,7 @@ const ProviderSettings: FC = () => {
apiKey: '', apiKey: '',
apiHost: '', apiHost: '',
models: [], models: [],
enabled: false, enabled: true,
isSystem: false isSystem: false
} as Provider } as Provider
addProvider(provider) addProvider(provider)

View File

@@ -71,7 +71,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
provider: _provider.id, provider: _provider.id,
group: getDefaultGroupName(model.id), group: getDefaultGroupName(model.id),
// @ts-ignore name // @ts-ignore name
description: model?.description description: model?.description,
owned_by: model?.owned_by
})) }))
) )
setLoading(false) setLoading(false)
@@ -178,7 +179,7 @@ const ListHeader = styled.div`
justify-content: space-between; justify-content: space-between;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
padding: 8px 22px; padding: 8px 22px;
color: var(--color-white); color: var(--color-text);
opacity: 0.4; opacity: 0.4;
` `
@@ -200,14 +201,14 @@ const ListItemHeader = styled.div`
` `
const ListItemName = styled.div` const ListItemName = styled.div`
color: var(--color-white); color: var(--color-text);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
margin-left: 6px; margin-left: 6px;
` `
const ModelHeaderTitle = styled.div` const ModelHeaderTitle = styled.div`
color: var(--color-white); color: var(--color-text);
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
margin-right: 10px; margin-right: 10px;

View File

@@ -34,7 +34,7 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
useEffect(() => { useEffect(() => {
document.body.setAttribute('theme-mode', _theme) document.body.setAttribute('theme-mode', _theme)
window.api.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light') window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
}, [_theme]) }, [_theme])
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider> return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>

View File

@@ -1,4 +1,4 @@
import { Assistant, Message, Provider } from '@renderer/types' import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import OpenAI from 'openai' import OpenAI from 'openai'
import Anthropic from '@anthropic-ai/sdk' import Anthropic from '@anthropic-ai/sdk'
import { getDefaultModel, getTopNamingModel } from './assistant' import { getDefaultModel, getTopNamingModel } from './assistant'
@@ -76,7 +76,10 @@ export default class ProviderSDK {
public async translate(message: Message, assistant: Assistant) { public async translate(message: Message, assistant: Assistant) {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const messages = [{ role: 'system', content: assistant.prompt }, message] const messages = [
{ role: 'system', content: assistant.prompt },
{ role: 'user', content: message.content }
]
if (this.isAnthropic) { if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({ const response = await this.anthropicSdk.messages.create({
@@ -131,6 +134,28 @@ export default class ProviderSDK {
} }
} }
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
const model = assistant.model
if (!model) {
return []
}
const response: any = await this.openaiSdk.request({
method: 'post',
path: '/advice_questions',
body: {
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
model: model.id,
max_tokens: 0,
temperature: 0,
n: 0
}
})
return response?.questions?.filter(Boolean)?.map((q: any) => ({ content: q })) || []
}
public async check(): Promise<{ valid: boolean; error: Error | null }> { public async check(): Promise<{ valid: boolean; error: Error | null }> {
const model = this.provider.models[0] const model = this.provider.models[0]
const body = { const body = {

View File

@@ -1,7 +1,7 @@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Provider, Topic } from '@renderer/types' import { Assistant, Message, Provider, Suggestion, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
@@ -109,6 +109,34 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
} }
} }
export async function fetchSuggestions({
messages,
assistant
}: {
messages: Message[]
assistant: Assistant
}): Promise<Suggestion[]> {
console.debug('fetchSuggestions', messages, assistant)
const provider = getAssistantProvider(assistant)
const providerSdk = new ProviderSDK(provider)
console.debug('fetchSuggestions', provider)
const model = assistant.model
if (!model) {
return []
}
if (model.owned_by !== 'graphrag') {
return []
}
try {
return await providerSdk.suggestions(messages, assistant)
} catch (error: any) {
return []
}
}
export async function checkApi(provider: Provider) { export async function checkApi(provider: Provider) {
const model = provider.models[0] const model = provider.models[0]
const key = 'api-check' const key = 'api-check'

View File

@@ -36,7 +36,7 @@ export function getTranslateModel() {
return store.getState().llm.translateModel return store.getState().llm.translateModel
} }
export function getAssistantProvider(assistant: Assistant) { export function getAssistantProvider(assistant: Assistant): Provider {
const providers = store.getState().llm.providers const providers = store.getState().llm.providers
const provider = providers.find((p) => p.id === assistant.model?.provider) const provider = providers.find((p) => p.id === assistant.model?.provider)
return provider || getDefaultProvider() return provider || getDefaultProvider()

View File

@@ -55,6 +55,7 @@ export type Model = {
provider: string provider: string
name: string name: string
group: string group: string
owned_by?: string
description?: string description?: string
} }
@@ -66,3 +67,7 @@ export type SystemAssistant = {
prompt: string prompt: string
group: string group: string
} }
export type Suggestion = {
content: string
}

View File

@@ -94,7 +94,7 @@ export function droppableReorder<T>(list: T[], startIndex: number, endIndex: num
} }
export function firstLetter(str: string): string { export function firstLetter(str: string): string {
const match = str.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u) const match = str?.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
return match ? match[0] : '' return match ? match[0] : ''
} }

View File

@@ -1,209 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta name="keywords" content="Cherry Studio AI, AI 助手, GPT 客户端, 多模型, iOS, macOS, Windows, LLM" />
<meta name="author" content="kangfenmao" />
<link rel="canonical" href="https://cherry-ai.com" />
<link rel="icon" type="image/png" href="https://cherry-ai.com/logo.png" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://cherry-ai.com" />
<meta property="og:title" content="Cherry Studio AI - 多模型 AI 助手" />
<meta
property="og:description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta property="og:image" content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://x.com/kangfenmao" />
<meta property="twitter:title" content="Cherry Studio AI - 多模型 AI 助手" />
<meta
property="twitter:description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta
property="twitter:image"
content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
<title>Cherry Studio AI - 多模型AI助手</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
background-color: #000000;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
}
.logo {
width: 100px;
height: 100px;
margin-bottom: 20px;
border-radius: 10%;
margin-top: -10vh;
}
h1 {
font-size: 48px;
margin-bottom: 10px;
}
.description {
font-size: 18px;
margin-bottom: 30px;
color: #a0a0a0;
}
.download-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin-bottom: 30px;
}
.download-btn {
background-color: #ffffff;
color: #000000;
padding: 10px 20px;
border-radius: 25px;
text-decoration: none;
font-weight: bold;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
.download-btn:hover {
background-color: #e0e0e0;
}
.download-btn svg {
margin-right: 10px;
width: 24px;
height: 24px;
}
.new-app {
margin-top: 20px;
font-size: 14px;
color: #a0a0a0;
}
.footer {
position: absolute;
bottom: 20px;
font-size: 14px;
color: #a0a0a0;
}
.footer a {
color: #a0a0a0;
text-decoration: none;
margin: 0 10px;
}
a {
color: #ffffff;
text-decoration: underline;
}
.loading {
flex-direction: row;
justify-content: center;
height: 200px;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body id="app">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">
<img src="https://cherry-ai.com/logo.png" alt="Cherry Studio AI Logo" class="logo" />
</a>
<h1>Cherry Studio AI</h1>
<p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons">
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-x64.dmg`"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
fill="currentColor"
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
</svg>
macOS Intel
</a>
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-arm64.dmg`"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
fill="currentColor"
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
</svg>
macOS Apple Silicon
</a>
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-setup.exe`"
class="download-btn">
<svg viewBox="0 0 448 512" width="24" height="24">
<path
fill="currentColor"
d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" />
</svg>
下载 Windows 版本
</a>
</div>
<p class="new-app">
🎉 <a href="https://github.com/kangfenmao/cherry-studio" target="_blank">Cherry Studio AI</a> 最新版本
<a :href="`https://github.com/kangfenmao/cherry-studio/releases/tag/v${version}`" target="_blank" v-cloak
>v{{version}}</a
>
发布啦!
</p>
<div class="footer">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">开源</a> |
<a href="https://github.com/kangfenmao/cherry-studio/blob/main/README.md" target="_blank">帮助</a> |
<a href="mailto:kangfenmao@qq.com" target="_blank">联系</a>
</div>
<!-- 结构化数据 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Cherry Studio AI",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "iOS, macOS, Windows",
"description": "Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue
createApp({
data() {
return {
version: '0.3.2',
loading: true
}
},
mounted() {
this.loading = true
fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest')
.then((response) => response.json())
.then((data) => (this.version = data.tag_name.replace('v', '')))
.finally(() => (this.loading = false))
}
}).mount('#app')
</script>
</body>
</html>

View File

@@ -3479,6 +3479,7 @@ __metadata:
react-redux: "npm:^9.1.2" react-redux: "npm:^9.1.2"
react-router: "npm:6" react-router: "npm:6"
react-router-dom: "npm:6" react-router-dom: "npm:6"
react-spinners: "npm:^0.14.1"
react-syntax-highlighter: "npm:^15.5.0" react-syntax-highlighter: "npm:^15.5.0"
redux-persist: "npm:^6.0.0" redux-persist: "npm:^6.0.0"
sass: "npm:^1.77.2" sass: "npm:^1.77.2"
@@ -8601,6 +8602,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-spinners@npm:^0.14.1":
version: 0.14.1
resolution: "react-spinners@npm:0.14.1"
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/5b3c101f789716331a0b6afad156293fb9aa05620e65494753001afcdb611788057f379b5979b34d570d527fa978003293266b59db505bf2d243ebab899ceeda
languageName: node
linkType: hard
"react-syntax-highlighter@npm:^15.5.0": "react-syntax-highlighter@npm:^15.5.0":
version: 15.5.0 version: 15.5.0
resolution: "react-syntax-highlighter@npm:15.5.0" resolution: "react-syntax-highlighter@npm:15.5.0"