Compare commits

...

13 Commits

Author SHA1 Message Date
kangfenmao
be0799a4c6 chore(version): 0.5.6 2024-08-14 21:32:14 +08:00
kangfenmao
d0f5547419 feat: new windows and linux sidebar style 2024-08-14 21:28:44 +08:00
kangfenmao
076011b02b fix: anthropic message generating error 2024-08-14 20:35:57 +08:00
kangfenmao
ba5c70c45a feat: add minapp popup 2024-08-14 19:47:58 +08:00
kangfenmao
ebe74ffd05 chore(version): 0.5.5 2024-08-13 21:10:04 +08:00
kangfenmao
d0bea0491f fix(settings): provider list scroll 2024-08-13 21:04:04 +08:00
kangfenmao
514e1a4796 chore: remove ahooks 2024-08-13 20:50:54 +08:00
kangfenmao
2ffedadee4 Revert "feat(translate): use full screen input"
This reverts commit b0c479190c.
2024-08-13 20:48:51 +08:00
kangfenmao
7b72783ae7 feat: add graphrag provider 2024-08-13 20:48:38 +08:00
kangfenmao
4485a00395 feat: add doubao provider 2024-08-13 19:41:01 +08:00
kangfenmao
77c0952635 feat: add stepfun provider 2024-08-13 18:02:00 +08:00
kangfenmao
e1c7a25b87 feat: add gemini provider 2024-08-13 16:51:52 +08:00
kangfenmao
b0c479190c feat(translate): use full screen input 2024-08-13 14:57:46 +08:00
34 changed files with 718 additions and 143 deletions

View File

@@ -56,4 +56,6 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
错误修复,优化体验
增加 Gemini、豆包、阶跃星辰等服务商支持
修复 Anthropic 回复问题
新的 Windows, Linux 侧边栏样式

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.5.4",
"version": "0.5.6",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -37,6 +37,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.16.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
@@ -45,8 +46,8 @@
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"ahooks": "^3.8.0",
"antd": "^5.18.3",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"dotenv-cli": "^7.4.2",

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:; frame-src *" />
</head>
<body>
<div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Standard_product_icon__x28_1:1_x29_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="192px" height="192px"
viewBox="0 0 192 192" enable-background="new 0 0 192 192" xml:space="preserve">
<symbol id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" viewBox="-96 -96 192 192">
<g opacity="0.4">
<defs>
<path id="SVGID_1_" opacity="0.4" d="M-96,96V-96H96V96H-96z"/>
</defs>
<clipPath id="SVGID_00000071517564283228984050000017848131202901217410_">
<use xlink:href="#SVGID_1_" overflow="visible"/>
</clipPath>
<g clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)">
<g>
<path d="M95.75,95.75v-191.5h-191.5v191.5H95.75 M96,96H-96V-96H96V96L96,96z"/>
</g>
<circle fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="64"/>
</g>
<circle clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="88"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M64,76H-64c-6.6,0-12-5.4-12-12V-64c0-6.6,5.4-12,12-12H64c6.6,0,12,5.4,12,12V64C76,70.6,70.6,76,64,76z"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M52,88H-52c-6.6,0-12-5.4-12-12V-76c0-6.6,5.4-12,12-12H52c6.6,0,12,5.4,12,12V76C64,82.6,58.6,88,52,88z"/>
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
M76,64H-76c-6.6,0-12-5.4-12-12V-52c0-6.6,5.4-12,12-12H76c6.6,0,12,5.4,12,12V52C88,58.6,82.6,64,76,64z"/>
</g>
</symbol>
<rect id="bounding_box_1_" display="none" fill="none" width="192" height="192"/>
<g id="art_layer">
<g>
<path fill="#F9AB00" d="M96,181.92L96,181.92c6.63,0,12-5.37,12-12v-104H84v104C84,176.55,89.37,181.92,96,181.92z"/>
<g>
<path fill="#5BB974" d="M143.81,103.87C130.87,90.94,111.54,88.32,96,96l51.37,51.37c2.12,2.12,5.77,1.28,6.67-1.57
C158.56,131.49,155.15,115.22,143.81,103.87z"/>
</g>
<g>
<path fill="#129EAF" d="M48.19,103.87C61.13,90.94,80.46,88.32,96,96l-51.37,51.37c-2.12,2.12-5.77,1.28-6.67-1.57
C33.44,131.49,36.85,115.22,48.19,103.87z"/>
</g>
<g>
<path fill="#AF5CF7" d="M140,64c-20.44,0-37.79,13.4-44,32h81.24c3.33,0,5.55-3.52,4.04-6.49C173.56,74.36,157.98,64,140,64z"/>
</g>
<g>
<path fill="#FF8BCB" d="M104.49,42.26C90.03,56.72,87.24,78.45,96,96l57.45-57.45c2.36-2.36,1.44-6.42-1.73-7.45
C135.54,25.85,117.2,29.55,104.49,42.26z"/>
</g>
<g>
<path fill="#FA7B17" d="M87.51,42.26C101.97,56.72,104.76,78.45,96,96L38.55,38.55c-2.36-2.36-1.44-6.42,1.73-7.45
C56.46,25.85,74.8,29.55,87.51,42.26z"/>
</g>
<g>
<g>
<path fill="#4285F4" d="M52,64c20.44,0,37.79,13.4,44,32H14.76c-3.33,0-5.55-3.52-4.04-6.49C18.44,74.36,34.02,64,52,64z"/>
</g>
</g>
</g>
</g>
<g id="keylines" display="none">
<use xlink:href="#material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" width="192" height="192" id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines" x="-96" y="-96" transform="matrix(1 0 0 -1 96 96)" display="inline" overflow="visible"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -162,3 +162,40 @@ body,
.ant-segmented-group {
gap: 4px;
}
.dragable {
-webkit-app-region: drag;
}
.dragdisable {
-webkit-app-region: no-drag;
}
.minapp-drawer {
.ant-drawer-header-title {
flex-direction: row-reverse;
}
.ant-drawer-close {
position: absolute;
top: 6px;
right: 15px;
padding: 15px;
margin-right: -5px;
-webkit-app-region: no-drag;
z-index: 100000;
}
.ant-drawer-header {
height: calc(var(--navbar-height) + 0.5px);
background: var(--navbar-background);
width: calc(100vw - var(--sidebar-width));
padding-right: 10px !important;
border-bottom: 0.5px solid var(--color-border);
margin-top: -0.5px;
}
.ant-drawer-body {
padding: 0;
}
.minapp-mask {
background-color: transparent !important;
}
}

