From c66dfa50cf54d133243d5c66ca6d429da9fc2de5 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 9 May 2025 15:53:51 +0800 Subject: [PATCH 1/6] fix: message citations styles and bugs --- .../home/Messages/Blocks/MainTextBlock.tsx | 4 +- .../pages/home/Messages/ChatNavigation.tsx | 1 + .../src/pages/home/Messages/CitationsList.tsx | 53 +++++++++++-------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx index 1ef3edc6b5..d57a0f650c 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx @@ -57,8 +57,10 @@ const MainTextBlock: React.FC = ({ block, citationBlockId, role, mentions title: citation.title || citation.hostname || '', content: citation.content?.substring(0, 200) } + const isLink = citation.url.startsWith('http') const citationJson = encodeHTML(JSON.stringify(supData)) - const citationTag = `[${citationNum}](${citation.url})` + const supTag = `${citationNum}` + const citationTag = isLink ? `[${supTag}](${citation.url})` : supTag // Replace all occurrences of [citationNum] with the formatted citation const regex = new RegExp(`\\[${citationNum}\\]`, 'g') diff --git a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx index a075e85d0c..b0f1c834e5 100644 --- a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx +++ b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx @@ -375,6 +375,7 @@ const ChatNavigation: FC = ({ containerId }) => { width={680} destroyOnClose styles={{ + header: { border: 'none' }, body: { padding: 0, height: 'calc(100% - 55px)' diff --git a/src/renderer/src/pages/home/Messages/CitationsList.tsx b/src/renderer/src/pages/home/Messages/CitationsList.tsx index 9a9134766f..f56d8d7e47 100644 --- a/src/renderer/src/pages/home/Messages/CitationsList.tsx +++ b/src/renderer/src/pages/home/Messages/CitationsList.tsx @@ -2,7 +2,7 @@ import Favicon from '@renderer/components/Icons/FallbackFavicon' import { HStack } from '@renderer/components/Layout' import { fetchWebContent } from '@renderer/utils/fetch' import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query' -import { Button, Drawer } from 'antd' +import { Button, Drawer, Skeleton } from 'antd' import { FileSearch } from 'lucide-react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -90,6 +90,7 @@ const CitationsList: React.FC = ({ citations }) => { onClose={() => setOpen(false)} open={open} width={680} + styles={{ header: { border: 'none' }, body: { paddingTop: 0 } }} destroyOnClose={false}> {open && citations.map((citation) => ( @@ -114,8 +115,6 @@ const handleLinkClick = (url: string, event: React.MouseEvent) => { } const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => { - const { t } = useTranslation() - const { data: fetchedContent, isLoading } = useQuery({ queryKey: ['webContent', citation.url], queryFn: async () => { @@ -129,44 +128,49 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => { return ( -
+ {citation.showFavicon && citation.url && ( )} - handleLinkClick(citation.url, e)}> + handleLinkClick(citation.url, e)}> {citation.title || {citation.hostname}} -
- {isLoading ?
{t('common.loading')}
: fetchedContent} + + {isLoading ? ( + + ) : ( + {fetchedContent} + )}
) } const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => ( -
+ {citation.showFavicon && } - handleLinkClick(citation.url, e)}> + handleLinkClick(citation.url, e)}> {citation.title} -
- {citation.content && truncateText(citation.content, 100)} + + {citation.content && truncateText(citation.content, 100)}
) const OpenButton = styled(Button)` display: flex; align-items: center; - padding: 2px 6px; + padding: 3px 8px; margin-bottom: 8px; align-self: flex-start; font-size: 12px; + background-color: var(--color-background-soft); + border-radius: var(--list-item-border-radius); ` const PreviewIcons = styled.div` display: flex; align-items: center; - margin-right: 8px; ` const PreviewIcon = styled.div` @@ -193,10 +197,6 @@ const CitationLink = styled.a` color: var(--color-text-1); text-decoration: none; - &:hover { - text-decoration: underline; - } - .hostname { color: var(--color-link); } @@ -212,13 +212,20 @@ const WebSearchCard = styled.div` border: 1px solid var(--color-border); background-color: var(--color-background); transition: all 0.3s ease; +` - &:hover { - box-shadow: 0 4px 12px var(--color-border-soft); - background-color: var(--color-hover); - border-color: var(--color-primary-soft); - transform: translateY(-2px); - } +const WebSearchCardHeader = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin-bottom: 6px; +` + +const WebSearchCardContent = styled.div` + font-size: 13px; + line-height: 1.6; + color: var(--color-text-2); ` export default CitationsList From 8786ba6410c967aa70d1115ec079cf71acaa6c9d Mon Sep 17 00:00:00 2001 From: one Date: Fri, 9 May 2025 16:43:22 +0800 Subject: [PATCH 2/6] fix: SelectModelPopup sticky header (#5795) * fix: remove console logs * refactor: use onScroll instead of onItemsRendered --- .../components/Popups/SelectModelPopup.tsx | 84 +++++++++++-------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/src/renderer/src/components/Popups/SelectModelPopup.tsx b/src/renderer/src/components/Popups/SelectModelPopup.tsx index c883e02ffa..c19c5def9c 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup.tsx @@ -59,9 +59,11 @@ const PopupContainer: React.FC = ({ model, resolve }) => { const [pinnedModels, setPinnedModels] = useState([]) const [_focusedItemKey, setFocusedItemKey] = useState('') const focusedItemKey = useDeferredValue(_focusedItemKey) - const [currentStickyGroup, setCurrentStickyGroup] = useState(null) + const [_stickyGroup, setStickyGroup] = useState(null) + const stickyGroup = useDeferredValue(_stickyGroup) const firstGroupRef = useRef(null) const scrollTriggerRef = useRef('initial') + const lastScrollOffsetRef = useRef(0) // 当前选中的模型ID const currentModelId = model ? getModelUniqId(model) : '' @@ -220,6 +222,45 @@ const PopupContainer: React.FC = ({ model, resolve }) => { return items }, [providers, getFilteredModels, pinnedModels, searchText, t, createModelItem]) + // 基于滚动位置更新sticky分组标题 + const updateStickyGroup = useCallback( + (scrollOffset?: number) => { + if (listItems.length === 0) { + setStickyGroup(null) + return + } + + // 基于滚动位置计算当前可见的第一个项的索引 + const estimatedIndex = Math.floor((scrollOffset ?? lastScrollOffsetRef.current) / ITEM_HEIGHT) + + // 从该索引向前查找最近的分组标题 + for (let i = estimatedIndex - 1; i >= 0; i--) { + if (i < listItems.length && listItems[i]?.type === 'group') { + setStickyGroup(listItems[i]) + return + } + } + + // 找不到则使用第一个分组标题 + setStickyGroup(firstGroupRef.current ?? null) + }, + [listItems] + ) + + // 在listItems变化时更新sticky group + useEffect(() => { + updateStickyGroup() + }, [listItems, updateStickyGroup]) + + // 处理列表滚动事件,更新lastScrollOffset并更新sticky分组 + const handleScroll = useCallback( + ({ scrollOffset }) => { + lastScrollOffsetRef.current = scrollOffset + updateStickyGroup(scrollOffset) + }, + [updateStickyGroup] + ) + // 获取可选择的模型项(过滤掉分组标题) const modelItems = useMemo(() => { return listItems.filter((item) => item.type === 'model') @@ -257,9 +298,6 @@ const PopupContainer: React.FC = ({ model, resolve }) => { const alignment = scrollTriggerRef.current === 'keyboard' ? 'auto' : 'center' listRef.current?.scrollToItem(index, alignment) - console.log('focusedItemKey', focusedItemKey) - console.log('scrollToFocusedItem', index, alignment) - // 滚动后重置触发器 scrollTriggerRef.current = 'none' }, [focusedItemKey, listItems]) @@ -365,41 +403,19 @@ const PopupContainer: React.FC = ({ model, resolve }) => { if (!open) return setTimeout(() => inputRef.current?.focus(), 0) scrollTriggerRef.current = 'initial' + lastScrollOffsetRef.current = 0 }, [open]) - // 初始化sticky分组标题 - useEffect(() => { - if (firstGroupRef.current) { - setCurrentStickyGroup(firstGroupRef.current) - } - }, [listItems]) - - const handleItemsRendered = useCallback( - ({ visibleStartIndex }: { visibleStartIndex: number; visibleStopIndex: number }) => { - // 从可见区域的起始位置向前查找最近的分组标题 - for (let i = visibleStartIndex - 1; i >= 0; i--) { - if (listItems[i]?.type === 'group') { - setCurrentStickyGroup(listItems[i]) - return - } - } - - // 找不到则使用第一个分组标题 - setCurrentStickyGroup(firstGroupRef.current ?? null) - }, - [listItems] - ) - const RowData = useMemo( (): VirtualizedRowData => ({ listItems, focusedItemKey, setFocusedItemKey, - currentStickyGroup, + stickyGroup, handleItemClick, togglePin }), - [currentStickyGroup, focusedItemKey, handleItemClick, listItems, togglePin] + [stickyGroup, focusedItemKey, handleItemClick, listItems, togglePin] ) const listHeight = useMemo(() => { @@ -456,7 +472,7 @@ const PopupContainer: React.FC = ({ model, resolve }) => { {listItems.length > 0 ? ( setIsMouseOver(true)}> {/* Sticky Group Banner,它会替换第一个分组名称 */} - {currentStickyGroup?.name} + {stickyGroup?.name} = ({ model, resolve }) => { itemData={RowData} itemKey={(index, data) => data.listItems[index].key} overscanCount={4} - onItemsRendered={handleItemsRendered} + onScroll={handleScroll} style={{ pointerEvents: isMouseOver ? 'auto' : 'none' }}> {VirtualizedRow} @@ -484,7 +500,7 @@ interface VirtualizedRowData { listItems: FlatListItem[] focusedItemKey: string setFocusedItemKey: (key: string) => void - currentStickyGroup: FlatListItem | null + stickyGroup: FlatListItem | null handleItemClick: (item: FlatListItem) => void togglePin: (modelId: string) => void } @@ -494,7 +510,7 @@ interface VirtualizedRowData { */ const VirtualizedRow = React.memo( ({ data, index, style }: { data: VirtualizedRowData; index: number; style: React.CSSProperties }) => { - const { listItems, focusedItemKey, setFocusedItemKey, handleItemClick, togglePin, currentStickyGroup } = data + const { listItems, focusedItemKey, setFocusedItemKey, handleItemClick, togglePin, stickyGroup } = data const item = listItems[index] @@ -505,7 +521,7 @@ const VirtualizedRow = React.memo( return (
{item.type === 'group' ? ( - {item.name} + {item.name} ) : ( Date: Fri, 9 May 2025 20:04:44 +0800 Subject: [PATCH 3/6] =?UTF-8?q?Revert=20"feat:=20ParateraAI=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=94=AF=E6=8C=81=20(#5792)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e134cacbec7307cf9e5e1d5b83fb4833dfceb7cc. --- .../src/assets/images/apps/paratera.ico | Bin 1096 -> 0 bytes src/renderer/src/config/minapps.ts | 7 --- src/renderer/src/config/models.ts | 56 ------------------ src/renderer/src/config/providers.ts | 15 +---- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/llm.ts | 10 ---- src/renderer/src/store/migrate.ts | 9 --- 7 files changed, 2 insertions(+), 97 deletions(-) delete mode 100644 src/renderer/src/assets/images/apps/paratera.ico diff --git a/src/renderer/src/assets/images/apps/paratera.ico b/src/renderer/src/assets/images/apps/paratera.ico deleted file mode 100644 index ff3958618c65a159b7a2bcb0837a1e34c0da3db8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1096 zcmV-O1h@N%P)7o_jA`B+!eb5X1n5Sj&ds7+yjVE6QNahO0CyOLLLT(#>qjT3~Zc2IaKu z2h=jLt+fm&Y4l51E)2oM6_T^a5+XqmxZEG_J?*{(N?iQ!@{c@!p7%Y^`99z0Jm2R$ z0$w!8+=x7(Oxrv{BzuG~;71cEk5ry!rJN;NP3IvYbNAh^3G)FE7C)Zr%eCF?ajq1` z+_!&pBU2D!lFtXD@-zyOTd65O(>_sT-gF4^-c^$x`c(%`0hnUodSpNm8YsE!@ZeG4 zn*pUTOArbiYP>)R6xJXE{SFTfZTPzc!b409I^FcyH6N8N@QU5Ms2~y}EOZaL`KqO7 zA^=eO`0YGki=k+LGk^CEak#02^aLyM5h2WvpTVlEIb?46oYsy(rUyw@FN&pPbt)By zPqSiC0^%xVRVX&U)4&Hu+OZ{saPv$v7rI@1|7+g_08k`F22-#s5sO)H@>~zE)t;r` z-Z+xuthn8Znuhb#Hgytd4dk<;EaD?VDXuy})sc4MW=+FOu5MnL6N;bOd+8p~2%Bag z+8Usz;B4oRZvg2D)7i5=1(P)5*1U8MJM%_j5MY!>&ivWKL%SL<8sR}(B<1TC@Gal{{VTNdGm|#4ItDaS#n!AA8yHJP5C!8ox3!e(-pQDHZ7k= za^WYmb=b*WFq`rx(=eMP>CaX3NOl5J3O(|<5y7lllECY=Eo^wJ5#^)PkH3UU5gTq{ zS!yJ&uD7vtYqd_@h!y7E!os8*xodkpe{>IVN74-TZ(7Xxe+I~SzHZDR1vzur`B*CR zH&)ZuG2pZ1_yka~A&q-dq6xMbY5S{pOzW6?Qz-p|E=oT>#ja0Vxgk)}Sau&rTRM1R z&ky=dAI_Iz?^CJdq(qalv6>#AmEo%lmC`pK+?;`wkpIeeqW~m|3-9Mse6X3_wQYcb z*jW}GKylSc%4%9i?~AuozbwwiBqfQ8m)>x+}ZsNa#po!jg9`g9k`F(E|F3?e8%Qnogonugz5xA*uJX8xCm zaR4$BtyDgn$)(G7*6uz=b9>+DmaJRDC|$FFKk&nc>0ZF9Q~zd!Y9W#044(jnhiLd9-Nw2E3YV_SM5wv;cyE^1kz#@dbAh& zUHZSlCIbe8Kq-YwQw({1T*I!%W;B+LDF&t(5F)QqQ}}ltoTv)*!9*X;gfZos#p;ib z3XEtc^x@Ql`G69WJe(?JH!IglF*=?jw`oEq`#T*YGhyDe)qM$d7UI!om O0000 = { name: 'Qwen2.5 72B Instruct', group: 'Qwen' } - ], - paratera: [ - { - id: 'GLM-Z1-Flash-P002', - provider: 'paratera', - name: 'GLM-Z1-Flash-P002', - group: 'GLM' - }, - { - id: 'GLM-Z1-AirX-P002', - provider: 'paratera', - name: 'GLM-Z1-AirX-P002', - group: 'GLM' - }, - { - id: 'DeepSeek-V3-250324-P001', - provider: 'paratera', - name: 'DeepSeek-V3-250324-P001', - group: 'DeepSeek' - }, - { - id: 'DeepSeek-R1', - provider: 'paratera', - name: 'DeepSeek-R1', - group: 'DeepSeek' - }, - { - id: 'QwQ-N011-32B', - provider: 'paratera', - name: 'QwQ-N011-32B', - group: 'Qwen' - }, - { - id: 'GLM-Embedding-2-P002', - provider: 'paratera', - name: 'GLM-Embedding-2-P002', - group: 'GLM' - }, - { - id: 'GLM-Embedding-3-P002', - provider: 'paratera', - name: 'GLM-Embedding-3-P002', - group: 'GLM' - }, - { - id: 'Doubao-Embedding-Text-P001', - provider: 'paratera', - name: 'Doubao-Embedding-Text-P001', - group: 'Doubao' - }, - { - id: 'Doubao-Embedding-Large-Text-P001', - provider: 'paratera', - name: 'Doubao-Embedding-Large-Text-P001', - group: 'Doubao' - } ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 48ca7c9cc7..81407a8ae2 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -42,7 +42,6 @@ import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png' import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png' import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png' -import ParateraLogo from '@renderer/assets/images/apps/paratera.ico' const PROVIDER_LOGO_MAP = { openai: OpenAiProviderLogo, @@ -89,8 +88,7 @@ const PROVIDER_LOGO_MAP = { gpustack: GPUStackProviderLogo, alayanew: AlayaNewProviderLogo, voyageai: VoyageAIProviderLogo, - qiniu: QiniuProviderLogo, - paratera: ParateraLogo + qiniu: QiniuProviderLogo } as const export function getProviderLogo(providerId: string) { @@ -585,16 +583,5 @@ export const PROVIDER_CONFIG = { docs: 'https://developer.qiniu.com/aitokenapi', models: 'https://developer.qiniu.com/aitokenapi/12883/model-list' } - }, - paratera: { - api: { - url: 'https://llmapi.paratera.com' - }, - websites: { - official: 'https://ai.paratera.com/', - apiKey: 'https://ai.paratera.com/#/lms/api', - docs: 'https://ai.paratera.com/document/llm/quickStart/useApi', - models: 'https://ai.paratera.com/#/lms/model' - } } } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index df7af60f69..e8f4c2ac32 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -46,7 +46,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 99, + version: 98, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index 293a8db50f..27a68b342b 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -476,16 +476,6 @@ export const INITIAL_PROVIDERS: Provider[] = [ models: SYSTEM_MODELS.voyageai, isSystem: true, enabled: false - }, - { - id: 'paratera', - name: 'Paratera AI', - type: 'openai-compatible', - apiKey: '', - apiHost: 'https://llmapi.paratera.com', - models: SYSTEM_MODELS.paratera, - isSystem: true, - enabled: false } ] diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 69abd39eb9..f09ce39536 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1248,15 +1248,6 @@ const migrateConfig = { provider.type = 'openai-compatible' } }) - return state - } catch (error) { - return state - } - }, - '99': (state: RootState) => { - try { - addProvider(state, 'paratera') - return state } catch (error) { return state From c0cb1693daa9bf7a0dde75c40b7dcff96dd1a01c Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 9 May 2025 20:15:43 +0800 Subject: [PATCH 4/6] feat: replace n8n icon with SVG version and update references * removed the old n8n.ico file * added new n8n.svg file * updated references in minapps configuration and app components to use the new SVG logo * changed file handling from 'customMiniAPP' to 'custom-minapps.json' for consistency --- src/renderer/src/assets/images/apps/n8n.ico | Bin 15086 -> 0 bytes src/renderer/src/assets/images/apps/n8n.svg | 1 + src/renderer/src/config/minapps.ts | 13 +- src/renderer/src/pages/apps/App.tsx | 8 +- .../MiniappSettings/MiniAppSettings.tsx | 119 +----------------- 5 files changed, 15 insertions(+), 126 deletions(-) delete mode 100644 src/renderer/src/assets/images/apps/n8n.ico create mode 100644 src/renderer/src/assets/images/apps/n8n.svg diff --git a/src/renderer/src/assets/images/apps/n8n.ico b/src/renderer/src/assets/images/apps/n8n.ico deleted file mode 100644 index 4df30bfedac76030e187377589832346b680bf46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeHOX>3(R6uuSr4UCE#N=1o#L@^o_X;DPpz3-tU7MB=hQA7}W-zx}448CwXhEp+B|cbw}T$0;l{`A&|r9=uMVEFW>S<4l3TrI0}tXD|}^>UXgw z@!!}3u?J!g#2$z}5PKl@Ku9`lF|6W46uEjPKp4OeMJJZ8^z#zVw%G5iN#TVdp-|OZ9WlW^aq10+E0K# zxH^i>_kyvMLpxSS`R)0f@x%;`J=jD4`fhk1@|(zm|6jJ)R6AX8YntHSDqzLl|lCLemfg`QVH*D36t zs4I1=Z?LjW(emOA$73#g*VL_lzZq97J=9!={`)v&cjd@$Ds@HgTw9rhxW>wjbH&-V zyu4f<1z){s%OZ4N#tk2}a?PO_|F1^sR}eSyb&7}gSzas=9ne2NDeeYejXlt6`VMrb zlJ&$6u0>Xs6d0eZ=W+>*az=#I%MXili_n^iJHr8S#v1;i@FLc-oZ1r&-jTg zw}QWqw&@SGMsl6>#82cQ&9i+bb7#%@!0@}_&(iB@_6lJ0!PE0$QcP_Qo_uBeRB)HE z`&ZP@)C2x!rmVhC*}R_&Zj9Pe<3_jZAX&6+)!2U3Cq?C3>KQ&7U@+B=POLpHsuz9Z z3yU|G=Js`9oR}*U$*-N@Z9grlyQQAtm(H-W*VvkA-+}4p`^o3JNc&7?Y^XWIVq%{) zYm#JNCf~m!JO^|8gQ&0e2=K+UmcpE?2k+o{gP-p{MEWg~U(C6}*6|Y}figX|Ic}5U z7v$AA0TtM{bg{CenWS2Csdsaf-`KMnj~FZF-Oo#VVOdjsk3NU~nMlgtpt=uZk1}fD zrqgq6VVpmJFM@zZl*ixnj1)8Pr7AImo9Bewpvu&{FNwdx&s>#nA-) zprKuUpK)Epd%@b-u&nKJYQNx$!5Hr!0?YD4q$*-WT4C2vqT@UUo*I{2Q`-)4ewNJl zm>RQK3upNKvxVqqJ%If?)2@*FwEVQTt#r;lEIR)jK-v9miza_-B_*;0=BNqqx0;9O zcg#iWIM3y;t^I0ntc*(p7jZssKNPX$*aNW#Vh_X~h&>Q{AojqK(zBs zdb72NW}$hNJF?nNfu?na%GxE0FKe~DJkV9=slT=T!h2eUNX1C?_q1@AEB?hEh&>Q{ zptU`~m@Z;TL(*&+2<+U_LtoD1xmG!wu@H+gx zhi3_l)xtKd}a(b{2ub#VdDin>sT)QM!A2^u);5OIpxH1tm5kY*_;E)==lQAx`1;K z$`;@jvki%l^#| z7$4xj2OGh|8NTubV!uHp`7GDxrRYzdSO^)$PZ9qO7g&E#CcmF7FuHb{ z80RABWSja^52?!I&W0)H8O$PUgSe3g{w=)1e$Y?NMV=VOGtkDy!01HwOfzmbqb+N4 zi1Nb~^!QBPu>}(D3nfQ2T!163*-(-wa;nQOizH0fo2>QPvHk>~WhW@_w$;?G% zDbdULVJ>7_d@dTBs=g31~db}nj-HG$)UV~KOHO?W4-!56rf5_+67 z;G@rhaVLFYF|{V;`J@@6)*n1eeyBBV=IZA86z64*Tdoti<#9gtKn8n \ No newline at end of file diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index 6a2f01df40..42234b2400 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -1,8 +1,7 @@ -import n8nLogo from '@renderer/assets/images/apps/n8n.ico?url' -import ApplicationLogo from '@renderer/assets/images/apps/application.png?url' import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url' import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url' import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url' +import ApplicationLogo from '@renderer/assets/images/apps/application.png?url' import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url' import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url' import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url' @@ -28,6 +27,7 @@ import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url' import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url' import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url' import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url' +import n8nLogo from '@renderer/assets/images/apps/n8n.svg?url' import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url' import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url' import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url' @@ -61,11 +61,11 @@ const loadCustomMiniApp = async (): Promise => { try { let content: string try { - content = await window.api.file.read('customMiniAPP') + content = await window.api.file.read('custom-minapps.json') } catch (error) { // 如果文件不存在,创建一个空的 JSON 数组 content = '[]' - await window.api.file.writeWithId('customMiniAPP', content) + await window.api.file.writeWithId('custom-minapps.json', content) } const customApps = JSON.parse(content) @@ -455,7 +455,10 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [ name: 'n8n', logo: n8nLogo, url: 'https://app.n8n.cloud/', - bodered: true + bodered: true, + style: { + padding: 5 + } } ] diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx index cbf6ec75c6..c787a6ab1c 100644 --- a/src/renderer/src/pages/apps/App.tsx +++ b/src/renderer/src/pages/apps/App.tsx @@ -40,7 +40,7 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { const handleAddCustomApp = async (values: any) => { try { - const content = await window.api.file.read('customMiniAPP') + const content = await window.api.file.read('custom-minapps.json') const customApps = JSON.parse(content) // Check for duplicate ID @@ -62,7 +62,7 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { addTime: new Date().toISOString() } customApps.push(newApp) - await window.api.file.writeWithId('customMiniAPP', JSON.stringify(customApps, null, 2)) + await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2)) message.success(t('settings.miniapps.custom.save_success')) setIsModalVisible(false) form.resetFields() @@ -138,10 +138,10 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { danger: true, onClick: async () => { try { - const content = await window.api.file.read('customMiniAPP') + const content = await window.api.file.read('custom-minapps.json') const customApps = JSON.parse(content) const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id) - await window.api.file.writeWithId('customMiniAPP', JSON.stringify(updatedApps, null, 2)) + await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2)) message.success(t('settings.miniapps.custom.remove_success')) const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] updateDefaultMinApps(reloadedApps) diff --git a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx index 6dd3d52963..134b9e52bd 100644 --- a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx +++ b/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx @@ -1,10 +1,5 @@ import { UndoOutlined } from '@ant-design/icons' // 导入重置图标 -import { - DEFAULT_MIN_APPS, - loadCustomMiniApp, - ORIGIN_DEFAULT_MIN_APPS, - updateDefaultMinApps -} from '@renderer/config/minapps' +import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { useTheme } from '@renderer/context/ThemeProvider' import { useMinapps } from '@renderer/hooks/useMinapps' import { useSettings } from '@renderer/hooks/useSettings' @@ -14,7 +9,7 @@ import { setMinappsOpenLinkExternal, setShowOpenedMinappsInSidebar } from '@renderer/store/settings' -import { Button, Input, message, Slider, Switch, Tooltip } from 'antd' +import { Button, message, Slider, Switch, Tooltip } from 'antd' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -36,92 +31,6 @@ const MiniAppSettings: FC = () => { const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || []) const [messageApi, contextHolder] = message.useMessage() const debounceTimerRef = useRef(null) - const [customMiniAppContent, setCustomMiniAppContent] = useState('[]') - - // 加载自定义小应用配置 - useEffect(() => { - const loadCustomMiniApp = async () => { - try { - const content = await window.api.file.read('customMiniAPP') - let validContent = '[]' - try { - const parsed = JSON.parse(content) - validContent = JSON.stringify(parsed) - } catch (e) { - console.error('Invalid JSON format in custom mini app config:', e) - } - setCustomMiniAppContent(validContent) - } catch (error) { - console.error('Failed to load custom mini app config:', error) - setCustomMiniAppContent('[]') - } - } - loadCustomMiniApp() - }, []) - - // 保存自定义小应用配置 - const handleSaveCustomMiniApp = useCallback(async () => { - try { - // 验证 JSON 格式 - if (customMiniAppContent === '') { - setCustomMiniAppContent('[]') - } - const parsedContent = JSON.parse(customMiniAppContent) - // 确保是数组 - if (!Array.isArray(parsedContent)) { - throw new Error('Content must be an array') - } - - // 检查自定义应用中的重复ID - const customIds = new Set() - const duplicateIds = new Set() - parsedContent.forEach((app: any) => { - if (app.id) { - if (customIds.has(app.id)) { - duplicateIds.add(app.id) - } - customIds.add(app.id) - } - }) - - // 检查与默认应用的ID重复 - const defaultIds = new Set(ORIGIN_DEFAULT_MIN_APPS.map((app) => app.id)) - const conflictingIds = new Set() - customIds.forEach((id) => { - if (defaultIds.has(id)) { - conflictingIds.add(id) - } - }) - - // 如果有重复ID,显示错误信息 - if (duplicateIds.size > 0 || conflictingIds.size > 0) { - let errorMessage = '' - if (duplicateIds.size > 0) { - errorMessage += t('settings.miniapps.custom.duplicate_ids', { ids: Array.from(duplicateIds).join(', ') }) - } - if (conflictingIds.size > 0) { - console.log('conflictingIds', Array.from(conflictingIds)) - if (errorMessage) errorMessage += '\n' - errorMessage += t('settings.miniapps.custom.conflicting_ids', { ids: Array.from(conflictingIds).join(', ') }) - } - messageApi.error(errorMessage) - return - } - - // 保存文件 - await window.api.file.writeWithId('customMiniAPP', customMiniAppContent) - messageApi.success(t('settings.miniapps.custom.save_success')) - // 重新加载应用列表 - console.log('Reloading mini app list...') - const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] - updateDefaultMinApps(reloadedApps) - console.log('Reloaded mini app list:', reloadedApps) - updateMinapps(reloadedApps) - } catch (error) { - messageApi.error(t('settings.miniapps.custom.save_error')) - console.error('Failed to save custom mini app config:', error) - } - }, [customMiniAppContent, messageApi, t, updateMinapps]) const handleResetMinApps = useCallback(() => { setVisibleMiniApps(DEFAULT_MIN_APPS) @@ -235,30 +144,6 @@ const MiniAppSettings: FC = () => { onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))} /> - - - - {t('settings.miniapps.custom.edit_title')} - {t('settings.miniapps.custom.edit_description')} - - - - setCustomMiniAppContent(e.target.value)} - placeholder={t('settings.miniapps.custom.placeholder')} - style={{ - minHeight: 200, - fontFamily: 'monospace', - backgroundColor: 'var(--color-bg-2)', - color: 'var(--color-text)', - borderColor: 'var(--color-border)' - }} - /> - - ) From ce8b85020b9327f5d421e11bde2241b4bf2420ab Mon Sep 17 00:00:00 2001 From: Camol Date: Fri, 9 May 2025 20:20:16 +0800 Subject: [PATCH 5/6] feat: support both function call and system prompt for MCP tools (#5499) * feat: support both function call and system prompt for MCP tools - Add support for using both function call and system prompt to implement MCP tool calls - Refactor tool handling logic to be more flexible and maintainable - Improve code readability with better variable naming and comments - Fix potential issues with tool call implementation * fix: Add tool_calls in OpenAI streaming logic * refactor: enhance OpenAICompatibleProvider and BaseOpenAiProvider structure * feat: add tool call setting to SettingsTab component * fix: enhance tool call handling in OpenAICompatibleProvider * fix: enhance content handling in GeminiProvider for nonstreaming response * refactor: improve tool property filtering logic in OpenAIProvider and mcp-tools utility * fix: resolve eslint errors * fix: add history for function call message in GeminiProvider * refactor: unify MCP tool response handling across providers for consistency * refactor: update mcp tools conversion logic in OpenAICompatibleProvider and OpenAIProvider * refactor: enhance AihubmixProvider and BaseProvider with MCP tool handling methods * refactor: introduce SYSTEM_PROMPT_THRESHOLD constant in BaseProvider for improved readability * refactor: rename tool_call to enable_tool_use for clarity and consistency across the application * refactor: remove unnecessary onChunk call in processStream for cleaner code * fix: add toolCallId to response structure and enhance content handling in AnthropicProvider * fix: respond image data to llm while using function call * fix: add reasoning handling in OpenAICompatibleProvider for improved response processing --------- Co-authored-by: kanweiwei Co-authored-by: jay --- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + .../src/pages/home/Messages/MessageTools.tsx | 2 +- .../src/pages/home/Tabs/SettingsTab.tsx | 13 + .../AssistantModelSettings.tsx | 13 + .../providers/AiProvider/AihubmixProvider.ts | 15 +- .../providers/AiProvider/AnthropicProvider.ts | 154 +++++-- .../src/providers/AiProvider/BaseProvider.ts | 42 ++ .../providers/AiProvider/GeminiProvider.ts | 397 ++++++++++++------ .../AiProvider/OpenAICompatibleProvider.ts | 262 +++++++++--- .../providers/AiProvider/OpenAIProvider.ts | 260 ++++++++++-- src/renderer/src/services/AssistantService.ts | 1 + src/renderer/src/store/thunk/messageThunk.ts | 12 +- src/renderer/src/types/index.ts | 20 +- src/renderer/src/utils/mcp-tools.ts | 383 ++++++++++------- src/renderer/src/utils/prompt.ts | 2 +- 19 files changed, 1178 insertions(+), 403 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index dae4ed6ca5..3d8ab74b96 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -705,6 +705,7 @@ "rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add.", "search": "Search models...", "stream_output": "Stream output", + "enable_tool_use": "Enable Tool Use", "type": { "embedding": "Embedding", "free": "Free", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 046d2ff589..f1b12358d8 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -705,6 +705,7 @@ "rerank_model_tooltip": "設定->モデルサービスに移動し、管理ボタンをクリックして追加します。", "search": "モデルを検索...", "stream_output": "ストリーム出力", + "enable_tool_use": "ツール呼び出し", "type": { "embedding": "埋め込み", "free": "無料", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 41242b74c3..2692e1c270 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -705,6 +705,7 @@ "rerank_model_tooltip": "В настройках -> Служба модели нажмите кнопку \"Управление\", чтобы добавить.", "search": "Поиск моделей...", "stream_output": "Потоковый вывод", + "enable_tool_use": "Вызов инструмента", "type": { "embedding": "Встраиваемые", "free": "Бесплатные", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a57c106b49..e68409a642 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -705,6 +705,7 @@ "rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加", "search": "搜索模型...", "stream_output": "流式输出", + "enable_tool_use": "工具调用", "type": { "embedding": "嵌入", "free": "免费", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index d91d8fb257..893a7b75a1 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -705,6 +705,7 @@ "rerank_model_tooltip": "在設定->模型服務中點擊管理按鈕添加", "search": "搜尋模型...", "stream_output": "串流輸出", + "enable_tool_use": "工具調用", "type": { "embedding": "嵌入", "free": "免費", diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index 495c56cf10..b281e40642 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -67,7 +67,7 @@ const MessageTools: FC = ({ blocks }) => { const isDone = status === 'done' const hasError = isDone && response?.isError === true const result = { - params: tool.inputSchema, + params: toolResponse.arguments, response: toolResponse.response } diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 9a21b1c808..0c8972db6d 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -70,6 +70,7 @@ const SettingsTab: FC = (props) => { const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [fontSizeValue, setFontSizeValue] = useState(fontSize) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) + const [enableToolUse, setEnableToolUse] = useState(assistant?.settings?.enableToolUse ?? false) const { t } = useTranslation() const dispatch = useAppDispatch() @@ -222,6 +223,18 @@ const SettingsTab: FC = (props) => { /> + + {t('models.enable_tool_use')} + { + setEnableToolUse(checked) + updateAssistantSettings({ enableToolUse: checked }) + }} + /> + + diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx index 31075ebbb7..9ea4559b47 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx @@ -24,6 +24,7 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) + const [enableToolUse, setEnableToolUse] = useState(assistant?.settings?.enableToolUse ?? false) const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel) const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1) const [customParameters, setCustomParameters] = useState( @@ -377,6 +378,18 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA /> + + + { + setEnableToolUse(checked) + updateAssistantSettings({ enableToolUse: checked }) + }} + /> + +