mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 23:12:38 +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 {
|
||||
CircleHelp,
|
||||
Code,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Languages,
|
||||
@ -153,7 +154,8 @@ const MainMenus: FC = () => {
|
||||
translate: <Languages size={18} className="icon" />,
|
||||
minapp: <LayoutGrid 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 = {
|
||||
@ -163,7 +165,8 @@ const MainMenus: FC = () => {
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files'
|
||||
files: '/files',
|
||||
code_tools: '/code'
|
||||
}
|
||||
|
||||
return sidebarIcons.visible.map((icon) => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||
@ -22,6 +23,8 @@ const CLI_TOOLS = [
|
||||
{ value: 'gemini-cli', label: 'Gemini CLI' }
|
||||
]
|
||||
|
||||
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
||||
|
||||
const logger = loggerService.withContext('CodeToolsPage')
|
||||
|
||||
const CodeToolsPage: FC = () => {
|
||||
@ -54,12 +57,23 @@ const CodeToolsPage: FC = () => {
|
||||
}
|
||||
|
||||
const openAiProviders = providers.filter((p) => p.type.includes('openai'))
|
||||
const geminiProviders = providers.filter((p) => p.type === 'gemini')
|
||||
const claudeProviders = providers.filter((p) => p.type === 'anthropic')
|
||||
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
|
||||
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
|
||||
|
||||
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 =
|
||||
@ -176,13 +190,19 @@ const CodeToolsPage: FC = () => {
|
||||
if (selectedCliTool === 'claude-code') {
|
||||
env = {
|
||||
ANTHROPIC_API_KEY: apiKey,
|
||||
ANTHROPIC_BASE_URL: modelProvider.apiHost,
|
||||
ANTHROPIC_MODEL: selectedModel.id
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCliTool === 'gemini-cli') {
|
||||
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
|
||||
const apiBaseUrl = modelProvider.apiHost + apiSuffix
|
||||
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 (
|
||||
<Container>
|
||||
<Title>{t('code.title')}</Title>
|
||||
<Description>{t('code.description')}</Description>
|
||||
<Navbar>
|
||||
<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 安装状态提示 */}
|
||||
{!isBunInstalled && (
|
||||
<BunInstallAlert>
|
||||
<Alert
|
||||
type="warning"
|
||||
banner
|
||||
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
||||
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: (
|
||||
{/* Bun 安装状态提示 */}
|
||||
{!isBunInstalled && (
|
||||
<BunInstallAlert>
|
||||
<Alert
|
||||
type="warning"
|
||||
banner
|
||||
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
||||
message={
|
||||
<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)}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
|
||||
{t('code.select_folder')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</SettingsItem>
|
||||
}
|
||||
/>
|
||||
</BunInstallAlert>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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' }}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
// 样式组件
|
||||
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;
|
||||
margin: auto;
|
||||
`
|
||||
@ -347,7 +384,6 @@ const Title = styled.h1`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
margin-top: -50px;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@ -54,7 +54,7 @@ const LaunchpadPage: FC = () => {
|
||||
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
|
||||
},
|
||||
{
|
||||
icon: <Terminal size={32} className="icon" />,
|
||||
icon: <Code size={32} className="icon" />,
|
||||
text: t('title.code'),
|
||||
path: '/code',
|
||||
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI:高级暗黑色,代表专业和技术
|
||||
|
||||
@ -62,7 +62,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 132,
|
||||
version: 133,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -2119,6 +2119,15 @@ const migrateConfig = {
|
||||
logger.error('migrate 132 error', error as Error)
|
||||
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 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[] = [
|
||||
'assistants',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user