mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
refactor: stabilize selector preview cards (#36105)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
c34fc429ae
commit
03861bcee3
@ -2987,22 +2987,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/featured-tools.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/featured-triggers.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/hooks.ts": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
@ -3038,29 +3022,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/tool.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/item.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 4
|
||||
|
||||
@ -118,6 +118,23 @@ describe('ModelSelector', () => {
|
||||
expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('should use the default model settings popup width when the trigger is narrow', () => {
|
||||
renderWithQueryClient(
|
||||
<div className="w-[355px]">
|
||||
<ModelSelector modelList={[makeModel()]} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('combobox'))
|
||||
|
||||
expect(
|
||||
Array.from(document.body.querySelectorAll('[class]')).some(element =>
|
||||
element.className.includes('w-[432px]')
|
||||
&& element.className.includes('max-w-[432px]'),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should not open popup when readonly', () => {
|
||||
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} readonly />)
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { DefaultModel, Model, ModelItem } from '../../declarations'
|
||||
import { Combobox } from '@langgenius/dify-ui/combobox'
|
||||
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
@ -106,6 +107,11 @@ const makeProvider = (overrides: Record<string, unknown> = {}) => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const previewCardProps = () => ({
|
||||
previewCardHandle: createPreviewCardHandle(),
|
||||
onPreviewCardClose: vi.fn(),
|
||||
})
|
||||
|
||||
const createComboboxNode = (
|
||||
node: ReactElement,
|
||||
onValueChange = vi.fn(),
|
||||
@ -152,7 +158,7 @@ describe('PopupItem', () => {
|
||||
})
|
||||
|
||||
const { container } = renderWithCombobox(
|
||||
<PopupItem model={makeModel()} onHide={vi.fn()} />,
|
||||
<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />,
|
||||
)
|
||||
|
||||
expect(container.textContent).toBe('')
|
||||
@ -160,7 +166,7 @@ describe('PopupItem', () => {
|
||||
|
||||
it('should select the combobox value when clicking an active model', () => {
|
||||
const onValueChange = vi.fn()
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />, onValueChange)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />, onValueChange)
|
||||
|
||||
fireEvent.click(screen.getByText('GPT-4'))
|
||||
|
||||
@ -170,10 +176,27 @@ describe('PopupItem', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should close the shared preview before pressing an active model', () => {
|
||||
const onPreviewCardClose = vi.fn()
|
||||
renderWithCombobox(
|
||||
<PopupItem
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
onPreviewCardClose={onPreviewCardClose}
|
||||
model={makeModel()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.pointerDown(screen.getByText('GPT-4'))
|
||||
|
||||
expect(onPreviewCardClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not select the combobox value when model is not active', () => {
|
||||
const onValueChange = vi.fn()
|
||||
renderWithCombobox(
|
||||
<PopupItem
|
||||
{...previewCardProps()}
|
||||
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.disabled })] })}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
@ -188,7 +211,7 @@ describe('PopupItem', () => {
|
||||
it('should open model modal when clicking add on unconfigured model', () => {
|
||||
const onValueChange = vi.fn()
|
||||
const { rerender } = renderWithCombobox(
|
||||
<PopupItem model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
|
||||
<PopupItem {...previewCardProps()} model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
|
||||
onValueChange,
|
||||
)
|
||||
|
||||
@ -206,6 +229,7 @@ describe('PopupItem', () => {
|
||||
|
||||
rerender(createComboboxNode(
|
||||
<PopupItem
|
||||
{...previewCardProps()}
|
||||
model={makeModel({
|
||||
models: [makeModelItem({ status: ModelStatusEnum.noConfigure, model_type: undefined as unknown as ModelTypeEnum })],
|
||||
})}
|
||||
@ -225,6 +249,7 @@ describe('PopupItem', () => {
|
||||
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
|
||||
renderWithCombobox(
|
||||
<PopupItem
|
||||
{...previewCardProps()}
|
||||
defaultModel={defaultModel}
|
||||
model={makeModel()}
|
||||
onHide={vi.fn()}
|
||||
@ -239,6 +264,7 @@ describe('PopupItem', () => {
|
||||
|
||||
renderWithCombobox(
|
||||
<PopupItem
|
||||
{...previewCardProps()}
|
||||
model={makeModel({
|
||||
label: { en_US: 'OpenAI only' } as Model['label'],
|
||||
models: [makeModelItem({ label: { en_US: 'GPT-4 only' } as ModelItem['label'] })],
|
||||
@ -252,7 +278,7 @@ describe('PopupItem', () => {
|
||||
})
|
||||
|
||||
it('should toggle collapsed state when clicking provider header', () => {
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('GPT-4'))!.toBeInTheDocument()
|
||||
|
||||
@ -266,7 +292,7 @@ describe('PopupItem', () => {
|
||||
})
|
||||
|
||||
it('should show credential name when using custom provider', () => {
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('my-api-key'))!.toBeInTheDocument()
|
||||
})
|
||||
@ -283,7 +309,7 @@ describe('PopupItem', () => {
|
||||
credits: 200,
|
||||
})
|
||||
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('stale-key'))!.toBeInTheDocument()
|
||||
expect(document.querySelector('.bg-components-badge-status-light-error-bg')).not.toBeNull()
|
||||
@ -309,7 +335,7 @@ describe('PopupItem', () => {
|
||||
credits: 0,
|
||||
})
|
||||
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText(/modelProvider\.selector\.configureRequired/))!.toBeInTheDocument()
|
||||
})
|
||||
@ -331,7 +357,7 @@ describe('PopupItem', () => {
|
||||
credits: 200,
|
||||
})
|
||||
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText(/modelProvider\.selector\.aiCredits/))!.toBeInTheDocument()
|
||||
})
|
||||
@ -356,7 +382,7 @@ describe('PopupItem', () => {
|
||||
credits: 0,
|
||||
})
|
||||
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText(/modelProvider\.selector\.creditsExhausted/))!.toBeInTheDocument()
|
||||
})
|
||||
@ -364,7 +390,7 @@ describe('PopupItem', () => {
|
||||
it('should close the dropdown through dropdown content callbacks', () => {
|
||||
const onHide = vi.fn()
|
||||
|
||||
renderWithCombobox(<PopupItem model={makeModel()} onHide={onHide} />)
|
||||
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={onHide} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /my-api-key/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'close dropdown' }))
|
||||
|
||||
@ -130,7 +130,7 @@ function ModelSelector({
|
||||
<ComboboxContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName={cn('min-w-[320px] overflow-hidden rounded-xl', popupClassName)}
|
||||
popupClassName={cn('w-[432px] max-w-[432px] overflow-hidden rounded-xl', popupClassName)}
|
||||
>
|
||||
<Popup
|
||||
defaultModel={defaultModel}
|
||||
|
||||
@ -1,32 +1,41 @@
|
||||
import type { DefaultModel, Model } from '../declarations'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { DefaultModel, Model, ModelItem } from '../declarations'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ConfigurationMethodEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
|
||||
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
|
||||
import { useLanguage, useUpdateModelList, useUpdateModelProviders } from '../hooks'
|
||||
import ModelBadge from '../model-badge'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import DropdownContent from '../provider-added-card/model-auth-dropdown/dropdown-content'
|
||||
import { useChangeProviderPriority } from '../provider-added-card/use-change-provider-priority'
|
||||
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
|
||||
import { modelTypeFormat, sizeFormat } from '../utils'
|
||||
import FeatureIcon from './feature-icon'
|
||||
|
||||
export type ModelSelectorPreviewPayload = {
|
||||
provider: Model
|
||||
modelItem: ModelItem
|
||||
}
|
||||
|
||||
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
|
||||
|
||||
type PopupItemProps = {
|
||||
defaultModel?: DefaultModel
|
||||
model: Model
|
||||
previewCardHandle: PreviewCardHandle
|
||||
onPreviewCardClose: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
function PopupItem({
|
||||
defaultModel,
|
||||
model,
|
||||
previewCardHandle,
|
||||
onPreviewCardClose,
|
||||
onHide,
|
||||
}: PopupItemProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
@ -167,7 +176,11 @@ function PopupItem({
|
||||
)
|
||||
const itemRender = modelItem.status === ModelStatusEnum.noConfigure
|
||||
? (
|
||||
<div className={rowClassName} aria-disabled="true">
|
||||
<div
|
||||
className={rowClassName}
|
||||
aria-disabled="true"
|
||||
onPointerDown={onPreviewCardClose}
|
||||
>
|
||||
{rowContent}
|
||||
<button
|
||||
type="button"
|
||||
@ -186,67 +199,21 @@ function PopupItem({
|
||||
}}
|
||||
disabled={modelItem.status !== ModelStatusEnum.active}
|
||||
className={rowClassName}
|
||||
onPointerDown={onPreviewCardClose}
|
||||
>
|
||||
{rowContent}
|
||||
</ComboboxItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<PreviewCard key={modelItem.model}>
|
||||
<PreviewCardTrigger
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
render={itemRender}
|
||||
/>
|
||||
<PreviewCardContent
|
||||
placement="right"
|
||||
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<ModelIcon
|
||||
className={cn('h-5 w-5 shrink-0')}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{!!modelItem.model_type && (
|
||||
<ModelBadge>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{!!modelItem.model_properties.mode && (
|
||||
<ModelBadge>
|
||||
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{!!modelItem.model_properties.context_size && (
|
||||
<ModelBadge>
|
||||
{sizeFormat(modelItem.model_properties.context_size as number)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
|
||||
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
|
||||
&& (
|
||||
<div className="pt-2">
|
||||
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{modelItem.features?.map(feature => (
|
||||
<FeatureIcon
|
||||
key={feature}
|
||||
feature={feature}
|
||||
showFeaturesLabel
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
key={modelItem.model}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ provider: model, modelItem }}
|
||||
render={itemRender}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ComboboxGroup>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { DefaultModel, Model, ModelFeatureEnum } from '../declarations'
|
||||
import type { DefaultModel, Model } from '../declarations'
|
||||
import type { ModelSelectorPreviewPayload } from './popup-item'
|
||||
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
||||
import { ComboboxList } from '@langgenius/dify-ui/combobox'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent } from '@langgenius/dify-ui/preview-card'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@ -12,12 +14,15 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import { CustomConfigurationStatusEnum, ModelStatusEnum } from '../declarations'
|
||||
import { CustomConfigurationStatusEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
|
||||
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
|
||||
import ModelBadge from '../model-badge'
|
||||
import ModelIcon from '../model-icon'
|
||||
import CreditsExhaustedAlert from '../provider-added-card/model-auth-dropdown/credits-exhausted-alert'
|
||||
import { useTrialCredits } from '../provider-added-card/use-trial-credits'
|
||||
import { providerSupportsCredits } from '../supports-credits'
|
||||
import { MODEL_PROVIDER_QUOTA_GET_PAID, providerKeyToPluginId } from '../utils'
|
||||
import { MODEL_PROVIDER_QUOTA_GET_PAID, modelTypeFormat, providerKeyToPluginId, sizeFormat } from '../utils'
|
||||
import FeatureIcon from './feature-icon'
|
||||
import MarketplaceSection from './marketplace-section'
|
||||
import { createModelSelectorSearchIndex, filterModelSelectorModels } from './model-search'
|
||||
import ModelSelectorEmptyState from './popup-empty-state'
|
||||
@ -43,6 +48,7 @@ function Popup({
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const language = useLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<ModelSelectorPreviewPayload>(), [])
|
||||
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const { modelProviders } = useProviderContext()
|
||||
@ -151,6 +157,9 @@ function Popup({
|
||||
onHide()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}, [onHide, setShowAccountSettingModal])
|
||||
const handleClosePreviewCard = useCallback(() => {
|
||||
previewCardHandle.close()
|
||||
}, [previewCardHandle])
|
||||
|
||||
return (
|
||||
<ModelSelectorPopupFrame>
|
||||
@ -170,6 +179,8 @@ function Popup({
|
||||
key={model.provider}
|
||||
defaultModel={defaultModel}
|
||||
model={model}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onPreviewCardClose={handleClosePreviewCard}
|
||||
onHide={onHide}
|
||||
/>
|
||||
))
|
||||
@ -201,9 +212,86 @@ function Popup({
|
||||
/>
|
||||
</div>
|
||||
</ModelSelectorScrollBody>
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<ModelSelectorPreviewCard
|
||||
capabilitiesLabel={t('model.capabilities', { ns: 'common' })}
|
||||
language={language}
|
||||
payload={payload as ModelSelectorPreviewPayload | undefined}
|
||||
/>
|
||||
)}
|
||||
</PreviewCard>
|
||||
<ModelProviderSettingsFooter onOpenSettings={handleOpenSettings} />
|
||||
</ModelSelectorPopupFrame>
|
||||
)
|
||||
}
|
||||
|
||||
type ModelSelectorPreviewCardProps = {
|
||||
capabilitiesLabel: string
|
||||
language: string
|
||||
payload?: ModelSelectorPreviewPayload
|
||||
}
|
||||
|
||||
function ModelSelectorPreviewCard({
|
||||
capabilitiesLabel,
|
||||
language,
|
||||
payload,
|
||||
}: ModelSelectorPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
const { provider, modelItem } = payload
|
||||
|
||||
return (
|
||||
<PreviewCardContent
|
||||
placement="right"
|
||||
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<ModelIcon
|
||||
className="h-5 w-5 shrink-0"
|
||||
provider={provider}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{!!modelItem.model_type && (
|
||||
<ModelBadge>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{!!modelItem.model_properties.mode && (
|
||||
<ModelBadge>
|
||||
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{!!modelItem.model_properties.context_size && (
|
||||
<ModelBadge>
|
||||
{sizeFormat(modelItem.model_properties.context_size as number)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
|
||||
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
|
||||
&& (
|
||||
<div className="pt-2">
|
||||
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{capabilitiesLabel}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{modelItem.features?.map(feature => (
|
||||
<FeatureIcon
|
||||
key={feature}
|
||||
feature={feature}
|
||||
showFeaturesLabel
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popup
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { NodeDefault } from '../types'
|
||||
import type { BlockClassificationEnum } from './types'
|
||||
import {
|
||||
createPreviewCardHandle,
|
||||
PreviewCard,
|
||||
PreviewCardContent,
|
||||
PreviewCardTrigger,
|
||||
@ -25,6 +26,10 @@ type BlocksProps = {
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
blocks?: NodeDefault[]
|
||||
}
|
||||
type BlockPreviewPayload = {
|
||||
block: NodeDefault
|
||||
}
|
||||
|
||||
const Blocks = ({
|
||||
searchText,
|
||||
onSelect,
|
||||
@ -34,6 +39,7 @@ const Blocks = ({
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const blocksFromHooks = useBlocks()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<BlockPreviewPayload>(), [])
|
||||
|
||||
// Use external blocks if provided, otherwise fallback to hook-based blocks
|
||||
const blocks = blocksFromProps || blocksFromHooks.map(block => ({
|
||||
@ -101,51 +107,38 @@ const Blocks = ({
|
||||
// hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
filteredList.map(block => (
|
||||
<PreviewCard key={block.metaData.type}>
|
||||
<PreviewCardTrigger
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.metaData.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
|
||||
{
|
||||
block.metaData.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
|
||||
className="ml-2 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PreviewCardContent
|
||||
placement="right"
|
||||
popupClassName="w-[200px] border-none px-3 py-2"
|
||||
>
|
||||
<div>
|
||||
<PreviewCardTrigger
|
||||
key={block.metaData.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.metaData.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className="mb-1 system-md-medium text-text-primary">{block.metaData.title}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{block.metaData.description}</div>
|
||||
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
|
||||
{
|
||||
block.metaData.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
|
||||
className="ml-2 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}, [groups, onSelect, t, store])
|
||||
}, [groups, onSelect, previewCardHandle, t, store])
|
||||
|
||||
return (
|
||||
<div className="max-h-[480px] max-w-[500px] overflow-y-auto p-1">
|
||||
@ -157,8 +150,43 @@ const Blocks = ({
|
||||
{
|
||||
!isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup)
|
||||
}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<BlockPreviewCard payload={payload as BlockPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type BlockPreviewCardProps = {
|
||||
payload?: BlockPreviewPayload
|
||||
}
|
||||
|
||||
function BlockPreviewCard({
|
||||
payload,
|
||||
}: BlockPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
const { block } = payload
|
||||
|
||||
return (
|
||||
<PreviewCardContent
|
||||
placement="right"
|
||||
popupClassName="w-[200px] border-none px-3 py-2"
|
||||
>
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className="mb-1 system-md-medium text-text-primary">{block.metaData.title}</div>
|
||||
<div className="system-xs-regular wrap-break-word text-text-tertiary">{block.metaData.description}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Blocks)
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { ToolWithProvider } from '../types'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -33,6 +34,11 @@ type FeaturedToolsProps = {
|
||||
isLoading?: boolean
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
type FeaturedToolPreviewPayload = {
|
||||
plugin: Plugin
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
|
||||
|
||||
@ -46,7 +52,9 @@ const FeaturedTools = ({
|
||||
}: FeaturedToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedToolPreviewPayload>(), [])
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (isServer)
|
||||
return false
|
||||
@ -54,23 +62,16 @@ const FeaturedTools = ({
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleCountPlugins !== plugins) {
|
||||
setVisibleCountPlugins(plugins)
|
||||
setVisibleCount(INITIAL_VISIBLE_COUNT)
|
||||
}, [plugins])
|
||||
}
|
||||
|
||||
const limitedPlugins = useMemo(
|
||||
() => plugins.slice(0, MAX_RECOMMENDED_COUNT),
|
||||
@ -174,10 +175,11 @@ const FeaturedTools = ({
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
language={language}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t as any}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -214,6 +216,11 @@ const FeaturedTools = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<FeaturedToolPreviewCard payload={payload as FeaturedToolPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -221,13 +228,15 @@ const FeaturedTools = ({
|
||||
type FeaturedToolUninstalledItemProps = {
|
||||
plugin: Plugin
|
||||
language: Locale
|
||||
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedToolPreviewPayload>>
|
||||
onInstallSuccess?: () => Promise<void> | void
|
||||
t: (key: string, options?: Record<string, any>) => string
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
function FeaturedToolUninstalledItem({
|
||||
plugin,
|
||||
language,
|
||||
previewCardHandle,
|
||||
onInstallSuccess,
|
||||
t,
|
||||
}: FeaturedToolUninstalledItemProps) {
|
||||
@ -296,16 +305,13 @@ function FeaturedToolUninstalledItem({
|
||||
// Preview is supplementary: icon / label / brief are all reachable from
|
||||
// the InstallFromMarketplace modal that opens on click, so hover/focus-only
|
||||
// activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ plugin, label, description }}
|
||||
render={row}
|
||||
/>
|
||||
)
|
||||
: row}
|
||||
{isInstallModalOpen && (
|
||||
@ -325,4 +331,25 @@ function FeaturedToolUninstalledItem({
|
||||
)
|
||||
}
|
||||
|
||||
type FeaturedToolPreviewCardProps = {
|
||||
payload?: FeaturedToolPreviewPayload
|
||||
}
|
||||
|
||||
function FeaturedToolPreviewCard({
|
||||
payload,
|
||||
}: FeaturedToolPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.Tool} toolIcon={payload.plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label}</div>
|
||||
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.description}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedTools
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -17,6 +19,7 @@ import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item'
|
||||
import TriggerPluginItem from './trigger-plugin/item'
|
||||
|
||||
const MAX_RECOMMENDED_COUNT = 15
|
||||
@ -29,6 +32,11 @@ type FeaturedTriggersProps = {
|
||||
isLoading?: boolean
|
||||
onInstallSuccess?: () => void | Promise<void>
|
||||
}
|
||||
type FeaturedTriggerPreviewPayload = {
|
||||
plugin: Plugin
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_triggers_featured_collapsed'
|
||||
|
||||
@ -41,7 +49,10 @@ const FeaturedTriggers = ({
|
||||
}: FeaturedTriggersProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedTriggerPreviewPayload>(), [])
|
||||
const triggerActionPreviewCardHandle = useMemo(() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(), [])
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (isServer)
|
||||
return false
|
||||
@ -49,23 +60,16 @@ const FeaturedTriggers = ({
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleCountPlugins !== plugins) {
|
||||
setVisibleCountPlugins(plugins)
|
||||
setVisibleCount(INITIAL_VISIBLE_COUNT)
|
||||
}, [plugins])
|
||||
}
|
||||
|
||||
const limitedPlugins = useMemo(
|
||||
() => plugins.slice(0, MAX_RECOMMENDED_COUNT),
|
||||
@ -156,6 +160,7 @@ const FeaturedTriggers = ({
|
||||
key={provider.id}
|
||||
payload={provider}
|
||||
hasSearchText={false}
|
||||
previewCardHandle={triggerActionPreviewCardHandle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
@ -169,10 +174,11 @@ const FeaturedTriggers = ({
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
language={language}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t as any}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -209,6 +215,16 @@ const FeaturedTriggers = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<FeaturedTriggerPreviewCard payload={payload as FeaturedTriggerPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
<PreviewCard handle={triggerActionPreviewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<TriggerPluginActionPreviewCard payload={payload as TriggerPluginActionPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -216,13 +232,15 @@ const FeaturedTriggers = ({
|
||||
type FeaturedTriggerUninstalledItemProps = {
|
||||
plugin: Plugin
|
||||
language: Locale
|
||||
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedTriggerPreviewPayload>>
|
||||
onInstallSuccess?: () => Promise<void> | void
|
||||
t: (key: string, options?: Record<string, any>) => string
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
function FeaturedTriggerUninstalledItem({
|
||||
plugin,
|
||||
language,
|
||||
previewCardHandle,
|
||||
onInstallSuccess,
|
||||
t,
|
||||
}: FeaturedTriggerUninstalledItemProps) {
|
||||
@ -291,16 +309,13 @@ function FeaturedTriggerUninstalledItem({
|
||||
// Preview is supplementary: icon / label / brief are all reachable from
|
||||
// the InstallFromMarketplace modal that opens on click, so hover/focus-only
|
||||
// activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ plugin, label, description }}
|
||||
render={row}
|
||||
/>
|
||||
)
|
||||
: row}
|
||||
{isInstallModalOpen && (
|
||||
@ -320,4 +335,25 @@ function FeaturedTriggerUninstalledItem({
|
||||
)
|
||||
}
|
||||
|
||||
type FeaturedTriggerPreviewCardProps = {
|
||||
payload?: FeaturedTriggerPreviewPayload
|
||||
}
|
||||
|
||||
function FeaturedTriggerPreviewCard({
|
||||
payload,
|
||||
}: FeaturedTriggerPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.TriggerPlugin} toolIcon={payload.plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label}</div>
|
||||
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.description}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedTriggers
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { BlockEnum, ToolWithProvider } from '../../types'
|
||||
import type { ToolActionPreviewPayload } from '../tool/action-item'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { groupItems } from '../index-bar'
|
||||
import { ToolActionPreviewCard } from '../tool/action-item'
|
||||
import ToolListFlatView from '../tool/tool-list-flat-view/list'
|
||||
import ToolListTreeView from '../tool/tool-list-tree-view/list'
|
||||
import { ViewType } from '../view-type-select'
|
||||
@ -27,6 +30,7 @@ const List = ({
|
||||
className,
|
||||
}: ListProps) => {
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<ToolActionPreviewPayload>(), [])
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
|
||||
const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => tool.label[language]![0]!)
|
||||
@ -58,7 +62,7 @@ const List = ({
|
||||
return result
|
||||
}, [withLetterAndGroupViewToolsData, letters])
|
||||
|
||||
const toolRefs = useRef({})
|
||||
const toolRefsRef = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
|
||||
const handleSelect = useCallback((type: BlockEnum, tool: ToolDefaultValue) => {
|
||||
onSelect(type, tool)
|
||||
@ -70,9 +74,10 @@ const List = ({
|
||||
isFlatView
|
||||
? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
toolRefs={toolRefsRef}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
previewCardHandle={previewCardHandle}
|
||||
isShowLetterIndex={false}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
@ -83,12 +88,18 @@ const List = ({
|
||||
: (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
previewCardHandle={previewCardHandle}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
canNotSelectMultiple
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
{
|
||||
unInstalledPlugins.map((item) => {
|
||||
return (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import type { TriggerDefaultValue } from './types'
|
||||
import {
|
||||
createPreviewCardHandle,
|
||||
PreviewCard,
|
||||
PreviewCardContent,
|
||||
PreviewCardTrigger,
|
||||
@ -25,6 +26,9 @@ type StartBlocksProps = {
|
||||
onContentStateChange?: (hasContent: boolean) => void
|
||||
hideUserInput?: boolean
|
||||
}
|
||||
type StartBlockPreviewPayload = {
|
||||
block: typeof START_BLOCKS[number]
|
||||
}
|
||||
|
||||
const StartBlocks = ({
|
||||
searchText,
|
||||
@ -35,6 +39,7 @@ const StartBlocks = ({
|
||||
}: StartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<StartBlockPreviewPayload>(), [])
|
||||
// const nodeMetaData = useNodeMetaData()
|
||||
|
||||
const filteredBlocks = useMemo(() => {
|
||||
@ -74,54 +79,31 @@ const StartBlocks = ({
|
||||
// the start node, so hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => (
|
||||
<PreviewCard key={block.type}>
|
||||
<PreviewCardTrigger
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.type}
|
||||
/>
|
||||
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
|
||||
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
|
||||
{block.type === BlockEnumValues.Start && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<PreviewCardTrigger
|
||||
key={block.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
className="mr-2 shrink-0"
|
||||
type={block.type}
|
||||
/>
|
||||
<div className="mb-1 system-md-medium text-text-primary">
|
||||
{block.type === BlockEnumValues.TriggerWebhook
|
||||
? t('customWebhook', { ns: 'workflow' })
|
||||
: t(`blocks.${block.type}`, { ns: 'workflow' })}
|
||||
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
|
||||
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
|
||||
{block.type === BlockEnumValues.Start && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-secondary">
|
||||
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
|
||||
<div className="mt-1 mb-1 system-xs-regular text-text-tertiary">
|
||||
{t('author', { ns: 'tools' })}
|
||||
{' '}
|
||||
{t('difyTeam', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
), [onSelect, t])
|
||||
)}
|
||||
/>
|
||||
), [onSelect, previewCardHandle, t])
|
||||
|
||||
if (isEmpty)
|
||||
return null
|
||||
@ -140,8 +122,58 @@ const StartBlocks = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<StartBlockPreviewCard
|
||||
payload={payload as StartBlockPreviewPayload | undefined}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type StartBlockPreviewCardProps = {
|
||||
payload?: StartBlockPreviewPayload
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}
|
||||
|
||||
function StartBlockPreviewCard({
|
||||
payload,
|
||||
t,
|
||||
}: StartBlockPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
const { block } = payload
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={block.type}
|
||||
/>
|
||||
<div className="mb-1 system-md-medium text-text-primary">
|
||||
{block.type === BlockEnumValues.TriggerWebhook
|
||||
? t('customWebhook', { ns: 'workflow' })
|
||||
: t(`blocks.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="system-xs-regular wrap-break-word text-text-secondary">
|
||||
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
|
||||
<div className="mt-1 mb-1 system-xs-regular text-text-tertiary">
|
||||
{t('author', { ns: 'tools' })}
|
||||
{' '}
|
||||
{t('difyTeam', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(StartBlocks)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
@ -51,6 +52,7 @@ describe('Tool', () => {
|
||||
createTool('tool-b', 'Tool B'),
|
||||
],
|
||||
})}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={false}
|
||||
onSelect={onSelect}
|
||||
@ -82,6 +84,7 @@ describe('Tool', () => {
|
||||
type: CollectionType.workflow,
|
||||
tools: [createTool('workflow-tool', 'Workflow Tool')],
|
||||
})}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={false}
|
||||
onSelect={onSelect}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ComponentProps, FC } from 'react'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -27,14 +27,25 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||
type Props = {
|
||||
provider: ToolWithProvider
|
||||
payload: Tool
|
||||
previewCardHandle: PreviewCardHandle
|
||||
disabled?: boolean
|
||||
isAdded?: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
}
|
||||
|
||||
export type ToolActionPreviewPayload = {
|
||||
providerIcon: ToolWithProvider['icon']
|
||||
payload: Tool
|
||||
language: ReturnType<typeof useGetLanguage>
|
||||
}
|
||||
|
||||
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
|
||||
export type ToolActionPreviewCardHandle = PreviewCardHandle
|
||||
|
||||
const ToolItem: FC<Props> = ({
|
||||
provider,
|
||||
payload,
|
||||
previewCardHandle,
|
||||
onSelect,
|
||||
disabled,
|
||||
isAdded,
|
||||
@ -107,21 +118,45 @@ const ToolItem: FC<Props> = ({
|
||||
// reachable from the node inspector after the row is clicked to add the tool,
|
||||
// so hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
<PreviewCard key={payload.name}>
|
||||
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
|
||||
<PreviewCardContent placement="right" popupClassName="w-[200px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={providerIcon}
|
||||
/>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
key={payload.name}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{
|
||||
providerIcon,
|
||||
payload,
|
||||
language,
|
||||
}}
|
||||
render={row}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type ToolActionPreviewCardProps = {
|
||||
payload?: ToolActionPreviewPayload
|
||||
}
|
||||
|
||||
export function ToolActionPreviewCard({
|
||||
payload,
|
||||
}: ToolActionPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[200px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={payload.providerIcon}
|
||||
/>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.payload.label[payload.language]}</div>
|
||||
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.payload.description[payload.language]}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolItem)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
@ -37,6 +38,7 @@ describe('ToolListFlatView', () => {
|
||||
render(
|
||||
<List
|
||||
letters={['A', 'B']}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
payload={[
|
||||
createToolProvider({
|
||||
id: 'provider-a',
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FC, RefObject } from 'react'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import type { ToolActionPreviewCardHandle } from '../action-item'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
@ -9,6 +10,7 @@ import Tool from '../tool'
|
||||
|
||||
type Props = {
|
||||
payload: ToolWithProvider[]
|
||||
previewCardHandle: ToolActionPreviewCardHandle
|
||||
isShowLetterIndex: boolean
|
||||
indexBar: React.ReactNode
|
||||
hasSearchText: boolean
|
||||
@ -16,13 +18,14 @@ type Props = {
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
letters: string[]
|
||||
toolRefs: any
|
||||
toolRefs: RefObject<Record<string, HTMLDivElement | null>>
|
||||
selectedTools?: ToolValue[]
|
||||
}
|
||||
|
||||
const ToolViewFlatView: FC<Props> = ({
|
||||
letters,
|
||||
payload,
|
||||
previewCardHandle,
|
||||
isShowLetterIndex,
|
||||
indexBar,
|
||||
hasSearchText,
|
||||
@ -55,6 +58,7 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
>
|
||||
<Tool
|
||||
payload={tool}
|
||||
previewCardHandle={previewCardHandle}
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
@ -36,6 +37,7 @@ describe('ToolListTreeView Item', () => {
|
||||
toolList={[createToolProvider({
|
||||
label: { en_US: 'Provider Alpha', zh_Hans: 'Provider Alpha' },
|
||||
})]}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
hasSearchText={false}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
@ -43,6 +44,7 @@ describe('ToolListTreeView', () => {
|
||||
label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' },
|
||||
})],
|
||||
}}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
hasSearchText={false}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import type { ToolActionPreviewCardHandle } from '../action-item'
|
||||
import * as React from 'react'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
import Tool from '../tool'
|
||||
@ -9,6 +10,7 @@ import Tool from '../tool'
|
||||
type Props = {
|
||||
groupName: string
|
||||
toolList: ToolWithProvider[]
|
||||
previewCardHandle: ToolActionPreviewCardHandle
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
@ -19,6 +21,7 @@ type Props = {
|
||||
const Item: FC<Props> = ({
|
||||
groupName,
|
||||
toolList,
|
||||
previewCardHandle,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
@ -35,6 +38,7 @@ const Item: FC<Props> = ({
|
||||
<Tool
|
||||
key={tool.id}
|
||||
payload={tool}
|
||||
previewCardHandle={previewCardHandle}
|
||||
viewType={ViewType.tree}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import type { ToolActionPreviewCardHandle } from '../action-item'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,6 +11,7 @@ import Item from './item'
|
||||
|
||||
type Props = {
|
||||
payload: Record<string, ToolWithProvider[]>
|
||||
previewCardHandle: ToolActionPreviewCardHandle
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
@ -19,6 +21,7 @@ type Props = {
|
||||
|
||||
const ToolListTreeView: FC<Props> = ({
|
||||
payload,
|
||||
previewCardHandle,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
@ -49,6 +52,7 @@ const ToolListTreeView: FC<Props> = ({
|
||||
key={groupName}
|
||||
groupName={getI18nGroupName(groupName)}
|
||||
toolList={payload[groupName]!}
|
||||
previewCardHandle={previewCardHandle}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
|
||||
@ -3,11 +3,12 @@ import type { FC } from 'react'
|
||||
import type { Tool as ToolType } from '../../../tools/types'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../types'
|
||||
import type { ToolActionPreviewCardHandle } from './action-item'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useHover } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
@ -33,6 +34,7 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||
type Props = {
|
||||
className?: string
|
||||
payload: ToolWithProvider
|
||||
previewCardHandle: ToolActionPreviewCardHandle
|
||||
viewType: ViewType
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
@ -45,6 +47,7 @@ type Props = {
|
||||
const Tool: FC<Props> = ({
|
||||
className,
|
||||
payload,
|
||||
previewCardHandle,
|
||||
viewType,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
@ -59,7 +62,8 @@ const Tool: FC<Props> = ({
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.tools
|
||||
const hasAction = !notShowProvider
|
||||
const [isFold, setFold] = React.useState<boolean>(true)
|
||||
const [isFold, setIsFold] = React.useState<boolean>(true)
|
||||
const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText)
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
const isMCPTool = payload.type === CollectionType.mcp
|
||||
@ -146,14 +150,10 @@ const Tool: FC<Props> = ({
|
||||
)
|
||||
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSearchText && isFold) {
|
||||
setFold(false)
|
||||
return
|
||||
}
|
||||
if (!hasSearchText && !isFold)
|
||||
setFold(true)
|
||||
}, [hasSearchText])
|
||||
if (isFoldHasSearchText !== hasSearchText) {
|
||||
setIsFoldHasSearchText(hasSearchText)
|
||||
setIsFold(!hasSearchText)
|
||||
}
|
||||
|
||||
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||
|
||||
@ -181,7 +181,7 @@ const Tool: FC<Props> = ({
|
||||
className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
setIsFold(!isFold)
|
||||
return
|
||||
}
|
||||
|
||||
@ -240,6 +240,7 @@ const Tool: FC<Props> = ({
|
||||
key={action.name}
|
||||
provider={payload}
|
||||
payload={action}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onSelect={onSelect}
|
||||
disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip}
|
||||
isAdded={getIsDisabled(action)}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import type { BlockEnum, ToolWithProvider } from '../types'
|
||||
import type { ToolActionPreviewPayload } from './tool/action-item'
|
||||
import type { ToolDefaultValue, ToolTypeEnum, ToolValue } from './types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
|
||||
import { memo, useMemo, useRef } from 'react'
|
||||
import Empty from '@/app/components/tools/provider/empty'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import IndexBar, { groupItems } from './index-bar'
|
||||
import { ToolActionPreviewCard } from './tool/action-item'
|
||||
import ToolListFlatView from './tool/tool-list-flat-view/list'
|
||||
import ToolListTreeView from './tool/tool-list-tree-view/list'
|
||||
import { ViewType } from './view-type-select'
|
||||
@ -35,8 +38,8 @@ const Tools = ({
|
||||
indexBarClassName,
|
||||
selectedTools,
|
||||
}: ToolsProps) => {
|
||||
// const tools: any = []
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<ToolActionPreviewPayload>(), [])
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
const isShowLetterIndex = isFlatView && tools.length > 10
|
||||
|
||||
@ -85,7 +88,7 @@ const Tools = ({
|
||||
return result
|
||||
}, [withLetterAndGroupViewToolsData, letters])
|
||||
|
||||
const toolRefs = useRef({})
|
||||
const toolRefsRef = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
|
||||
return (
|
||||
<div className={cn('max-w-full p-1', className)}>
|
||||
@ -98,21 +101,23 @@ const Tools = ({
|
||||
isFlatView
|
||||
? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
toolRefs={toolRefsRef}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
previewCardHandle={previewCardHandle}
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
indexBar={<IndexBar letters={letters} itemRefs={toolRefsRef} className={indexBarClassName} />}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
previewCardHandle={previewCardHandle}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
@ -121,6 +126,11 @@ const Tools = ({
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ComponentProps, FC } from 'react'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
|
||||
import type { Event } from '@/app/components/tools/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
@ -13,14 +13,25 @@ import { BlockEnum } from '../../types'
|
||||
type Props = {
|
||||
provider: TriggerWithProvider
|
||||
payload: Event
|
||||
previewCardHandle: TriggerPluginActionPreviewCardHandle
|
||||
disabled?: boolean
|
||||
isAdded?: boolean
|
||||
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
|
||||
}
|
||||
|
||||
export type TriggerPluginActionPreviewPayload = {
|
||||
provider: TriggerWithProvider
|
||||
payload: Event
|
||||
language: ReturnType<typeof useGetLanguage>
|
||||
}
|
||||
|
||||
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
|
||||
export type TriggerPluginActionPreviewCardHandle = PreviewCardHandle
|
||||
|
||||
const TriggerPluginActionItem: FC<Props> = ({
|
||||
provider,
|
||||
payload,
|
||||
previewCardHandle,
|
||||
onSelect,
|
||||
disabled,
|
||||
isAdded,
|
||||
@ -37,7 +48,7 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
return
|
||||
const params: Record<string, string> = {}
|
||||
if (payload.parameters) {
|
||||
payload.parameters.forEach((item: any) => {
|
||||
payload.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
@ -73,21 +84,41 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
// reachable from the node inspector after the row is clicked to add the trigger,
|
||||
// so hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
<PreviewCard key={payload.name}>
|
||||
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={provider.icon}
|
||||
/>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
key={payload.name}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ provider, payload, language }}
|
||||
render={row}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type TriggerPluginActionPreviewCardProps = {
|
||||
payload?: TriggerPluginActionPreviewPayload
|
||||
}
|
||||
|
||||
export function TriggerPluginActionPreviewCard({
|
||||
payload,
|
||||
}: TriggerPluginActionPreviewCardProps) {
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={payload.provider.icon}
|
||||
/>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.payload.label[payload.language]}</div>
|
||||
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.payload.description[payload.language]}</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TriggerPluginActionItem)
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { TriggerPluginActionPreviewCardHandle } from './action-item'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
@ -27,6 +28,7 @@ type Props = {
|
||||
className?: string
|
||||
payload: TriggerWithProvider
|
||||
hasSearchText: boolean
|
||||
previewCardHandle: TriggerPluginActionPreviewCardHandle
|
||||
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
|
||||
}
|
||||
|
||||
@ -34,6 +36,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
className,
|
||||
payload,
|
||||
hasSearchText,
|
||||
previewCardHandle,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -42,17 +45,14 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.events
|
||||
const hasAction = !notShowProvider
|
||||
const [isFold, setFold] = React.useState<boolean>(true)
|
||||
const [isFold, setIsFold] = React.useState<boolean>(true)
|
||||
const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText)
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSearchText && isFold) {
|
||||
setFold(false)
|
||||
return
|
||||
}
|
||||
if (!hasSearchText && !isFold)
|
||||
setFold(true)
|
||||
}, [hasSearchText])
|
||||
if (isFoldHasSearchText !== hasSearchText) {
|
||||
setIsFoldHasSearchText(hasSearchText)
|
||||
setIsFold(!hasSearchText)
|
||||
}
|
||||
|
||||
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||
|
||||
@ -97,14 +97,14 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
setIsFold(!isFold)
|
||||
return
|
||||
}
|
||||
|
||||
const event = actions[0]
|
||||
const params: Record<string, string> = {}
|
||||
if (event!.parameters) {
|
||||
event!.parameters.forEach((item: any) => {
|
||||
event!.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
@ -150,6 +150,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
key={action.name}
|
||||
provider={providerWithResolvedIcon}
|
||||
payload={action}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onSelect={onSelect}
|
||||
disabled={false}
|
||||
isAdded={false}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
'use client'
|
||||
import type { BlockEnum } from '../../types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
|
||||
import type { TriggerPluginActionPreviewPayload } from './action-item'
|
||||
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { TriggerPluginActionPreviewCard } from './action-item'
|
||||
import TriggerPluginItem from './item'
|
||||
|
||||
type TriggerPluginListProps = {
|
||||
@ -20,6 +23,7 @@ const TriggerPluginList = ({
|
||||
}: TriggerPluginListProps) => {
|
||||
const { data: triggerPluginsData } = useAllTriggerPlugins()
|
||||
const language = useGetLanguage()
|
||||
const previewCardHandle = useMemo(() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(), [])
|
||||
|
||||
const normalizedSearch = searchText.trim().toLowerCase()
|
||||
const triggerPlugins = useMemo(() => {
|
||||
@ -96,8 +100,14 @@ const TriggerPluginList = ({
|
||||
payload={plugin}
|
||||
onSelect={onSelect}
|
||||
hasSearchText={!!searchText}
|
||||
previewCardHandle={previewCardHandle}
|
||||
/>
|
||||
))}
|
||||
<PreviewCard handle={previewCardHandle}>
|
||||
{({ payload }) => (
|
||||
<TriggerPluginActionPreviewCard payload={payload as TriggerPluginActionPreviewPayload | undefined} />
|
||||
)}
|
||||
</PreviewCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user