Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,4 @@ requiredVars.env

bwsconfig.json
.bwsconfig.cache
_docs/
6 changes: 6 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ let nextConfig = withBundleAnalyzer({
hostname: 'localhost',
port: '4000',
pathname: '/**'
},
{
protocol: 'https',
hostname: '**.theanswer.ai',
port: '',
pathname: '/**'
}
]
},
Expand Down
1 change: 1 addition & 0 deletions copilot/flowise/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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 = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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 = {}
Expand Down
12 changes: 8 additions & 4 deletions packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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`
Comment on lines +131 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Env var can’t satisfy both MCP metadata and SSE URLs

The new configuration assumes a single ATLASSIAN_MCP_SERVER_URL is used for all Atlassian MCP endpoints, but the code now derives the SSE address as ${ATLASSIAN_MCP_SERVER_URL}/sse here while fetchMCPMetadata builds its OAuth well‑known URL as ${ATLASSIAN_MCP_SERVER_URL}/.well-known/oauth-authorization-server. On the current Atlassian service the SSE endpoint lives under /v1/sse while the metadata lives at /.well-known/..., so no single value for ATLASSIAN_MCP_SERVER_URL can make both requests correct. Setting it to the host breaks SSE (…/sse), and adding /v1 fixes SSE but causes metadata requests to …/v1/.well-known, which will 404. This effectively breaks existing installations unless a second variable or path handling is added.

Useful? React with 👍 / 👎.

}

const toolkit = new MCPToolkit(serverParams, 'sse', credentialData.access_token)
Expand Down
6 changes: 4 additions & 2 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -35,14 +36,15 @@
"typescript": "~5.5.4"
},
"engines": {
"node": ">=18.0"
"node": ">=20.0"
},
"name": "docs",
"private": true,
"scripts": {
"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",
Expand Down
180 changes: 136 additions & 44 deletions packages/docs/src/components/ElevenLabsInlineWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -31,14 +32,23 @@ const ElevenLabsInlineWidget: React.FC<ElevenLabsInlineWidgetProps> = ({
const [error, setError] = useState<string | null>(null)
const [hasStarted, setHasStarted] = useState(false)
const [hasEnded, setHasEnded] = useState(false)
const [elapsedSeconds, setElapsedSeconds] = useState(0)
const callStartRef = useRef<number | null>(null)
const timerRef = useRef<number | null>(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')
Expand All @@ -65,12 +75,64 @@ const ElevenLabsInlineWidget: React.FC<ElevenLabsInlineWidgetProps> = ({
}
}

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<HTMLElement | null>(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
Expand All @@ -83,57 +145,87 @@ const ElevenLabsInlineWidget: React.FC<ElevenLabsInlineWidgetProps> = ({
conversation.status === 'connecting' && styles.loading
)

if (inline) {
const isConnected = conversation.status === 'connected'
const isConnecting = conversation.status === 'connecting'

const renderCallPanel = (inlineMode = false) => (
<div
className={clsx(styles.callPanel, inlineMode && styles.callPanelInline, variant === 'chip' && styles.callPanelChip)}
role='status'
aria-live='polite'
>
<div className={styles.callHeader}>
<span className={styles.callDot} aria-hidden='true' />
<span className={styles.callLabel}>Live with AnswerAgent</span>
<span className={styles.callTimer}>{formatDuration(elapsedSeconds)}</span>
</div>
<div className={styles.callWave} aria-hidden='true'>
<span />
<span />
<span />
</div>
<button onClick={endCall} className={clsx(styles.widgetButton, styles.danger, styles.callAction)}>
End Call
</button>
</div>
)

const renderStatus = () => {
if (!showStatus) {
return null
}

if (isConnected) {
return null
}

return (
<>
{conversation.status !== 'connected' ? (
<button onClick={startCall} disabled={conversation.status === 'connecting'} className={buttonClass}>
{variant === 'chip' && emoji && <span className={styles.chipEmoji}>{emoji}</span>}
{conversation.status === 'connecting' ? 'Connecting...' : text}
</button>
) : (
<button onClick={endCall} className={clsx(styles.widgetButton, styles.danger)}>
End Call
</button>
)}
{showStatus && (
<div
style={{
position: 'absolute',
bottom: -24,
left: '50%',
transform: 'translateX(-50%)',
whiteSpace: 'nowrap',
fontSize: '0.875rem'
}}
>
{conversation.status === 'connected' && <span>Call in progress...</span>}
{error && <span style={{ color: '#ef4444' }}>{error}</span>}
{hasStarted && hasEnded && conversation.status !== 'connected' && !error && <span>Call ended.</span>}
<div className={styles.statusArea} role='status' aria-live='polite'>
{isConnecting && (
<div className={styles.connectingNotice}>
<span className={styles.connectingSpinner} aria-hidden='true' />
<span>Connecting to AnswerAgent…</span>
</div>
)}
</>
{error && <div className={styles.errorText}>{error}</div>}
{hasStarted && hasEnded && !error && <div className={styles.endedText}>Call ended. Restart anytime.</div>}
</div>
)
}

return (
<div className={wrapperClassName} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{conversation.status !== 'connected' ? (
<button onClick={startCall} disabled={conversation.status === 'connecting'} className={buttonClass}>
if (inline) {
return isConnected ? (
renderCallPanel(true)
) : (
<div className={styles.inlineContainer}>
<button onClick={startCall} disabled={isConnecting} className={buttonClass}>
{variant === 'chip' && emoji && <span className={styles.chipEmoji}>{emoji}</span>}
{conversation.status === 'connecting' ? 'Connecting...' : text}
</button>
) : (
<button onClick={endCall} className={clsx(styles.widgetButton, styles.danger)}>
End Call
{isConnecting ? 'Connecting…' : text}
</button>
)}
{showStatus && (
<div style={{ marginTop: 8, minHeight: 24 }}>
{conversation.status === 'connected' && <span>Call in progress...</span>}
{error && <span style={{ color: '#ef4444' }}>{error}</span>}
{hasStarted && hasEnded && conversation.status !== 'connected' && !error && <span>Call ended.</span>}
{renderStatus()}
</div>
)
}

const floatingPanel = isConnected && floatingContainer ? createPortal(renderCallPanel(), floatingContainer) : null

return (
<div className={clsx(styles.widgetWrapper, wrapperClassName)}>
{floatingPanel}
{isConnected ? (
<div className={styles.floatingPlaceholder}>
<span role='status' aria-live='polite'>
Live call active — controls are docked bottom right.
</span>
</div>
) : (
<>
<button onClick={startCall} disabled={isConnecting} className={buttonClass}>
{variant === 'chip' && emoji && <span className={styles.chipEmoji}>{emoji}</span>}
{isConnecting ? 'Connecting…' : text}
</button>
{renderStatus()}
</>
)}
</div>
)
Expand Down
Loading
Loading