Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00c66e54a | ||
|
|
62b0908dfa | ||
|
|
cb0b9de1e9 | ||
|
|
d8d4afbc0d | ||
|
|
c50ff4585a | ||
|
|
a5ee8548f3 | ||
|
|
15b286a095 | ||
|
|
d47d4a158d | ||
|
|
cd85dcddf8 | ||
|
|
925a9fb8ec | ||
|
|
17c3437e02 | ||
|
|
69293846fc | ||
|
|
20a7fbfc48 | ||
|
|
64d4b8450a |
114
LICENSE
114
LICENSE
@@ -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:**
|
||||||
|
|||||||
@@ -56,5 +56,5 @@ electronDownload:
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
支持主题切换
|
新增发送按钮
|
||||||
修复一些问题
|
输入区域展开可以全屏显示
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }))
|
||||||
|
|||||||
@@ -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': '主题',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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] : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
11
yarn.lock
11
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user