mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 23:00:25 -04:00
refactor(web): split app publisher menu content into sections and add tests
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import type { AppPublisherMenuContentProps } from '../menu-content.types'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import MenuContent from '../menu-content'
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/upgrade-btn', () => ({
|
||||
default: () => <div data-testid="upgrade-btn">upgrade-btn</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({
|
||||
default: () => <div data-testid="workflow-tool-configure-button">workflow-tool-configure-button</div>,
|
||||
}))
|
||||
|
||||
const createSystemFeatures = (overrides: Partial<SystemFeatures> = {}): SystemFeatures => ({
|
||||
...defaultSystemFeatures,
|
||||
...overrides,
|
||||
branding: {
|
||||
...defaultSystemFeatures.branding,
|
||||
...overrides.branding,
|
||||
},
|
||||
license: {
|
||||
...defaultSystemFeatures.license,
|
||||
...overrides.license,
|
||||
},
|
||||
plugin_installation_permission: {
|
||||
...defaultSystemFeatures.plugin_installation_permission,
|
||||
...overrides.plugin_installation_permission,
|
||||
},
|
||||
webapp_auth: {
|
||||
...defaultSystemFeatures.webapp_auth,
|
||||
...overrides.webapp_auth,
|
||||
sso_config: {
|
||||
...defaultSystemFeatures.webapp_auth.sso_config,
|
||||
...overrides.webapp_auth?.sso_config,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const createAppDetail = (overrides: Partial<AppDetailResponse> = {}): AppDetailResponse => ({
|
||||
access_mode: AccessMode.PUBLIC,
|
||||
description: 'Workflow description',
|
||||
icon: '🤖',
|
||||
icon_background: '#ffffff',
|
||||
icon_type: 'emoji',
|
||||
id: 'app-1',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
name: 'Workflow app',
|
||||
...overrides,
|
||||
} as AppDetailResponse)
|
||||
|
||||
const createProps = (overrides: Partial<AppPublisherMenuContentProps> = {}): AppPublisherMenuContentProps => ({
|
||||
appDetail: createAppDetail(),
|
||||
appURL: '/apps/app-1',
|
||||
debugWithMultipleModel: false,
|
||||
disabledFunctionButton: false,
|
||||
disabledFunctionTooltip: undefined,
|
||||
draftUpdatedAt: 5678,
|
||||
formatTimeFromNow: time => `from-now:${time}`,
|
||||
hasHumanInputNode: false,
|
||||
hasTriggerNode: false,
|
||||
inputs: [],
|
||||
isAppAccessSet: true,
|
||||
isChatApp: false,
|
||||
isGettingAppWhiteListSubjects: false,
|
||||
isGettingUserCanAccessApp: false,
|
||||
missingStartNode: false,
|
||||
multipleModelConfigs: [],
|
||||
onOpenEmbedding: vi.fn(),
|
||||
onOpenInExplore: vi.fn(),
|
||||
onPublish: vi.fn(),
|
||||
onPublishToMarketplace: vi.fn(),
|
||||
onRefreshData: vi.fn(),
|
||||
onRestore: vi.fn(),
|
||||
onShowAppAccessControl: vi.fn(),
|
||||
outputs: [],
|
||||
publishDisabled: false,
|
||||
published: false,
|
||||
publishedAt: 1234,
|
||||
publishingToMarketplace: false,
|
||||
publishLoading: false,
|
||||
startNodeLimitExceeded: false,
|
||||
systemFeatures: createSystemFeatures(),
|
||||
toolPublished: false,
|
||||
upgradeHighlightStyle: { backgroundImage: 'linear-gradient(90deg, red, blue)' },
|
||||
workflowToolDisabled: false,
|
||||
workflowToolMessage: undefined,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const renderMenuContent = (overrides: Partial<AppPublisherMenuContentProps> = {}) => {
|
||||
return render(<MenuContent {...createProps(overrides)} />)
|
||||
}
|
||||
|
||||
describe('AppPublisherMenuContent', () => {
|
||||
it('should render published metadata and trigger restore for chat apps', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onRestore = vi.fn()
|
||||
|
||||
renderMenuContent({
|
||||
isChatApp: true,
|
||||
onRestore,
|
||||
publishedAt: 1234,
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.common.latestPublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/workflow\.common\.publishedAt/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/from-now:1234/)).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.common.restore' }))
|
||||
|
||||
expect(onRestore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render draft state, start node limit hint, and publish action for unpublished apps', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onPublish = vi.fn()
|
||||
|
||||
renderMenuContent({
|
||||
draftUpdatedAt: 5678,
|
||||
onPublish,
|
||||
publishedAt: undefined,
|
||||
startNodeLimitExceeded: true,
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/workflow\.common\.autoSaved/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/from-now:5678/)).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.publishLimit.startNodeTitlePrefix')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.publishLimit.startNodeTitleSuffix')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.publishLimit.startNodeDesc')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.common\.publishUpdate/i }))
|
||||
|
||||
expect(onPublish).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render access control status and open the access dialog when clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onShowAppAccessControl = vi.fn()
|
||||
|
||||
renderMenuContent({
|
||||
appDetail: createAppDetail({ access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS }),
|
||||
isAppAccessSet: false,
|
||||
onShowAppAccessControl,
|
||||
systemFeatures: createSystemFeatures({
|
||||
webapp_auth: {
|
||||
...defaultSystemFeatures.webapp_auth,
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(screen.getByText('app.publishApp.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.accessControlDialog.accessItems.specific')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.publishApp.notSet')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.publishApp.notSetDesc')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('app.publishApp.notSet'))
|
||||
|
||||
expect(onShowAppAccessControl).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render workflow actions and call the explore handler when requested', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenInExplore = vi.fn()
|
||||
|
||||
renderMenuContent({
|
||||
onOpenInExplore,
|
||||
publishedAt: 1234,
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.common.runApp')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.batchRunApp')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.openInExplore')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.accessAPIReference')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-tool-configure-button')).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByText('workflow.common.runApp').closest('a')).toHaveAttribute('href', '/apps/app-1')
|
||||
expect(screen.getByText('workflow.common.batchRunApp').closest('a')).toHaveAttribute('href', '/apps/app-1?mode=batch')
|
||||
|
||||
await user.click(screen.getByText('workflow.common.openInExplore'))
|
||||
|
||||
expect(onOpenInExplore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable unavailable actions and surface marketplace publishing state', () => {
|
||||
renderMenuContent({
|
||||
disabledFunctionButton: true,
|
||||
disabledFunctionTooltip: 'permission denied',
|
||||
missingStartNode: true,
|
||||
publishingToMarketplace: true,
|
||||
systemFeatures: createSystemFeatures({
|
||||
enable_creators_platform: true,
|
||||
}),
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.common.runApp').closest('a')).not.toHaveAttribute('href')
|
||||
expect(screen.getByText('workflow.common.batchRunApp').closest('a')).not.toHaveAttribute('href')
|
||||
expect(screen.getByText('workflow.common.openInExplore').closest('a')).not.toHaveAttribute('href')
|
||||
expect(screen.getByText('workflow.common.accessAPIReference').closest('a')).not.toHaveAttribute('href')
|
||||
|
||||
expect(screen.getByText('workflow.common.publishingToMarketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide suggested actions when the workflow already has a trigger node', () => {
|
||||
renderMenuContent({
|
||||
hasTriggerNode: true,
|
||||
})
|
||||
|
||||
expect(screen.queryByText('workflow.common.runApp')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('workflow-tool-configure-button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not attempt to open explore when the action is disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenInExplore = vi.fn()
|
||||
|
||||
renderMenuContent({
|
||||
disabledFunctionButton: true,
|
||||
disabledFunctionTooltip: 'permission denied',
|
||||
onOpenInExplore,
|
||||
publishedAt: undefined,
|
||||
})
|
||||
|
||||
await user.click(screen.getByText('workflow.common.openInExplore'))
|
||||
|
||||
expect(onOpenInExplore).not.toHaveBeenCalled()
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,8 @@ import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import {
|
||||
RiLoader2Line,
|
||||
RiStore2Line,
|
||||
} from '@remixicon/react'
|
||||
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import {
|
||||
memo,
|
||||
@@ -22,19 +18,15 @@ import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
@@ -47,58 +39,14 @@ import { useInvalidateAppWorkflow } from '@/service/use-workflow'
|
||||
import { fetchPublishedWorkflow } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Divider from '../../base/divider'
|
||||
import Loading from '../../base/loading'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
|
||||
type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'>
|
||||
import AppPublisherMenuContent from './menu-content'
|
||||
|
||||
type InstalledAppsResponse = {
|
||||
installed_apps?: InstalledApp[]
|
||||
}
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: AccessModeLabel, icon: string }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
label: 'organization',
|
||||
icon: 'i-ri-building-line',
|
||||
},
|
||||
[AccessMode.SPECIFIC_GROUPS_MEMBERS]: {
|
||||
label: 'specific',
|
||||
icon: 'i-ri-lock-line',
|
||||
},
|
||||
[AccessMode.PUBLIC]: {
|
||||
label: 'anyone',
|
||||
icon: 'i-ri-global-line',
|
||||
},
|
||||
[AccessMode.EXTERNAL_MEMBERS]: {
|
||||
label: 'external',
|
||||
icon: 'i-ri-verified-badge-line',
|
||||
},
|
||||
}
|
||||
|
||||
const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!mode || !ACCESS_MODE_MAP[mode])
|
||||
return null
|
||||
|
||||
const { icon, label } = ACCESS_MODE_MAP[mode]
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={`${icon} h-4 w-4 shrink-0 text-text-secondary`} />
|
||||
<div className="grow truncate">
|
||||
<span className="text-text-secondary system-sm-medium">{t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
publishDisabled?: boolean
|
||||
@@ -124,8 +72,6 @@ export type AppPublisherProps = {
|
||||
hasHumanInputNode?: boolean
|
||||
}
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
|
||||
const AppPublisher = ({
|
||||
disabled = false,
|
||||
publishDisabled = false,
|
||||
@@ -180,7 +126,12 @@ const AppPublisher = ({
|
||||
return true
|
||||
}, [appAccessSubjects, appDetail])
|
||||
|
||||
const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp])
|
||||
const noAccessPermission = useMemo(() => Boolean(
|
||||
systemFeatures.webapp_auth.enabled
|
||||
&& appDetail
|
||||
&& appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS
|
||||
&& !userCanAccessApp?.result,
|
||||
), [systemFeatures, appDetail, userCanAccessApp])
|
||||
const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission])
|
||||
|
||||
const disabledFunctionTooltip = useMemo(() => {
|
||||
@@ -333,7 +284,6 @@ const AppPublisher = ({
|
||||
const hasPublishedVersion = !!publishedAt
|
||||
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
|
||||
const workflowToolMessage = workflowToolDisabled ? t('common.workflowAsToolDisabledHint', { ns: 'workflow' }) : undefined
|
||||
const showStartNodeLimitHint = Boolean(startNodeLimitExceeded)
|
||||
const upgradeHighlightStyle = useMemo(() => ({
|
||||
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
@@ -364,220 +314,47 @@ const AppPublisher = ({
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[11]">
|
||||
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center text-text-tertiary system-xs-medium-uppercase">
|
||||
{publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })}
|
||||
</div>
|
||||
{publishedAt
|
||||
? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.publishedAt', { ns: 'workflow' })}
|
||||
{' '}
|
||||
{formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
{isChatApp && (
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
onClick={handleRestore}
|
||||
disabled={published}
|
||||
>
|
||||
{t('common.restore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.autoSaved', { ns: 'workflow' })}
|
||||
{' '}
|
||||
·
|
||||
{Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
|
||||
</div>
|
||||
)}
|
||||
{debugWithMultipleModel
|
||||
? (
|
||||
<PublishWithMultipleModel
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onSelect={item => handlePublish(item)}
|
||||
// textGenerationModelList={textGenerationModelList}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-3 w-full"
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published || publishLoading}
|
||||
loading={publishLoading}
|
||||
>
|
||||
{
|
||||
publishLoading
|
||||
? t('common.publishing', { ns: 'workflow' })
|
||||
: published
|
||||
? t('common.published', { ns: 'workflow' })
|
||||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
{showStartNodeLimitHint && (
|
||||
<div className="mt-3 flex flex-col items-stretch">
|
||||
<p
|
||||
className="text-sm font-semibold leading-5 text-transparent"
|
||||
style={upgradeHighlightStyle}
|
||||
>
|
||||
<span className="block">{t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })}</span>
|
||||
<span className="block">{t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })}</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-4 text-text-secondary">
|
||||
{t('publishLimit.startNodeDesc', { ns: 'workflow' })}
|
||||
</p>
|
||||
<UpgradeBtn
|
||||
isShort
|
||||
className="mb-[12px] mt-[9px] h-[32px] w-[93px] self-start"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(systemFeatures.webapp_auth.enabled && (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects))
|
||||
? <div className="py-2"><Loading /></div>
|
||||
: (
|
||||
<>
|
||||
<Divider className="my-0" />
|
||||
{systemFeatures.webapp_auth.enabled && (
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center">
|
||||
<p className="text-text-tertiary system-xs-medium">{t('publishApp.title', { ns: 'app' })}</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2 hover:bg-primary-50 hover:text-text-accent"
|
||||
onClick={() => {
|
||||
setShowAppAccessControl(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex grow items-center gap-x-1.5 overflow-hidden pr-1">
|
||||
<AccessModeDisplay mode={appDetail?.access_mode} />
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="shrink-0 text-text-tertiary system-xs-regular">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<span className="i-ri-arrow-right-s-line h-4 w-4 text-text-quaternary" />
|
||||
</div>
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="mt-1 text-text-warning system-xs-regular">{t('publishApp.notSetDesc', { ns: 'app' })}</p>}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
// Hide run/batch run app buttons when there is a trigger node.
|
||||
!hasTriggerNode && (
|
||||
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<Tooltip triggerClassName="flex" disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
link={appURL}
|
||||
icon={<span className="i-ri-play-circle-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.runApp', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION
|
||||
? (
|
||||
<Tooltip triggerClassName="flex" disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<span className="i-ri-play-list-2-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.batchRunApp', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className="h-4 w-4" />}
|
||||
>
|
||||
{t('common.embedIntoSite', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<Tooltip triggerClassName="flex" disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
if (publishedAt)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<span className="i-ri-planet-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.openInExplore', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
<Tooltip triggerClassName="flex" disabled={!!publishedAt && !missingStartNode} popupContent={!publishedAt ? t('notPublishedYet', { ns: 'app' }) : t('noUserInputNode', { ns: 'app' })} asChild={false}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={!publishedAt || missingStartNode}
|
||||
link="./develop"
|
||||
icon={<span className="i-ri-terminal-box-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.accessAPIReference', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={workflowToolDisabled}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
name={appDetail?.name}
|
||||
description={appDetail?.description}
|
||||
inputs={inputs}
|
||||
outputs={outputs}
|
||||
handlePublish={handlePublish}
|
||||
onRefreshData={onRefreshData}
|
||||
disabledReason={workflowToolMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{systemFeatures.enable_creators_platform && (
|
||||
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
onClick={handlePublishToMarketplace}
|
||||
disabled={publishingToMarketplace}
|
||||
icon={publishingToMarketplace
|
||||
? <RiLoader2Line className="h-4 w-4 animate-spin" />
|
||||
: <RiStore2Line className="h-4 w-4" />}
|
||||
>
|
||||
{publishingToMarketplace
|
||||
? t('common.publishingToMarketplace', { ns: 'workflow' })
|
||||
: t('common.publishToMarketplace', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<AppPublisherMenuContent
|
||||
publishedAt={publishedAt}
|
||||
draftUpdatedAt={draftUpdatedAt}
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
publishDisabled={publishDisabled}
|
||||
publishLoading={publishLoading}
|
||||
toolPublished={toolPublished}
|
||||
inputs={inputs}
|
||||
outputs={outputs}
|
||||
onRefreshData={onRefreshData}
|
||||
workflowToolAvailable={workflowToolAvailable}
|
||||
hasTriggerNode={hasTriggerNode}
|
||||
missingStartNode={missingStartNode}
|
||||
startNodeLimitExceeded={startNodeLimitExceeded}
|
||||
hasHumanInputNode={hasHumanInputNode}
|
||||
appDetail={appDetail}
|
||||
appURL={appURL}
|
||||
disabledFunctionButton={disabledFunctionButton}
|
||||
disabledFunctionTooltip={disabledFunctionTooltip}
|
||||
formatTimeFromNow={formatTimeFromNow}
|
||||
isAppAccessSet={isAppAccessSet}
|
||||
isChatApp={isChatApp}
|
||||
isGettingAppWhiteListSubjects={isGettingAppWhiteListSubjects}
|
||||
isGettingUserCanAccessApp={isGettingUserCanAccessApp}
|
||||
onOpenEmbedding={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
onOpenInExplore={handleOpenInExplore}
|
||||
onPublish={handlePublish}
|
||||
onPublishToMarketplace={handlePublishToMarketplace}
|
||||
onRestore={handleRestore}
|
||||
onShowAppAccessControl={() => setShowAppAccessControl(true)}
|
||||
published={published}
|
||||
publishingToMarketplace={publishingToMarketplace}
|
||||
systemFeatures={systemFeatures}
|
||||
upgradeHighlightStyle={upgradeHighlightStyle}
|
||||
workflowToolDisabled={workflowToolDisabled}
|
||||
workflowToolMessage={workflowToolMessage}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
<EmbeddedModal
|
||||
siteInfo={appDetail?.site}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AppPublisherMenuContentProps } from './menu-content.types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccessModeDisplay } from './menu-content-shared'
|
||||
|
||||
type MenuContentAccessSectionProps = Pick<
|
||||
AppPublisherMenuContentProps,
|
||||
| 'appDetail'
|
||||
| 'isAppAccessSet'
|
||||
| 'onShowAppAccessControl'
|
||||
| 'systemFeatures'
|
||||
>
|
||||
|
||||
const MenuContentAccessSection: FC<MenuContentAccessSectionProps> = ({
|
||||
appDetail,
|
||||
isAppAccessSet,
|
||||
onShowAppAccessControl,
|
||||
systemFeatures,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!systemFeatures.webapp_auth.enabled)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center">
|
||||
<p className="text-text-tertiary system-xs-medium">{t('publishApp.title', { ns: 'app' })}</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2 hover:bg-primary-50 hover:text-text-accent"
|
||||
onClick={onShowAppAccessControl}
|
||||
>
|
||||
<div className="flex grow items-center gap-x-1.5 overflow-hidden pr-1">
|
||||
<AccessModeDisplay mode={appDetail?.access_mode} />
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="shrink-0 text-text-tertiary system-xs-regular">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<span className="i-ri-arrow-right-s-line h-4 w-4 text-text-quaternary" />
|
||||
</div>
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="mt-1 text-text-warning system-xs-regular">{t('publishApp.notSetDesc', { ns: 'app' })}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuContentAccessSection
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AppPublisherMenuContentProps } from './menu-content.types'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { SuggestedActionWithTooltip } from './menu-content-shared'
|
||||
import { getBatchRunLink } from './menu-content.utils'
|
||||
import SuggestedAction from './suggested-action'
|
||||
|
||||
type MenuContentActionsSectionProps = Pick<
|
||||
AppPublisherMenuContentProps,
|
||||
| 'appDetail'
|
||||
| 'appURL'
|
||||
| 'disabledFunctionButton'
|
||||
| 'disabledFunctionTooltip'
|
||||
| 'hasHumanInputNode'
|
||||
| 'hasTriggerNode'
|
||||
| 'inputs'
|
||||
| 'missingStartNode'
|
||||
| 'onOpenEmbedding'
|
||||
| 'onOpenInExplore'
|
||||
| 'onPublish'
|
||||
| 'onRefreshData'
|
||||
| 'outputs'
|
||||
| 'published'
|
||||
| 'publishedAt'
|
||||
| 'toolPublished'
|
||||
| 'workflowToolDisabled'
|
||||
| 'workflowToolMessage'
|
||||
>
|
||||
|
||||
const MenuContentActionsSection: FC<MenuContentActionsSectionProps> = ({
|
||||
appDetail,
|
||||
appURL,
|
||||
disabledFunctionButton,
|
||||
disabledFunctionTooltip,
|
||||
hasHumanInputNode = false,
|
||||
hasTriggerNode = false,
|
||||
inputs,
|
||||
missingStartNode = false,
|
||||
onOpenEmbedding,
|
||||
onOpenInExplore,
|
||||
onPublish,
|
||||
onRefreshData,
|
||||
outputs,
|
||||
published,
|
||||
publishedAt,
|
||||
toolPublished,
|
||||
workflowToolDisabled,
|
||||
workflowToolMessage,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (hasTriggerNode)
|
||||
return null
|
||||
|
||||
const mode = appDetail?.mode
|
||||
const isBatchActionVisible = mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.COMPLETION
|
||||
const apiReferenceDisabled = !publishedAt || missingStartNode
|
||||
const apiReferenceTooltip = !publishedAt
|
||||
? t('notPublishedYet', { ns: 'app' })
|
||||
: missingStartNode
|
||||
? t('noUserInputNode', { ns: 'app' })
|
||||
: undefined
|
||||
|
||||
const handleOpenInExplore = () => {
|
||||
if (publishedAt) {
|
||||
onOpenInExplore()
|
||||
return
|
||||
}
|
||||
|
||||
toast.error(t('notPublishedYet', { ns: 'app' }))
|
||||
}
|
||||
|
||||
const handleWorkflowToolPublish = async (params?: PublishWorkflowParams) => {
|
||||
await onPublish(params)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<SuggestedActionWithTooltip
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<span className="i-ri-play-circle-line h-4 w-4" />}
|
||||
link={appURL}
|
||||
tooltip={disabledFunctionButton ? disabledFunctionTooltip : undefined}
|
||||
>
|
||||
{t('common.runApp', { ns: 'workflow' })}
|
||||
</SuggestedActionWithTooltip>
|
||||
{isBatchActionVisible
|
||||
? (
|
||||
<SuggestedActionWithTooltip
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<span className="i-ri-play-list-2-line h-4 w-4" />}
|
||||
link={getBatchRunLink(appURL)}
|
||||
tooltip={disabledFunctionButton ? disabledFunctionTooltip : undefined}
|
||||
>
|
||||
{t('common.batchRunApp', { ns: 'workflow' })}
|
||||
</SuggestedActionWithTooltip>
|
||||
)
|
||||
: (
|
||||
<SuggestedAction
|
||||
disabled={!publishedAt}
|
||||
icon={<span className="i-custom-vender-line-development-code-browser h-4 w-4" />}
|
||||
onClick={onOpenEmbedding}
|
||||
>
|
||||
{t('common.embedIntoSite', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<SuggestedActionWithTooltip
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<span className="i-ri-planet-line h-4 w-4" />}
|
||||
onClick={handleOpenInExplore}
|
||||
tooltip={disabledFunctionButton ? disabledFunctionTooltip : undefined}
|
||||
>
|
||||
{t('common.openInExplore', { ns: 'workflow' })}
|
||||
</SuggestedActionWithTooltip>
|
||||
<SuggestedActionWithTooltip
|
||||
className="flex-1"
|
||||
disabled={apiReferenceDisabled}
|
||||
icon={<span className="i-ri-terminal-box-line h-4 w-4" />}
|
||||
link="./develop"
|
||||
tooltip={apiReferenceTooltip}
|
||||
>
|
||||
{t('common.accessAPIReference', { ns: 'workflow' })}
|
||||
</SuggestedActionWithTooltip>
|
||||
{mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && appDetail && (
|
||||
<WorkflowToolConfigureButton
|
||||
description={appDetail.description}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
disabled={workflowToolDisabled}
|
||||
disabledReason={workflowToolMessage}
|
||||
handlePublish={handleWorkflowToolPublish}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
inputs={inputs}
|
||||
name={appDetail.name}
|
||||
onRefreshData={onRefreshData}
|
||||
outputs={outputs}
|
||||
published={!!toolPublished}
|
||||
workflowAppId={appDetail.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuContentActionsSection
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AppPublisherMenuContentProps } from './menu-content.types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SuggestedAction from './suggested-action'
|
||||
|
||||
type MenuContentMarketplaceSectionProps = Pick<
|
||||
AppPublisherMenuContentProps,
|
||||
| 'onPublishToMarketplace'
|
||||
| 'publishingToMarketplace'
|
||||
| 'systemFeatures'
|
||||
>
|
||||
|
||||
const MenuContentMarketplaceSection: FC<MenuContentMarketplaceSectionProps> = ({
|
||||
onPublishToMarketplace,
|
||||
publishingToMarketplace,
|
||||
systemFeatures,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!systemFeatures.enable_creators_platform)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={publishingToMarketplace}
|
||||
icon={publishingToMarketplace
|
||||
? <span className="i-ri-loader-2-line h-4 w-4 animate-spin" />
|
||||
: <span className="i-ri-store-2-line h-4 w-4" />}
|
||||
onClick={onPublishToMarketplace}
|
||||
>
|
||||
{publishingToMarketplace
|
||||
? t('common.publishingToMarketplace', { ns: 'workflow' })
|
||||
: t('common.publishToMarketplace', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuContentMarketplaceSection
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AppPublisherMenuContentProps } from './menu-content.types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import { PUBLISH_SHORTCUT } from './menu-content.utils'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
|
||||
type MenuContentPublishSectionProps = Pick<
|
||||
AppPublisherMenuContentProps,
|
||||
| 'debugWithMultipleModel'
|
||||
| 'draftUpdatedAt'
|
||||
| 'formatTimeFromNow'
|
||||
| 'isChatApp'
|
||||
| 'multipleModelConfigs'
|
||||
| 'onPublish'
|
||||
| 'onRestore'
|
||||
| 'publishDisabled'
|
||||
| 'published'
|
||||
| 'publishedAt'
|
||||
| 'publishLoading'
|
||||
| 'startNodeLimitExceeded'
|
||||
| 'upgradeHighlightStyle'
|
||||
>
|
||||
|
||||
const MenuContentPublishSection: FC<MenuContentPublishSectionProps> = ({
|
||||
debugWithMultipleModel = false,
|
||||
draftUpdatedAt,
|
||||
formatTimeFromNow,
|
||||
isChatApp,
|
||||
multipleModelConfigs = [],
|
||||
onPublish,
|
||||
onRestore,
|
||||
publishDisabled = false,
|
||||
published,
|
||||
publishedAt,
|
||||
publishLoading = false,
|
||||
startNodeLimitExceeded = false,
|
||||
upgradeHighlightStyle,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center text-text-tertiary system-xs-medium-uppercase">
|
||||
{publishedAt
|
||||
? t('common.latestPublished', { ns: 'workflow' })
|
||||
: t('common.currentDraftUnpublished', { ns: 'workflow' })}
|
||||
</div>
|
||||
{publishedAt
|
||||
? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.publishedAt', { ns: 'workflow' })}
|
||||
{' '}
|
||||
{formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
{isChatApp && (
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
onClick={onRestore}
|
||||
disabled={published}
|
||||
>
|
||||
{t('common.restore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.autoSaved', { ns: 'workflow' })}
|
||||
{' '}
|
||||
·
|
||||
{Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
|
||||
</div>
|
||||
)}
|
||||
{debugWithMultipleModel
|
||||
? (
|
||||
<PublishWithMultipleModel
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onSelect={item => onPublish(item)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-3 w-full"
|
||||
onClick={() => onPublish()}
|
||||
disabled={publishDisabled || published || publishLoading}
|
||||
loading={publishLoading}
|
||||
>
|
||||
{publishLoading
|
||||
? t('common.publishing', { ns: 'workflow' })
|
||||
: published
|
||||
? t('common.published', { ns: 'workflow' })
|
||||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{startNodeLimitExceeded && (
|
||||
<div className="mt-3 flex flex-col items-stretch">
|
||||
<p
|
||||
className="text-sm font-semibold leading-5 text-transparent"
|
||||
style={upgradeHighlightStyle}
|
||||
>
|
||||
<span className="block">{t('publishLimit.startNodeTitlePrefix', { ns: 'workflow' })}</span>
|
||||
<span className="block">{t('publishLimit.startNodeTitleSuffix', { ns: 'workflow' })}</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-4 text-text-secondary">
|
||||
{t('publishLimit.startNodeDesc', { ns: 'workflow' })}
|
||||
</p>
|
||||
<UpgradeBtn
|
||||
isShort
|
||||
className="mb-[12px] mt-[9px] h-[32px] w-[93px] self-start"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuContentPublishSection
|
||||
72
web/app/components/app/app-publisher/menu-content-shared.tsx
Normal file
72
web/app/components/app/app-publisher/menu-content-shared.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { SuggestedActionProps } from './suggested-action'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../base/ui/tooltip'
|
||||
import SuggestedAction from './suggested-action'
|
||||
|
||||
type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'>
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: AccessModeLabel, icon: string }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
label: 'organization',
|
||||
icon: 'i-ri-building-line',
|
||||
},
|
||||
[AccessMode.SPECIFIC_GROUPS_MEMBERS]: {
|
||||
label: 'specific',
|
||||
icon: 'i-ri-lock-line',
|
||||
},
|
||||
[AccessMode.PUBLIC]: {
|
||||
label: 'anyone',
|
||||
icon: 'i-ri-global-line',
|
||||
},
|
||||
[AccessMode.EXTERNAL_MEMBERS]: {
|
||||
label: 'external',
|
||||
icon: 'i-ri-verified-badge-line',
|
||||
},
|
||||
}
|
||||
|
||||
export const AccessModeDisplay: FC<{ mode?: AccessMode }> = ({ mode }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!mode || !ACCESS_MODE_MAP[mode])
|
||||
return null
|
||||
|
||||
const { icon, label } = ACCESS_MODE_MAP[mode]
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={`${icon} h-4 w-4 shrink-0 text-text-secondary`} />
|
||||
<div className="grow truncate">
|
||||
<span className="text-text-secondary system-sm-medium">{t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type SuggestedActionWithTooltipProps = SuggestedActionProps & {
|
||||
tooltip?: ReactNode
|
||||
}
|
||||
|
||||
export const SuggestedActionWithTooltip = ({
|
||||
children,
|
||||
tooltip,
|
||||
...props
|
||||
}: SuggestedActionWithTooltipProps) => {
|
||||
const action = (
|
||||
<SuggestedAction {...props}>
|
||||
{children}
|
||||
</SuggestedAction>
|
||||
)
|
||||
|
||||
if (!tooltip)
|
||||
return action
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={action} />
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
110
web/app/components/app/app-publisher/menu-content.tsx
Normal file
110
web/app/components/app/app-publisher/menu-content.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AppPublisherMenuContentProps } from './menu-content.types'
|
||||
import Divider from '../../base/divider'
|
||||
import Loading from '../../base/loading'
|
||||
import MenuContentAccessSection from './menu-content-access-section'
|
||||
import MenuContentActionsSection from './menu-content-actions-section'
|
||||
import MenuContentMarketplaceSection from './menu-content-marketplace-section'
|
||||
import MenuContentPublishSection from './menu-content-publish-section'
|
||||
|
||||
const AppPublisherMenuContent: FC<AppPublisherMenuContentProps> = ({
|
||||
appDetail,
|
||||
appURL,
|
||||
debugWithMultipleModel = false,
|
||||
disabledFunctionButton,
|
||||
disabledFunctionTooltip,
|
||||
draftUpdatedAt,
|
||||
formatTimeFromNow,
|
||||
hasHumanInputNode = false,
|
||||
hasTriggerNode = false,
|
||||
inputs,
|
||||
isAppAccessSet,
|
||||
isChatApp,
|
||||
isGettingAppWhiteListSubjects,
|
||||
isGettingUserCanAccessApp,
|
||||
missingStartNode = false,
|
||||
multipleModelConfigs = [],
|
||||
onOpenEmbedding,
|
||||
onOpenInExplore,
|
||||
onPublish,
|
||||
onPublishToMarketplace,
|
||||
onRefreshData,
|
||||
onRestore,
|
||||
onShowAppAccessControl,
|
||||
outputs,
|
||||
publishDisabled = false,
|
||||
published,
|
||||
publishedAt,
|
||||
publishingToMarketplace,
|
||||
publishLoading = false,
|
||||
startNodeLimitExceeded = false,
|
||||
systemFeatures,
|
||||
toolPublished,
|
||||
upgradeHighlightStyle,
|
||||
workflowToolAvailable: _workflowToolAvailable = true,
|
||||
workflowToolDisabled,
|
||||
workflowToolMessage,
|
||||
}) => {
|
||||
const showAccessLoading = systemFeatures.webapp_auth.enabled
|
||||
&& (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)
|
||||
|
||||
return (
|
||||
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
|
||||
<MenuContentPublishSection
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
draftUpdatedAt={draftUpdatedAt}
|
||||
formatTimeFromNow={formatTimeFromNow}
|
||||
isChatApp={isChatApp}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onPublish={onPublish}
|
||||
onRestore={onRestore}
|
||||
publishDisabled={publishDisabled}
|
||||
published={published}
|
||||
publishedAt={publishedAt}
|
||||
publishLoading={publishLoading}
|
||||
startNodeLimitExceeded={startNodeLimitExceeded}
|
||||
upgradeHighlightStyle={upgradeHighlightStyle}
|
||||
/>
|
||||
{showAccessLoading
|
||||
? <div className="py-2"><Loading /></div>
|
||||
: (
|
||||
<>
|
||||
<Divider className="my-0" />
|
||||
<MenuContentAccessSection
|
||||
appDetail={appDetail}
|
||||
isAppAccessSet={isAppAccessSet}
|
||||
onShowAppAccessControl={onShowAppAccessControl}
|
||||
systemFeatures={systemFeatures}
|
||||
/>
|
||||
<MenuContentActionsSection
|
||||
appDetail={appDetail}
|
||||
appURL={appURL}
|
||||
disabledFunctionButton={disabledFunctionButton}
|
||||
disabledFunctionTooltip={disabledFunctionTooltip}
|
||||
hasHumanInputNode={hasHumanInputNode}
|
||||
hasTriggerNode={hasTriggerNode}
|
||||
inputs={inputs}
|
||||
missingStartNode={missingStartNode}
|
||||
onOpenEmbedding={onOpenEmbedding}
|
||||
onOpenInExplore={onOpenInExplore}
|
||||
onPublish={onPublish}
|
||||
onRefreshData={onRefreshData}
|
||||
outputs={outputs}
|
||||
published={published}
|
||||
publishedAt={publishedAt}
|
||||
toolPublished={toolPublished}
|
||||
workflowToolDisabled={workflowToolDisabled}
|
||||
workflowToolMessage={workflowToolMessage}
|
||||
/>
|
||||
<MenuContentMarketplaceSection
|
||||
onPublishToMarketplace={onPublishToMarketplace}
|
||||
publishingToMarketplace={publishingToMarketplace}
|
||||
systemFeatures={systemFeatures}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppPublisherMenuContent
|
||||
47
web/app/components/app/app-publisher/menu-content.types.ts
Normal file
47
web/app/components/app/app-publisher/menu-content.types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { AppPublisherProps } from './index'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
|
||||
export type AppPublisherMenuContentProps = Pick<
|
||||
AppPublisherProps,
|
||||
| 'publishedAt'
|
||||
| 'draftUpdatedAt'
|
||||
| 'debugWithMultipleModel'
|
||||
| 'multipleModelConfigs'
|
||||
| 'publishDisabled'
|
||||
| 'publishLoading'
|
||||
| 'toolPublished'
|
||||
| 'inputs'
|
||||
| 'outputs'
|
||||
| 'onRefreshData'
|
||||
| 'workflowToolAvailable'
|
||||
| 'hasTriggerNode'
|
||||
| 'missingStartNode'
|
||||
| 'startNodeLimitExceeded'
|
||||
| 'hasHumanInputNode'
|
||||
> & {
|
||||
appDetail?: AppDetailResponse
|
||||
appURL: string
|
||||
disabledFunctionButton: boolean
|
||||
disabledFunctionTooltip?: string
|
||||
formatTimeFromNow: (time: number) => string
|
||||
isAppAccessSet: boolean
|
||||
isChatApp: boolean
|
||||
isGettingAppWhiteListSubjects: boolean
|
||||
isGettingUserCanAccessApp: boolean
|
||||
onOpenEmbedding: () => void
|
||||
onOpenInExplore: () => void
|
||||
onPublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void> | void
|
||||
onPublishToMarketplace: () => Promise<void> | void
|
||||
onRestore: () => Promise<void> | void
|
||||
onShowAppAccessControl: () => void
|
||||
published: boolean
|
||||
publishingToMarketplace: boolean
|
||||
systemFeatures: SystemFeatures
|
||||
upgradeHighlightStyle: CSSProperties
|
||||
workflowToolDisabled: boolean
|
||||
workflowToolMessage?: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
|
||||
export const getBatchRunLink = (appURL: string) => `${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ExternalDataToolProvider as Provider } from './helpers'
|
||||
import type {
|
||||
CodeBasedExtensionItem,
|
||||
ExternalDataTool,
|
||||
} from '@/models/common'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
@@ -16,21 +16,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions } from '@/service/use-common'
|
||||
import {
|
||||
formatExternalDataToolForSave,
|
||||
getExternalDataToolDefaultConfig,
|
||||
getExternalDataToolValidationError,
|
||||
getInitialExternalDataTool,
|
||||
|
||||
SYSTEM_EXTERNAL_DATA_TOOL_TYPES,
|
||||
} from './helpers'
|
||||
|
||||
const systemTypes = ['api']
|
||||
type ExternalDataToolModalProps = {
|
||||
data: ExternalDataTool
|
||||
onCancel: () => void
|
||||
onSave: (externalDataTool: ExternalDataTool) => void
|
||||
onValidateBeforeSave?: (externalDataTool: ExternalDataTool) => boolean
|
||||
}
|
||||
type Provider = {
|
||||
key: string
|
||||
name: string
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
data,
|
||||
onCancel,
|
||||
@@ -40,7 +41,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const locale = useLocale()
|
||||
const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
|
||||
const [localeData, setLocaleData] = useState(getInitialExternalDataTool(data))
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('external_data_tool')
|
||||
|
||||
@@ -64,19 +65,10 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
const currentProvider = providers.find(provider => provider.key === localeData.type)
|
||||
|
||||
const handleDataTypeChange = (type: string) => {
|
||||
let config: undefined | Record<string, any>
|
||||
const currProvider = providers.find(provider => provider.key === type)
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
||||
prev[next.variable] = next.default
|
||||
return prev
|
||||
}, {} as Record<string, any>)
|
||||
}
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
type,
|
||||
config,
|
||||
config: getExternalDataToolDefaultConfig(type, providers),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,70 +99,30 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const formatData = (originData: ExternalDataTool) => {
|
||||
const { type, config } = originData
|
||||
const params: Record<string, string | undefined> = {}
|
||||
|
||||
if (type === 'api')
|
||||
params.api_based_extension_id = config?.api_based_extension_id
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
|
||||
currentProvider.form_schema.forEach((form) => {
|
||||
params[form.variable] = config?.[form.variable]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...originData,
|
||||
type,
|
||||
enabled: data.type ? data.enabled : true,
|
||||
config: {
|
||||
...params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!localeData.type) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) }))
|
||||
const validationError = getExternalDataToolValidationError({
|
||||
localeData,
|
||||
currentProvider,
|
||||
locale,
|
||||
})
|
||||
if (validationError) {
|
||||
const translatedLabel = validationError.label.includes('.')
|
||||
? t(validationError.label, validationError.label, { ns: 'appDebug' })
|
||||
: validationError.label
|
||||
|
||||
if (validationError.kind === 'invalid')
|
||||
toast.error(t('varKeyError.notValid', { ns: 'appDebug', key: translatedLabel }))
|
||||
else
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: translatedLabel }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!localeData.label) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!localeData.variable) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.variable && !/^[a-z_]\w{0,29}$/i.test(localeData.variable)) {
|
||||
toast.error(t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formattedData = formatData(localeData)
|
||||
const formattedData = formatExternalDataToolForSave(localeData, currentProvider, data.type ? !!data.enabled : true)
|
||||
|
||||
if (onValidateBeforeSave && !onValidateBeforeSave(formattedData))
|
||||
return
|
||||
|
||||
onSave(formatData(formattedData))
|
||||
onSave(formatExternalDataToolForSave(formattedData, currentProvider, !!formattedData.enabled))
|
||||
}
|
||||
|
||||
const action = data.type ? t('operation.edit', { ns: 'common' }) : t('operation.add', { ns: 'common' })
|
||||
@@ -258,7 +210,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
!SYSTEM_EXTERNAL_DATA_TOOL_TYPES.includes(localeData.type as typeof SYSTEM_EXTERNAL_DATA_TOOL_TYPES[number])
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
|
||||
184
web/app/components/app/configuration/tools/helpers.ts
Normal file
184
web/app/components/app/configuration/tools/helpers.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import type {
|
||||
CodeBasedExtensionItem,
|
||||
ExternalDataTool,
|
||||
} from '@/models/common'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
|
||||
export const SYSTEM_EXTERNAL_DATA_TOOL_TYPES = ['api'] as const
|
||||
|
||||
export type ExternalDataToolProvider = {
|
||||
key: string
|
||||
name: string
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
|
||||
type PromptVariableLike = {
|
||||
key: string
|
||||
}
|
||||
|
||||
type ExternalDataToolValidationError = {
|
||||
kind: 'required' | 'invalid'
|
||||
label: string
|
||||
}
|
||||
|
||||
const isSystemExternalDataToolType = (type?: string) => (
|
||||
!!type && SYSTEM_EXTERNAL_DATA_TOOL_TYPES.includes(type as typeof SYSTEM_EXTERNAL_DATA_TOOL_TYPES[number])
|
||||
)
|
||||
|
||||
const getLocalizedLabel = (
|
||||
label: Record<string, string> | undefined,
|
||||
locale: string,
|
||||
) => {
|
||||
if (!label)
|
||||
return ''
|
||||
|
||||
if (locale === LanguagesSupported[1])
|
||||
return label['zh-Hans'] || label['en-US'] || ''
|
||||
|
||||
return label['en-US'] || label['zh-Hans'] || ''
|
||||
}
|
||||
|
||||
export const getInitialExternalDataTool = (data: ExternalDataTool): ExternalDataTool => (
|
||||
data.type ? data : { ...data, type: 'api' }
|
||||
)
|
||||
|
||||
export const getExternalDataToolDefaultConfig = (
|
||||
type: string,
|
||||
providers: ExternalDataToolProvider[],
|
||||
) => {
|
||||
const provider = providers.find(item => item.key === type)
|
||||
if (isSystemExternalDataToolType(type) || !provider?.form_schema)
|
||||
return undefined
|
||||
|
||||
return provider.form_schema.reduce<Record<string, unknown>>((acc, field) => {
|
||||
acc[field.variable] = field.default
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const upsertExternalDataTool = (
|
||||
externalDataTools: ExternalDataTool[],
|
||||
externalDataTool: ExternalDataTool,
|
||||
index: number,
|
||||
) => {
|
||||
if (index > -1) {
|
||||
return [
|
||||
...externalDataTools.slice(0, index),
|
||||
externalDataTool,
|
||||
...externalDataTools.slice(index + 1),
|
||||
]
|
||||
}
|
||||
|
||||
return [...externalDataTools, externalDataTool]
|
||||
}
|
||||
|
||||
export const removeExternalDataTool = (
|
||||
externalDataTools: ExternalDataTool[],
|
||||
index: number,
|
||||
) => {
|
||||
return [
|
||||
...externalDataTools.slice(0, index),
|
||||
...externalDataTools.slice(index + 1),
|
||||
]
|
||||
}
|
||||
|
||||
export const findExternalDataToolVariableConflict = (
|
||||
variable: string | undefined,
|
||||
externalDataTools: ExternalDataTool[],
|
||||
promptVariables: PromptVariableLike[],
|
||||
index: number,
|
||||
) => {
|
||||
if (!variable)
|
||||
return undefined
|
||||
|
||||
const promptVariable = promptVariables.find(item => item.key === variable)
|
||||
if (promptVariable)
|
||||
return promptVariable.key
|
||||
|
||||
return externalDataTools
|
||||
.filter((_item, toolIndex) => toolIndex !== index)
|
||||
.find(item => item.variable === variable)
|
||||
?.variable
|
||||
}
|
||||
|
||||
export const formatExternalDataToolForSave = (
|
||||
originData: ExternalDataTool,
|
||||
currentProvider: ExternalDataToolProvider | undefined,
|
||||
fallbackEnabled: boolean,
|
||||
) => {
|
||||
const { type, config } = originData
|
||||
const params: Record<string, unknown> = {}
|
||||
|
||||
if (type === 'api')
|
||||
params.api_based_extension_id = config?.api_based_extension_id
|
||||
|
||||
if (!isSystemExternalDataToolType(type) && currentProvider?.form_schema) {
|
||||
currentProvider.form_schema.forEach((field) => {
|
||||
params[field.variable] = config?.[field.variable]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...originData,
|
||||
type,
|
||||
enabled: fallbackEnabled,
|
||||
config: params,
|
||||
}
|
||||
}
|
||||
|
||||
export const getExternalDataToolValidationError = ({
|
||||
localeData,
|
||||
currentProvider,
|
||||
locale,
|
||||
}: {
|
||||
localeData: ExternalDataTool
|
||||
currentProvider?: ExternalDataToolProvider
|
||||
locale: string
|
||||
}): ExternalDataToolValidationError | null => {
|
||||
if (!localeData.type) {
|
||||
return {
|
||||
kind: 'required',
|
||||
label: 'feature.tools.modal.toolType.title',
|
||||
}
|
||||
}
|
||||
|
||||
if (!localeData.label) {
|
||||
return {
|
||||
kind: 'required',
|
||||
label: 'feature.tools.modal.name.title',
|
||||
}
|
||||
}
|
||||
|
||||
if (!localeData.variable) {
|
||||
return {
|
||||
kind: 'required',
|
||||
label: 'feature.tools.modal.variableName.title',
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^[a-z_]\w{0,29}$/i.test(localeData.variable)) {
|
||||
return {
|
||||
kind: 'invalid',
|
||||
label: 'feature.tools.modal.variableName.title',
|
||||
}
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) {
|
||||
return {
|
||||
kind: 'required',
|
||||
label: locale === LanguagesSupported[1] ? 'API 扩展' : 'API Extension',
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSystemExternalDataToolType(localeData.type) && currentProvider?.form_schema) {
|
||||
const requiredField = currentProvider.form_schema.find(field => !localeData.config?.[field.variable] && field.required)
|
||||
if (requiredField) {
|
||||
return {
|
||||
kind: 'required',
|
||||
label: getLocalizedLabel(requiredField.label, locale),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -22,6 +22,11 @@ import {
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import {
|
||||
findExternalDataToolVariableConflict,
|
||||
removeExternalDataTool,
|
||||
upsertExternalDataTool,
|
||||
} from './helpers'
|
||||
|
||||
const Tools = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -35,42 +40,20 @@ const Tools = () => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleSaveExternalDataToolModal = (externalDataTool: ExternalDataTool, index: number) => {
|
||||
if (index > -1) {
|
||||
setExternalDataToolsConfig([
|
||||
...externalDataToolsConfig.slice(0, index),
|
||||
externalDataTool,
|
||||
...externalDataToolsConfig.slice(index + 1),
|
||||
])
|
||||
}
|
||||
else {
|
||||
setExternalDataToolsConfig([...externalDataToolsConfig, externalDataTool])
|
||||
}
|
||||
setExternalDataToolsConfig(upsertExternalDataTool(externalDataToolsConfig, externalDataTool, index))
|
||||
}
|
||||
|
||||
const handleValidateBeforeSaveExternalDataToolModal = (newExternalDataTool: ExternalDataTool, index: number) => {
|
||||
const promptVariables = modelConfig?.configs?.prompt_variables || []
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let existedExternalDataTools = []
|
||||
if (index > -1) {
|
||||
existedExternalDataTools = [
|
||||
...externalDataToolsConfig.slice(0, index),
|
||||
...externalDataToolsConfig.slice(index + 1),
|
||||
]
|
||||
}
|
||||
else {
|
||||
existedExternalDataTools = [...externalDataToolsConfig]
|
||||
}
|
||||
|
||||
for (let i = 0; i < existedExternalDataTools.length; i++) {
|
||||
if (existedExternalDataTools[i].variable === newExternalDataTool.variable) {
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }))
|
||||
return false
|
||||
}
|
||||
const conflictKey = findExternalDataToolVariableConflict(
|
||||
newExternalDataTool.variable,
|
||||
externalDataToolsConfig,
|
||||
promptVariables,
|
||||
index,
|
||||
)
|
||||
if (conflictKey) {
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: conflictKey }))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -181,7 +164,7 @@ const Tools = () => {
|
||||
</div>
|
||||
<div
|
||||
className="group/action hidden h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-[#FEE4E2] group-hover:flex"
|
||||
onClick={() => setExternalDataToolsConfig([...externalDataToolsConfig.slice(0, index), ...externalDataToolsConfig.slice(index + 1)])}
|
||||
onClick={() => setExternalDataToolsConfig(removeExternalDataTool(externalDataToolsConfig, index))}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-gray-500 group-hover/action:text-[#D92D20]" />
|
||||
</div>
|
||||
|
||||
141
web/app/components/app/text-generate/item/action-bar.tsx
Normal file
141
web/app/components/app/text-generate/item/action-bar.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import {
|
||||
RiBookmark3Line,
|
||||
RiClipboardLine,
|
||||
RiFileList3Line,
|
||||
RiResetLeftLine,
|
||||
RiSparklingLine,
|
||||
RiThumbDownLine,
|
||||
RiThumbUpLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
type GenerationItemActionBarProps = {
|
||||
appSourceType: AppSourceType
|
||||
currentTab: string
|
||||
depth: number
|
||||
feedback?: FeedbackType
|
||||
isError: boolean
|
||||
isInWebApp: boolean
|
||||
isResponding?: boolean
|
||||
isShowTextToSpeech?: boolean
|
||||
isTryApp: boolean
|
||||
isWorkflow?: boolean
|
||||
messageId?: string | null
|
||||
moreLikeThis?: boolean
|
||||
onCopy: () => void
|
||||
onFeedback?: (feedback: FeedbackType) => void
|
||||
onMoreLikeThis: () => void
|
||||
onOpenLogModal: () => void
|
||||
onRetry: () => void
|
||||
onSave?: (messageId: string) => void
|
||||
supportFeedback?: boolean
|
||||
voice?: string
|
||||
workflowProcessData?: WorkflowProcess
|
||||
}
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
|
||||
const GenerationItemActionBar = ({
|
||||
appSourceType,
|
||||
currentTab,
|
||||
depth,
|
||||
feedback,
|
||||
isError,
|
||||
isInWebApp,
|
||||
isResponding,
|
||||
isShowTextToSpeech,
|
||||
isTryApp,
|
||||
isWorkflow,
|
||||
messageId,
|
||||
moreLikeThis,
|
||||
onCopy,
|
||||
onFeedback,
|
||||
onMoreLikeThis,
|
||||
onOpenLogModal,
|
||||
onRetry,
|
||||
onSave,
|
||||
supportFeedback,
|
||||
voice,
|
||||
workflowProcessData,
|
||||
}: GenerationItemActionBarProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isInWebApp && appSourceType !== AppSourceType.installedApp && !isResponding && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
<ActionButton disabled={isError || !messageId} onClick={onOpenLogModal}>
|
||||
<RiFileList3Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{moreLikeThis && !isTryApp && (
|
||||
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={onMoreLikeThis}>
|
||||
<RiSparklingLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isShowTextToSpeech && !isTryApp && messageId && (
|
||||
<NewAudioButton
|
||||
id={messageId}
|
||||
voice={voice}
|
||||
/>
|
||||
)}
|
||||
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
|
||||
<ActionButton
|
||||
disabled={isError || !messageId}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<RiClipboardLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && isError && (
|
||||
<ActionButton onClick={onRetry}>
|
||||
<RiResetLeftLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && !isWorkflow && !isTryApp && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => messageId && onSave?.(messageId)}>
|
||||
<RiBookmark3Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{!feedback?.rating && (
|
||||
<>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
|
||||
<RiThumbUpLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
|
||||
<RiThumbDownLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{feedback?.rating === 'like' && (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbUpLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{feedback?.rating === 'dislike' && (
|
||||
<ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbDownLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{depth > MAX_DEPTH && (
|
||||
<span className="sr-only">{t('errorMessage.waitForResponse', { ns: 'appDebug' })}</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GenerationItemActionBar
|
||||
@@ -4,15 +4,8 @@ import type { FeedbackType, IChatItem } from '@/app/components/base/chat/chat/ty
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import {
|
||||
RiBookmark3Line,
|
||||
RiClipboardLine,
|
||||
RiFileList3Line,
|
||||
RiPlayList2Line,
|
||||
RiResetLeftLine,
|
||||
RiSparklingFill,
|
||||
RiSparklingLine,
|
||||
RiThumbDownLine,
|
||||
RiThumbUpLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
@@ -20,21 +13,17 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import HumanInputFilledFormList from '@/app/components/base/chat/chat/answer/human-input-filled-form-list'
|
||||
import HumanInputFormList from '@/app/components/base/chat/chat/answer/human-input-form-list'
|
||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||
import { AppSourceType, fetchMoreLikeThis, submitHumanInputForm, updateFeedback } from '@/service/share'
|
||||
import { submitHumanInputForm as submitHumanInputFormService } from '@/service/workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ResultTab from './result-tab'
|
||||
import GenerationItemActionBar from './action-bar'
|
||||
import WorkflowContent from './workflow-content'
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
|
||||
@@ -44,7 +33,7 @@ export type IGenerationItemProps = {
|
||||
className?: string
|
||||
isError: boolean
|
||||
onRetry: () => void
|
||||
content: any
|
||||
content: unknown
|
||||
messageId?: string | null
|
||||
conversationId?: string
|
||||
isLoading?: boolean
|
||||
@@ -123,7 +112,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
|
||||
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
|
||||
|
||||
const childProps = {
|
||||
const childProps: IGenerationItemProps = {
|
||||
isInWebApp,
|
||||
content: completionRes,
|
||||
messageId: childMessageId,
|
||||
@@ -141,6 +130,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
isWorkflow,
|
||||
siteInfo,
|
||||
taskId,
|
||||
isError: false,
|
||||
onRetry,
|
||||
}
|
||||
|
||||
const handleMoreLikeThis = async () => {
|
||||
@@ -207,7 +198,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
}
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>('DETAIL')
|
||||
const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length || (workflowProcessData?.humanInputFormDataList && workflowProcessData?.humanInputFormDataList.length > 0) || (workflowProcessData?.humanInputFilledFormDataList && workflowProcessData?.humanInputFilledFormDataList.length > 0)
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
}
|
||||
@@ -224,6 +214,15 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
await submitHumanInputForm(formToken, formData)
|
||||
}, [appSourceType])
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const copyContent = isWorkflow ? workflowProcessData?.resultText : content
|
||||
if (typeof copyContent === 'string')
|
||||
copy(copyContent)
|
||||
else
|
||||
copy(JSON.stringify(copyContent))
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}, [content, isWorkflow, t, workflowProcessData?.resultText])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('relative', !isTop && 'mt-3', className)}>
|
||||
@@ -240,71 +239,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
>
|
||||
{workflowProcessData && (
|
||||
<>
|
||||
<div className={cn(
|
||||
'p-3',
|
||||
showResultTabs && 'border-b border-divider-subtle',
|
||||
)}
|
||||
>
|
||||
{taskId && (
|
||||
<div className={cn('mb-2 flex items-center text-text-accent-secondary system-2xs-medium-uppercase', isError && 'text-text-destructive')}>
|
||||
<RiPlayList2Line className="mr-1 h-3 w-3" />
|
||||
<span>{t('generation.execution', { ns: 'share' })}</span>
|
||||
<span className="px-1">·</span>
|
||||
<span>{taskId}</span>
|
||||
</div>
|
||||
)}
|
||||
{siteInfo && workflowProcessData && (
|
||||
<WorkflowProcessItem
|
||||
data={workflowProcessData}
|
||||
expand={workflowProcessData.expand}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
hideInfo={hideProcessDetail}
|
||||
readonly={!siteInfo.show_workflow_steps}
|
||||
/>
|
||||
)}
|
||||
{showResultTabs && (
|
||||
<div className="flex items-center space-x-6 px-1">
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary system-sm-semibold-uppercase',
|
||||
currentTab === 'RESULT' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => switchTab('RESULT')}
|
||||
>
|
||||
{t('result', { ns: 'runLog' })}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary system-sm-semibold-uppercase',
|
||||
currentTab === 'DETAIL' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isError && (
|
||||
<>
|
||||
{currentTab === 'RESULT' && workflowProcessData.humanInputFormDataList && workflowProcessData.humanInputFormDataList.length > 0 && (
|
||||
<div className="px-4 pt-4">
|
||||
<HumanInputFormList
|
||||
humanInputFormDataList={workflowProcessData.humanInputFormDataList}
|
||||
onHumanInputFormSubmit={handleSubmitHumanInputForm}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentTab === 'RESULT' && workflowProcessData.humanInputFilledFormDataList && workflowProcessData.humanInputFilledFormDataList.length > 0 && (
|
||||
<div className="px-4 pt-4">
|
||||
<HumanInputFilledFormList
|
||||
humanInputFilledFormDataList={workflowProcessData.humanInputFilledFormDataList}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
|
||||
</>
|
||||
)}
|
||||
<WorkflowContent
|
||||
content={content}
|
||||
currentTab={currentTab}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
isError={isError}
|
||||
onSubmitHumanInputForm={handleSubmitHumanInputForm}
|
||||
onSwitchTab={switchTab}
|
||||
siteInfo={siteInfo}
|
||||
taskId={taskId}
|
||||
workflowProcessData={workflowProcessData}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!workflowProcessData && taskId && (
|
||||
@@ -327,88 +272,41 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
{/* meta data */}
|
||||
<div className={cn(
|
||||
'relative mt-1 h-4 px-4 text-text-quaternary system-xs-regular',
|
||||
isMobile && ((childMessageId || isQuerying) && depth < 3) && 'pl-10',
|
||||
isMobile && ((childMessageId || isQuerying) && depth < MAX_DEPTH) && 'pl-10',
|
||||
)}
|
||||
>
|
||||
{!isWorkflow && (
|
||||
<span>
|
||||
{content?.length}
|
||||
{typeof content === 'string' ? content.length : 0}
|
||||
{' '}
|
||||
{t('unit.char', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
{/* action buttons */}
|
||||
<div className="absolute bottom-1 right-2 flex items-center">
|
||||
{!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
|
||||
<RiFileList3Line className="h-4 w-4" />
|
||||
{/* <div>{t('common.operation.log')}</div> */}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{moreLikeThis && !isTryApp && (
|
||||
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
|
||||
<RiSparklingLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isShowTextToSpeech && !isTryApp && (
|
||||
<NewAudioButton
|
||||
id={messageId!}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
)}
|
||||
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
|
||||
<ActionButton
|
||||
disabled={isError || !messageId}
|
||||
onClick={() => {
|
||||
const copyContent = isWorkflow ? workflowProcessData?.resultText : content
|
||||
if (typeof copyContent === 'string')
|
||||
copy(copyContent)
|
||||
else
|
||||
copy(JSON.stringify(copyContent))
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<RiClipboardLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && isError && (
|
||||
<ActionButton onClick={onRetry}>
|
||||
<RiResetLeftLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && !isWorkflow && !isTryApp && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
||||
<RiBookmark3Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{!feedback?.rating && (
|
||||
<>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
|
||||
<RiThumbUpLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
|
||||
<RiThumbDownLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{feedback?.rating === 'like' && (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbUpLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{feedback?.rating === 'dislike' && (
|
||||
<ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbDownLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<GenerationItemActionBar
|
||||
appSourceType={appSourceType}
|
||||
currentTab={currentTab}
|
||||
depth={depth}
|
||||
feedback={feedback}
|
||||
isError={isError}
|
||||
isInWebApp={isInWebApp}
|
||||
isResponding={isResponding}
|
||||
isShowTextToSpeech={isShowTextToSpeech}
|
||||
isTryApp={isTryApp}
|
||||
isWorkflow={isWorkflow}
|
||||
messageId={messageId}
|
||||
moreLikeThis={moreLikeThis}
|
||||
onCopy={handleCopy}
|
||||
onFeedback={onFeedback}
|
||||
onMoreLikeThis={handleMoreLikeThis}
|
||||
onOpenLogModal={handleOpenLogModal}
|
||||
onRetry={onRetry}
|
||||
onSave={onSave}
|
||||
supportFeedback={supportFeedback}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
workflowProcessData={workflowProcessData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* more like this elements */}
|
||||
@@ -431,8 +329,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{((childMessageId || isQuerying) && depth < 3) && (
|
||||
<GenerationItem {...childProps as any} />
|
||||
{((childMessageId || isQuerying) && depth < MAX_DEPTH) && (
|
||||
<GenerationItem {...childProps} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
116
web/app/components/app/text-generate/item/workflow-content.tsx
Normal file
116
web/app/components/app/text-generate/item/workflow-content.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import {
|
||||
RiPlayList2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import HumanInputFilledFormList from '@/app/components/base/chat/chat/answer/human-input-filled-form-list'
|
||||
import HumanInputFormList from '@/app/components/base/chat/chat/answer/human-input-form-list'
|
||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ResultTab from './result-tab'
|
||||
|
||||
type WorkflowContentProps = {
|
||||
content: unknown
|
||||
currentTab: string
|
||||
hideProcessDetail?: boolean
|
||||
isError: boolean
|
||||
onSubmitHumanInputForm: (formToken: string, formData: { inputs: Record<string, string>, action: string }) => Promise<void>
|
||||
onSwitchTab: (tab: string) => void
|
||||
siteInfo: SiteInfo | null
|
||||
taskId?: string
|
||||
workflowProcessData: WorkflowProcess
|
||||
}
|
||||
|
||||
const WorkflowContent: FC<WorkflowContentProps> = ({
|
||||
content,
|
||||
currentTab,
|
||||
hideProcessDetail,
|
||||
isError,
|
||||
onSubmitHumanInputForm,
|
||||
onSwitchTab,
|
||||
siteInfo,
|
||||
taskId,
|
||||
workflowProcessData,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const showResultTabs = !!workflowProcessData.resultText
|
||||
|| !!workflowProcessData.files?.length
|
||||
|| !!workflowProcessData.humanInputFormDataList?.length
|
||||
|| !!workflowProcessData.humanInputFilledFormDataList?.length
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(
|
||||
'p-3',
|
||||
showResultTabs && 'border-b border-divider-subtle',
|
||||
)}
|
||||
>
|
||||
{taskId && (
|
||||
<div className={cn('mb-2 flex items-center text-text-accent-secondary system-2xs-medium-uppercase', isError && 'text-text-destructive')}>
|
||||
<RiPlayList2Line className="mr-1 h-3 w-3" />
|
||||
<span>{t('generation.execution', { ns: 'share' })}</span>
|
||||
<span className="px-1">·</span>
|
||||
<span>{taskId}</span>
|
||||
</div>
|
||||
)}
|
||||
{siteInfo && (
|
||||
<WorkflowProcessItem
|
||||
data={workflowProcessData}
|
||||
expand={workflowProcessData.expand}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
hideInfo={hideProcessDetail}
|
||||
readonly={!siteInfo.show_workflow_steps}
|
||||
/>
|
||||
)}
|
||||
{showResultTabs && (
|
||||
<div className="flex items-center space-x-6 px-1">
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary system-sm-semibold-uppercase',
|
||||
currentTab === 'RESULT' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => onSwitchTab('RESULT')}
|
||||
>
|
||||
{t('result', { ns: 'runLog' })}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary system-sm-semibold-uppercase',
|
||||
currentTab === 'DETAIL' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
|
||||
)}
|
||||
onClick={() => onSwitchTab('DETAIL')}
|
||||
>
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isError && (
|
||||
<>
|
||||
{currentTab === 'RESULT' && workflowProcessData.humanInputFormDataList && workflowProcessData.humanInputFormDataList.length > 0 && (
|
||||
<div className="px-4 pt-4">
|
||||
<HumanInputFormList
|
||||
humanInputFormDataList={workflowProcessData.humanInputFormDataList}
|
||||
onHumanInputFormSubmit={onSubmitHumanInputForm}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentTab === 'RESULT' && workflowProcessData.humanInputFilledFormDataList && workflowProcessData.humanInputFilledFormDataList.length > 0 && (
|
||||
<div className="px-4 pt-4">
|
||||
<HumanInputFilledFormList
|
||||
humanInputFilledFormDataList={workflowProcessData.humanInputFilledFormDataList}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowContent
|
||||
Reference in New Issue
Block a user