From e93372de48a275d146d3a8746458dea1ba38e9e5 Mon Sep 17 00:00:00 2001 From: yessenia Date: Fri, 29 Aug 2025 20:30:20 +0800 Subject: [PATCH] feat: show tool readme info --- web/app/(commonLayout)/layout.tsx | 13 +- .../agent-tools/setting-built-in-tool.tsx | 15 ++- web/app/components/base/drawer/index.tsx | 10 +- .../components/base/markdown-blocks/img.tsx | 29 ++++- .../base/markdown-blocks/paragraph.tsx | 34 +++-- .../components/base/markdown-blocks/utils.ts | 9 +- web/app/components/base/markdown/index.tsx | 7 +- web/app/components/base/modal/index.tsx | 10 +- web/app/components/base/modal/modal.tsx | 4 +- .../plugin-auth/authorize/api-key-modal.tsx | 3 + .../authorize/oauth-client-settings.tsx | 20 +-- .../components/plugins/plugin-auth/types.ts | 3 + .../plugin-detail-panel/detail-header.tsx | 11 +- .../plugin-detail-panel/endpoint-card.tsx | 5 +- .../plugin-detail-panel/endpoint-list.tsx | 2 + .../plugin-detail-panel/endpoint-modal.tsx | 9 +- .../plugins/plugin-detail-panel/index.tsx | 26 ++-- .../tool-selector/index.tsx | 7 +- .../plugins/plugin-title-info/index.tsx | 119 +++++++++++++++++ .../plugins/readme-panel/constants.ts | 6 + .../plugins/readme-panel/context.tsx | 63 +++++++++ .../plugins/readme-panel/entrance.tsx | 48 +++++++ .../components/plugins/readme-panel/index.tsx | 123 ++++++++++++++++++ .../_base/components/workflow-panel/index.tsx | 17 ++- web/i18n/en-US/plugin.ts | 5 + web/i18n/zh-Hans/plugin.ts | 5 + web/service/use-plugins.ts | 16 +++ 27 files changed, 550 insertions(+), 69 deletions(-) create mode 100644 web/app/components/plugins/plugin-title-info/index.tsx create mode 100644 web/app/components/plugins/readme-panel/constants.ts create mode 100644 web/app/components/plugins/readme-panel/context.tsx create mode 100644 web/app/components/plugins/readme-panel/entrance.tsx create mode 100644 web/app/components/plugins/readme-panel/index.tsx diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index ed1c995e25..9119ad2304 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -8,6 +8,7 @@ import Header from '@/app/components/header' import { EventEmitterContextProvider } from '@/context/event-emitter' import { ProviderContextProvider } from '@/context/provider-context' import { ModalContextProvider } from '@/context/modal-context' +import { ReadmePanelProvider } from '@/app/components/plugins/readme-panel/context' import GotoAnything from '@/app/components/goto-anything' const Layout = ({ children }: { children: ReactNode }) => { @@ -19,11 +20,13 @@ const Layout = ({ children }: { children: ReactNode }) => { - -
- - {children} - + + +
+ + {children} + + diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index dad5441a54..bd6e7c6417 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -28,6 +28,7 @@ import { AuthCategory, PluginAuthInAgent, } from '@/app/components/plugins/plugin-auth' +import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' type Props = { showBackButton?: boolean @@ -212,6 +213,7 @@ const SettingBuiltInTool: FC = ({ pluginPayload={{ provider: collection.name, category: AuthCategory.tool, + detail: collection as any, }} credentialId={credentialId} onAuthorizationItemClick={onAuthorizationItemClick} @@ -241,13 +243,14 @@ const SettingBuiltInTool: FC = ({ )}
{isInfoActive ? infoUI : settingUI} + {!readonly && !isInfoActive && ( +
+ + +
+ )}
- {!readonly && !isInfoActive && ( -
- - -
- )} + diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx index 8217caae97..960bb69719 100644 --- a/web/app/components/base/drawer/index.tsx +++ b/web/app/components/base/drawer/index.tsx @@ -10,6 +10,7 @@ export type IDrawerProps = { description?: string dialogClassName?: string dialogBackdropClassName?: string + containerClassName?: string panelClassName?: string children: React.ReactNode footer?: React.ReactNode @@ -22,6 +23,7 @@ export type IDrawerProps = { onCancel?: () => void onOk?: () => void unmount?: boolean + noOverlay?: boolean } export default function Drawer({ @@ -29,6 +31,7 @@ export default function Drawer({ description = '', dialogClassName = '', dialogBackdropClassName = '', + containerClassName = '', panelClassName = '', children, footer, @@ -41,6 +44,7 @@ export default function Drawer({ onCancel, onOk, unmount = false, + noOverlay = false, }: IDrawerProps) { const { t } = useTranslation() return ( @@ -50,14 +54,14 @@ export default function Drawer({ onClose={() => !clickOutsideNotOpen && onClose()} className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)} > -
+
{/* mask */} - { !clickOutsideNotOpen && onClose() }} - /> + />}
<>
diff --git a/web/app/components/base/markdown-blocks/img.tsx b/web/app/components/base/markdown-blocks/img.tsx index 33fce13f0b..ec5a05a601 100644 --- a/web/app/components/base/markdown-blocks/img.tsx +++ b/web/app/components/base/markdown-blocks/img.tsx @@ -3,11 +3,34 @@ * Extracted from the main markdown renderer for modularity. * Uses the ImageGallery component to display images. */ -import React from 'react' +import React, { useEffect, useMemo } from 'react' import ImageGallery from '@/app/components/base/image-gallery' +import { getMarkdownImageURL } from './utils' +import { usePluginReadmeAsset } from '@/service/use-plugins' -const Img = ({ src }: any) => { - return
+const Img = ({ src, pluginUniqueIdentifier }: { src: string, pluginUniqueIdentifier?: string }) => { + const imgURL = getMarkdownImageURL(src, pluginUniqueIdentifier) + const { data: asset } = usePluginReadmeAsset({ plugin_unique_identifier: pluginUniqueIdentifier, file_name: src }) + + const blobUrl = useMemo(() => { + if (asset) + return URL.createObjectURL(asset) + + return imgURL + }, [asset, imgURL]) + + useEffect(() => { + return () => { + if (blobUrl && asset) + URL.revokeObjectURL(blobUrl) + } + }, [blobUrl]) + + return ( +
+ +
+ ) } export default Img diff --git a/web/app/components/base/markdown-blocks/paragraph.tsx b/web/app/components/base/markdown-blocks/paragraph.tsx index fb1612477a..12287eb109 100644 --- a/web/app/components/base/markdown-blocks/paragraph.tsx +++ b/web/app/components/base/markdown-blocks/paragraph.tsx @@ -3,25 +3,43 @@ * Extracted from the main markdown renderer for modularity. * Handles special rendering for paragraphs that directly contain an image. */ -import React from 'react' +import React, { useEffect, useMemo } from 'react' import ImageGallery from '@/app/components/base/image-gallery' +// import { getMarkdownImageURL } from './utils' +import { usePluginReadmeAsset } from '@/service/use-plugins' -const Paragraph = (paragraph: any) => { - const { node }: any = paragraph +const Paragraph = (props: { pluginUniqueIdentifier?: string, node?: any, children?: any }) => { + const { node, pluginUniqueIdentifier, children } = props const children_node = node.children - if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { + const { data: asset } = usePluginReadmeAsset({ plugin_unique_identifier: pluginUniqueIdentifier, file_name: children_node[0].properties?.src }) + const blobUrl = useMemo(() => { + if (asset) + return URL.createObjectURL(asset) + + return '' + }, [asset]) + + useEffect(() => { + return () => { + if (blobUrl && asset) + URL.revokeObjectURL(blobUrl) + } + }, [blobUrl]) + + if (children_node?.[0]?.tagName === 'img') { + // const imageURL = getMarkdownImageURL(children_node[0].properties?.src, pluginUniqueIdentifier) return (
- + { - Array.isArray(paragraph.children) && paragraph.children.length > 1 && ( -
{paragraph.children.slice(1)}
+ Array.isArray(children) && children.length > 1 && ( +
{children.slice(1)}
) }
) } - return

{paragraph.children}

+ return

{children}

} export default Paragraph diff --git a/web/app/components/base/markdown-blocks/utils.ts b/web/app/components/base/markdown-blocks/utils.ts index d8df76aefc..50433b9eaa 100644 --- a/web/app/components/base/markdown-blocks/utils.ts +++ b/web/app/components/base/markdown-blocks/utils.ts @@ -1,7 +1,14 @@ -import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config' +import { ALLOW_UNSAFE_DATA_SCHEME, MARKETPLACE_API_PREFIX } from '@/config' export const isValidUrl = (url: string): boolean => { const validPrefixes = ['http:', 'https:', '//', 'mailto:'] if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:') return validPrefixes.some(prefix => url.startsWith(prefix)) } + +export const getMarkdownImageURL = (url: string, pathname?: string) => { + const regex = /(^\.\/_assets|^_assets)/ + if (regex.test(url)) + return `${MARKETPLACE_API_PREFIX}${pathname ?? ''}${url.replace(regex, '/_assets')}` + return url +} diff --git a/web/app/components/base/markdown/index.tsx b/web/app/components/base/markdown/index.tsx index bab5ac8eba..7371b2f391 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -33,10 +33,11 @@ export type MarkdownProps = { className?: string customDisallowedElements?: string[] customComponents?: Record> + pluginUniqueIdentifier?: string } export const Markdown = (props: MarkdownProps) => { - const { customComponents = {} } = props + const { customComponents = {}, pluginUniqueIdentifier } = props const latexContent = flow([ preprocessThinkTag, preprocessLaTeX, @@ -76,11 +77,11 @@ export const Markdown = (props: MarkdownProps) => { disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} components={{ code: CodeBlock, - img: Img, + img: props => , video: VideoBlock, audio: AudioBlock, a: Link, - p: Paragraph, + p: props => , button: MarkdownButton, form: MarkdownForm, script: ScriptBlock as any, diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index 426953261e..814589172e 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -8,6 +8,7 @@ import { noop } from 'lodash-es' type IModal = { className?: string wrapperClassName?: string + containerClassName?: string isShow: boolean onClose?: () => void title?: React.ReactNode @@ -16,11 +17,13 @@ type IModal = { closable?: boolean overflowVisible?: boolean highPriority?: boolean // For modals that need to appear above dropdowns + noOverlay?: boolean } export default function Modal({ className, wrapperClassName, + containerClassName, isShow, onClose = noop, title, @@ -29,18 +32,19 @@ export default function Modal({ closable = false, overflowVisible = false, highPriority = false, + noOverlay = false, }: IModal) { return ( - + {!noOverlay &&
- + }
-
+
{ const { t } = useTranslation() @@ -79,7 +81,7 @@ const Modal = ({
{ children && ( -
{children}
+
{children}
) }
diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index 21946c4b51..a5340886ea 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -20,6 +20,8 @@ import { useGetPluginCredentialSchemaHook, useUpdatePluginCredentialHook, } from '../hooks/use-credential' +import { ReadmeEntrance } from '../../readme-panel/entrance' +import { ReadmeShowType } from '../../readme-panel/context' export type ApiKeyModalProps = { pluginPayload: PluginPayload @@ -142,6 +144,7 @@ const ApiKeyModal = ({ onExtraButtonClick={onRemove} disabled={disabled || isLoading || doingAction} > + { isLoading && (
diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx index c10b06166b..dff210a696 100644 --- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -23,6 +23,8 @@ import type { } from '@/app/components/base/form/types' import { useToastContext } from '@/app/components/base/toast' import Button from '@/app/components/base/button' +import { ReadmeEntrance } from '../../readme-panel/entrance' +import { ReadmeShowType } from '../../readme-panel/context' type OAuthClientSettingsProps = { pluginPayload: PluginPayload @@ -154,16 +156,16 @@ const OAuthClientSettings = ({
) } + containerClassName='pt-0' > - <> - - + + ) } diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts index ad41733bde..347f63dc0f 100644 --- a/web/app/components/plugins/plugin-auth/types.ts +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -1,3 +1,5 @@ +import type { PluginDetail } from '../types' + export enum AuthCategory { tool = 'tool', datasource = 'datasource', @@ -7,6 +9,7 @@ export enum AuthCategory { export type PluginPayload = { category: AuthCategory provider: string + detail: PluginDetail } export enum CredentialTypeEnum { diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 3f03a632ef..c8a0832f2e 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -61,7 +61,7 @@ const DetailHeader = ({ onUpdate, }: Props) => { const { t } = useTranslation() - const { userProfile: { timezone } } = useAppContext() + const { userProfile: { timezone } } = useAppContext() const { theme } = useTheme() const locale = useGetLanguage() @@ -128,13 +128,13 @@ const DetailHeader = ({ return false if (!autoUpgradeInfo || !isFromMarketplace) return false - if(autoUpgradeInfo.strategy_setting === 'disabled') + if (autoUpgradeInfo.strategy_setting === 'disabled') return false - if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all) + if (autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all) return true - if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id)) + if (autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id)) return true - if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id)) + if (autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id)) return true return false }, [autoUpgradeInfo, plugin_id, isFromMarketplace]) @@ -331,6 +331,7 @@ const DetailHeader = ({ pluginPayload={{ provider: provider?.name || '', category: AuthCategory.tool, + detail, }} /> ) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index 00cd1b88ae..911026e321 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react' -import type { EndpointListItem } from '../types' +import type { EndpointListItem, PluginDetail } from '../types' import EndpointModal from './endpoint-modal' import { NAME_FIELD } from './utils' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' @@ -22,11 +22,13 @@ import { } from '@/service/use-endpoints' type Props = { + pluginDetail: PluginDetail data: EndpointListItem handleChange: () => void } const EndpointCard = ({ + pluginDetail, data, handleChange, }: Props) => { @@ -210,6 +212,7 @@ const EndpointCard = ({ defaultValues={formValue} onCancel={hideEndpointModalConfirm} onSaved={handleUpdate} + pluginDetail={pluginDetail} /> )}
diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 5735022c5d..8e37bc9913 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -102,6 +102,7 @@ const EndpointList = ({ detail }: Props) => { key={index} data={item} handleChange={() => invalidateEndpointList(detail.plugin_id)} + pluginDetail={detail} /> ))}
@@ -110,6 +111,7 @@ const EndpointList = ({ detail }: Props) => { formSchemas={formSchemas} onCancel={hideEndpointModal} onSaved={handleCreate} + pluginDetail={detail} /> )}
diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index a715237a43..8fff1fed72 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -10,12 +10,15 @@ import Form from '@/app/components/header/account-setting/model-provider-page/mo import Toast from '@/app/components/base/toast' import { useRenderI18nObject } from '@/hooks/use-i18n' import cn from '@/utils/classnames' +import { ReadmeEntrance } from '../readme-panel/entrance' +import type { PluginDetail } from '../types' type Props = { formSchemas: any defaultValues?: any onCancel: () => void onSaved: (value: Record) => void + pluginDetail: PluginDetail } const extractDefaultValues = (schemas: any[]) => { @@ -32,6 +35,7 @@ const EndpointModal: FC = ({ defaultValues = {}, onCancel, onSaved, + pluginDetail, }) => { const getValueFromI18nObject = useRenderI18nObject() const { t } = useTranslation() @@ -55,9 +59,9 @@ const EndpointModal: FC = ({ const value = processedCredential[field.name] if (typeof value === 'string') processedCredential[field.name] = value === 'true' || value === '1' || value === 'True' - else if (typeof value === 'number') + else if (typeof value === 'number') processedCredential[field.name] = value === 1 - else if (typeof value === 'boolean') + else if (typeof value === 'boolean') processedCredential[field.name] = value } }) @@ -84,6 +88,7 @@ const EndpointModal: FC = ({
{t('plugin.detailPanel.endpointModalDesc')}
+
diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 3ec867faae..ac49908c2b 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -1,5 +1,5 @@ 'use client' -import React from 'react' +import React, { useCallback } from 'react' import type { FC } from 'react' import DetailHeader from './detail-header' import EndpointList from './endpoint-list' @@ -9,6 +9,7 @@ import AgentStrategyList from './agent-strategy-list' import Drawer from '@/app/components/base/drawer' import type { PluginDetail } from '@/app/components/plugins/types' import cn from '@/utils/classnames' +import { ReadmeEntrance } from '../readme-panel/entrance' type Props = { detail?: PluginDetail @@ -21,11 +22,11 @@ const PluginDetailPanel: FC = ({ onUpdate, onHide, }) => { - const handleUpdate = (isDelete = false) => { + const handleUpdate = useCallback((isDelete = false) => { if (isDelete) onHide() onUpdate() - } + }, [onHide, onUpdate]) if (!detail) return null @@ -42,16 +43,17 @@ const PluginDetailPanel: FC = ({ > {detail && ( <> - +
- {!!detail.declaration.tool && } - {!!detail.declaration.agent_strategy && } - {!!detail.declaration.endpoint && } - {!!detail.declaration.model && } +
+
+ {!!detail.declaration.tool && } + {!!detail.declaration.agent_strategy && } + {!!detail.declaration.endpoint && } + {!!detail.declaration.model && } +
+ +
)} diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index d2797b99f4..9ad40f67a7 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -40,6 +40,7 @@ import { AuthCategory, PluginAuthInAgent, } from '@/app/components/plugins/plugin-auth' +import { ReadmeEntrance } from '../../readme-panel/entrance' type Props = { disabled?: boolean @@ -272,7 +273,10 @@ const ToolSelector: FC = ({ {/* base form */}
-
{t('plugin.detailPanel.toolSelector.toolLabel')}
+
+ {t('plugin.detailPanel.toolSelector.toolLabel')} + +
= ({ pluginPayload={{ provider: currentProvider.name, category: AuthCategory.tool, + detail: currentProvider as any, }} credentialId={value?.credential_id} onAuthorizationItemClick={handleAuthorizationItemClick} diff --git a/web/app/components/plugins/plugin-title-info/index.tsx b/web/app/components/plugins/plugin-title-info/index.tsx new file mode 100644 index 0000000000..b69e206582 --- /dev/null +++ b/web/app/components/plugins/plugin-title-info/index.tsx @@ -0,0 +1,119 @@ +'use client' +import React from 'react' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiBugLine, + RiHardDrive3Line, + RiVerifiedBadgeLine, +} from '@remixicon/react' +import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' +import { Github } from '@/app/components/base/icons/src/public/common' +import Tooltip from '@/app/components/base/tooltip' +import Badge from '@/app/components/base/badge' +import { API_PREFIX } from '@/config' +import { useAppContext } from '@/context/app-context' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import type { PluginDetail } from '@/app/components/plugins/types' +import { PluginSource } from '@/app/components/plugins/types' +import OrgInfo from '@/app/components/plugins/card/base/org-info' +import Icon from '@/app/components/plugins/card/base/card-icon' + +type PluginInfoProps = { + detail: PluginDetail + size?: 'default' | 'large' +} + +const PluginInfo: FC = ({ + detail, + size = 'default', +}) => { + const { t } = useTranslation() + const { currentWorkspace } = useAppContext() + const locale = useLanguage() + + const tenant_id = currentWorkspace?.id + const { + version, + source, + } = detail + + const { + icon, + label, + author, + name, + verified, + } = detail.declaration || detail + + const isLarge = size === 'large' + const iconSize = isLarge ? 'h-10 w-10' : 'h-8 w-8' + const titleSize = isLarge ? 'text-sm' : 'text-xs' + + return ( +
+ {/* Plugin Icon */} +
+ +
+ + {/* Plugin Details */} +
+ {/* Name and Version */} +
+

+ {label[locale]} +

+ {verified && } + +
+ + {/* Organization and Source */} +
+ +
·
+ + {/* Source Icon */} + {source === PluginSource.marketplace && ( + +
+ +
+
+ )} + {source === PluginSource.github && ( + +
+ +
+
+ )} + {source === PluginSource.local && ( + +
+ +
+
+ )} + {source === PluginSource.debugging && ( + +
+ +
+
+ )} +
+
+
+ ) +} + +export default PluginInfo diff --git a/web/app/components/plugins/readme-panel/constants.ts b/web/app/components/plugins/readme-panel/constants.ts new file mode 100644 index 0000000000..7d6782e665 --- /dev/null +++ b/web/app/components/plugins/readme-panel/constants.ts @@ -0,0 +1,6 @@ +export const BUILTIN_TOOLS_ARRAY = [ + 'code', + 'audio', + 'time', + 'webscraper', +] diff --git a/web/app/components/plugins/readme-panel/context.tsx b/web/app/components/plugins/readme-panel/context.tsx new file mode 100644 index 0000000000..63d99ad26a --- /dev/null +++ b/web/app/components/plugins/readme-panel/context.tsx @@ -0,0 +1,63 @@ +'use client' +import React, { createContext, useContext, useState } from 'react' +import type { FC, ReactNode } from 'react' +import type { PluginDetail } from '@/app/components/plugins/types' +import ReadmeDrawer from './index' + +type ReadmePanelContextValue = { + openReadme: (detail: PluginDetail, showType?: ReadmeShowType) => void + closeReadme: () => void + currentDetailInfo?: { + detail: PluginDetail + showType: ReadmeShowType + } +} + +const ReadmePanelContext = createContext(null) + +export const useReadmePanel = (): ReadmePanelContextValue => { + const context = useContext(ReadmePanelContext) + if (!context) + throw new Error('useReadmePanel must be used within ReadmePanelProvider') + + return context +} + +type ReadmePanelProviderProps = { + children: ReactNode +} + +export enum ReadmeShowType { + drawer = 'drawer', + modal = 'modal', +} + +export const ReadmePanelProvider: FC = ({ children }) => { + const [currentDetailInfo, setCurrentDetailInfo] = useState<{ + detail: PluginDetail + showType: ReadmeShowType + } | undefined>() + + const openReadme = (detail: PluginDetail, showType?: ReadmeShowType) => { + setCurrentDetailInfo({ + detail, + showType: showType || ReadmeShowType.drawer, + }) + } + + const closeReadme = () => { + setCurrentDetailInfo(undefined) + } + + // todo: use zustand + return ( + + {children} + + + ) +} diff --git a/web/app/components/plugins/readme-panel/entrance.tsx b/web/app/components/plugins/readme-panel/entrance.tsx new file mode 100644 index 0000000000..37eec4a6cf --- /dev/null +++ b/web/app/components/plugins/readme-panel/entrance.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { RiBookReadLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import type { PluginDetail } from '../types' +import { ReadmeShowType, useReadmePanel } from './context' +import { BUILTIN_TOOLS_ARRAY } from './constants' + +export const ReadmeEntrance = ({ + detail, + showType = ReadmeShowType.drawer, + className, + showShortTip = false, +}: { + detail: PluginDetail + showType?: ReadmeShowType + className?: string + showShortTip?: boolean +}) => { + const { t } = useTranslation() + const { openReadme } = useReadmePanel() + const handleReadmeClick = () => { + if (detail) + openReadme(detail, showType) + } + if (BUILTIN_TOOLS_ARRAY.includes(detail.id)) + return null + + return ( +
+ {!showShortTip &&
+
+
} + + +
+ ) +} diff --git a/web/app/components/plugins/readme-panel/index.tsx b/web/app/components/plugins/readme-panel/index.tsx new file mode 100644 index 0000000000..396c9d0a2a --- /dev/null +++ b/web/app/components/plugins/readme-panel/index.tsx @@ -0,0 +1,123 @@ +'use client' +import React from 'react' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { RiBookReadLine, RiCloseLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import Drawer from '@/app/components/base/drawer' +import { Markdown } from '@/app/components/base/markdown' +import { usePluginReadme } from '@/service/use-plugins' +// import type { PluginDetail } from '@/app/components/plugins/types' +import Loading from '@/app/components/base/loading' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import PluginTitleInfo from '@/app/components/plugins/plugin-title-info' +import Modal from '@/app/components/base/modal' +import ActionButton from '@/app/components/base/action-button' +import { ReadmeShowType, useReadmePanel } from './context' + +const ReadmePanel: FC = () => { + const { currentDetailInfo, closeReadme: onClose } = useReadmePanel() + const detail = currentDetailInfo?.detail + const showType = currentDetailInfo?.showType + const { t } = useTranslation() + const language = useLanguage() + + const pluginUniqueIdentifier = detail?.plugin_unique_identifier || '' + + const readmeLanguage = language === 'zh-Hans' ? undefined : language + const { data: readmeData, isLoading, error } = usePluginReadme( + { plugin_unique_identifier: pluginUniqueIdentifier, language: readmeLanguage }, + ) + + if (!detail) return null + + const children = ( +
+
+
+
+ + + {t('plugin.readmeInfo.title')} + +
+ + + +
+ + +
+ +
+ {(() => { + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+

{t('plugin.readmeInfo.noReadmeAvailable')}

+
+ ) + } + + if (readmeData?.readme) { + return ( + + ) + } + + return ( +
+

{t('plugin.readmeInfo.noReadmeAvailable')}

+
+ ) + })()} +
+
+ ) + + return ( + showType === ReadmeShowType.drawer ? ( + + {children} + + ) : ( + + {children} + + ) + ) +} + +export default ReadmePanel diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 4723b2dce7..a5c623f8c3 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -65,6 +65,7 @@ import { } from '@/app/components/plugins/plugin-auth' import { AuthCategory } from '@/app/components/plugins/plugin-auth' import { canFindTool } from '@/utils' +import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' type BasePanelProps = { children: ReactNode @@ -169,11 +170,11 @@ const BasePanel: FC = ({ const [isPaused, setIsPaused] = useState(false) useEffect(() => { - if(data._singleRunningStatus === NodeRunningStatus.Running) { + if (data._singleRunningStatus === NodeRunningStatus.Running) { hasClickRunning.current = true setIsPaused(false) } - else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) { + else if (data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) { setIsPaused(true) hasClickRunning.current = false } @@ -248,9 +249,9 @@ const BasePanel: FC = ({ }) }, [handleNodeDataUpdateWithSyncDraft, id]) - if(logParams.showSpecialResultPanel) { + if (logParams.showSpecialResultPanel) { return ( -
= ({
{ - if(isSingleRunning) { + if (isSingleRunning) { handleNodeDataUpdate({ id, data: { @@ -356,7 +357,7 @@ const BasePanel: FC = ({ > { isSingleRunning ? - : + : }
@@ -387,6 +388,7 @@ const BasePanel: FC = ({ pluginPayload={{ provider: currCollection?.name || '', category: AuthCategory.tool, + detail: currCollection as any, }} >
@@ -398,6 +400,7 @@ const BasePanel: FC = ({ pluginPayload={{ provider: currCollection?.name || '', category: AuthCategory.tool, + detail: currCollection as any, }} onAuthorizationItemClick={handleAuthorizationItemClick} credentialId={data.credential_id} @@ -483,6 +486,8 @@ const BasePanel: FC = ({ {...passedLogParams} /> )} + + {data.type === BlockEnum.Tool && }
) diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index aa127eaf13..c64cd496a1 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -298,6 +298,11 @@ const translation = { clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use', oauthClient: 'OAuth Client', }, + readmeInfo: { + title: 'README', + needHelpCheckReadme: 'Need help? Check the README.', + noReadmeAvailable: 'No README available', + }, } export default translation diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index a080a26a8c..7c27a38d88 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -298,6 +298,11 @@ const translation = { clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用', oauthClient: 'OAuth 客户端', }, + readmeInfo: { + title: 'README', + needHelpCheckReadme: '需要帮助?查看 README。', + noReadmeAvailable: 'README 文档不可用', + }, } export default translation diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 2877ef15f2..76649d8127 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -626,3 +626,19 @@ export const useFetchDynamicOptions = (plugin_id: string, provider: string, acti }), }) } + +export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin_unique_identifier: string, language?: string }) => { + return useQuery({ + queryKey: ['pluginReadme', plugin_unique_identifier, language], + queryFn: () => get<{ readme: string }>('/workspaces/current/plugin/readme', { params: { plugin_unique_identifier, language } }), + enabled: !!plugin_unique_identifier, + }) +} + +export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => { + return useQuery({ + queryKey: ['pluginReadmeAsset', plugin_unique_identifier, file_name], + queryFn: () => get('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name } }), + enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name), + }) +}