Compare commits

...

8 Commits

Author SHA1 Message Date
kangfenmao
731fb7860b 0.3.3 2024-07-23 12:37:40 +08:00
kangfenmao
4a32976483 fix: proxy check 2024-07-23 12:37:12 +08:00
kangfenmao
dedabe320e feat: new navbar style 2024-07-23 12:29:20 +08:00
kangfenmao
235b481645 feat: change icons 2024-07-23 10:42:58 +08:00
kangfenmao
58c5ace678 fix: inputbar setShowRightSidebar 2024-07-23 10:20:57 +08:00
kangfenmao
973d24271b feat(settings): add proxy setting 2024-07-23 00:28:41 +08:00
kangfenmao
f434fe1231 feat: add show or hide assistant sidebar 2024-07-22 21:57:39 +08:00
kangfenmao
a0c147ae3f feat(website): fetch github release info 2024-07-22 15:40:30 +08:00
24 changed files with 231 additions and 72 deletions

3
.gitignore vendored
View File

@@ -34,6 +34,9 @@ npm/*/*
!.yarn/sdks
!.yarn/versions
# Windows
Thumbs.db
# Project
node_modules
dist

View File

@@ -56,6 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
支持设置模型 Temperature 参数
支持设置上下文数量
输入框增加 Token 消耗预估
支持配置网络代理
支持隐藏左侧智能体

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.3.2",
"version": "0.3.3",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",

View File

@@ -1,6 +1,6 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
@@ -101,6 +101,10 @@ app.whenReady().then(() => {
shell.openExternal(url)
})
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')

View File

@@ -10,6 +10,7 @@ declare global {
}>
checkForUpdate: () => void
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
}
}
}

View File

@@ -5,7 +5,8 @@ import { electronAPI } from '@electron-toolkit/preload'
const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url)
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -12,7 +12,7 @@
--color-white-mute: #f2f2f2;
--color-black: #1b1b1f;
--color-black-soft: #303030;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #515c67;
@@ -34,7 +34,7 @@
--navbar-height: 42px;
--sidebar-width: 55px;
--assistants-width: 235px;
--assistants-width: 250px;
--topic-list-width: 250px;
--settings-width: var(--assistants-width);
--status-bar-height: 40px;
@@ -114,7 +114,6 @@ body,
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {

View File

@@ -25,16 +25,16 @@ const NavbarContainer = styled.div`
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
border-bottom: 0.5px solid var(--color-border);
-webkit-app-region: drag;
background-color: #1f1f1f;
margin-left: calc(var(--sidebar-width) * -1);
padding-left: var(--sidebar-width);
border-bottom: 0.5px solid var(--color-border);
border-top: 0.5px solid var(--color-border);
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
border-right: 1px solid var(--color-border);
padding: 0 10px;
display: flex;
flex-direction: row;
@@ -48,7 +48,6 @@ const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
border-right: 1px solid var(--color-border);
padding: 0 20px;
font-size: 14px;
font-weight: bold;

View File

@@ -3,6 +3,7 @@ import Logo from '@renderer/assets/images/logo.png'
import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar'
import { isMac, isWindows } from '@renderer/config/constant'
const Sidebar: FC = () => {
const { pathname } = useLocation()
@@ -11,7 +12,8 @@ const Sidebar: FC = () => {
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container>
<Container style={isWindows ? { paddingTop: 0 } : {}}>
{isMac && <PlaceholderBorder />}
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
@@ -50,7 +52,9 @@ const Container = styled.div`
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
border-top: 0.5px solid var(--color-border);
padding-top: var(--navbar-height);
position: relative;
`
const AvatarImg = styled.img`
@@ -59,7 +63,7 @@ const AvatarImg = styled.img`
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: 12px;
margin-top: ${isMac ? '16px' : '7px'};
`
const MainMenus = styled.div`
display: flex;
@@ -111,4 +115,14 @@ const StyledLink = styled(Link)`
}
`
const PlaceholderBorder = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
border-right: 1px solid #1f1f1f;
border-bottom: 0.5px solid var(--color-border);
position: absolute;
top: -0.5px;
left: 0.5px;
`
export default Sidebar

View File

@@ -1,2 +1,5 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 5
export const platform = window.electron?.process?.platform === 'darwin' ? 'macos' : 'windows'
export const isMac = platform === 'macos'
export const isWindows = platform === 'windows'

View File

@@ -4,9 +4,11 @@ import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
import { useSettings } from './useSettings'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl } = useSettings()
useEffect(() => {
runAsyncFunction(async () => {
@@ -22,4 +24,8 @@ export function useAppInit() {
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
})
}, [])
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
}

View File

@@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { toggleRightSidebar } from '@renderer/store/settings'
import { toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
export function useShowRightSidebar() {
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
@@ -7,6 +7,16 @@ export function useShowRightSidebar() {
return {
showRightSidebar,
setShowRightSidebar: () => dispatch(toggleRightSidebar())
toggleRightSidebar: () => dispatch(toggleRightSidebar())
}
}
export function useShowAssistants() {
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
const dispatch = useAppDispatch()
return {
showAssistants,
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}

View File

@@ -39,6 +39,7 @@ const resources = {
'error.enter.api.key': 'Please enter your API key first',
'error.enter.api.host': 'Please enter your API host first',
'error.enter.model': 'Please select a model first',
'error.invalid.proxy.url': 'Invalid proxy URL',
'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused',
@@ -142,7 +143,8 @@ const resources = {
'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact',
'about.contact.button': 'Email'
'about.contact.button': 'Email',
'proxy.title': 'Proxy Address'
}
}
},
@@ -182,6 +184,7 @@ const resources = {
'error.enter.api.key': '请输入您的 API 密钥',
'error.enter.api.host': '请输入您的 API 地址',
'error.enter.model': '请选择一个模型',
'error.invalid.proxy.url': '无效的代理地址',
'api.connection.failed': '连接失败',
'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止',
@@ -286,7 +289,8 @@ const resources = {
'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件'
'about.contact.button': '邮件',
'proxy.title': '代理地址'
}
}
}

View File

@@ -5,15 +5,18 @@ import styled from 'styled-components'
import Chat from './components/Chat'
import Assistants from './components/Assistants'
import { uuid } from '@renderer/utils'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd'
import Navigation from './components/Navigation'
import Navigation from './components/NavigationCenter'
import { useTranslation } from 'react-i18next'
import { PlusCircleOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
const HomePage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const [activeAssistant, setActiveAssistant] = useState(assistants[0])
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
const { showRightSidebar, toggleRightSidebar } = useShowRightSidebar()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { defaultAssistant } = useDefaultAssistant()
const { t } = useTranslation()
@@ -26,29 +29,36 @@ const HomePage: FC = () => {
return (
<Container>
<Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
<NewButton onClick={onCreateAssistant}>
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<i className="iconfont icon-hidesidebarhoriz" />
</NewButton>
<NewButton onClick={onCreateAssistant}>
<PlusCircleOutlined />
</NewButton>
</NavbarLeft>
)}
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<NavbarRight style={{ justifyContent: 'flex-end', padding: '0 7px' }}>
<Tooltip
placement="left"
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
arrow>
<NewButton onClick={setShowRightSidebar}>
<NewButton onClick={toggleRightSidebar}>
<i className={`iconfont ${showRightSidebar ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton>
</Tooltip>
</NavbarRight>
</Navbar>
<ContentContainer>
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
/>
{showAssistants && (
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
/>
)}
<Chat assistant={activeAssistant} />
</ContentContainer>
</Container>
@@ -59,33 +69,31 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
`
const NewButton = styled.div`
export const NewButton = styled.div`
-webkit-app-region: none;
border-radius: 4px;
width: 34px;
height: 34px;
width: 30px;
height: 30px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
color: var(--color-icon);
.iconfont {
font-size: 22px;
.anticon {
font-size: 19px;
}
.icon-showsidebarhoriz,
.icon-hidesidebarhoriz {
font-size: 18px;
font-size: 17px;
}
&:hover {
background-color: var(--color-background-soft);

View File

@@ -4,9 +4,9 @@ import {
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MessageOutlined,
MoreOutlined,
PauseCircleOutlined,
PlusCircleOutlined
PauseCircleOutlined
} from '@ant-design/icons'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@@ -35,7 +35,7 @@ interface Props {
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState('')
const { setShowRightSidebar } = useShowRightSidebar()
const { toggleRightSidebar } = useShowRightSidebar()
const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut } = useSettings()
const [expended, setExpend] = useState(false)
@@ -134,11 +134,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<PlusCircleOutlined />
<MessageOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={setShowRightSidebar}>
<ToolbarButton type="text" onClick={toggleRightSidebar}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>

View File

@@ -3,7 +3,7 @@ import { Avatar, Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import useAvatar from '@renderer/hooks/useAvatar'
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DeleteOutlined, EditOutlined, SwitcherOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
@@ -91,7 +91,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<CopyOutlined onClick={onCopy} />
<SwitcherOutlined onClick={onCopy} />
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<DeleteOutlined onClick={onDelete} />

View File

@@ -7,16 +7,21 @@ import { Button, Dropdown, MenuProps } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { capitalizeFirstLetter } from '@renderer/utils'
import { isMac } from '@renderer/config/constant'
interface Props {
activeAssistant: Assistant
}
const Navigation: FC<Props> = ({ activeAssistant }) => {
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)
@@ -33,12 +38,17 @@ const Navigation: FC<Props> = ({ activeAssistant }) => {
}))
return (
<NavbarCenter style={{ border: 'none', padding: '0 15px' }}>
{assistant?.name}
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
{!showAssistants && (
<NewButton onClick={toggleShowAssistants} style={{ marginRight: 8 }}>
<i className="iconfont icon-showsidebarhoriz" />
</NewButton>
)}
<AssistantName>{assistant?.name}</AssistantName>
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<Button size="small" type="primary" ghost style={{ fontSize: '11px' }}>
{model ? model.name : t('button.select_model')}
</Button>
<DropdownButton size="small" type="primary" ghost>
{model ? capitalizeFirstLetter(model.name) : t('button.select_model')}
</DropdownButton>
</DropdownMenu>
</NavbarCenter>
)
@@ -49,4 +59,15 @@ const DropdownMenu = styled(Dropdown)`
margin-left: 10px;
`
export default Navigation
const AssistantName = styled.span`
font-weight: bold;
margin-left: 5px;
`
const DropdownButton = styled(Button)`
font-size: 10px;
border-radius: 15px;
padding: 0 8px;
`
export default NavigationCenter

View File

@@ -1,20 +1,22 @@
import { FC } from 'react'
import { FC, useState } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, Select, Upload } from 'antd'
import { Avatar, Input, Select, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage } from '@renderer/utils'
import { compressImage, isValidProxyUrl } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => {
const avatar = useAvatar()
const { language } = useSettings()
const { language, proxyUrl: storeProxyUrl } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch()
const { t } = useTranslation()
@@ -24,6 +26,16 @@ const GeneralSettings: FC = () => {
localStorage.setItem('language', value)
}
const onSetProxyUrl = () => {
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
return
}
dispatch(_setProxyUrl(proxyUrl))
window.api.setProxy(proxyUrl)
}
return (
<SettingContainer>
<SettingTitle>{t('settings.general.title')}</SettingTitle>
@@ -62,6 +74,18 @@ const GeneralSettings: FC = () => {
</Upload>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
placeholder="socks5://127.0.0.1:6153"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 300 }}
onBlur={() => onSetProxyUrl()}
type="url"
/>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}

View File

@@ -2,7 +2,7 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Provider, Topic } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { getAssistantProvider, getDefaultModel, getProviderByModel, getTopNamingModel } from './assistant'
import { EVENT_NAMES, EventEmitter } from './event'
@@ -92,15 +92,13 @@ export async function checkApi(provider: Provider) {
const providerSdk = new ProviderSDK(provider)
const { valid, error } = await providerSdk.check()
const { valid } = await providerSdk.check()
window.message[valid ? 'success' : 'error']({
key: 'api-check',
style: { marginTop: '3vh' },
duration: valid ? 2 : 8,
content: valid
? i18n.t('message.api.connection.success')
: i18n.t('message.api.connection.failed') + ' : ' + getErrorMessage(error)
content: valid ? i18n.t('message.api.connection.success') : i18n.t('message.api.connection.failed')
})
return valid

View File

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

View File

@@ -243,6 +243,17 @@ const migrate = createMigrate({
}
}
}
},
// @ts-ignore store type is unknown
'14': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
showAssistants: true,
proxyUrl: undefined
}
}
}
})

View File

@@ -4,14 +4,18 @@ export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
export interface SettingsState {
showRightSidebar: boolean
showAssistants: boolean
sendMessageShortcut: SendMessageShortcut
language: string
proxyUrl?: string
}
const initialState: SettingsState = {
showRightSidebar: true,
showAssistants: true,
sendMessageShortcut: 'Enter',
language: navigator.language
language: navigator.language,
proxyUrl: undefined
}
const settingsSlice = createSlice({
@@ -21,15 +25,22 @@ const settingsSlice = createSlice({
toggleRightSidebar: (state) => {
state.showRightSidebar = !state.showRightSidebar
},
toggleShowAssistants: (state) => {
state.showAssistants = !state.showAssistants
},
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
state.sendMessageShortcut = action.payload
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload
},
setProxyUrl: (state, action: PayloadAction<string | undefined>) => {
state.proxyUrl = action.payload
}
}
})
export const { toggleRightSidebar, setSendMessageShortcut, setLanguage } = settingsSlice.actions
export const { toggleRightSidebar, toggleShowAssistants, setSendMessageShortcut, setLanguage, setProxyUrl } =
settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -198,3 +198,13 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
return all.usedTokens - 7
}
// 首字母大写
export const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// is valid proxy url
export const isValidProxyUrl = (url: string) => {
return url.includes('://')
}

View File

@@ -51,6 +51,7 @@
height: 100px;
margin-bottom: 20px;
border-radius: 10%;
margin-top: -10vh;
}
h1 {
font-size: 48px;
@@ -64,6 +65,7 @@
.download-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin-bottom: 30px;
}
@@ -107,9 +109,17 @@
color: #ffffff;
text-decoration: underline;
}
.loading {
flex-direction: row;
justify-content: center;
height: 200px;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<body id="app">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">
<img src="https://easys.run/cherry-studio/logo.png" alt="Cherry Studio AI Logo" class="logo" />
</a>
@@ -117,7 +127,7 @@
<p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons">
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-x64.dmg"
: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
@@ -127,7 +137,7 @@
macOS Intel
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-arm64.dmg"
: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
@@ -137,7 +147,7 @@
macOS Apple Silicon
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-setup.exe"
: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
@@ -149,7 +159,10 @@
</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/v0.3.0" target="_blank">v0.3.0</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> |
@@ -172,5 +185,25 @@
}
}
</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>