View File

@@ -0,0 +1,125 @@
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { Drawer } from 'antd'
import { useRef, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface ShowParams {
title: string
url: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
const [open, setOpen] = useState(true)
const iframeRef = useRef<HTMLIFrameElement>(null)
const onClose = () => {
setOpen(false)
setTimeout(() => resolve({}), 300)
}
const onReload = () => {
if (iframeRef.current) {
iframeRef.current.src = url
}
}
const onOpenLink = () => {
window.api.openWebsite(url)
}
return (
<Drawer
title={title}
placement="bottom"
onClose={onClose}
open={open}
mask={true}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
<Frame src={url} ref={iframeRef} />
<ButtonsGroup>
<Button onClick={onReload}>
<ReloadOutlined />
</Button>
<Button onClick={onOpenLink}>
<ExportOutlined />
</Button>
<Button onClick={onClose}>
<CloseOutlined />
</Button>
</ButtonsGroup>
</Drawer>
)
}
const Frame = styled.iframe`
width: calc(100vw - var(--sidebar-width));
height: calc(100vh - var(--navbar-height));
border: none;
`
const ButtonsGroup = styled.div`
position: absolute;
top: 0;
right: 0;
height: var(--navbar-height);
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
padding: 0 10px;
`
const Button = styled.div`
-webkit-app-region: no-drag;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
`
export default class MinApp {
static topviewId = 0
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
}
static start(props: ShowParams) {
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
}

View File

@@ -1,11 +1,19 @@
import { isMac } from '@renderer/config/constant'
import { useRuntime } from '@renderer/hooks/useStore'
import { FC, PropsWithChildren } from 'react'
import styled from 'styled-components'
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
export const Navbar: FC<Props> = ({ children, ...props }) => {
return <NavbarContainer {...props}>{children}</NavbarContainer>
const { minappShow } = useRuntime()
const backgroundColor = minappShow ? 'var(--color-background)' : 'var(--navbar-background)'
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
@@ -26,15 +34,16 @@ const NavbarContainer = styled.div`
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
-webkit-app-region: drag;
margin-left: calc(var(--sidebar-width) * -1);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
border-bottom: 0.5px solid var(--color-border);
background-color: var(--navbar-background);
transition: background-color 0.3s ease;
-webkit-app-region: drag;
`
const NavbarLeftContainer = styled.div`
min-width: ${isMac ? 'var(--assistants-width)' : 'calc(var(--sidebar-width) + var(--assistants-width))'};
min-width: var(--assistants-width);
padding: 0 10px;
display: flex;
flex-direction: row;
@@ -56,5 +65,5 @@ const NavbarRightContainer = styled.div`
min-width: var(--settings-width);
display: flex;
align-items: center;
padding: 0 16px;
padding: 0 12px;
`

View File

@@ -1,6 +1,9 @@
import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png'
import { isMac } from '@renderer/config/constant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useRuntime } from '@renderer/hooks/useStore'
import { Avatar } from 'antd'
import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
@@ -8,11 +11,12 @@ import styled from 'styled-components'
const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar()
const { minappShow } = useRuntime()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container>
<Container style={{ backgroundColor: minappShow ? 'var(--color-background)' : 'var(--sidebar-background)' }}>
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
@@ -52,21 +56,21 @@ const Container = styled.div`
align-items: center;
padding: 8px 0;
width: var(--sidebar-width);
height: calc(100vh - var(--navbar-height));
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
-webkit-app-region: drag !important;
border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height);
margin-bottom: var(--navbar-height);
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
background-color: var(--sidebar-background);
transition: background-color 0.3s ease;
`
const AvatarImg = styled.img`
border-radius: 50%;
const AvatarImg = styled(Avatar)`
width: 28px;
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: 5px;
margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: ${isMac ? '5px' : '2px'};
border: none;
`
const MainMenus = styled.div`
display: flex;

View File

@@ -33,6 +33,22 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true
}
],
gemini: [
{
id: 'gemini-1.5-flash',
provider: 'gemini',
name: 'Gemini 1.5 Flash',
group: 'Gemini 1.5',
enabled: true
},
{
id: 'gemini-1.5-pro-exp-0801',
provider: 'gemini',
name: 'Gemini 1.5 Pro Experimental 0801',
group: 'Gemini 1.5',
enabled: true
}
],
silicon: [
{
id: 'Qwen/Qwen2-7B-Instruct',
@@ -320,6 +336,23 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true
}
],
stepfun: [
{
id: 'step-1-8k',
provider: 'stepfun',
name: 'Step 1 8K',
group: 'Step 1',
enabled: true
},
{
id: 'step-1-flash',
provider: 'stepfun',
name: 'Step 1 Flash',
group: 'Step 1',
enabled: true
}
],
doubao: [],
aihubmix: [
{
id: 'gpt-4o-mini',

View File

@@ -3,24 +3,33 @@ import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import StepModelLogo from '@renderer/assets/images/models/step.jpg'
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
@@ -52,6 +61,14 @@ export function getProviderLogo(providerId: string) {
return AnthropicProviderLogo
case 'aihubmix':
return AiHubMixProviderLogo
case 'gemini':
return GeminiProviderLogo
case 'stepfun':
return StepFunProviderLogo
case 'doubao':
return DoubaoProviderLogo
case 'graphrag-kylin-mountain':
return GraphRagProviderLogo
default:
return undefined
}
@@ -75,7 +92,13 @@ export function getModelLogo(modelId: string) {
moonshot: MoonshotModelLogo,
phi: MicrosoftModelLogo,
baichuan: BaichuanModelLogo,
claude: ClaudeModelLogo
claude: ClaudeModelLogo,
gemini: GeminiModelLogo,
embedding: EmbeddingModelLogo,
bison: PalmModelLogo,
palm: PalmModelLogo,
step: StepModelLogo,
'ep-202': DoubaoModelLogo
}
for (const key in logoMap) {
@@ -100,6 +123,18 @@ export const PROVIDER_CONFIG = {
models: 'https://platform.openai.com/docs/models'
}
},
gemini: {
api: {
url: 'https://generativelanguage.googleapis.com',
editable: false
},
websites: {
official: 'https://gemini.google.com/',
apiKey: 'https://aistudio.google.com/app/apikey',
docs: 'https://ai.google.dev/gemini-api/docs',
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
}
},
silicon: {
api: {
url: 'https://cloud.siliconflow.cn',
@@ -184,6 +219,36 @@ export const PROVIDER_CONFIG = {
models: 'https://dashscope.console.aliyun.com/model'
}
},
stepfun: {
api: {
url: 'https://api.stepfun.com',
editable: false
},
websites: {
official: 'https://platform.stepfun.com/',
apiKey: 'https://platform.stepfun.com/interface-key',
docs: 'https://platform.stepfun.com/docs/overview/concept',
models: 'https://platform.stepfun.com/docs/llm/text'
}
},
doubao: {
api: {
url: 'https://ark.cn-beijing.volces.com/api/v3/',
editable: true
},
websites: {
official: 'https://console.volcengine.com/ark/',
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
docs: 'https://www.volcengine.com/docs/82379/1182403',
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
}
},
'graphrag-kylin-mountain': {
api: {
url: '',
editable: true
}
},
openrouter: {
api: {
url: 'https://openrouter.ai/api/v1/',

View File

@@ -16,3 +16,7 @@ export function useActiveTopic(assistant: Assistant) {
return { activeTopic, setActiveTopic }
}
export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
}

View File

@@ -106,6 +106,7 @@ const resources = {
},
provider: {
openai: 'OpenAI',
gemini: 'Gemini',
deepseek: 'DeepSeek',
moonshot: 'Moonshot',
silicon: 'SiliconFlow',
@@ -117,7 +118,10 @@ const resources = {
baichuan: 'Baichuan',
dashscope: 'DashScope',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
aihubmix: 'AiHubMix',
stepfun: 'StepFun',
doubao: 'Doubao',
'graphrag-kylin-mountain': 'GraphRAG'
},
settings: {
title: 'Settings',
@@ -323,6 +327,7 @@ const resources = {
},
provider: {
openai: 'OpenAI',
gemini: 'Gemini',
deepseek: '深度求索',
moonshot: '月之暗面',
silicon: '硅基流动',
@@ -334,7 +339,10 @@ const resources = {
baichuan: '百川',
dashscope: '阿里云灵积',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
aihubmix: 'AiHubMix',
stepfun: '阶跃星辰',
doubao: '豆包',
'graphrag-kylin-mountain': 'GraphRAG'
},
settings: {
title: '设置',

View File

@@ -51,7 +51,7 @@ const HomePage: FC = () => {
</NavbarLeft>
)}
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 12 }}>
<ThemeSwitch
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
@@ -120,7 +120,7 @@ export const NewButton = styled.div`
const ThemeSwitch = styled(Switch)`
-webkit-app-region: none;
margin-right: 8px;
margin-right: 10px;
.icon-theme {
font-size: 14px;
}

View File

@@ -67,9 +67,9 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const getUserName = useCallback(() => {
if (message.id === 'assistant') return assistant?.name
if (message.role === 'assistant') return upperFirst(message.modelId)
if (message.role === 'assistant') return upperFirst(model.name || model.id)
return userName || t('common.you')
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
}, [assistant?.name, message.id, message.role, model.id, model.name, t, userName])
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY

View File

@@ -1,5 +1,6 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { getTopic } from '@renderer/hooks/useTopic'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages'
@@ -50,9 +51,10 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
)
const autoRenameTopic = useCallback(async () => {
if (topic.name === t('chat.default.topic.name') && messages.length >= 2) {
const _topic = getTopic(assistant, topic.id)
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText })
summaryText && updateTopic({ ..._topic, name: summaryText })
}
}, [assistant, messages, topic, updateTopic])

View File

@@ -148,7 +148,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<HelpTextRow>
<HelpText>{t('settings.provider.docs_check')} </HelpText>
<HelpLink target="_blank" href={docsWebsite}>
{t(`provider.${provider.id}`)}
{t(`provider.${provider.id}`) + ' '}
{t('common.docs')}
</HelpLink>
<HelpText>{t('common.and')}</HelpText>

View File

@@ -105,10 +105,13 @@ const ProvidersList: FC = () => {
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={25} />}
{provider.isSystem && (
<Avatar shape="square" src={getProviderLogo(provider.id)} size={25} />
)}
{!provider.isSystem && (
<Avatar
size={25}
shape="square"
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
{getFirstCharacter(provider.name)}
</Avatar>
@@ -156,7 +159,6 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
padding: 8px;
overflow-y: auto;
`
@@ -164,6 +166,9 @@ const ProviderList = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
overflow: auto;
padding: 8px;
`
const ProviderListItem = styled.div`
@@ -200,7 +205,7 @@ const AddButtonWrapper = styled.div`
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10px 0;
padding: 10px 8px;
`
export default ProvidersList

View File

@@ -1,10 +1,12 @@
import Anthropic from '@anthropic-ai/sdk'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { GoogleGenerativeAI } from '@google/generative-ai'
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import { removeQuotes } from '@renderer/utils'
import { sum, takeRight } from 'lodash'
import axios from 'axios'
import { isEmpty, sum, takeRight } from 'lodash'
import OpenAI from 'openai'
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
@@ -15,6 +17,7 @@ export default class ProviderSDK {
provider: Provider
openaiSdk: OpenAI
anthropicSdk: Anthropic
geminiSdk: GoogleGenerativeAI
constructor(provider: Provider) {
this.provider = provider
@@ -22,12 +25,17 @@ export default class ProviderSDK {
const baseURL = host.endsWith('/') ? host : `${provider.apiHost}/v1/`
this.anthropicSdk = new Anthropic({ apiKey: provider.apiKey, baseURL })
this.openaiSdk = new OpenAI({ dangerouslyAllowBrowser: true, apiKey: provider.apiKey, baseURL })
this.geminiSdk = new GoogleGenerativeAI(provider.apiKey)
}
private get isAnthropic() {
return this.provider.id === 'anthropic'
}
private get isGemini() {
return this.provider.id === 'gemini'
}
private get keepAliveTime() {
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
}
@@ -42,49 +50,99 @@ export default class ProviderSDK {
const { contextCount, maxTokens } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
role: message.role,
content: message.content
}))
if (this.isAnthropic) {
await this.anthropicSdk.messages
.stream({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
temperature: assistant?.settings?.temperature
})
.on('text', (text) => onChunk({ text: text || '' }))
.on('finalMessage', (message) =>
onChunk({
usage: {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
}
return new Promise<void>((resolve, reject) => {
const stream = this.anthropicSdk.messages
.stream({
model: model.id,
messages: userMessages.filter(Boolean) as MessageParam[],
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
temperature: assistant?.settings?.temperature,
system: assistant.prompt,
stream: true
})
)
} else {
// @ts-ignore key is not typed
const stream = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true,
temperature: assistant?.settings?.temperature,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime
.on('text', (text) => {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
resolve()
return stream.controller.abort()
}
onChunk({ text })
})
.on('finalMessage', (message) => {
onChunk({
text: '',
usage: {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
}
})
resolve()
})
.on('error', (error) => reject(error))
})
for await (const chunk of stream) {
}
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id,
systemInstruction: assistant.prompt,
generationConfig: {
maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature
}
})
const userLastMessage = userMessages.pop()
const chat = geminiModel.startChat({
history: userMessages.map((message) => ({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
}))
})
const userMessagesStream = await chat.sendMessageStream(userLastMessage?.content!)
for await (const chunk of userMessagesStream.stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
onChunk({ text: chunk.choices[0]?.delta?.content || '', usage: chunk.usage })
onChunk({
text: chunk.text(),
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
}
})
}
return
}
// @ts-ignore key is not typed
const stream = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true,
temperature: assistant?.settings?.temperature,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime
})
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
onChunk({ text: chunk.choices[0]?.delta?.content || '', usage: chunk.usage })
}
}
public async translate(message: Message, assistant: Assistant) {
const defaultModel = getDefaultModel()
const { maxTokens } = getAssistantSettings(assistant)
const model = assistant.model || defaultModel
const messages = [
{ role: 'system', content: assistant.prompt },
@@ -94,29 +152,47 @@ export default class ProviderSDK {
if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({
model: model.id,
messages: messages as MessageParam[],
messages: messages.filter((m) => m.role === 'user') as MessageParam[],
max_tokens: 4096,
temperature: assistant?.settings?.temperature,
system: assistant.prompt,
stream: false
})
return response.content[0].type === 'text' ? response.content[0].text : ''
} else {
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false,
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
}
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id,
systemInstruction: assistant.prompt,
generationConfig: {
maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature
}
})
const { response } = await geminiModel.generateContent(message.content)
return response.text()
}
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false,
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
}
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({
role: 'user',
role: message.role,
content: message.content
}))
@@ -127,25 +203,49 @@ export default class ProviderSDK {
if (this.isAnthropic) {
const message = await this.anthropicSdk.messages.create({
messages: [systemMessage, ...userMessages] as Anthropic.Messages.MessageParam[],
messages: userMessages as Anthropic.Messages.MessageParam[],
model: model.id,
system: systemMessage.content,
stream: false,
max_tokens: 50
max_tokens: 4096
})
return message.content[0].type === 'text' ? message.content[0].text : null
} else {
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
}
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({
model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
stream: false,
max_tokens: 50,
keep_alive: this.keepAliveTime
systemInstruction: systemMessage.content,
generationConfig: {
temperature: assistant?.settings?.temperature
}
})
return removeQuotes(response.choices[0].message?.content || '')
const lastUserMessage = userMessages.pop()
const chat = await geminiModel.startChat({
history: userMessages.map((message) => ({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
}))
})
const { response } = await chat.sendMessage(lastUserMessage?.content!)
return response.text()
}
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
stream: false,
max_tokens: 50,
keep_alive: this.keepAliveTime
})
return removeQuotes(response.choices[0].message?.content || '')
}
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
@@ -172,6 +272,7 @@ export default class ProviderSDK {
public async check(): Promise<{ valid: boolean; error: Error | null }> {
const model = this.provider.models[0]
const body = {
model: model.id,
messages: [{ role: 'user', content: 'hi' }],
@@ -182,13 +283,32 @@ export default class ProviderSDK {
try {
if (this.isAnthropic) {
const message = await this.anthropicSdk.messages.create(body as MessageCreateParamsNonStreaming)
return { valid: message.content.length > 0, error: null }
} else {
const response = await this.openaiSdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
return { valid: Boolean(response?.choices[0].message), error: null }
return {
valid: message.content.length > 0,
error: null
}
}
if (this.isGemini) {
const geminiModel = this.geminiSdk.getGenerativeModel({ model: body.model })
const result = await geminiModel.generateContent(body.messages[0].content)
return {
valid: !isEmpty(result.response.text()),
error: null
}
}
const response = await this.openaiSdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
return {
valid: Boolean(response?.choices[0].message),
error: null
}
} catch (error: any) {
return { valid: false, error }
return {
valid: false,
error
}
}
}
@@ -198,6 +318,22 @@ export default class ProviderSDK {
return []
}
if (this.isGemini) {
const api = this.provider.apiHost + '/v1beta/models'
const { data } = await axios.get(api, { params: { key: this.provider.apiKey } })
return data.models.map(
(m: any) =>
({
id: m.name.replace('models/', ''),
name: m.displayName,
description: m.description,
object: 'model',
created: Date.now(),
owned_by: 'gemini'
}) as OpenAI.Models.Model
)
}
const response = await this.openaiSdk.models.list()
return response.data
} catch (error) {

View File

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

View File

@@ -31,6 +31,15 @@ const initialState: LlmState = {
isSystem: true,
enabled: true
},
{
id: 'gemini',
name: 'Gemini',
apiKey: '',
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'silicon',
name: 'Silicon',
@@ -95,11 +104,20 @@ const initialState: LlmState = {
enabled: false
},
{
id: 'anthropic',
name: 'Anthropic',
id: 'stepfun',
name: 'StepFun',
apiKey: '',
apiHost: 'https://api.anthropic.com/',
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
apiHost: 'https://api.stepfun.com',
models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'doubao',
name: 'doubao',
apiKey: '',
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
@@ -112,6 +130,15 @@ const initialState: LlmState = {
isSystem: true,
enabled: false
},
{
id: 'graphrag-kylin-mountain',
name: 'GraphRAG',
apiKey: '',
apiHost: '',
models: [],
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',
@@ -130,6 +157,15 @@ const initialState: LlmState = {
isSystem: true,
enabled: false
},
{
id: 'anthropic',
name: 'Anthropic',
apiKey: '',
apiHost: 'https://api.anthropic.com/',
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'ollama',
name: 'Ollama',

View File

@@ -296,6 +296,53 @@ const migrateConfig = {
fontSize: 14
}
}
},
'21': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: [
...state.llm.providers,
{
id: 'gemini',
name: 'Gemini',
apiKey: '',
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'stepfun',
name: 'StepFun',
apiKey: '',
apiHost: 'https://api.stepfun.com',
models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'doubao',
name: 'doubao',
apiKey: '',
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'graphrag-kylin-mountain',
name: 'GraphRAG',
apiKey: '',
apiHost: '',
models: [],
isSystem: true,
enabled: false
}
]
}
}
}
}

View File

@@ -4,11 +4,13 @@ import Logo from '@renderer/assets/images/logo.png'
export interface RuntimeState {
avatar: string
generating: boolean
minappShow: boolean
}
const initialState: RuntimeState = {
avatar: Logo,
generating: false
generating: false,
minappShow: false
}
const runtimeSlice = createSlice({
@@ -20,10 +22,13 @@ const runtimeSlice = createSlice({
},
setGenerating: (state, action: PayloadAction<boolean>) => {
state.generating = action.payload
},
setMinappShow: (state, action: PayloadAction<boolean>) => {
state.minappShow = action.payload
}
}
})
export const { setAvatar, setGenerating } = runtimeSlice.actions
export const { setAvatar, setGenerating, setMinappShow } = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@@ -961,6 +961,13 @@ __metadata:
languageName: node
linkType: hard
"@google/generative-ai@npm:^0.16.0":
version: 0.16.0
resolution: "@google/generative-ai@npm:0.16.0"
checksum: 10c0/5d561a41cb7be60fc9b49965b66359e15df907bf6679009de7917beff138ba69d4a0772ab2a9d6f0e543d658d72bd19b83e6abdb87a6cdfa402a8764b08eed4c
languageName: node
linkType: hard
"@hello-pangea/dnd@npm:^16.6.0":
version: 16.6.0
resolution: "@hello-pangea/dnd@npm:16.6.0"
@@ -2698,25 +2705,6 @@ __metadata:
languageName: node
linkType: hard
"ahooks@npm:^3.8.0":
version: 3.8.0
resolution: "ahooks@npm:3.8.0"
dependencies:
"@babel/runtime": "npm:^7.21.0"
dayjs: "npm:^1.9.1"
intersection-observer: "npm:^0.12.0"
js-cookie: "npm:^2.x.x"
lodash: "npm:^4.17.21"
react-fast-compare: "npm:^3.2.2"
resize-observer-polyfill: "npm:^1.5.1"
screenfull: "npm:^5.0.0"
tslib: "npm:^2.4.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/28344a10443a1374066d931ac687e8f18f714717c32fc4c12b5fea1d0b29b9dfbddd5e9943df2394dd7912440e474413d899e1a2877bf80093fbe8f1c3b5ea58
languageName: node
linkType: hard
"ajv-formats@npm:^2.1.1":
version: 2.1.1
resolution: "ajv-formats@npm:2.1.1"
@@ -3099,6 +3087,17 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.3":
version: 1.7.3
resolution: "axios@npm:1.7.3"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/a18cbe559203efa05fb1fec2d1898e23bf6329bd2575784ee32aa11b5bbe1d54b9f472c49a261294125519cf62aa4fe5ef6e647bb7482eafc15bffe15ab314ce
languageName: node
linkType: hard
"bail@npm:^2.0.0":
version: 2.0.2
resolution: "bail@npm:2.0.2"
@@ -3446,6 +3445,7 @@ __metadata:
"@electron-toolkit/preload": "npm:^3.0.0"
"@electron-toolkit/tsconfig": "npm:^1.0.1"
"@electron-toolkit/utils": "npm:^3.0.0"
"@google/generative-ai": "npm:^0.16.0"
"@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0"
"@reduxjs/toolkit": "npm:^2.2.5"
@@ -3455,8 +3455,8 @@ __metadata:
"@types/react": "npm:^18.2.48"
"@types/react-dom": "npm:^18.2.18"
"@vitejs/plugin-react": "npm:^4.2.1"
ahooks: "npm:^3.8.0"
antd: "npm:^5.18.3"
axios: "npm:^1.7.3"
browser-image-compression: "npm:^2.0.2"
dayjs: "npm:^1.11.11"
dotenv-cli: "npm:^7.4.2"
@@ -3831,7 +3831,7 @@ __metadata:
languageName: node
linkType: hard
"dayjs@npm:^1.11.11, dayjs@npm:^1.9.1":
"dayjs@npm:^1.11.11":
version: 1.11.11
resolution: "dayjs@npm:1.11.11"
checksum: 10c0/0131d10516b9945f05a57e13f4af49a6814de5573a494824e103131a3bbe4cc470b1aefe8e17e51f9a478a22cd116084be1ee5725cedb66ec4c3f9091202dc4b
@@ -5037,6 +5037,16 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.6
resolution: "follow-redirects@npm:1.15.6"
peerDependenciesMeta:
debug:
optional: true
checksum: 10c0/9ff767f0d7be6aa6870c82ac79cf0368cd73e01bbc00e9eb1c2a16fbb198ec105e3c9b6628bb98e9f3ac66fe29a957b9645bcb9a490bb7aa0d35f908b6b85071
languageName: node
linkType: hard
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
@@ -5876,13 +5886,6 @@ __metadata:
languageName: node
linkType: hard
"intersection-observer@npm:^0.12.0":
version: 0.12.2
resolution: "intersection-observer@npm:0.12.2"
checksum: 10c0/9591f46b2b742f5801ed69dbc8860f487771b4af8361e7a5dcb28a377beff2ba56336a2b090af261825430d225dae9417121496d2e6925e000e4a469958843ff
languageName: node
linkType: hard
"ip-address@npm:^9.0.5":
version: 9.0.5
resolution: "ip-address@npm:9.0.5"
@@ -6320,13 +6323,6 @@ __metadata:
languageName: node
linkType: hard
"js-cookie@npm:^2.x.x":
version: 2.2.1
resolution: "js-cookie@npm:2.2.1"
checksum: 10c0/ee67fc0f8495d0800b851910b5eb5bf49d3033adff6493d55b5c097ca6da46f7fe666b10e2ecb13cfcaf5b88d71c205ce00a7e646de791689bfd053bbb36a376
languageName: node
linkType: hard
"js-tiktoken@npm:^1.0.10, js-tiktoken@npm:^1.0.7":
version: 1.0.12
resolution: "js-tiktoken@npm:1.0.12"
@@ -8251,6 +8247,13 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"pump@npm:^3.0.0":
version: 3.0.0
resolution: "pump@npm:3.0.0"
@@ -8844,13 +8847,6 @@ __metadata:
languageName: node
linkType: hard
"react-fast-compare@npm:^3.2.2":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
languageName: node
linkType: hard
"react-i18next@npm:^14.1.2":
version: 14.1.2
resolution: "react-i18next@npm:14.1.2"
@@ -9514,13 +9510,6 @@ __metadata:
languageName: node
linkType: hard
"screenfull@npm:^5.0.0":
version: 5.2.0
resolution: "screenfull@npm:5.2.0"
checksum: 10c0/86fd49983e2edc153ee2e674a570c711cb0961a9cacca659309f79636ccc8ca8a0b830ea4dacdae7403a8bb7ba6affd5bcdce053aa97782961247a49bfd2ba68
languageName: node
linkType: hard
"scroll-into-view-if-needed@npm:^3.1.0":
version: 3.1.0
resolution: "scroll-into-view-if-needed@npm:3.1.0"
@@ -10106,7 +10095,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.1.0, tslib@npm:^2.4.1":
"tslib@npm:^2.1.0":
version: 2.6.3
resolution: "tslib@npm:2.6.3"
checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a