fix(trigger): show subscription url & add readme in trigger plugin node

This commit is contained in:
yessenia 2025-10-29 14:35:12 +08:00
parent bc3421add8
commit db2c6678e4
10 changed files with 208 additions and 159 deletions

View File

@ -3,48 +3,11 @@
* Extracted from the main markdown renderer for modularity.
* Uses the ImageGallery component to display images.
*/
import React, { useEffect, useMemo, useState } from 'react'
import React from 'react'
import ImageGallery from '@/app/components/base/image-gallery'
import { getMarkdownImageURL } from './utils'
import { usePluginReadmeAsset } from '@/service/use-plugins'
import type { SimplePluginInfo } from '../markdown/react-markdown-wrapper'
type ImgProps = {
src: string
pluginInfo?: SimplePluginInfo
}
const Img: React.FC<ImgProps> = ({ src, pluginInfo }) => {
const { plugin_unique_identifier, plugin_id } = pluginInfo || {}
const { data: assetData } = usePluginReadmeAsset({ plugin_unique_identifier, file_name: src })
const [blobUrl, setBlobUrl] = useState<string>()
useEffect(() => {
if (!assetData) {
setBlobUrl(undefined)
return
}
const objectUrl = URL.createObjectURL(assetData)
setBlobUrl(objectUrl)
return () => {
URL.revokeObjectURL(objectUrl)
}
}, [assetData])
const imageUrl = useMemo(() => {
if (blobUrl)
return blobUrl
return getMarkdownImageURL(src, plugin_id)
}, [blobUrl, plugin_id, src])
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[imageUrl]} />
</div>
)
const Img = ({ src }: any) => {
return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div>
}
export default Img

View File

@ -5,9 +5,11 @@
export { default as AudioBlock } from './audio-block'
export { default as CodeBlock } from './code-block'
export * from './plugin-img'
export * from './plugin-paragraph'
export { default as Img } from './img'
export { default as Link } from './link'
export { default as Paragraph } from './paragraph'
export { default as Link } from './link'
export { default as PreCode } from './pre-code'
export { default as ScriptBlock } from './script-block'
export { default as VideoBlock } from './video-block'

View File

@ -3,69 +3,25 @@
* Extracted from the main markdown renderer for modularity.
* Handles special rendering for paragraphs that directly contain an image.
*/
import React, { useEffect, useMemo, useState } from 'react'
import React from 'react'
import ImageGallery from '@/app/components/base/image-gallery'
import { getMarkdownImageURL } from './utils'
import { usePluginReadmeAsset } from '@/service/use-plugins'
import type { SimplePluginInfo } from '../markdown/react-markdown-wrapper'
type ParagraphProps = {
pluginInfo?: SimplePluginInfo
node?: any
children?: React.ReactNode
}
const Paragraph: React.FC<ParagraphProps> = ({ pluginInfo, node, children }) => {
const { plugin_unique_identifier, plugin_id } = pluginInfo || {}
const childrenNode = node?.children as Array<any> | undefined
const firstChild = childrenNode?.[0]
const isImageParagraph = firstChild?.tagName === 'img'
const imageSrc = isImageParagraph ? firstChild?.properties?.src : undefined
const { data: assetData } = usePluginReadmeAsset({
plugin_unique_identifier,
file_name: isImageParagraph && imageSrc ? imageSrc : '',
})
const [blobUrl, setBlobUrl] = useState<string>()
useEffect(() => {
if (!assetData) {
setBlobUrl(undefined)
return
}
const objectUrl = URL.createObjectURL(assetData)
setBlobUrl(objectUrl)
return () => {
URL.revokeObjectURL(objectUrl)
}
}, [assetData])
const imageUrl = useMemo(() => {
if (blobUrl)
return blobUrl
if (isImageParagraph && imageSrc)
return getMarkdownImageURL(imageSrc, plugin_id)
return ''
}, [blobUrl, imageSrc, isImageParagraph, plugin_id])
if (isImageParagraph) {
const remainingChildren = Array.isArray(children) && children.length > 1 ? children.slice(1) : undefined
const Paragraph = (paragraph: any) => {
const { node }: any = paragraph
const children_node = node.children
if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[imageUrl]} />
{remainingChildren && (
<div className="mt-2">{remainingChildren}</div>
)}
<ImageGallery srcs={[children_node[0].properties.src]} />
{
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
<div className="mt-2">{paragraph.children.slice(1)}</div>
)
}
</div>
)
}
return <p>{children}</p>
return <p>{paragraph.children}</p>
}
export default Paragraph

