mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 23:15:59 +08:00
Merge branch 'feat/plugins' of github.com:langgenius/dify into feat/plugins
This commit is contained in:
commit
a387ff1c38
45
web/app/components/plugins/marketplace/context.tsx
Normal file
45
web/app/components/plugins/marketplace/context.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContextSelector,
|
||||||
|
} from 'use-context-selector'
|
||||||
|
|
||||||
|
export type MarketplaceContextValue = {
|
||||||
|
intersected: boolean
|
||||||
|
setIntersected: (intersected: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarketplaceContext = createContext<MarketplaceContextValue>({
|
||||||
|
intersected: true,
|
||||||
|
setIntersected: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
type MarketplaceContextProviderProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) {
|
||||||
|
return useContextSelector(MarketplaceContext, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarketplaceContextProvider = ({
|
||||||
|
children,
|
||||||
|
}: MarketplaceContextProviderProps) => {
|
||||||
|
const [intersected, setIntersected] = useState(true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarketplaceContext.Provider
|
||||||
|
value={{
|
||||||
|
intersected,
|
||||||
|
setIntersected,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MarketplaceContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
@ -1,39 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import IntersectionLine from '../intersection-line'
|
|
||||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
|
||||||
|
|
||||||
type DescriptionWrapperProps = {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
const DescriptionWrapper = ({
|
|
||||||
children,
|
|
||||||
}: DescriptionWrapperProps) => {
|
|
||||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
|
||||||
const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled)
|
|
||||||
|
|
||||||
const handleScrollIntersectionChange = useCallback((isIntersecting: boolean) => {
|
|
||||||
if (!isIntersecting && !scrollDisabled) {
|
|
||||||
setScrollDisabled(true)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (containerRef && containerRef.current)
|
|
||||||
containerRef.current.scrollTop = 0
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
}, [containerRef, scrollDisabled, setScrollDisabled])
|
|
||||||
|
|
||||||
return !scrollDisabled && (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<IntersectionLine
|
|
||||||
containerRef={containerRef}
|
|
||||||
intersectedCallback={handleScrollIntersectionChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DescriptionWrapper
|
|
@ -1,20 +0,0 @@
|
|||||||
import Description from '../description'
|
|
||||||
import DescriptionWrapper from '../description/wrapper'
|
|
||||||
import SearchBoxWrapper from '../search-box/wrapper'
|
|
||||||
import PluginTypeSwitch from '../plugin-type-switch'
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DescriptionWrapper>
|
|
||||||
<Description />
|
|
||||||
</DescriptionWrapper>
|
|
||||||
<div className='flex items-center justify-center mt-[15px] mb-4'>
|
|
||||||
<SearchBoxWrapper />
|
|
||||||
</div>
|
|
||||||
<PluginTypeSwitch />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
@ -1,27 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
|
|
||||||
type HeaderWrapperProps = {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
const HeaderWrapper = ({
|
|
||||||
children,
|
|
||||||
}: HeaderWrapperProps) => {
|
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'py-10',
|
|
||||||
scrollDisabled && 'absolute left-1/2 -translate-x-1/2 -top-[100px] pb-3',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HeaderWrapper
|
|
@ -1,18 +1,19 @@
|
|||||||
import Header from './header'
|
import { MarketplaceContextProvider } from './context'
|
||||||
import HeaderWrapper from './header/wrapper'
|
import Description from './description'
|
||||||
|
import IntersectionLine from './intersection-line'
|
||||||
|
import SearchBox from './search-box'
|
||||||
|
import PluginTypeSwitch from './plugin-type-switch'
|
||||||
import List from './list'
|
import List from './list'
|
||||||
import ListWrapper from './list/wrapper'
|
|
||||||
|
|
||||||
const Marketplace = () => {
|
const Marketplace = () => {
|
||||||
return (
|
return (
|
||||||
<div className='grow relative flex flex-col w-full h-0'>
|
<MarketplaceContextProvider>
|
||||||
<HeaderWrapper>
|
<Description />
|
||||||
<Header />
|
<IntersectionLine />
|
||||||
</HeaderWrapper>
|
<SearchBox />
|
||||||
<ListWrapper>
|
<PluginTypeSwitch />
|
||||||
<List />
|
<List />
|
||||||
</ListWrapper>
|
</MarketplaceContextProvider>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||||
|
import { useMarketplaceContext } from '@/app/components/plugins/marketplace/context'
|
||||||
|
|
||||||
export const useScrollIntersection = (
|
export const useScrollIntersection = (
|
||||||
containerRef: React.RefObject<HTMLDivElement>,
|
|
||||||
anchorRef: React.RefObject<HTMLDivElement>,
|
anchorRef: React.RefObject<HTMLDivElement>,
|
||||||
callback: (isIntersecting: boolean) => void,
|
|
||||||
) => {
|
) => {
|
||||||
|
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||||
|
const intersected = useMarketplaceContext(v => v.intersected)
|
||||||
|
const setIntersected = useMarketplaceContext(v => v.setIntersected)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let observer: IntersectionObserver | undefined
|
let observer: IntersectionObserver | undefined
|
||||||
if (containerRef?.current && anchorRef.current) {
|
if (containerRef?.current && anchorRef.current) {
|
||||||
observer = new IntersectionObserver((entries) => {
|
observer = new IntersectionObserver((entries) => {
|
||||||
const isIntersecting = entries[0].isIntersecting
|
const isIntersecting = entries[0].isIntersecting
|
||||||
callback(isIntersecting)
|
|
||||||
|
if (isIntersecting && !intersected)
|
||||||
|
setIntersected(true)
|
||||||
|
|
||||||
|
if (!isIntersecting && intersected)
|
||||||
|
setIntersected(false)
|
||||||
}, {
|
}, {
|
||||||
root: containerRef.current,
|
root: containerRef.current,
|
||||||
})
|
})
|
||||||
observer.observe(anchorRef.current)
|
observer.observe(anchorRef.current)
|
||||||
}
|
}
|
||||||
return () => observer?.disconnect()
|
return () => observer?.disconnect()
|
||||||
}, [containerRef, anchorRef, callback])
|
}, [containerRef, anchorRef, intersected, setIntersected])
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,13 @@
|
|||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useScrollIntersection } from './hooks'
|
import { useScrollIntersection } from './hooks'
|
||||||
|
|
||||||
type IntersectionLineProps = {
|
const IntersectionLine = () => {
|
||||||
containerRef: React.RefObject<HTMLDivElement>
|
|
||||||
intersectedCallback: (isIntersecting: boolean) => void
|
|
||||||
}
|
|
||||||
const IntersectionLine = ({
|
|
||||||
containerRef,
|
|
||||||
intersectedCallback,
|
|
||||||
}: IntersectionLineProps) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useScrollIntersection(
|
useScrollIntersection(ref)
|
||||||
containerRef,
|
|
||||||
ref,
|
|
||||||
intersectedCallback,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className='h-[1px] bg-transparent'></div>
|
<div ref={ref} className='mb-4 h-[1px] bg-transparent'></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ const List = () => {
|
|||||||
const locale = getLocaleOnServer()
|
const locale = getLocaleOnServer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='px-12 py-2 bg-background-default-subtle'>
|
||||||
<div className='py-3'>
|
<div className='py-3'>
|
||||||
<div className='title-xl-semi-bold text-text-primary'>Featured</div>
|
<div className='title-xl-semi-bold text-text-primary'>Featured</div>
|
||||||
<div className='system-xs-regular text-text-tertiary'>Our top picks to get you started</div>
|
<div className='system-xs-regular text-text-tertiary'>Our top picks to get you started</div>
|
||||||
@ -223,7 +223,7 @@ const List = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
|
|
||||||
type ListWrapperProps = {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
const ListWrapper = ({
|
|
||||||
children,
|
|
||||||
}: ListWrapperProps) => {
|
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
|
||||||
const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
scrollDisabled && (
|
|
||||||
<div className='h-[60px]'></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'px-12 py-2 bg-background-default-subtle',
|
|
||||||
scrollDisabled && 'grow h-0 overflow-y-auto',
|
|
||||||
)}
|
|
||||||
onScroll={(e) => {
|
|
||||||
if ((e.target as HTMLElement).scrollTop <= 0)
|
|
||||||
setScrollDisabled(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListWrapper
|
|
@ -43,7 +43,9 @@ const PluginTypeSwitch = ({
|
|||||||
const [activeType, setActiveType] = useState('all')
|
const [activeType, setActiveType] = useState('all')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center space-x-2'>
|
<div className={cn(
|
||||||
|
'sticky top-[60px] flex items-center justify-center py-3 bg-background-body space-x-2 z-10',
|
||||||
|
)}>
|
||||||
{
|
{
|
||||||
options.map(option => (
|
options.map(option => (
|
||||||
<div
|
<div
|
||||||
|
@ -5,18 +5,18 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useMarketplaceContext } from '../context'
|
||||||
import TagsFilter from './tags-filter'
|
import TagsFilter from './tags-filter'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type SearchBoxProps = {
|
type SearchBoxProps = {
|
||||||
onChange?: (searchText: string, tags: string[]) => void
|
onChange?: (searchText: string, tags: string[]) => void
|
||||||
widthShouldChange?: boolean
|
|
||||||
}
|
}
|
||||||
const SearchBox = ({
|
const SearchBox = ({
|
||||||
onChange,
|
onChange,
|
||||||
widthShouldChange,
|
|
||||||
}: SearchBoxProps) => {
|
}: SearchBoxProps) => {
|
||||||
|
const intersected = useMarketplaceContext(v => v.intersected)
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||||
|
|
||||||
@ -28,8 +28,8 @@ const SearchBox = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center p-1.5 w-[640px] h-11 border border-components-chat-input-border bg-components-panel-bg-blur rounded-xl shadow-md',
|
'sticky top-3 flex items-center m-auto p-1.5 w-[640px] h-11 border border-components-chat-input-border bg-components-panel-bg-blur rounded-xl shadow-md z-[11]',
|
||||||
widthShouldChange && 'w-[508px] transition-[width] duration-300',
|
!intersected && 'w-[508px] transition-[width] duration-300',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TagsFilter
|
<TagsFilter
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import SearchBox from '.'
|
|
||||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
|
||||||
|
|
||||||
const Wrapper = () => {
|
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SearchBox widthShouldChange={scrollDisabled} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Wrapper
|
|
@ -3,7 +3,6 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@ -12,14 +11,10 @@ import {
|
|||||||
|
|
||||||
export type PluginPageContextValue = {
|
export type PluginPageContextValue = {
|
||||||
containerRef: React.RefObject<HTMLDivElement>
|
containerRef: React.RefObject<HTMLDivElement>
|
||||||
scrollDisabled: boolean
|
|
||||||
setScrollDisabled: (scrollDisabled: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PluginPageContext = createContext<PluginPageContextValue>({
|
export const PluginPageContext = createContext<PluginPageContextValue>({
|
||||||
containerRef: { current: null },
|
containerRef: { current: null },
|
||||||
scrollDisabled: false,
|
|
||||||
setScrollDisabled: () => {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type PluginPageContextProviderProps = {
|
type PluginPageContextProviderProps = {
|
||||||
@ -34,14 +29,11 @@ export const PluginPageContextProvider = ({
|
|||||||
children,
|
children,
|
||||||
}: PluginPageContextProviderProps) => {
|
}: PluginPageContextProviderProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [scrollDisabled, setScrollDisabled] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PluginPageContext.Provider
|
<PluginPageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
containerRef,
|
containerRef,
|
||||||
scrollDisabled,
|
|
||||||
setScrollDisabled,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -36,7 +36,6 @@ const PluginPage = ({
|
|||||||
const { setShowPluginSettingModal } = useModalContext() as any
|
const { setShowPluginSettingModal } = useModalContext() as any
|
||||||
const [currentFile, setCurrentFile] = useState<File | null>(null)
|
const [currentFile, setCurrentFile] = useState<File | null>(null)
|
||||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
|
||||||
|
|
||||||
const { dragging, fileUploader, fileChangeHandle, removeFile } = useUploader({
|
const { dragging, fileUploader, fileChangeHandle, removeFile } = useUploader({
|
||||||
onFileChange: setCurrentFile,
|
onFileChange: setCurrentFile,
|
||||||
@ -60,12 +59,11 @@ const PluginPage = ({
|
|||||||
className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins'
|
className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins'
|
||||||
? 'rounded-t-xl bg-components-panel-bg'
|
? 'rounded-t-xl bg-components-panel-bg'
|
||||||
: 'bg-background-body',
|
: 'bg-background-body',
|
||||||
activeTab === 'discover' && scrollDisabled && 'overflow-hidden',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky top-0 flex min-h-[60px] px-12 pt-4 pb-2 items-center self-stretch gap-1',
|
'sticky top-0 flex min-h-[60px] px-12 pt-4 pb-2 items-center self-stretch gap-1 bg-background-body z-10',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex justify-between items-center w-full'>
|
<div className='flex justify-between items-center w-full'>
|
||||||
|
@ -12,6 +12,8 @@ import { useToolTabs } from './hooks'
|
|||||||
import ViewTypeSelect, { ViewType } from './view-type-select'
|
import ViewTypeSelect, { ViewType } from './view-type-select'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
|
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||||
|
import { extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
|
||||||
|
|
||||||
type AllToolsProps = {
|
type AllToolsProps = {
|
||||||
searchText: string
|
searchText: string
|
||||||
@ -71,6 +73,7 @@ const AllTools = ({
|
|||||||
</div>
|
</div>
|
||||||
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
|
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
|
||||||
</div>
|
</div>
|
||||||
|
<PluginList list={[toolNotion, extensionDallE, modelGPT4] as any} />
|
||||||
<Tools
|
<Tools
|
||||||
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
|
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
|
||||||
tools={tools}
|
tools={tools}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiMoreFill } from '@remixicon/react'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
|
// import Button from '@/app/components/base/button'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
}
|
||||||
|
|
||||||
|
const OperationDropdown: FC<Props> = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, doSetOpen] = useState(false)
|
||||||
|
const openRef = useRef(open)
|
||||||
|
const setOpen = useCallback((v: boolean) => {
|
||||||
|
doSetOpen(v)
|
||||||
|
openRef.current = v
|
||||||
|
}, [doSetOpen])
|
||||||
|
|
||||||
|
const handleTrigger = useCallback(() => {
|
||||||
|
setOpen(!openRef.current)
|
||||||
|
}, [setOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom-end'
|
||||||
|
offset={{
|
||||||
|
mainAxis: 0,
|
||||||
|
crossAxis: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||||
|
<ActionButton className={cn(open && 'bg-state-base-hover')}>
|
||||||
|
<RiMoreFill className='w-4 h-4 text-components-button-secondary-accent-text' />
|
||||||
|
</ActionButton>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-50'>
|
||||||
|
<div className='w-[112px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||||
|
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.download')}</div>
|
||||||
|
{/* Wait marketplace */}
|
||||||
|
{/* <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.viewDetail')}</div> */}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(OperationDropdown)
|
@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Action from './action'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types.ts'
|
||||||
|
import I18n from '@/context/i18n'
|
||||||
|
|
||||||
|
import { formatNumber } from '@/utils/format'
|
||||||
|
|
||||||
|
enum ActionType {
|
||||||
|
install = 'install',
|
||||||
|
download = 'download',
|
||||||
|
// viewDetail = 'viewDetail', // wait for marketplace api
|
||||||
|
}
|
||||||
|
type Props = {
|
||||||
|
payload: Plugin
|
||||||
|
onAction: (type: ActionType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item: FC<Props> = ({
|
||||||
|
payload,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { locale } = useContext(I18n)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex rounded-lg py-2 pr-1 pl-3 hover:bg-state-base-hover'>
|
||||||
|
<div
|
||||||
|
className='shrink-0 relative w-6 h-6 border-[0.5px] border-components-panel-border-subtle rounded-md bg-center bg-no-repeat bg-contain'
|
||||||
|
style={{ backgroundImage: `url(${payload.icon})` }}
|
||||||
|
/>
|
||||||
|
<div className='ml-2 w-0 grow flex'>
|
||||||
|
<div className='w-0 grow'>
|
||||||
|
<div className='h-4 leading-4 text-text-primary system-sm-medium truncate '>{payload.label[locale]}</div>
|
||||||
|
<div className='h-5 leading-5 text-text-tertiary system-xs-regular truncate'>{payload.brief[locale]}</div>
|
||||||
|
<div className='flex text-text-tertiary system-xs-regular space-x-1'>
|
||||||
|
<div>{payload.org}</div>
|
||||||
|
<div>·</div>
|
||||||
|
<div>{t('plugin.install', { num: formatNumber(payload.install_count || 0) })}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Action */}
|
||||||
|
<div className='flex items-center space-x-1 h-4 text-components-button-secondary-accent-text system-xs-medium'>
|
||||||
|
<div className='px-1.5'>{t('plugin.installAction')}</div>
|
||||||
|
<Action />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(Item)
|
@ -0,0 +1,35 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Item from './item'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types.ts'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
list: Plugin[]
|
||||||
|
// onInstall: () =>
|
||||||
|
}
|
||||||
|
|
||||||
|
const List: FC<Props> = ({
|
||||||
|
list,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='pt-3 px-4 py-1 text-text-primary system-sm-medium'>
|
||||||
|
{t('plugin.fromMarketplace')}
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<Item
|
||||||
|
key={index}
|
||||||
|
payload={item}
|
||||||
|
onAction={() => { }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(List)
|
@ -75,13 +75,13 @@ const ToolItem: FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center h-8'>
|
<div className='flex grow items-center h-8'>
|
||||||
<BlockIcon
|
<BlockIcon
|
||||||
className='shrink-0'
|
className='shrink-0'
|
||||||
type={BlockEnum.Tool}
|
type={BlockEnum.Tool}
|
||||||
toolIcon={provider.icon}
|
toolIcon={provider.icon}
|
||||||
/>
|
/>
|
||||||
<div className='ml-2 text-sm text-gray-900 flex-1 min-w-0 truncate'>{payload.label[language]}</div>
|
<div className='ml-2 text-sm text-gray-900 flex-1 w-0 grow truncate'>{payload.label[language]}</div>
|
||||||
</div>
|
</div>
|
||||||
{isToolPlugin && (
|
{isToolPlugin && (
|
||||||
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
|
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
from: 'From',
|
from: 'From',
|
||||||
|
fromMarketplace: 'From Marketplace',
|
||||||
endpointsEnabled: '{{num}} sets of endpoints enabled',
|
endpointsEnabled: '{{num}} sets of endpoints enabled',
|
||||||
detailPanel: {
|
detailPanel: {
|
||||||
operation: {
|
operation: {
|
||||||
@ -18,6 +19,8 @@ const translation = {
|
|||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
modelNum: '{{num}} MODELS INCLUDED',
|
modelNum: '{{num}} MODELS INCLUDED',
|
||||||
},
|
},
|
||||||
|
install: '{{num}} installs',
|
||||||
|
installAction: 'Install',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
@ -198,7 +198,7 @@ const translation = {
|
|||||||
'searchTool': 'Search tool',
|
'searchTool': 'Search tool',
|
||||||
'tools': 'Tools',
|
'tools': 'Tools',
|
||||||
'allTool': 'All',
|
'allTool': 'All',
|
||||||
'builtInTool': 'Built-in',
|
'plugin': 'Plugin',
|
||||||
'customTool': 'Custom',
|
'customTool': 'Custom',
|
||||||
'workflowTool': 'Workflow',
|
'workflowTool': 'Workflow',
|
||||||
'question-understand': 'Question Understand',
|
'question-understand': 'Question Understand',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
from: '来自',
|
from: '来自',
|
||||||
|
fromMarketplace: '来自市场',
|
||||||
endpointsEnabled: '{{num}} 组端点已启用',
|
endpointsEnabled: '{{num}} 组端点已启用',
|
||||||
detailPanel: {
|
detailPanel: {
|
||||||
operation: {
|
operation: {
|
||||||
@ -18,6 +19,8 @@ const translation = {
|
|||||||
disabled: '停用',
|
disabled: '停用',
|
||||||
modelNum: '{{num}} 模型已包含',
|
modelNum: '{{num}} 模型已包含',
|
||||||
},
|
},
|
||||||
|
install: '{{num}} 次安装',
|
||||||
|
installAction: '安装',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
Loading…
x
Reference in New Issue
Block a user