mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 14:51:13 +08:00
feat: embed agent webapp
This commit is contained in:
parent
01c138e7a9
commit
4ddc9ceb5c
@ -920,11 +920,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/customize/index.tsx": {
|
||||
"jsx-a11y/anchor-has-content": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/settings/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
|
||||
20
web/__tests__/proxy-frame-options.spec.ts
Normal file
20
web/__tests__/proxy-frame-options.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canEmbedPath } from '@/proxy'
|
||||
|
||||
describe('proxy frame options', () => {
|
||||
it('should allow embedded share routes', () => {
|
||||
expect(canEmbedPath('/chatbot/token')).toBe(true)
|
||||
expect(canEmbedPath('/workflow/token')).toBe(true)
|
||||
expect(canEmbedPath('/completion/token')).toBe(true)
|
||||
expect(canEmbedPath('/webapp-signin')).toBe(true)
|
||||
expect(canEmbedPath('/agent/token')).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny non-embedded console routes by default', () => {
|
||||
expect(canEmbedPath('/agents')).toBe(false)
|
||||
expect(canEmbedPath('/agent-settings')).toBe(false)
|
||||
expect(canEmbedPath('/agentic')).toBe(false)
|
||||
expect(canEmbedPath('/roster/agent/agent-1/access')).toBe(false)
|
||||
expect(canEmbedPath('/apps')).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -285,6 +285,18 @@ describe('app-card-utils', () => {
|
||||
expect(snippet).not.toContain('isDev: true')
|
||||
})
|
||||
|
||||
it('should generate an agent embedded script route when requested', () => {
|
||||
const snippet = getEmbeddedScriptSnippet({
|
||||
url: 'https://example.com',
|
||||
token: 'agent-token',
|
||||
webAppRoute: 'agent',
|
||||
primaryColor: '#1C64F2',
|
||||
inputValues: {},
|
||||
})
|
||||
|
||||
expect(snippet).toContain('routeSegment: \'agent\'')
|
||||
})
|
||||
|
||||
it('should compress and encode base64 using CompressionStream when available', async () => {
|
||||
const result = await compressAndEncodeBase64('hello')
|
||||
expect(typeof result).toBe('string')
|
||||
|
||||
@ -11,6 +11,7 @@ type OverviewCardType = 'api' | 'webapp'
|
||||
|
||||
export type OverviewOperationKey = 'launch' | 'embedded' | 'customize' | 'settings' | 'develop'
|
||||
export type WorkflowLaunchInputValue = string | boolean
|
||||
export type EmbeddedWebAppRoute = 'chatbot' | 'agent'
|
||||
export type WorkflowHiddenStartVariable = Pick<
|
||||
InputVar,
|
||||
'default' | 'hide' | 'label' | 'max_length' | 'options' | 'required' | 'type' | 'variable'
|
||||
@ -156,12 +157,14 @@ ${entries.map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`).join('\
|
||||
export const getEmbeddedScriptSnippet = ({
|
||||
url,
|
||||
token,
|
||||
webAppRoute = 'chatbot',
|
||||
primaryColor,
|
||||
isTestEnv,
|
||||
inputValues,
|
||||
}: {
|
||||
url: string
|
||||
token: string
|
||||
webAppRoute?: EmbeddedWebAppRoute
|
||||
primaryColor: string
|
||||
isTestEnv?: boolean
|
||||
inputValues: Record<string, WorkflowLaunchInputValue>
|
||||
@ -174,6 +177,9 @@ export const getEmbeddedScriptSnippet = ({
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}${basePath}'`
|
||||
: ''}${webAppRoute !== 'chatbot'
|
||||
? `,
|
||||
routeSegment: '${webAppRoute}'`
|
||||
: ''},
|
||||
inputs: ${getScriptInputsContent(inputValues)},
|
||||
systemVariables: {
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import type { MutableRefObject } from 'react'
|
||||
import type { WorkflowHiddenStartVariable, WorkflowLaunchInputValue } from '../app-card-utils'
|
||||
import type { EmbeddedWebAppRoute, WorkflowHiddenStartVariable, WorkflowLaunchInputValue } from '../app-card-utils'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -33,6 +29,7 @@ type Props = Readonly<{
|
||||
onClose: () => void
|
||||
accessToken?: string
|
||||
appBaseUrl?: string
|
||||
webAppRoute?: EmbeddedWebAppRoute
|
||||
hiddenInputs?: WorkflowHiddenStartVariable[]
|
||||
className?: string
|
||||
}>
|
||||
@ -62,15 +59,17 @@ const getSerializedHiddenInputValue = (
|
||||
const buildEmbeddedIframeUrl = async ({
|
||||
appBaseUrl,
|
||||
accessToken,
|
||||
webAppRoute,
|
||||
variables,
|
||||
values,
|
||||
}: {
|
||||
appBaseUrl: string
|
||||
accessToken: string
|
||||
webAppRoute: EmbeddedWebAppRoute
|
||||
variables: WorkflowHiddenStartVariable[]
|
||||
values: Record<string, WorkflowLaunchInputValue>
|
||||
}) => {
|
||||
const iframeUrl = new URL(`${appBaseUrl}${basePath}/chatbot/${accessToken}`, window.location.origin)
|
||||
const iframeUrl = new URL(`${appBaseUrl}${basePath}/${webAppRoute}/${accessToken}`, window.location.origin)
|
||||
|
||||
await Promise.all(variables.map(async (variable) => {
|
||||
iframeUrl.searchParams.set(variable.variable, await compressAndEncodeBase64(getSerializedHiddenInputValue(variable, values)))
|
||||
@ -101,8 +100,9 @@ const EmbeddedContent = ({
|
||||
siteInfo,
|
||||
appBaseUrl,
|
||||
accessToken,
|
||||
webAppRoute = 'chatbot',
|
||||
hiddenInputs,
|
||||
}: Required<Pick<Props, 'accessToken' | 'appBaseUrl'>> & Pick<Props, 'siteInfo' | 'hiddenInputs'>) => {
|
||||
}: Required<Pick<Props, 'accessToken' | 'appBaseUrl'>> & Pick<Props, 'siteInfo' | 'webAppRoute' | 'hiddenInputs'>) => {
|
||||
const { t } = useTranslation()
|
||||
const supportedHiddenInputs = useMemo<WorkflowHiddenStartVariable[]>(
|
||||
() => (hiddenInputs ?? []).filter(isWorkflowLaunchInputSupported),
|
||||
@ -122,6 +122,7 @@ const EmbeddedContent = ({
|
||||
() => buildEmbeddedIframeUrl({
|
||||
appBaseUrl,
|
||||
accessToken,
|
||||
webAppRoute,
|
||||
variables: supportedHiddenInputs,
|
||||
values: initialHiddenInputValues,
|
||||
}),
|
||||
@ -143,6 +144,7 @@ const EmbeddedContent = ({
|
||||
setPreviewIframeUrlPromise(buildEmbeddedIframeUrl({
|
||||
appBaseUrl,
|
||||
accessToken,
|
||||
webAppRoute,
|
||||
variables: supportedHiddenInputs,
|
||||
values: nextHiddenInputValues,
|
||||
}))
|
||||
@ -150,15 +152,17 @@ const EmbeddedContent = ({
|
||||
const scriptsContent = useMemo(() => getEmbeddedScriptSnippet({
|
||||
url: appBaseUrl,
|
||||
token: accessToken,
|
||||
webAppRoute,
|
||||
primaryColor: themeBuilder.theme?.primaryColor ?? '#1C64F2',
|
||||
isTestEnv,
|
||||
inputValues: hiddenInputValues,
|
||||
}), [accessToken, appBaseUrl, hiddenInputValues, isTestEnv, themeBuilder.theme?.primaryColor])
|
||||
}), [accessToken, appBaseUrl, hiddenInputValues, isTestEnv, themeBuilder.theme?.primaryColor, webAppRoute])
|
||||
|
||||
const onClickCopy = async () => {
|
||||
const latestIframeUrl = await buildEmbeddedIframeUrl({
|
||||
appBaseUrl,
|
||||
accessToken,
|
||||
webAppRoute,
|
||||
variables: supportedHiddenInputs,
|
||||
values: hiddenInputValues,
|
||||
})
|
||||
@ -211,8 +215,8 @@ const EmbeddedContent = ({
|
||||
</div>
|
||||
</div>
|
||||
{hiddenInputsCollapsed
|
||||
? <RiArrowRightSLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
: <RiArrowDownSLine className="size-4 shrink-0 text-text-tertiary" />}
|
||||
? <span aria-hidden className="i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary" />
|
||||
: <span aria-hidden className="i-ri-arrow-down-s-line size-4 shrink-0 text-text-tertiary" />}
|
||||
</button>
|
||||
{!hiddenInputsCollapsed && (
|
||||
<div className="max-h-72 space-y-4 overflow-y-auto border-t-[0.5px] border-divider-subtle px-4 py-4">
|
||||
@ -307,7 +311,7 @@ const EmbeddedContent = ({
|
||||
)
|
||||
}
|
||||
|
||||
const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, hiddenInputs, className }: Props) => {
|
||||
const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, webAppRoute = 'chatbot', hiddenInputs, className }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@ -327,10 +331,11 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, hiddenIn
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||
{isShow && (
|
||||
<EmbeddedContent
|
||||
key={`${appBaseUrl ?? ''}:${accessToken ?? ''}:${JSON.stringify(hiddenInputs ?? [])}`}
|
||||
key={`${appBaseUrl ?? ''}:${accessToken ?? ''}:${webAppRoute}:${JSON.stringify(hiddenInputs ?? [])}`}
|
||||
siteInfo={siteInfo}
|
||||
appBaseUrl={appBaseUrl ?? ''}
|
||||
accessToken={accessToken ?? ''}
|
||||
webAppRoute={webAppRoute}
|
||||
hiddenInputs={hiddenInputs}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -3,14 +3,14 @@ import { useTranslation } from 'react-i18next'
|
||||
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { RETRIEVE_TYPE } from '@/types/app'
|
||||
|
||||
export type KnowledgeValidationIssueCode =
|
||||
| 'name_required'
|
||||
| 'name_duplicate'
|
||||
| 'datasets_required'
|
||||
| 'custom_query_required'
|
||||
| 'single_model_required'
|
||||
| 'metadata_model_required'
|
||||
| 'metadata_conditions_required'
|
||||
export type KnowledgeValidationIssueCode
|
||||
= | 'name_required'
|
||||
| 'name_duplicate'
|
||||
| 'datasets_required'
|
||||
| 'custom_query_required'
|
||||
| 'single_model_required'
|
||||
| 'metadata_model_required'
|
||||
| 'metadata_conditions_required'
|
||||
|
||||
export type KnowledgeValidationField = 'name' | 'datasets' | 'query' | 'retrieval' | 'metadata'
|
||||
|
||||
|
||||
@ -33,6 +33,23 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/embedded-chatbot/theme/theme-context', () => ({
|
||||
useThemeContext: () => ({
|
||||
buildTheme: vi.fn(),
|
||||
theme: {
|
||||
primaryColor: '#1C64F2',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'PRODUCTION',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
@ -202,6 +219,46 @@ describe('Agent access surface cards', () => {
|
||||
expect(within(dialog).getByRole('button', { name: /appOverview\.overview\.appInfo\.customize\.way1\.step1Operation/ })).toHaveAttribute('href', 'https://github.com/langgenius/webapp-conversation')
|
||||
})
|
||||
|
||||
it('should open the embedded dialog with the Agent web app route', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderWithQueryClient(
|
||||
<WebAppAccessCard agent={createAgent()} agentId="agent-1" isLoading={false} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.embedded' }))
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: 'appOverview.overview.appInfo.embedded.title' })
|
||||
await waitFor(() => {
|
||||
expect(dialog).toHaveTextContent('https://chat.example.test/agent/site-token')
|
||||
})
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: 'appOverview.overview.appInfo.embedded.scripts' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dialog).toHaveTextContent('routeSegment: \'agent\'')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep embedded disabled until the backing app id and web app token are available', () => {
|
||||
renderWithQueryClient(
|
||||
<WebAppAccessCard
|
||||
agent={createAgent({
|
||||
app_id: null,
|
||||
site: {
|
||||
...createAgent().site!,
|
||||
access_token: null,
|
||||
code: null,
|
||||
},
|
||||
})}
|
||||
agentId="agent-1"
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.embedded' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should keep customize disabled until the generated contract provides the required fields', () => {
|
||||
renderWithQueryClient(
|
||||
<WebAppAccessCard agent={createAgent({ api_base_url: null })} agentId="agent-1" isLoading={false} />,
|
||||
|
||||
@ -7,17 +7,12 @@ 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 ShareQRCode from '@/app/components/base/qrcode'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { accessSurfaceActionClassName, AccessSurfaceCard } from './access-surface-card'
|
||||
|
||||
type AgentWebAppSite = NonNullable<AgentAppDetailWithSite['site']> & {
|
||||
access_token?: string | null
|
||||
app_base_url?: string | null
|
||||
code?: string | null
|
||||
}
|
||||
|
||||
export function WebAppAccessCard({
|
||||
agent,
|
||||
agentId,
|
||||
@ -32,9 +27,23 @@ export function WebAppAccessCard({
|
||||
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 customizeConfig = appId && apiBaseUrl
|
||||
? {
|
||||
apiBaseUrl,
|
||||
@ -43,6 +52,7 @@ export function WebAppAccessCard({
|
||||
: null
|
||||
const showSsoBadge = agent?.access_mode === AccessMode.EXTERNAL_MEMBERS
|
||||
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
|
||||
const [showEmbeddedModal, setShowEmbeddedModal] = useState(false)
|
||||
const toggleSiteMutation = useMutation(consoleQuery.apps.byAppId.siteEnable.post.mutationOptions({
|
||||
onSuccess: (_updatedApp, variables) => {
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
@ -65,7 +75,7 @@ export function WebAppAccessCard({
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
|
||||
(agentDetail) => {
|
||||
if (!agentDetail)
|
||||
if (!agentDetail || !agentDetail.site)
|
||||
return agentDetail
|
||||
|
||||
return {
|
||||
@ -74,7 +84,7 @@ export function WebAppAccessCard({
|
||||
...agentDetail.site,
|
||||
...site,
|
||||
access_token: site.code,
|
||||
} as AgentWebAppSite,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -162,7 +172,13 @@ export function WebAppAccessCard({
|
||||
{t('agentDetail.access.webApp.actions.launch')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" size="medium" className="gap-1.5 px-3" disabled>
|
||||
<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>
|
||||
@ -189,12 +205,22 @@ export function WebAppAccessCard({
|
||||
sourceCodeRepository="webapp-conversation"
|
||||
/>
|
||||
)}
|
||||
{embeddedConfig && (
|
||||
<EmbeddedModal
|
||||
isShow={showEmbeddedModal}
|
||||
onClose={() => setShowEmbeddedModal(false)}
|
||||
appBaseUrl={embeddedConfig.appBaseUrl}
|
||||
accessToken={embeddedConfig.accessToken}
|
||||
siteInfo={embeddedConfig.siteInfo}
|
||||
webAppRoute="agent"
|
||||
/>
|
||||
)}
|
||||
</AccessSurfaceCard>
|
||||
)
|
||||
}
|
||||
|
||||
function getAgentWebAppUrl(agent?: AgentAppDetailWithSite) {
|
||||
const site = agent?.site as AgentWebAppSite | null | undefined
|
||||
const site = agent?.site
|
||||
const token = site?.access_token ?? site?.code
|
||||
if (!token)
|
||||
return ''
|
||||
|
||||
@ -48,7 +48,7 @@ export function AgentOrchestratePanel({
|
||||
nodeId,
|
||||
activeConfigIsPublished,
|
||||
activeConfigSnapshot,
|
||||
agentSoulConfig,
|
||||
agentSoulConfig: _agentSoulConfig,
|
||||
agentName,
|
||||
currentModel,
|
||||
textGenerationModelList,
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
|
||||
@ -35,10 +35,10 @@ import {
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { DATASET_DEFAULT } from '@/config'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useKnowledgeValidationMessage, validateKnowledgeRetrievals } from '@/features/agent-v2/agent-composer/knowledge-validation'
|
||||
import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge'
|
||||
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
||||
import { AppModeEnum, RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app'
|
||||
import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge'
|
||||
import { useKnowledgeValidationMessage, validateKnowledgeRetrievals } from '@/features/agent-v2/agent-composer/knowledge-validation'
|
||||
|
||||
type KnowledgeRetrievalQueryMode = 'agent' | 'custom'
|
||||
type MetadataFilteringConditions = {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
|
||||
@ -8,11 +8,17 @@ import { env } from '@/env'
|
||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://ungh.cc https://api2.amplitude.com *.amplitude.com'
|
||||
const CURRENT_PATHNAME_HEADER = 'x-dify-pathname'
|
||||
const CURRENT_SEARCH_HEADER = 'x-dify-search'
|
||||
const EMBEDDABLE_PATH_PREFIXES = ['/chat', '/workflow', '/completion', '/webapp-signin']
|
||||
const EMBEDDABLE_PATH_SEGMENTS = ['/agent']
|
||||
|
||||
export const canEmbedPath = (pathname: string) =>
|
||||
EMBEDDABLE_PATH_PREFIXES.some(prefix => pathname.startsWith(prefix))
|
||||
|| EMBEDDABLE_PATH_SEGMENTS.some(segment => pathname === segment || pathname.startsWith(`${segment}/`))
|
||||
|
||||
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
||||
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
// Chatbot page should be allowed to be embedded in iframe. It's a feature
|
||||
if (env.NEXT_PUBLIC_ALLOW_EMBED !== true && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin'))
|
||||
if (env.NEXT_PUBLIC_ALLOW_EMBED !== true && !canEmbedPath(pathname))
|
||||
response.headers.set('X-Frame-Options', 'DENY')
|
||||
|
||||
return response
|
||||
|
||||
@ -133,6 +133,7 @@
|
||||
|
||||
const baseUrl =
|
||||
config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`;
|
||||
const routeSegment = (config.routeSegment || "chatbot").replace(/^\/+|\/+$/g, "") || "chatbot";
|
||||
const targetOrigin = new URL(baseUrl).origin;
|
||||
|
||||
// Pass sendOnEnter config as URL parameter
|
||||
@ -141,7 +142,7 @@
|
||||
}
|
||||
|
||||
// pre-check the length of the URL
|
||||
const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`;
|
||||
const iframeUrl = `${baseUrl}/${routeSegment}/${config.token}?${params}`;
|
||||
// 1) CREATE the iframe immediately, so it can load in the background:
|
||||
const preloadedIframe = createIframe();
|
||||
// 2) HIDE it by default:
|
||||
|
||||
65
web/public/embed.min.js
vendored
65
web/public/embed.min.js
vendored
@ -1,17 +1,11 @@
|
||||
(function(){const configKey="difyChatbotConfig";const buttonId="dify-chatbot-bubble-button";const iframeId="dify-chatbot-bubble-window";const config=window[configKey];let isExpanded=false;const svgIcons=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
|
||||
</svg>
|
||||
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`;const originalIframeStyleText=`
|
||||
(()=>{let t="difyChatbotConfig",m="dify-chatbot-bubble-button",h="dify-chatbot-bubble-window",y=window[t],l=!1,c=`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
top: unset;
|
||||
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
right: var(--${m}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
bottom: var(--${m}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
left: unset;
|
||||
width: 24rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
@ -25,42 +19,25 @@
|
||||
transition-property: width, height;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
`;const expandedIframeStyleText=`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
top: unset;
|
||||
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
|
||||
left: unset;
|
||||
min-width: 24rem;
|
||||
width: 48%;
|
||||
max-width: 40rem; /* Match mobile breakpoint*/
|
||||
min-height: 43.75rem;
|
||||
height: 88%;
|
||||
max-height: calc(100vh - 6rem);
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
z-index: 2147483640;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
transition-property: width, height;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
`;async function embedChatbot(){let isDragging=false;if(!config||!config.token){console.error(`${configKey} is empty or token is not provided`);return}async function compressAndEncodeBase64(input){const uint8Array=(new TextEncoder).encode(input);const compressedStream=new Response(new Blob([uint8Array]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();const compressedUint8Array=new Uint8Array(await compressedStream);return btoa(String.fromCharCode(...compressedUint8Array))}async function getCompressedInputsFromConfig(){const inputs=config?.inputs||{};const compressedInputs={};await Promise.all(Object.entries(inputs).map(async([key,value])=>{compressedInputs[key]=await compressAndEncodeBase64(value)}));return compressedInputs}async function getCompressedSystemVariablesFromConfig(){const systemVariables=config?.systemVariables||{};const compressedSystemVariables={};await Promise.all(Object.entries(systemVariables).map(async([key,value])=>{compressedSystemVariables[`sys.${key}`]=await compressAndEncodeBase64(value)}));return compressedSystemVariables}async function getCompressedUserVariablesFromConfig(){const userVariables=config?.userVariables||{};const compressedUserVariables={};await Promise.all(Object.entries(userVariables).map(async([key,value])=>{compressedUserVariables[`user.${key}`]=await compressAndEncodeBase64(value)}));return compressedUserVariables}const params=new URLSearchParams({...await getCompressedInputsFromConfig(),...await getCompressedSystemVariablesFromConfig(),...await getCompressedUserVariablesFromConfig()});const baseUrl=config.baseUrl||`https://${config.isDev?"dev.":""}udify.app`;const targetOrigin=new URL(baseUrl).origin;if(config.sendOnEnter===false){params.set("sendOnEnter","false")}const iframeUrl=`${baseUrl}/chatbot/${config.token}?${params}`;const preloadedIframe=createIframe();preloadedIframe.style.display="none";document.body.appendChild(preloadedIframe);if(iframeUrl.length>2048){console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load")}function createIframe(){const iframe=document.createElement("iframe");iframe.allow="fullscreen;microphone";iframe.title="dify chatbot bubble window";iframe.id=iframeId;iframe.src=iframeUrl;iframe.style.cssText=originalIframeStyleText;return iframe}function resetIframePosition(){if(window.innerWidth<=640)return;const targetIframe=document.getElementById(iframeId);const targetButton=document.getElementById(buttonId);if(targetIframe&&targetButton){const buttonRect=targetButton.getBoundingClientRect();const viewportCenterY=window.innerHeight/2;const buttonCenterY=buttonRect.top+buttonRect.height/2;if(buttonCenterY<viewportCenterY){targetIframe.style.top=`var(--${buttonId}-bottom, 1rem)`;targetIframe.style.bottom="unset"}else{targetIframe.style.bottom=`var(--${buttonId}-bottom, 1rem)`;targetIframe.style.top="unset"}const viewportCenterX=window.innerWidth/2;const buttonCenterX=buttonRect.left+buttonRect.width/2;if(buttonCenterX<viewportCenterX){targetIframe.style.left=`var(--${buttonId}-right, 1rem)`;targetIframe.style.right="unset"}else{targetIframe.style.right=`var(--${buttonId}-right, 1rem)`;targetIframe.style.left="unset"}}}function toggleExpand(){isExpanded=!isExpanded;const targetIframe=document.getElementById(iframeId);if(!targetIframe)return;if(isExpanded){targetIframe.style.cssText=expandedIframeStyleText}else{targetIframe.style.cssText=originalIframeStyleText}resetIframePosition()}window.addEventListener("message",event=>{if(event.origin!==targetOrigin)return;const targetIframe=document.getElementById(iframeId);if(!targetIframe||event.source!==targetIframe.contentWindow)return;if(event.data.type==="dify-chatbot-iframe-ready"){targetIframe.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:true,isDraggable:!!config.draggable}},targetOrigin)}if(event.data.type==="dify-chatbot-expand-change"){toggleExpand()}});function createButton(){const containerDiv=document.createElement("div");Object.entries(config.containerProps||{}).forEach(([key,value])=>{if(key==="className"){containerDiv.classList.add(...value.split(" "))}else if(key==="style"){if(typeof value==="object"){Object.assign(containerDiv.style,value)}else{containerDiv.style.cssText=value}}else if(typeof value==="function"){containerDiv.addEventListener(key.replace(/^on/,"").toLowerCase(),value)}else{containerDiv[key]=value}});containerDiv.id=buttonId;const styleSheet=document.createElement("style");document.head.appendChild(styleSheet);styleSheet.sheet.insertRule(`
|
||||
#${containerDiv.id} {
|
||||
`;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await r(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await r(t)})),n})(),...await(async()=>{var e=y?.userVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["user."+e]=await r(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`,i=(y.routeSegment||"chatbot").replace(/^\/+|\/+$/g,"")||"chatbot";let o=new URL(n).origin,t=(!1===y.sendOnEnter&&e.set("sendOnEnter","false"),`${n}/${i}/${y.token}?`+e);n=s();async function r(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=h,e.src=t,e.style.cssText=c,e}function d(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(h),t=document.getElementById(m),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${m}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${m}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${m}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${m}-right, 1rem)`,e.style.left="unset")))}function a(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=m;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(`
|
||||
#${n.id} {
|
||||
position: fixed;
|
||||
bottom: var(--${containerDiv.id}-bottom, 1rem);
|
||||
right: var(--${containerDiv.id}-right, 1rem);
|
||||
left: var(--${containerDiv.id}-left, unset);
|
||||
top: var(--${containerDiv.id}-top, unset);
|
||||
width: var(--${containerDiv.id}-width, 48px);
|
||||
height: var(--${containerDiv.id}-height, 48px);
|
||||
border-radius: var(--${containerDiv.id}-border-radius, 25px);
|
||||
background-color: var(--${containerDiv.id}-bg-color, #155EEF);
|
||||
box-shadow: var(--${containerDiv.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px);
|
||||
bottom: var(--${n.id}-bottom, 1rem);
|
||||
right: var(--${n.id}-right, 1rem);
|
||||
left: var(--${n.id}-left, unset);
|
||||
top: var(--${n.id}-top, unset);
|
||||
width: var(--${n.id}-width, 48px);
|
||||
height: var(--${n.id}-height, 48px);
|
||||
border-radius: var(--${n.id}-border-radius, 25px);
|
||||
background-color: var(--${n.id}-bg-color, #155EEF);
|
||||
box-shadow: var(--${n.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px);
|
||||
cursor: pointer;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
`);const displayDiv=document.createElement("div");displayDiv.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";displayDiv.innerHTML=svgIcons;containerDiv.appendChild(displayDiv);document.body.appendChild(containerDiv);containerDiv.addEventListener("click",handleClick);containerDiv.addEventListener("touchend",event=>{event.preventDefault();handleClick()},{passive:false});function handleClick(){if(isDragging)return;const targetIframe=document.getElementById(iframeId);if(!targetIframe){containerDiv.appendChild(createIframe());resetIframePosition();this.title="Exit (ESC)";setSvgIcon("close");document.addEventListener("keydown",handleEscKey);return}targetIframe.style.display=targetIframe.style.display==="none"?"block":"none";targetIframe.style.display==="none"?setSvgIcon("open"):setSvgIcon("close");if(targetIframe.style.display==="none"){document.removeEventListener("keydown",handleEscKey)}else{document.addEventListener("keydown",handleEscKey)}resetIframePosition()}if(config.draggable){enableDragging(containerDiv,config.dragAxis||"both")}}function enableDragging(element,axis){let startX,startY,startClientX,startClientY;element.addEventListener("mousedown",startDragging);element.addEventListener("touchstart",startDragging);function startDragging(e){isDragging=false;if(e.type==="touchstart"){startX=e.touches[0].clientX-element.offsetLeft;startY=e.touches[0].clientY-element.offsetTop;startClientX=e.touches[0].clientX;startClientY=e.touches[0].clientY}else{startX=e.clientX-element.offsetLeft;startY=e.clientY-element.offsetTop;startClientX=e.clientX;startClientY=e.clientY}document.addEventListener("mousemove",drag);document.addEventListener("touchmove",drag,{passive:false});document.addEventListener("mouseup",stopDragging);document.addEventListener("touchend",stopDragging);e.preventDefault()}function drag(e){const touch=e.type==="touchmove"?e.touches[0]:e;const deltaX=touch.clientX-startClientX;const deltaY=touch.clientY-startClientY;if(Math.abs(deltaX)>8||Math.abs(deltaY)>8){isDragging=true}if(!isDragging)return;element.style.transition="none";element.style.cursor="grabbing";const targetIframe=document.getElementById(iframeId);if(targetIframe){targetIframe.style.display="none";setSvgIcon("open")}let newLeft,newBottom;if(e.type==="touchmove"){newLeft=e.touches[0].clientX-startX;newBottom=window.innerHeight-e.touches[0].clientY-startY}else{newLeft=e.clientX-startX;newBottom=window.innerHeight-e.clientY-startY}const elementRect=element.getBoundingClientRect();const maxX=window.innerWidth-elementRect.width;const maxY=window.innerHeight-elementRect.height;if(axis==="x"||axis==="both"){element.style.setProperty(`--${buttonId}-left`,`${Math.max(0,Math.min(newLeft,maxX))}px`)}if(axis==="y"||axis==="both"){element.style.setProperty(`--${buttonId}-bottom`,`${Math.max(0,Math.min(newBottom,maxY))}px`)}}function stopDragging(){setTimeout(()=>{isDragging=false},0);element.style.transition="";element.style.cursor="pointer";document.removeEventListener("mousemove",drag);document.removeEventListener("touchmove",drag);document.removeEventListener("mouseup",stopDragging);document.removeEventListener("touchend",stopDragging)}}if(!document.getElementById(buttonId)){createButton()}}function setSvgIcon(type="open"){if(type==="open"){document.getElementById("openIcon").style.display="block";document.getElementById("closeIcon").style.display="none"}else{document.getElementById("openIcon").style.display="none";document.getElementById("closeIcon").style.display="block"}}function handleEscKey(event){if(event.key==="Escape"){const targetIframe=document.getElementById(iframeId);if(targetIframe&&targetIframe.style.display!=="none"){targetIframe.style.display="none";setSvgIcon("open")}}}document.addEventListener("keydown",handleEscKey);if(config?.dynamicScript){embedChatbot()}else{document.body.onload=embedChatbot}})();
|
||||
`),document.createElement("div"));function t(){var e;u||((e=document.getElementById(h))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
|
||||
</svg>
|
||||
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",e=>{e.preventDefault(),t()},{passive:!1}),y.draggable){var a=n;var l=y.dragAxis||"both";let r,s,t,d;function o(e){u=!1,d=("touchstart"===e.type?(r=e.touches[0].clientX-a.offsetLeft,s=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(r=e.clientX-a.offsetLeft,s=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-d;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(h);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-r,window.innerHeight-n.touches[0].clientY-s):(e=n.clientX-r,window.innerHeight-n.clientY-s);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${m}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${m}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(h))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(l=!l,n=document.getElementById(h))&&(l?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n border-radius: 1rem;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=c,d())}),document.getElementById(m)||a()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(h))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}m,m,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})();
|
||||
Loading…
Reference in New Issue
Block a user