View File

@ -0,0 +1,48 @@
/**
* @fileoverview Img component for rendering <img> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Uses the ImageGallery component to display images.
*/
import React, { useEffect, useMemo, useState } from 'react'
import ImageGallery from '@/app/components/base/image-gallery'
import { getMarkdownImageURL } from './utils'
import { usePluginReadmeAsset } from '@/service/use-plugins'
import type { SimplePluginInfo } from '../markdown/react-markdown-wrapper'
type ImgProps = {
src: string
pluginInfo?: SimplePluginInfo
}
export const PluginImg: React.FC<ImgProps> = ({ src, pluginInfo }) => {
const { pluginUniqueIdentifier, pluginId } = pluginInfo || {}
const { data: assetData } = usePluginReadmeAsset({ plugin_unique_identifier: pluginUniqueIdentifier, file_name: src })
const [blobUrl, setBlobUrl] = useState<string>()
useEffect(() => {
if (!assetData) {
setBlobUrl(undefined)
return
}
const objectUrl = URL.createObjectURL(assetData)
setBlobUrl(objectUrl)
return () => {
URL.revokeObjectURL(objectUrl)
}
}, [assetData])
const imageUrl = useMemo(() => {
if (blobUrl)
return blobUrl
return getMarkdownImageURL(src, pluginId)
}, [blobUrl, pluginId, src])
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[imageUrl]} />
</div>
)
}

View File

@ -0,0 +1,69 @@
/**
* @fileoverview Paragraph component for rendering <p> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Handles special rendering for paragraphs that directly contain an image.
*/
import ImageGallery from '@/app/components/base/image-gallery'
import { usePluginReadmeAsset } from '@/service/use-plugins'
import React, { useEffect, useMemo, useState } from 'react'
import type { SimplePluginInfo } from '../markdown/react-markdown-wrapper'
import { getMarkdownImageURL } from './utils'
type PluginParagraphProps = {
pluginInfo?: SimplePluginInfo
node?: any
children?: React.ReactNode
}
export const PluginParagraph: React.FC<PluginParagraphProps> = ({ pluginInfo, node, children }) => {
const { pluginUniqueIdentifier, pluginId } = pluginInfo || {}
const childrenNode = node?.children as Array<any> | undefined
const firstChild = childrenNode?.[0]
const isImageParagraph = firstChild?.tagName === 'img'
const imageSrc = isImageParagraph ? firstChild?.properties?.src : undefined
const { data: assetData } = usePluginReadmeAsset({
plugin_unique_identifier: pluginUniqueIdentifier,
file_name: isImageParagraph && imageSrc ? imageSrc : '',
})
const [blobUrl, setBlobUrl] = useState<string>()
useEffect(() => {
if (!assetData) {
setBlobUrl(undefined)
return
}
const objectUrl = URL.createObjectURL(assetData)
setBlobUrl(objectUrl)
return () => {
URL.revokeObjectURL(objectUrl)
}
}, [assetData])
const imageUrl = useMemo(() => {
if (blobUrl)
return blobUrl
if (isImageParagraph && imageSrc)
return getMarkdownImageURL(imageSrc, pluginId)
return ''
}, [blobUrl, imageSrc, isImageParagraph, pluginId])
if (isImageParagraph) {
const remainingChildren = Array.isArray(children) && children.length > 1 ? children.slice(1) : undefined
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[imageUrl]} />
{remainingChildren && (
<div className="mt-2">{remainingChildren}</div>
)}
</div>
)
}
return <p>{children}</p>
}

