From b1bd5d05317de7df118f49eadf00aa79c4b5189c Mon Sep 17 00:00:00 2001 From: Hamm Date: Tue, 8 Apr 2025 16:53:31 +0800 Subject: [PATCH 01/21] =?UTF-8?q?refactor(reranker):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E9=87=8D=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=20=20(#4539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(reranker): 重构重排序功能以提高可维护性 - 将 BaseReranker 类中的公共逻辑提取到受保护的方法中 - 优化了 JinaReranker、SiliconFlowReranker 和 VoyageReranker 的实现 - 新增 getRerankUrl 和 getRerankResult 方法以提高代码复用性 - 简化了重排序结果的处理逻辑 * refactor(reranker): 将 formatErrorMessage 方法的访问权限改为受保护 - 将 formatErrorMessage 方法的访问权限从公共 (public) 改为受保护 (protected) - 这一更改限制了方法的访问范围,仅允许子类访问该方法 - 有助于提高代码的封装性和安全性 --- src/main/reranker/BaseReranker.ts | 48 +++++++++++++++++++++++- src/main/reranker/JinaReranker.ts | 27 +------------ src/main/reranker/SiliconFlowReranker.ts | 26 +------------ src/main/reranker/VoyageReranker.ts | 26 +------------ 4 files changed, 53 insertions(+), 74 deletions(-) diff --git a/src/main/reranker/BaseReranker.ts b/src/main/reranker/BaseReranker.ts index 58a6426914..4109f53986 100644 --- a/src/main/reranker/BaseReranker.ts +++ b/src/main/reranker/BaseReranker.ts @@ -3,14 +3,60 @@ import { KnowledgeBaseParams } from '@types' export default abstract class BaseReranker { protected base: KnowledgeBaseParams + constructor(base: KnowledgeBaseParams) { if (!base.rerankModel) { throw new Error('Rerank model is required') } this.base = base } + abstract rerank(query: string, searchResults: ExtractChunkData[]): Promise + /** + * Get Rerank Request Url + */ + protected getRerankUrl() { + let baseURL = this.base?.rerankBaseURL?.endsWith('/') + ? this.base.rerankBaseURL.slice(0, -1) + : this.base.rerankBaseURL + // 必须携带/v1,否则会404 + if (baseURL && !baseURL.endsWith('/v1')) { + baseURL = `${baseURL}/v1` + } + + return `${baseURL}/rerank` + } + + /** + * Get Rerank Result + * @param searchResults + * @param rerankResults + * @protected + */ + protected getRerankResult( + searchResults: ExtractChunkData[], + rerankResults: Array<{ + index: number + relevance_score: number + }> + ) { + const resultMap = new Map(rerankResults.map((result) => [result.index, result.relevance_score || 0])) + + return searchResults + .map((doc: ExtractChunkData, index: number) => { + const score = resultMap.get(index) + if (score === undefined) return undefined + + return { + ...doc, + score + } + }) + .filter((doc): doc is ExtractChunkData => doc !== undefined) + .sort((a, b) => b.score - a.score) + } + public defaultHeaders() { return { Authorization: `Bearer ${this.base.rerankApiKey}`, @@ -18,7 +64,7 @@ export default abstract class BaseReranker { } } - public formatErrorMessage(url: string, error: any, requestBody: any) { + protected formatErrorMessage(url: string, error: any, requestBody: any) { const errorDetails = { url: url, message: error.message, diff --git a/src/main/reranker/JinaReranker.ts b/src/main/reranker/JinaReranker.ts index 718774ee22..207ddcb992 100644 --- a/src/main/reranker/JinaReranker.ts +++ b/src/main/reranker/JinaReranker.ts @@ -10,16 +10,7 @@ export default class JinaReranker extends BaseReranker { } public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise => { - let baseURL = this.base?.rerankBaseURL?.endsWith('/') - ? this.base.rerankBaseURL.slice(0, -1) - : this.base.rerankBaseURL - - // 必须携带/v1,否则会404 - if (baseURL && !baseURL.endsWith('/v1')) { - baseURL = `${baseURL}/v1` - } - - const url = `${baseURL}/rerank` + const url = this.getRerankUrl() const requestBody = { model: this.base.rerankModel, @@ -32,23 +23,9 @@ export default class JinaReranker extends BaseReranker { const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) const rerankResults = data.results - console.log(rerankResults) - const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0])) - return searchResults - .map((doc: ExtractChunkData, index: number) => { - const score = resultMap.get(index) - if (score === undefined) return undefined - - return { - ...doc, - score - } - }) - .filter((doc): doc is ExtractChunkData => doc !== undefined) - .sort((a, b) => b.score - a.score) + return this.getRerankResult(searchResults, rerankResults) } catch (error: any) { const errorDetails = this.formatErrorMessage(url, error, requestBody) - console.error('Jina Reranker API Error:', errorDetails) throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } diff --git a/src/main/reranker/SiliconFlowReranker.ts b/src/main/reranker/SiliconFlowReranker.ts index d37f547b24..0a27cf7e2a 100644 --- a/src/main/reranker/SiliconFlowReranker.ts +++ b/src/main/reranker/SiliconFlowReranker.ts @@ -10,16 +10,7 @@ export default class SiliconFlowReranker extends BaseReranker { } public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise => { - let baseURL = this.base?.rerankBaseURL?.endsWith('/') - ? this.base.rerankBaseURL.slice(0, -1) - : this.base.rerankBaseURL - - // 必须携带/v1,否则会404 - if (baseURL && !baseURL.endsWith('/v1')) { - baseURL = `${baseURL}/v1` - } - - const url = `${baseURL}/rerank` + const url = this.getRerankUrl() const requestBody = { model: this.base.rerankModel, @@ -34,20 +25,7 @@ export default class SiliconFlowReranker extends BaseReranker { const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) const rerankResults = data.results - const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0])) - - return searchResults - .map((doc: ExtractChunkData, index: number) => { - const score = resultMap.get(index) - if (score === undefined) return undefined - - return { - ...doc, - score - } - }) - .filter((doc): doc is ExtractChunkData => doc !== undefined) - .sort((a, b) => b.score - a.score) + return this.getRerankResult(searchResults, rerankResults) } catch (error: any) { const errorDetails = this.formatErrorMessage(url, error, requestBody) diff --git a/src/main/reranker/VoyageReranker.ts b/src/main/reranker/VoyageReranker.ts index 0cfc024eee..a2c0f5f8af 100644 --- a/src/main/reranker/VoyageReranker.ts +++ b/src/main/reranker/VoyageReranker.ts @@ -10,15 +10,7 @@ export default class VoyageReranker extends BaseReranker { } public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise => { - let baseURL = this.base?.rerankBaseURL?.endsWith('/') - ? this.base.rerankBaseURL.slice(0, -1) - : this.base.rerankBaseURL - - if (baseURL && !baseURL.endsWith('/v1')) { - baseURL = `${baseURL}/v1` - } - - const url = `${baseURL}/rerank` + const url = this.getRerankUrl() const requestBody = { model: this.base.rerankModel, @@ -37,21 +29,7 @@ export default class VoyageReranker extends BaseReranker { }) const rerankResults = data.data - - const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0])) - - return searchResults - .map((doc: ExtractChunkData, index: number) => { - const score = resultMap.get(index) - if (score === undefined) return undefined - - return { - ...doc, - score - } - }) - .filter((doc): doc is ExtractChunkData => doc !== undefined) - .sort((a, b) => b.score - a.score) + return this.getRerankResult(searchResults, rerankResults) } catch (error: any) { const errorDetails = this.formatErrorMessage(url, error, requestBody) From d38c4c7368dd8cb5eaa97729194c28c8e859bcab Mon Sep 17 00:00:00 2001 From: suyao Date: Tue, 8 Apr 2025 17:09:31 +0800 Subject: [PATCH 02/21] fix(MessageContent): handle optional chaining for grounding metadata and citations --- .../pages/home/Messages/MessageContent.tsx | 24 +++++++++++-------- src/renderer/src/types/index.ts | 3 ++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index a0fadbb612..df35839ceb 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -86,7 +86,7 @@ const MessageContent: React.FC = ({ message: _message, model }) => { const searchResults = message?.metadata?.webSearch?.results || message?.metadata?.webSearchInfo || - message?.metadata?.groundingMetadata?.groundingChunks.map((chunk) => chunk.web) || + message?.metadata?.groundingMetadata?.groundingChunks?.map((chunk) => chunk?.web) || message?.metadata?.annotations?.map((annotation) => annotation.url_citation) || [] const citationsUrls = formattedCitations || [] @@ -222,18 +222,22 @@ const MessageContent: React.FC = ({ message: _message, model }) => { {message?.metadata?.groundingMetadata && message.status == 'success' && ( <> ({ - number: index + 1, - url: chunk.web?.uri, - title: chunk.web?.title, - showFavicon: false - }))} + citations={ + message.metadata.groundingMetadata?.groundingChunks?.map((chunk, index) => ({ + number: index + 1, + url: chunk?.web?.uri || '', + title: chunk?.web?.title, + showFavicon: false + })) || [] + } /> diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index ca5fbafcc2..7047c469a7 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -1,3 +1,4 @@ +import { GroundingMetadata } from '@google/generative-ai' import OpenAI from 'openai' import React from 'react' import { BuiltinTheme } from 'shiki' @@ -72,7 +73,7 @@ export type Message = { enabledMCPs?: MCPServer[] metadata?: { // Gemini - groundingMetadata?: any + groundingMetadata?: GroundingMetadata // Perplexity Or Openrouter citations?: string[] // OpenAI From 97c1d67cbf310f5fab1110e10716f0fa9676043c Mon Sep 17 00:00:00 2001 From: suyao Date: Tue, 8 Apr 2025 17:18:39 +0800 Subject: [PATCH 03/21] fix(formats): add optional chaining for grounding support properties to prevent errors --- src/renderer/src/utils/formats.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/utils/formats.ts b/src/renderer/src/utils/formats.ts index f08bb13141..23a579131a 100644 --- a/src/renderer/src/utils/formats.ts +++ b/src/renderer/src/utils/formats.ts @@ -71,12 +71,16 @@ export function withGeminiGrounding(message: Message) { let content = message.content groundingSupports.forEach((support) => { - const text = support.segment.text - const indices = support.groundingChunkIndices - const nodes = indices.reduce((acc, index) => { + const text = support?.segment + const indices = support?.groundingChunckIndices + + if (!text || !indices) return + + const nodes = indices.reduce((acc, index) => { acc.push(`${index + 1}`) return acc }, []) + content = content.replace(text, `${text} ${nodes.join(' ')}`) }) From 037027f1f407b69b378bcadf38e9e0f426ce73f5 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 8 Apr 2025 15:01:43 +0800 Subject: [PATCH 04/21] fix(McpService): improve client connection handling with error logging --- src/main/services/MCPService.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index b226251365..6f6199a501 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -46,18 +46,22 @@ class McpService { // Check if we already have a client for this server configuration const existingClient = this.clients.get(serverKey) if (existingClient) { - // Check if the existing client is still connected - const pingResult = await existingClient.ping() - Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult) - // If the ping fails, remove the client from the cache - // and create a new one - if (!pingResult) { + try { + // Check if the existing client is still connected + const pingResult = await existingClient.ping() + Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult) + // If the ping fails, remove the client from the cache + // and create a new one + if (!pingResult) { + this.clients.delete(serverKey) + } else { + return existingClient + } + } catch (error) { + Logger.error(`[MCP] Error pinging server ${server.name}:`, error) this.clients.delete(serverKey) - } else { - return existingClient } } - // Create new client instance for each connection const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} }) From 3aaa1848f00fdd07a0af23285229a6fb5a0f0f6b Mon Sep 17 00:00:00 2001 From: Teo Date: Tue, 8 Apr 2025 19:37:11 +0800 Subject: [PATCH 05/21] style(ProviderSettings): Refactor ProviderSettings UI (#4475) * chore(version): 1.1.19 * style(ProviderSettings): Refactor ProviderSettings UI * style(CustomTag, ModelTagsWithLabel): enhance layout and styling for better UI consistency * refactor(CustomTag, ModelTagsWithLabel, MentionModelsButton): update props handling and improve component usage * feat(CustomTag, ModelTagsWithLabel): add tooltip support and improve label visibility based on container size * fix(ModelTagsWithLabel): adjust maxWidth for non-Chinese languages to improve layout * style(ModelList): add text overflow handling for list item names * feat(ModelList): enhance group label with item count using CustomTag * feat(FileItem): add style prop for customizable background color in FileItem component * style(index.scss): update border color variables for improved UI consistency * style(EditModelsPopup): update background color for model items to enhance visual distinction * style(HealthCheckPopup): update button size for improved usability * feat(CustomCollapse): add collapsible prop to customize collapse behavior * chore: remove hover models color --------- Co-authored-by: kangfenmao Co-authored-by: eeee0717 --- src/renderer/src/assets/styles/index.scss | 6 +- .../src/components/CustomCollapse.tsx | 16 +- src/renderer/src/components/CustomTag.tsx | 40 +++ src/renderer/src/components/Icons/SVGIcon.tsx | 13 + .../src/components/ModelTagsWithLabel.tsx | 113 +++++++++ src/renderer/src/pages/files/FileItem.tsx | 33 ++- src/renderer/src/pages/files/FileList.tsx | 4 +- .../home/Inputbar/MentionModelsButton.tsx | 4 +- .../src/pages/home/Tabs/TopicsTab.tsx | 1 - .../src/pages/knowledge/KnowledgeContent.tsx | 92 +++---- .../ProviderSettings/AddModelPopup.tsx | 12 +- .../ProviderSettings/EditModelsPopup.tsx | 230 ++++++++++-------- .../ProviderSettings/HealthCheckPopup.tsx | 2 +- .../ProviderSettings/ModelEditContent.tsx | 14 +- .../settings/ProviderSettings/ModelList.tsx | 208 +++++++++------- .../ProviderSettings/ProviderSetting.tsx | 7 +- src/renderer/src/pages/settings/index.tsx | 2 +- 17 files changed, 518 insertions(+), 279 deletions(-) create mode 100644 src/renderer/src/components/CustomTag.tsx create mode 100644 src/renderer/src/components/Icons/SVGIcon.tsx create mode 100644 src/renderer/src/components/ModelTagsWithLabel.tsx diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 09659681fe..13eb0f9a0e 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -36,7 +36,7 @@ --color-text: var(--color-text-1); --color-icon: #ffffff99; --color-icon-white: #ffffff; - --color-border: #ffffff15; + --color-border: #ffffff19; --color-border-soft: #ffffff10; --color-border-mute: #ffffff05; --color-error: #f44336; @@ -80,7 +80,7 @@ body { body[theme-mode='light'] { --color-white: #ffffff; - --color-white-soft: #f2f2f2; + --color-white-soft: rgba(0, 0, 0, 0.04); --color-white-mute: #eee; --color-black: #1b1b1f; @@ -108,7 +108,7 @@ body[theme-mode='light'] { --color-text: var(--color-text-1); --color-icon: #00000099; --color-icon-white: #000000; - --color-border: #00000015; + --color-border: #00000019; --color-border-soft: #00000010; --color-border-mute: #00000005; --color-error: #f44336; diff --git a/src/renderer/src/components/CustomCollapse.tsx b/src/renderer/src/components/CustomCollapse.tsx index bcafec7a17..9b0ba05315 100644 --- a/src/renderer/src/components/CustomCollapse.tsx +++ b/src/renderer/src/components/CustomCollapse.tsx @@ -5,9 +5,19 @@ interface CustomCollapseProps { label: React.ReactNode extra: React.ReactNode children: React.ReactNode + destroyInactivePanel?: boolean + defaultActiveKey?: string[] + collapsible?: 'header' | 'icon' | 'disabled' } -const CustomCollapse: FC = ({ label, extra, children }) => { +const CustomCollapse: FC = ({ + label, + extra, + children, + destroyInactivePanel = false, + defaultActiveKey = ['1'], + collapsible = undefined +}) => { const CollapseStyle = { width: '100%', background: 'transparent', @@ -27,7 +37,9 @@ const CustomCollapse: FC = ({ label, extra, children }) => = ({ children, icon, color, size = 12, tooltip }) => { + return ( + + + {icon && icon} {children} + + + ) +} + +export default CustomTag + +const Tag = styled.div<{ $color: string; $size: number }>` + display: inline-flex; + align-items: center; + gap: 4px; + padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px; + border-radius: 99px; + color: ${({ $color }) => $color}; + background-color: ${({ $color }) => $color + '20'}; + font-size: ${({ $size }) => $size}px; + line-height: 1; + white-space: nowrap; + .iconfont { + font-size: ${({ $size }) => $size}px; + color: ${({ $color }) => $color}; + } +` diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx new file mode 100644 index 0000000000..e6521a97c7 --- /dev/null +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -0,0 +1,13 @@ +import { SVGProps } from 'react' + +export const StreamlineGoodHealthAndWellBeing = (props: SVGProps) => { + return ( + + {/* Icon from Streamline by Streamline - https://creativecommons.org/licenses/by/4.0/ */} + + + + + + ) +} diff --git a/src/renderer/src/components/ModelTagsWithLabel.tsx b/src/renderer/src/components/ModelTagsWithLabel.tsx new file mode 100644 index 0000000000..de92fdd7f8 --- /dev/null +++ b/src/renderer/src/components/ModelTagsWithLabel.tsx @@ -0,0 +1,113 @@ +import { EyeOutlined, GlobalOutlined, ToolOutlined } from '@ant-design/icons' +import { + isEmbeddingModel, + isFunctionCallingModel, + isReasoningModel, + isRerankModel, + isVisionModel, + isWebSearchModel +} from '@renderer/config/models' +import i18n from '@renderer/i18n' +import { Model } from '@renderer/types' +import { isFreeModel } from '@renderer/utils' +import { FC, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import CustomTag from './CustomTag' + +interface ModelTagsProps { + model: Model + showFree?: boolean + showReasoning?: boolean + showToolsCalling?: boolean + size?: number + showLabel?: boolean +} + +const ModelTagsWithLabel: FC = ({ + model, + showFree = true, + showReasoning = true, + showToolsCalling = true, + size = 12, + showLabel = true +}) => { + const { t } = useTranslation() + const [_showLabel, _setShowLabel] = useState(showLabel) + const containerRef = useRef(null) + const resizeObserver = useRef(null) + + useEffect(() => { + if (!showLabel) return + + if (containerRef.current) { + const currentElement = containerRef.current + resizeObserver.current = new ResizeObserver((entries) => { + const maxWidth = i18n.language.startsWith('zh') ? 300 : 350 + + for (const entry of entries) { + const { width } = entry.contentRect + _setShowLabel(width >= maxWidth) + } + }) + resizeObserver.current.observe(currentElement) + + return () => { + if (resizeObserver.current) { + resizeObserver.current.unobserve(currentElement) + } + } + } + + return undefined + }, [showLabel]) + + return ( + + {isVisionModel(model) && ( + } tooltip={t('models.type.vision')}> + {_showLabel ? t('models.type.vision') : ''} + + )} + {isWebSearchModel(model) && ( + } tooltip={t('models.type.websearch')}> + {_showLabel ? t('models.type.websearch') : ''} + + )} + {showReasoning && isReasoningModel(model) && ( + } + tooltip={t('models.type.reasoning')}> + {_showLabel ? t('models.type.reasoning') : ''} + + )} + {showToolsCalling && isFunctionCallingModel(model) && ( + } tooltip={t('models.function_calling')}> + {_showLabel ? t('models.function_calling') : ''} + + )} + {isEmbeddingModel(model) && ( + + )} + {showFree && isFreeModel(model) && ( + + )} + {isRerankModel(model) && ( + + )} + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + flex-wrap: wrap; +` + +export default ModelTagsWithLabel diff --git a/src/renderer/src/pages/files/FileItem.tsx b/src/renderer/src/pages/files/FileItem.tsx index 5aa0f7c286..13bc4f7291 100644 --- a/src/renderer/src/pages/files/FileItem.tsx +++ b/src/renderer/src/pages/files/FileItem.tsx @@ -18,11 +18,13 @@ import styled from 'styled-components' interface FileItemProps { fileInfo: { + icon?: React.ReactNode name: React.ReactNode | string ext: string extra?: React.ReactNode | string actions: React.ReactNode } + style?: React.CSSProperties } const getFileIcon = (type?: string) => { @@ -73,18 +75,18 @@ const getFileIcon = (type?: string) => { return } -const FileItem: React.FC = ({ fileInfo }) => { - const { name, ext, extra, actions } = fileInfo +const FileItem: React.FC = ({ fileInfo, style }) => { + const { name, ext, extra, actions, icon } = fileInfo return ( - + - {getFileIcon(ext)} - + {icon || getFileIcon(ext)} + {name} {extra && {extra}} - {actions} + {actions} ) @@ -96,7 +98,9 @@ const FileItemCard = styled.div` overflow: hidden; border: 0.5px solid var(--color-border); flex-shrink: 0; - transition: box-shadow 0.2s ease; + transition: + box-shadow 0.2s ease, + background-color 0.2s ease; --shadow-color: rgba(0, 0, 0, 0.05); &:hover { box-shadow: @@ -109,15 +113,19 @@ const FileItemCard = styled.div` ` const CardContent = styled.div` - padding: 8px 16px; + padding: 8px 8px 8px 16px; display: flex; - align-items: center; + align-items: stretch; gap: 16px; ` const FileIcon = styled.div` + max-height: 44px; color: var(--color-text-3); font-size: 32px; + display: flex; + align-items: center; + justify-content: center; ` const FileName = styled.div` @@ -140,4 +148,11 @@ const FileInfo = styled.div` color: var(--color-text-2); ` +const FileActions = styled.div` + max-height: 44px; + display: flex; + align-items: center; + justify-content: center; +` + export default memo(FileItem) diff --git a/src/renderer/src/pages/files/FileList.tsx b/src/renderer/src/pages/files/FileList.tsx index 41b1ebb100..e00c7cbb92 100644 --- a/src/renderer/src/pages/files/FileList.tsx +++ b/src/renderer/src/pages/files/FileList.tsx @@ -66,7 +66,7 @@ const FileList: React.FC = ({ id, list, files }) => { = ({ id, list, files }) => { {(item) => (
= ({ ref, mentionModels, onMentionModel, To .reverse() .map((item) => ({ label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`, - description: , + description: , icon: ( {first(item.model.name)} diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 8ff853e1b2..000294a3bb 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -493,7 +493,6 @@ const TopicListItem = styled.div` } .menu { opacity: 1; - background-color: var(--color-background-soft); &:hover { color: var(--color-text-2); } diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 2b58243291..866bb20f89 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -7,6 +7,7 @@ import { SearchOutlined, SettingOutlined } from '@ant-design/icons' +import CustomTag from '@renderer/components/CustomTag' import Ellipsis from '@renderer/components/Ellipsis' import { HStack } from '@renderer/components/Layout' import PromptPopup from '@renderer/components/Popups/PromptPopup' @@ -18,7 +19,7 @@ import { getProviderName } from '@renderer/services/ProviderService' import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types' import { formatFileSize } from '@renderer/utils' import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant' -import { Alert, Button, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd' +import { Alert, Button, Dropdown, Empty, Flex, message, Tooltip, Upload } from 'antd' import dayjs from 'dayjs' import VirtualList from 'rc-virtual-list' import { FC } from 'react' @@ -269,8 +270,8 @@ const KnowledgeContent: FC = ({ selectedBase }) => { ) : ( 5 ? 400 : fileItems.length * 80} - itemHeight={80} + height={fileItems.length > 5 ? 400 : fileItems.length * 75} + itemHeight={75} itemKey="id" styles={{ verticalScrollBar: { @@ -283,7 +284,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => { {(item) => { const file = item.content as FileType return ( -
+
= ({ selectedBase }) => { ))} - -
- -
-
-
- -
-
- {providerName && {providerName}} - {base.model.name} - {t('models.dimensions', { dimensions: base.dimensions || 0 })} -
-
- - {base.rerankModel && ( + + {t('knowledge.model_info')} + + }> +
- +
- {rerankModelProviderName && {rerankModelProviderName}} - {base.rerankModel?.name} + {providerName && {providerName}} + {base.model.name} + {t('models.dimensions', { dimensions: base.dimensions || 0 })}
- )} -
- - - + {base.rerankModel && ( +
+
+ +
+
+ {rerankModelProviderName && {rerankModelProviderName}} + {base.rerankModel?.name} +
+
+ )} +
+ @@ -588,9 +599,9 @@ const CollapseLabel = ({ label, count }: { label: string; count: number }) => { return ( - + {count} - + ) } @@ -605,12 +616,6 @@ const MainContent = styled(Scrollbar)` gap: 16px; ` -const IndexSection = styled.div` - margin-top: 20px; - display: flex; - justify-content: center; -` - const ModelInfo = styled.div` display: flex; flex-direction: column; @@ -629,6 +634,7 @@ const ModelInfo = styled.div` display: flex; align-items: flex-start; gap: 10px; + margin-top: 16px; } .label-column { diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx index d45c9c9d53..6b45ac78f9 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddModelPopup.tsx @@ -120,13 +120,11 @@ const PopupContainer: React.FC = ({ title, provider, resolve }) => { tooltip={t('settings.models.add.group_name.tooltip')}> - - -
- -
+ + + diff --git a/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx index ab094599b9..8766df3aff 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/EditModelsPopup.tsx @@ -1,6 +1,7 @@ -import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons' -import { Center } from '@renderer/components/Layout' -import ModelTags from '@renderer/components/ModelTags' +import { LoadingOutlined, MinusOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons' +import CustomCollapse from '@renderer/components/CustomCollapse' +import CustomTag from '@renderer/components/CustomTag' +import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel' import { getModelLogo, isEmbeddingModel, @@ -12,11 +13,12 @@ import { SYSTEM_MODELS } from '@renderer/config/models' import { useProvider } from '@renderer/hooks/useProvider' +import FileItem from '@renderer/pages/files/FileItem' import { fetchModels } from '@renderer/services/ApiService' import { Model, Provider } from '@renderer/types' import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils' -import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd' -import Search from 'antd/es/input/Search' +import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'antd' +import Input from 'antd/es/input/Input' import { groupBy, isEmpty, uniqBy } from 'lodash' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -163,51 +165,63 @@ const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { width="680px" styles={{ content: { padding: 0 }, - header: { padding: 22, paddingBottom: 15 } + header: { padding: '16px 22px 30px 22px' } }} centered> -
- setFilterType(e.target.value)} - buttonStyle="solid"> - {t('models.all')} - {t('models.type.reasoning')} - {t('models.type.vision')} - {t('models.type.websearch')} - {t('models.type.free')} - {t('models.type.embedding')} - {t('models.type.rerank')} - {t('models.type.function_calling')} - -
- } + size="large" ref={searchInputRef} placeholder={t('settings.provider.search_placeholder')} allowClear onChange={(e) => setSearchText(e.target.value)} - onSearch={setSearchText} + /> + setFilterType(key)} />
- {Object.keys(modelGroups).map((group) => { + {Object.keys(modelGroups).map((group, i) => { const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id)) return ( -
- - {group} -
+ = 5 ? [] : ['1']} + label={ + + {group} + + {modelGroups[group].length} + + + } + extra={ +
-
- {modelGroups[group].map((model) => { - return ( - - - - {model?.name?.[0]?.toUpperCase()} - - - - {model.name} - - - {!isEmpty(model.description) && ( - - - - )} - - - {isModelInProvider(provider, model.id) ? ( -
+ + }> + + {modelGroups[group].map((model) => ( + {model?.name?.[0]?.toUpperCase()}, + name: ( + + + {model.id} + + } + placement="top"> + {model.name} + + + ), + extra: ( +
+ + + {model.description && ( + + {model.description} + + )} +
+ ), + ext: '.model', + actions: ( +
+ {isModelInProvider(provider, model.id) ? ( +
+ ) + }} + /> + ))} +
+ ) })} {isEmpty(list) && } @@ -264,7 +306,6 @@ const SearchContainer = styled.div` flex-direction: column; gap: 15px; padding: 0 22px; - padding-bottom: 10px; margin-top: -10px; .ant-radio-group { @@ -274,37 +315,21 @@ const SearchContainer = styled.div` ` const ListContainer = styled.div` - max-height: 70vh; + height: calc(100vh - 300px); overflow-y: scroll; - padding-bottom: 20px; -` - -const ListHeader = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: var(--color-background-soft); - padding: 8px 22px; - color: var(--color-text); - opacity: 0.4; -` - -const ListItem = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 10px 22px; -` - -const ListItemHeader = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; + padding: 0 6px 16px 6px; + margin-left: 16px; margin-right: 10px; - height: 22px; + display: flex; + flex-direction: column; + gap: 16px; +` + +const FlexColumn = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; ` const ListItemName = styled.div` @@ -314,8 +339,8 @@ const ListItemName = styled.div` gap: 10px; color: var(--color-text); font-size: 14px; + line-height: 1; font-weight: 600; - margin-left: 6px; ` const ModelHeaderTitle = styled.div` @@ -325,11 +350,6 @@ const ModelHeaderTitle = styled.div` margin-right: 10px; ` -const Question = styled(QuestionCircleOutlined)` - cursor: pointer; - color: #888; -` - export default class EditModelsPopup { static topviewId = 0 static hide() { diff --git a/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx index 22aac71c0a..839dc1fd92 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/HealthCheckPopup.tsx @@ -160,7 +160,7 @@ const PopupContainer: React.FC = ({ title, apiKeys, resolve }) => { /> - diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx index 2372e04168..7d458dbb3e 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx @@ -102,18 +102,14 @@ const ModelEditContent: FC = ({ model, onUpdateModel, ope
- -
- -
- setShowModelTypes(!showModelTypes)} - style={{ position: 'absolute', right: 0 }}> + + setShowModelTypes(!showModelTypes)}> {t('settings.moresetting')} {showModelTypes ? : } +
{showModelTypes && ( diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx index b461332a30..d9f8e1e7d3 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx @@ -5,20 +5,24 @@ import { ExclamationCircleFilled, LoadingOutlined, MinusCircleOutlined, + MinusOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' -import ModelTags from '@renderer/components/ModelTags' +import CustomCollapse from '@renderer/components/CustomCollapse' +import CustomTag from '@renderer/components/CustomTag' +import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel' import { getModelLogo } from '@renderer/config/models' import { PROVIDER_CONFIG } from '@renderer/config/providers' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' import { useProvider } from '@renderer/hooks/useProvider' +import FileItem from '@renderer/pages/files/FileItem' import { ModelCheckStatus } from '@renderer/services/HealthCheckService' import { useAppDispatch } from '@renderer/store' import { setModel } from '@renderer/store/assistants' import { Model } from '@renderer/types' import { maskApiKey } from '@renderer/utils/api' -import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd' +import { Avatar, Button, Flex, Tooltip, Typography } from 'antd' import { groupBy, sortBy, toPairs } from 'lodash' import React, { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -240,71 +244,107 @@ const ModelList: React.FC = ({ providerId, modelStatuses = [], s return ( <> - {Object.keys(sortedModelGroups).map((group) => ( - - - modelGroups[group] - .filter((model) => provider.models.some((m) => m.id === model.id)) - .forEach((model) => removeModel(model)) - } - /> - - } - style={{ marginBottom: '10px', border: '0.5px solid var(--color-border)' }} - size="small"> - {sortedModelGroups[group].map((model) => { - const modelStatus = modelStatuses.find((status) => status.model.id === model.id) - const isChecking = modelStatus?.checking === true - console.log('model', model.id, getModelLogo(model.id)) + + {Object.keys(sortedModelGroups).map((group, i) => ( + + {group} + + {modelGroups[group].length} + + + } + extra={ + + + modelGroups[group] + .filter((model) => provider.models.some((m) => m.id === model.id)) + .forEach((model) => removeModel(model)) + } + /> + + }> + + {sortedModelGroups[group].map((model) => { + const modelStatus = modelStatuses.find((status) => status.model.id === model.id) + const isChecking = modelStatus?.checking === true - return ( - - - - {model?.name?.[0]?.toUpperCase()} - - - {model?.name} - - - !isChecking && onEditModel(model)} - style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }} + return ( + {model?.name?.[0]?.toUpperCase()}, + name: ( + + + {model.id} + + } + placement="top"> + {model.name} + + + ), + extra: ( +
+ +
+ ), + ext: '.model', + actions: ( + + {renderLatencyText(modelStatus)} + {renderStatusIndicator(modelStatus)} + + - }> - handleDrop([file as File])} - multiple={true} - accept={fileTypes.join(',')} - style={{ marginTop: 10, background: 'transparent' }}> -

{t('knowledge.drag_file')}

-

- {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} -

-
- - - {fileItems.length === 0 ? ( - - ) : ( - 5 ? 400 : fileItems.length * 75} - itemHeight={75} - itemKey="id" - styles={{ - verticalScrollBar: { - width: 6 - }, - verticalScrollBarThumb: { - background: 'var(--color-scrollbar-thumb)' - } - }}> - {(item) => { - const file = item.content as FileType - return ( -
- window.api.file.openPath(file.path)}> - - {file.origin_name} - - - ), - ext: file.ext, - extra: `${dayjs(file.created_at).format('MM-DD HH:mm')} · ${formatFileSize(file.size)}`, - actions: ( - - {item.uniqueId && ( -
- ) - }} -
- )} -
- - - } - extra={ - - }> - - {directoryItems.length === 0 && } - {directoryItems.reverse().map((item) => ( - window.api.file.openPath(item.content as string)}> - - {item.content as string} - - - ), - ext: '.folder', - extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, - actions: ( - - {item.uniqueId && - }> - - {urlItems.length === 0 && } - {urlItems.reverse().map((item) => ( - , - label: t('knowledge.edit_remark'), - onClick: () => handleEditRemark(item) - }, - { - key: 'copy', - icon: , - label: t('common.copy'), - onClick: () => { - navigator.clipboard.writeText(item.content as string) - message.success(t('message.copied')) - } - } - ] - }} - trigger={['contextMenu']}> - - - - - {item.remark || (item.content as string)} - - - - - - ), - ext: '.url', - extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, - actions: ( - - {item.uniqueId && - }> - - {sitemapItems.length === 0 && } - {sitemapItems.reverse().map((item) => ( - - - - - {item.content as string} - - - - - ), - ext: '.sitemap', - extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, - actions: ( - - {item.uniqueId && - }> - - {noteItems.length === 0 && } - {noteItems.reverse().map((note) => ( - handleEditNote(note)}>{(note.content as string).slice(0, 50)}..., - ext: '.txt', - extra: `${dayjs(note.created_at).format('MM-DD HH:mm')}`, - actions: ( - - - }> + + + + + + }> + handleDrop([file as File])} + multiple={true} + accept={fileTypes.join(',')} + style={{ marginTop: 10, background: 'transparent' }}> +

{t('knowledge.drag_file')}

+

+ {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} +

+
- - + + {fileItems.length === 0 ? ( + + ) : ( + 5 ? 400 : fileItems.length * 75} + itemHeight={75} + itemKey="id" + styles={{ + verticalScrollBar: { + width: 6 + }, + verticalScrollBarThumb: { + background: 'var(--color-scrollbar-thumb)' + } + }}> + {(item) => { + const file = item.content as FileType + return ( +
+ window.api.file.openPath(file.path)}> + + {file.origin_name} + + + ), + ext: file.ext, + extra: `${dayjs(file.created_at).format('MM-DD HH:mm')} · ${formatFileSize(file.size)}`, + actions: ( + + {item.uniqueId && ( +
+ ) + }} +
+ )} +
+
+ + } + defaultActiveKey={[]} + activeKey={expandAll ? ['1'] : undefined} + extra={ + + }> + + {directoryItems.length === 0 && } + {directoryItems.reverse().map((item) => ( + window.api.file.openPath(item.content as string)}> + + {item.content as string} + + + ), + ext: '.folder', + extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, + actions: ( + + {item.uniqueId && + }> + + {urlItems.length === 0 && } + {urlItems.reverse().map((item) => ( + , + label: t('knowledge.edit_remark'), + onClick: () => handleEditRemark(item) + }, + { + key: 'copy', + icon: , + label: t('common.copy'), + onClick: () => { + navigator.clipboard.writeText(item.content as string) + message.success(t('message.copied')) + } + } + ] + }} + trigger={['contextMenu']}> + + + + + {item.remark || (item.content as string)} + + + + + + ), + ext: '.url', + extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, + actions: ( + + {item.uniqueId && + }> + + {sitemapItems.length === 0 && } + {sitemapItems.reverse().map((item) => ( + + + + + {item.content as string} + + + + + ), + ext: '.sitemap', + extra: `${dayjs(item.created_at).format('MM-DD HH:mm')}`, + actions: ( + + {item.uniqueId && + }> + + {noteItems.length === 0 && } + {noteItems.reverse().map((note) => ( + handleEditNote(note)}>{(note.content as string).slice(0, 50)}..., + ext: '.txt', + extra: `${dayjs(note.created_at).format('MM-DD HH:mm')}`, + actions: ( + +