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/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 = {} 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/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