mirror of
https://github.com/langgenius/dify.git
synced 2025-12-20 14:42:37 +00:00
Compare commits
6 Commits
feat/fallb
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93372de48 | ||
|
|
e981bf21a5 | ||
|
|
a015f05aea | ||
|
|
11f4743624 | ||
|
|
7db77cf9f8 | ||
|
|
19c10f9075 |
@@ -107,6 +107,22 @@ class PluginIconApi(Resource):
|
||||
icon_cache_max_age = dify_config.TOOL_ICON_CACHE_MAX_AGE
|
||||
return send_file(io.BytesIO(icon_bytes), mimetype=mimetype, max_age=icon_cache_max_age)
|
||||
|
||||
class PluginAssetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
req = reqparse.RequestParser()
|
||||
req.add_argument("plugin_unique_identifier", type=str, required=True, location="args")
|
||||
req.add_argument("file_name", type=str, required=True, location="args")
|
||||
args = req.parse_args()
|
||||
|
||||
tenant_id = current_user.current_tenant_id
|
||||
try:
|
||||
binary = PluginService.extract_asset(tenant_id, args["plugin_unique_identifier"], args["file_name"])
|
||||
return send_file(io.BytesIO(binary), mimetype="application/octet-stream")
|
||||
except PluginDaemonClientSideError as e:
|
||||
raise ValueError(e)
|
||||
|
||||
class PluginUploadFromPkgApi(Resource):
|
||||
@setup_required
|
||||
@@ -643,11 +659,34 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
|
||||
return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args["plugin_id"])})
|
||||
|
||||
|
||||
class PluginReadmeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
tenant_id = current_user.current_tenant_id
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("plugin_unique_identifier", type=str, required=True, location="args")
|
||||
parser.add_argument("language", type=str, required=False, location="args")
|
||||
args = parser.parse_args()
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"readme": PluginService.fetch_plugin_readme(
|
||||
tenant_id,
|
||||
args["plugin_unique_identifier"],
|
||||
args.get("language", "en-US")
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
|
||||
api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
|
||||
api.add_resource(PluginReadmeApi, "/workspaces/current/plugin/readme")
|
||||
api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions")
|
||||
api.add_resource(PluginListInstallationsFromIdsApi, "/workspaces/current/plugin/list/installations/ids")
|
||||
api.add_resource(PluginIconApi, "/workspaces/current/plugin/icon")
|
||||
api.add_resource(PluginAssetApi, "/workspaces/current/plugin/asset")
|
||||
api.add_resource(PluginUploadFromPkgApi, "/workspaces/current/plugin/upload/pkg")
|
||||
api.add_resource(PluginUploadFromGithubApi, "/workspaces/current/plugin/upload/github")
|
||||
api.add_resource(PluginUploadFromBundleApi, "/workspaces/current/plugin/upload/bundle")
|
||||
|
||||
@@ -196,3 +196,7 @@ class PluginListResponse(BaseModel):
|
||||
|
||||
class PluginDynamicSelectOptionsResponse(BaseModel):
|
||||
options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.")
|
||||
|
||||
class PluginReadmeResponse(BaseModel):
|
||||
content: str = Field(description="The readme of the plugin.")
|
||||
language: str = Field(description="The language of the readme.")
|
||||
|
||||
@@ -10,3 +10,9 @@ class PluginAssetManager(BasePluginClient):
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"can not found asset {id}")
|
||||
return response.content
|
||||
|
||||
def extract_asset(self, tenant_id: str, plugin_unique_identifier: str, filename: str) -> bytes:
|
||||
response = self._request(method="GET", path=f"plugin/{tenant_id}/asset/{plugin_unique_identifier}")
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"can not found asset {plugin_unique_identifier}, {str(response.status_code)}")
|
||||
return response.content
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
from core.plugin.entities.bundle import PluginBundleDependency
|
||||
from core.plugin.entities.plugin import (
|
||||
GenericProviderID,
|
||||
@@ -14,11 +16,34 @@ from core.plugin.entities.plugin_daemon import (
|
||||
PluginInstallTask,
|
||||
PluginInstallTaskStartResponse,
|
||||
PluginListResponse,
|
||||
PluginReadmeResponse,
|
||||
)
|
||||
from core.plugin.impl.base import BasePluginClient
|
||||
|
||||
|
||||
class PluginInstaller(BasePluginClient):
|
||||
def fetch_plugin_readme(self, tenant_id: str, plugin_unique_identifier: str, language: str) -> str:
|
||||
"""
|
||||
Fetch plugin readme
|
||||
"""
|
||||
try:
|
||||
response = self._request_with_plugin_daemon_response(
|
||||
"GET",
|
||||
f"plugin/{tenant_id}/management/fetch/readme",
|
||||
PluginReadmeResponse,
|
||||
params={
|
||||
"tenant_id":tenant_id,
|
||||
"plugin_unique_identifier": plugin_unique_identifier,
|
||||
"language": language
|
||||
}
|
||||
)
|
||||
return response.content
|
||||
except HTTPError as e:
|
||||
message = e.args[0]
|
||||
if "404" in message:
|
||||
return ""
|
||||
raise e
|
||||
|
||||
def fetch_plugin_by_identifier(
|
||||
self,
|
||||
tenant_id: str,
|
||||
|
||||
@@ -186,6 +186,11 @@ class PluginService:
|
||||
mime_type, _ = guess_type(asset_file)
|
||||
return manager.fetch_asset(tenant_id, asset_file), mime_type or "application/octet-stream"
|
||||
|
||||
@staticmethod
|
||||
def extract_asset(tenant_id: str, plugin_unique_identifier: str, file_name: str) -> bytes:
|
||||
manager = PluginAssetManager()
|
||||
return manager.extract_asset(tenant_id, plugin_unique_identifier, file_name)
|
||||
|
||||
@staticmethod
|
||||
def check_plugin_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
|
||||
"""
|
||||
@@ -492,3 +497,11 @@ class PluginService:
|
||||
"""
|
||||
manager = PluginInstaller()
|
||||
return manager.check_tools_existence(tenant_id, provider_ids)
|
||||
|
||||
@staticmethod
|
||||
def fetch_plugin_readme(tenant_id: str, plugin_unique_identifier: str, language: str) -> str:
|
||||
"""
|
||||
Fetch plugin readme
|
||||
"""
|
||||
manager = PluginInstaller()
|
||||
return manager.fetch_plugin_readme(tenant_id, plugin_unique_identifier, language)
|
||||
|
||||
@@ -8,6 +8,7 @@ import Header from '@/app/components/header'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { ReadmePanelProvider } from '@/app/components/plugins/readme-panel/context'
|
||||
import GotoAnything from '@/app/components/goto-anything'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
@@ -19,11 +20,13 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<ReadmePanelProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
{children}
|
||||
<GotoAnything />
|
||||
</ReadmePanelProvider>
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
AuthCategory,
|
||||
PluginAuthInAgent,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
|
||||
type Props = {
|
||||
showBackButton?: boolean
|
||||
@@ -212,6 +213,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
pluginPayload={{
|
||||
provider: collection.name,
|
||||
category: AuthCategory.tool,
|
||||
detail: collection as any,
|
||||
}}
|
||||
credentialId={credentialId}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
@@ -241,14 +243,15 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
)}
|
||||
<div className='h-0 grow overflow-y-auto px-4'>
|
||||
{isInfoActive ? infoUI : settingUI}
|
||||
</div>
|
||||
{!readonly && !isInfoActive && (
|
||||
<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'>
|
||||
<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>
|
||||
<ReadmeEntrance detail={collection as any} className='mt-auto' />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type IDrawerProps = {
|
||||
description?: string
|
||||
dialogClassName?: string
|
||||
dialogBackdropClassName?: string
|
||||
containerClassName?: string
|
||||
panelClassName?: string
|
||||
children: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
@@ -22,6 +23,7 @@ export type IDrawerProps = {
|
||||
onCancel?: () => void
|
||||
onOk?: () => void
|
||||
unmount?: boolean
|
||||
noOverlay?: boolean
|
||||
}
|
||||
|
||||
export default function Drawer({
|
||||
@@ -29,6 +31,7 @@ export default function Drawer({
|
||||
description = '',
|
||||
dialogClassName = '',
|
||||
dialogBackdropClassName = '',
|
||||
containerClassName = '',
|
||||
panelClassName = '',
|
||||
children,
|
||||
footer,
|
||||
@@ -41,6 +44,7 @@ export default function Drawer({
|
||||
onCancel,
|
||||
onOk,
|
||||
unmount = false,
|
||||
noOverlay = false,
|
||||
}: IDrawerProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
@@ -50,14 +54,14 @@ export default function Drawer({
|
||||
onClose={() => !clickOutsideNotOpen && onClose()}
|
||||
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
|
||||
>
|
||||
<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 */}
|
||||
<DialogBackdrop
|
||||
{!noOverlay && <DialogBackdrop
|
||||
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
|
||||
onClick={() => {
|
||||
!clickOutsideNotOpen && 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='flex justify-between'>
|
||||
|
||||
@@ -3,11 +3,34 @@
|
||||
* Extracted from the main markdown renderer for modularity.
|
||||
* Uses the ImageGallery component to display images.
|
||||
*/
|
||||
import React from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
import { getMarkdownImageURL } from './utils'
|
||||
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||
|
||||
const Img = ({ src }: any) => {
|
||||
return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div>
|
||||
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 (
|
||||
<div className='markdown-img-wrapper'>
|
||||
<ImageGallery srcs={[blobUrl]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Img
|
||||
|
||||
@@ -3,25 +3,43 @@
|
||||
* Extracted from the main markdown renderer for modularity.
|
||||
* Handles special rendering for paragraphs that directly contain an image.
|
||||
*/
|
||||
import React from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
// import { getMarkdownImageURL } from './utils'
|
||||
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||
|
||||
const Paragraph = (paragraph: any) => {
|
||||
const { node }: any = paragraph
|
||||
const Paragraph = (props: { pluginUniqueIdentifier?: string, node?: any, children?: any }) => {
|
||||
const { node, pluginUniqueIdentifier, children } = props
|
||||
const children_node = node.children
|
||||
if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
|
||||
const { data: asset } = usePluginReadmeAsset({ plugin_unique_identifier: pluginUniqueIdentifier, file_name: children_node[0].properties?.src })
|
||||
const blobUrl = useMemo(() => {
|
||||
if (asset)
|
||||
return URL.createObjectURL(asset)
|
||||
|
||||
return ''
|
||||
}, [asset])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (blobUrl && asset)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}, [blobUrl])
|
||||
|
||||
if (children_node?.[0]?.tagName === 'img') {
|
||||
// const imageURL = getMarkdownImageURL(children_node[0].properties?.src, pluginUniqueIdentifier)
|
||||
return (
|
||||
<div className="markdown-img-wrapper">
|
||||
<ImageGallery srcs={[children_node[0].properties.src]} />
|
||||
<ImageGallery srcs={[blobUrl]} />
|
||||
{
|
||||
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
|
||||
<div className="mt-2">{paragraph.children.slice(1)}</div>
|
||||
Array.isArray(children) && children.length > 1 && (
|
||||
<div className="mt-2">{children.slice(1)}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <p>{paragraph.children}</p>
|
||||
return <p>{children}</p>
|
||||
}
|
||||
|
||||
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 => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ export type MarkdownProps = {
|
||||
className?: string
|
||||
customDisallowedElements?: string[]
|
||||
customComponents?: Record<string, React.ComponentType<any>>
|
||||
pluginUniqueIdentifier?: string
|
||||
}
|
||||
|
||||
export const Markdown = (props: MarkdownProps) => {
|
||||
const { customComponents = {} } = props
|
||||
const { customComponents = {}, pluginUniqueIdentifier } = props
|
||||
const latexContent = flow([
|
||||
preprocessThinkTag,
|
||||
preprocessLaTeX,
|
||||
@@ -76,11 +77,11 @@ export const Markdown = (props: MarkdownProps) => {
|
||||
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
|
||||
components={{
|
||||
code: CodeBlock,
|
||||
img: Img,
|
||||
img: props => <Img {...{ pluginUniqueIdentifier, ...props, src: props.src as string }} />,
|
||||
video: VideoBlock,
|
||||
audio: AudioBlock,
|
||||
a: Link,
|
||||
p: Paragraph,
|
||||
p: props => <Paragraph {...{ pluginUniqueIdentifier, ...props }} />,
|
||||
button: MarkdownButton,
|
||||
form: MarkdownForm,
|
||||
script: ScriptBlock as any,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { noop } from 'lodash-es'
|
||||
type IModal = {
|
||||
className?: string
|
||||
wrapperClassName?: string
|
||||
containerClassName?: string
|
||||
isShow: boolean
|
||||
onClose?: () => void
|
||||
title?: React.ReactNode
|
||||
@@ -16,11 +17,13 @@ type IModal = {
|
||||
closable?: boolean
|
||||
overflowVisible?: boolean
|
||||
highPriority?: boolean // For modals that need to appear above dropdowns
|
||||
noOverlay?: boolean
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
className,
|
||||
wrapperClassName,
|
||||
containerClassName,
|
||||
isShow,
|
||||
onClose = noop,
|
||||
title,
|
||||
@@ -29,18 +32,19 @@ export default function Modal({
|
||||
closable = false,
|
||||
overflowVisible = false,
|
||||
highPriority = false,
|
||||
noOverlay = false,
|
||||
}: IModal) {
|
||||
return (
|
||||
<Transition appear show={isShow} as={Fragment}>
|
||||
<Dialog as="div" className={classNames('relative', highPriority ? 'z-[1100]' : 'z-[60]', wrapperClassName)} onClose={onClose}>
|
||||
<TransitionChild>
|
||||
{!noOverlay && <TransitionChild>
|
||||
<div className={classNames(
|
||||
'fixed inset-0 bg-background-overlay',
|
||||
'duration-300 ease-in data-[closed]:opacity-0',
|
||||
'data-[enter]:opacity-100',
|
||||
'data-[leave]:opacity-0',
|
||||
)} />
|
||||
</TransitionChild>
|
||||
</TransitionChild>}
|
||||
|
||||
<div
|
||||
className="fixed inset-0 overflow-y-auto"
|
||||
@@ -49,7 +53,7 @@ export default function Modal({
|
||||
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>
|
||||
<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',
|
||||
|
||||
@@ -26,6 +26,7 @@ type ModalProps = {
|
||||
footerSlot?: React.ReactNode
|
||||
bottomSlot?: React.ReactNode
|
||||
disabled?: boolean
|
||||
containerClassName?: string
|
||||
}
|
||||
const Modal = ({
|
||||
onClose,
|
||||
@@ -44,6 +45,7 @@ const Modal = ({
|
||||
footerSlot,
|
||||
bottomSlot,
|
||||
disabled,
|
||||
containerClassName,
|
||||
}: ModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -79,7 +81,7 @@ const Modal = ({
|
||||
</div>
|
||||
{
|
||||
children && (
|
||||
<div className='px-6 py-3'>{children}</div>
|
||||
<div className={cn('px-6 py-3', containerClassName)}>{children}</div>
|
||||
)
|
||||
}
|
||||
<div className='flex justify-between p-6 pt-5'>
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
useGetPluginCredentialSchemaHook,
|
||||
useUpdatePluginCredentialHook,
|
||||
} from '../hooks/use-credential'
|
||||
import { ReadmeEntrance } from '../../readme-panel/entrance'
|
||||
import { ReadmeShowType } from '../../readme-panel/context'
|
||||
|
||||
export type ApiKeyModalProps = {
|
||||
pluginPayload: PluginPayload
|
||||
@@ -142,6 +144,7 @@ const ApiKeyModal = ({
|
||||
onExtraButtonClick={onRemove}
|
||||
disabled={disabled || isLoading || doingAction}
|
||||
>
|
||||
<ReadmeEntrance detail={pluginPayload.detail} showType={ReadmeShowType.modal} />
|
||||
{
|
||||
isLoading && (
|
||||
<div className='flex h-40 items-center justify-center'>
|
||||
|
||||
@@ -23,6 +23,8 @@ import type {
|
||||
} from '@/app/components/base/form/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ReadmeEntrance } from '../../readme-panel/entrance'
|
||||
import { ReadmeShowType } from '../../readme-panel/context'
|
||||
|
||||
type OAuthClientSettingsProps = {
|
||||
pluginPayload: PluginPayload
|
||||
@@ -154,8 +156,9 @@ const OAuthClientSettings = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
containerClassName='pt-0'
|
||||
>
|
||||
<>
|
||||
<ReadmeEntrance detail={pluginPayload.detail} showType={ReadmeShowType.modal} />
|
||||
<AuthForm
|
||||
formFromProps={form}
|
||||
ref={formRef}
|
||||
@@ -163,7 +166,6 @@ const OAuthClientSettings = ({
|
||||
defaultValues={editValues || defaultValues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PluginDetail } from '../types'
|
||||
|
||||
export enum AuthCategory {
|
||||
tool = 'tool',
|
||||
datasource = 'datasource',
|
||||
@@ -7,6 +9,7 @@ export enum AuthCategory {
|
||||
export type PluginPayload = {
|
||||
category: AuthCategory
|
||||
provider: string
|
||||
detail: PluginDetail
|
||||
}
|
||||
|
||||
export enum CredentialTypeEnum {
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
|
||||
import type { EndpointListItem } from '../types'
|
||||
import type { EndpointListItem, PluginDetail } from '../types'
|
||||
import EndpointModal from './endpoint-modal'
|
||||
import { NAME_FIELD } from './utils'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
@@ -22,11 +22,13 @@ import {
|
||||
} from '@/service/use-endpoints'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
data: EndpointListItem
|
||||
handleChange: () => void
|
||||
}
|
||||
|
||||
const EndpointCard = ({
|
||||
pluginDetail,
|
||||
data,
|
||||
handleChange,
|
||||
}: Props) => {
|
||||
@@ -210,6 +212,7 @@ const EndpointCard = ({
|
||||
defaultValues={formValue}
|
||||
onCancel={hideEndpointModalConfirm}
|
||||
onSaved={handleUpdate}
|
||||
pluginDetail={pluginDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
key={index}
|
||||
data={item}
|
||||
handleChange={() => invalidateEndpointList(detail.plugin_id)}
|
||||
pluginDetail={detail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -110,6 +111,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
formSchemas={formSchemas}
|
||||
onCancel={hideEndpointModal}
|
||||
onSaved={handleCreate}
|
||||
pluginDetail={detail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,15 @@ import Form from '@/app/components/header/account-setting/model-provider-page/mo
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ReadmeEntrance } from '../readme-panel/entrance'
|
||||
import type { PluginDetail } from '../types'
|
||||
|
||||
type Props = {
|
||||
formSchemas: any
|
||||
defaultValues?: any
|
||||
onCancel: () => void
|
||||
onSaved: (value: Record<string, any>) => void
|
||||
pluginDetail: PluginDetail
|
||||
}
|
||||
|
||||
const extractDefaultValues = (schemas: any[]) => {
|
||||
@@ -32,6 +35,7 @@ const EndpointModal: FC<Props> = ({
|
||||
defaultValues = {},
|
||||
onCancel,
|
||||
onSaved,
|
||||
pluginDetail,
|
||||
}) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
@@ -84,6 +88,7 @@ const EndpointModal: FC<Props> = ({
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-0.5 text-text-tertiary'>{t('plugin.detailPanel.endpointModalDesc')}</div>
|
||||
<ReadmeEntrance detail={pluginDetail} className='px-0 pt-3' />
|
||||
</div>
|
||||
<div className='grow overflow-y-auto'>
|
||||
<div className='px-4 py-2'>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import DetailHeader from './detail-header'
|
||||
import EndpointList from './endpoint-list'
|
||||
@@ -9,6 +9,7 @@ import AgentStrategyList from './agent-strategy-list'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ReadmeEntrance } from '../readme-panel/entrance'
|
||||
|
||||
type Props = {
|
||||
detail?: PluginDetail
|
||||
@@ -21,11 +22,11 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
onUpdate,
|
||||
onHide,
|
||||
}) => {
|
||||
const handleUpdate = (isDelete = false) => {
|
||||
const handleUpdate = useCallback((isDelete = false) => {
|
||||
if (isDelete)
|
||||
onHide()
|
||||
onUpdate()
|
||||
}
|
||||
}, [onHide, onUpdate])
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
@@ -42,17 +43,18 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<DetailHeader
|
||||
detail={detail}
|
||||
onHide={onHide}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
<DetailHeader detail={detail} onUpdate={handleUpdate} onHide={onHide} />
|
||||
<div className='grow overflow-y-auto'>
|
||||
<div className='flex min-h-full flex-col'>
|
||||
<div className='flex-1'>
|
||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||
{!!detail.declaration.model && <ModelList detail={detail} />}
|
||||
</div>
|
||||
<ReadmeEntrance detail={detail} className='mt-auto' />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
@@ -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<Props> = ({
|
||||
{/* base form */}
|
||||
<div className='flex flex-col gap-3 px-4 py-2'>
|
||||
<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 detail={currentProvider as any} showShortTip className='pb-0' />
|
||||
</div>
|
||||
<ToolPicker
|
||||
placement='bottom'
|
||||
offset={offset}
|
||||
@@ -314,6 +318,7 @@ const ToolSelector: FC<Props> = ({
|
||||
pluginPayload={{
|
||||
provider: currentProvider.name,
|
||||
category: AuthCategory.tool,
|
||||
detail: currentProvider as any,
|
||||
}}
|
||||
credentialId={value?.credential_id}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
|
||||
119
web/app/components/plugins/plugin-title-info/index.tsx
Normal file
119
web/app/components/plugins/plugin-title-info/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiBugLine,
|
||||
RiHardDrive3Line,
|
||||
RiVerifiedBadgeLine,
|
||||
} from '@remixicon/react'
|
||||
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
|
||||
import { Github } from '@/app/components/base/icons/src/public/common'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
import OrgInfo from '@/app/components/plugins/card/base/org-info'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
|
||||
type PluginInfoProps = {
|
||||
detail: PluginDetail
|
||||
size?: 'default' | 'large'
|
||||
}
|
||||
|
||||
const PluginInfo: FC<PluginInfoProps> = ({
|
||||
detail,
|
||||
size = 'default',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace } = useAppContext()
|
||||
const locale = useLanguage()
|
||||
|
||||
const tenant_id = currentWorkspace?.id
|
||||
const {
|
||||
version,
|
||||
source,
|
||||
} = detail
|
||||
|
||||
const {
|
||||
icon,
|
||||
label,
|
||||
author,
|
||||
name,
|
||||
verified,
|
||||
} = detail.declaration || detail
|
||||
|
||||
const isLarge = size === 'large'
|
||||
const iconSize = isLarge ? 'h-10 w-10' : 'h-8 w-8'
|
||||
const titleSize = isLarge ? 'text-sm' : 'text-xs'
|
||||
|
||||
return (
|
||||
<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={`${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',
|
||||
]
|
||||
63
web/app/components/plugins/readme-panel/context.tsx
Normal file
63
web/app/components/plugins/readme-panel/context.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import ReadmeDrawer from './index'
|
||||
|
||||
type ReadmePanelContextValue = {
|
||||
openReadme: (detail: PluginDetail, showType?: ReadmeShowType) => void
|
||||
closeReadme: () => void
|
||||
currentDetailInfo?: {
|
||||
detail: PluginDetail
|
||||
showType: ReadmeShowType
|
||||
}
|
||||
}
|
||||
|
||||
const ReadmePanelContext = createContext<ReadmePanelContextValue | null>(null)
|
||||
|
||||
export const useReadmePanel = (): ReadmePanelContextValue => {
|
||||
const context = useContext(ReadmePanelContext)
|
||||
if (!context)
|
||||
throw new Error('useReadmePanel must be used within ReadmePanelProvider')
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type ReadmePanelProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export enum ReadmeShowType {
|
||||
drawer = 'drawer',
|
||||
modal = 'modal',
|
||||
}
|
||||
|
||||
export const ReadmePanelProvider: FC<ReadmePanelProviderProps> = ({ children }) => {
|
||||
const [currentDetailInfo, setCurrentDetailInfo] = useState<{
|
||||
detail: PluginDetail
|
||||
showType: ReadmeShowType
|
||||
} | undefined>()
|
||||
|
||||
const openReadme = (detail: PluginDetail, showType?: ReadmeShowType) => {
|
||||
setCurrentDetailInfo({
|
||||
detail,
|
||||
showType: showType || ReadmeShowType.drawer,
|
||||
})
|
||||
}
|
||||
|
||||
const closeReadme = () => {
|
||||
setCurrentDetailInfo(undefined)
|
||||
}
|
||||
|
||||
// todo: use zustand
|
||||
return (
|
||||
<ReadmePanelContext.Provider value={{
|
||||
openReadme,
|
||||
closeReadme,
|
||||
currentDetailInfo,
|
||||
}}>
|
||||
{children}
|
||||
<ReadmeDrawer />
|
||||
</ReadmePanelContext.Provider>
|
||||
)
|
||||
}
|
||||
48
web/app/components/plugins/readme-panel/entrance.tsx
Normal file
48
web/app/components/plugins/readme-panel/entrance.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookReadLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { PluginDetail } from '../types'
|
||||
import { ReadmeShowType, useReadmePanel } from './context'
|
||||
import { BUILTIN_TOOLS_ARRAY } from './constants'
|
||||
|
||||
export const ReadmeEntrance = ({
|
||||
detail,
|
||||
showType = ReadmeShowType.drawer,
|
||||
className,
|
||||
showShortTip = false,
|
||||
}: {
|
||||
detail: PluginDetail
|
||||
showType?: ReadmeShowType
|
||||
className?: string
|
||||
showShortTip?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { openReadme } = useReadmePanel()
|
||||
const handleReadmeClick = () => {
|
||||
if (detail)
|
||||
openReadme(detail, showType)
|
||||
}
|
||||
if (BUILTIN_TOOLS_ARRAY.includes(detail.id))
|
||||
return null
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
123
web/app/components/plugins/readme-panel/index.tsx
Normal file
123
web/app/components/plugins/readme-panel/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookReadLine, RiCloseLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { usePluginReadme } from '@/service/use-plugins'
|
||||
// import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import PluginTitleInfo from '@/app/components/plugins/plugin-title-info'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { ReadmeShowType, useReadmePanel } from './context'
|
||||
|
||||
const ReadmePanel: FC = () => {
|
||||
const { currentDetailInfo, closeReadme: onClose } = useReadmePanel()
|
||||
const detail = currentDetailInfo?.detail
|
||||
const showType = currentDetailInfo?.showType
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
|
||||
const pluginUniqueIdentifier = detail?.plugin_unique_identifier || ''
|
||||
|
||||
const readmeLanguage = language === 'zh-Hans' ? undefined : language
|
||||
const { data: readmeData, isLoading, error } = usePluginReadme(
|
||||
{ plugin_unique_identifier: pluginUniqueIdentifier, language: readmeLanguage },
|
||||
)
|
||||
|
||||
if (!detail) return null
|
||||
|
||||
const children = (
|
||||
<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
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
) : (
|
||||
<Modal
|
||||
isShow={!!detail}
|
||||
onClose={onClose}
|
||||
noOverlay
|
||||
className='h-[calc(100vh-16px)] max-w-[800px] p-0'
|
||||
wrapperClassName='!z-[10000]'
|
||||
containerClassName='p-2'
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default ReadmePanel
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
@@ -169,11 +170,11 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if(data._singleRunningStatus === NodeRunningStatus.Running) {
|
||||
if (data._singleRunningStatus === NodeRunningStatus.Running) {
|
||||
hasClickRunning.current = true
|
||||
setIsPaused(false)
|
||||
}
|
||||
else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
|
||||
else if (data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
|
||||
setIsPaused(true)
|
||||
hasClickRunning.current = false
|
||||
}
|
||||
@@ -248,7 +249,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
})
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
if(logParams.showSpecialResultPanel) {
|
||||
if (logParams.showSpecialResultPanel) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
@@ -340,7 +341,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if(isSingleRunning) {
|
||||
if (isSingleRunning) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
@@ -387,6 +388,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
pluginPayload={{
|
||||
provider: currCollection?.name || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currCollection as any,
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
@@ -398,6 +400,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
pluginPayload={{
|
||||
provider: currCollection?.name || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currCollection as any,
|
||||
}}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
credentialId={data.credential_id}
|
||||
@@ -483,6 +486,8 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
{...passedLogParams}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.type === BlockEnum.Tool && <ReadmeEntrance detail={currCollection as any} className='mt-auto' />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -298,6 +298,11 @@ const translation = {
|
||||
clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use',
|
||||
oauthClient: 'OAuth Client',
|
||||
},
|
||||
readmeInfo: {
|
||||
title: 'README',
|
||||
needHelpCheckReadme: 'Need help? Check the README.',
|
||||
noReadmeAvailable: 'No README available',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -298,6 +298,11 @@ const translation = {
|
||||
clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用',
|
||||
oauthClient: 'OAuth 客户端',
|
||||
},
|
||||
readmeInfo: {
|
||||
title: 'README',
|
||||
needHelpCheckReadme: '需要帮助?查看 README。',
|
||||
noReadmeAvailable: 'README 文档不可用',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -626,3 +626,19 @@ export const useFetchDynamicOptions = (plugin_id: string, provider: string, acti
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin_unique_identifier: string, language?: string }) => {
|
||||
return useQuery({
|
||||
queryKey: ['pluginReadme', plugin_unique_identifier, language],
|
||||
queryFn: () => get<{ readme: string }>('/workspaces/current/plugin/readme', { params: { plugin_unique_identifier, language } }),
|
||||
enabled: !!plugin_unique_identifier,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => {
|
||||
return useQuery({
|
||||
queryKey: ['pluginReadmeAsset', plugin_unique_identifier, file_name],
|
||||
queryFn: () => get<Blob>('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name } }),
|
||||
enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user