View File

@ -1,30 +1,20 @@
import ReactMarkdown from 'react-markdown'
import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw'
import { AudioBlock, Img, Link, MarkdownButton, MarkdownForm, Paragraph, PluginImg, PluginParagraph, ScriptBlock, ThinkBlock, VideoBlock } from '@/app/components/base/markdown-blocks'
import { ENABLE_SINGLE_DOLLAR_LATEX } from '@/config'
import AudioBlock from '@/app/components/base/markdown-blocks/audio-block'
import Img from '@/app/components/base/markdown-blocks/img'
import Link from '@/app/components/base/markdown-blocks/link'
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form'
import Paragraph from '@/app/components/base/markdown-blocks/paragraph'
import ScriptBlock from '@/app/components/base/markdown-blocks/script-block'
import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
import VideoBlock from '@/app/components/base/markdown-blocks/video-block'
import { customUrlTransform } from './markdown-utils'
import type { FC } from 'react'
import dynamic from 'next/dynamic'
import type { FC } from 'react'
import ReactMarkdown from 'react-markdown'
import RehypeKatex from 'rehype-katex'
import RehypeRaw from 'rehype-raw'
import RemarkBreaks from 'remark-breaks'
import RemarkGfm from 'remark-gfm'
import RemarkMath from 'remark-math'
import { customUrlTransform } from './markdown-utils'
const CodeBlock = dynamic(() => import('@/app/components/base/markdown-blocks/code-block'), { ssr: false })
export type SimplePluginInfo = {
pluginUniqueIdentifier: string
plugin_id: string
pluginId: string
}
export type ReactMarkdownWrapperProps = {
@ -70,11 +60,11 @@ export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{
code: CodeBlock,
img: (props: any) => <Img {...props} pluginInfo={pluginInfo} />,
img: (props: any) => pluginInfo ? <PluginImg {...props} pluginInfo={pluginInfo} /> : <Img {...props} />,
video: VideoBlock,
audio: AudioBlock,
a: Link,
p: (props: any) => <Paragraph {...props} pluginInfo={pluginInfo} />,
p: (props: any) => pluginInfo ? <PluginParagraph {...props} pluginInfo={pluginInfo} /> : <Paragraph {...props} />,
button: MarkdownButton,
form: MarkdownForm,
script: ScriptBlock as any,

View File

@ -1,5 +1,6 @@
'use client'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
import cn from '@/utils/classnames'
import {
@ -48,9 +49,19 @@ const SubscriptionCard = ({ data }: Props) => {
</div>
<div className='mt-1 flex items-center justify-between'>
<div className='system-xs-regular flex-1 truncate text-text-tertiary'>
{data.endpoint}
</div>
<Tooltip
disabled={!data.endpoint}
popupContent={data.endpoint && (
<div className='max-w-[320px] break-all'>
{data.endpoint}
</div>
)}
position='left'
>
<div className='system-xs-regular flex-1 truncate text-text-tertiary'>
{data.endpoint}
</div>
</Tooltip>
<div className="mx-2 text-xs text-text-tertiary opacity-30">·</div>
<div className='system-xs-regular shrink-0 text-text-tertiary'>
{data.workflows_in_use > 0 ? t('pluginTrigger.subscription.list.item.usedByNum', { num: data.workflows_in_use }) : t('pluginTrigger.subscription.list.item.noUsed')}

View File

@ -3,13 +3,12 @@ import ActionButton from '@/app/components/base/action-button'
import Loading from '@/app/components/base/loading'
import { Markdown } from '@/app/components/base/markdown'
import Modal from '@/app/components/base/modal'
import Drawer from '@/app/components/base/drawer'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { usePluginReadme } from '@/service/use-plugins'
import cn from '@/utils/classnames'
import { RiBookReadLine, RiCloseLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import DetailHeader from '../plugin-detail-panel/detail-header'
import { ReadmeShowType, useReadmePanelStore } from './store'
@ -34,7 +33,7 @@ const ReadmePanel: FC = () => {
const children = (
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="bg-background-body px-4 py-4">
<div className="rounded-t-xl 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" />
@ -71,7 +70,7 @@ const ReadmePanel: FC = () => {
return (
<Markdown
content={readmeData.readme}
pluginInfo={{ plugin_unique_identifier: pluginUniqueIdentifier, plugin_id: detail.plugin_id }}
pluginInfo={{ pluginUniqueIdentifier, pluginId: detail.plugin_id }}
/>
)
}
@ -86,38 +85,29 @@ const ReadmePanel: FC = () => {
</div>
)
return (
showType === ReadmeShowType.drawer ? (
<Drawer
isOpen={!!detail}
onClose={onClose}
footer={null}
positionCenter={false}
showClose={false}
panelClassName={cn(
'!pointer-events-auto 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]',
return showType === ReadmeShowType.drawer ? createPortal(
<div className='pointer-events-none fixed inset-0 z-[9997] flex justify-start'>
<div
className={cn(
'pointer-events-auto mb-2 ml-2 mr-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',
)}
dialogClassName={cn('!pointer-events-none')}
containerClassName='!justify-start'
noOverlay
clickOutsideNotOpen={true}
>
{children}
</Drawer>
) : (
<Modal
isShow={!!detail}
onClose={onClose}
overlayOpacity={true}
className='h-[calc(100vh-16px)] max-w-[800px] p-0'
wrapperClassName='!z-[102]'
containerClassName='p-2'
clickOutsideNotClose={true}
>
{children}
</Modal>
)
</div>
</div>,
document.body,
) : (
<Modal
isShow={!!detail}
onClose={onClose}
overlayOpacity={true}
className='h-[calc(100vh-16px)] max-w-[800px] p-0'
wrapperClassName='!z-[102]'
containerClassName='p-2'
clickOutsideNotClose={true}
>
{children}
</Modal>
)
}

View File

@ -342,6 +342,25 @@ const BasePanel: FC<BasePanelProps> = ({
)
}, [handleNodeDataUpdateWithSyncDraft, id])
const readmeEntranceComponent = useMemo(() => {
let pluginDetail
switch (data.type) {
case BlockEnum.Tool:
pluginDetail = currToolCollection
break
case BlockEnum.DataSource:
pluginDetail = currentDataSource
break
case BlockEnum.TriggerPlugin:
pluginDetail = currentTriggerProvider
break
default:
break
}
return !pluginDetail ? null : <ReadmeEntrance pluginDetail={pluginDetail as any} className='mt-auto' />
}, [data.type, currToolCollection, currentDataSource, currentTriggerProvider])
if (logParams.showSpecialResultPanel) {
return (
<div className={cn(
@ -564,7 +583,7 @@ const BasePanel: FC<BasePanelProps> = ({
<Split />
</div>
{tabType === TabType.settings && (
<div className='flex-1 overflow-y-auto'>
<div className='flex flex-1 flex-col overflow-y-auto'>
<div>
{cloneElement(children as any, {
id,
@ -609,6 +628,7 @@ const BasePanel: FC<BasePanelProps> = ({
</div>
)
}
{readmeEntranceComponent}
</div>
)}
@ -628,8 +648,6 @@ const BasePanel: FC<BasePanelProps> = ({
/>
)}
{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>
)

View File

@ -4,6 +4,8 @@ import React from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import Loading from '@/app/components/base/loading'
import { AppModeEnum } from '@/types/app'
const Service: FC = () => {
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
@ -21,7 +23,7 @@ const Service: FC = () => {
const handleCreateApp = async () => {
await createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: 'chat',
mode: AppModeEnum.CHAT,
})
// reload app list
mutate({ url: '/apps', params: { page: 1 } })