diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 170c4ca909..2c15302bee 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -1,5 +1,8 @@ name: Pull Request CI +permissions: + contents: read + on: workflow_dispatch: pull_request: diff --git a/.yarn/patches/antd-npm-5.26.7-029c5c381a.patch b/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch similarity index 100% rename from .yarn/patches/antd-npm-5.26.7-029c5c381a.patch rename to .yarn/patches/antd-npm-5.27.0-aa91c36546.patch diff --git a/electron-builder.yml b/electron-builder.yml index 7c40aad3d8..ce50a8ec36 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -119,9 +119,11 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - 支持 GPT-5 模型 - 新增代码工具,支持快速启动 Qwen Code, Gemini Cli, Claude Code - 翻译页面改版,支持更多设置 - 支持保存整个话题到知识库 - 坚果云备份支持设置最大备份数量 + 输入框快捷菜单增加清除按钮 + 侧边栏增加代码工具入口,代码工具增加环境变量设置 + 小程序增加多语言显示 + 优化 MCP 服务器列表 + 新增 Web 搜索图标 + 优化 SVG 预览,优化 HTML 内容样式 + 修复知识库文档预处理失败问题 稳定性改进和错误修复 diff --git a/package.json b/package.json index f0e8a02b3d..46566cc422 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.5.6", + "version": "1.5.7-rc.1", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -165,7 +165,7 @@ "@viz-js/lang-dot": "^1.0.5", "@viz-js/viz": "^3.14.0", "@xyflow/react": "^12.4.4", - "antd": "patch:antd@npm%3A5.26.7#~/.yarn/patches/antd-npm-5.26.7-029c5c381a.patch", + "antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch", "archiver": "^7.0.1", "async-mutex": "^0.5.0", "axios": "^1.7.3", @@ -228,7 +228,6 @@ "prettier": "^3.5.3", "prettier-plugin-sort-json": "^4.1.1", "proxy-agent": "^6.5.0", - "rc-virtual-list": "^3.18.6", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.6.1", @@ -246,7 +245,9 @@ "reflect-metadata": "0.2.2", "rehype-katex": "^7.0.1", "rehype-mathjax": "^7.1.0", + "rehype-parse": "^9.0.1", "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", "remark-cjk-friendly": "^1.2.0", "remark-gfm": "^4.0.1", "remark-github-blockquote-alert": "^2.0.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 9bd70ba02d..78044e3a3e 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -35,6 +35,7 @@ export enum IpcChannel { App_InstallBunBinary = 'app:install-bun-binary', App_LogToMain = 'app:log-to-main', App_SaveData = 'app:save-data', + App_SetFullScreen = 'app:set-full-screen', 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 c7ae15a123..7c307741b7 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -192,6 +192,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) } + ipcMain.handle(IpcChannel.App_SetFullScreen, (_, value: boolean): void => { + mainWindow.setFullScreen(value) + }) + ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { configManager.set(key, value, isNotify) }) diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 3ed00dae1a..5fa2ce87c2 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' +import { isWin } from '@main/constant' import { removeEnvProxy } from '@main/utils' import { isUserInChina } from '@main/utils/ipService' import { getBinaryName } from '@main/utils/process' @@ -114,9 +115,21 @@ class CodeToolsService { } else { logger.info(`Fetching latest version for ${packageName} from npm`) try { - const bunPath = await this.getBunPath() - const { stdout } = await execAsync(`"${bunPath}" info ${packageName} version`, { timeout: 15000 }) - latestVersion = stdout.trim().replace(/["']/g, '') + // Get registry URL + const registryUrl = await this.getNpmRegistryUrl() + + // Fetch package info directly from npm registry API + const packageUrl = `${registryUrl}/${packageName}/latest` + const response = await fetch(packageUrl, { + signal: AbortSignal.timeout(15000) + }) + + if (!response.ok) { + throw new Error(`Failed to fetch package info: ${response.statusText}`) + } + + const packageInfo = await response.json() + latestVersion = packageInfo.version logger.info(`${packageName} latest version: ${latestVersion}`) // Cache the result @@ -179,7 +192,7 @@ class CodeToolsService { ? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&` : `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&` - const updateCommand = `${installEnvPrefix} "${bunPath}" install -g ${packageName}` + const updateCommand = `${installEnvPrefix} ${bunPath} install -g ${packageName}` logger.info(`Executing update command: ${updateCommand}`) await execAsync(updateCommand, { timeout: 60000 }) @@ -283,12 +296,11 @@ class CodeToolsService { } // Build command to execute - let baseCommand: string + let baseCommand = isWin ? `${executablePath}` : `${bunPath} ${executablePath}` const bunInstallPath = path.join(os.homedir(), '.cherrystudio') if (isInstalled) { // If already installed, run executable directly (with optional update message) - baseCommand = `"${executablePath}"` if (updateMessage) { baseCommand = `echo "Checking ${cliTool} version..."${updateMessage} && ${baseCommand}` } @@ -300,8 +312,8 @@ class CodeToolsService { ? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&` : `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&` - const installCommand = `${installEnvPrefix} "${bunPath}" install -g ${packageName}` - baseCommand = `echo "Installing ${packageName}..." && ${installCommand} && echo "Installation complete, starting ${cliTool}..." && "${executablePath}"` + const installCommand = `${installEnvPrefix} ${bunPath} install -g ${packageName}` + baseCommand = `echo "Installing ${packageName}..." && ${installCommand} && echo "Installation complete, starting ${cliTool}..." && ${baseCommand}` } switch (platform) { diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 185901322f..e2c2dc4866 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -223,26 +223,26 @@ export class WindowService { }) // 添加Escape键退出全屏的支持 - mainWindow.webContents.on('before-input-event', (event, input) => { - // 当按下Escape键且窗口处于全屏状态时退出全屏 - if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) { - if (mainWindow.isFullScreen()) { - // 获取 shortcuts 配置 - const shortcuts = configManager.getShortcuts() - const exitFullscreenShortcut = shortcuts.find((s) => s.key === 'exit_fullscreen') - if (exitFullscreenShortcut == undefined) { - mainWindow.setFullScreen(false) - return - } - if (exitFullscreenShortcut?.enabled) { - event.preventDefault() - mainWindow.setFullScreen(false) - return - } - } - } - return - }) + // mainWindow.webContents.on('before-input-event', (event, input) => { + // // 当按下Escape键且窗口处于全屏状态时退出全屏 + // if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) { + // if (mainWindow.isFullScreen()) { + // // 获取 shortcuts 配置 + // const shortcuts = configManager.getShortcuts() + // const exitFullscreenShortcut = shortcuts.find((s) => s.key === 'exit_fullscreen') + // if (exitFullscreenShortcut == undefined) { + // mainWindow.setFullScreen(false) + // return + // } + // if (exitFullscreenShortcut?.enabled) { + // event.preventDefault() + // mainWindow.setFullScreen(false) + // return + // } + // } + // } + // return + // }) } private setupWebContentsHandlers(mainWindow: BrowserWindow) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 09c5046ec7..7f21040302 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -77,6 +77,7 @@ const api = { clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), logToMain: (source: LogSourceWithContext, level: LogLevel, message: string, data: any[]) => ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data), + setFullScreen: (value: boolean): Promise => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value), mac: { isProcessTrusted: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), requestProcessTrust: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) @@ -296,7 +297,8 @@ const api = { return ipcRenderer.invoke(IpcChannel.Mcp_UploadDxt, buffer, file.name) }, abortTool: (callId: string) => ipcRenderer.invoke(IpcChannel.Mcp_AbortTool, callId), - getServerVersion: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server) + getServerVersion: (server: MCPServer): Promise => + ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server) }, python: { execute: (script: string, context?: Record, timeout?: number) => diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 7568ee69be..7fcae3823c 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -5,7 +5,9 @@ import { GEMINI_FLASH_MODEL_REGEX, getOpenAIWebSearchParams, getThinkModelType, + isClaudeReasoningModel, isDoubaoThinkingAutoModel, + isGeminiReasoningModel, isGPT5SeriesModel, isGrokReasoningModel, isNotSupportSystemMessageModel, @@ -46,6 +48,7 @@ import { Model, OpenAIServiceTier, Provider, + SystemProviderIds, ToolCallResponse, TranslateAssistant, WebSearchSource @@ -557,17 +560,28 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } } - const lastUserMsg = userMessages.findLast((m) => m.role === 'user') - if ( - lastUserMsg && - isSupportedThinkingTokenQwenModel(model) && - !isSupportEnableThinkingProvider(this.provider) - ) { - const postsuffix = '/no_think' - const qwenThinkModeEnabled = assistant.settings?.qwenThinkMode === true - const currentContent = lastUserMsg.content + // poe 需要通过用户消息传递 reasoningEffort + const reasoningEffort = this.getReasoningEffort(assistant, model) - lastUserMsg.content = processPostsuffixQwen3Model(currentContent, postsuffix, qwenThinkModeEnabled) as any + const lastUserMsg = userMessages.findLast((m) => m.role === 'user') + if (lastUserMsg) { + if (isSupportedThinkingTokenQwenModel(model) && !isSupportEnableThinkingProvider(this.provider)) { + const postsuffix = '/no_think' + const qwenThinkModeEnabled = assistant.settings?.qwenThinkMode === true + const currentContent = lastUserMsg.content + + lastUserMsg.content = processPostsuffixQwen3Model(currentContent, postsuffix, qwenThinkModeEnabled) as any + } + if (this.provider.id === SystemProviderIds.poe) { + // 如果以后 poe 支持 reasoning_effort 参数了,可以删掉这部分 + if (isGPT5SeriesModel(model) && reasoningEffort.reasoning_effort) { + lastUserMsg.content += ` --reasoning_effort ${reasoningEffort.reasoning_effort}` + } else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) { + lastUserMsg.content += ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}` + } else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) { + lastUserMsg.content += ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}` + } + } } // 4. 最终请求消息 @@ -585,8 +599,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient< // Note: Some providers like Mistral don't support stream_options const shouldIncludeStreamOptions = streamOutput && isSupportStreamOptionsProvider(this.provider) - const reasoningEffort = this.getReasoningEffort(assistant, model) - // minimal cannot be used with web_search tool if (isGPT5SeriesModel(model) && reasoningEffort.reasoning_effort === 'minimal' && enableWebSearch) { reasoningEffort.reasoning_effort = 'low' diff --git a/src/renderer/src/aiCore/middleware/schemas.ts b/src/renderer/src/aiCore/middleware/schemas.ts index d429add463..fcb59d4aff 100644 --- a/src/renderer/src/aiCore/middleware/schemas.ts +++ b/src/renderer/src/aiCore/middleware/schemas.ts @@ -22,8 +22,10 @@ export interface CompletionsParams { * 'search': 搜索摘要 * 'generate': 生成 * 'check': API检查 + * 'test': 测试调用 + * 'translate-lang-detect': 翻译语言检测 */ - callType?: 'chat' | 'translate' | 'summary' | 'search' | 'generate' | 'check' | 'test' + callType?: 'chat' | 'translate' | 'summary' | 'search' | 'generate' | 'check' | 'test' | 'translate-lang-detect' // 基础对话数据 messages: Message[] | string // 联合类型方便判断是否为空 diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss index 7a2a2ce271..9ebc658010 100644 --- a/src/renderer/src/assets/styles/ant.scss +++ b/src/renderer/src/assets/styles/ant.scss @@ -184,3 +184,28 @@ box-shadow: 0 1px 4px 0px rgb(128 128 128 / 50%) !important; } } + +.ant-splitter-bar { + .ant-splitter-bar-dragger { + &::before { + background-color: var(--color-border) !important; + transition: + background-color 0.15s ease, + width 0.15s ease; + } + &:hover { + &::before { + width: 4px !important; + background-color: var(--color-primary) !important; + transition-delay: 0.15s; + } + } + } + + .ant-splitter-bar-dragger-active { + &::before { + width: 4px !important; + background-color: var(--color-primary) !important; + } + } +} diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 6bd29fc8ac..2760ecb6e5 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -103,7 +103,7 @@ } span { - white-space: pre; + white-space: pre-wrap; } p code, @@ -260,10 +260,6 @@ text-decoration: underline; } } - - > *:last-child { - margin-bottom: 0 !important; - } } .footnotes { diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx index 67b583e9e6..3a82db90fa 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx @@ -1,11 +1,11 @@ -import { CodeOutlined, LinkOutlined } from '@ant-design/icons' +import { CodeOutlined } from '@ant-design/icons' import { loggerService } from '@logger' import { useTheme } from '@renderer/context/ThemeProvider' import { ThemeMode } from '@renderer/types' import { extractTitle } from '@renderer/utils/formats' import { Button } from 'antd' -import { Code, Download, Globe, Sparkles } from 'lucide-react' -import { FC, useMemo, useState } from 'react' +import { Code, DownloadIcon, Globe, LinkIcon, Sparkles } from 'lucide-react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import { ClipLoader } from 'react-spinners' import styled, { keyframes } from 'styled-components' @@ -14,92 +14,10 @@ import HtmlArtifactsPopup from './HtmlArtifactsPopup' const logger = loggerService.withContext('HtmlArtifactsCard') -const HTML_VOID_ELEMENTS = new Set([ - 'area', - 'base', - 'br', - 'col', - 'embed', - 'hr', - 'img', - 'input', - 'link', - 'meta', - 'param', - 'source', - 'track', - 'wbr' -]) - -const HTML_COMPLETION_PATTERNS = [ - /<\/html\s*>/i, - //i, - /<\/div\s*>/i, - /<\/script\s*>/i, - /<\/style\s*>/i -] - interface Props { html: string -} - -function hasUnmatchedTags(html: string): boolean { - const stack: string[] = [] - const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g - let match - - while ((match = tagRegex.exec(html)) !== null) { - const [fullTag, tagName] = match - const isClosing = fullTag.startsWith('') || HTML_VOID_ELEMENTS.has(tagName.toLowerCase()) - - if (isSelfClosing) continue - - if (isClosing) { - if (stack.length === 0 || stack.pop() !== tagName.toLowerCase()) { - return true - } - } else { - stack.push(tagName.toLowerCase()) - } - } - - return stack.length > 0 -} - -function checkIsStreaming(html: string): boolean { - if (!html?.trim()) return false - - const trimmed = html.trim() - - // 快速检查:如果有明显的完成标志,直接返回false - for (const pattern of HTML_COMPLETION_PATTERNS) { - if (pattern.test(trimmed)) { - // 特殊情况:同时有DOCTYPE和 - if (trimmed.includes('/i.test(trimmed)) { - return false - } - // 如果只是以结尾,也认为是完成的 - if (/<\/html\s*>$/i.test(trimmed)) { - return false - } - } - } - - // 检查未完成的标志 - const hasIncompleteTag = /<[^>]*$/.test(trimmed) - const hasUnmatched = hasUnmatchedTags(trimmed) - - if (hasIncompleteTag || hasUnmatched) return true - - // 对于简单片段,如果长度较短且没有明显结束标志,可能还在生成 - const hasStructureTags = /<(html|body|head)[^>]*>/i.test(trimmed) - if (!hasStructureTags && trimmed.length < 500) { - return !HTML_COMPLETION_PATTERNS.some((pattern) => pattern.test(trimmed)) - } - - return false + onSave?: (html: string) => void + isStreaming?: boolean } const getTerminalStyles = (theme: ThemeMode) => ({ @@ -108,7 +26,7 @@ const getTerminalStyles = (theme: ThemeMode) => ({ promptColor: theme === 'dark' ? '#00ff00' : '#007700' }) -const HtmlArtifactsCard: FC = ({ html }) => { +const HtmlArtifactsCard: FC = ({ html, onSave, isStreaming = false }) => { const { t } = useTranslation() const title = extractTitle(html) || 'HTML Artifacts' const [isPopupOpen, setIsPopupOpen] = useState(false) @@ -116,7 +34,6 @@ const HtmlArtifactsCard: FC = ({ html }) => { const htmlContent = html || '' const hasContent = htmlContent.trim().length > 0 - const isStreaming = useMemo(() => checkIsStreaming(htmlContent), [htmlContent]) const handleOpenExternal = async () => { const path = await window.api.file.createTempFile('artifacts-preview.html') @@ -181,10 +98,10 @@ const HtmlArtifactsCard: FC = ({ html }) => { - - @@ -192,7 +109,13 @@ const HtmlArtifactsCard: FC = ({ html }) => { - setIsPopupOpen(false)} /> + setIsPopupOpen(false)} + /> ) } @@ -286,7 +209,6 @@ const ButtonContainer = styled.div` margin: 10px 16px !important; display: flex; flex-direction: row; - gap: 8px; ` const TerminalPreview = styled.div<{ $theme: ThemeMode }>` @@ -294,7 +216,7 @@ const TerminalPreview = styled.div<{ $theme: ThemeMode }>` background: ${(props) => getTerminalStyles(props.$theme).background}; border-radius: 8px; overflow: hidden; - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; + font-family: var(--code-font-family); ` const TerminalContent = styled.div<{ $theme: ThemeMode }>` diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index dba34f29a7..a548d5e163 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -1,8 +1,8 @@ -import CodeEditor from '@renderer/components/CodeEditor' +import CodeEditor, { CodeEditorHandles } from '@renderer/components/CodeEditor' import { isLinux, isMac, isWin } from '@renderer/config/constant' import { classNames } from '@renderer/utils' -import { Button, Modal } from 'antd' -import { Code, Maximize2, Minimize2, Monitor, MonitorSpeaker, X } from 'lucide-react' +import { Button, Modal, Splitter, Tooltip } from 'antd' +import { Code, Eye, Maximize2, Minimize2, SaveIcon, SquareSplitHorizontal, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -11,60 +11,17 @@ interface HtmlArtifactsPopupProps { open: boolean title: string html: string + onSave?: (html: string) => void onClose: () => void } type ViewMode = 'split' | 'code' | 'preview' -const HtmlArtifactsPopup: React.FC = ({ open, title, html, onClose }) => { +const HtmlArtifactsPopup: React.FC = ({ open, title, html, onSave, onClose }) => { const { t } = useTranslation() const [viewMode, setViewMode] = useState('split') - const [currentHtml, setCurrentHtml] = useState(html) const [isFullscreen, setIsFullscreen] = useState(false) - - // Preview refresh related state - const [previewHtml, setPreviewHtml] = useState(html) - const intervalRef = useRef(null) - const latestHtmlRef = useRef(html) - const currentPreviewHtmlRef = useRef(html) - - // Sync internal state when external html updates - useEffect(() => { - setCurrentHtml(html) - latestHtmlRef.current = html - }, [html]) - - // Update reference when internally edited html changes - useEffect(() => { - latestHtmlRef.current = currentHtml - }, [currentHtml]) - - // Update reference when preview content changes - useEffect(() => { - currentPreviewHtmlRef.current = previewHtml - }, [previewHtml]) - - // Check and refresh preview every 2 seconds (only when content changes) - useEffect(() => { - if (!open) return - - // Set initial preview content immediately - setPreviewHtml(latestHtmlRef.current) - - // Set timer to check for content changes every 2 seconds - intervalRef.current = setInterval(() => { - if (latestHtmlRef.current !== currentPreviewHtmlRef.current) { - setPreviewHtml(latestHtmlRef.current) - } - }, 2000) - - // Cleanup function - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - } - } - }, [open]) + const codeEditorRef = useRef(null) // Prevent body scroll when fullscreen useEffect(() => { @@ -79,8 +36,9 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht } }, [isFullscreen, open]) - const showCode = viewMode === 'split' || viewMode === 'code' - const showPreview = viewMode === 'split' || viewMode === 'preview' + const handleSave = () => { + codeEditorRef.current?.save?.() + } const renderHeader = () => ( setIsFullscreen(!isFullscreen)} className={classNames({ drag: isFullscreen })}> @@ -93,7 +51,7 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht } + icon={} onClick={() => setViewMode('split')}> {t('html_artifacts.split')} @@ -107,7 +65,7 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht } + icon={} onClick={() => setViewMode('preview')}> {t('html_artifacts.preview')} @@ -126,6 +84,75 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht ) + const renderContent = () => { + const codePanel = ( + + + + + - - } - /> - - )} - - - -
{t('code.cli_tool')}
- { - const label = typeof option?.label === 'string' ? option.label : String(option?.value || '') - return label.toLowerCase().includes(input.toLowerCase()) - }} - options={directories.map((dir) => ({ - value: dir, - label: ( + {/* Bun 安装状态提示 */} + {!isBunInstalled && ( + + - {dir} - handleRemoveDirectory(dir, e)} - /> + {t('code.bun_required_message')} + - ) - }))} - /> - - -
+ } + /> + + )} - -
{t('code.update_options')}
- setAutoUpdateToLatest(e.target.checked)}> - {t('code.auto_update_to_latest')} - -
-
+ + +
{t('code.cli_tool')}
+ { + const label = typeof option?.label === 'string' ? option.label : String(option?.value || '') + return label.toLowerCase().includes(input.toLowerCase()) + }} + options={directories.map((dir) => ({ + value: dir, + label: ( +
+ {dir} + handleRemoveDirectory(dir, e)} + /> +
+ ) + }))} + /> + + +
+ + +
{t('code.environment_variables')}
+ +
{t('code.env_vars_help')}
+
+ + +
{t('code.update_options')}
+ setAutoUpdateToLatest(e.target.checked)}> + {t('code.auto_update_to_latest')} + +
+
+ + + + ) } -// 样式组件 const Container = styled.div` + display: flex; + flex: 1; + flex-direction: column; +` + +const ContentContainer = styled.div` + display: flex; + flex: 1; +` + +const MainContent = styled.div` width: 600px; margin: auto; ` @@ -347,7 +419,6 @@ const Title = styled.h1` font-size: 24px; font-weight: 600; margin-bottom: 8px; - margin-top: -50px; color: var(--color-text-1); ` diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index b84f4d6274..df690c03d0 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -20,6 +20,7 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' +import useTranslate from '@renderer/hooks/useTranslate' import { getDefaultTopic } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import FileManager from '@renderer/services/FileManager' @@ -43,7 +44,6 @@ import { getTextFromDropEvent, isSendMessageKeyPressed } from '@renderer/utils/input' -import { getLanguageByLangcode } from '@renderer/utils/translate' import { documentExts, imageExts, textExts } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { Button, Tooltip } from 'antd' @@ -58,8 +58,6 @@ import styled from 'styled-components' import NarrowLayout from '../Messages/NarrowLayout' import AttachmentPreview from './AttachmentPreview' import InputbarTools, { InputbarToolsRef } from './InputbarTools' -import KnowledgeBaseInput from './KnowledgeBaseInput' -import MentionModelsInput from './MentionModelsInput' import SendMessageButton from './SendMessageButton' import TokenCount from './TokenCount' @@ -87,15 +85,15 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = showInputEstimatedTokens, autoTranslateWithSpace, enableQuickPanelTriggers, - enableBackspaceDeleteModel, enableSpellCheck } = useSettings() - const [expended, setExpend] = useState(false) + const [expanded, setExpand] = useState(false) const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [contextCount, setContextCount] = useState({ current: 0, max: 0 }) const textareaRef = useRef(null) const [files, setFiles] = useState(_files) const { t } = useTranslation() + const { getLanguageByLangcode } = useTranslate() const containerRef = useRef(null) const { searching } = useRuntime() const { pauseMessages } = useMessageOperations(topic) @@ -258,7 +256,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setFiles([]) setTimeout(() => setText(''), 500) setTimeout(() => resizeTextArea(true), 0) - setExpend(false) + setExpand(false) } catch (error) { logger.warn('Failed to send message:', error as Error) parent?.recordException(error as Error) @@ -280,7 +278,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } finally { setIsTranslating(false) } - }, [isTranslating, text, targetLanguage, resizeTextArea]) + }, [isTranslating, text, getLanguageByLangcode, targetLanguage, resizeTextArea]) const openKnowledgeFileList = useCallback( (base: KnowledgeBase) => { @@ -398,9 +396,10 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } } - if (expended) { + if (expanded) { if (event.key === 'Escape') { - return onToggleExpended() + event.stopPropagation() + return onToggleExpanded() } } @@ -438,12 +437,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } } - if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionedModels.length > 0) { - setMentionedModels((prev) => prev.slice(0, -1)) - return event.preventDefault() - } - - if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && files.length > 0) { + if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) { setFiles((prev) => prev.slice(0, -1)) return event.preventDefault() } @@ -500,7 +494,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) } - const onInput = () => !expended && resizeTextArea() + const onInput = () => !expanded && resizeTextArea() const onChange = useCallback( (e: React.ChangeEvent) => { @@ -530,7 +524,11 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') { - inputbarToolsRef.current?.openMentionModelsPanel() + inputbarToolsRef.current?.openMentionModelsPanel({ + type: 'input', + position: cursorPosition - 1, + originalText: newText + }) } }, [enableQuickPanelTriggers, quickPanel, t, files, couldAddImageFile, openSelectFileMenu, translate] @@ -636,7 +634,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = if (textArea) { textArea.style.height = `${newHeight}px` - setExpend(newHeight == maxHeightInPixels) + setExpand(newHeight == maxHeightInPixels) setTextareaHeight(newHeight) } }, @@ -761,19 +759,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setSelectedKnowledgeBases(bases ?? []) } - const handleRemoveModel = (model: Model) => { - setMentionedModels(mentionedModels.filter((m) => m.id !== model.id)) - } - - const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => { - const newKnowledgeBases = assistant.knowledge_bases?.filter((kb) => kb.id !== knowledgeBase.id) - updateAssistant({ - ...assistant, - knowledge_bases: newKnowledgeBases - }) - setSelectedKnowledgeBases(newKnowledgeBases ?? []) - } - const onEnableGenerateImage = () => { updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage }) } @@ -809,10 +794,12 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = [couldMentionNotVisionModel] ) - const onToggleExpended = () => { - const currentlyExpanded = expended || !!textareaHeight + const onClearMentionModels = useCallback(() => setMentionedModels([]), [setMentionedModels]) + + const onToggleExpanded = () => { + const currentlyExpanded = expanded || !!textareaHeight const shouldExpand = !currentlyExpanded - setExpend(shouldExpand) + setExpand(shouldExpand) const textArea = textareaRef.current?.resizableTextArea?.textArea if (!textArea) return if (shouldExpand) { @@ -832,7 +819,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = focusTextarea() } - const isExpended = expended || !!textareaHeight + const isExpanded = expanded || !!textareaHeight const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model) if (isMultiSelectMode) { @@ -853,15 +840,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = className={classNames('inputbar-container', inputFocus && 'focus', isFileDragging && 'file-dragging')} ref={containerRef}> {files.length > 0 && } - {selectedKnowledgeBases.length > 0 && ( - - )} - {mentionedModels.length > 0 && ( - - )}