mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 20:17:29 +08:00
feat: add plugin readme
This commit is contained in:
parent
9aa43c9165
commit
4b4ec3438f
@ -10,6 +10,7 @@ import { ProviderContextProvider } from '@/context/provider-context'
|
|||||||
import { ModalContextProvider } from '@/context/modal-context'
|
import { ModalContextProvider } from '@/context/modal-context'
|
||||||
import GotoAnything from '@/app/components/goto-anything'
|
import GotoAnything from '@/app/components/goto-anything'
|
||||||
import Zendesk from '@/app/components/base/zendesk'
|
import Zendesk from '@/app/components/base/zendesk'
|
||||||
|
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||||
|
|
||||||
const Layout = ({ children }: { children: ReactNode }) => {
|
const Layout = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@ -24,6 +25,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
<Header />
|
<Header />
|
||||||
</HeaderWrapper>
|
</HeaderWrapper>
|
||||||
{children}
|
{children}
|
||||||
|
<ReadmePanel />
|
||||||
<GotoAnything />
|
<GotoAnything />
|
||||||
</ModalContextProvider>
|
</ModalContextProvider>
|
||||||
</ProviderContextProvider>
|
</ProviderContextProvider>
|
||||||
|
|||||||
@ -8,8 +8,7 @@ import {
|
|||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
import type { InputVar } from '@/app/components/workflow/types'
|
||||||
import { BlockEnum, type InputVar } from '@/app/components/workflow/types'
|
|
||||||
import { appDefaultIconBackground } from '@/config'
|
import { appDefaultIconBackground } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
@ -41,7 +40,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNodes } from 'reactflow'
|
|
||||||
import Divider from '../../base/divider'
|
import Divider from '../../base/divider'
|
||||||
import Loading from '../../base/loading'
|
import Loading from '../../base/loading'
|
||||||
import Toast from '../../base/toast'
|
import Toast from '../../base/toast'
|
||||||
@ -106,6 +104,7 @@ export type AppPublisherProps = {
|
|||||||
inputs?: InputVar[]
|
inputs?: InputVar[]
|
||||||
onRefreshData?: () => void
|
onRefreshData?: () => void
|
||||||
workflowToolAvailable?: boolean
|
workflowToolAvailable?: boolean
|
||||||
|
missingStartNode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||||
@ -125,6 +124,7 @@ const AppPublisher = ({
|
|||||||
inputs,
|
inputs,
|
||||||
onRefreshData,
|
onRefreshData,
|
||||||
workflowToolAvailable = true,
|
workflowToolAvailable = true,
|
||||||
|
missingStartNode = false,
|
||||||
}: AppPublisherProps) => {
|
}: AppPublisherProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -147,9 +147,6 @@ const AppPublisher = ({
|
|||||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
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 { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||||
|
|
||||||
const nodes = useNodes<CommonNodeType>()
|
|
||||||
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 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])
|
const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission])
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
AuthCategory,
|
AuthCategory,
|
||||||
PluginAuthInAgent,
|
PluginAuthInAgent,
|
||||||
} from '@/app/components/plugins/plugin-auth'
|
} from '@/app/components/plugins/plugin-auth'
|
||||||
|
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showBackButton?: boolean
|
showBackButton?: boolean
|
||||||
@ -214,6 +215,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||||||
pluginPayload={{
|
pluginPayload={{
|
||||||
provider: collection.name,
|
provider: collection.name,
|
||||||
category: AuthCategory.tool,
|
category: AuthCategory.tool,
|
||||||
|
detail: collection as any,
|
||||||
}}
|
}}
|
||||||
credentialId={credentialId}
|
credentialId={credentialId}
|
||||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||||
@ -243,13 +245,14 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
<div className='h-0 grow overflow-y-auto px-4'>
|
<div className='h-0 grow overflow-y-auto px-4'>
|
||||||
{isInfoActive ? infoUI : settingUI}
|
{isInfoActive ? infoUI : settingUI}
|
||||||
|
{!readonly && !isInfoActive && (
|
||||||
|
<div className='flex shrink-0 justify-end space-x-2 rounded-b-[10px] bg-components-panel-bg py-2'>
|
||||||
|
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium ' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||||
|
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!readonly && !isInfoActive && (
|
<ReadmeEntrance pluginDetail={collection as any} className='mt-auto' />
|
||||||
<div className='mt-2 flex shrink-0 justify-end space-x-2 rounded-b-[10px] border-t border-divider-regular bg-components-panel-bg px-6 py-4'>
|
|
||||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium ' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
|
||||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type IDrawerProps = {
|
|||||||
description?: string
|
description?: string
|
||||||
dialogClassName?: string
|
dialogClassName?: string
|
||||||
dialogBackdropClassName?: string
|
dialogBackdropClassName?: string
|
||||||
|
containerClassName?: string
|
||||||
panelClassName?: string
|
panelClassName?: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
footer?: React.ReactNode
|
footer?: React.ReactNode
|
||||||
@ -22,6 +23,7 @@ export type IDrawerProps = {
|
|||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
onOk?: () => void
|
onOk?: () => void
|
||||||
unmount?: boolean
|
unmount?: boolean
|
||||||
|
noOverlay?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Drawer({
|
export default function Drawer({
|
||||||
@ -29,6 +31,7 @@ export default function Drawer({
|
|||||||
description = '',
|
description = '',
|
||||||
dialogClassName = '',
|
dialogClassName = '',
|
||||||
dialogBackdropClassName = '',
|
dialogBackdropClassName = '',
|
||||||
|
containerClassName = '',
|
||||||
panelClassName = '',
|
panelClassName = '',
|
||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
@ -41,6 +44,7 @@ export default function Drawer({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onOk,
|
onOk,
|
||||||
unmount = false,
|
unmount = false,
|
||||||
|
noOverlay = false,
|
||||||
}: IDrawerProps) {
|
}: IDrawerProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
@ -53,15 +57,15 @@ export default function Drawer({
|
|||||||
}}
|
}}
|
||||||
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
|
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
|
||||||
>
|
>
|
||||||
<div className={cn('flex h-screen w-screen justify-end', positionCenter && '!justify-center')}>
|
<div className={cn('flex h-screen w-screen justify-end', positionCenter && '!justify-center', containerClassName)}>
|
||||||
{/* mask */}
|
{/* mask */}
|
||||||
<DialogBackdrop
|
{!noOverlay && <DialogBackdrop
|
||||||
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
|
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!clickOutsideNotOpen)
|
if (!clickOutsideNotOpen)
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
/>
|
/>}
|
||||||
<div className={cn('relative z-[50] flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}>
|
<div className={cn('relative z-[50] flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}>
|
||||||
<>
|
<>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const EncryptedBottom = (props: Props) => {
|
|||||||
const { frontTextKey, backTextKey, className } = props
|
const { frontTextKey, backTextKey, className } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('system-xs-regular flex items-center rounded-b-2xl border-t-[0.5px] border-divider-subtle bg-background-soft px-2 py-3 text-text-tertiary', className)}>
|
<div className={cn('system-xs-regular flex items-center justify-center rounded-b-2xl border-t-[0.5px] border-divider-subtle bg-background-soft px-2 py-3 text-text-tertiary', className)}>
|
||||||
<RiLock2Fill className='mx-1 h-3 w-3 text-text-quaternary' />
|
<RiLock2Fill className='mx-1 h-3 w-3 text-text-quaternary' />
|
||||||
{t(frontTextKey || 'common.provider.encrypted.front')}
|
{t(frontTextKey || 'common.provider.encrypted.front')}
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const getExtraProps = (type: FormTypeEnum) => {
|
const getExtraProps = (type: FormTypeEnum) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -91,6 +92,7 @@ const BaseField = ({
|
|||||||
fieldState,
|
fieldState,
|
||||||
}: BaseFieldProps) => {
|
}: BaseFieldProps) => {
|
||||||
const renderI18nObject = useRenderI18nObject()
|
const renderI18nObject = useRenderI18nObject()
|
||||||
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
@ -111,13 +113,15 @@ const BaseField = ({
|
|||||||
const disabled = propsDisabled || formSchemaDisabled
|
const disabled = propsDisabled || formSchemaDisabled
|
||||||
|
|
||||||
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
|
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
|
||||||
return [
|
const results = [
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
tooltip,
|
tooltip,
|
||||||
description,
|
description,
|
||||||
help,
|
help,
|
||||||
].map(v => getTranslatedContent({ content: v, render: renderI18nObject }))
|
].map(v => getTranslatedContent({ content: v, render: renderI18nObject }))
|
||||||
|
if (!results[1]) results[1] = t('common.placeholder.input')
|
||||||
|
return results
|
||||||
}, [label, placeholder, tooltip, description, help, renderI18nObject])
|
}, [label, placeholder, tooltip, description, help, renderI18nObject])
|
||||||
|
|
||||||
const watchedVariables = useMemo(() => {
|
const watchedVariables = useMemo(() => {
|
||||||
|
|||||||
@ -38,8 +38,7 @@ const ImageGallery: FC<Props> = ({
|
|||||||
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
|
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
|
||||||
{/* TODO: support preview */}
|
{/* TODO: support preview */}
|
||||||
{srcs.map((src, index) => (
|
{srcs.map((src, index) => (
|
||||||
|
!src ? null : <img
|
||||||
<img
|
|
||||||
key={index}
|
key={index}
|
||||||
className={s.item}
|
className={s.item}
|
||||||
style={imgStyle}
|
style={imgStyle}
|
||||||
|
|||||||
@ -3,11 +3,34 @@
|
|||||||
* Extracted from the main markdown renderer for modularity.
|
* Extracted from the main markdown renderer for modularity.
|
||||||
* Uses the ImageGallery component to display images.
|
* 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 ImageGallery from '@/app/components/base/image-gallery'
|
||||||
|
import { getMarkdownImageURL } from './utils'
|
||||||
|
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||||
|
|
||||||
const Img = ({ src }: any) => {
|
const Img = ({ src, pluginUniqueIdentifier }: { src: string, pluginUniqueIdentifier?: string }) => {
|
||||||
return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div>
|
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 (
|
||||||
|
<div className='markdown-img-wrapper'>
|
||||||
|
<ImageGallery srcs={[blobUrl]} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Img
|
export default Img
|
||||||
|
|||||||
@ -3,25 +3,44 @@
|
|||||||
* Extracted from the main markdown renderer for modularity.
|
* Extracted from the main markdown renderer for modularity.
|
||||||
* Handles special rendering for paragraphs that directly contain an image.
|
* 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 ImageGallery from '@/app/components/base/image-gallery'
|
||||||
|
import { getMarkdownImageURL } from './utils'
|
||||||
|
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||||
|
|
||||||
const Paragraph = (paragraph: any) => {
|
const Paragraph = (props: { pluginUniqueIdentifier?: string, node?: any, children?: any }) => {
|
||||||
const { node }: any = paragraph
|
const { node, pluginUniqueIdentifier, children } = props
|
||||||
const children_node = node.children
|
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 (
|
return (
|
||||||
<div className="markdown-img-wrapper">
|
<div className="markdown-img-wrapper">
|
||||||
<ImageGallery srcs={[children_node[0].properties.src]} />
|
<ImageGallery srcs={[blobUrl]} />
|
||||||
{
|
{
|
||||||
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
|
Array.isArray(children) && children.length > 1 && (
|
||||||
<div className="mt-2">{paragraph.children.slice(1)}</div>
|
<div className="mt-2">{children.slice(1)}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <p>{paragraph.children}</p>
|
return <p>{children}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Paragraph
|
export default Paragraph
|
||||||
|
|||||||
@ -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 => {
|
export const isValidUrl = (url: string): boolean => {
|
||||||
const validPrefixes = ['http:', 'https:', '//', 'mailto:']
|
const validPrefixes = ['http:', 'https:', '//', 'mailto:']
|
||||||
if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:')
|
if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:')
|
||||||
return validPrefixes.some(prefix => url.startsWith(prefix))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -17,10 +17,11 @@ const ReactMarkdown = dynamic(() => import('./react-markdown-wrapper').then(mod
|
|||||||
export type MarkdownProps = {
|
export type MarkdownProps = {
|
||||||
content: string
|
content: string
|
||||||
className?: string
|
className?: string
|
||||||
|
pluginUniqueIdentifier?: string
|
||||||
} & Pick<ReactMarkdownWrapperProps, 'customComponents' | 'customDisallowedElements'>
|
} & Pick<ReactMarkdownWrapperProps, 'customComponents' | 'customDisallowedElements'>
|
||||||
|
|
||||||
export const Markdown = (props: MarkdownProps) => {
|
export const Markdown = (props: MarkdownProps) => {
|
||||||
const { customComponents = {} } = props
|
const { customComponents = {}, pluginUniqueIdentifier } = props
|
||||||
const latexContent = flow([
|
const latexContent = flow([
|
||||||
preprocessThinkTag,
|
preprocessThinkTag,
|
||||||
preprocessLaTeX,
|
preprocessLaTeX,
|
||||||
@ -28,7 +29,7 @@ export const Markdown = (props: MarkdownProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
|
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
|
||||||
<ReactMarkdown latexContent={latexContent} customComponents={customComponents} customDisallowedElements={props.customDisallowedElements} />
|
<ReactMarkdown pluginUniqueIdentifier={pluginUniqueIdentifier} latexContent={latexContent} customComponents={customComponents} customDisallowedElements={props.customDisallowedElements} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,11 @@ export type ReactMarkdownWrapperProps = {
|
|||||||
latexContent: any
|
latexContent: any
|
||||||
customDisallowedElements?: string[]
|
customDisallowedElements?: string[]
|
||||||
customComponents?: Record<string, React.ComponentType<any>>
|
customComponents?: Record<string, React.ComponentType<any>>
|
||||||
|
pluginUniqueIdentifier?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
|
export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
|
||||||
const { customComponents, latexContent } = props
|
const { customComponents, latexContent, pluginUniqueIdentifier } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -40,7 +41,7 @@ export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
|
|||||||
rehypePlugins={[
|
rehypePlugins={[
|
||||||
RehypeKatex,
|
RehypeKatex,
|
||||||
RehypeRaw as any,
|
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) => {
|
return (tree: any) => {
|
||||||
const iterate = (node: any) => {
|
const iterate = (node: any) => {
|
||||||
@ -63,11 +64,11 @@ export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
|
|||||||
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
|
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
|
||||||
components={{
|
components={{
|
||||||
code: CodeBlock,
|
code: CodeBlock,
|
||||||
img: Img,
|
img: (props: any) => <Img {...props} pluginUniqueIdentifier={pluginUniqueIdentifier} />,
|
||||||
video: VideoBlock,
|
video: VideoBlock,
|
||||||
audio: AudioBlock,
|
audio: AudioBlock,
|
||||||
a: Link,
|
a: Link,
|
||||||
p: Paragraph,
|
p: (props: any) => <Paragraph {...props} pluginUniqueIdentifier={pluginUniqueIdentifier} />,
|
||||||
button: MarkdownButton,
|
button: MarkdownButton,
|
||||||
form: MarkdownForm,
|
form: MarkdownForm,
|
||||||
script: ScriptBlock as any,
|
script: ScriptBlock as any,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { noop } from 'lodash-es'
|
|||||||
type IModal = {
|
type IModal = {
|
||||||
className?: string
|
className?: string
|
||||||
wrapperClassName?: string
|
wrapperClassName?: string
|
||||||
|
containerClassName?: string
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
@ -23,6 +24,7 @@ type IModal = {
|
|||||||
export default function Modal({
|
export default function Modal({
|
||||||
className,
|
className,
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
|
containerClassName,
|
||||||
isShow,
|
isShow,
|
||||||
onClose = noop,
|
onClose = noop,
|
||||||
title,
|
title,
|
||||||
@ -46,7 +48,6 @@ export default function Modal({
|
|||||||
'data-[leave]:opacity-0',
|
'data-[leave]:opacity-0',
|
||||||
)} />
|
)} />
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 overflow-y-auto"
|
className="fixed inset-0 overflow-y-auto"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -54,7 +55,7 @@ export default function Modal({
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className={classNames('flex min-h-full items-center justify-center p-4 text-center', containerClassName)}>
|
||||||
<TransitionChild>
|
<TransitionChild>
|
||||||
<DialogPanel className={classNames(
|
<DialogPanel className={classNames(
|
||||||
'relative w-full max-w-[480px] rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all',
|
'relative w-full max-w-[480px] rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all',
|
||||||
|
|||||||
@ -27,6 +27,7 @@ type ModalProps = {
|
|||||||
bottomSlot?: React.ReactNode
|
bottomSlot?: React.ReactNode
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
containerClassName?: string
|
containerClassName?: string
|
||||||
|
clickOutsideNotClose?: boolean
|
||||||
}
|
}
|
||||||
const Modal = ({
|
const Modal = ({
|
||||||
onClose,
|
onClose,
|
||||||
@ -46,6 +47,7 @@ const Modal = ({
|
|||||||
bottomSlot,
|
bottomSlot,
|
||||||
disabled,
|
disabled,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
clickOutsideNotClose = false,
|
||||||
}: ModalProps) => {
|
}: ModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ const Modal = ({
|
|||||||
<PortalToFollowElem open>
|
<PortalToFollowElem open>
|
||||||
<PortalToFollowElemContent
|
<PortalToFollowElemContent
|
||||||
className='z-[9998] flex h-full w-full items-center justify-center bg-background-overlay'
|
className='z-[9998] flex h-full w-full items-center justify-center bg-background-overlay'
|
||||||
onClick={onClose}
|
onClick={clickOutsideNotClose ? undefined : onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
|
||||||
import Modal from '@/app/components/base/modal/modal'
|
import Modal from '@/app/components/base/modal/modal'
|
||||||
import { CredentialTypeEnum } from '../types'
|
import { CredentialTypeEnum } from '../types'
|
||||||
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||||
@ -23,6 +22,9 @@ import {
|
|||||||
useGetPluginCredentialSchemaHook,
|
useGetPluginCredentialSchemaHook,
|
||||||
useUpdatePluginCredentialHook,
|
useUpdatePluginCredentialHook,
|
||||||
} from '../hooks/use-credential'
|
} from '../hooks/use-credential'
|
||||||
|
import { ReadmeEntrance } from '../../readme-panel/entrance'
|
||||||
|
import { ReadmeShowType } from '../../readme-panel/store'
|
||||||
|
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||||
|
|
||||||
export type ApiKeyModalProps = {
|
export type ApiKeyModalProps = {
|
||||||
pluginPayload: PluginPayload
|
pluginPayload: PluginPayload
|
||||||
@ -134,25 +136,14 @@ const ApiKeyModal = ({
|
|||||||
footerSlot={
|
footerSlot={
|
||||||
(<div></div>)
|
(<div></div>)
|
||||||
}
|
}
|
||||||
bottomSlot={
|
bottomSlot={<EncryptedBottom />}
|
||||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
|
||||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
|
||||||
{t('common.modelProvider.encrypted.front')}
|
|
||||||
<a
|
|
||||||
className='mx-1 text-text-accent'
|
|
||||||
target='_blank' rel='noopener noreferrer'
|
|
||||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
|
||||||
>
|
|
||||||
PKCS1_OAEP
|
|
||||||
</a>
|
|
||||||
{t('common.modelProvider.encrypted.back')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
showExtraButton={!!editValues}
|
showExtraButton={!!editValues}
|
||||||
onExtraButtonClick={onRemove}
|
onExtraButtonClick={onRemove}
|
||||||
disabled={disabled || isLoading || doingAction}
|
disabled={disabled || isLoading || doingAction}
|
||||||
|
clickOutsideNotClose={true}
|
||||||
>
|
>
|
||||||
|
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
|
||||||
{
|
{
|
||||||
isLoading && (
|
isLoading && (
|
||||||
<div className='flex h-40 items-center justify-center'>
|
<div className='flex h-40 items-center justify-center'>
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import type {
|
|||||||
} from '@/app/components/base/form/types'
|
} from '@/app/components/base/form/types'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import { ReadmeEntrance } from '../../readme-panel/entrance'
|
||||||
|
import { ReadmeShowType } from '../../readme-panel/store'
|
||||||
|
|
||||||
type OAuthClientSettingsProps = {
|
type OAuthClientSettingsProps = {
|
||||||
pluginPayload: PluginPayload
|
pluginPayload: PluginPayload
|
||||||
@ -154,16 +156,17 @@ const OAuthClientSettings = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
containerClassName='pt-0'
|
||||||
|
clickOutsideNotClose={true}
|
||||||
>
|
>
|
||||||
<>
|
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
|
||||||
<AuthForm
|
<AuthForm
|
||||||
formFromProps={form}
|
formFromProps={form}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
formSchemas={schemas}
|
formSchemas={schemas}
|
||||||
defaultValues={editValues || defaultValues}
|
defaultValues={editValues || defaultValues}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { PluginDetail } from '../types'
|
||||||
|
|
||||||
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
|
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
|
||||||
export type { AddOAuthButtonProps } from './authorize/add-oauth-button'
|
export type { AddOAuthButtonProps } from './authorize/add-oauth-button'
|
||||||
|
|
||||||
@ -11,6 +13,7 @@ export enum AuthCategory {
|
|||||||
export type PluginPayload = {
|
export type PluginPayload = {
|
||||||
category: AuthCategory
|
category: AuthCategory
|
||||||
provider: string
|
provider: string
|
||||||
|
detail: PluginDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CredentialTypeEnum {
|
export enum CredentialTypeEnum {
|
||||||
|
|||||||
@ -128,13 +128,13 @@ const DetailHeader = ({
|
|||||||
return false
|
return false
|
||||||
if (!autoUpgradeInfo || !isFromMarketplace)
|
if (!autoUpgradeInfo || !isFromMarketplace)
|
||||||
return false
|
return false
|
||||||
if(autoUpgradeInfo.strategy_setting === 'disabled')
|
if (autoUpgradeInfo.strategy_setting === 'disabled')
|
||||||
return false
|
return false
|
||||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all)
|
if (autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all)
|
||||||
return true
|
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
|
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 true
|
||||||
return false
|
return false
|
||||||
}, [autoUpgradeInfo, plugin_id, isFromMarketplace])
|
}, [autoUpgradeInfo, plugin_id, isFromMarketplace])
|
||||||
@ -331,6 +331,7 @@ const DetailHeader = ({
|
|||||||
pluginPayload={{
|
pluginPayload={{
|
||||||
provider: provider?.name || '',
|
provider: provider?.name || '',
|
||||||
category: AuthCategory.tool,
|
category: AuthCategory.tool,
|
||||||
|
detail,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
|
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 EndpointModal from './endpoint-modal'
|
||||||
import { NAME_FIELD } from './utils'
|
import { NAME_FIELD } from './utils'
|
||||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||||
@ -22,11 +22,13 @@ import {
|
|||||||
} from '@/service/use-endpoints'
|
} from '@/service/use-endpoints'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
pluginDetail: PluginDetail
|
||||||
data: EndpointListItem
|
data: EndpointListItem
|
||||||
handleChange: () => void
|
handleChange: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const EndpointCard = ({
|
const EndpointCard = ({
|
||||||
|
pluginDetail,
|
||||||
data,
|
data,
|
||||||
handleChange,
|
handleChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@ -206,10 +208,11 @@ const EndpointCard = ({
|
|||||||
)}
|
)}
|
||||||
{isShowEndpointModal && (
|
{isShowEndpointModal && (
|
||||||
<EndpointModal
|
<EndpointModal
|
||||||
formSchemas={formSchemas}
|
formSchemas={formSchemas as any}
|
||||||
defaultValues={formValue}
|
defaultValues={formValue}
|
||||||
onCancel={hideEndpointModalConfirm}
|
onCancel={hideEndpointModalConfirm}
|
||||||
onSaved={handleUpdate}
|
onSaved={handleUpdate}
|
||||||
|
pluginDetail={pluginDetail}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -102,14 +102,16 @@ const EndpointList = ({ detail }: Props) => {
|
|||||||
key={index}
|
key={index}
|
||||||
data={item}
|
data={item}
|
||||||
handleChange={() => invalidateEndpointList(detail.plugin_id)}
|
handleChange={() => invalidateEndpointList(detail.plugin_id)}
|
||||||
|
pluginDetail={detail}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{isShowEndpointModal && (
|
{isShowEndpointModal && (
|
||||||
<EndpointModal
|
<EndpointModal
|
||||||
formSchemas={formSchemas}
|
formSchemas={formSchemas as any}
|
||||||
onCancel={hideEndpointModal}
|
onCancel={hideEndpointModal}
|
||||||
onSaved={handleCreate}
|
onSaved={handleCreate}
|
||||||
|
pluginDetail={detail}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,12 +10,16 @@ import Form from '@/app/components/header/account-setting/model-provider-page/mo
|
|||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
import cn from '@/utils/classnames'
|
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 = {
|
type Props = {
|
||||||
formSchemas: any
|
formSchemas: FormSchema[]
|
||||||
defaultValues?: any
|
defaultValues?: any
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onSaved: (value: Record<string, any>) => void
|
onSaved: (value: Record<string, any>) => void
|
||||||
|
pluginDetail: PluginDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractDefaultValues = (schemas: any[]) => {
|
const extractDefaultValues = (schemas: any[]) => {
|
||||||
@ -32,6 +36,7 @@ const EndpointModal: FC<Props> = ({
|
|||||||
defaultValues = {},
|
defaultValues = {},
|
||||||
onCancel,
|
onCancel,
|
||||||
onSaved,
|
onSaved,
|
||||||
|
pluginDetail,
|
||||||
}) => {
|
}) => {
|
||||||
const getValueFromI18nObject = useRenderI18nObject()
|
const getValueFromI18nObject = useRenderI18nObject()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -43,7 +48,7 @@ const EndpointModal: FC<Props> = ({
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
for (const field of formSchemas) {
|
for (const field of formSchemas) {
|
||||||
if (field.required && !tempCredential[field.name]) {
|
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<string, string>) }) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +89,7 @@ const EndpointModal: FC<Props> = ({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<div className='system-xs-regular mt-0.5 text-text-tertiary'>{t('plugin.detailPanel.endpointModalDesc')}</div>
|
<div className='system-xs-regular mt-0.5 text-text-tertiary'>{t('plugin.detailPanel.endpointModalDesc')}</div>
|
||||||
|
<ReadmeEntrance pluginDetail={pluginDetail} className='px-0 pt-3' />
|
||||||
</div>
|
</div>
|
||||||
<div className='grow overflow-y-auto'>
|
<div className='grow overflow-y-auto'>
|
||||||
<div className='px-4 py-2'>
|
<div className='px-4 py-2'>
|
||||||
@ -92,7 +98,7 @@ const EndpointModal: FC<Props> = ({
|
|||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
setTempCredential(v)
|
setTempCredential(v)
|
||||||
}}
|
}}
|
||||||
formSchemas={formSchemas}
|
formSchemas={formSchemas as any}
|
||||||
isEditMode={true}
|
isEditMode={true}
|
||||||
showOnVariableMap={{}}
|
showOnVariableMap={{}}
|
||||||
validating={false}
|
validating={false}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Drawer from '@/app/components/base/drawer'
|
|||||||
import { PluginCategoryEnum, type PluginDetail } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum, type PluginDetail } from '@/app/components/plugins/types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import ActionList from './action-list'
|
import ActionList from './action-list'
|
||||||
import AgentStrategyList from './agent-strategy-list'
|
import AgentStrategyList from './agent-strategy-list'
|
||||||
import DatasourceActionList from './datasource-action-list'
|
import DatasourceActionList from './datasource-action-list'
|
||||||
@ -11,8 +11,9 @@ import DetailHeader from './detail-header'
|
|||||||
import EndpointList from './endpoint-list'
|
import EndpointList from './endpoint-list'
|
||||||
import ModelList from './model-list'
|
import ModelList from './model-list'
|
||||||
import { SubscriptionList } from './subscription-list'
|
import { SubscriptionList } from './subscription-list'
|
||||||
import { usePluginStore } from './subscription-list/store'
|
import { usePluginStore } from './store'
|
||||||
import { TriggerEventsList } from './trigger/event-list'
|
import { TriggerEventsList } from './trigger/event-list'
|
||||||
|
import { ReadmeEntrance } from '../readme-panel/entrance'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
detail?: PluginDetail
|
detail?: PluginDetail
|
||||||
@ -25,19 +26,22 @@ const PluginDetailPanel: FC<Props> = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onHide,
|
onHide,
|
||||||
}) => {
|
}) => {
|
||||||
const handleUpdate = (isDelete = false) => {
|
const handleUpdate = useCallback((isDelete = false) => {
|
||||||
if (isDelete)
|
if (isDelete)
|
||||||
onHide()
|
onHide()
|
||||||
onUpdate()
|
onUpdate()
|
||||||
}
|
}, [onHide, onUpdate])
|
||||||
|
|
||||||
const { setDetail } = usePluginStore()
|
const { setDetail } = usePluginStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDetail(!detail ? undefined : {
|
setDetail(!detail ? undefined : {
|
||||||
plugin_id: detail.plugin_id,
|
plugin_id: detail.plugin_id,
|
||||||
provider: `${detail.plugin_id}/${detail.declaration.name}`,
|
provider: `${detail.plugin_id}/${detail.declaration.name}`,
|
||||||
|
plugin_unique_identifier: detail.plugin_unique_identifier || '',
|
||||||
declaration: detail.declaration,
|
declaration: detail.declaration,
|
||||||
name: detail.name,
|
name: detail.name,
|
||||||
|
id: detail.id,
|
||||||
})
|
})
|
||||||
}, [detail])
|
}, [detail])
|
||||||
|
|
||||||
@ -56,23 +60,24 @@ const PluginDetailPanel: FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{detail && (
|
{detail && (
|
||||||
<>
|
<>
|
||||||
<DetailHeader
|
<DetailHeader detail={detail} onUpdate={handleUpdate} onHide={onHide} />
|
||||||
detail={detail}
|
|
||||||
onHide={onHide}
|
|
||||||
onUpdate={handleUpdate}
|
|
||||||
/>
|
|
||||||
<div className='grow overflow-y-auto'>
|
<div className='grow overflow-y-auto'>
|
||||||
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
<div className='flex min-h-full flex-col'>
|
||||||
<>
|
<div className='flex-1'>
|
||||||
<SubscriptionList />
|
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
||||||
<TriggerEventsList />
|
<>
|
||||||
</>
|
<SubscriptionList />
|
||||||
)}
|
<TriggerEventsList />
|
||||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
</>
|
||||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
)}
|
||||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||||
{!!detail.declaration.model && <ModelList detail={detail} />}
|
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||||
{!!detail.declaration.datasource && <DatasourceActionList detail={detail} />}
|
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||||
|
{!!detail.declaration.model && <ModelList detail={detail} />}
|
||||||
|
{!!detail.declaration.datasource && <DatasourceActionList detail={detail} />}
|
||||||
|
</div>
|
||||||
|
<ReadmeEntrance pluginDetail={detail} className='mt-auto' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
14
web/app/components/plugins/plugin-detail-panel/store.ts
Normal file
14
web/app/components/plugins/plugin-detail-panel/store.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import type { PluginDetail } from '../types'
|
||||||
|
|
||||||
|
export type SimpleDetail = Pick<PluginDetail, 'plugin_id' | 'declaration' | 'name' | 'plugin_unique_identifier' | 'id'> & { provider: string }
|
||||||
|
|
||||||
|
type Shape = {
|
||||||
|
detail: SimpleDetail | undefined
|
||||||
|
setDetail: (detail?: SimpleDetail) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePluginStore = create<Shape>(set => ({
|
||||||
|
detail: undefined,
|
||||||
|
setDetail: (detail?: SimpleDetail) => set({ detail }),
|
||||||
|
}))
|
||||||
@ -23,7 +23,8 @@ import { debounce } from 'lodash-es'
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import LogViewer from '../log-viewer'
|
import LogViewer from '../log-viewer'
|
||||||
import { usePluginStore, usePluginSubscriptionStore } from '../store'
|
import { usePluginSubscriptionStore } from '../store'
|
||||||
|
import { usePluginStore } from '../../store'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { useBoolean } from 'ahooks'
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SupportedCreationMethods } from '../../../types'
|
import { SupportedCreationMethods } from '../../../types'
|
||||||
import { usePluginStore } from '../store'
|
import { usePluginStore } from '../../store'
|
||||||
import { useSubscriptionList } from '../use-subscription-list'
|
import { useSubscriptionList } from '../use-subscription-list'
|
||||||
import { CommonCreateModal } from './common-modal'
|
import { CommonCreateModal } from './common-modal'
|
||||||
import { OAuthClientSettingsModal } from './oauth-client'
|
import { OAuthClientSettingsModal } from './oauth-client'
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePluginStore } from '../store'
|
import { usePluginStore } from '../../store'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
oauthConfig?: TriggerOAuthConfig
|
oauthConfig?: TriggerOAuthConfig
|
||||||
|
|||||||
@ -1,17 +1,4 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { PluginDetail } from '../../types'
|
|
||||||
|
|
||||||
export type SimpleDetail = Pick<PluginDetail, 'plugin_id' | 'declaration' | 'name'> & { provider: string }
|
|
||||||
|
|
||||||
type Shape = {
|
|
||||||
detail: SimpleDetail | undefined
|
|
||||||
setDetail: (detail?: SimpleDetail) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePluginStore = create<Shape>(set => ({
|
|
||||||
detail: undefined,
|
|
||||||
setDetail: (detail?: SimpleDetail) => set({ detail }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
type ShapeSubscription = {
|
type ShapeSubscription = {
|
||||||
refresh?: () => void
|
refresh?: () => void
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||||
import { usePluginStore, usePluginSubscriptionStore } from './store'
|
import { usePluginStore } from '../store'
|
||||||
|
import { usePluginSubscriptionStore } from './store'
|
||||||
|
|
||||||
export const useSubscriptionList = () => {
|
export const useSubscriptionList = () => {
|
||||||
const detail = usePluginStore(state => state.detail)
|
const detail = usePluginStore(state => state.detail)
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import {
|
|||||||
AuthCategory,
|
AuthCategory,
|
||||||
PluginAuthInAgent,
|
PluginAuthInAgent,
|
||||||
} from '@/app/components/plugins/plugin-auth'
|
} from '@/app/components/plugins/plugin-auth'
|
||||||
|
import { ReadmeEntrance } from '../../readme-panel/entrance'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@ -272,7 +273,10 @@ const ToolSelector: FC<Props> = ({
|
|||||||
{/* base form */}
|
{/* base form */}
|
||||||
<div className='flex flex-col gap-3 px-4 py-2'>
|
<div className='flex flex-col gap-3 px-4 py-2'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
<div className='system-sm-semibold flex h-6 items-center justify-between text-text-secondary'>
|
||||||
|
{t('plugin.detailPanel.toolSelector.toolLabel')}
|
||||||
|
<ReadmeEntrance pluginDetail={currentProvider as any} showShortTip className='pb-0' />
|
||||||
|
</div>
|
||||||
<ToolPicker
|
<ToolPicker
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
offset={offset}
|
offset={offset}
|
||||||
@ -314,6 +318,7 @@ const ToolSelector: FC<Props> = ({
|
|||||||
pluginPayload={{
|
pluginPayload={{
|
||||||
provider: currentProvider.name,
|
provider: currentProvider.name,
|
||||||
category: AuthCategory.tool,
|
category: AuthCategory.tool,
|
||||||
|
detail: currentProvider as any,
|
||||||
}}
|
}}
|
||||||
credentialId={value?.credential_id}
|
credentialId={value?.credential_id}
|
||||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useTriggerProviderInfo } from '@/service/use-triggers'
|
|||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePluginStore } from '../subscription-list/store'
|
import { usePluginStore } from '../store'
|
||||||
import { EventDetailDrawer } from './event-detail-drawer'
|
import { EventDetailDrawer } from './event-detail-drawer'
|
||||||
|
|
||||||
type TriggerEventCardProps = {
|
type TriggerEventCardProps = {
|
||||||
|
|||||||
118
web/app/components/plugins/plugin-title-info/index.tsx
Normal file
118
web/app/components/plugins/plugin-title-info/index.tsx
Normal file
@ -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<string>, author?: string, name?: string, verified?: boolean }
|
||||||
|
size?: 'default' | 'large'
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginInfo: FC<PluginInfoProps> = ({
|
||||||
|
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 (
|
||||||
|
<div className={`flex items-center ${isLarge ? 'gap-3' : 'gap-2'}`}>
|
||||||
|
{/* Plugin Icon */}
|
||||||
|
<div className={`shrink-0 overflow-hidden rounded-lg border border-components-panel-border-subtle ${iconSize}`}>
|
||||||
|
<Icon src={icon?.startsWith('http') ? icon : `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plugin Details */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
{/* Name and Version */}
|
||||||
|
<div className="mb-0.5 flex items-center gap-1">
|
||||||
|
<h3 className={`truncate font-semibold text-text-secondary ${titleSize}`}>
|
||||||
|
{label?.[locale]}
|
||||||
|
</h3>
|
||||||
|
{verified && <RiVerifiedBadgeLine className="h-3 w-3 shrink-0 text-text-accent" />}
|
||||||
|
<Badge
|
||||||
|
className="mx-1"
|
||||||
|
uppercase={false}
|
||||||
|
text={version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organization and Source */}
|
||||||
|
<div className="flex items-center text-xs">
|
||||||
|
<OrgInfo
|
||||||
|
packageNameClassName="w-auto"
|
||||||
|
orgName={author}
|
||||||
|
packageName={name}
|
||||||
|
/>
|
||||||
|
<div className="ml-1 mr-0.5 text-text-quaternary">·</div>
|
||||||
|
|
||||||
|
{/* Source Icon */}
|
||||||
|
{source === PluginSource.marketplace && (
|
||||||
|
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.marketplace')}>
|
||||||
|
<div>
|
||||||
|
<BoxSparkleFill className="h-3.5 w-3.5 text-text-tertiary hover:text-text-accent" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{source === PluginSource.github && (
|
||||||
|
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.github')}>
|
||||||
|
<div>
|
||||||
|
<Github className="h-3.5 w-3.5 text-text-secondary hover:text-text-primary" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{source === PluginSource.local && (
|
||||||
|
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.local')}>
|
||||||
|
<div>
|
||||||
|
<RiHardDrive3Line className="h-3.5 w-3.5 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{source === PluginSource.debugging && (
|
||||||
|
<Tooltip popupContent={t('plugin.detailPanel.categoryTip.debugging')}>
|
||||||
|
<div>
|
||||||
|
<RiBugLine className="h-3.5 w-3.5 text-text-tertiary hover:text-text-warning" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginInfo
|
||||||
6
web/app/components/plugins/readme-panel/constants.ts
Normal file
6
web/app/components/plugins/readme-panel/constants.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const BUILTIN_TOOLS_ARRAY = [
|
||||||
|
'code',
|
||||||
|
'audio',
|
||||||
|
'time',
|
||||||
|
'webscraper',
|
||||||
|
]
|
||||||
49
web/app/components/plugins/readme-panel/entrance.tsx
Normal file
49
web/app/components/plugins/readme-panel/entrance.tsx
Normal file
@ -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 (
|
||||||
|
<div className={cn('flex flex-col items-start justify-center gap-2 pb-4 pt-0', showType === ReadmeShowType.drawer && 'px-4', className)}>
|
||||||
|
{!showShortTip && <div className="relative h-1 w-8 shrink-0">
|
||||||
|
<div className="h-px w-full bg-divider-regular"></div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleReadmeClick}
|
||||||
|
className="flex w-full items-center justify-start gap-1 text-text-tertiary transition-opacity hover:text-text-accent-light-mode-only"
|
||||||
|
>
|
||||||
|
<div className="relative flex h-3 w-3 items-center justify-center overflow-hidden">
|
||||||
|
<RiBookReadLine className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-normal leading-4">
|
||||||
|
{!showShortTip ? t('plugin.readmeInfo.needHelpCheckReadme') : t('plugin.readmeInfo.title')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
web/app/components/plugins/readme-panel/index.tsx
Normal file
127
web/app/components/plugins/readme-panel/index.tsx
Normal file
@ -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 = (
|
||||||
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
|
<div className="bg-background-body px-4 py-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<RiBookReadLine className="h-3 w-3 text-text-tertiary" />
|
||||||
|
<span className="text-xs font-medium uppercase text-text-tertiary">
|
||||||
|
{t('plugin.readmeInfo.title')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ActionButton onClick={onClose}>
|
||||||
|
<RiCloseLine className='h-4 w-4' />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PluginTitleInfo detail={detail} size="large" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
|
{(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-40 items-center justify-center">
|
||||||
|
<Loading type="area" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="py-8 text-center text-text-tertiary">
|
||||||
|
<p>{t('plugin.readmeInfo.noReadmeAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readmeData?.readme) {
|
||||||
|
return (
|
||||||
|
<Markdown
|
||||||
|
content={readmeData.readme}
|
||||||
|
className="prose-sm prose max-w-none"
|
||||||
|
pluginUniqueIdentifier={pluginUniqueIdentifier}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-8 text-center text-text-tertiary">
|
||||||
|
<p>{t('plugin.readmeInfo.noReadmeAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
showType === ReadmeShowType.drawer ? (
|
||||||
|
<Drawer
|
||||||
|
isOpen={!!detail}
|
||||||
|
onClose={onClose}
|
||||||
|
footer={null}
|
||||||
|
positionCenter={false}
|
||||||
|
showClose={false}
|
||||||
|
panelClassName={cn(
|
||||||
|
'mb-2 ml-2 mt-16 !w-[600px] !max-w-[600px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl',
|
||||||
|
'!z-[9999]',
|
||||||
|
)}
|
||||||
|
dialogClassName={cn('!z-[9998]')}
|
||||||
|
containerClassName='!justify-start'
|
||||||
|
noOverlay
|
||||||
|
clickOutsideNotOpen={false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
) : (
|
||||||
|
<Modal
|
||||||
|
isShow={!!detail}
|
||||||
|
onClose={onClose}
|
||||||
|
overlayOpacity={true}
|
||||||
|
className='h-[calc(100vh-16px)] max-w-[800px] p-0'
|
||||||
|
wrapperClassName='!z-[10000]'
|
||||||
|
containerClassName='p-2'
|
||||||
|
clickOutsideNotClose={true}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadmePanel
|
||||||
25
web/app/components/plugins/readme-panel/store.ts
Normal file
25
web/app/components/plugins/readme-panel/store.ts
Normal file
@ -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<Shape>(set => ({
|
||||||
|
currentPluginDetail: undefined,
|
||||||
|
setCurrentPluginDetail: (detail?: PluginDetail, showType?: ReadmeShowType) => set({
|
||||||
|
currentPluginDetail: !detail ? undefined : {
|
||||||
|
detail,
|
||||||
|
showType: showType ?? ReadmeShowType.drawer,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
@ -35,7 +35,7 @@ const Empty = ({
|
|||||||
const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title`
|
const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-[336px] flex-col items-center justify-center'>
|
<div className='flex flex-col items-center justify-center'>
|
||||||
<NoToolPlaceholder className={theme === 'dark' ? 'invert' : ''} />
|
<NoToolPlaceholder className={theme === 'dark' ? 'invert' : ''} />
|
||||||
<div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'>
|
<div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'>
|
||||||
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
|
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow'
|
import { useEdges, useNodes } from 'reactflow'
|
||||||
import { RiApps2AddLine } from '@remixicon/react'
|
import { RiApps2AddLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
@ -37,6 +37,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { useIsChatMode } from '../../hooks'
|
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 FeaturesTrigger = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -51,11 +53,12 @@ const FeaturesTrigger = () => {
|
|||||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||||
const toolPublished = useStore(s => s.toolPublished)
|
const toolPublished = useStore(s => s.toolPublished)
|
||||||
const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput)
|
const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput)
|
||||||
const startVariables = useReactflowStore(
|
|
||||||
s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables,
|
|
||||||
)
|
|
||||||
const nodes = useNodes<CommonNodeType>()
|
const nodes = useNodes<CommonNodeType>()
|
||||||
|
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||||
|
const startVariables = (startNode as Node<StartNodeType>)?.data?.variables
|
||||||
const edges = useEdges<CommonEdgeType>()
|
const edges = useEdges<CommonEdgeType>()
|
||||||
|
|
||||||
const fileSettings = useFeatures(s => s.features.file)
|
const fileSettings = useFeatures(s => s.features.file)
|
||||||
const variables = useMemo(() => {
|
const variables = useMemo(() => {
|
||||||
const data = startVariables || []
|
const data = startVariables || []
|
||||||
@ -185,6 +188,7 @@ const FeaturesTrigger = () => {
|
|||||||
onToggle: onPublisherToggle,
|
onToggle: onPublisherToggle,
|
||||||
workflowToolAvailable: lastPublishedHasUserInput,
|
workflowToolAvailable: lastPublishedHasUserInput,
|
||||||
crossAxisOffset: 4,
|
crossAxisOffset: 4,
|
||||||
|
missingStartNode: !startNode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -78,6 +78,7 @@ import PanelWrap from '../before-run-form/panel-wrap'
|
|||||||
import LastRun from './last-run'
|
import LastRun from './last-run'
|
||||||
import useLastRun from './last-run/use-last-run'
|
import useLastRun from './last-run/use-last-run'
|
||||||
import { TriggerSubscription } from './trigger-subscription'
|
import { TriggerSubscription } from './trigger-subscription'
|
||||||
|
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||||
|
|
||||||
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
|
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
|
||||||
const nodeType = params.payload.type
|
const nodeType = params.payload.type
|
||||||
@ -492,6 +493,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
pluginPayload={{
|
pluginPayload={{
|
||||||
provider: currToolCollection?.name || '',
|
provider: currToolCollection?.name || '',
|
||||||
category: AuthCategory.tool,
|
category: AuthCategory.tool,
|
||||||
|
detail: currToolCollection as any,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||||
@ -503,6 +505,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
pluginPayload={{
|
pluginPayload={{
|
||||||
provider: currToolCollection?.name || '',
|
provider: currToolCollection?.name || '',
|
||||||
category: AuthCategory.tool,
|
category: AuthCategory.tool,
|
||||||
|
detail: currToolCollection as any,
|
||||||
}}
|
}}
|
||||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||||
credentialId={data.credential_id}
|
credentialId={data.credential_id}
|
||||||
@ -619,6 +622,9 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
{...passedLogParams}
|
{...passedLogParams}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{data.type === BlockEnum.Tool && <ReadmeEntrance pluginDetail={currToolCollection as any} className='mt-auto' />}
|
||||||
|
{data.type === BlockEnum.DataSource && <ReadmeEntrance pluginDetail={currentDataSource as any} className='mt-auto' />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 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 { 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 { 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 { 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 useConfig from '@/app/components/workflow/nodes/trigger-plugin/use-config'
|
||||||
import type { Node } from '@/app/components/workflow/types'
|
import type { Node } from '@/app/components/workflow/types'
|
||||||
|
|||||||
@ -309,6 +309,11 @@ const translation = {
|
|||||||
connectedWorkspace: 'Connected Workspace',
|
connectedWorkspace: 'Connected Workspace',
|
||||||
emptyAuth: 'Please configure authentication',
|
emptyAuth: 'Please configure authentication',
|
||||||
},
|
},
|
||||||
|
readmeInfo: {
|
||||||
|
title: 'README',
|
||||||
|
needHelpCheckReadme: 'Need help? Check the README.',
|
||||||
|
noReadmeAvailable: 'No README available',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
@ -309,6 +309,11 @@ const translation = {
|
|||||||
connectedWorkspace: '已连接的工作区',
|
connectedWorkspace: '已连接的工作区',
|
||||||
emptyAuth: '请配置凭据',
|
emptyAuth: '请配置凭据',
|
||||||
},
|
},
|
||||||
|
readmeInfo: {
|
||||||
|
title: 'README',
|
||||||
|
needHelpCheckReadme: '需要帮助?查看 README。',
|
||||||
|
noReadmeAvailable: 'README 文档不可用',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
@ -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<Blob>('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name } }),
|
||||||
|
enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user