From 89d442b5523d2a6070d63a5b96bed893ad7fd6e3 Mon Sep 17 00:00:00 2001 From: Jaime Morales Date: Thu, 18 Sep 2025 13:58:59 -0700 Subject: [PATCH 1/3] added support for a custom atlassian remote MCP server (#545) --- .../nodes/tools/MCP/Atlassian/AtlassianMcp.ts | 12 ++++--- packages/server/src/config/passport.ts | 7 +++- packages/server/src/utils/constants.ts | 1 + packages/server/src/utils/index.ts | 33 +++++++------------ packages/server/src/utils/mcp-metadata.ts | 3 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts index 535b5777179..11af2a1b7ea 100644 --- a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts +++ b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts @@ -1,8 +1,8 @@ /** * Atlassian Remote MCP Server Node * - * This file implements an Atlassian MCP server node that uses the remote MCP server - * at https://mcp.atlassian.com/v1/sse with OAuth 2.0 bearer token authentication. + * This file implements an Atlassian MCP server node that uses a custom remote MCP server + * with OAuth 2.0 bearer token authentication. * * Key features: * - Uses atlassianOAuth credential with access_token, refresh_token, expiration_time @@ -128,9 +128,13 @@ class Atlassian_MCP implements INode { throw new Error('Access token not found in credential data') } - // For remote MCP servers, we need to provide the URL directly + if (!process.env.ATLASSIAN_MCP_SERVER_URL) { + console.error('ATLASSIAN_MCP_SERVER_URL environment variable is not set') + return [] + } + const serverParams = { - url: 'https://mcp.atlassian.com/v1/sse' + url: `${process.env.ATLASSIAN_MCP_SERVER_URL}/sse` } const toolkit = new MCPToolkit(serverParams, 'sse', credentialData.access_token) diff --git a/packages/server/src/config/passport.ts b/packages/server/src/config/passport.ts index e89eb837454..88b876a0111 100644 --- a/packages/server/src/config/passport.ts +++ b/packages/server/src/config/passport.ts @@ -96,6 +96,11 @@ export default function (passport: any) { return done(null, false, { message: 'Authorization code missing' }) } + const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL + if (!baseUrl) { + return done(null, false, { message: 'ATLASSIAN_MCP_SERVER_URL environment variable is not set' }) + } + // Get MCP client info from state parameter (sessionId) const sessionId = state const mcpClientInfo = sessionId ? getPendingRegistration(sessionId) : null @@ -105,7 +110,7 @@ export default function (passport: any) { } // Fetch MCP metadata and use MCP client credentials - const metadata = await fetchMCPMetadata() + const metadata = await fetchMCPMetadata(baseUrl) const tokenURL = metadata.token_endpoint const clientId = mcpClientInfo.client_id const clientSecret = mcpClientInfo.client_secret diff --git a/packages/server/src/utils/constants.ts b/packages/server/src/utils/constants.ts index e075e839382..6a298e9e329 100644 --- a/packages/server/src/utils/constants.ts +++ b/packages/server/src/utils/constants.ts @@ -26,6 +26,7 @@ export const WHITELIST_URLS = [ '/api/v1/salesforce-auth/callback', '/api/v1/atlassian-auth', '/api/v1/atlassian-auth/callback', + '/api/v1/atlassian-auth/mcp-initialize', '/api/v1/gmail/labels', '/api/v1/gmail/messages', '/api/v1/gmail/message', diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 77801d97e13..5a2faa0fe3f 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -2107,11 +2107,15 @@ export async function registerOAuthClient(redirectUri: string): Promise<{ authorization_endpoint: string scope: string }> { + const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL + if (!baseUrl) { + throw new Error('ATLASSIAN_MCP_SERVER_URL environment variable is not set') + } try { logger.info('[ATLASSIAN OAUTH]: Starting OAuth client registration') // Step 1: Get MCP metadata - const metadata = await fetchMCPMetadata() + const metadata = await fetchMCPMetadata(baseUrl) logger.debug('[ATLASSIAN OAUTH]: Retrieved MCP metadata') // Step 2: Register dynamic client @@ -2121,25 +2125,7 @@ export async function registerOAuthClient(redirectUri: string): Promise<{ client_uri: process.env.API_HOST, grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], - scope: [ - 'offline_access', - 'read:account', - 'read:confluence-content.all', - 'read:confluence-content.summary', - 'read:confluence-groups', - 'read:confluence-props', - 'read:confluence-space.summary', - 'read:confluence-user', - 'read:jira-user', - 'read:jira-work', - 'read:me', - 'readonly:content.attachment:confluence', - 'search:confluence', - 'write:confluence-content', - 'write:confluence-file', - 'write:confluence-props', - 'write:jira-work' - ].join(' ') + scope: `offline-access read:me read:account read:jira-user read:jira-work write:jira-work read:confluence-user write:confluence-content read:confluence-content.all search:confluence read:space:confluence read:page:confluence write:page:confluence read:hierarchical-content:confluence read:attachment:confluence read:comment:confluence write:comment:confluence` } logger.debug('[ATLASSIAN OAUTH]: Registering client with MCP server') @@ -2305,8 +2291,13 @@ export async function refreshStoredCredentialTokens(credentialId: string, appDat return true // Token is still valid } + const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL + if (!baseUrl) { + throw new Error('ATLASSIAN_MCP_SERVER_URL environment variable is not set') + } + // Get MCP metadata - const metadata = await fetchMCPMetadata() + const metadata = await fetchMCPMetadata(baseUrl) // Refresh tokens const tokenResponse = await fetch(metadata.token_endpoint, { diff --git a/packages/server/src/utils/mcp-metadata.ts b/packages/server/src/utils/mcp-metadata.ts index 0e8d9a24d79..87bc3fc7369 100644 --- a/packages/server/src/utils/mcp-metadata.ts +++ b/packages/server/src/utils/mcp-metadata.ts @@ -28,7 +28,8 @@ const CACHE_DURATION = 30 * 60 * 1000 * Fetches MCP OAuth metadata from the well-known endpoint * Uses caching to avoid repeated requests */ -export async function fetchMCPMetadata(baseUrl: string = 'https://mcp.atlassian.com'): Promise { + +export async function fetchMCPMetadata(baseUrl: string): Promise { const metadataUrl = `${baseUrl}/.well-known/oauth-authorization-server` const cacheKey = metadataUrl From 1561c5133ee193d78aecb4741e58f110a91e1dee Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:49:57 -0400 Subject: [PATCH 2/3] Ct/SUPPORT-5021-dalle-api-url-issues-with-domain-var (#547) This commit introduces several updates to the configuration and functionality of the DALL-E image generation tools. ### Changes: - Added a new environment variable `API_HOST` to the manifest for better API endpoint management. - Updated the `next.config.js` to support HTTPS for the specified hostname. - Modified the DALL-E image tools to prioritize the new `API_HOST` variable for constructing URLs, ensuring that the tools can correctly reference the API in production environments. - Incremented the version of the DALL-E tools from 1.0 to 1.1 to reflect these changes. ### Impact: These updates enhance the flexibility of the API configuration and improve the reliability of image generation by ensuring the correct API endpoint is used based on the environment settings. --- apps/web/next.config.js | 6 ++++++ copilot/flowise/manifest.yml | 1 + .../nodes/tools/CreateDalleImage/AAICreateDalleImage.ts | 9 +++++---- .../nodes/tools/CreateDalleImage/CreateDalleImage.ts | 9 +++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 9fb7c0fa255..cf6d0c3256a 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -84,6 +84,12 @@ let nextConfig = withBundleAnalyzer({ hostname: 'localhost', port: '4000', pathname: '/**' + }, + { + protocol: 'https', + hostname: '**.theanswer.ai', + port: '', + pathname: '/**' } ] }, diff --git a/copilot/flowise/manifest.yml b/copilot/flowise/manifest.yml index b5673d347e3..56784994fe9 100644 --- a/copilot/flowise/manifest.yml +++ b/copilot/flowise/manifest.yml @@ -64,6 +64,7 @@ variables: # Pass environment variables as key value pairs. CHATFLOW_DOMAIN: 'http://flowise:4000' DOMAIN: 'http://flowise:4000' FLOWISE_DOMAIN: 'http://flowise:4000' + API_HOST: 'https://api.${CLIENT_DOMAIN}' NODE_ENV: 'production' PORT: '4000' NUMBER_OF_PROXIES: '1' diff --git a/packages/components/nodes/tools/CreateDalleImage/AAICreateDalleImage.ts b/packages/components/nodes/tools/CreateDalleImage/AAICreateDalleImage.ts index 10b19c14230..21398da8f7c 100644 --- a/packages/components/nodes/tools/CreateDalleImage/AAICreateDalleImage.ts +++ b/packages/components/nodes/tools/CreateDalleImage/AAICreateDalleImage.ts @@ -101,7 +101,8 @@ export class AAIDallePostTool extends Tool { } // Convert FILE-STORAGE:: reference to a full URL with domain - const domain = process.env.DOMAIN || process.env.FLOWISE_DOMAIN || this.baseURL || 'http://localhost:4000' + const domain = + process.env.API_HOST || process.env.DOMAIN || process.env.FLOWISE_DOMAIN || this.baseURL || 'http://localhost:4000' const imageFileName = imageStorageUrl.replace('FILE-STORAGE::', '') const fullImageUrl = `${domain}/api/v1/get-upload-file?chatflowId=dalle-images&chatId=${orgId}%2F${usrId}&fileName=${imageFileName}` @@ -308,7 +309,7 @@ class AAIDallePost_Tool implements INode { constructor() { this.label = 'Answer DALL-E Post' this.name = 'aaiDallePost' - this.version = 1.0 + this.version = 1.1 this.type = 'AAIDallePost' this.icon = 'openai.svg' this.category = 'Tools' @@ -459,8 +460,8 @@ class AAIDallePost_Tool implements INode { if (process.env.NODE_ENV === 'development') { baseURL = 'http://localhost:4000' } else { - // In production, use the current domain (Flowise server) - baseURL = process.env.DOMAIN || process.env.FLOWISE_DOMAIN || 'http://localhost:4000' + // In production, prefer external API host, then fall back to Flowise domain + baseURL = process.env.API_HOST || process.env.DOMAIN || process.env.FLOWISE_DOMAIN || 'http://localhost:4000' } const obj: RequestParameters = {} diff --git a/packages/components/nodes/tools/CreateDalleImage/CreateDalleImage.ts b/packages/components/nodes/tools/CreateDalleImage/CreateDalleImage.ts index 29ec99dfa91..04943b0cbac 100644 --- a/packages/components/nodes/tools/CreateDalleImage/CreateDalleImage.ts +++ b/packages/components/nodes/tools/CreateDalleImage/CreateDalleImage.ts @@ -104,7 +104,8 @@ export class DallePostTool extends Tool { } // Convert FILE-STORAGE:: reference to a full URL with domain - const domain = process.env.DOMAIN || process.env.FLOWISE_DOMAIN || this.baseURL || 'http://localhost:4000' + const domain = + process.env.API_HOST || process.env.DOMAIN || process.env.FLOWISE_DOMAIN || this.baseURL || 'http://localhost:4000' const imageFileName = imageStorageUrl.replace('FILE-STORAGE::', '') const fullImageUrl = `${domain}/api/v1/get-upload-file?chatflowId=dalle-images&chatId=${orgId}%2F${usrId}&fileName=${imageFileName}` @@ -314,7 +315,7 @@ class DallePost_Tool implements INode { constructor() { this.label = 'Dall-E Post' this.name = 'dallePost' - this.version = 1.0 + this.version = 1.1 this.type = 'DallePost' this.icon = 'openai.svg' this.category = 'Tools' @@ -470,8 +471,8 @@ class DallePost_Tool implements INode { if (process.env.NODE_ENV === 'development') { baseURL = 'http://localhost:4000' } else { - // In production, use the current domain (Flowise server) - baseURL = process.env.DOMAIN || process.env.FLOWISE_DOMAIN || 'http://localhost:4000' + // In production, prefer external API host, then fall back to Flowise domain + baseURL = process.env.API_HOST || process.env.DOMAIN || process.env.FLOWISE_DOMAIN || 'http://localhost:4000' } const obj: RequestParameters = {} From 3dd34acd2eebc007507f30a03bfaf819270eee1a Mon Sep 17 00:00:00 2001 From: Max Techera Date: Tue, 23 Sep 2025 16:39:47 -0300 Subject: [PATCH 3/3] Update webinar pages and ElevenLabs widget (#549) ## Summary Updates to webinar landing pages, ElevenLabs widget integration, and supporting styles. ## Changes - Enhanced ElevenLabs inline widget component with improved functionality and styles - Updated webinar enterprise AI landing page with improved content structure - Refactored webinar thank you page layout and user experience - Streamlined mailerLite service implementation - Improved responsive design for index page with additional styles ## Test plan - [x] Tested webinar landing page locally - [x] Verified ElevenLabs widget integration works correctly - [x] Confirmed thank you page displays properly after registration - [x] Validated responsive design on mobile and desktop - [x] ESLint and prettier checks pass --- .gitignore | 1 + packages/docs/package.json | 6 +- .../ElevenLabsInlineWidget/index.tsx | 180 +- .../ElevenLabsInlineWidget/styles.module.css | 172 + .../docs/src/components/LazyThreeScene.tsx | 36 + .../src/components/WebinarCalendarButtons.tsx | 57 + .../docs/src/components/WebinarCountdown.tsx | 180 ++ .../src/components/WebinarLegalFooter.tsx | 20 + .../components/WebinarRegistrationForm.tsx | 241 ++ packages/docs/src/config/marketingConfig.ts | 264 ++ packages/docs/src/config/webinarContent.ts | 520 +++ packages/docs/src/pages/index.module.css | 2801 ++++++++++++++--- .../docs/src/pages/webinar-enterprise-ai.tsx | 1037 ++++++ packages/docs/src/pages/webinar-thank-you.tsx | 861 +++++ .../docs/src/services/mailerLiteService.ts | 108 + packages/docs/src/services/trackingService.ts | 368 +++ packages/docs/src/types/tracking.d.ts | 33 + packages/embed | 2 +- .../billing/stripe/StripeProvider.ts | 5 - pnpm-lock.yaml | 132 +- 20 files changed, 6404 insertions(+), 620 deletions(-) create mode 100644 packages/docs/src/components/LazyThreeScene.tsx create mode 100644 packages/docs/src/components/WebinarCalendarButtons.tsx create mode 100644 packages/docs/src/components/WebinarCountdown.tsx create mode 100644 packages/docs/src/components/WebinarLegalFooter.tsx create mode 100644 packages/docs/src/components/WebinarRegistrationForm.tsx create mode 100644 packages/docs/src/config/marketingConfig.ts create mode 100644 packages/docs/src/config/webinarContent.ts create mode 100644 packages/docs/src/pages/webinar-enterprise-ai.tsx create mode 100644 packages/docs/src/pages/webinar-thank-you.tsx create mode 100644 packages/docs/src/services/mailerLiteService.ts create mode 100644 packages/docs/src/services/trackingService.ts create mode 100644 packages/docs/src/types/tracking.d.ts diff --git a/.gitignore b/.gitignore index 881f685540c..681a6f15126 100644 --- a/.gitignore +++ b/.gitignore @@ -197,3 +197,4 @@ requiredVars.env bwsconfig.json .bwsconfig.cache +_docs/ diff --git a/packages/docs/package.json b/packages/docs/package.json index fa04befb3ea..285f6ee7663 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -17,11 +17,12 @@ "@docusaurus/preset-classic": "3.8.1", "@docusaurus/theme-mermaid": "3.8.1", "@elevenlabs/react": "^0.3.0", - "@mdx-js/react": "^3.1.1", + "@mdx-js/react": "^3.1.0", "clsx": "^2.1.1", "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", "js-yaml": "^4.1.0", + "dotenv": "^16.6.1", "prism-react-renderer": "^2.4.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -35,7 +36,7 @@ "typescript": "~5.5.4" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "name": "docs", "private": true, @@ -43,6 +44,7 @@ "build": "pnpm api-docs && docusaurus build && node scripts/fix-sitemap.js", "clear": "docusaurus clear", "deploy": "docusaurus deploy", + "dev": "docusaurus start --listen 4242", "docs": "docusaurus start --port 4242", "docusaurus": "docusaurus", "serve": "docusaurus serve", diff --git a/packages/docs/src/components/ElevenLabsInlineWidget/index.tsx b/packages/docs/src/components/ElevenLabsInlineWidget/index.tsx index f24d31893a0..69ce1c21f19 100644 --- a/packages/docs/src/components/ElevenLabsInlineWidget/index.tsx +++ b/packages/docs/src/components/ElevenLabsInlineWidget/index.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { useConversation } from '@elevenlabs/react' import clsx from 'clsx' import styles from './styles.module.css' @@ -31,14 +32,23 @@ const ElevenLabsInlineWidget: React.FC = ({ const [error, setError] = useState(null) const [hasStarted, setHasStarted] = useState(false) const [hasEnded, setHasEnded] = useState(false) + const [elapsedSeconds, setElapsedSeconds] = useState(0) + const callStartRef = useRef(null) + const timerRef = useRef(null) const conversation = useConversation({ onConnect: () => { setHasStarted(true) setHasEnded(false) + callStartRef.current = Date.now() + setElapsedSeconds(0) onConversationStart?.() }, onDisconnect: () => { if (hasStarted) setHasEnded(true) + if (timerRef.current) { + window.clearInterval(timerRef.current) + timerRef.current = null + } onConversationEnd?.() }, onError: (err: any) => setError(err?.message || String(err) || 'Unknown error') @@ -65,12 +75,64 @@ const ElevenLabsInlineWidget: React.FC = ({ } } - React.useEffect(() => { + useEffect(() => { if (conversation.status === 'connecting' || conversation.status === 'connected') { setHasEnded(false) } }, [conversation.status]) + useEffect(() => { + if (conversation.status === 'connected') { + if (!timerRef.current) { + timerRef.current = window.setInterval(() => { + if (callStartRef.current) { + setElapsedSeconds(Math.floor((Date.now() - callStartRef.current) / 1000)) + } + }, 1000) + } + } else { + if (timerRef.current) { + window.clearInterval(timerRef.current) + timerRef.current = null + } + } + + return () => { + if (timerRef.current) { + window.clearInterval(timerRef.current) + timerRef.current = null + } + } + }, [conversation.status]) + + const [floatingContainer, setFloatingContainer] = useState(null) + + useEffect(() => { + if (inline) { + return + } + + if (typeof document === 'undefined') { + return + } + + const node = document.createElement('div') + node.className = styles.floatingRoot + document.body.appendChild(node) + setFloatingContainer(node) + + return () => { + document.body.removeChild(node) + setFloatingContainer(null) + } + }, [inline]) + + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + // Button style selection const buttonClass = variant === 'cta' && buttonClassName @@ -83,57 +145,87 @@ const ElevenLabsInlineWidget: React.FC = ({ conversation.status === 'connecting' && styles.loading ) - if (inline) { + const isConnected = conversation.status === 'connected' + const isConnecting = conversation.status === 'connecting' + + const renderCallPanel = (inlineMode = false) => ( +
+
+
+ + +
+ ) + + const renderStatus = () => { + if (!showStatus) { + return null + } + + if (isConnected) { + return null + } + return ( - <> - {conversation.status !== 'connected' ? ( - - ) : ( - - )} - {showStatus && ( -
- {conversation.status === 'connected' && Call in progress...} - {error && {error}} - {hasStarted && hasEnded && conversation.status !== 'connected' && !error && Call ended.} +
+ {isConnecting && ( +
+
)} - + {error &&
{error}
} + {hasStarted && hasEnded && !error &&
Call ended. Restart anytime.
} +
) } - return ( -
- {conversation.status !== 'connected' ? ( - - ) : ( - - )} - {showStatus && ( -
- {conversation.status === 'connected' && Call in progress...} - {error && {error}} - {hasStarted && hasEnded && conversation.status !== 'connected' && !error && Call ended.} + {renderStatus()} +
+ ) + } + + const floatingPanel = isConnected && floatingContainer ? createPortal(renderCallPanel(), floatingContainer) : null + + return ( +
+ {floatingPanel} + {isConnected ? ( +
+ + Live call active — controls are docked bottom right. +
+ ) : ( + <> + + {renderStatus()} + )}
) diff --git a/packages/docs/src/components/ElevenLabsInlineWidget/styles.module.css b/packages/docs/src/components/ElevenLabsInlineWidget/styles.module.css index 95748a81983..95843944aa2 100644 --- a/packages/docs/src/components/ElevenLabsInlineWidget/styles.module.css +++ b/packages/docs/src/components/ElevenLabsInlineWidget/styles.module.css @@ -71,6 +71,178 @@ animation: pulse 2s infinite; } +.widgetWrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + width: 100%; +} + +.floatingPlaceholder { + font-size: 0.85rem; + color: rgba(17, 24, 39, 0.6); + text-align: center; + background: rgba(226, 232, 240, 0.6); + border-radius: 12px; + padding: 0.6rem 0.9rem; + max-width: 320px; +} + +.inlineContainer { + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + position: relative; +} + +.statusArea { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.85rem; + color: rgba(17, 24, 39, 0.7); + text-align: center; + min-height: 1.1rem; +} + +.connectingNotice { + display: inline-flex; + align-items: center; + gap: 0.45rem; + color: #2563eb; + font-weight: 500; +} + +.connectingSpinner { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid rgba(37, 99, 235, 0.4); + border-top-color: #2563eb; + animation: spin 1s linear infinite; +} + +.errorText { + color: #dc2626; +} + +.endedText { + color: #047857; +} + +.callPanel { + width: 100%; + max-width: 320px; + background: rgba(15, 23, 42, 0.95); + border: 1px solid rgba(148, 163, 184, 0.3); + border-radius: 18px; + padding: 1.2rem 1rem; + display: grid; + gap: 1rem; + box-shadow: 0 18px 36px rgba(15, 23, 42, 0.35); + color: #e2e8f0; +} + +.callPanelChip { + max-width: none; +} + +.callPanelInline { + max-width: 280px; +} + +.floatingRoot { + position: fixed; + bottom: 1.25rem; + right: 1.25rem; + z-index: 2147483646; + pointer-events: none; +} + +.floatingRoot .callPanel { + pointer-events: auto; +} + +.callHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 600; + color: rgba(226, 232, 240, 0.85); +} + +.callDot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 12px rgba(34, 197, 94, 0.8); +} + +.callLabel { + flex: 1; + margin-left: 0.5rem; +} + +.callTimer { + font-variant-numeric: tabular-nums; + font-weight: 500; + color: #38bdf8; +} + +.callWave { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: flex-end; + gap: 6px; + height: 32px; +} + +.callWave span { + display: block; + width: 100%; + border-radius: 6px; + background: linear-gradient(180deg, rgba(56, 189, 248, 0.9), rgba(8, 47, 73, 0.1)); + animation: wave 1.2s ease-in-out infinite; +} + +.callWave span:nth-child(2) { + animation-delay: 0.15s; +} + +.callWave span:nth-child(3) { + animation-delay: 0.3s; +} + +.callAction { + width: 100%; +} + +@keyframes wave { + 0%, + 100% { + height: 30%; + } + 50% { + height: 100%; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + @keyframes pulse { 0%, 100% { diff --git a/packages/docs/src/components/LazyThreeScene.tsx b/packages/docs/src/components/LazyThreeScene.tsx new file mode 100644 index 00000000000..d89892ace23 --- /dev/null +++ b/packages/docs/src/components/LazyThreeScene.tsx @@ -0,0 +1,36 @@ +import React, { useEffect, useState } from 'react' + +interface Props { + className?: string + fallbackClassName?: string +} + +type SceneComponent = React.ComponentType + +export default function LazyThreeScene({ className, fallbackClassName }: Props): JSX.Element { + const [ThreeScene, setThreeScene] = useState(null) + + useEffect(() => { + let mounted = true + + import('@site/src/components/Annimations/SphereScene') + .then((module) => { + if (mounted) { + setThreeScene(() => module.default) + } + }) + .catch((error) => { + console.warn('Unable to load ThreeJS scene', error) + }) + + return () => { + mounted = false + } + }, []) + + if (!ThreeScene) { + return