diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5c1d04ee120..abb273edb6c 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/web/__tests__/proxy-frame-options.spec.ts b/web/__tests__/proxy-frame-options.spec.ts new file mode 100644 index 00000000000..f6f6abe160c --- /dev/null +++ b/web/__tests__/proxy-frame-options.spec.ts @@ -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) + }) +}) diff --git a/web/app/components/app/overview/__tests__/app-card-utils.spec.ts b/web/app/components/app/overview/__tests__/app-card-utils.spec.ts index 11c2dd54bb1..694a9aec7ba 100644 --- a/web/app/components/app/overview/__tests__/app-card-utils.spec.ts +++ b/web/app/components/app/overview/__tests__/app-card-utils.spec.ts @@ -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') diff --git a/web/app/components/app/overview/app-card-utils.ts b/web/app/components/app/overview/app-card-utils.ts index 9475da4300a..acca7e162a0 100644 --- a/web/app/components/app/overview/app-card-utils.ts +++ b/web/app/components/app/overview/app-card-utils.ts @@ -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 @@ -174,6 +177,9 @@ export const getEmbeddedScriptSnippet = ({ : ''}${IS_CE_EDITION ? `, baseUrl: '${url}${basePath}'` + : ''}${webAppRoute !== 'chatbot' + ? `, + routeSegment: '${webAppRoute}'` : ''}, inputs: ${getScriptInputsContent(inputValues)}, systemVariables: { diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 70b90c3cc04..fd36938af79 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -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 }) => { - 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) => { +}: Required> & Pick) => { const { t } = useTranslation() const supportedHiddenInputs = useMemo( () => (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 = ({ {hiddenInputsCollapsed - ? - : } + ? + : } {!hiddenInputsCollapsed && (
@@ -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
{isShow && ( )} diff --git a/web/features/agent-v2/agent-composer/knowledge-validation.ts b/web/features/agent-v2/agent-composer/knowledge-validation.ts index 29fb8292974..66d7c674cef 100644 --- a/web/features/agent-v2/agent-composer/knowledge-validation.ts +++ b/web/features/agent-v2/agent-composer/knowledge-validation.ts @@ -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' diff --git a/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx b/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx index 0b703df3c55..893f9ea7b72 100644 --- a/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx +++ b/web/features/agent-v2/agent-detail/access/components/__tests__/access-surface-cards.spec.tsx @@ -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( + , + ) + + 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( + , + ) + + 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( , diff --git a/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx b/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx index aca6a8f7a1a..ae336ff1641 100644 --- a/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx +++ b/web/features/agent-v2/agent-detail/access/components/web-app-access-card.tsx @@ -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 & { - 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( @@ -65,7 +75,7 @@ export function WebAppAccessCard({ queryClient.setQueryData( 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')} )} - @@ -189,12 +205,22 @@ export function WebAppAccessCard({ sourceCodeRepository="webapp-conversation" /> )} + {embeddedConfig && ( + setShowEmbeddedModal(false)} + appBaseUrl={embeddedConfig.appBaseUrl} + accessToken={embeddedConfig.accessToken} + siteInfo={embeddedConfig.siteInfo} + webAppRoute="agent" + /> + )} ) } 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 '' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/index.tsx index 67925fd09fc..c27aa7ae4fe 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/index.tsx @@ -48,7 +48,7 @@ export function AgentOrchestratePanel({ nodeId, activeConfigIsPublished, activeConfigSnapshot, - agentSoulConfig, + agentSoulConfig: _agentSoulConfig, agentName, currentModel, textGenerationModelList, diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx index 842f5c21a88..f2434667119 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx @@ -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' diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx index dccbba87eed..0b8529617fe 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx @@ -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 = { diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index d9c1e95a1b9..da4ca31898b 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -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' diff --git a/web/proxy.ts b/web/proxy.ts index 354f8306192..8288bea5f23 100644 --- a/web/proxy.ts +++ b/web/proxy.ts @@ -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 diff --git a/web/public/embed.js b/web/public/embed.js index f7c6fdaf4a8..945058e975a 100644 --- a/web/public/embed.js +++ b/web/public/embed.js @@ -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: diff --git a/web/public/embed.min.js b/web/public/embed.min.js index 7c366f8f2eb..afbd1d24fed 100644 --- a/web/public/embed.min.js +++ b/web/public/embed.min.js @@ -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=` - - - - `;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{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{"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}})(); \ No newline at end of file + `),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=` + + + + `,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{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{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})(); \ No newline at end of file