dify/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx
盐粒 Yanli 52c106b532
feat(agent-v2): sync nightly updates to main (2026-06-25) (#37915)
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: Yansong Zhang <916125788@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-26 13:13:35 +00:00

336 lines
12 KiB
TypeScript

'use client'
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
import type { AppSiteUpdatePayload } from '@dify/contracts/api/console/apps/types.gen'
import type { ConfigParams, SettingsAppInfo } from '@/app/components/app/overview/settings'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import CustomizeModal from '@/app/components/app/overview/customize'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import SettingsModal from '@/app/components/app/overview/settings'
import ShareQRCode from '@/app/components/base/qrcode'
import { AccessMode } from '@/models/access-control'
import { consoleQuery } from '@/service/client'
import { AppModeEnum } from '@/types/app'
import { accessSurfaceActionClassName, AccessSurfaceCard } from './access-surface-card'
export function WebAppAccessCard({
agent,
agentId,
isLoading,
}: {
agent?: AgentAppDetailWithSite
agentId: string
isLoading: boolean
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const queryClient = useQueryClient()
const appId = agent?.app_id
const apiBaseUrl = agent?.api_base_url
const site = agent?.site
const accessToken = site?.access_token ?? site?.code
const appBaseUrl = site?.app_base_url || (typeof window === 'undefined' ? '' : window.location.origin)
const webAppUrl = getAgentWebAppUrl(agent)
const isEnabled = Boolean(agent?.enable_site)
const canManageWebApp = Boolean(appId)
const embeddedConfig = appId && accessToken
? {
accessToken,
appBaseUrl,
siteInfo: {
title: site?.title ?? agent?.name ?? '',
chat_color_theme: site?.chat_color_theme ?? undefined,
chat_color_theme_inverted: site?.chat_color_theme_inverted ?? undefined,
},
}
: null
const settingsAppInfo = agent ? createSettingsAppInfo(agent) : null
const customizeConfig = appId && apiBaseUrl
? {
apiBaseUrl,
appId,
}
: null
const showSsoBadge = agent?.access_mode === AccessMode.EXTERNAL_MEMBERS
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
const [showEmbeddedModal, setShowEmbeddedModal] = useState(false)
const [showSettingsModal, setShowSettingsModal] = useState(false)
const agentDetailQueryKey = consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } })
const toggleSiteMutation = useMutation(consoleQuery.apps.byAppId.siteEnable.post.mutationOptions({
onSuccess: (_updatedApp, variables) => {
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
agentDetailQueryKey,
agentDetail => agentDetail
? {
...agentDetail,
enable_site: variables.body.enable_site,
}
: agentDetail,
)
toast.success(tCommon('actionMsg.modifiedSuccessfully'))
},
onError: () => {
toast.error(tCommon('actionMsg.modifiedUnsuccessfully'))
},
}))
const resetAccessTokenMutation = useMutation(consoleQuery.apps.byAppId.site.accessTokenReset.post.mutationOptions({
onSuccess: (site) => {
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
agentDetailQueryKey,
(agentDetail) => {
if (!agentDetail || !agentDetail.site)
return agentDetail
return {
...agentDetail,
site: {
...agentDetail.site,
...site,
access_token: site.code,
},
}
},
)
toast.success(tCommon('actionMsg.generatedSuccessfully'))
},
onError: () => {
toast.error(tCommon('actionMsg.generatedUnsuccessfully'))
},
}))
const updateSiteMutation = useMutation(consoleQuery.apps.byAppId.site.post.mutationOptions())
const isBusy = toggleSiteMutation.isPending || resetAccessTokenMutation.isPending || updateSiteMutation.isPending
function handleEnabledChange(enabled: boolean) {
if (!appId)
return
toggleSiteMutation.mutate({
params: {
app_id: appId,
},
body: {
enable_site: enabled,
},
})
}
function handleRefreshUrl() {
if (!appId)
return
resetAccessTokenMutation.mutate({
params: {
app_id: appId,
},
})
}
async function handleSaveSettings(params: ConfigParams) {
if (!appId)
return
const { enable_sso: _enableSso, ...body } = params
const sitePayload = body satisfies AppSiteUpdatePayload
try {
const updatedSite = await updateSiteMutation.mutateAsync({
params: {
app_id: appId,
},
body: sitePayload,
})
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
agentDetailQueryKey,
agentDetail => agentDetail
? {
...agentDetail,
site: {
...agentDetail.site,
...updatedSite,
...sitePayload,
access_token: updatedSite.code ?? agentDetail.site?.access_token ?? agentDetail.site?.code ?? null,
code: updatedSite.code ?? agentDetail.site?.code ?? agentDetail.site?.access_token ?? null,
app_base_url: agentDetail.site?.app_base_url ?? site?.app_base_url ?? null,
icon_url: agentDetail.site?.icon_url ?? null,
},
}
: agentDetail,
)
await queryClient.invalidateQueries({ queryKey: agentDetailQueryKey })
toast.success(tCommon('actionMsg.modifiedSuccessfully'))
}
catch {
toast.error(tCommon('actionMsg.modifiedUnsuccessfully'))
}
}
return (
<AccessSurfaceCard
title={t('agentDetail.access.webApp.title')}
icon="i-ri-window-line"
iconClassName="bg-state-accent-solid text-text-primary-on-surface"
endpointLabel={t('agentDetail.access.webApp.accessUrl')}
endpoint={webAppUrl}
enabled={isEnabled}
onEnabledChange={handleEnabledChange}
copyLabel={t('agentDetail.access.copyAccessUrl')}
badge={showSsoBadge ? <SsoBadge /> : undefined}
endpointActions={webAppUrl
? (
<>
<span className="mx-1.5 h-3.5 w-px shrink-0 bg-divider-regular" />
<ShareQRCode content={webAppUrl} />
<Button
variant="ghost"
size="small"
className="size-6 shrink-0 px-0 text-text-tertiary hover:text-text-secondary"
aria-label={t('agentDetail.access.webApp.refreshUrl')}
disabled={!canManageWebApp || isBusy}
onClick={handleRefreshUrl}
>
<span aria-hidden className="i-ri-refresh-line size-4" />
</Button>
</>
)
: undefined}
disabled={isLoading || !canManageWebApp}
busy={isBusy}
>
{webAppUrl && isEnabled
? (
<a
href={webAppUrl}
target="_blank"
rel="noreferrer"
aria-label={t('agentDetail.access.webApp.actions.launch')}
className={accessSurfaceActionClassName}
>
<span aria-hidden className="i-ri-external-link-line size-4" />
{t('agentDetail.access.webApp.actions.launch')}
</a>
)
: (
<Button variant="secondary" size="medium" className="gap-1.5 px-3" disabled>
<span aria-hidden className="i-ri-external-link-line size-4" />
{t('agentDetail.access.webApp.actions.launch')}
</Button>
)}
<Button
variant="secondary"
size="medium"
className="gap-1.5 px-3"
disabled={!embeddedConfig}
onClick={() => setShowEmbeddedModal(true)}
>
<span aria-hidden className="i-ri-window-line size-4" />
{t('agentDetail.access.webApp.actions.embedded')}
</Button>
<Button
variant="secondary"
size="medium"
className="gap-1.5 px-3"
disabled={!customizeConfig}
onClick={() => setShowCustomizeModal(true)}
>
<span aria-hidden className="i-ri-paint-brush-line size-4" />
{t('agentDetail.access.webApp.actions.customize')}
</Button>
<Button
variant="secondary"
size="medium"
className="gap-1.5 px-3"
disabled={!settingsAppInfo || updateSiteMutation.isPending}
onClick={() => setShowSettingsModal(true)}
>
<span aria-hidden className="i-ri-equalizer-2-line size-4" />
{t('agentDetail.access.webApp.actions.settings')}
</Button>
{settingsAppInfo && (
<SettingsModal
isChat
appInfo={settingsAppInfo}
isShow={showSettingsModal}
onClose={() => setShowSettingsModal(false)}
onSave={handleSaveSettings}
/>
)}
{customizeConfig && (
<CustomizeModal
isShow={showCustomizeModal}
onClose={() => setShowCustomizeModal(false)}
appId={customizeConfig.appId}
api_base_url={customizeConfig.apiBaseUrl}
sourceCodeRepository="webapp-conversation"
/>
)}
{embeddedConfig && (
<EmbeddedModal
isShow={showEmbeddedModal}
onClose={() => setShowEmbeddedModal(false)}
appBaseUrl={embeddedConfig.appBaseUrl}
accessToken={embeddedConfig.accessToken}
siteInfo={embeddedConfig.siteInfo}
webAppRoute="agent"
/>
)}
</AccessSurfaceCard>
)
}
function createSettingsAppInfo(agent: AgentAppDetailWithSite): SettingsAppInfo | null {
const site = agent.site
const appId = agent.app_id
if (!site || !appId)
return null
return {
id: appId,
mode: AppModeEnum.CHAT,
site: {
title: site.title ?? agent.name,
description: site.description ?? agent.description ?? '',
default_language: (site.default_language ?? 'en-US') as SettingsAppInfo['site']['default_language'],
chat_color_theme: site.chat_color_theme ?? '',
chat_color_theme_inverted: site.chat_color_theme_inverted ?? false,
copyright: site.copyright ?? '',
privacy_policy: site.privacy_policy ?? '',
custom_disclaimer: site.custom_disclaimer ?? '',
input_placeholder: site.input_placeholder ?? '',
icon_type: site.icon_type === 'image' || site.icon_type === 'emoji' || site.icon_type === 'link'
? site.icon_type
: 'emoji',
icon: site.icon ?? agent.icon ?? '',
icon_background: site.icon_background ?? agent.icon_background ?? null,
icon_url: site.icon_url ?? null,
show_workflow_steps: site.show_workflow_steps ?? false,
use_icon_as_answer_icon: site.use_icon_as_answer_icon ?? false,
},
}
}
function getAgentWebAppUrl(agent?: AgentAppDetailWithSite) {
const site = agent?.site
const token = site?.access_token ?? site?.code
if (!token)
return ''
const baseUrl = site?.app_base_url || (typeof window === 'undefined' ? '' : window.location.origin)
return `${baseUrl.replace(/\/$/, '')}/agent/${token}`
}
function SsoBadge() {
const { t } = useTranslation('agentV2')
return (
<span className="inline-flex h-4.5 shrink-0 items-center gap-1 rounded-sm border border-divider-deep px-1.5 system-2xs-semibold-uppercase text-text-tertiary">
<span aria-hidden className="i-ri-shield-check-line size-3" />
{t('agentDetail.access.webApp.ssoEnabled')}
</span>
)
}