mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
feat(Sidebar): add 'code_tools' icon and route; enhance CodeToolsPage layout with Navbar and improved provider filtering
This commit is contained in:
parent
e0bc3bb2c5
commit
29d4e37f6b
@ -16,6 +16,7 @@ import { isEmoji } from '@renderer/utils'
|
|||||||
import { Avatar, Tooltip } from 'antd'
|
import { Avatar, Tooltip } from 'antd'
|
||||||
import {
|
import {
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Code,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
Folder,
|
Folder,
|
||||||
Languages,
|
Languages,
|
||||||
@ -153,7 +154,8 @@ const MainMenus: FC = () => {
|
|||||||
translate: <Languages size={18} className="icon" />,
|
translate: <Languages size={18} className="icon" />,
|
||||||
minapp: <LayoutGrid size={18} className="icon" />,
|
minapp: <LayoutGrid size={18} className="icon" />,
|
||||||
knowledge: <FileSearch size={18} className="icon" />,
|
knowledge: <FileSearch size={18} className="icon" />,
|
||||||
files: <Folder size={17} className="icon" />
|
files: <Folder size={17} className="icon" />,
|
||||||
|
code_tools: <Code size={18} className="icon" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathMap = {
|
const pathMap = {
|
||||||
@ -163,7 +165,8 @@ const MainMenus: FC = () => {
|
|||||||
translate: '/translate',
|
translate: '/translate',
|
||||||
minapp: '/apps',
|
minapp: '/apps',
|
||||||
knowledge: '/knowledge',
|
knowledge: '/knowledge',
|
||||||
files: '/files'
|
files: '/files',
|
||||||
|
code_tools: '/code'
|
||||||
}
|
}
|
||||||
|
|
||||||
return sidebarIcons.visible.map((icon) => {
|
return sidebarIcons.visible.map((icon) => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AiProvider from '@renderer/aiCore'
|
import AiProvider from '@renderer/aiCore'
|
||||||
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import ModelSelector from '@renderer/components/ModelSelector'
|
import ModelSelector from '@renderer/components/ModelSelector'
|
||||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||||
@ -22,6 +23,8 @@ const CLI_TOOLS = [
|
|||||||
{ value: 'gemini-cli', label: 'Gemini CLI' }
|
{ value: 'gemini-cli', label: 'Gemini CLI' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
||||||
|
|
||||||
const logger = loggerService.withContext('CodeToolsPage')
|
const logger = loggerService.withContext('CodeToolsPage')
|
||||||
|
|
||||||
const CodeToolsPage: FC = () => {
|
const CodeToolsPage: FC = () => {
|
||||||
@ -54,12 +57,23 @@ const CodeToolsPage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAiProviders = providers.filter((p) => p.type.includes('openai'))
|
const openAiProviders = providers.filter((p) => p.type.includes('openai'))
|
||||||
const geminiProviders = providers.filter((p) => p.type === 'gemini')
|
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
|
||||||
const claudeProviders = providers.filter((p) => p.type === 'anthropic')
|
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
|
||||||
|
|
||||||
const modelPredicate = useCallback(
|
const modelPredicate = useCallback(
|
||||||
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
|
(m: Model) => {
|
||||||
[]
|
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (selectedCliTool === 'claude-code') {
|
||||||
|
return m.id.includes('claude')
|
||||||
|
}
|
||||||
|
if (selectedCliTool === 'gemini-cli') {
|
||||||
|
return m.id.includes('gemini')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
[selectedCliTool]
|
||||||
)
|
)
|
||||||
|
|
||||||
const availableProviders =
|
const availableProviders =
|
||||||
@ -176,13 +190,19 @@ const CodeToolsPage: FC = () => {
|
|||||||
if (selectedCliTool === 'claude-code') {
|
if (selectedCliTool === 'claude-code') {
|
||||||
env = {
|
env = {
|
||||||
ANTHROPIC_API_KEY: apiKey,
|
ANTHROPIC_API_KEY: apiKey,
|
||||||
|
ANTHROPIC_BASE_URL: modelProvider.apiHost,
|
||||||
ANTHROPIC_MODEL: selectedModel.id
|
ANTHROPIC_MODEL: selectedModel.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCliTool === 'gemini-cli') {
|
if (selectedCliTool === 'gemini-cli') {
|
||||||
|
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
|
||||||
|
const apiBaseUrl = modelProvider.apiHost + apiSuffix
|
||||||
env = {
|
env = {
|
||||||
GEMINI_API_KEY: apiKey
|
GEMINI_API_KEY: apiKey,
|
||||||
|
GEMINI_BASE_URL: apiBaseUrl,
|
||||||
|
GOOGLE_GEMINI_BASE_URL: apiBaseUrl,
|
||||||
|
GEMINI_MODEL: selectedModel.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,117 +248,134 @@ const CodeToolsPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Title>{t('code.title')}</Title>
|
<Navbar>
|
||||||
<Description>{t('code.description')}</Description>
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('code.title')}</NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer id="content-container">
|
||||||
|
<MainContent>
|
||||||
|
<Title>{t('code.title')}</Title>
|
||||||
|
<Description>{t('code.description')}</Description>
|
||||||
|
|
||||||
{/* Bun 安装状态提示 */}
|
{/* Bun 安装状态提示 */}
|
||||||
{!isBunInstalled && (
|
{!isBunInstalled && (
|
||||||
<BunInstallAlert>
|
<BunInstallAlert>
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
banner
|
banner
|
||||||
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
||||||
message={
|
message={
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span>{t('code.bun_required_message')}</span>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
icon={<Download size={14} />}
|
|
||||||
onClick={handleInstallBun}
|
|
||||||
loading={isInstallingBun}
|
|
||||||
disabled={isInstallingBun}>
|
|
||||||
{isInstallingBun ? t('code.installing_bun') : t('code.install_bun')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</BunInstallAlert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SettingsPanel>
|
|
||||||
<SettingsItem>
|
|
||||||
<div className="settings-label">{t('code.cli_tool')}</div>
|
|
||||||
<Select
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={t('code.cli_tool_placeholder')}
|
|
||||||
value={selectedCliTool}
|
|
||||||
onChange={handleCliToolChange}
|
|
||||||
options={CLI_TOOLS}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
|
|
||||||
<SettingsItem>
|
|
||||||
<div className="settings-label">{t('code.model')}</div>
|
|
||||||
<ModelSelector
|
|
||||||
providers={availableProviders}
|
|
||||||
predicate={modelPredicate}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={t('code.model_placeholder')}
|
|
||||||
value={selectedModel ? getModelUniqId(selectedModel) : undefined}
|
|
||||||
onChange={handleModelChange}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
|
|
||||||
<SettingsItem>
|
|
||||||
<div className="settings-label">{t('code.working_directory')}</div>
|
|
||||||
<Space.Compact style={{ width: '100%', display: 'flex' }}>
|
|
||||||
<Select
|
|
||||||
style={{ flex: 1, width: 480 }}
|
|
||||||
placeholder={t('code.folder_placeholder')}
|
|
||||||
value={currentDirectory || undefined}
|
|
||||||
onChange={handleDirectoryChange}
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
filterOption={(input, option) => {
|
|
||||||
const label = typeof option?.label === 'string' ? option.label : String(option?.value || '')
|
|
||||||
return label.toLowerCase().includes(input.toLowerCase())
|
|
||||||
}}
|
|
||||||
options={directories.map((dir) => ({
|
|
||||||
value: dir,
|
|
||||||
label: (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{dir}</span>
|
<span>{t('code.bun_required_message')}</span>
|
||||||
<X
|
<Button
|
||||||
size={14}
|
type="primary"
|
||||||
style={{ marginLeft: 8, cursor: 'pointer', color: '#999' }}
|
size="small"
|
||||||
onClick={(e) => handleRemoveDirectory(dir, e)}
|
icon={<Download size={14} />}
|
||||||
/>
|
onClick={handleInstallBun}
|
||||||
|
loading={isInstallingBun}
|
||||||
|
disabled={isInstallingBun}>
|
||||||
|
{isInstallingBun ? t('code.installing_bun') : t('code.install_bun')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
}
|
||||||
}))}
|
/>
|
||||||
/>
|
</BunInstallAlert>
|
||||||
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
|
)}
|
||||||
{t('code.select_folder')}
|
|
||||||
</Button>
|
|
||||||
</Space.Compact>
|
|
||||||
</SettingsItem>
|
|
||||||
|
|
||||||
<SettingsItem>
|
<SettingsPanel>
|
||||||
<div className="settings-label">{t('code.update_options')}</div>
|
<SettingsItem>
|
||||||
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>
|
<div className="settings-label">{t('code.cli_tool')}</div>
|
||||||
{t('code.auto_update_to_latest')}
|
<Select
|
||||||
</Checkbox>
|
style={{ width: '100%' }}
|
||||||
</SettingsItem>
|
placeholder={t('code.cli_tool_placeholder')}
|
||||||
</SettingsPanel>
|
value={selectedCliTool}
|
||||||
|
onChange={handleCliToolChange}
|
||||||
|
options={CLI_TOOLS}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
<Button
|
<SettingsItem>
|
||||||
type="primary"
|
<div className="settings-label">{t('code.model')}</div>
|
||||||
icon={<Terminal size={16} />}
|
<ModelSelector
|
||||||
size="large"
|
providers={availableProviders}
|
||||||
onClick={handleLaunch}
|
predicate={modelPredicate}
|
||||||
loading={isLaunching}
|
style={{ width: '100%' }}
|
||||||
disabled={!canLaunch || !isBunInstalled}
|
placeholder={t('code.model_placeholder')}
|
||||||
block>
|
value={selectedModel ? getModelUniqId(selectedModel) : undefined}
|
||||||
{isLaunching ? t('code.launching') : t('code.launch.label')}
|
onChange={handleModelChange}
|
||||||
</Button>
|
allowClear
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem>
|
||||||
|
<div className="settings-label">{t('code.working_directory')}</div>
|
||||||
|
<Space.Compact style={{ width: '100%', display: 'flex' }}>
|
||||||
|
<Select
|
||||||
|
style={{ flex: 1, width: 480 }}
|
||||||
|
placeholder={t('code.folder_placeholder')}
|
||||||
|
value={currentDirectory || undefined}
|
||||||
|
onChange={handleDirectoryChange}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
filterOption={(input, option) => {
|
||||||
|
const label = typeof option?.label === 'string' ? option.label : String(option?.value || '')
|
||||||
|
return label.toLowerCase().includes(input.toLowerCase())
|
||||||
|
}}
|
||||||
|
options={directories.map((dir) => ({
|
||||||
|
value: dir,
|
||||||
|
label: (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{dir}</span>
|
||||||
|
<X
|
||||||
|
size={14}
|
||||||
|
style={{ marginLeft: 8, cursor: 'pointer', color: '#999' }}
|
||||||
|
onClick={(e) => handleRemoveDirectory(dir, e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
|
||||||
|
{t('code.select_folder')}
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem>
|
||||||
|
<div className="settings-label">{t('code.update_options')}</div>
|
||||||
|
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>
|
||||||
|
{t('code.auto_update_to_latest')}
|
||||||
|
</Checkbox>
|
||||||
|
</SettingsItem>
|
||||||
|
</SettingsPanel>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Terminal size={16} />}
|
||||||
|
size="large"
|
||||||
|
onClick={handleLaunch}
|
||||||
|
loading={isLaunching}
|
||||||
|
disabled={!canLaunch || !isBunInstalled}
|
||||||
|
block>
|
||||||
|
{isLaunching ? t('code.launching') : t('code.launch.label')}
|
||||||
|
</Button>
|
||||||
|
</MainContent>
|
||||||
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 样式组件
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MainContent = styled.div`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
`
|
`
|
||||||
@ -347,7 +384,6 @@ const Title = styled.h1`
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
margin-top: -50px;
|
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
|
|||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import tabsService from '@renderer/services/TabsService'
|
import tabsService from '@renderer/services/TabsService'
|
||||||
import { FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle, Terminal } from 'lucide-react'
|
import { Code, FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle } from 'lucide-react'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
@ -54,7 +54,7 @@ const LaunchpadPage: FC = () => {
|
|||||||
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
|
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Terminal size={32} className="icon" />,
|
icon: <Code size={32} className="icon" />,
|
||||||
text: t('title.code'),
|
text: t('title.code'),
|
||||||
path: '/code',
|
path: '/code',
|
||||||
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI:高级暗黑色,代表专业和技术
|
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI:高级暗黑色,代表专业和技术
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 132,
|
version: 133,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2119,6 +2119,15 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 132 error', error as Error)
|
logger.error('migrate 132 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'133': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
state.settings.sidebarIcons.visible.push('code_tools')
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 133 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,15 @@ import { RemoteSyncState } from './backup'
|
|||||||
|
|
||||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter'
|
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter'
|
||||||
|
|
||||||
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
export type SidebarIcon =
|
||||||
|
| 'assistants'
|
||||||
|
| 'agents'
|
||||||
|
| 'paintings'
|
||||||
|
| 'translate'
|
||||||
|
| 'minapp'
|
||||||
|
| 'knowledge'
|
||||||
|
| 'files'
|
||||||
|
| 'code_tools'
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||||
'assistants',
|
'assistants',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user