diff --git a/package.json b/package.json index 29b7ef5476..8cc9a8f753 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "express": "^5.1.0", "express-validator": "^7.2.1", "faiss-node": "^0.5.1", + "font-list": "^2.0.0", "graceful-fs": "^4.2.11", "jsdom": "26.1.0", "node-stream-zip": "^1.15.0", @@ -99,6 +100,7 @@ "@ai-sdk/amazon-bedrock": "^3.0.0", "@ai-sdk/google-vertex": "^3.0.25", "@ai-sdk/mistral": "^2.0.0", + "@ai-sdk/perplexity": "^2.0.8", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@anthropic-ai/sdk": "^0.41.0", "@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index fa02991682..a2ef66284c 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -38,6 +38,7 @@ export enum IpcChannel { App_GetDiskInfo = 'app:get-disk-info', App_SetFullScreen = 'app:set-full-screen', App_IsFullScreen = 'app:is-full-screen', + App_GetSystemFonts = 'app:get-system-fonts', App_MacIsProcessTrusted = 'app:mac-is-process-trusted', App_MacRequestProcessTrust = 'app:mac-request-process-trust', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 6b36a96a35..1926145e86 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -14,6 +14,7 @@ import { IpcChannel } from '@shared/IpcChannel' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types' import checkDiskSpace from 'check-disk-space' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' +import fontList from 'font-list' import { Notification } from 'src/renderer/src/types/notification' import { apiServerService } from './services/ApiServerService' @@ -219,6 +220,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { return mainWindow.isFullScreen() }) + // Get System Fonts + ipcMain.handle(IpcChannel.App_GetSystemFonts, async () => { + try { + const fonts = await fontList.getFonts() + return fonts.map((font: string) => font.replace(/^"(.*)"$/, '$1')).filter((font: string) => font.length > 0) + } catch (error) { + logger.error('Failed to get system fonts:', error as Error) + return [] + } + }) + ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { configManager.set(key, value, isNotify) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index d8c55dd256..12cab36118 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -84,6 +84,7 @@ const api = { ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data), setFullScreen: (value: boolean): Promise => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value), isFullScreen: (): Promise => ipcRenderer.invoke(IpcChannel.App_IsFullScreen), + getSystemFonts: (): Promise => ipcRenderer.invoke(IpcChannel.App_GetSystemFonts), mac: { isProcessTrusted: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), requestProcessTrust: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index a28000d9dd..8af4388d5f 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -4,8 +4,9 @@ */ import { loggerService } from '@logger' -import { MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types' +import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types' import { Chunk, ChunkType } from '@renderer/types/chunk' +import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter' import type { TextStreamPart, ToolSet } from 'ai' import { ToolCallChunkHandler } from './handleToolCallChunk' @@ -29,13 +30,18 @@ export interface CherryStudioChunk { export class AiSdkToChunkAdapter { toolCallHandler: ToolCallChunkHandler private accumulate: boolean | undefined + private isFirstChunk = true + private enableWebSearch: boolean = false + constructor( private onChunk: (chunk: Chunk) => void, mcpTools: MCPTool[] = [], - accumulate?: boolean + accumulate?: boolean, + enableWebSearch?: boolean ) { this.toolCallHandler = new ToolCallChunkHandler(onChunk, mcpTools) this.accumulate = accumulate + this.enableWebSearch = enableWebSearch || false } /** @@ -65,11 +71,24 @@ export class AiSdkToChunkAdapter { webSearchResults: [], reasoningId: '' } + // Reset link converter state at the start of stream + this.isFirstChunk = true + try { while (true) { const { done, value } = await reader.read() if (done) { + // Flush any remaining content from link converter buffer if web search is enabled + if (this.enableWebSearch) { + const remainingText = flushLinkConverterBuffer() + if (remainingText) { + this.onChunk({ + type: ChunkType.TEXT_DELTA, + text: remainingText + }) + } + } break } @@ -87,7 +106,7 @@ export class AiSdkToChunkAdapter { */ private convertAndEmitChunk( chunk: TextStreamPart, - final: { text: string; reasoningContent: string; webSearchResults: any[]; reasoningId: string } + final: { text: string; reasoningContent: string; webSearchResults: AISDKWebSearchResult[]; reasoningId: string } ) { logger.silly(`AI SDK chunk type: ${chunk.type}`, chunk) switch (chunk.type) { @@ -97,17 +116,44 @@ export class AiSdkToChunkAdapter { type: ChunkType.TEXT_START }) break - case 'text-delta': - if (this.accumulate) { - final.text += chunk.text || '' + case 'text-delta': { + const processedText = chunk.text || '' + let finalText: string + + // Only apply link conversion if web search is enabled + if (this.enableWebSearch) { + const result = convertLinks(processedText, this.isFirstChunk) + + if (this.isFirstChunk) { + this.isFirstChunk = false + } + + // Handle buffered content + if (result.hasBufferedContent) { + finalText = result.text + } else { + finalText = result.text || processedText + } } else { - final.text = chunk.text || '' + // Without web search, just use the original text + finalText = processedText + } + + if (this.accumulate) { + final.text += finalText + } else { + final.text = finalText + } + + // Only emit chunk if there's text to send + if (finalText) { + this.onChunk({ + type: ChunkType.TEXT_DELTA, + text: this.accumulate ? final.text : finalText + }) } - this.onChunk({ - type: ChunkType.TEXT_DELTA, - text: final.text || '' - }) break + } case 'text-end': this.onChunk({ type: ChunkType.TEXT_COMPLETE, @@ -200,7 +246,7 @@ export class AiSdkToChunkAdapter { [WebSearchSource.ANTHROPIC]: WebSearchSource.ANTHROPIC, [WebSearchSource.OPENROUTER]: WebSearchSource.OPENROUTER, [WebSearchSource.GEMINI]: WebSearchSource.GEMINI, - [WebSearchSource.PERPLEXITY]: WebSearchSource.PERPLEXITY, + // [WebSearchSource.PERPLEXITY]: WebSearchSource.PERPLEXITY, [WebSearchSource.QWEN]: WebSearchSource.QWEN, [WebSearchSource.HUNYUAN]: WebSearchSource.HUNYUAN, [WebSearchSource.ZHIPU]: WebSearchSource.ZHIPU, @@ -268,18 +314,9 @@ export class AiSdkToChunkAdapter { // === 源和文件相关事件 === case 'source': if (chunk.sourceType === 'url') { - // if (final.webSearchResults.length === 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { sourceType: _, ...rest } = chunk final.webSearchResults.push(rest) - // } - // this.onChunk({ - // type: ChunkType.LLM_WEB_SEARCH_COMPLETE, - // llm_web_search: { - // source: WebSearchSource.AISDK, - // results: final.webSearchResults - // } - // }) } break case 'file': diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index 8da8e8c7c1..0225d15d26 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -284,7 +284,7 @@ export default class ModernAiProvider { // 创建带有中间件的执行器 if (config.onChunk) { const accumulate = this.model!.supported_text_delta !== false // true and undefined - const adapter = new AiSdkToChunkAdapter(config.onChunk, config.mcpTools, accumulate) + const adapter = new AiSdkToChunkAdapter(config.onChunk, config.mcpTools, accumulate, config.enableWebSearch) const streamResult = await executor.streamText({ ...params, diff --git a/src/renderer/src/aiCore/legacy/middleware/core/WebSearchMiddleware.ts b/src/renderer/src/aiCore/legacy/middleware/core/WebSearchMiddleware.ts index 4c72e877a9..ae346af836 100644 --- a/src/renderer/src/aiCore/legacy/middleware/core/WebSearchMiddleware.ts +++ b/src/renderer/src/aiCore/legacy/middleware/core/WebSearchMiddleware.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import { ChunkType } from '@renderer/types/chunk' -import { flushLinkConverterBuffer, smartLinkConverter } from '@renderer/utils/linkConverter' +import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter' import { CompletionsParams, CompletionsResult, GenericChunk } from '../schemas' import { CompletionsContext, CompletionsMiddleware } from '../types' @@ -28,8 +28,6 @@ export const WebSearchMiddleware: CompletionsMiddleware = } // 调用下游中间件 const result = await next(ctx, params) - - const model = params.assistant?.model! let isFirstChunk = true // 响应后处理:记录Web搜索事件 @@ -42,15 +40,9 @@ export const WebSearchMiddleware: CompletionsMiddleware = new TransformStream({ transform(chunk: GenericChunk, controller) { if (chunk.type === ChunkType.TEXT_DELTA) { - const providerType = model.provider || 'openai' // 使用当前可用的Web搜索结果进行链接转换 const text = chunk.text - const result = smartLinkConverter( - text, - providerType, - isFirstChunk, - ctx._internal.webSearchState!.results - ) + const result = convertLinks(text, isFirstChunk) if (isFirstChunk) { isFirstChunk = false } diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index 04b5efd1b5..f0d3b2eb59 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -20,6 +20,7 @@ export interface AiSdkMiddlewareConfig { isSupportedToolUse: boolean // image generation endpoint isImageGenerationEndpoint: boolean + // 是否开启内置搜索 enableWebSearch: boolean enableGenerateImage: boolean enableUrlContext: boolean diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index ddbb178f3f..654b06114c 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -15,7 +15,7 @@ import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useV import { getProviderByModel } from '@renderer/services/AssistantService' import { loggerService } from '@renderer/services/LoggerService' import store from '@renderer/store' -import type { Model, Provider } from '@renderer/types' +import { isSystemProvider, type Model, type Provider } from '@renderer/types' import { formatApiHost } from '@renderer/utils/api' import { cloneDeep, isEmpty } from 'lodash' @@ -61,14 +61,16 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider { // return createVertexProvider(provider) // } - if (provider.id === 'aihubmix') { - return aihubmixProviderCreator(model, provider) - } - if (provider.id === 'newapi') { - return newApiResolverCreator(model, provider) - } - if (provider.id === 'vertexai') { - return vertexAnthropicProviderCreator(model, provider) + if (isSystemProvider(provider)) { + if (provider.id === 'aihubmix') { + return aihubmixProviderCreator(model, provider) + } + if (provider.id === 'new-api') { + return newApiResolverCreator(model, provider) + } + if (provider.id === 'vertexai') { + return vertexAnthropicProviderCreator(model, provider) + } } return provider } diff --git a/src/renderer/src/aiCore/provider/providerInitialization.ts b/src/renderer/src/aiCore/provider/providerInitialization.ts index cf3366d70a..3c188313b9 100644 --- a/src/renderer/src/aiCore/provider/providerInitialization.ts +++ b/src/renderer/src/aiCore/provider/providerInitialization.ts @@ -39,6 +39,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [ creatorFunctionName: 'createAmazonBedrock', supportsImageGeneration: true, aliases: ['aws-bedrock'] + }, + { + id: 'perplexity', + name: 'Perplexity', + import: () => import('@ai-sdk/perplexity'), + creatorFunctionName: 'createPerplexity', + supportsImageGeneration: false, + aliases: ['perplexity'] } ] as const diff --git a/src/renderer/src/assets/styles/font.css b/src/renderer/src/assets/styles/font.css index be71cef964..8e122fe887 100644 --- a/src/renderer/src/assets/styles/font.css +++ b/src/renderer/src/assets/styles/font.css @@ -1,23 +1,24 @@ :root { --font-family: - Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans', - 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + var(--user-font-family), Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, + Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; --font-family-serif: serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace; + --code-font-family: var(--user-code-font-family), 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace; } /* Windows系统专用字体配置 */ body[os='windows'] { --font-family: - 'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, - Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; + var(--user-font-family), 'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, + Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --code-font-family: - 'Cascadia Code', 'Fira Code', 'Consolas', 'Sarasa Mono SC', 'Microsoft YaHei UI', Courier, monospace; + var(--user-code-font-family), 'Cascadia Code', 'Fira Code', 'Consolas', 'Sarasa Mono SC', 'Microsoft YaHei UI', + Courier, monospace; } diff --git a/src/renderer/src/hooks/useUserTheme.ts b/src/renderer/src/hooks/useUserTheme.ts index 0b1bc5fb24..03191f5984 100644 --- a/src/renderer/src/hooks/useUserTheme.ts +++ b/src/renderer/src/hooks/useUserTheme.ts @@ -15,6 +15,10 @@ export default function useUserTheme() { document.body.style.setProperty('--primary', colorPrimary.toString()) document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString()) document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString()) + + // Set font family CSS variables + document.documentElement.style.setProperty('--user-font-family', `'${theme.userFontFamily}'`) + document.documentElement.style.setProperty('--user-code-font-family', `'${theme.userCodeFontFamily}'`) } return { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index c873762a3e..6dcf51aaeb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3135,6 +3135,13 @@ "placeholder": "/* Put custom CSS here */" } }, + "font": { + "code": "Code Font", + "default": "Default", + "global": "Global Font", + "select": "Select Font", + "title": "Font Settings" + }, "navbar": { "position": { "label": "Navbar Position", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f60434d807..e67f75c87d 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3138,6 +3138,13 @@ "placeholder": "/* 这里写自定义 CSS */" } }, + "font": { + "code": "代码字体", + "default": "默认", + "global": "全局字体", + "select": "选择字体", + "title": "字体设置" + }, "navbar": { "position": { "label": "导航栏位置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 753981be90..53ab001bb6 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3116,6 +3116,13 @@ "placeholder": "/* 這裡寫自訂 CSS */" } }, + "font": { + "code": "程式碼字型", + "default": "預設", + "global": "全域字型", + "select": "選擇字體", + "title": "字型設定" + }, "navbar": { "position": { "label": "導航欄位置", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 8f1c235ece..d06daef3bd 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3116,6 +3116,13 @@ "placeholder": "/* Γράψτε εδώ την προσαρμοστική CSS */" } }, + "font": { + "code": "γραμματοσειρά κώδικα", + "default": "προεπιλογή", + "global": "Γενική γραμματοσειρά", + "select": "Επιλέξτε γραμματοσειρά", + "title": "Ρύθμιση γραμματοσειράς" + }, "navbar": { "position": { "label": "Θέση Γραμμής Πλοήγησης", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 905e720b8e..5cabb387a4 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3116,6 +3116,13 @@ "placeholder": "/* Escribe tu CSS personalizado aquí */" } }, + "font": { + "code": "fuente de código", + "default": "predeterminado", + "global": "Fuente global", + "select": "Seleccionar fuente", + "title": "Configuración de fuente" + }, "navbar": { "position": { "label": "Posición de la barra de navegación", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 5c2a0d2a5d..49f70e76e0 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3116,6 +3116,13 @@ "placeholder": "/* Écrire votre CSS personnalisé ici */" } }, + "font": { + "code": "police de code", + "default": "Par défaut", + "global": "Police de caractère globale", + "select": "Sélectionner la police", + "title": "Paramètres de police" + }, "navbar": { "position": { "label": "Position de la barre de navigation", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 391cbec4e3..8a0348119d 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3116,6 +3116,13 @@ "placeholder": "/* ここにカスタムCSSを入力 */" } }, + "font": { + "code": "コードフォント", + "default": "デフォルト", + "global": "グローバルフォント", + "select": "フォントを選択", + "title": "フォント設定" + }, "navbar": { "position": { "label": "ナビゲーションバー位置", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4d1edba05d..1365994997 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3116,6 +3116,13 @@ "placeholder": "/* Escreva seu CSS personalizado aqui */" } }, + "font": { + "code": "fonte de código", + "default": "padrão", + "global": "Fonte global", + "select": "Selecionar fonte", + "title": "Configuração de fonte" + }, "navbar": { "position": { "label": "Posição da Barra de Navegação", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 493a565508..29ad608c03 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3116,6 +3116,13 @@ "placeholder": "/* Здесь введите пользовательский CSS */" } }, + "font": { + "code": "шрифт кода", + "default": "по умолчанию", + "global": "Глобальный шрифт", + "select": "Выбрать шрифт", + "title": "Настройки шрифта" + }, "navbar": { "position": { "label": "Положение навигации", diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index 2bedfbbd8b..b1d670792c 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -18,7 +18,7 @@ import { setSidebarIcons } from '@renderer/store/settings' import { ThemeMode } from '@renderer/types' -import { Button, ColorPicker, Segmented, Switch } from 'antd' +import { Button, ColorPicker, Segmented, Select, Switch } from 'antd' import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react' import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -78,6 +78,7 @@ const DisplaySettings: FC = () => { const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS) const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || []) + const [fontList, setFontList] = useState([]) const handleWindowStyleChange = useCallback( (checked: boolean) => { @@ -136,6 +137,11 @@ const DisplaySettings: FC = () => { ) useEffect(() => { + // 初始化获取所有系统字体 + window.api.getSystemFonts().then((fonts: string[]) => { + setFontList(fonts) + }) + // 初始化获取当前缩放值 window.api.handleZoomFactor(0).then((factor) => { setCurrentZoom(factor) @@ -160,6 +166,26 @@ const DisplaySettings: FC = () => { setCurrentZoom(zoomFactor) } + const handleUserFontChange = useCallback( + (value: string) => { + setUserTheme({ + ...userTheme, + userFontFamily: value + }) + }, + [setUserTheme, userTheme] + ) + + const handleUserCodeFontChange = useCallback( + (value: string) => { + setUserTheme({ + ...userTheme, + userCodeFontFamily: value + }) + }, + [setUserTheme, userTheme] + ) + const assistantIconTypeOptions = useMemo( () => [ { value: 'model', label: t('settings.assistant.icon.type.model') }, @@ -194,6 +220,7 @@ const DisplaySettings: FC = () => { ))} handleColorPrimaryChange(color.toHexString())} @@ -255,6 +282,75 @@ const DisplaySettings: FC = () => { + + + {t('settings.display.font.title')} + + + + {t('settings.display.font.global')} + + + {t('settings.display.font.default')} + + ), + value: '' + }, + ...fontList.map((font) => ({ label: {font}, value: font })) + ]} + value={userTheme.userCodeFontFamily || ''} + onChange={(font) => handleUserCodeFontChange(font)} + showSearch + getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body} + /> +