Merge branch 'main' into feat/agents-new

This commit is contained in:
kangfenmao 2025-09-25 15:02:52 +08:00
commit 3088887e57
44 changed files with 372 additions and 177 deletions

22
.github/workflows/delete-branch.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Delete merged branch
on:
pull_request:
types:
- closed
jobs:
delete-branch:
runs-on: ubuntu-latest
permissions:
contents: write
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Delete merged branch
uses: actions/github-script@v7
with:
script: |
github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${context.payload.pull_request.head.ref}`,
})

View File

@ -10,12 +10,14 @@ on:
- main
- develop
- v2
types: [ready_for_review, synchronize, opened]
jobs:
build:
runs-on: ubuntu-latest
env:
PRCI: true
if: github.event.pull_request.draft == false
steps:
- name: Check out Git repository

View File

@ -125,16 +125,30 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
✨ 新功能:
- 新增 CherryIN 服务商
- 新增 AiOnly AI 服务商
- 更新 MCP 服务器卡片布局和样式,改为列表视图
🚀 New Features:
- Refactored AI core engine for more efficient and stable content generation
- Added support for multiple AI model providers: CherryIN, AiOnly
- Added API server functionality for external application integration
- Added PaddleOCR document recognition for enhanced document processing
- Added Anthropic OAuth authentication support
- Added data storage space limit notifications
- Added font settings for global and code fonts customization
- Added auto-copy feature after translation completion
- Added keyboard shortcuts: rename topic, edit last message, etc.
- Added text attachment preview for viewing file contents in messages
- Added custom window control buttons (minimize, maximize, close)
- Support for Qwen long-text (qwen-long) and document analysis (qwen-doc) models with native file uploads
- Support for Qwen image recognition models (Qwen-Image)
- Added iFlow CLI support
- Converted knowledge base and web search to tool-calling approach for better flexibility
🐛 问题修复:
- 修复 QwenMT 模型的翻译内容处理逻辑
- 修复无法将外部笔记添加到知识库的问题
🎨 UI Improvements & Bug Fixes:
- Integrated HeroUI and Tailwind CSS framework
- Optimized message notification styles with unified toast component
- Moved free models to bottom with fixed position for easier access
- Refactored quick panel and input bar tools for smoother operation
- Optimized responsive design for navbar and sidebar
- Improved scrollbar component with horizontal scrolling support
- Fixed multiple translation issues: paste handling, file processing, state management
- Various UI optimizations and bug fixes
🚀 性能优化:
- 提升输入框响应速度
- 优化模型切换性能
- 改进翻译功能的引用和邮件格式处理

View File

@ -185,7 +185,7 @@ export class AiSdkToChunkAdapter {
case 'reasoning-end':
this.onChunk({
type: ChunkType.THINKING_COMPLETE,
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || '',
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || final.reasoningContent,
thinking_millsec: (chunk.providerMetadata?.metadata?.thinking_millsec as number) || 0
})
final.reasoningContent = ''

View File

@ -1,4 +1,5 @@
import { loggerService } from '@logger'
import { isNewApiProvider } from '@renderer/config/providers'
import { Provider } from '@renderer/types'
import { AihubmixAPIClient } from './aihubmix/AihubmixAPIClient'
@ -45,7 +46,7 @@ export class ApiClientFactory {
return instance
}
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
logger.debug(`Creating NewAPIClient for provider: ${provider.id}`)
instance = new NewAPIClient(provider) as BaseApiClient
return instance

View File

@ -67,7 +67,9 @@ vi.mock('@renderer/config/models', () => ({
silicon: [],
defaultModel: []
},
isOpenAIModel: vi.fn(() => false)
isOpenAIModel: vi.fn(() => false),
glm45FlashModel: {},
qwen38bModel: {}
}))
describe('ApiClientFactory', () => {

View File

@ -35,18 +35,8 @@ vi.mock('@renderer/config/models', () => ({
findTokenLimit: vi.fn().mockReturnValue(4096),
isFunctionCallingModel: vi.fn().mockReturnValue(false),
DEFAULT_MAX_TOKENS: 4096,
qwen38bModel: {
id: 'Qwen/Qwen3-8B',
name: 'Qwen3-8B',
provider: 'cherryai',
group: 'Qwen'
},
glm45FlashModel: {
id: 'glm-4.5-flash',
name: 'GLM-4.5-Flash',
provider: 'cherryai',
group: 'GLM-4.5'
}
qwen38bModel: {},
glm45FlashModel: {}
}))
vi.mock('@renderer/services/AssistantService', () => ({

View File

@ -143,12 +143,14 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
const tagName = {
reasoning: 'reasoning',
think: 'think',
thought: 'thought'
thought: 'thought',
seedThink: 'seed:think'
}
function getReasoningTagName(modelId: string | undefined): string {
if (modelId?.includes('gpt-oss')) return tagName.reasoning
if (modelId?.includes('gemini')) return tagName.thought
if (modelId?.includes('seed-oss-36b')) return tagName.seedThink
return tagName.think
}

View File

@ -6,6 +6,7 @@ import {
type ProviderSettingsMap
} from '@cherrystudio/ai-core/provider'
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockRegion,
@ -65,7 +66,7 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
if (provider.id === 'aihubmix') {
return aihubmixProviderCreator(model, provider)
}
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider)
}
if (provider.id === 'vertexai') {

View File

@ -52,7 +52,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
return {}
}
// Don't disable reasoning for models that require it
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model)) {
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model) || model.id.includes('seed-oss')) {
return {}
}
return { reasoning: { enabled: false, exclude: true } }
@ -112,6 +112,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
return {
enable_thinking: true
}
case SystemProviderIds.hunyuan:
case SystemProviderIds['tencent-cloud-ti']:
case SystemProviderIds.doubao:
return {
thinking: {

View File

@ -1,7 +1,6 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { MinAppType } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
@ -11,31 +10,52 @@ interface Props {
}
const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => {
// First try to find in DEFAULT_MIN_APPS for predefined styling
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) {
return null
// If found in DEFAULT_MIN_APPS, use predefined styling
if (_app) {
return (
<img
src={_app.logo}
className="select-none rounded-2xl"
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
userSelect: 'none',
...(sidebar ? {} : app.style),
...style
}}
draggable={false}
alt={app.name || 'MinApp Icon'}
/>
)
}
return (
<Container
src={_app.logo}
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...(sidebar ? {} : app.style),
...style
}}
/>
)
// If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
if (app.logo) {
return (
<img
src={app.logo}
className="select-none rounded-2xl"
style={{
border: 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: 'transparent',
userSelect: 'none',
...(sidebar ? {} : app.style),
...style
}}
draggable={false}
alt={app.name || 'MinApp Icon'}
/>
)
}
return null
}
const Container = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
export default MinAppIcon

View File

@ -1,15 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MinAppIcon > should render correctly with various props 1`] = `
.c0 {
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
}
<img
class="c0"
alt="Test App"
class="select-none rounded-2xl"
draggable="false"
src="/test-logo-1.png"
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); user-select: none; opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
/>
`;

