mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
Merge branch 'main' into feat/agents-new
This commit is contained in:
commit
3088887e57
22
.github/workflows/delete-branch.yml
vendored
Normal file
22
.github/workflows/delete-branch.yml
vendored
Normal 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}`,
|
||||
})
|
||||
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
🚀 性能优化:
|
||||
- 提升输入框响应速度
|
||||
- 优化模型切换性能
|
||||
- 改进翻译功能的引用和邮件格式处理
|
||||
|
||||
@ -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 = ''
|
||||
|
||||
@ -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
|
||||
|
||||
@ -67,7 +67,9 @@ vi.mock('@renderer/config/models', () => ({
|
||||
silicon: [],
|
||||
defaultModel: []
|
||||
},
|
||||
isOpenAIModel: vi.fn(() => false)
|
||||
isOpenAIModel: vi.fn(() => false),
|
||||
glm45FlashModel: {},
|
||||
qwen38bModel: {}
|
||||
}))
|
||||
|
||||
describe('ApiClientFactory', () => {
|
||||
|
||||
@ -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', () => ({
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;"
|
||||
/>
|
||||
`;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)' : '')};
|
||||
|
||||
@ -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')};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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')}>
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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) })
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user