diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index be9c4fe49a..a78227258e 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -10,6 +10,7 @@ import { ProviderContextProvider } from '@/context/provider-context' import { ModalContextProvider } from '@/context/modal-context' import GotoAnything from '@/app/components/goto-anything' import Zendesk from '@/app/components/base/zendesk' +import ReadmePanel from '@/app/components/plugins/readme-panel' const Layout = ({ children }: { children: ReactNode }) => { return ( @@ -24,6 +25,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
{children} + diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 4ca58acbf8..caa957b359 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -8,8 +8,7 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' -import type { CommonNodeType } from '@/app/components/workflow/types' -import { BlockEnum, type InputVar } from '@/app/components/workflow/types' +import type { InputVar } from '@/app/components/workflow/types' import { appDefaultIconBackground } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' @@ -41,7 +40,6 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { useNodes } from 'reactflow' import Divider from '../../base/divider' import Loading from '../../base/loading' import Toast from '../../base/toast' @@ -106,6 +104,7 @@ export type AppPublisherProps = { inputs?: InputVar[] onRefreshData?: () => void workflowToolAvailable?: boolean + missingStartNode?: boolean } const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] @@ -125,6 +124,7 @@ const AppPublisher = ({ inputs, onRefreshData, workflowToolAvailable = true, + missingStartNode = false, }: AppPublisherProps) => { const { t } = useTranslation() @@ -147,9 +147,6 @@ const AppPublisher = ({ const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false }) const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS) - const nodes = useNodes() - const missingStartNode = !nodes.some(node => node.data.type === BlockEnum.Start) - const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp]) const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission]) 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 4a4d76f1d8..d4ef4c55b7 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 @@ -214,6 +215,7 @@ const SettingBuiltInTool: FC = ({ pluginPayload={{ provider: collection.name, category: AuthCategory.tool, + detail: collection as any, }} credentialId={credentialId} onAuthorizationItemClick={onAuthorizationItemClick} @@ -243,13 +245,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 c35acbeac7..101ac22b6c 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 ( @@ -53,15 +57,15 @@ export default function Drawer({ }} className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)} > -
+
{/* mask */} - { if (!clickOutsideNotOpen) onClose() }} - /> + />}
<>
diff --git a/web/app/components/base/encrypted-bottom/index.tsx b/web/app/components/base/encrypted-bottom/index.tsx index ccbbbcbc4b..8416217517 100644 --- a/web/app/components/base/encrypted-bottom/index.tsx +++ b/web/app/components/base/encrypted-bottom/index.tsx @@ -14,7 +14,7 @@ export const EncryptedBottom = (props: Props) => { const { frontTextKey, backTextKey, className } = props return ( -
+
{t(frontTextKey || 'common.provider.encrypted.front')} { switch (type) { @@ -91,6 +92,7 @@ const BaseField = ({ fieldState, }: BaseFieldProps) => { const renderI18nObject = useRenderI18nObject() + const { t } = useTranslation() const { name, label, @@ -111,13 +113,15 @@ const BaseField = ({ const disabled = propsDisabled || formSchemaDisabled const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => { - return [ + const results = [ label, placeholder, tooltip, description, help, ].map(v => getTranslatedContent({ content: v, render: renderI18nObject })) + if (!results[1]) results[1] = t('common.placeholder.input') + return results }, [label, placeholder, tooltip, description, help, renderI18nObject]) const watchedVariables = useMemo(() => { diff --git a/web/app/components/base/image-gallery/index.tsx b/web/app/components/base/image-gallery/index.tsx index 0f9061fdb6..8a5c60de27 100644 --- a/web/app/components/base/image-gallery/index.tsx +++ b/web/app/components/base/image-gallery/index.tsx @@ -38,8 +38,7 @@ const ImageGallery: FC = ({
{/* TODO: support preview */} {srcs.map((src, index) => ( - - { - 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..8becf4b5f2 100644 --- a/web/app/components/base/markdown-blocks/paragraph.tsx +++ b/web/app/components/base/markdown-blocks/paragraph.tsx @@ -3,25 +3,44 @@ * 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 imgURL = getMarkdownImageURL(children_node[0].properties?.src, pluginUniqueIdentifier) + 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 imgURL + }, [asset, imgURL]) + + useEffect(() => { + return () => { + if (blobUrl && asset) + URL.revokeObjectURL(blobUrl) + } + }, [blobUrl]) + + if (children_node?.[0]?.tagName === 'img') { 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 19f39d8aaa..ef2bd13867 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -17,10 +17,11 @@ const ReactMarkdown = dynamic(() => import('./react-markdown-wrapper').then(mod export type MarkdownProps = { content: string className?: string + pluginUniqueIdentifier?: string } & Pick export const Markdown = (props: MarkdownProps) => { - const { customComponents = {} } = props + const { customComponents = {}, pluginUniqueIdentifier } = props const latexContent = flow([ preprocessThinkTag, preprocessLaTeX, @@ -28,7 +29,7 @@ export const Markdown = (props: MarkdownProps) => { return (
- +
) } diff --git a/web/app/components/base/markdown/react-markdown-wrapper.tsx b/web/app/components/base/markdown/react-markdown-wrapper.tsx index 054b5f66cb..0632b83245 100644 --- a/web/app/components/base/markdown/react-markdown-wrapper.tsx +++ b/web/app/components/base/markdown/react-markdown-wrapper.tsx @@ -25,10 +25,11 @@ export type ReactMarkdownWrapperProps = { latexContent: any customDisallowedElements?: string[] customComponents?: Record> + pluginUniqueIdentifier?: string } export const ReactMarkdownWrapper: FC = (props) => { - const { customComponents, latexContent } = props + const { customComponents, latexContent, pluginUniqueIdentifier } = props return ( = (props) => { rehypePlugins={[ RehypeKatex, RehypeRaw as any, - // The Rehype plug-in is used to remove the ref attribute of an element + // The Rehype plug-in is used to remove the ref attribute of an element () => { return (tree: any) => { const iterate = (node: any) => { @@ -63,11 +64,11 @@ export const ReactMarkdownWrapper: FC = (props) => { disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} components={{ code: CodeBlock, - img: Img, + img: (props: any) => , video: VideoBlock, audio: AudioBlock, a: Link, - p: Paragraph, + p: (props: any) => , 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 018a4ccace..f091717191 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 @@ -23,6 +24,7 @@ type IModal = { export default function Modal({ className, wrapperClassName, + containerClassName, isShow, onClose = noop, title, @@ -46,7 +48,6 @@ export default function Modal({ 'data-[leave]:opacity-0', )} /> -
{ @@ -54,7 +55,7 @@ export default function Modal({ e.stopPropagation() }} > -
+
{ const { t } = useTranslation() @@ -53,7 +55,7 @@ const Modal = ({
) } - bottomSlot={ -
- - {t('common.modelProvider.encrypted.front')} - - PKCS1_OAEP - - {t('common.modelProvider.encrypted.back')} -
- } + bottomSlot={} onConfirm={handleConfirm} showExtraButton={!!editValues} onExtraButtonClick={onRemove} disabled={disabled || isLoading || doingAction} + clickOutsideNotClose={true} > + { 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..2db595ea72 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/store' type OAuthClientSettingsProps = { pluginPayload: PluginPayload @@ -154,16 +156,17 @@ const OAuthClientSettings = ({
) } + containerClassName='pt-0' + clickOutsideNotClose={true} > - <> - - + + ) } diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts index b98238fd58..aab29f8cec 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 type { AddApiKeyButtonProps } from './authorize/add-api-key-button' export type { AddOAuthButtonProps } from './authorize/add-oauth-button' @@ -11,6 +13,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 7a8cdcec67..941e91fee8 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -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..9c3765def3 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) => { @@ -206,10 +208,11 @@ const EndpointCard = ({ )} {isShowEndpointModal && ( )}
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..fff6775495 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -102,14 +102,16 @@ const EndpointList = ({ detail }: Props) => { key={index} data={item} handleChange={() => invalidateEndpointList(detail.plugin_id)} + pluginDetail={detail} /> ))}
{isShowEndpointModal && ( )}
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 3041f13f2f..ce0df8123b 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,16 @@ 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' +import type { FormSchema } from '../../base/form/types' type Props = { - formSchemas: any + formSchemas: FormSchema[] defaultValues?: any onCancel: () => void onSaved: (value: Record) => void + pluginDetail: PluginDetail } const extractDefaultValues = (schemas: any[]) => { @@ -32,6 +36,7 @@ const EndpointModal: FC = ({ defaultValues = {}, onCancel, onSaved, + pluginDetail, }) => { const getValueFromI18nObject = useRenderI18nObject() const { t } = useTranslation() @@ -43,7 +48,7 @@ const EndpointModal: FC = ({ const handleSave = () => { for (const field of formSchemas) { if (field.required && !tempCredential[field.name]) { - Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) }) + Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record) }) }) return } } @@ -84,6 +89,7 @@ const EndpointModal: FC = ({
{t('plugin.detailPanel.endpointModalDesc')}
+
@@ -92,7 +98,7 @@ const EndpointModal: FC = ({ onChange={(v) => { setTempCredential(v) }} - formSchemas={formSchemas} + formSchemas={formSchemas as any} isEditMode={true} showOnVariableMap={{}} validating={false} diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index ae82b1f55a..380d2329f6 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -3,7 +3,7 @@ import Drawer from '@/app/components/base/drawer' import { PluginCategoryEnum, type PluginDetail } from '@/app/components/plugins/types' import cn from '@/utils/classnames' import type { FC } from 'react' -import { useEffect } from 'react' +import { useCallback, useEffect } from 'react' import ActionList from './action-list' import AgentStrategyList from './agent-strategy-list' import DatasourceActionList from './datasource-action-list' @@ -11,8 +11,9 @@ import DetailHeader from './detail-header' import EndpointList from './endpoint-list' import ModelList from './model-list' import { SubscriptionList } from './subscription-list' -import { usePluginStore } from './subscription-list/store' +import { usePluginStore } from './store' import { TriggerEventsList } from './trigger/event-list' +import { ReadmeEntrance } from '../readme-panel/entrance' type Props = { detail?: PluginDetail @@ -25,19 +26,22 @@ const PluginDetailPanel: FC = ({ onUpdate, onHide, }) => { - const handleUpdate = (isDelete = false) => { + const handleUpdate = useCallback((isDelete = false) => { if (isDelete) onHide() onUpdate() - } + }, [onHide, onUpdate]) + const { setDetail } = usePluginStore() useEffect(() => { setDetail(!detail ? undefined : { plugin_id: detail.plugin_id, provider: `${detail.plugin_id}/${detail.declaration.name}`, + plugin_unique_identifier: detail.plugin_unique_identifier || '', declaration: detail.declaration, name: detail.name, + id: detail.id, }) }, [detail]) @@ -56,23 +60,24 @@ const PluginDetailPanel: FC = ({ > {detail && ( <> - +
- {detail.declaration.category === PluginCategoryEnum.trigger && ( - <> - - - - )} - {!!detail.declaration.tool && } - {!!detail.declaration.agent_strategy && } - {!!detail.declaration.endpoint && } - {!!detail.declaration.model && } - {!!detail.declaration.datasource && } +
+
+ {detail.declaration.category === PluginCategoryEnum.trigger && ( + <> + + + + )} + {!!detail.declaration.tool && } + {!!detail.declaration.agent_strategy && } + {!!detail.declaration.endpoint && } + {!!detail.declaration.model && } + {!!detail.declaration.datasource && } +
+ +
)} diff --git a/web/app/components/plugins/plugin-detail-panel/store.ts b/web/app/components/plugins/plugin-detail-panel/store.ts new file mode 100644 index 0000000000..26b778f288 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/store.ts @@ -0,0 +1,14 @@ +import { create } from 'zustand' +import type { PluginDetail } from '../types' + +export type SimpleDetail = Pick & { provider: string } + +type Shape = { + detail: SimpleDetail | undefined + setDetail: (detail?: SimpleDetail) => void +} + +export const usePluginStore = create(set => ({ + detail: undefined, + setDetail: (detail?: SimpleDetail) => set({ detail }), +})) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index c006c8d26e..b4d2b9ab31 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -23,7 +23,8 @@ import { debounce } from 'lodash-es' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import LogViewer from '../log-viewer' -import { usePluginStore, usePluginSubscriptionStore } from '../store' +import { usePluginSubscriptionStore } from '../store' +import { usePluginStore } from '../../store' type Props = { onClose: () => void diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx index 3ec1c8cacf..7515ba4b4a 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -14,7 +14,7 @@ import { useBoolean } from 'ahooks' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { SupportedCreationMethods } from '../../../types' -import { usePluginStore } from '../store' +import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' import { CommonCreateModal } from './common-modal' import { OAuthClientSettingsModal } from './oauth-client' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx index 3a5ab85881..490652f1c0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx @@ -20,7 +20,7 @@ import { } from '@remixicon/react' import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { usePluginStore } from '../store' +import { usePluginStore } from '../../store' type Props = { oauthConfig?: TriggerOAuthConfig diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts index e197a124c7..24840e9971 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts @@ -1,17 +1,4 @@ import { create } from 'zustand' -import type { PluginDetail } from '../../types' - -export type SimpleDetail = Pick & { provider: string } - -type Shape = { - detail: SimpleDetail | undefined - setDetail: (detail?: SimpleDetail) => void -} - -export const usePluginStore = create(set => ({ - detail: undefined, - setDetail: (detail?: SimpleDetail) => set({ detail }), -})) type ShapeSubscription = { refresh?: () => void diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts index a31b46792d..ff3e903a31 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { useTriggerSubscriptions } from '@/service/use-triggers' -import { usePluginStore, usePluginSubscriptionStore } from './store' +import { usePluginStore } from '../store' +import { usePluginSubscriptionStore } from './store' export const useSubscriptionList = () => { const detail = usePluginStore(state => state.detail) 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..a41f44c3d5 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-detail-panel/trigger/event-list.tsx b/web/app/components/plugins/plugin-detail-panel/trigger/event-list.tsx index 0756035a49..1eef047771 100644 --- a/web/app/components/plugins/plugin-detail-panel/trigger/event-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/trigger/event-list.tsx @@ -5,7 +5,7 @@ import { useTriggerProviderInfo } from '@/service/use-triggers' import cn from '@/utils/classnames' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { usePluginStore } from '../subscription-list/store' +import { usePluginStore } from '../store' import { EventDetailDrawer } from './event-detail-drawer' type TriggerEventCardProps = { 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..2096fadbf1 --- /dev/null +++ b/web/app/components/plugins/plugin-title-info/index.tsx @@ -0,0 +1,118 @@ +'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' +import type { TypeWithI18N } from '../../base/form/types' + +type PluginInfoProps = { + detail: PluginDetail & { icon?: string, label?: TypeWithI18N, author?: string, name?: string, verified?: boolean } + 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 = detail.declaration?.icon || detail?.icon + const label = detail.declaration?.label || detail?.label + const author = detail.declaration?.author || detail?.author + const name = detail.declaration?.name || detail?.name + const verified = detail.declaration?.verified || detail?.verified + + 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/entrance.tsx b/web/app/components/plugins/readme-panel/entrance.tsx new file mode 100644 index 0000000000..f3b4c98412 --- /dev/null +++ b/web/app/components/plugins/readme-panel/entrance.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { RiBookReadLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import { ReadmeShowType, useReadmePanelStore } from './store' +import { BUILTIN_TOOLS_ARRAY } from './constants' +import type { PluginDetail } from '../types' + +export const ReadmeEntrance = ({ + pluginDetail, + showType = ReadmeShowType.drawer, + className, + showShortTip = false, +}: { + pluginDetail: PluginDetail + showType?: ReadmeShowType + className?: string + showShortTip?: boolean +}) => { + const { t } = useTranslation() + const { setCurrentPluginDetail } = useReadmePanelStore() + + const handleReadmeClick = () => { + if (pluginDetail) + setCurrentPluginDetail(pluginDetail, showType) + } + if (!pluginDetail || BUILTIN_TOOLS_ARRAY.includes(pluginDetail.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..274a5200db --- /dev/null +++ b/web/app/components/plugins/readme-panel/index.tsx @@ -0,0 +1,127 @@ +'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, useReadmePanelStore } from './store' + +const ReadmePanel: FC = () => { + const { currentPluginDetail, setCurrentPluginDetail } = useReadmePanelStore() + const { detail, showType } = currentPluginDetail || {} + const { t } = useTranslation() + const language = useLanguage() + + const pluginUniqueIdentifier = detail?.plugin_unique_identifier || '' + + const { data: readmeData, isLoading, error } = usePluginReadme( + { plugin_unique_identifier: pluginUniqueIdentifier, language: language === 'zh-Hans' ? undefined : language }, + ) + + const onClose = () => { + setCurrentPluginDetail() + } + + 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/plugins/readme-panel/store.ts b/web/app/components/plugins/readme-panel/store.ts new file mode 100644 index 0000000000..29c989db10 --- /dev/null +++ b/web/app/components/plugins/readme-panel/store.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand' +import type { PluginDetail } from '@/app/components/plugins/types' + +export enum ReadmeShowType { + drawer = 'drawer', + modal = 'modal', +} + +type Shape = { + currentPluginDetail?: { + detail: PluginDetail + showType: ReadmeShowType + } + setCurrentPluginDetail: (detail?: PluginDetail, showType?: ReadmeShowType) => void +} + +export const useReadmePanelStore = create(set => ({ + currentPluginDetail: undefined, + setCurrentPluginDetail: (detail?: PluginDetail, showType?: ReadmeShowType) => set({ + currentPluginDetail: !detail ? undefined : { + detail, + showType: showType ?? ReadmeShowType.drawer, + }, + }), +})) diff --git a/web/app/components/tools/add-tool-modal/empty.tsx b/web/app/components/tools/add-tool-modal/empty.tsx index 5759589c8e..4d69dc1076 100644 --- a/web/app/components/tools/add-tool-modal/empty.tsx +++ b/web/app/components/tools/add-tool-modal/empty.tsx @@ -35,7 +35,7 @@ const Empty = ({ const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title` return ( -
+
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'} diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 9fed2bc07e..444d2e7b01 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo, } from 'react' -import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow' +import { useEdges, useNodes } from 'reactflow' import { RiApps2AddLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { @@ -37,6 +37,8 @@ import { useStore as useAppStore } from '@/app/components/app/store' import useTheme from '@/hooks/use-theme' import cn from '@/utils/classnames' import { useIsChatMode } from '../../hooks' +import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' +import type { Node } from '@/app/components/workflow/types' const FeaturesTrigger = () => { const { t } = useTranslation() @@ -51,11 +53,12 @@ const FeaturesTrigger = () => { const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const toolPublished = useStore(s => s.toolPublished) const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput) - const startVariables = useReactflowStore( - s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables, - ) + const nodes = useNodes() + const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + const startVariables = (startNode as Node)?.data?.variables const edges = useEdges() + const fileSettings = useFeatures(s => s.features.file) const variables = useMemo(() => { const data = startVariables || [] @@ -185,6 +188,7 @@ const FeaturesTrigger = () => { onToggle: onPublisherToggle, workflowToolAvailable: lastPublishedHasUserInput, crossAxisOffset: 4, + missingStartNode: !startNode, }} /> 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 4d24f6af22..d0e9de31b2 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 @@ -78,6 +78,7 @@ import PanelWrap from '../before-run-form/panel-wrap' import LastRun from './last-run' import useLastRun from './last-run/use-last-run' import { TriggerSubscription } from './trigger-subscription' +import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => { const nodeType = params.payload.type @@ -492,6 +493,7 @@ const BasePanel: FC = ({ pluginPayload={{ provider: currToolCollection?.name || '', category: AuthCategory.tool, + detail: currToolCollection as any, }} >
@@ -503,6 +505,7 @@ const BasePanel: FC = ({ pluginPayload={{ provider: currToolCollection?.name || '', category: AuthCategory.tool, + detail: currToolCollection as any, }} onAuthorizationItemClick={handleAuthorizationItemClick} credentialId={data.credential_id} @@ -619,6 +622,9 @@ const BasePanel: FC = ({ {...passedLogParams} /> )} + + {data.type === BlockEnum.Tool && } + {data.type === BlockEnum.DataSource && }
) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx index 90b7aca0a7..4ae499580f 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx @@ -2,7 +2,7 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list' import { CreateButtonType, CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create' import { SubscriptionSelectorEntry } from '@/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry' -import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/subscription-list/store' +import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store' import { useSubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list' import useConfig from '@/app/components/workflow/nodes/trigger-plugin/use-config' import type { Node } from '@/app/components/workflow/types' diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 16b7bebd82..b84c9cabdb 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -309,6 +309,11 @@ const translation = { connectedWorkspace: 'Connected Workspace', emptyAuth: 'Please configure authentication', }, + 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 db92e34485..d07881a2bc 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -309,6 +309,11 @@ const translation = { connectedWorkspace: '已连接的工作区', emptyAuth: '请配置凭据', }, + 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 5904da6d27..9319ee7106 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -673,3 +673,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), + }) +}