View File

@ -180,7 +180,7 @@ const PopupContainer: React.FC<Props> = ({ model, filter: baseFilter, showTagFil
key: `provider-${p.id}`,
type: 'group',
name: getFancyProviderName(p),
actions: (
actions: p.id !== 'cherryai' && (
<Tooltip title={t('navigate.provider_settings')} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
<Settings2
size={12}

View File

@ -1,4 +1,5 @@
import { PlusOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { isMac } from '@renderer/config/constant'
@ -12,9 +13,10 @@ import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import { ThemeMode } from '@renderer/types'
import { MinAppType, ThemeMode } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Tooltip } from 'antd'
import { LRUCache } from 'lru-cache'
import {
FileSearch,
Folder,
@ -45,14 +47,40 @@ interface TabsContainerProps {
children: React.ReactNode
}
const getTabIcon = (tabId: string, minapps: any[]): React.ReactNode | undefined => {
const logger = loggerService.withContext('TabContainer')
const getTabIcon = (
tabId: string,
minapps: MinAppType[],
minAppsCache?: LRUCache<string, MinAppType>
): React.ReactNode | undefined => {
// Check if it's a minapp tab (format: apps:appId)
if (tabId.startsWith('apps:')) {
const appId = tabId.replace('apps:', '')
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// If not found in permanent apps, search in temporary apps cache
// The cache stores apps opened via openSmartMinapp() for top navbar mode
// These are temporary MinApps that were opened but not yet saved to user's config
// The cache is LRU (Least Recently Used) with max size from settings
// Cache validity: Apps in cache are currently active/recently used, not outdated
if (!app && minAppsCache) {
app = minAppsCache.get(appId)
// Defensive programming: If app not found in cache but tab exists,
// the cache entry may have been evicted due to LRU policy
// Log warning for debugging potential sync issues
if (!app) {
logger.warn(`MinApp ${appId} not found in cache, using fallback icon`)
}
}
if (app) {
return <MinAppIcon size={14} app={app} />
}
// Fallback: If no app found (cache evicted), show default icon
return <LayoutGrid size={14} />
}
switch (tabId) {
@ -94,7 +122,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const isFullscreen = useFullscreen()
const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup } = useMinappPopup()
const { hideMinappPopup, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps()
const { t } = useTranslation()
@ -112,8 +140,23 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
// Check if it's a minapp tab
if (tabId.startsWith('apps:')) {
const appId = tabId.replace('apps:', '')
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
return app ? app.name : 'MinApp'
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// If not found in permanent apps, search in temporary apps cache
// This ensures temporary MinApps display proper titles while being used
// The LRU cache automatically manages app lifecycle and prevents memory leaks
if (!app && minAppsCache) {
app = minAppsCache.get(appId)
// Defensive programming: If app not found in cache but tab exists,
// the cache entry may have been evicted due to LRU policy
if (!app) {
logger.warn(`MinApp ${appId} not found in cache, using fallback title`)
}
}
// Return app name if found, otherwise use fallback with appId
return app ? app.name : `MinApp-${appId}`
}
return getTitleLabel(tabId)
}
@ -196,7 +239,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
renderItem={(tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
@ -259,7 +302,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
flex-direction: row;
align-items: center;
gap: 5px;
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-x)' : '15px')};
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'calc(env(titlebar-area-x) + 4px)' : '15px')};
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')};
height: var(--navbar-height);
min-height: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-height)' : '')};

View File

@ -88,6 +88,7 @@ const NavbarCenterContainer = styled.div`
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
padding-left: 10px;
font-weight: bold;
color: var(--color-text-1);
position: relative;
@ -108,7 +109,8 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0};
padding-right: ${isMac ? '20px' : 0};
padding-left: 10px;
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};

View File

@ -17,6 +17,7 @@ interface ItemRendererProps<T> {
transform?: Transform | null
transition?: string | null
listeners?: DraggableSyntheticListeners
itemStyle?: React.CSSProperties
}
export function ItemRenderer<T>({
@ -30,6 +31,7 @@ export function ItemRenderer<T>({
transform,
transition,
listeners,
itemStyle,
...props
}: ItemRendererProps<T>) {
useEffect(() => {
@ -44,7 +46,7 @@ export function ItemRenderer<T>({
}
}, [dragOverlay])
const wrapperStyle = {
const style = {
transition,
transform: CSS.Transform.toString(transform ?? null)
} as React.CSSProperties
@ -54,7 +56,7 @@ export function ItemRenderer<T>({
ref={ref}
data-index={index}
className={classNames({ dragOverlay: dragOverlay })}
style={{ ...wrapperStyle }}>
style={{ ...style, ...itemStyle }}>
<DraggableItem
className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
{...listeners}

View File

@ -61,6 +61,8 @@ interface SortableProps<T> {
className?: string
/** Item list style */
listStyle?: React.CSSProperties
/** Item style */
itemStyle?: React.CSSProperties
/** Item gap */
gap?: number | string
/** Restrictions, shortcuts for some modifiers */
@ -87,6 +89,7 @@ function Sortable<T>({
showGhost = false,
className,
listStyle,
itemStyle,
gap,
restrictions,
modifiers: customModifiers
@ -195,19 +198,19 @@ function Sortable<T>({
renderItem={renderItem}
useDragOverlay={useDragOverlay}
showGhost={showGhost}
itemStyle={itemStyle}
/>
))}
</ListWrapper>
</SortableContext>
{useDragOverlay
? createPortal(
<DragOverlay adjustScale dropAnimation={dropAnimation}>
{activeItem ? <ItemRenderer item={activeItem} renderItem={renderItem} dragOverlay /> : null}
</DragOverlay>,
document.body
)
: null}
{useDragOverlay &&
createPortal(
<DragOverlay adjustScale dropAnimation={dropAnimation}>
{activeItem && <ItemRenderer item={activeItem} renderItem={renderItem} itemStyle={itemStyle} dragOverlay />}
</DragOverlay>,
document.body
)}
</DndContext>
)
}

View File

@ -10,6 +10,7 @@ interface SortableItemProps<T> {
renderItem: RenderItemType<T>
useDragOverlay?: boolean
showGhost?: boolean
itemStyle?: React.CSSProperties
}
export function SortableItem<T>({
@ -18,7 +19,8 @@ export function SortableItem<T>({
index,
renderItem,
useDragOverlay = true,
showGhost = true
showGhost = true,
itemStyle
}: SortableItemProps<T>) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id
@ -36,6 +38,7 @@ export function SortableItem<T>({
transform={transform}
transition={transition}
listeners={listeners}
itemStyle={itemStyle}
{...attributes}
/>
)

View File

@ -93,7 +93,17 @@ export function isSupportedThinkingTokenModel(model?: Model): boolean {
// Specifically for DeepSeek V3.1. White list for now
if (isDeepSeekHybridInferenceModel(model)) {
return (
['openrouter', 'dashscope', 'modelscope', 'doubao', 'silicon', 'nvidia', 'ppio'] satisfies SystemProviderId[]
[
'openrouter',
'dashscope',
'modelscope',
'doubao',
'silicon',
'nvidia',
'ppio',
'hunyuan',
'tencent-cloud-ti'
] satisfies SystemProviderId[]
).some((id) => id === model.provider)
}
@ -381,7 +391,8 @@ export function isReasoningModel(model?: Model): boolean {
isDeepSeekHybridInferenceModel(model) ||
modelId.includes('magistral') ||
modelId.includes('minimax-m1') ||
modelId.includes('pangu-pro-moe')
modelId.includes('pangu-pro-moe') ||
modelId.includes('seed-oss')
) {
return true
}

View File

@ -138,16 +138,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
ppio: {
id: 'ppio',
name: 'PPIO',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ppinfra.com/v3/openai/',
models: SYSTEM_MODELS.ppio,
isSystem: true,
enabled: false
},
alayanew: {
id: 'alayanew',
name: 'AlayaNew',
@ -158,16 +148,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
qiniu: {
id: 'qiniu',
name: 'Qiniu',
type: 'openai',
apiKey: '',
apiHost: 'https://api.qnaigc.com',
models: SYSTEM_MODELS.qiniu,
isSystem: true,
enabled: false
},
dmxapi: {
id: 'dmxapi',
name: 'DMXAPI',
@ -178,6 +158,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
},
burncloud: {
id: 'burncloud',
name: 'BurnCloud',
@ -238,6 +228,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
ppio: {
id: 'ppio',
name: 'PPIO',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ppinfra.com/v3/openai/',
models: SYSTEM_MODELS.ppio,
isSystem: true,
enabled: false
},
qiniu: {
id: 'qiniu',
name: 'Qiniu',
type: 'openai',
apiKey: '',
apiHost: 'https://api.qnaigc.com',
models: SYSTEM_MODELS.qiniu,
isSystem: true,
enabled: false
},
openrouter: {
id: 'openrouter',
name: 'OpenRouter',
@ -612,16 +622,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
models: SYSTEM_MODELS['poe'],
isSystem: true,
enabled: false
},
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
}
} as const
@ -1375,3 +1375,7 @@ const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as con
export const isGeminiWebSearchProvider = (provider: Provider) => {
return SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS.some((id) => id === provider.id)
}
export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id)
}

View File

@ -172,7 +172,10 @@ export function useAssistant(id: string) {
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
[assistant, dispatch]
),
updateAssistant: useCallback((assistant: Partial<Assistant>) => dispatch(updateAssistant(assistant)), [dispatch]),
updateAssistant: useCallback(
(update: Partial<Omit<Assistant, 'id'>>) => dispatch(updateAssistant({ id, ...update })),
[dispatch, id]
),
updateAssistantSettings
}
}

View File

@ -1,6 +1,7 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
import NavigationService from '@renderer/services/NavigationService'
import TabsService from '@renderer/services/TabsService'
import { useAppDispatch } from '@renderer/store'
import {
@ -14,6 +15,8 @@ import { clearWebviewState } from '@renderer/utils/webviewStateManager'
import { LRUCache } from 'lru-cache'
import { useCallback } from 'react'
import { useNavbarPosition } from './useSettings'
let minAppsCache: LRUCache<string, MinAppType>
/**
@ -34,6 +37,7 @@ export const useMinappPopup = () => {
const dispatch = useAppDispatch()
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
const { maxKeepAliveMinapps } = useSettings() // 使用设置中的值
const { isTopNavbar } = useNavbarPosition()
const createLRUCache = useCallback(() => {
return new LRUCache<string, MinAppType>({
@ -165,6 +169,33 @@ export const useMinappPopup = () => {
dispatch(setMinappShow(false))
}, [dispatch, minappShow, openedOneOffMinapp])
/** Smart open minapp that adapts to navbar position */
const openSmartMinapp = useCallback(
(config: MinAppType, keepAlive: boolean = false) => {
if (isTopNavbar) {
// For top navbar mode, need to add to cache first for temporary apps
const cacheApp = minAppsCache.get(config.id)
if (!cacheApp) {
// Add temporary app to cache so MinAppPage can find it
minAppsCache.set(config.id, config)
}
// Set current minapp and show state
dispatch(setCurrentMinappId(config.id))
dispatch(setMinappShow(true))
// Then navigate to the app tab using NavigationService
if (NavigationService.navigate) {
NavigationService.navigate(`/apps/${config.id}`)
}
} else {
// For side navbar, use the traditional popup system
openMinapp(config, keepAlive)
}
},
[isTopNavbar, openMinapp, dispatch]
)
return {
openMinapp,
openMinappKeepAlive,
@ -172,6 +203,7 @@ export const useMinappPopup = () => {
closeMinapp,
hideMinappPopup,
closeAllMinapps,
openSmartMinapp,
// Expose cache instance for TabsService integration
minAppsCache
}

View File

@ -1,4 +1,4 @@
import { LoadingIcon } from '@renderer/components/Icons'
import { Spinner } from '@heroui/react'
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
import React from 'react'
import styled from 'styled-components'
@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
return (
<MessageContentLoading>
<LoadingIcon />
<Spinner color="current" variant="dots" />
</MessageContentLoading>
)
}

View File

@ -3,7 +3,7 @@ import type { RootState } from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import type { ImageMessageBlock, Message, MessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { isMainTextBlock, isVideoBlock } from '@renderer/utils/messageUtils/is'
import { isMainTextBlock, isMessageProcessing, isVideoBlock } from '@renderer/utils/messageUtils/is'
import { AnimatePresence, motion, type Variants } from 'motion/react'
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
@ -107,6 +107,9 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
const renderedBlocks = blocks.map((blockId) => blockEntities[blockId]).filter(Boolean)
const groupedBlocks = useMemo(() => groupSimilarBlocks(renderedBlocks), [renderedBlocks])
// Check if message is still processing
const isProcessing = isMessageProcessing(message)
return (
<AnimatePresence mode="sync">
{groupedBlocks.map((block) => {
@ -151,9 +154,6 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
switch (block.type) {
case MessageBlockType.UNKNOWN:
if (block.status === MessageBlockStatus.PROCESSING) {
blockComponent = <PlaceholderBlock key={block.id} block={block} />
}
break
case MessageBlockType.MAIN_TEXT:
case MessageBlockType.CODE: {
@ -213,6 +213,19 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
</AnimatedBlockWrapper>
)
})}
{isProcessing && (
<AnimatedBlockWrapper key="message-loading-placeholder" enableAnimation={true}>
<PlaceholderBlock
block={{
id: `loading-${message.id}`,
messageId: message.id,
type: MessageBlockType.UNKNOWN,
status: MessageBlockStatus.PROCESSING,
createdAt: new Date().toISOString()
}}
/>
</AnimatedBlockWrapper>
)}
</AnimatePresence>
)
}

View File

@ -1,7 +1,7 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isWin } from '@renderer/config/constant'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
@ -86,7 +86,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
)}
</AnimatePresence>
{!showAssistants && (
<NavbarLeft style={{ justifyContent: 'flex-start', borderRight: 'none', padding: '0 10px', minWidth: 'auto' }}>
<NavbarLeft
style={{
justifyContent: 'flex-start',
borderRight: 'none',
paddingLeft: 0,
paddingRight: 10,
minWidth: 'auto'
}}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelRightClose size={18} />
@ -106,7 +113,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</AnimatePresence>
</NavbarLeft>
)}
<HStack alignItems="center" gap={6}>
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
<SelectModelButton assistant={assistant} />
</HStack>
<NavbarRight
@ -114,7 +121,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
justifyContent: 'flex-end',
flex: 1,
position: 'relative',
paddingRight: isWin || isLinux ? '144px' : '6px'
paddingRight: isWin || isLinux ? '144px' : '15px'
}}
className="home-navbar-right">
<HStack alignItems="center" gap={6}>

View File

@ -412,7 +412,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={true}>
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={false}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall>
@ -441,7 +441,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={false}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
@ -585,7 +585,7 @@ const SettingsTab: FC<Props> = (props) => {
</SettingGroup>
<SettingDivider />
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={false}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>

View File

@ -44,11 +44,20 @@ const MinAppPage: FC = () => {
}
}, [isTopNavbar])
// Find the app from all available apps
// Find the app from all available apps (including cached ones)
const app = useMemo(() => {
if (!appId) return null
return [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
}, [appId, minapps])
// First try to find in default and custom mini-apps
let foundApp = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
// If not found and we have cache, try to find in cache (for temporary apps)
if (!foundApp && minAppsCache) {
foundApp = minAppsCache.get(appId)
}
return foundApp
}, [appId, minapps, minAppsCache])
useEffect(() => {
// If app not found, redirect to apps list

View File

@ -111,7 +111,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
behavior: 'instant'
})
}
}

View File

@ -307,7 +307,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
const createNewPainting = () => {
const handleAddPainting = () => {
if (generating) return
const newPainting = getNewPainting()
const addedPainting = addPainting('zhipu_paintings', newPainting)
@ -342,12 +342,12 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Container>
<Navbar>
<NavbarCenter>
<Title>{t('title.paintings')}</Title>
</NavbarCenter>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight>
<Button type="text" icon={<PlusOutlined />} onClick={createNewPainting} disabled={generating} />
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
@ -482,7 +482,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={createNewPainting}
onNewPainting={handleAddPainting}
/>
</ContentContainer>
</Container>
@ -556,12 +556,6 @@ const ToolbarMenu = styled.div`
gap: 8px;
`
const Title = styled.h1`
margin: 0;
font-size: 18px;
font-weight: 600;
`
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;

View File

@ -14,7 +14,7 @@ import { runAsyncFunction } from '@renderer/utils'
import { UpgradeChannel } from '@shared/config/constant'
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react'
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
import { BadgeQuestionMark } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -32,7 +32,7 @@ const AboutSettings: FC = () => {
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { update } = useRuntime()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const onCheckUpdate = debounce(
async () => {
@ -79,7 +79,7 @@ const AboutSettings: FC = () => {
const showLicense = async () => {
const { appPath } = await window.api.getAppInfo()
openMinapp({
openSmartMinapp({
id: 'cherrystudio-license',
name: t('settings.about.license.title'),
url: `file://${appPath}/resources/cherry-studio/license.html`,
@ -89,7 +89,7 @@ const AboutSettings: FC = () => {
const showReleases = async () => {
const { appPath } = await window.api.getAppInfo()
openMinapp({
openSmartMinapp({
id: 'cherrystudio-releases',
name: t('settings.about.releases.title'),
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
@ -309,7 +309,7 @@ const AboutSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<Github size={18} />
<GithubOutlined size={18} />
{t('settings.about.feedback.title')}
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}>

View File

@ -1,5 +1,6 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
@ -16,7 +17,7 @@ const JoplinSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
@ -66,10 +67,11 @@ const JoplinSettings: FC = () => {
}
const handleJoplinHelpClick = () => {
openMinapp({
openSmartMinapp({
id: 'joplin-help',
name: 'Joplin Help',
url: 'https://joplinapp.org/help/apps/clipper'
url: 'https://joplinapp.org/help/apps/clipper',
logo: AppLogo
})
}

View File

@ -1,6 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { Client } from '@notionhq/client'
import { HStack } from '@renderer/components/Layout'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
@ -21,7 +22,7 @@ const NotionSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
@ -67,10 +68,11 @@ const NotionSettings: FC = () => {
}
const handleNotionTitleClick = () => {
openMinapp({
openSmartMinapp({
id: 'notion-help',
name: 'Notion Help',
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
url: 'https://docs.cherry-ai.com/advanced-basic/notion',
logo: AppLogo
})
}

View File

@ -3,6 +3,7 @@ import { HStack } from '@renderer/components/Layout'
import { S3BackupManager } from '@renderer/components/S3BackupManager'
import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
import Selector from '@renderer/components/Selector'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useSettings } from '@renderer/hooks/useSettings'
@ -47,7 +48,7 @@ const S3Settings: FC = () => {
const dispatch = useAppDispatch()
const { theme } = useTheme()
const { t } = useTranslation()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const { s3Sync } = useAppSelector((state) => state.backup)
@ -62,10 +63,11 @@ const S3Settings: FC = () => {
}
const handleTitleClick = () => {
openMinapp({
openSmartMinapp({
id: 's3-help',
name: 'S3 Compatible Storage Help',
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible'
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible',
logo: AppLogo
})
}

View File

@ -1,6 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { HStack } from '@renderer/components/Layout'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
@ -16,7 +17,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
const logger = loggerService.withContext('SiyuanSettings')
const SiyuanSettings: FC = () => {
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
@ -43,10 +44,11 @@ const SiyuanSettings: FC = () => {
}
const handleSiyuanHelpClick = () => {
openMinapp({
openSmartMinapp({
id: 'siyuan-help',
name: 'Siyuan Help',
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan',
logo: AppLogo
})
}

View File

@ -1,5 +1,6 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
@ -16,7 +17,7 @@ const YuqueSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const { openSmartMinapp } = useMinappPopup()
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
@ -65,10 +66,11 @@ const YuqueSettings: FC = () => {
}
const handleYuqueHelpClick = () => {
openMinapp({
openSmartMinapp({
id: 'yuque-help',
name: 'Yuque Help',
url: 'https://www.yuque.com/settings/tokens'
url: 'https://www.yuque.com/settings/tokens',
logo: AppLogo
})
}

View File

@ -183,7 +183,7 @@ const CardContainer = styled.div<{ $isActive: boolean }>`
margin-bottom: 5px;
height: 125px;
opacity: ${(props) => (props.$isActive ? 1 : 0.6)};
width: calc(100vw - var(--settings-width) - 40px);
width: 100%;
&:hover {
opacity: 1;

View File

@ -251,7 +251,8 @@ const McpServersList: FC = () => {
onSortEnd={onSortEnd}
layout="list"
horizontal={false}
listStyle={{ display: 'flex', flexDirection: 'column' }}
listStyle={{ display: 'flex', flexDirection: 'column', width: '100%' }}
itemStyle={{ width: '100%' }}
gap="12px"
restrictions={{ scrollableAncestor: true }}
useDragOverlay

View File

@ -17,6 +17,7 @@ import {
isVisionModel,
isWebSearchModel
} from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
@ -78,7 +79,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: formValues.id || model.id,
name: formValues.name || model.name,
group: formValues.group || model.group,
endpoint_type: provider.id === 'new-api' ? formValues.endpointType : model.endpoint_type,
endpoint_type: isNewApiProvider(provider) ? formValues.endpointType : model.endpoint_type,
capabilities: overrides?.capabilities ?? modelCapabilities,
supported_text_delta: overrides?.supported_text_delta ?? supportedTextDelta,
pricing: {
@ -97,7 +98,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
id: values.id || model.id,
name: values.name || model.name,
group: values.group || model.group,
endpoint_type: provider.id === 'new-api' ? values.endpointType : model.endpoint_type,
endpoint_type: isNewApiProvider(provider) ? values.endpointType : model.endpoint_type,
capabilities: modelCapabilities,
supported_text_delta: supportedTextDelta,
pricing: {
@ -247,7 +248,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
<Modal title={t('models.edit')} footer={null} transitionName="animation-move-down" centered {...props}>
<Form
form={form}
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
labelCol={{ flex: isNewApiProvider(provider) ? labelWidth : '110px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 15 }}
@ -309,7 +310,7 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
{provider.id === 'new-api' && (
{isNewApiProvider(provider) && (
<Form.Item
name="endpointType"
label={t('settings.models.add.endpoint_type.label')}

View File

@ -3,6 +3,7 @@ import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import FileItem from '@renderer/pages/files/FileItem'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
import { Model, Provider } from '@renderer/types'
@ -91,7 +92,7 @@ const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provid
// 添加整组
const wouldAddModels = models.filter((model) => !isModelInProvider(provider, model.id))
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
if (wouldAddModels.every(isValidNewApiModel)) {
wouldAddModels.forEach(onAddModel)
} else {

View File

@ -13,6 +13,7 @@ import {
isWebSearchModel,
SYSTEM_MODELS
} from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider'
import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup'
import NewApiBatchAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiBatchAddModelPopup'
@ -129,7 +130,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, resolve }) => {
const onAddModel = useCallback(
(model: Model) => {
if (!isEmpty(model.name)) {
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
addModel({
...model,
@ -160,7 +161,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, resolve }) => {
content: t('settings.models.manage.add_listed.confirm'),
centered: true,
onOk: () => {
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
if (models.every(isValidNewApiModel)) {
wouldAddModel.forEach(onAddModel)
} else {

View File

@ -2,7 +2,7 @@ import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { PROVIDER_URLS } from '@renderer/config/providers'
import { isNewApiProvider, PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider'
import { getProviderLabel } from '@renderer/i18n/label'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings'
@ -86,7 +86,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
}, [provider.id])
const onAddModel = useCallback(() => {
if (provider.id === 'new-api') {
if (isNewApiProvider(provider)) {
NewApiAddModelPopup.show({ title: t('settings.models.add.add_model'), provider })
} else {
AddModelPopup.show({ title: t('settings.models.add.add_model'), provider })

View File

@ -1,6 +1,7 @@
import { TopView } from '@renderer/components/TopView'
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
import { isNotSupportedTextDelta } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers'
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
import { useProvider } from '@renderer/hooks/useProvider'
import { EndpointType, Model, Provider } from '@renderer/types'
@ -60,7 +61,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve, model, endp
provider: provider.id,
name: values.name ? values.name : id.toUpperCase(),
group: values.group ?? getDefaultGroupName(id),
endpoint_type: provider.id === 'new-api' ? values.endpointType : undefined
endpoint_type: isNewApiProvider(provider) ? values.endpointType : undefined
}
addModel({ ...model, supported_text_delta: !isNotSupportedTextDelta(model) })

View File

@ -46,8 +46,9 @@ const assistantsSlice = createSlice({
removeAssistant: (state, action: PayloadAction<{ id: string }>) => {
state.assistants = state.assistants.filter((c) => c.id !== action.payload.id)
},
updateAssistant: (state, action: PayloadAction<Partial<Assistant>>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? { ...c, ...action.payload } : c))
updateAssistant: (state, action: PayloadAction<Partial<Assistant> & { id: string }>) => {
const { id, ...update } = action.payload
state.assistants = state.assistants.map((c) => (c.id === id ? { ...c, ...update } : c))
},
updateAssistantSettings: (
state,

View File

@ -2495,6 +2495,7 @@ const migrateConfig = {
'157': (state: RootState) => {
try {
addProvider(state, 'aionly')
state.llm.providers = moveProvider(state.llm.providers, 'aionly', 10)
const cherryinProvider = state.llm.providers.find((provider) => provider.id === 'cherryin')