diff --git a/package.json b/package.json index 26cfc51cef..4ee48cc4e1 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@langchain/ollama": "^0.2.1", "@langchain/openai": "^0.6.7", "@mistralai/mistralai": "^1.7.5", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.5", "@mozilla/readability": "^0.6.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^1.1.2", @@ -221,6 +221,7 @@ "axios": "^1.7.3", "browser-image-compression": "^2.0.2", "chardet": "^2.1.0", + "check-disk-space": "3.4.0", "cheerio": "^1.1.2", "chokidar": "^4.0.3", "cli-progress": "^3.12.0", diff --git a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts index 04d3bd5cdb..fce028f5cd 100644 --- a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts +++ b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts @@ -156,8 +156,10 @@ Assistant: The population of Shanghai is 26 million, while Guangzhou has a popul /** * 构建可用工具部分(提取自 Cherry Studio) */ -function buildAvailableTools(tools: ToolSet): string { +function buildAvailableTools(tools: ToolSet): string | null { const availableTools = Object.keys(tools) + if (availableTools.length === 0) return null + const result = availableTools .map((toolName: string) => { const tool = tools[toolName] return ` @@ -172,7 +174,7 @@ function buildAvailableTools(tools: ToolSet): string { }) .join('\n') return ` -${availableTools} +${result} ` } @@ -181,6 +183,7 @@ ${availableTools} */ function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet): string { const availableTools = buildAvailableTools(tools) + if (availableTools === null) return userSystemPrompt const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES) .replace('{{ AVAILABLE_TOOLS }}', availableTools) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 87243df0c7..fa02991682 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_GetDiskInfo = 'app:get-disk-info', App_SetFullScreen = 'app:set-full-screen', App_IsFullScreen = 'app:is-full-screen', diff --git a/packages/shared/utils.ts b/packages/shared/utils.ts new file mode 100644 index 0000000000..e87e2f2bef --- /dev/null +++ b/packages/shared/utils.ts @@ -0,0 +1,6 @@ +export const defaultAppHeaders = () => { + return { + 'HTTP-Referer': 'https://cherry-ai.com', + 'X-Title': 'Cherry Studio' + } +} diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 875e70b656..6b36a96a35 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -12,6 +12,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant' 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 { Notification } from 'src/renderer/src/types/notification' @@ -783,6 +784,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { addStreamMessage(spanId, modelName, context, msg) ) + ipcMain.handle(IpcChannel.App_GetDiskInfo, async (_, directoryPath: string) => { + try { + const diskSpace = await checkDiskSpace(directoryPath) // { free, size } in bytes + logger.debug('disk space', diskSpace) + const { free, size } = diskSpace + return { + free, + size + } + } catch (error) { + logger.error('check disk space error', error as Error) + return null + } + }) // API Server apiServerService.registerIpcHandlers() diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ecb34500c5..710867da88 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -16,6 +16,7 @@ import { type StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory' +import { McpError, type Tool as SDKTool } from '@modelcontextprotocol/sdk/types' // Import notification schemas from MCP SDK import { CancelledNotificationSchema, @@ -29,6 +30,7 @@ import { import { nanoid } from '@reduxjs/toolkit' import { MCPProgressEvent } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' +import { defaultAppHeaders } from '@shared/utils' import { BuiltinMCPServerNames, type GetResourceResponse, @@ -94,7 +96,7 @@ function getServerLogger(server: MCPServer, extra?: Record) { baseUrl: server?.baseUrl, type: server?.type || (server?.command ? 'stdio' : server?.baseUrl ? 'http' : 'inmemory') } - return loggerService.withContext('MCPService', { ...base, ...(extra || {}) }) + return loggerService.withContext('MCPService', { ...base, ...extra }) } /** @@ -193,11 +195,18 @@ class McpService { return existingClient } } catch (error: any) { - getServerLogger(server).error(`Error pinging server`, error as Error) + getServerLogger(server).error(`Error pinging server ${server.name}`, error as Error) this.clients.delete(serverKey) } } + const prepareHeaders = () => { + return { + ...defaultAppHeaders(), + ...server.headers + } + } + // Create a promise for the initialization process const initPromise = (async () => { try { @@ -235,8 +244,11 @@ class McpService { } else if (server.baseUrl) { if (server.type === 'streamableHttp') { const options: StreamableHTTPClientTransportOptions = { + fetch: async (url, init) => { + return net.fetch(typeof url === 'string' ? url : url.toString(), init) + }, requestInit: { - headers: server.headers || {} + headers: prepareHeaders() }, authProvider } @@ -249,25 +261,11 @@ class McpService { const options: SSEClientTransportOptions = { eventSourceInit: { fetch: async (url, init) => { - const headers = { ...(server.headers || {}), ...(init?.headers || {}) } - - // Get tokens from authProvider to make sure using the latest tokens - if (authProvider && typeof authProvider.tokens === 'function') { - try { - const tokens = await authProvider.tokens() - if (tokens && tokens.access_token) { - headers['Authorization'] = `Bearer ${tokens.access_token}` - } - } catch (error) { - getServerLogger(server).error('Failed to fetch tokens:', error as Error) - } - } - - return net.fetch(typeof url === 'string' ? url : url.toString(), { ...init, headers }) + return net.fetch(typeof url === 'string' ? url : url.toString(), init) } }, requestInit: { - headers: server.headers || {} + headers: prepareHeaders() }, authProvider } @@ -444,9 +442,9 @@ class McpService { logger.debug(`Activated server: ${server.name}`) return client - } catch (error: any) { - getServerLogger(server).error(`Error activating server`, error as Error) - throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`) + } catch (error) { + getServerLogger(server).error(`Error activating server ${server.name}`, error as Error) + throw error } } finally { // Clean up the pending promise when done @@ -614,12 +612,11 @@ class McpService { } private async listToolsImpl(server: MCPServer): Promise { - getServerLogger(server).debug(`Listing tools`) const client = await this.initClient(server) try { const { tools } = await client.listTools() const serverTools: MCPTool[] = [] - tools.map((tool: any) => { + tools.map((tool: SDKTool) => { const serverTool: MCPTool = { ...tool, id: buildFunctionCallToolName(server.name, tool.name), @@ -628,11 +625,12 @@ class McpService { type: 'mcp' } serverTools.push(serverTool) + getServerLogger(server).debug(`Listing tools`, { tool: serverTool }) }) return serverTools - } catch (error: any) { + } catch (error: unknown) { getServerLogger(server).error(`Failed to list tools`, error as Error) - return [] + throw error } } @@ -739,9 +737,9 @@ class McpService { serverId: server.id, serverName: server.name })) - } catch (error: any) { + } catch (error: unknown) { // -32601 is the code for the method not found - if (error?.code !== -32601) { + if (error instanceof McpError && error.code !== -32601) { getServerLogger(server).error(`Failed to list prompts`, error as Error) } return [] diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 97e87bf9ea..f9363a500e 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -398,11 +398,15 @@ export function validateFileName(fileName: string, platform = process.platform): * @returns 合法的文件名 */ export function checkName(fileName: string): string { - const validation = validateFileName(fileName) + const baseName = path.basename(fileName) + const validation = validateFileName(baseName) if (!validation.valid) { - throw new Error(`Invalid file name: ${fileName}. ${validation.error}`) + // 自动清理非法字符,而不是抛出错误 + const sanitized = sanitizeFilename(baseName) + logger.warn(`File name contains invalid characters, auto-sanitized: "${baseName}" -> "${sanitized}"`) + return sanitized } - return fileName + return baseName } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index 0c9e89ad76..d8c55dd256 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -44,6 +44,8 @@ export function tracedInvoke(channel: string, spanContext: SpanContext | undefin // Custom APIs for renderer const api = { getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info), + getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> => + ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), setProxy: (proxy: string | undefined, bypassRules?: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), diff --git a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts index bc146ae685..430b9e4df7 100644 --- a/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/BaseApiClient.ts @@ -46,6 +46,7 @@ import { isJSON, parseJSON } from '@renderer/utils' import { addAbortController, removeAbortController } from '@renderer/utils/abortController' import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { defaultTimeout } from '@shared/config/constant' +import { defaultAppHeaders } from '@shared/utils' import { isEmpty } from 'lodash' import { CompletionsContext } from '../middleware/types' @@ -179,8 +180,7 @@ export abstract class BaseApiClient< public defaultHeaders() { return { - 'HTTP-Referer': 'https://cherry-ai.com', - 'X-Title': 'Cherry Studio', + ...defaultAppHeaders(), 'X-Api-Key': this.apiKey } } diff --git a/src/renderer/src/assets/images/banner.png b/src/renderer/src/assets/images/banner.png deleted file mode 100644 index e29198cf82..0000000000 Binary files a/src/renderer/src/assets/images/banner.png and /dev/null differ diff --git a/src/renderer/src/assets/styles/index.css b/src/renderer/src/assets/styles/index.css index 40899314d0..b344e60ae6 100644 --- a/src/renderer/src/assets/styles/index.css +++ b/src/renderer/src/assets/styles/index.css @@ -169,10 +169,6 @@ ul { display: flow-root; } -.bubble:not(.multi-select-mode) .markdown *:last-child { - margin-bottom: 0; -} - .lucide:not(.lucide-custom) { color: var(--color-icon); } diff --git a/src/renderer/src/components/HorizontalScrollContainer/index.tsx b/src/renderer/src/components/HorizontalScrollContainer/index.tsx new file mode 100644 index 0000000000..ed5cdc52de --- /dev/null +++ b/src/renderer/src/components/HorizontalScrollContainer/index.tsx @@ -0,0 +1,179 @@ +import Scrollbar from '@renderer/components/Scrollbar' +import { ChevronRight } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import styled from 'styled-components' + +/** + * 水平滚动容器 + * @param children 子元素 + * @param dependencies 依赖项 + * @param scrollDistance 滚动距离 + * @param className 类名 + * @param gap 间距 + * @param expandable 是否可展开 + */ +export interface HorizontalScrollContainerProps { + children: React.ReactNode + dependencies?: readonly unknown[] + scrollDistance?: number + className?: string + gap?: string + expandable?: boolean +} + +const HorizontalScrollContainer: React.FC = ({ + children, + dependencies = [], + scrollDistance = 200, + className, + gap = '8px', + expandable = false +}) => { + const scrollRef = useRef(null) + const [canScroll, setCanScroll] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const [isScrolledToEnd, setIsScrolledToEnd] = useState(false) + + const handleScrollRight = (event: React.MouseEvent) => { + scrollRef.current?.scrollBy({ left: scrollDistance, behavior: 'smooth' }) + event.stopPropagation() + } + + const handleContainerClick = (e: React.MouseEvent) => { + if (expandable) { + // 确保不是点击了其他交互元素(如 tag 的关闭按钮) + const target = e.target as HTMLElement + if (!target.closest('[data-no-expand]')) { + setIsExpanded(!isExpanded) + } + } + } + + const checkScrollability = () => { + const scrollElement = scrollRef.current + if (scrollElement) { + const parentElement = scrollElement.parentElement + const availableWidth = parentElement ? parentElement.clientWidth : scrollElement.clientWidth + + // 确保容器不会超出可用宽度 + const canScrollValue = scrollElement.scrollWidth > Math.min(availableWidth, scrollElement.clientWidth) + setCanScroll(canScrollValue) + + // 检查是否滚动到最右侧 + if (canScrollValue) { + const isAtEnd = Math.abs(scrollElement.scrollLeft + scrollElement.clientWidth - scrollElement.scrollWidth) <= 1 + setIsScrolledToEnd(isAtEnd) + } else { + setIsScrolledToEnd(false) + } + } + } + + useEffect(() => { + const scrollElement = scrollRef.current + if (!scrollElement) return + + checkScrollability() + + const handleScroll = () => { + checkScrollability() + } + + const resizeObserver = new ResizeObserver(checkScrollability) + resizeObserver.observe(scrollElement) + + scrollElement.addEventListener('scroll', handleScroll) + window.addEventListener('resize', checkScrollability) + + return () => { + resizeObserver.disconnect() + scrollElement.removeEventListener('scroll', handleScroll) + window.removeEventListener('resize', checkScrollability) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies) + + return ( + + + {children} + + {canScroll && !isExpanded && !isScrolledToEnd && ( + + + + )} + + ) +} + +const Container = styled.div<{ $expandable?: boolean; $disableHoverButton?: boolean }>` + display: flex; + align-items: center; + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + position: relative; + cursor: ${(props) => (props.$expandable ? 'pointer' : 'default')}; + + ${(props) => + !props.$disableHoverButton && + ` + &:hover { + .scroll-right-button { + opacity: 1; + } + } + `} +` + +const ScrollContent = styled(Scrollbar)<{ + $gap: string + $isExpanded?: boolean + $expandable?: boolean +}>` + display: flex; + overflow-x: ${(props) => (props.$expandable && props.$isExpanded ? 'hidden' : 'auto')}; + overflow-y: hidden; + white-space: ${(props) => (props.$expandable && props.$isExpanded ? 'normal' : 'nowrap')}; + gap: ${(props) => props.$gap}; + flex-wrap: ${(props) => (props.$expandable && props.$isExpanded ? 'wrap' : 'nowrap')}; + + &::-webkit-scrollbar { + display: none; + } +` + +const ScrollButton = styled.div` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + z-index: 1; + opacity: 0; + transition: opacity 0.2s ease-in-out; + cursor: pointer; + background: var(--color-background); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + color: var(--color-text-2); + + &:hover { + color: var(--color-text); + background: var(--color-list-item); + } +` + +export default HorizontalScrollContainer diff --git a/src/renderer/src/components/MinApp/MinApp.tsx b/src/renderer/src/components/MinApp/MinApp.tsx index 853d37d109..5833ed9b1a 100644 --- a/src/renderer/src/components/MinApp/MinApp.tsx +++ b/src/renderer/src/components/MinApp/MinApp.tsx @@ -135,6 +135,7 @@ const Container = styled.div` align-items: center; cursor: pointer; overflow: hidden; + min-height: 85px; ` const IconContainer = styled.div` diff --git a/src/renderer/src/components/OGCard.tsx b/src/renderer/src/components/OGCard.tsx index 8a0036e8e2..93446b6749 100644 --- a/src/renderer/src/components/OGCard.tsx +++ b/src/renderer/src/components/OGCard.tsx @@ -1,8 +1,7 @@ -import CherryLogo from '@renderer/assets/images/banner.png' import Favicon from '@renderer/components/Icons/FallbackFavicon' import { useMetaDataParser } from '@renderer/hooks/useMetaDataParser' import { Skeleton, Typography } from 'antd' -import { useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import styled from 'styled-components' const { Title, Paragraph } = Typography @@ -11,6 +10,8 @@ type Props = { show: boolean } +const IMAGE_HEIGHT = '9rem' // equals h-36 + export const OGCard = ({ link, show }: Props) => { const openGraph = ['og:title', 'og:description', 'og:image', 'og:imageAlt'] as const const { metadata, isLoading, parseMetadata } = useMetaDataParser(link, openGraph) @@ -32,6 +33,14 @@ export const OGCard = ({ link, show }: Props) => { } }, [parseMetadata, isLoading, show]) + const GeneratedGraph = useCallback(() => { + return ( +
+

{metadata['og:title'] || hostname}

+
+ ) + }, [hostname, metadata]) + if (isLoading) { return } @@ -45,7 +54,7 @@ export const OGCard = ({ link, show }: Props) => { )} {!hasImage && ( - + )} @@ -113,8 +122,8 @@ const PreviewContainer = styled.div<{ hasImage?: boolean }>` const PreviewImageContainer = styled.div` width: 100%; - height: 140px; - min-height: 140px; + height: ${IMAGE_HEIGHT}; + min-height: ${IMAGE_HEIGHT}; overflow: hidden; ` @@ -128,7 +137,7 @@ const PreviewContent = styled.div` const PreviewImage = styled.img` width: 100%; - height: 140px; + height: ${IMAGE_HEIGHT}; object-fit: cover; ` diff --git a/src/renderer/src/components/QuickPanel/provider.tsx b/src/renderer/src/components/QuickPanel/provider.tsx index c06d337248..57eae70ef2 100644 --- a/src/renderer/src/components/QuickPanel/provider.tsx +++ b/src/renderer/src/components/QuickPanel/provider.tsx @@ -32,6 +32,11 @@ export const QuickPanelProvider: React.FC = ({ children setList((prevList) => prevList.map((item) => (item === targetItem ? { ...item, isSelected } : item))) }, []) + // 添加更新整个列表的方法 + const updateList = useCallback((newList: QuickPanelListItem[]) => { + setList(newList) + }, []) + const open = useCallback((options: QuickPanelOpenOptions) => { if (clearTimer.current) { clearTimeout(clearTimer.current) @@ -56,7 +61,7 @@ export const QuickPanelProvider: React.FC = ({ children const close = useCallback( (action?: QuickPanelCloseAction, searchText?: string) => { setIsVisible(false) - onClose?.({ symbol, action, triggerInfo, searchText, item: {} as QuickPanelListItem, multiple: false }) + onClose?.({ action, searchText, item: {} as QuickPanelListItem, context: this }) clearTimer.current = setTimeout(() => { setList([]) @@ -68,7 +73,7 @@ export const QuickPanelProvider: React.FC = ({ children setTriggerInfo(undefined) }, 200) }, - [onClose, symbol, triggerInfo] + [onClose] ) useEffect(() => { @@ -85,6 +90,7 @@ export const QuickPanelProvider: React.FC = ({ children open, close, updateItemSelection, + updateList, isVisible, symbol, @@ -103,6 +109,7 @@ export const QuickPanelProvider: React.FC = ({ children open, close, updateItemSelection, + updateList, isVisible, symbol, list, diff --git a/src/renderer/src/components/QuickPanel/types.ts b/src/renderer/src/components/QuickPanel/types.ts index d8e2ff26b0..97e072dea0 100644 --- a/src/renderer/src/components/QuickPanel/types.ts +++ b/src/renderer/src/components/QuickPanel/types.ts @@ -8,13 +8,10 @@ export type QuickPanelTriggerInfo = { } export type QuickPanelCallBackOptions = { - symbol: string + context: QuickPanelContextType action: QuickPanelCloseAction item: QuickPanelListItem searchText?: string - /** 是否处于多选状态 */ - multiple?: boolean - triggerInfo?: QuickPanelTriggerInfo } export type QuickPanelOpenOptions = { @@ -68,6 +65,7 @@ export interface QuickPanelContextType { readonly open: (options: QuickPanelOpenOptions) => void readonly close: (action?: QuickPanelCloseAction, searchText?: string) => void readonly updateItemSelection: (targetItem: QuickPanelListItem, isSelected: boolean) => void + readonly updateList: (newList: QuickPanelListItem[]) => void readonly isVisible: boolean readonly symbol: string readonly list: QuickPanelListItem[] diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 08878b8478..30955f96f3 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -222,11 +222,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { // 创建更新后的item对象用于回调 const updatedItem = { ...item, isSelected: newSelectedState } const quickPanelCallBackOptions: QuickPanelCallBackOptions = { - symbol: ctx.symbol, + context: ctx, action, item: updatedItem, - searchText: searchText, - multiple: ctx.multiple + searchText: searchText } ctx.beforeAction?.(quickPanelCallBackOptions) @@ -236,11 +235,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { } const quickPanelCallBackOptions: QuickPanelCallBackOptions = { - symbol: ctx.symbol, + context: ctx, action, item, - searchText: searchText, - multiple: ctx.multiple + searchText: searchText } ctx.beforeAction?.(quickPanelCallBackOptions) diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx index 60258d8c8b..e50e128d50 100644 --- a/src/renderer/src/components/Scrollbar/index.tsx +++ b/src/renderer/src/components/Scrollbar/index.tsx @@ -2,12 +2,12 @@ import { throttle } from 'lodash' import { FC, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' -interface Props extends Omit, 'onScroll'> { +export interface ScrollbarProps extends Omit, 'onScroll'> { ref?: React.Ref onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll } -const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => { +const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => { const [isScrolling, setIsScrolling] = useState(false) const timeoutRef = useRef(null) diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 4f7d9f13d6..efa19565d1 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -1,6 +1,6 @@ import { PlusOutlined } from '@ant-design/icons' import { Sortable, useDndReorder } from '@renderer/components/dnd' -import Scrollbar from '@renderer/components/Scrollbar' +import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import { isMac } from '@renderer/config/constant' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { useTheme } from '@renderer/context/ThemeProvider' @@ -14,9 +14,8 @@ import type { Tab } from '@renderer/store/tabs' import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs' import { ThemeMode } from '@renderer/types' import { classNames } from '@renderer/utils' -import { Button, Tooltip } from 'antd' +import { Tooltip } from 'antd' import { - ChevronRight, FileSearch, Folder, Hammer, @@ -33,7 +32,7 @@ import { Terminal, X } from 'lucide-react' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -98,8 +97,6 @@ const TabsContainer: React.FC = ({ children }) => { const { hideMinappPopup } = useMinappPopup() const { minapps } = useMinapps() const { t } = useTranslation() - const scrollRef = useRef(null) - const [canScroll, setCanScroll] = useState(false) const getTabId = (path: string): string => { if (path === '/') return 'home' @@ -175,31 +172,6 @@ const TabsContainer: React.FC = ({ children }) => { navigate(tab.path) } - const handleScrollRight = () => { - scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' }) - } - - useEffect(() => { - const scrollElement = scrollRef.current - if (!scrollElement) return - - const checkScrollability = () => { - setCanScroll(scrollElement.scrollWidth > scrollElement.clientWidth) - } - - checkScrollability() - - const resizeObserver = new ResizeObserver(checkScrollability) - resizeObserver.observe(scrollElement) - - window.addEventListener('resize', checkScrollability) - - return () => { - resizeObserver.disconnect() - window.removeEventListener('resize', checkScrollability) - } - }, [tabs]) - const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs]) const { onSortEnd } = useDndReorder({ @@ -212,46 +184,39 @@ const TabsContainer: React.FC = ({ children }) => { return ( - - - ( - handleTabClick(tab)}> - - {tab.id && {getTabIcon(tab.id, minapps)}} - {getTabTitle(tab.id)} - - {tab.id !== 'home' && ( - { - e.stopPropagation() - closeTab(tab.id) - }}> - - - )} - - )} - /> - - {canScroll && ( - - - - )} + + ( + handleTabClick(tab)}> + + {tab.id && {getTabIcon(tab.id, minapps)}} + {getTabTitle(tab.id)} + + {tab.id !== 'home' && ( + { + e.stopPropagation() + closeTab(tab.id) + }}> + + + )} + + )} + /> - + ` z-index: 1; -webkit-app-region: no-drag; } -` -const TabsArea = styled.div` - display: flex; - align-items: center; - flex: 1 1 auto; - min-width: 0; - gap: 6px; - padding-right: 2rem; - position: relative; + .tab-scroll-container { + -webkit-app-region: drag; - -webkit-app-region: drag; - - > * { - -webkit-app-region: no-drag; - } - - &:hover { - .scroll-right-button { - opacity: 1; + > * { + -webkit-app-region: no-drag; } } ` -const TabsScroll = styled(Scrollbar)` - &::-webkit-scrollbar { - display: none; - } -` - const Tab = styled.div<{ active?: boolean }>` display: flex; align-items: center; @@ -414,22 +359,6 @@ const AddTabButton = styled.div` } ` -const ScrollButton = styled(Button)` - position: absolute; - right: 4rem; - top: 50%; - transform: translateY(-50%); - z-index: 1; - opacity: 0; - transition: opacity 0.2s ease-in-out; - - border: none; - box-shadow: - 0 6px 16px 0 rgba(0, 0, 0, 0.08), - 0 3px 6px -4px rgba(0, 0, 0, 0.12), - 0 9px 28px 8px rgba(0, 0, 0, 0.05); -` - const RightButtonsContainer = styled.div` display: flex; align-items: center; diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 2a481f564b..571e5877a8 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -12,6 +12,7 @@ import { handleSaveData } from '@renderer/store' import { selectMemoryConfig } from '@renderer/store/memory' import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime' import { delay, runAsyncFunction } from '@renderer/utils' +import { checkDataLimit } from '@renderer/utils' import { defaultLanguage } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { useLiveQuery } from 'dexie-react-hooks' @@ -20,7 +21,7 @@ import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' import useFullScreenNotice from './useFullScreenNotice' import { useRuntime } from './useRuntime' -import { useSettings } from './useSettings' +import { useNavbarPosition, useSettings } from './useSettings' import useUpdateHandler from './useUpdateHandler' const logger = loggerService.withContext('useAppInit') @@ -37,6 +38,7 @@ export function useAppInit() { customCss, enableDataCollection } = useSettings() + const { isTopNavbar } = useNavbarPosition() const { minappShow } = useRuntime() const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel() const avatar = useLiveQuery(() => db.settings.get('image://avatar')) @@ -102,14 +104,14 @@ export function useAppInit() { useEffect(() => { const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow - if (minappShow) { + if (minappShow && isTopNavbar) { window.root.style.background = windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)' return } window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)' - }, [windowStyle, minappShow, theme]) + }, [windowStyle, minappShow, theme, isTopNavbar]) useEffect(() => { if (isLocalAi) { @@ -158,4 +160,8 @@ export function useAppInit() { logger.error('Failed to update memory config:', error) }) }, [memoryConfig]) + + useEffect(() => { + checkDataLimit() + }, []) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 32fb1fd7e5..066478b2f1 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1708,7 +1708,7 @@ "delete_confirm": "Are you sure you want to delete this {{type}}?", "delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?", "delete_note_confirm": "Are you sure you want to delete the note \"{{name}}\"?", - "drop_markdown_hint": "Drop markdown files here to import", + "drop_markdown_hint": "Drop .md files or folders here to import", "empty": "No notes available yet", "expand": "unfold", "export_failed": "Failed to export to knowledge base", @@ -1719,8 +1719,7 @@ "new_note": "Create a new note", "no_content_to_copy": "No content to copy", "no_file_selected": "Please select the file to upload", - "only_markdown": "Only Markdown files are supported", - "only_one_file_allowed": "Only one file can be uploaded", + "no_valid_files": "No valid file was uploaded", "open_folder": "Open an external folder", "open_outside": "Open from external", "rename": "Rename", @@ -1782,7 +1781,7 @@ "sort_updated_asc": "Update time (oldest first)", "sort_updated_desc": "Update time (newest first)", "sort_z2a": "File name (Z-A)", - "star": "Favorite", + "star": "Favorite note", "starred_notes": "Collected notes", "title": "Notes", "unsaved_changes": "You have unsaved content, are you sure you want to leave?", @@ -2651,6 +2650,10 @@ "url": "Joplin Web Clipper Service URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Disk Space Warning", + "appDataDiskQuotaDescription": "Data directory space is almost full, please clear disk space, otherwise data will be lost" + }, "local": { "autoSync": { "label": "Auto Backup", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b432dac525..002284335b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -132,6 +132,7 @@ }, "title": "API 服务器" }, + "assistants": { "abbr": "助手", "clear": { @@ -1708,7 +1709,7 @@ "delete_confirm": "确定要删除这个{{type}}吗?", "delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?", "delete_note_confirm": "确定要删除笔记 \"{{name}}\" 吗?", - "drop_markdown_hint": "拖拽 Markdown 文件到此处导入", + "drop_markdown_hint": "拖拽 .md 文件或目录到此处导入", "empty": "暂无笔记", "expand": "展开", "export_failed": "导出到知识库失败", @@ -1719,8 +1720,7 @@ "new_note": "新建笔记", "no_content_to_copy": "没有内容可复制", "no_file_selected": "请选择要上传的文件", - "only_markdown": "仅支持 Markdown 格式", - "only_one_file_allowed": "只能上传一个文件", + "no_valid_files": "没有上传有效的文件", "open_folder": "打开外部文件夹", "open_outside": "从外部打开", "rename": "重命名", @@ -1782,7 +1782,7 @@ "sort_updated_asc": "更新时间(从旧到新)", "sort_updated_desc": "更新时间(从新到旧)", "sort_z2a": "文件名(Z-A)", - "star": "收藏", + "star": "收藏笔记", "starred_notes": "收藏的笔记", "title": "笔记", "unsaved_changes": "你有未保存的内容,确定要离开吗?", @@ -2651,6 +2651,10 @@ "url": "Joplin 剪裁服务监听 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "磁盘空间警告", + "appDataDiskQuotaDescription": "数据目录空间即将用尽, 请清理磁盘空间, 否则会丢失数据" + }, "local": { "autoSync": { "label": "自动备份", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 73288e916f..753981be90 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1708,7 +1708,7 @@ "delete_confirm": "確定要刪除此 {{type}} 嗎?", "delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?", "delete_note_confirm": "確定要刪除筆記 \"{{name}}\" 嗎?", - "drop_markdown_hint": "拖拽 Markdown 文件到此處導入", + "drop_markdown_hint": "拖拽 .md 文件或資料夾到此處導入", "empty": "暫無筆記", "expand": "展開", "export_failed": "匯出至知識庫失敗", @@ -1719,8 +1719,7 @@ "new_note": "新建筆記", "no_content_to_copy": "沒有內容可複制", "no_file_selected": "請選擇要上傳的文件", - "only_markdown": "僅支援 Markdown 格式", - "only_one_file_allowed": "只能上傳一個文件", + "no_valid_files": "沒有上傳有效的檔案", "open_folder": "打開外部文件夾", "open_outside": "從外部打開", "rename": "重命名", @@ -1782,7 +1781,7 @@ "sort_updated_asc": "更新時間(從舊到新)", "sort_updated_desc": "更新時間(從新到舊)", "sort_z2a": "文件名(Z-A)", - "star": "收藏", + "star": "收藏筆記", "starred_notes": "收藏的筆記", "title": "筆記", "unsaved_changes": "你有未儲存的內容,確定要離開嗎?", @@ -2651,6 +2650,10 @@ "url": "Joplin 剪輯服務 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "磁碟空間警告", + "appDataDiskQuotaDescription": "資料目錄空間即將用盡, 請清理磁碟空間, 否則會丟失數據" + }, "local": { "autoSync": { "label": "自動備份", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 2403c487b5..8f1c235ece 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -538,6 +538,10 @@ "tip": "Στη γραμμή εργαλείων των εκτελέσιμων blocks κώδικα θα εμφανίζεται το κουμπί εκτέλεσης· προσέξτε να μην εκτελέσετε επικίνδυνο κώδικα!", "title": "Εκτέλεση Κώδικα" }, + "code_fancy_block": { + "label": "Μπλοκ κώδικα με στυλ", + "tip": "Χρησιμοποιήστε πιο όμορφο στιλ μπλοκ κώδικα, για παράδειγμα κάρτες HTML" + }, "code_image_tools": { "label": "Ενεργοποίηση εργαλείου προεπισκόπησης", "tip": "Ενεργοποίηση εργαλείου προεπισκόπησης για εικόνες που αποδίδονται από blocks κώδικα όπως το mermaid" @@ -1704,7 +1708,7 @@ "delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};", "delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;", "delete_note_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη σημείωση \"{{name}}\";;", - "drop_markdown_hint": "Σύρετε το αρχείο Markdown εδώ για εισαγωγή", + "drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους .md εδώ για εισαγωγή", "empty": "δεν υπάρχει σημείωση για τώρα", "expand": "να ανοίξει", "export_failed": "Εξαγωγή στη βάση γνώσης απέτυχε", @@ -1715,8 +1719,7 @@ "new_note": "Δημιουργία νέας σημείωσης", "no_content_to_copy": "Δεν υπάρχει περιεχόμενο προς αντιγραφή", "no_file_selected": "Επιλέξτε το αρχείο για μεταφόρτωση", - "only_markdown": "Υποστηρίζεται μόνο η μορφή Markdown", - "only_one_file_allowed": "Μπορείτε να ανεβάσετε μόνο ένα αρχείο", + "no_valid_files": "Δεν ανέβηκε έγκυρο αρχείο", "open_folder": "Άνοιγμα εξωτερικού φακέλου", "open_outside": "Από το εξωτερικό", "rename": "μετονομασία", @@ -1742,8 +1745,15 @@ "compress_content": "μείωση πλάτους στήλης", "compress_content_description": "Ενεργοποιώντας το, θα περιορίζεται ο αριθμός των χαρακτήρων ανά γραμμή, μειώνοντας την οθόνη που εμφανίζεται", "default_font": "προεπιλεγμένη γραμματοσειρά", + "font_size": "μέγεθος γραμματοσειράς", + "font_size_description": "Ρυθμίστε το μέγεθος της γραμματοσειράς για καλύτερη εμπειρία ανάγνωσης (10-30px)", + "font_size_large": "Μεγάλος", + "font_size_medium": "中", + "font_size_small": "μικρό", "font_title": "ρυθμίσεις γραμματοσειράς", "serif_font": "σειρά γραμματοσειρών", + "show_table_of_contents": "Εμφάνιση περιεχομένων καταλόγου", + "show_table_of_contents_description": "Εμφάνιση πλευρικής στήλης περιεχομένων για εύκολη πλοήγηση στο έγγραφο", "title": "ρυθμίσεις εμφάνισης" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_z2a": "όνομα αρχείου (Z-A)", - "star": "Αποθήκευση", + "star": "Αγαπημένες σημειώσεις", "starred_notes": "Σημειώσεις συλλογής", "title": "σημειώσεις", "unsaved_changes": "Έχετε μη αποθηκευμένο περιεχόμενο, είστε βέβαιοι ότι θέλετε να φύγετε;", @@ -2235,7 +2245,7 @@ "changeType": "Αλλαγή τύπου", "deleteProperty": "διαγραφή χαρακτηριστικού", "editValue": "Επεξεργασία τιμής", - "empty": "\n空\n", + "empty": "Κενό", "moreActions": "Περισσότερες ενέργειες", "propertyName": "όνομα χαρακτηριστικού" }, @@ -2640,6 +2650,10 @@ "url": "URL υπηρεσίας περικοπής Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Προειδοποίηση χώρου δίσκου", + "appDataDiskQuotaDescription": "Ο κατάλογος δεδομένων της εφαρμογής είναι σχεδόν γεμάτος, παρακαλώ απομακρύνετε τον χώρο δίσκου, αλλιώς θα χαθούν τα δεδομένα" + }, "local": { "autoSync": { "label": "Αυτόματο αντίγραφο ασφαλείας", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f25b2be8f5..905e720b8e 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -538,6 +538,10 @@ "tip": "En la barra de herramientas de bloques de código ejecutables se mostrará un botón de ejecución. ¡Tenga cuidado en no ejecutar código peligroso!", "title": "Ejecución de Código" }, + "code_fancy_block": { + "label": "Bloque de código con estilo", + "tip": "Utiliza un estilo de bloque de código más atractivo, como una tarjeta HTML" + }, "code_image_tools": { "label": "Habilitar herramienta de vista previa", "tip": "Habilitar herramientas de vista previa para imágenes renderizadas de bloques de código como mermaid" @@ -1704,7 +1708,7 @@ "delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?", "delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?", "delete_note_confirm": "¿Está seguro de que desea eliminar la nota \"{{name}}\"?", - "drop_markdown_hint": "Arrastra archivos Markdown aquí para importar", + "drop_markdown_hint": "Arrastre y suelte archivos o carpetas de .md aquí para importar", "empty": "Sin notas por el momento", "expand": "expandir", "export_failed": "Exportación a la base de conocimientos fallida", @@ -1715,8 +1719,7 @@ "new_note": "Crear nota nueva", "no_content_to_copy": "No hay contenido para copiar", "no_file_selected": "Por favor, seleccione el archivo a subir", - "only_markdown": "Solo se admite el formato Markdown", - "only_one_file_allowed": "solo se puede subir un archivo", + "no_valid_files": "No se ha cargado un archivo válido", "open_folder": "abrir carpeta externa", "open_outside": "Abrir desde el exterior", "rename": "renombrar", @@ -1742,8 +1745,15 @@ "compress_content": "reducir el ancho de la columna", "compress_content_description": "Al activarlo, se limitará el número de caracteres por línea, reduciendo el contenido mostrado en pantalla.", "default_font": "fuente predeterminada", + "font_size": "Tamaño de fuente", + "font_size_description": "Ajusta el tamaño de la fuente para una mejor experiencia de lectura (10-30px)", + "font_size_large": "Grande", + "font_size_medium": "中", + "font_size_small": "pequeño", "font_title": "Configuración de fuente", "serif_font": "fuente serif", + "show_table_of_contents": "Mostrar esquema del directorio", + "show_table_of_contents_description": "Mostrar la barra lateral del índice para facilitar la navegación dentro del documento", "title": "configuración de visualización" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)", "sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)", "sort_z2a": "Nombre de archivo (Z-A)", - "star": "Colección", + "star": "Notas guardadas", "starred_notes": "notas guardadas", "title": "notas", "unsaved_changes": "Tienes contenido no guardado, ¿estás seguro de que quieres salir?", @@ -2640,6 +2650,10 @@ "url": "URL a la que escucha el servicio de recorte de Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Advertencia de espacio en disco", + "appDataDiskQuotaDescription": "El espacio de almacenamiento de datos está casi lleno, por favor, limpie el espacio en disco, de lo contrario, se perderán los datos" + }, "local": { "autoSync": { "label": "Copia de seguridad automática", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 36f9cc192b..5c2a0d2a5d 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -538,6 +538,10 @@ "tip": "Une bouton d'exécution s'affichera dans la barre d'outils des blocs de code exécutables. Attention à ne pas exécuter de code dangereux !", "title": "Exécution de code" }, + "code_fancy_block": { + "label": "bloc de code fantaisie", + "tip": "Utiliser un style de bloc de code plus esthétique, comme une carte HTML" + }, "code_image_tools": { "label": "Activer l'outil d'aperçu", "tip": "Activer les outils de prévisualisation pour les images rendues des blocs de code tels que mermaid" @@ -1704,7 +1708,7 @@ "delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?", "delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?", "delete_note_confirm": "Êtes-vous sûr de vouloir supprimer la note \"{{name}}\" ?", - "drop_markdown_hint": "Glissez-déposez le fichier Markdown ici pour l'importer", + "drop_markdown_hint": "Déposez ici des fichiers ou dossiers .md pour les importer", "empty": "Aucune note pour le moment", "expand": "développer", "export_failed": "Échec de l'exportation vers la base de connaissances", @@ -1715,8 +1719,7 @@ "new_note": "Nouvelle note", "no_content_to_copy": "Aucun contenu à copier", "no_file_selected": "Veuillez sélectionner le fichier à télécharger", - "only_markdown": "uniquement le format Markdown est pris en charge", - "only_one_file_allowed": "On ne peut télécharger qu'un seul fichier", + "no_valid_files": "Aucun fichier valide n’a été téléversé", "open_folder": "ouvrir le dossier externe", "open_outside": "Ouvrir depuis l'extérieur", "rename": "renommer", @@ -1742,8 +1745,15 @@ "compress_content": "réduire la largeur des colonnes", "compress_content_description": "L'activation limitera le nombre de caractères par ligne, réduisant ainsi le contenu affiché à l'écran.", "default_font": "police par défaut", + "font_size": "Taille de police", + "font_size_description": "Ajuster la taille de la police pour une meilleure expérience de lecture (10-30px)", + "font_size_large": "Grand", + "font_size_medium": "中", + "font_size_small": "petit", "font_title": "paramétrage des polices", "serif_font": "police à empattements", + "show_table_of_contents": "Afficher le plan du sommaire", + "show_table_of_contents_description": "Afficher la barre latérale de la table des matières pour faciliter la navigation dans le document", "title": "Paramètres d'affichage" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)", "sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)", "sort_z2a": "Nom de fichier (Z-A)", - "star": "Favori", + "star": "Notes enregistrées", "starred_notes": "notes de collection", "title": "notes", "unsaved_changes": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?", @@ -2640,6 +2650,10 @@ "url": "URL surveillée par le service de découpage de Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Avertissement d'espace sur le disque", + "appDataDiskQuotaDescription": "L'espace de stockage des données est presque plein, veuillez nettoyer l'espace sur le disque, sinon les données seront perdues" + }, "local": { "autoSync": { "label": "Sauvegarde automatique", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 2635cf6500..391cbec4e3 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -538,6 +538,10 @@ "tip": "実行可能なコードブロックのツールバーには実行ボタンが表示されます。危険なコードを実行しないでください!", "title": "コード実行" }, + "code_fancy_block": { + "label": "\n装飾的なコードブロック\n", + "tip": "より見栄えの良いコードブロックスタイルを使用する、例えばHTMLカード" + }, "code_image_tools": { "label": "プレビューツールを有効にする", "tip": "mermaid などのコードブロックから生成された画像に対してプレビューツールを有効にする" @@ -1704,7 +1708,7 @@ "delete_confirm": "この{{type}}を本当に削除しますか?", "delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?", "delete_note_confirm": "メモ \"{{name}}\" を削除してもよろしいですか?", - "drop_markdown_hint": "マークダウンファイルをドラッグアンドドロップしてここにインポートします", + "drop_markdown_hint": ".md ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください", "empty": "暫無ノート", "expand": "展開", "export_failed": "知識ベースへのエクスポートに失敗しました", @@ -1715,8 +1719,7 @@ "new_note": "新規ノート作成", "no_content_to_copy": "コピーするコンテンツはありません", "no_file_selected": "アップロードするファイルを選択してください", - "only_markdown": "Markdown ファイルのみをアップロードできます", - "only_one_file_allowed": "アップロードできるファイルは1つだけです", + "no_valid_files": "有効なファイルがアップロードされていません", "open_folder": "外部フォルダーを開きます", "open_outside": "外部から開く", "rename": "名前の変更", @@ -1742,8 +1745,15 @@ "compress_content": "バーの幅を減らします", "compress_content_description": "有効にすると、1行あたりの単語数が制限され、画面に表示されるコンテンツが減少します。", "default_font": "デフォルトフォント", + "font_size": "フォントサイズ", + "font_size_description": "フォントサイズを調整して読書体験を向上させる(10-30px)", + "font_size_large": "大", + "font_size_medium": "中", + "font_size_small": "小", "font_title": "フォント設定", "serif_font": "セリフフォント", + "show_table_of_contents": "目次アウトラインを表示", + "show_table_of_contents_description": "目次アウトラインサイドバーを表示し、文書内のナビゲーションを容易にする", "title": "見せる" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "更新日時(古い順)", "sort_updated_desc": "更新日時(新しい順)", "sort_z2a": "ファイル名(Z-A)", - "star": "お気に入りに追加する", + "star": "お気に入りのノート", "starred_notes": "収集したノート", "title": "ノート", "unsaved_changes": "保存されていないコンテンツがあります。本当に離れますか?", @@ -2640,6 +2650,10 @@ "url": "Joplin 剪輯服務 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "ディスク容量警告", + "appDataDiskQuotaDescription": "データディレクトリの容量がほぼ満杯になっており、新しいデータの保存ができなくなる可能性があります。まずデータをバックアップしてから、ディスク容量を整理してください。" + }, "local": { "autoSync": { "label": "自動バックアップ", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 227f5b3c4b..4d1edba05d 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -538,6 +538,10 @@ "tip": "A barra de ferramentas de blocos de código executáveis exibirá um botão de execução; atenção para não executar códigos perigosos!", "title": "Execução de Código" }, + "code_fancy_block": { + "label": "Bloco de código estilizado", + "tip": "Use um estilo de bloco de código mais agradável, como cartões HTML" + }, "code_image_tools": { "label": "Habilitar ferramenta de visualização", "tip": "Ativar ferramentas de visualização para imagens renderizadas de blocos de código como mermaid" @@ -1704,7 +1708,7 @@ "delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?", "delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?", "delete_note_confirm": "Tem a certeza de que deseja eliminar a nota \"{{name}}\"?", - "drop_markdown_hint": "Arraste o arquivo Markdown para aqui e importe", + "drop_markdown_hint": "Arraste e solte arquivos ou pastas .md aqui para importar", "empty": "Ainda não existem notas", "expand": "expandir", "export_failed": "Falha ao exportar para a base de conhecimento", @@ -1715,8 +1719,7 @@ "new_note": "Nova nota", "no_content_to_copy": "Não há conteúdo para copiar", "no_file_selected": "Selecione o arquivo a ser enviado", - "only_markdown": "Apenas o formato Markdown é suportado", - "only_one_file_allowed": "só é possível enviar um arquivo", + "no_valid_files": "Nenhum arquivo válido foi carregado", "open_folder": "Abrir pasta externa", "open_outside": "Abrir externamente", "rename": "renomear", @@ -1742,8 +1745,15 @@ "compress_content": "reduzir a largura da coluna", "compress_content_description": "Ativando isso limitará o número de caracteres por linha, reduzindo o conteúdo exibido na tela.", "default_font": "fonte padrão", + "font_size": "tamanho da fonte", + "font_size_description": "Ajuste o tamanho da fonte para uma melhor experiência de leitura (10-30px)", + "font_size_large": "Grande", + "font_size_medium": "中", + "font_size_small": "pequeno", "font_title": "configuração de fonte", "serif_font": "fonte com serifa", + "show_table_of_contents": "Mostrar esboço do diretório", + "show_table_of_contents_description": "Mostrar barra lateral do índice, facilitando a navegação dentro do documento", "title": "configurações de exibição" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)", "sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)", "sort_z2a": "Nome do arquivo (Z-A)", - "star": "coleções", + "star": "Notas favoritas", "starred_notes": "notas salvas", "title": "nota", "unsaved_changes": "Você tem conteúdo não salvo, tem certeza que deseja sair?", @@ -2640,6 +2650,10 @@ "url": "URL para o qual o serviço de recorte do Joplin está escutando", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Aviso de espaço em disco", + "appDataDiskQuotaDescription": "O espaço de armazenamento de dados está quase cheio, por favor, limpe o espaço em disco, caso contrário, os dados serão perdidos" + }, "local": { "autoSync": { "label": "Backup automático", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 903d809a13..493a565508 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -538,6 +538,10 @@ "tip": "Выполнение кода в блоке кода возможно, но не рекомендуется выполнять опасный код!", "title": "Выполнение кода" }, + "code_fancy_block": { + "label": "Форматированные блоки кода", + "tip": "Используйте более эстетичный стиль блоков кода, например, HTML-карточки" + }, "code_image_tools": { "label": "Включить инструменты предпросмотра", "tip": "Включить инструменты предпросмотра для изображений, сгенерированных из блоков кода (например mermaid)" @@ -1704,7 +1708,7 @@ "delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?", "delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?", "delete_note_confirm": "Вы действительно хотите удалить заметку \"{{name}}\"?", - "drop_markdown_hint": "Перетаскивать файл разметки, чтобы импортировать его здесь", + "drop_markdown_hint": "Перетащите сюда файлы или папки .md для импорта", "empty": "заметок пока нет", "expand": "развернуть", "export_failed": "Экспорт в базу знаний не выполнен", @@ -1715,8 +1719,7 @@ "new_note": "Создать заметку", "no_content_to_copy": "Нет контента для копирования", "no_file_selected": "Пожалуйста, выберите файл для загрузки", - "only_markdown": "Только Markdown", - "only_one_file_allowed": "Можно загрузить только один файл", + "no_valid_files": "Не загружен действительный файл", "open_folder": "Откройте внешнюю папку", "open_outside": "открыть снаружи", "rename": "переименовать", @@ -1742,8 +1745,15 @@ "compress_content": "Уменьшить ширину стержня", "compress_content_description": "При включении он ограничит количество слов на строку, уменьшая содержимое, отображаемое на экране.", "default_font": "По умолчанию шрифт", + "font_size": "Размер шрифта", + "font_size_description": "Отрегулируйте размер шрифта для лучшего чтения (10–30 пикселей)", + "font_size_large": "Большой", + "font_size_medium": "中", + "font_size_small": "\nмаленький\n", "font_title": "Настройки шрифта", "serif_font": "Serif Font", + "show_table_of_contents": "Показать оглавление", + "show_table_of_contents_description": "显示目录大纲侧边栏,方便文档内导航", "title": "показывать" }, "editor": { @@ -1771,7 +1781,7 @@ "sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_desc": "Время обновления (от нового к старому)", "sort_z2a": "Имя файла (Я-А)", - "star": "Сохранить", + "star": "Избранные заметки", "starred_notes": "Сохраненные заметки", "title": "заметки", "unsaved_changes": "Вы не сохранили содержимое. Вы уверены, что хотите уйти?", @@ -2640,6 +2650,10 @@ "url": "URL Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Предупреждение о пространстве на диске", + "appDataDiskQuotaDescription": "Каталог данных почти заполнен, что может привести к невозможности сохранения новых данных. Сначала создайте резервную копию данных, затем освободите дисковое пространство." + }, "local": { "autoSync": { "label": "Автоматическое резервное копирование", diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 68fa4e5dc0..4ea6bcdd57 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -43,6 +43,7 @@ const Chat: FC = (props) => { const { showTopics } = useShowTopics() const { isMultiSelectMode } = useChatContext(props.activeTopic) const { isTopNavbar } = useNavbarPosition() + const chatMaxWidth = useChatMaxWidth() const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) @@ -153,7 +154,7 @@ const Chat: FC = (props) => { vertical flex={1} justify="space-between" - style={{ maxWidth: '100%', height: mainHeight }}> + style={{ maxWidth: chatMaxWidth, height: mainHeight }}> = ({ 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 }) } @@ -851,6 +866,15 @@ 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 && ( + + )}