diff --git a/.github/workflows/delete-branch.yml b/.github/workflows/delete-branch.yml new file mode 100644 index 0000000000..fae32c7477 --- /dev/null +++ b/.github/workflows/delete-branch.yml @@ -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}`, + }) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 137208bff0..4f462db95c 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -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 diff --git a/electron-builder.yml b/electron-builder.yml index 0fe2f2d3b9..9b9a239160 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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 - 🚀 性能优化: - - 提升输入框响应速度 - - 优化模型切换性能 - - 改进翻译功能的引用和邮件格式处理 diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 7932dea86d..cc2a93e511 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -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 = '' diff --git a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts index b38ab59537..f7b4a80f46 100644 --- a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts +++ b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts @@ -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 diff --git a/src/renderer/src/aiCore/legacy/clients/__tests__/ApiClientFactory.test.ts b/src/renderer/src/aiCore/legacy/clients/__tests__/ApiClientFactory.test.ts index 081469516b..550486afb2 100644 --- a/src/renderer/src/aiCore/legacy/clients/__tests__/ApiClientFactory.test.ts +++ b/src/renderer/src/aiCore/legacy/clients/__tests__/ApiClientFactory.test.ts @@ -67,7 +67,9 @@ vi.mock('@renderer/config/models', () => ({ silicon: [], defaultModel: [] }, - isOpenAIModel: vi.fn(() => false) + isOpenAIModel: vi.fn(() => false), + glm45FlashModel: {}, + qwen38bModel: {} })) describe('ApiClientFactory', () => { diff --git a/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts b/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts index d70d9c58f9..dd85730c36 100644 --- a/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts +++ b/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts @@ -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', () => ({ diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index 1f18e49bad..20b89cf2e5 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -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 } diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index eaaef15211..b91dad9cf7 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -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') { diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index bee07b1e0d..9328f7f0ce 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -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: { diff --git a/src/renderer/src/components/Icons/MinAppIcon.tsx b/src/renderer/src/components/Icons/MinAppIcon.tsx index c9612416bb..98974da745 100644 --- a/src/renderer/src/components/Icons/MinAppIcon.tsx +++ b/src/renderer/src/components/Icons/MinAppIcon.tsx @@ -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 = ({ 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 ( + {app.name + ) } - return ( - - ) + // If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps) + if (app.logo) { + return ( + {app.name + ) + } + + return null } -const Container = styled.img` - border-radius: 16px; - user-select: none; - -webkit-user-drag: none; -` - export default MinAppIcon diff --git a/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap b/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap index e41515fed6..3395e366f4 100644 --- a/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap +++ b/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap @@ -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; -} - Test App `; diff --git a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx index 45560bcd6c..0e4ca65036 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx @@ -180,7 +180,7 @@ const PopupContainer: React.FC = ({ model, filter: baseFilter, showTagFil key: `provider-${p.id}`, type: 'group', name: getFancyProviderName(p), - actions: ( + actions: p.id !== 'cherryai' && ( { +const logger = loggerService.withContext('TabContainer') + +const getTabIcon = ( + tabId: string, + minapps: MinAppType[], + minAppsCache?: LRUCache +): 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 } + + // Fallback: If no app found (cache evicted), show default icon + return } switch (tabId) { @@ -94,7 +122,7 @@ const TabsContainer: React.FC = ({ 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 = ({ 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 = ({ children }) => { renderItem={(tab) => ( handleTabClick(tab)}> - {tab.id && {getTabIcon(tab.id, minapps)}} + {tab.id && {getTabIcon(tab.id, minapps, minAppsCache)}} {getTabTitle(tab.id)} {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)' : '')}; diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 41bad7260e..1578e73a7e 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -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')}; diff --git a/src/renderer/src/components/dnd/ItemRenderer.tsx b/src/renderer/src/components/dnd/ItemRenderer.tsx index a33301df62..d13e298963 100644 --- a/src/renderer/src/components/dnd/ItemRenderer.tsx +++ b/src/renderer/src/components/dnd/ItemRenderer.tsx @@ -17,6 +17,7 @@ interface ItemRendererProps { transform?: Transform | null transition?: string | null listeners?: DraggableSyntheticListeners + itemStyle?: React.CSSProperties } export function ItemRenderer({ @@ -30,6 +31,7 @@ export function ItemRenderer({ transform, transition, listeners, + itemStyle, ...props }: ItemRendererProps) { useEffect(() => { @@ -44,7 +46,7 @@ export function ItemRenderer({ } }, [dragOverlay]) - const wrapperStyle = { + const style = { transition, transform: CSS.Transform.toString(transform ?? null) } as React.CSSProperties @@ -54,7 +56,7 @@ export function ItemRenderer({ ref={ref} data-index={index} className={classNames({ dragOverlay: dragOverlay })} - style={{ ...wrapperStyle }}> + style={{ ...style, ...itemStyle }}> { 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({ showGhost = false, className, listStyle, + itemStyle, gap, restrictions, modifiers: customModifiers @@ -195,19 +198,19 @@ function Sortable({ renderItem={renderItem} useDragOverlay={useDragOverlay} showGhost={showGhost} + itemStyle={itemStyle} /> ))} - {useDragOverlay - ? createPortal( - - {activeItem ? : null} - , - document.body - ) - : null} + {useDragOverlay && + createPortal( + + {activeItem && } + , + document.body + )} ) } diff --git a/src/renderer/src/components/dnd/SortableItem.tsx b/src/renderer/src/components/dnd/SortableItem.tsx index 60901223a0..ec91f54da8 100644 --- a/src/renderer/src/components/dnd/SortableItem.tsx +++ b/src/renderer/src/components/dnd/SortableItem.tsx @@ -10,6 +10,7 @@ interface SortableItemProps { renderItem: RenderItemType useDragOverlay?: boolean showGhost?: boolean + itemStyle?: React.CSSProperties } export function SortableItem({ @@ -18,7 +19,8 @@ export function SortableItem({ index, renderItem, useDragOverlay = true, - showGhost = true + showGhost = true, + itemStyle }: SortableItemProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id @@ -36,6 +38,7 @@ export function SortableItem({ transform={transform} transition={transition} listeners={listeners} + itemStyle={itemStyle} {...attributes} /> ) diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 74bddd2897..607df8fd95 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -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 } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 80ea9bdf7a..3b8821905a 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -138,16 +138,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = 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 = 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 = 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 = 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 = 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) +} diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index 096c91b5a1..8a038bec31 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -172,7 +172,10 @@ export function useAssistant(id: string) { (model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })), [assistant, dispatch] ), - updateAssistant: useCallback((assistant: Partial) => dispatch(updateAssistant(assistant)), [dispatch]), + updateAssistant: useCallback( + (update: Partial>) => dispatch(updateAssistant({ id, ...update })), + [dispatch, id] + ), updateAssistantSettings } } diff --git a/src/renderer/src/hooks/useMinappPopup.ts b/src/renderer/src/hooks/useMinappPopup.ts index e8765267a6..99b49e43e8 100644 --- a/src/renderer/src/hooks/useMinappPopup.ts +++ b/src/renderer/src/hooks/useMinappPopup.ts @@ -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 /** @@ -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({ @@ -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 } diff --git a/src/renderer/src/pages/home/Messages/Blocks/PlaceholderBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/PlaceholderBlock.tsx index bcc8a96859..7682ae2343 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/PlaceholderBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/PlaceholderBlock.tsx @@ -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 = ({ block }) => { if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) { return ( - + ) } diff --git a/src/renderer/src/pages/home/Messages/Blocks/index.tsx b/src/renderer/src/pages/home/Messages/Blocks/index.tsx index 5d6128e660..0e2d318e1e 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/index.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/index.tsx @@ -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 = ({ 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 ( {groupedBlocks.map((block) => { @@ -151,9 +154,6 @@ const MessageBlockRenderer: React.FC = ({ blocks, message }) => { switch (block.type) { case MessageBlockType.UNKNOWN: - if (block.status === MessageBlockStatus.PROCESSING) { - blockComponent = - } break case MessageBlockType.MAIN_TEXT: case MessageBlockType.CODE: { @@ -213,6 +213,19 @@ const MessageBlockRenderer: React.FC = ({ blocks, message }) => { ) })} + {isProcessing && ( + + + + )} ) } diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 51bf834149..43217d7ca3 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -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 = ({ activeAssistant, setActiveAssistant, activeTo )} {!showAssistants && ( - + toggleShowAssistants()}> @@ -106,7 +113,7 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} - + = ({ activeAssistant, setActiveAssistant, activeTo justifyContent: 'flex-end', flex: 1, position: 'relative', - paddingRight: isWin || isLinux ? '144px' : '6px' + paddingRight: isWin || isLinux ? '144px' : '15px' }} className="home-navbar-right"> diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 3738b7198a..f56357cc4e 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -412,7 +412,7 @@ const SettingsTab: FC = (props) => { - + {t('settings.math.engine.label')} @@ -441,7 +441,7 @@ const SettingsTab: FC = (props) => { - + {t('message.message.code_style')} @@ -585,7 +585,7 @@ const SettingsTab: FC = (props) => { - + {t('settings.messages.input.show_estimated_tokens')} diff --git a/src/renderer/src/pages/minapps/MinAppPage.tsx b/src/renderer/src/pages/minapps/MinAppPage.tsx index 9629b3c56d..c85afab22c 100644 --- a/src/renderer/src/pages/minapps/MinAppPage.tsx +++ b/src/renderer/src/pages/minapps/MinAppPage.tsx @@ -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 diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index c9803208c3..09a76b6153 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -111,7 +111,7 @@ const NotesSidebar: FC = ({ const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2 scrollContainer.scrollTo({ top: Math.max(0, targetScrollTop), - behavior: 'smooth' + behavior: 'instant' }) } } diff --git a/src/renderer/src/pages/paintings/ZhipuPage.tsx b/src/renderer/src/pages/paintings/ZhipuPage.tsx index ea8d7cfcea..9ebfe51a7d 100644 --- a/src/renderer/src/pages/paintings/ZhipuPage.tsx +++ b/src/renderer/src/pages/paintings/ZhipuPage.tsx @@ -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 ( - - {t('title.paintings')} - + {t('paintings.title')} {isMac && ( - - )} @@ -482,7 +482,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { selectedPainting={painting} onSelectPainting={onSelectPainting} onDeletePainting={onDeletePainting} - onNewPainting={createNewPainting} + onNewPainting={handleAddPainting} /> @@ -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; diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index b79f1cb099..40b0a99ecb 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -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 = () => { - + {t('settings.about.feedback.title')}