mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feature/tool-invocation-add-inputs-param
This commit is contained in:
commit
b2d5883c14
|
|
@ -37,7 +37,7 @@ dependencies = [
|
|||
"numpy~=1.26.4",
|
||||
"openpyxl~=3.1.5",
|
||||
"opik~=1.8.72",
|
||||
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
|
||||
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
|
||||
"opentelemetry-api==1.27.0",
|
||||
"opentelemetry-distro==0.48b0",
|
||||
"opentelemetry-exporter-otlp==1.27.0",
|
||||
|
|
@ -79,7 +79,6 @@ dependencies = [
|
|||
"tiktoken~=0.9.0",
|
||||
"transformers~=4.56.1",
|
||||
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
|
||||
"weave~=0.51.0",
|
||||
"yarl~=1.18.3",
|
||||
"webvtt-py~=0.5.1",
|
||||
"sseclient-py~=1.8.0",
|
||||
|
|
@ -90,6 +89,7 @@ dependencies = [
|
|||
"croniter>=6.0.0",
|
||||
"weaviate-client==4.17.0",
|
||||
"apscheduler>=3.11.0",
|
||||
"weave>=0.52.16",
|
||||
]
|
||||
# Before adding new dependency, consider place it in
|
||||
# alphabet order (a-z) and suitable group.
|
||||
|
|
|
|||
20
api/uv.lock
20
api/uv.lock
|
|
@ -1588,7 +1588,7 @@ requires-dist = [
|
|||
{ name = "tiktoken", specifier = "~=0.9.0" },
|
||||
{ name = "transformers", specifier = "~=4.56.1" },
|
||||
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
|
||||
{ name = "weave", specifier = "~=0.51.0" },
|
||||
{ name = "weave", specifier = ">=0.52.16" },
|
||||
{ name = "weaviate-client", specifier = "==4.17.0" },
|
||||
{ name = "webvtt-py", specifier = "~=0.5.1" },
|
||||
{ name = "yarl", specifier = "~=1.18.3" },
|
||||
|
|
@ -3538,15 +3538,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/95/e1/45373c06781340c7b74fe9b88b85278ac05321889a307eaa5be079a997d4/mysql_connector_python-9.5.0-py2.py3-none-any.whl", hash = "sha256:ace137b88eb6fdafa1e5b2e03ac76ce1b8b1844b3a4af1192a02ae7c1a45bdee", size = 479047, upload-time = "2025-10-22T09:02:27.809Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nest-asyncio"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.5"
|
||||
|
|
@ -6906,7 +6897,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "weave"
|
||||
version = "0.51.59"
|
||||
version = "0.52.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
@ -6914,18 +6905,17 @@ dependencies = [
|
|||
{ name = "eval-type-backport" },
|
||||
{ name = "gql", extra = ["aiohttp", "requests"] },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "packaging" },
|
||||
{ name = "polyfile-weave" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
{ name = "wandb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/53/1b0350a64837df3e29eda6149a542f3a51e706122086f82547153820e982/weave-0.51.59.tar.gz", hash = "sha256:fad34c0478f3470401274cba8fa2bfd45d14a187db0a5724bd507e356761b349", size = 480572, upload-time = "2025-07-25T22:05:07.458Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/30/b795b5a857e8a908e68f3ed969587bb2bc63527ef2260f72ac1a6fd983e8/weave-0.52.16.tar.gz", hash = "sha256:7bb8fdce0393007e9c40fb1769d0606bfe55401c4cd13146457ccac4b49c695d", size = 607024, upload-time = "2025-11-07T19:45:30.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/bc/fa5ffb887a1ee28109b29c62416c9e0f41da8e75e6871671208b3d42b392/weave-0.51.59-py3-none-any.whl", hash = "sha256:2238578574ecdf6285efdf028c78987769720242ac75b7b84b1dbc59060468ce", size = 612468, upload-time = "2025-07-25T22:05:05.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/a54513796605dfaef2c3c23c2733bcb4b24866a623635c057b2ffdb74052/weave-0.52.16-py3-none-any.whl", hash = "sha256:85985b8cf233032c6d915dfac95b3bcccb1304444d99a6b4a61f3666b58146ce", size = 764366, upload-time = "2025-11-07T19:45:28.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
|
|
@ -24,6 +24,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
|
|
@ -33,22 +34,56 @@ export type ICardViewProps = {
|
|||
|
||||
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const showMCPCard = isInPanel
|
||||
const showTriggerCard = isInPanel && appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const { data: currentWorkflow } = useAppWorkflow(appDetail?.mode === AppModeEnum.WORKFLOW ? appDetail.id : '')
|
||||
const hasTriggerNode = useMemo(() => {
|
||||
if (appDetail?.mode !== AppModeEnum.WORKFLOW)
|
||||
const showTriggerCard = isInPanel && isWorkflowApp
|
||||
const { data: currentWorkflow } = useAppWorkflow(isWorkflowApp ? appDetail.id : '')
|
||||
const hasTriggerNode = useMemo<boolean | null>(() => {
|
||||
if (!isWorkflowApp)
|
||||
return false
|
||||
const nodes = currentWorkflow?.graph?.nodes || []
|
||||
if (!currentWorkflow)
|
||||
return null
|
||||
const nodes = currentWorkflow.graph?.nodes || []
|
||||
return nodes.some((node) => {
|
||||
const nodeType = node.data?.type as BlockEnum | undefined
|
||||
return !!nodeType && isTriggerNode(nodeType)
|
||||
})
|
||||
}, [appDetail?.mode, currentWorkflow])
|
||||
}, [isWorkflowApp, currentWorkflow])
|
||||
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
|
||||
const disableAppCards = !shouldRenderAppCards
|
||||
|
||||
const triggerDocUrl = docLink('/guides/workflow/node/start')
|
||||
const buildTriggerModeMessage = useCallback((featureName: string) => (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-xs text-text-secondary'>
|
||||
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
window.open(triggerDocUrl, '_blank')
|
||||
}}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
</div>
|
||||
</div>
|
||||
), [t, triggerDocUrl])
|
||||
|
||||
const disableWebAppTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
|
||||
: null
|
||||
const disableApiTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
|
||||
: null
|
||||
const disableMcpTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('tools.mcp.server.title'))
|
||||
: null
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
|
|
@ -120,39 +155,48 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
if (!appDetail)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
|
||||
{
|
||||
!hasTriggerNode && (
|
||||
<>
|
||||
<AppCard
|
||||
appInfo={appDetail}
|
||||
cardType="webapp"
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeSiteStatus}
|
||||
onGenerateCode={onGenerateCode}
|
||||
onSaveSiteConfig={onSaveSiteConfig}
|
||||
/>
|
||||
<AppCard
|
||||
cardType="api"
|
||||
appInfo={appDetail}
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeApiStatus}
|
||||
/>
|
||||
{showMCPCard && (
|
||||
<MCPServiceCard
|
||||
appInfo={appDetail}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{showTriggerCard && (
|
||||
<TriggerCard
|
||||
const appCards = (
|
||||
<>
|
||||
<AppCard
|
||||
appInfo={appDetail}
|
||||
cardType="webapp"
|
||||
isInPanel={isInPanel}
|
||||
triggerModeDisabled={disableAppCards}
|
||||
triggerModeMessage={disableWebAppTooltip}
|
||||
onChangeStatus={onChangeSiteStatus}
|
||||
onGenerateCode={onGenerateCode}
|
||||
onSaveSiteConfig={onSaveSiteConfig}
|
||||
/>
|
||||
<AppCard
|
||||
cardType="api"
|
||||
appInfo={appDetail}
|
||||
isInPanel={isInPanel}
|
||||
triggerModeDisabled={disableAppCards}
|
||||
triggerModeMessage={disableApiTooltip}
|
||||
onChangeStatus={onChangeApiStatus}
|
||||
/>
|
||||
{showMCPCard && (
|
||||
<MCPServiceCard
|
||||
appInfo={appDetail}
|
||||
onToggleResult={handleCallbackResult}
|
||||
triggerModeDisabled={disableAppCards}
|
||||
triggerModeMessage={disableMcpTooltip}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const triggerCardNode = showTriggerCard ? (
|
||||
<TriggerCard
|
||||
appInfo={appDetail}
|
||||
onToggleResult={handleCallbackResult}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
|
||||
{disableAppCards && triggerCardNode}
|
||||
{appCards}
|
||||
{!disableAppCards && triggerCardNode}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload
|
|||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
import PromptLogModal from '../../base/prompt-log-modal'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
|
||||
type AppStoreState = ReturnType<typeof useAppStore.getState>
|
||||
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
|
||||
|
|
@ -779,15 +780,17 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
/>
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
)}
|
||||
{!isChatMode && showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export type IAppCardProps = {
|
|||
isInPanel?: boolean
|
||||
cardType?: 'api' | 'webapp'
|
||||
customBgColor?: string
|
||||
triggerModeDisabled?: boolean // true when Trigger Node mode needs UI locked to avoid conflicting actions
|
||||
triggerModeMessage?: React.ReactNode // contextual copy explaining why the card is disabled in trigger mode
|
||||
onChangeStatus: (val: boolean) => Promise<void>
|
||||
onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
|
||||
onGenerateCode?: () => Promise<void>
|
||||
|
|
@ -61,6 +63,8 @@ function AppCard({
|
|||
isInPanel,
|
||||
cardType = 'webapp',
|
||||
customBgColor,
|
||||
triggerModeDisabled = false,
|
||||
triggerModeMessage = '',
|
||||
onChangeStatus,
|
||||
onSaveSiteConfig,
|
||||
onGenerateCode,
|
||||
|
|
@ -111,7 +115,7 @@ function AppCard({
|
|||
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
|
||||
const missingStartNode = isWorkflowApp && !hasStartNode
|
||||
const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
|
||||
const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api)
|
||||
const isMinimalState = appUnpublished || missingStartNode
|
||||
const { app_base_url, access_token } = appInfo.site ?? {}
|
||||
|
|
@ -189,7 +193,20 @@ function AppCard({
|
|||
className={
|
||||
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`}
|
||||
>
|
||||
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}>
|
||||
<div className={`${customBgColor ?? 'bg-background-default'} relative rounded-xl ${triggerModeDisabled ? 'opacity-60' : ''}`}>
|
||||
{triggerModeDisabled && (
|
||||
triggerModeMessage
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={triggerModeMessage}
|
||||
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
|
||||
position="right"
|
||||
>
|
||||
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
|
||||
</Tooltip>
|
||||
)
|
||||
: <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
|
||||
)}
|
||||
<div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<AppBasic
|
||||
|
|
@ -214,18 +231,23 @@ function AppCard({
|
|||
</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
toggleDisabled && (appUnpublished || missingStartNode) ? (
|
||||
<>
|
||||
<div className="mb-1 text-xs font-normal text-text-secondary">
|
||||
{t('appOverview.overview.appInfo.enableTooltip.description')}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
|
||||
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
</div>
|
||||
</>
|
||||
toggleDisabled ? (
|
||||
triggerModeDisabled && triggerModeMessage
|
||||
? triggerModeMessage
|
||||
: (appUnpublished || missingStartNode) ? (
|
||||
<>
|
||||
<div className="mb-1 text-xs font-normal text-text-secondary">
|
||||
{t('appOverview.overview.appInfo.enableTooltip.description')}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
|
||||
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: ''
|
||||
) : ''
|
||||
}
|
||||
position="right"
|
||||
|
|
@ -329,9 +351,11 @@ function AppCard({
|
|||
{!isApp && <SecretKeyButton appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
const disabled
|
||||
= op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
? false
|
||||
: !runningStatus
|
||||
= triggerModeDisabled
|
||||
? true
|
||||
: op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
? false
|
||||
: !runningStatus
|
||||
return (
|
||||
<Button
|
||||
className="mr-1 min-w-[88px]"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
||||
import { RiDiscordFill, RiDiscussLine, RiGithubFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type CustomLinkProps = {
|
||||
|
|
@ -38,6 +38,9 @@ const Footer = () => {
|
|||
<CustomLink href='https://discord.gg/FngNHpbcY7'>
|
||||
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
|
||||
</CustomLink>
|
||||
<CustomLink href='https://forum.dify.ai'>
|
||||
<RiDiscussLine className='h-5 w-5 text-text-tertiary' />
|
||||
</CustomLink>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
.appIcon {
|
||||
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
|
||||
.appIcon.small {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
|
||||
.appIcon.tiny {
|
||||
@apply w-6 h-6 text-base;
|
||||
}
|
||||
|
||||
.appIcon.xs {
|
||||
@apply w-5 h-5 text-base;
|
||||
}
|
||||
|
||||
.appIcon.rounded {
|
||||
@apply rounded-full;
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import React from 'react'
|
||||
|
||||
declare const require: any
|
||||
|
||||
type IconComponent = React.ComponentType<Record<string, unknown>>
|
||||
|
||||
type IconEntry = {
|
||||
name: string
|
||||
category: string
|
||||
path: string
|
||||
Component: IconComponent
|
||||
}
|
||||
|
||||
const iconContext = require.context('./src', true, /\.tsx$/)
|
||||
|
||||
const iconEntries: IconEntry[] = iconContext
|
||||
.keys()
|
||||
.filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx'))
|
||||
.map((key: string) => {
|
||||
const mod = iconContext(key)
|
||||
const Component = mod.default as IconComponent | undefined
|
||||
if (!Component)
|
||||
return null
|
||||
|
||||
const relativePath = key.replace(/^\.\//, '')
|
||||
const path = `app/components/base/icons/src/${relativePath}`
|
||||
const parts = relativePath.split('/')
|
||||
const fileName = parts.pop() || ''
|
||||
const category = parts.length ? parts.join('/') : '(root)'
|
||||
const name = Component.displayName || fileName.replace(/\.tsx$/, '')
|
||||
|
||||
return {
|
||||
name,
|
||||
category,
|
||||
path,
|
||||
Component,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as IconEntry[]
|
||||
|
||||
const sortedEntries = [...iconEntries].sort((a, b) => {
|
||||
if (a.category === b.category)
|
||||
return a.name.localeCompare(b.name)
|
||||
return a.category.localeCompare(b.category)
|
||||
})
|
||||
|
||||
const filterEntries = (entries: IconEntry[], query: string) => {
|
||||
const normalized = query.trim().toLowerCase()
|
||||
if (!normalized)
|
||||
return entries
|
||||
|
||||
return entries.filter(entry =>
|
||||
entry.name.toLowerCase().includes(normalized)
|
||||
|| entry.path.toLowerCase().includes(normalized)
|
||||
|| entry.category.toLowerCase().includes(normalized),
|
||||
)
|
||||
}
|
||||
|
||||
const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => {
|
||||
if (!acc[entry.category])
|
||||
acc[entry.category] = []
|
||||
|
||||
acc[entry.category].push(entry)
|
||||
return acc
|
||||
}, {} as Record<string, IconEntry[]>)
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
padding: 24,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
}
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}
|
||||
|
||||
const controlsStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flexWrap: 'wrap',
|
||||
}
|
||||
|
||||
const searchInputStyle: React.CSSProperties = {
|
||||
padding: '8px 12px',
|
||||
minWidth: 280,
|
||||
borderRadius: 6,
|
||||
border: '1px solid #d0d0d5',
|
||||
}
|
||||
|
||||
const toggleButtonStyle: React.CSSProperties = {
|
||||
padding: '8px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #d0d0d5',
|
||||
background: '#fff',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
|
||||
const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' }
|
||||
|
||||
const sectionStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
}
|
||||
|
||||
const gridStyle: React.CSSProperties = {
|
||||
display: 'grid',
|
||||
gap: 12,
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
}
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
border: '1px solid #e1e1e8',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
minHeight: 140,
|
||||
}
|
||||
|
||||
const previewBaseStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 48,
|
||||
borderRadius: 6,
|
||||
}
|
||||
|
||||
const nameButtonBaseStyle: React.CSSProperties = {
|
||||
display: 'inline-flex',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const PREVIEW_SIZE = 40
|
||||
|
||||
const IconGalleryStory = () => {
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [copiedPath, setCopiedPath] = React.useState<string | null>(null)
|
||||
const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light')
|
||||
|
||||
const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query])
|
||||
|
||||
const grouped = React.useMemo(() => groupByCategory(filtered), [filtered])
|
||||
|
||||
const categoryOrder = React.useMemo(
|
||||
() => Object.keys(grouped).sort((a, b) => a.localeCompare(b)),
|
||||
[grouped],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copiedPath)
|
||||
return undefined
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
setCopiedPath(null)
|
||||
}, 1200)
|
||||
|
||||
return () => window.clearTimeout(timerId)
|
||||
}, [copiedPath])
|
||||
|
||||
const handleCopy = React.useCallback((text: string) => {
|
||||
navigator.clipboard?.writeText(text)
|
||||
.then(() => {
|
||||
setCopiedPath(text)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy icon path:', err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<header style={headerStyle}>
|
||||
<h1 style={{ margin: 0 }}>Icon Gallery</h1>
|
||||
<p style={{ margin: 0, color: '#5f5f66' }}>
|
||||
Browse all icon components sourced from <code>app/components/base/icons/src</code>. Use the search bar
|
||||
to filter by name or path.
|
||||
</p>
|
||||
<div style={controlsStyle}>
|
||||
<input
|
||||
style={searchInputStyle}
|
||||
placeholder="Search icons"
|
||||
value={query}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
/>
|
||||
<span style={{ color: '#5f5f66' }}>{filtered.length} icons</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTheme(prev => (prev === 'light' ? 'dark' : 'light'))}
|
||||
style={toggleButtonStyle}
|
||||
>
|
||||
Toggle {previewTheme === 'light' ? 'dark' : 'light'} preview
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{categoryOrder.length === 0 && (
|
||||
<p style={emptyTextStyle}>No icons match the current filter.</p>
|
||||
)}
|
||||
{categoryOrder.map(category => (
|
||||
<section key={category} style={sectionStyle}>
|
||||
<h2 style={{ margin: 0, fontSize: 18 }}>{category}</h2>
|
||||
<div style={gridStyle}>
|
||||
{grouped[category].map(entry => (
|
||||
<div key={entry.path} style={cardStyle}>
|
||||
<div
|
||||
style={{
|
||||
...previewBaseStyle,
|
||||
background: previewTheme === 'dark' ? '#1f2024' : '#fff',
|
||||
}}
|
||||
>
|
||||
<entry.Component style={{ width: PREVIEW_SIZE, height: PREVIEW_SIZE }} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(entry.path)}
|
||||
style={{
|
||||
...nameButtonBaseStyle,
|
||||
color: copiedPath === entry.path ? '#00754a' : '#24262c',
|
||||
}}
|
||||
>
|
||||
{copiedPath === entry.path ? 'Copied!' : entry.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof IconGalleryStory> = {
|
||||
title: 'Base/Icons/Icon Gallery',
|
||||
component: IconGalleryStory,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconGalleryStory>
|
||||
|
||||
export const All: Story = {
|
||||
render: () => <IconGalleryStory />,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { useStore } from '@/app/components/app/store'
|
|||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
|
||||
const SAMPLE_APP_DETAIL = {
|
||||
id: 'app-demo-1',
|
||||
|
|
@ -143,10 +144,12 @@ const MessageLogPreview = (props: MessageLogModalProps) => {
|
|||
|
||||
return (
|
||||
<div className="relative min-h-[640px] w-full bg-background-default-subtle p-6">
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import { debounce } from 'lodash-es'
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LogViewer from '../log-viewer'
|
||||
import { usePluginSubscriptionStore } from '../store'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { useSubscriptionList } from '../use-subscription-list'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
|
|
@ -91,7 +91,7 @@ const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
|
|||
export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
const { refetch } = useSubscriptionList()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
message: t('pluginTrigger.subscription.createSuccess'),
|
||||
})
|
||||
onClose()
|
||||
refresh?.()
|
||||
refetch?.()
|
||||
},
|
||||
onError: async (error: any) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed')
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Toast from '@/app/components/base/toast'
|
|||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginSubscriptionStore } from './store'
|
||||
import { useSubscriptionList } from './use-subscription-list'
|
||||
|
||||
type Props = {
|
||||
onClose: (deleted: boolean) => void
|
||||
|
|
@ -18,7 +18,7 @@ const tPrefix = 'pluginTrigger.subscription.list.item.actions.deleteConfirm'
|
|||
|
||||
export const DeleteConfirm = (props: Props) => {
|
||||
const { onClose, isShow, currentId, currentName, workflowsInUse } = props
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
const { refetch } = useSubscriptionList()
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
const { t } = useTranslation()
|
||||
const [inputName, setInputName] = useState('')
|
||||
|
|
@ -40,7 +40,7 @@ export const DeleteConfirm = (props: Props) => {
|
|||
message: t(`${tPrefix}.success`, { name: currentName }),
|
||||
className: 'z-[10000001]',
|
||||
})
|
||||
refresh?.()
|
||||
refetch?.()
|
||||
onClose(true)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
type ShapeSubscription = {
|
||||
refresh?: () => void
|
||||
setRefresh: (refresh: () => void) => void
|
||||
}
|
||||
|
||||
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
|
||||
refresh: undefined,
|
||||
setRefresh: (refresh: () => void) => set({ refresh }),
|
||||
}))
|
||||
|
|
@ -1,19 +1,11 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import { usePluginStore } from '../store'
|
||||
import { usePluginSubscriptionStore } from './store'
|
||||
|
||||
export const useSubscriptionList = () => {
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { setRefresh } = usePluginSubscriptionStore()
|
||||
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(detail?.provider || '')
|
||||
|
||||
useEffect(() => {
|
||||
if (refetch)
|
||||
setRefresh(refetch)
|
||||
}, [refetch, setRefresh])
|
||||
|
||||
return {
|
||||
detail,
|
||||
subscriptions,
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ import { useDocLink } from '@/context/i18n'
|
|||
|
||||
export type IAppCardProps = {
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
triggerModeDisabled?: boolean // align with Trigger Node vs User Input exclusivity
|
||||
triggerModeMessage?: React.ReactNode // display-only message explaining the trigger restriction
|
||||
}
|
||||
|
||||
function MCPServiceCard({
|
||||
appInfo,
|
||||
triggerModeDisabled = false,
|
||||
triggerModeMessage = '',
|
||||
}: IAppCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
|
@ -79,7 +83,7 @@ function MCPServiceCard({
|
|||
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
|
||||
const missingStartNode = isWorkflowApp && !hasStartNode
|
||||
const hasInsufficientPermissions = !isCurrentWorkspaceEditor
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
|
||||
const isMinimalState = appUnpublished || missingStartNode
|
||||
|
||||
const [activated, setActivated] = useState(serverActivated)
|
||||
|
|
@ -144,7 +148,18 @@ function MCPServiceCard({
|
|||
return (
|
||||
<>
|
||||
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight', isMinimalState && 'h-12')}>
|
||||
<div className='rounded-xl bg-background-default'>
|
||||
<div className={cn('relative rounded-xl bg-background-default', triggerModeDisabled && 'opacity-60')}>
|
||||
{triggerModeDisabled && (
|
||||
triggerModeMessage ? (
|
||||
<Tooltip
|
||||
popupContent={triggerModeMessage}
|
||||
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
|
||||
position="right"
|
||||
>
|
||||
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
|
||||
</Tooltip>
|
||||
) : <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
|
||||
)}
|
||||
<div className={cn('flex w-full flex-col items-start justify-center gap-3 self-stretch p-3', isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle')}>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<div className='flex grow items-center'>
|
||||
|
|
@ -182,7 +197,7 @@ function MCPServiceCard({
|
|||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
</div>
|
||||
</>
|
||||
) : ''
|
||||
) : triggerModeMessage || ''
|
||||
) : ''
|
||||
}
|
||||
position="right"
|
||||
|
|
|
|||
|
|
@ -249,6 +249,8 @@ export const useChecklistBeforePublish = () => {
|
|||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
let checkData = data
|
||||
|
|
@ -366,17 +368,22 @@ export const useChecklistBeforePublish = () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!validNodes.find(n => n.id === node.id)) {
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
|
||||
if (isUnconnected && !canSkipConnectionCheck) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
|
@ -391,7 +398,7 @@ export const useChecklistBeforePublish = () => {
|
|||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTriggerPlugin?.subscription_constructor) {
|
||||
if (currentTriggerPlugin) {
|
||||
setDetail({
|
||||
name: currentTriggerPlugin.label[language],
|
||||
plugin_id: currentTriggerPlugin.plugin_id || '',
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ const translation = {
|
|||
running: 'In Service',
|
||||
disable: 'Disabled',
|
||||
},
|
||||
disableTooltip: {
|
||||
triggerMode: 'The {{feature}} feature is not supported in Trigger Node mode.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analysis',
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ const translation = {
|
|||
running: '稼働中',
|
||||
disable: '無効',
|
||||
},
|
||||
disableTooltip: {
|
||||
triggerMode: 'トリガーノードモードでは{{feature}}機能を使用できません。',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: '分析',
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ const translation = {
|
|||
running: '运行中',
|
||||
disable: '已停用',
|
||||
},
|
||||
disableTooltip: {
|
||||
triggerMode: '触发节点模式下不支持{{feature}}功能。',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: '分析',
|
||||
|
|
|
|||
Loading…
Reference in New Issue