Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
731fb7860b | ||
|
|
4a32976483 | ||
|
|
dedabe320e | ||
|
|
235b481645 | ||
|
|
58c5ace678 | ||
|
|
973d24271b | ||
|
|
f434fe1231 | ||
|
|
a0c147ae3f | ||
|
|
8d7cde1231 | ||
|
|
87c04408de | ||
|
|
2592448c74 | ||
|
|
6f054874e8 | ||
|
|
40d687104e | ||
|
|
ac3cfe2878 | ||
|
|
e9a7735fce | ||
|
|
c1a8198575 | ||
|
|
8b45548b79 | ||
|
|
3f3b930819 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,6 +34,9 @@ npm/*/*
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
|
||||
# Project
|
||||
node_modules
|
||||
dist
|
||||
|
||||
@@ -56,6 +56,5 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
支持设置模型 Temperature 参数
|
||||
支持设置上下文数量
|
||||
输入框增加 Token 消耗预估
|
||||
支持配置网络代理
|
||||
支持隐藏左侧智能体
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.3",
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
|
||||
@@ -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('触发检查更新')
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -10,6 +10,7 @@ declare global {
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# CHANGES LOG
|
||||
|
||||
### v0.3.0 - 2024-07-21
|
||||
|
||||
- Supports setting the model Temperature parameter
|
||||
- Support for setting the number of contexts
|
||||
- Token consumption estimation added to the input box
|
||||
|
||||
### v0.2.9 - 2024-07-20
|
||||
|
||||
- 📢 Add AiHubMix provider
|
||||
|
||||
### v0.2.8 - 2024-07-20
|
||||
|
||||
- 🆕 Feature: Add customized service providers
|
||||
|
||||
### v0.2.7 - 2024-07-19
|
||||
|
||||
- 📢 Add DashScope Provider
|
||||
- 📢 Add Anthropic Provider
|
||||
|
||||
### v0.2.6 - 2024-07-17
|
||||
|
||||
- 🆕 Fixed the issue of the BaiChuan API KEY not displaying when clicking to obtain the URL
|
||||
- 📢 New intelligent body center style
|
||||
|
||||
### v0.2.5 - 2024-07-17
|
||||
|
||||
- 🆕 Baichuan AI Service Providers
|
||||
- 📢 New Intelligent Agent Page with Multiple Professional Assistants
|
||||
- 🌐 Multilingual Issue Fixes and Detailed Optimizations
|
||||
|
||||
### v0.2.4 - 2024-07-16
|
||||
|
||||
- Fixed the issue of the update log page cannot be scrolled
|
||||
- Added a check for updates button
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
- Fixed multi-language prompt errors
|
||||
- Fixed default model error issues with ZHIPU AI
|
||||
- Fixed OpenRouter API detection error issues
|
||||
- Fixed multi-language translation errors with model providers
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
- Fix the issue where the default assistant name is empty.
|
||||
- Fix the problem with default language detection during the first installation.
|
||||
- Adjust the changelog style.
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
- **Feature**: Add new feature for pausing message sending
|
||||
- **Fix**: Resolve incomplete translation issue upon language switch
|
||||
- **Build**: Support for macOS Intel architecture
|
||||
@@ -1,56 +0,0 @@
|
||||
# 更新日志
|
||||
|
||||
### v0.3.0 - 2024-07-21
|
||||
|
||||
- 支持设置模型 Temperature 参数
|
||||
- 支持设置上下文数量
|
||||
- 输入框增加 Token 消耗预估
|
||||
|
||||
### v0.2.9 - 2024-07-20
|
||||
|
||||
- 📢 新增 AiMixHub 服务提供商
|
||||
|
||||
### v0.2.8 - 2024-07-20
|
||||
|
||||
- 🆕 新功能: 可以添加自定义服务提供商了
|
||||
|
||||
### v0.2.7 - 2024-07-19
|
||||
|
||||
- 📢 新增阿里云灵积服务商
|
||||
- 📢 新增 Anthropic 服务商
|
||||
|
||||
### v0.2.6 - 2024-07-17
|
||||
|
||||
- 🆕 修复百川 API KEY 点击获取网址没有显示问题
|
||||
- 📢 新的智能体中心样式
|
||||
|
||||
### v0.2.5 - 2024-07-17
|
||||
|
||||
- 🆕 新增百川AI服务商
|
||||
- 📢 全新的智能体页面,新增多种职业助手
|
||||
- 🌐 多语言问题修复,细节优化
|
||||
|
||||
### v0.2.4 - 2024-07-16
|
||||
|
||||
- 修复更新日志页面不能滚动问题
|
||||
- 新增检查更新按钮
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
- 修复多语言提示错误
|
||||
- 修复智谱AI默认模型错误问题
|
||||
- 修复 OpenRouter API 检测出错问题
|
||||
- 修复模型提供商多语言翻译错误问题
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
- 修复默认助理名称为空的问题
|
||||
- 修复首次安装默认语言检测问题
|
||||
- 更新日志样式微调
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
- 【功能】新增消息暂停发送功能
|
||||
- 【修复】修复多语言切换不彻底问题
|
||||
- 【构建】支持 macOS Intel 架构
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppInitEffect } from '@renderer/hooks/useAppInitEffect'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { message, Modal } from 'antd'
|
||||
import { findIndex, pullAt } from 'lodash'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
@@ -29,7 +29,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
|
||||
useAppInitEffect()
|
||||
useAppInit()
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elements]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 useAppInitEffect() {
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@@ -22,4 +24,8 @@ export function useAppInitEffect() {
|
||||
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
}, [proxyUrl])
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addTopic as _addTopic,
|
||||
removeAllTopics as _removeAllTopics,
|
||||
removeTopic as _removeTopic,
|
||||
setModel as _setModel,
|
||||
updateAssistants as _updateAssistants,
|
||||
updateDefaultAssistant as _updateDefaultAssistant,
|
||||
updateTopic as _updateTopic,
|
||||
updateTopics as _updateTopics,
|
||||
addAssistant,
|
||||
addTopic,
|
||||
removeAllTopics,
|
||||
removeAssistant,
|
||||
updateAssistant
|
||||
removeTopic,
|
||||
setModel,
|
||||
updateAssistant,
|
||||
updateAssistants,
|
||||
updateAssistantSettings,
|
||||
updateDefaultAssistant,
|
||||
updateTopic,
|
||||
updateTopics
|
||||
} from '@renderer/store/assistants'
|
||||
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
|
||||
import { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||
import localforage from 'localforage'
|
||||
|
||||
export function useAssistants() {
|
||||
@@ -23,9 +24,8 @@ export function useAssistants() {
|
||||
|
||||
return {
|
||||
assistants,
|
||||
updateAssistants: (assistants: Assistant[]) => dispatch(_updateAssistants(assistants)),
|
||||
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
|
||||
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
|
||||
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
|
||||
removeAssistant: (id: string) => {
|
||||
dispatch(removeAssistant({ id }))
|
||||
const assistant = assistants.find((a) => a.id === id)
|
||||
@@ -44,17 +44,21 @@ export function useAssistant(id: string) {
|
||||
return {
|
||||
assistant,
|
||||
model: assistant?.model ?? defaultModel,
|
||||
addTopic: (topic: Topic) => dispatch(_addTopic({ assistantId: assistant.id, topic })),
|
||||
removeTopic: (topic: Topic) => dispatch(_removeTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopic: (topic: Topic) => dispatch(_updateTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopics: (topics: Topic[]) => dispatch(_updateTopics({ assistantId: assistant.id, topics })),
|
||||
removeAllTopics: () => dispatch(_removeAllTopics({ assistantId: assistant.id })),
|
||||
setModel: (model: Model) => dispatch(_setModel({ assistantId: assistant.id, model }))
|
||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
||||
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
||||
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
||||
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
|
||||
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
|
||||
updateAssistantSettings: (settings: AssistantSettings) => {
|
||||
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefaultAssistant() {
|
||||
const { defaultAssistant } = useAppSelector((state) => state.assistants)
|
||||
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
@@ -62,7 +66,7 @@ export function useDefaultAssistant() {
|
||||
...defaultAssistant,
|
||||
topics: [getDefaultTopic()]
|
||||
},
|
||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(_updateDefaultAssistant({ assistant }))
|
||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addModel as _addModel,
|
||||
removeModel as _removeModel,
|
||||
updateProvider as _updateProvider,
|
||||
updateProviders as _updateProviders,
|
||||
addModel,
|
||||
addProvider,
|
||||
removeProvider
|
||||
removeModel,
|
||||
removeProvider,
|
||||
updateProvider,
|
||||
updateProviders
|
||||
} from '@renderer/store/llm'
|
||||
import { Assistant, Model, Provider } from '@renderer/types'
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
@@ -24,8 +24,8 @@ export function useProviders() {
|
||||
providers,
|
||||
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
|
||||
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
|
||||
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
|
||||
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers))
|
||||
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
||||
updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ export function useProvider(id: string) {
|
||||
return {
|
||||
provider,
|
||||
models: provider.models,
|
||||
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
|
||||
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })),
|
||||
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model }))
|
||||
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
||||
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
|
||||
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ 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'
|
||||
'chat.completion.paused': 'Chat completion paused',
|
||||
'switch.disabled': 'Switching is disabled while the assistant is generating'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '😀 Default Assistant',
|
||||
@@ -66,7 +68,7 @@ const resources = {
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause',
|
||||
'input.settings': 'Settings',
|
||||
'input.estimated_tokens': 'Estimated tokens: ',
|
||||
'input.estimated_tokens': 'Estimated Tokens: ',
|
||||
'settings.temperature': 'Temperature',
|
||||
'settings.temperature.tip':
|
||||
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
|
||||
@@ -96,11 +98,11 @@ const resources = {
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
general: 'General',
|
||||
general: 'General Settings',
|
||||
provider: 'Model Provider',
|
||||
model: 'Model Settings',
|
||||
assistant: 'Default Assistant',
|
||||
about: 'About',
|
||||
about: 'About & Feedback',
|
||||
'general.title': 'General Settings',
|
||||
'provider.api_key': 'API Key',
|
||||
'provider.check': 'Check',
|
||||
@@ -132,7 +134,17 @@ const resources = {
|
||||
'provider.delete.title': 'Delete Provider',
|
||||
'provider.delete.content': 'Are you sure you want to delete this provider?',
|
||||
'provider.edit.name': 'Provider Name',
|
||||
'provider.edit.name.placeholder': 'Example: OpenAI'
|
||||
'provider.edit.name.placeholder': 'Example: OpenAI',
|
||||
'about.title': 'About',
|
||||
'about.releases.title': '📔 Release Notes',
|
||||
'about.releases.button': 'Releases',
|
||||
'about.website.title': '🌐 Official Website',
|
||||
'about.website.button': 'Website',
|
||||
'about.feedback.title': '📝 Feedback',
|
||||
'about.feedback.button': 'Feedback',
|
||||
'about.contact.title': '📧 Contact',
|
||||
'about.contact.button': 'Email',
|
||||
'proxy.title': 'Proxy Address'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -172,9 +184,11 @@ 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': '会话已停止'
|
||||
'chat.completion.paused': '会话已停止',
|
||||
'switch.disabled': '模型回复完成后才能切换'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '😃 默认助手 - Assistant',
|
||||
@@ -234,7 +248,7 @@ const resources = {
|
||||
provider: '模型提供商',
|
||||
model: '模型设置',
|
||||
assistant: '默认助手',
|
||||
about: '关于',
|
||||
about: '关于我们',
|
||||
'general.title': '常规设置',
|
||||
'provider.api_key': 'API 密钥',
|
||||
'provider.check': '检查',
|
||||
@@ -257,7 +271,7 @@ const resources = {
|
||||
'models.empty': '没有模型',
|
||||
'assistant.title': '默认助手',
|
||||
'assistant.model_params': '模型参数',
|
||||
'about.description': '一个为创造者而生的 AI 助手',
|
||||
'about.description': '一款为创造者而生的 AI 助手',
|
||||
'about.updateNotAvailable': '你的软件已是最新版本',
|
||||
'about.checkingUpdate': '正在检查更新...',
|
||||
'about.updateError': '更新出错',
|
||||
@@ -266,7 +280,17 @@ const resources = {
|
||||
'provider.delete.title': '删除提供商',
|
||||
'provider.delete.content': '确定要删除此模型提供商吗?',
|
||||
'provider.edit.name': '模型提供商名称',
|
||||
'provider.edit.name.placeholder': '例如 OpenAI'
|
||||
'provider.edit.name.placeholder': '例如 OpenAI',
|
||||
'about.title': '关于我们',
|
||||
'about.releases.title': '📔 更新日志',
|
||||
'about.releases.button': '查看',
|
||||
'about.website.title': '🌐 官方网站',
|
||||
'about.website.button': '查看',
|
||||
'about.feedback.title': '📝 意见反馈',
|
||||
'about.feedback.button': '反馈',
|
||||
'about.contact.title': '📧 邮件联系',
|
||||
'about.contact.button': '邮件',
|
||||
'proxy.title': '代理地址'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button, Col, InputNumber, Popover, Row, Slider, Tooltip } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
@@ -13,8 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const PopoverContent: FC<Props> = (props) => {
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { updateAssistant } = useAssistants()
|
||||
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
|
||||
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
const { t } = useTranslation()
|
||||
@@ -22,17 +21,17 @@ const PopoverContent: FC<Props> = (props) => {
|
||||
const onUpdateAssistantSettings = useCallback(
|
||||
debounce(
|
||||
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
settings: {
|
||||
...assistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
}
|
||||
updateAssistantSettings({
|
||||
...assistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: false, trailing: true }
|
||||
{
|
||||
leading: false,
|
||||
trailing: true
|
||||
}
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { droppableReorder, uuid } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
@@ -19,7 +20,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -69,6 +72,14 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchAssistant = (assistant: Assistant) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
@@ -81,7 +92,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||
<AssistantItem
|
||||
onClick={() => setActiveAssistant(assistant)}
|
||||
onClick={() => onSwitchAssistant(assistant)}
|
||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
||||
<AssistantName>{assistant.name}</AssistantName>
|
||||
</AssistantItem>
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { estimateInputTokenCount, uuid } from '@renderer/utils'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { MoreOutlined } from '@ant-design/icons'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import {
|
||||
ClearOutlined,
|
||||
ControlOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
HistoryOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlusCircleOutlined
|
||||
MessageOutlined,
|
||||
MoreOutlined,
|
||||
PauseCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
import SendMessageSetting from './SendMessageSetting'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import dayjs from 'dayjs'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { estimateInputTokenCount, uuid } from '@renderer/utils'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import AssistantSettings from './AssistantSettings'
|
||||
import SendMessageSetting from './SendMessageSetting'
|
||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -34,14 +35,13 @@ 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)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sendMessage = () => {
|
||||
@@ -86,7 +86,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(() => {
|
||||
const topic: Topic = getDefaultTopic()
|
||||
const topic = getDefaultTopic()
|
||||
addTopic(topic)
|
||||
setActiveTopic(topic)
|
||||
}, [addTopic, setActiveTopic])
|
||||
@@ -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>
|
||||
@@ -194,6 +194,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
/>
|
||||
<TextCount>
|
||||
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} |{' '}
|
||||
{t('assistant.input.estimated_tokens')}: {`${inputTokenCount}/${estimateTokenCount}`}
|
||||
</TextCount>
|
||||
</Container>
|
||||
@@ -258,6 +259,7 @@ const TextCount = styled.div`
|
||||
bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-3);
|
||||
z-index: 10;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
@@ -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} />
|
||||
@@ -105,7 +105,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
{message.usage && (
|
||||
<>
|
||||
<MessageMetadata>
|
||||
Tokens: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
|
||||
tokens: {message.usage.total_tokens} (in:{message.usage.prompt_tokens}/out:
|
||||
{message.usage.completion_tokens})
|
||||
</MessageMetadata>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
@@ -11,6 +11,7 @@ import LocalStorage from '@renderer/services/storage'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -18,10 +19,11 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
|
||||
const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||
const { showRightSidebar } = useShowRightSidebar()
|
||||
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
|
||||
const { assistant, removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(_assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const getTopicMenuItems = (topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
@@ -82,6 +84,14 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchTopic = (topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: showRightSidebar ? 'block' : 'none' }}>
|
||||
<TopicTitle>
|
||||
@@ -111,7 +121,7 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
|
||||
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||
<TopicListItem
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => setActiveTopic(topic)}>
|
||||
onClick={() => onSwitchTopic(topic)}>
|
||||
{topic.name}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Avatar, Button, Progress } from 'antd'
|
||||
import { Avatar, Button, Progress, Row, Tag } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Changelog from './components/Changelog'
|
||||
import { debounce } from 'lodash'
|
||||
import { ProgressInfo } from 'electron-updater'
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
|
||||
const AboutSettings: FC = () => {
|
||||
const [version, setVersion] = useState('')
|
||||
@@ -26,8 +26,17 @@ const AboutSettings: FC = () => {
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
|
||||
const onOpenWebsite = (suffix = '') => {
|
||||
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix)
|
||||
const onOpenWebsite = (url: string) => {
|
||||
window.api.openWebsite(url)
|
||||
}
|
||||
|
||||
const mailto = async () => {
|
||||
const email = 'kangfenmao@qq.com'
|
||||
const subject = 'Cherry Studio Feedback'
|
||||
const version = (await window.api.getAppInfo()).version
|
||||
const platform = window.electron.process.platform
|
||||
const url = `mailto:${email}?subject=${subject}&body=%0A%0AVersion: ${version} | Platform: ${platform}`
|
||||
onOpenWebsite(url)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,57 +78,92 @@ const AboutSettings: FC = () => {
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AvatarWrapper onClick={() => onOpenWebsite()}>
|
||||
{percent > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={104}
|
||||
percent={percent}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
/>
|
||||
)}
|
||||
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} />
|
||||
</AvatarWrapper>
|
||||
<Title>
|
||||
Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version>
|
||||
</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
|
||||
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
|
||||
</CheckUpdateButton>
|
||||
<Changelog />
|
||||
</Container>
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.about.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<AboutHeader>
|
||||
<Row align="middle">
|
||||
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
|
||||
{percent > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={84}
|
||||
percent={percent}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
/>
|
||||
)}
|
||||
<Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
|
||||
</AvatarWrapper>
|
||||
<VersionWrapper>
|
||||
<Title>Cherry Studio</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<Tag
|
||||
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
|
||||
color="cyan"
|
||||
style={{ marginTop: 8, cursor: 'pointer' }}>
|
||||
v{version}
|
||||
</Tag>
|
||||
</VersionWrapper>
|
||||
</Row>
|
||||
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
|
||||
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
|
||||
</CheckUpdateButton>
|
||||
</AboutHeader>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.releases.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}>
|
||||
{t('settings.about.releases.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}>
|
||||
{t('settings.about.website.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.feedback.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues')}>
|
||||
{t('settings.about.feedback.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
|
||||
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
const AboutHeader = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
padding-bottom: 50px;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
const VersionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const Version = styled.span`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const Description = styled.div`
|
||||
@@ -128,18 +172,17 @@ const Description = styled.div`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const CheckUpdateButton = styled(Button)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
const CheckUpdateButton = styled(Button)``
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
`
|
||||
|
||||
const ProgressCircle = styled(Progress)`
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
`
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import changelogEn from '@renderer/CHANGELOG.en.md?raw'
|
||||
import changelogZh from '@renderer/CHANGELOG.zh.md?raw'
|
||||
import { FC } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
import styles from './changelog.module.scss'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
const Changelog: FC = () => {
|
||||
const language = i18n.language
|
||||
const changelog = language === 'zh-CN' ? changelogZh : changelogEn
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Markdown className={styles.markdown}>{changelog}</Markdown>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
font-size: 14px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
width: 650px;
|
||||
`
|
||||
|
||||
export default Changelog
|
||||
@@ -1,79 +0,0 @@
|
||||
$background-color: #121212;
|
||||
$text-color: #ffffff;
|
||||
$heading-color: #00b96b;
|
||||
$link-color: #3498db;
|
||||
$code-background: #1e1e1e;
|
||||
$code-color: #f0e7db;
|
||||
|
||||
.markdown {
|
||||
body {
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: $heading-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $code-background;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: $code-background;
|
||||
color: $code-color;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid $heading-color;
|
||||
padding-left: 10px;
|
||||
margin-left: 0;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 20px;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export default class ProviderSDK {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要加标点符号'
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
if (this.isAnthropic) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
export interface AssistantsState {
|
||||
@@ -33,6 +33,16 @@ const assistantsSlice = createSlice({
|
||||
updateAssistant: (state, action: PayloadAction<Assistant>) => {
|
||||
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
|
||||
},
|
||||
updateAssistantSettings: (state, action: PayloadAction<{ assistantId: string; settings: AssistantSettings }>) => {
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
? {
|
||||
...assistant,
|
||||
settings: action.payload.settings
|
||||
}
|
||||
: assistant
|
||||
)
|
||||
},
|
||||
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
@@ -111,7 +121,8 @@ export const {
|
||||
updateTopic,
|
||||
updateTopics,
|
||||
removeAllTopics,
|
||||
setModel
|
||||
setModel,
|
||||
updateAssistantSettings
|
||||
} = assistantsSlice.actions
|
||||
|
||||
export default assistantsSlice.reducer
|
||||
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 13,
|
||||
version: 14,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('://')
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user