ResearchAI Embed Integration Guide
This document describes how to integrate ResearchAI as an embeddable widget using an iframe in external applications.
Overview
ResearchAI provides an embed mode that allows the research assistant to be embedded as an iframe widget within other applications. The embed mode uses postMessage API for parent-child communication and provides a streamlined UI optimized for embedded contexts.
Routes
| Route | Description |
|---|---|
/research-ai/embed |
Entry point for creating new research chats |
/research-ai/embed/:token |
Displays an existing chat by token |
/research-ai/embed/demo |
Demo page for testing embed integration |
/research-ai/chat/summary |
Creates a chat from an AI summary token |
Demo Page
Visit /research-ai/embed/demo to test all embed patterns interactively. The demo page provides buttons for each integration pattern and displays an event log showing postMessage communication.
Quick Start
Basic iframe Implementation
<iframe
src="https://your-domain.com/research-ai/embed?q=your+search+query&locale=en&embed=true"
title="Research AI Widget"
style="width: 100%; height: 600px; border: 1px solid #ccc; border-radius: 8px;"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation allow-top-navigation-by-user-activation allow-downloads"
></iframe>
URL Parameters
Entry Point (/research-ai/embed)
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | The initial research question. If provided, automatically creates a new chat and redirects to the token route. |
contentId |
string | No | A Statista content ID (e.g., statistic ID) to attach as context to the new chat. Only used when q is also provided. |
locale |
string | No | Locale for the interface (en, de). Defaults to detected locale or en. |
embed |
string | Yes | Must be set to "true" to enable embed mode styling. |
Summary Route (/research-ai/chat/summary)
Use this route to create a chat from an existing AI summary.
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | The cache key from an AI summary. Creates a chat from the summary. |
q |
string | No | A question to send immediately after creating the chat from the summary. |
embed |
string | No | Set to "true" to redirect to embed mode after chat creation. |
origin |
string | No | Origin identifier for tracking purposes. |
Token Route (/research-ai/embed/:token)
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | A follow-up question to send to an existing chat. |
locale |
string | No | Locale for the interface. |
embed |
string | Yes | Must be set to "true" to enable embed mode styling. |
Parent-Child Communication
The embed widget communicates with the parent window using the postMessage API.
Message Types
research-ai:chat-created
Sent from the iframe to the parent window when a new chat is created or when navigating to an existing chat.
type EmbedChatCreatedPayload = {
type: "research-ai:chat-created";
chatId: string; // The unique chat token/ID
question?: string | null; // The question that was asked (if available)
timestamp: number; // Unix timestamp when the message was sent
};
research-ai:close
Sent from the iframe to the parent window when the user clicks the close button in embed mode. The parent application should listen for this message to close the modal/iframe container.
type EmbedClosePayload = {
type: "research-ai:close";
messageCount: number; // The number of messages in the chat when the user closed
timestamp: number; // Unix timestamp when the message was sent
};
research-ai:content-loaded
Sent from the iframe to the parent window when the embed content has finished loading. The parent application should listen for this message to hide any loading indicators shown while the iframe was loading.
type EmbedContentLoadedPayload = {
type: "research-ai:content-loaded";
timestamp: number; // Unix timestamp when the message was sent
};
Listening for Messages (Parent Window)
window.addEventListener("message", (event) => {
// Validate origin for security
if (event.origin !== "https://your-domain.com") {
return;
}
const data = event.data;
// Handle chat created
if (data?.type === "research-ai:chat-created") {
console.log("Chat created:", data.chatId);
console.log("Question:", data.question);
// Store the chatId for future interactions
localStorage.setItem("research-ai:embed:last-chat-id", data.chatId);
}
// Handle close request
if (data?.type === "research-ai:close") {
console.log("Close requested by embed");
console.log("Message count:", data.messageCount);
// Close your modal/iframe container here
closeModal();
}
// Handle content loaded - hide loading spinner
if (data?.type === "research-ai:content-loaded") {
console.log("Embed content loaded");
// Hide your loading spinner here
hideLoadingSpinner();
}
});
Usage Patterns
Pattern 1: New Question
Opens the widget with a fresh question, creating a new chat session:
const question = "What are the latest e-commerce trends in Germany?";
const iframeSrc = `/research-ai/embed?q=${encodeURIComponent(question)}&embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
Pattern 2: Resume Existing Chat
Navigate to an existing chat using its token:
const chatToken = "abc123-def456-ghi789";
const iframeSrc = `/research-ai/embed/${chatToken}?embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
Pattern 3: Send Follow-up Question
Send a follow-up question to an existing chat:
const chatToken = "abc123-def456-ghi789";
const followUpQuestion = "How does this compare to last year?";
const iframeSrc = `/research-ai/embed/${chatToken}?q=${encodeURIComponent(followUpQuestion)}&embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
Pattern 4: Open Without Question (Login/Empty State)
Open the widget without a question to show a welcome or login state:
const iframeSrc = `/research-ai/embed?embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
Pattern 5: Create Chat from AI Summary
Create a chat using a summary token (cache key) from an existing AI summary:
const summaryToken = "summary-cache-key-123";
const iframeSrc = `/research-ai/chat/summary?token=${encodeURIComponent(summaryToken)}&embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
Optionally include a question to send immediately after creating the chat:
const summaryToken = "summary-cache-key-123";
const question = "What are the key insights from this data?";
const iframeSrc = `/research-ai/chat/summary?token=${encodeURIComponent(summaryToken)}&q=${encodeURIComponent(question)}&embed=true`;
document.getElementById("research-iframe").src = iframeSrc;
This pattern is useful when the parent application has already generated an AI summary (e.g., from search results) and wants to continue the conversation in the ResearchAI widget. The route shows a loading state while processing, then redirects to /research-ai/embed/:token.
Pattern 6: New Question with Content Context
Opens the widget with a question and attaches a Statista content (e.g., statistic) as context. The AI will use the referenced content to provide a more relevant answer:
import { useState } from "react";
const BASE_URL = "https://your-domain.com";
function StatisticWidget({ statisticId }: { statisticId: string }) {
const [iframeSrc, setIframeSrc] = useState("");
const [isOpen, setIsOpen] = useState(false);
const askAboutStatistic = (question: string) => {
const src = `${BASE_URL}/research-ai/embed?q=${encodeURIComponent(question)}&contentId=${encodeURIComponent(statisticId)}&embed=true`;
setIframeSrc(src);
setIsOpen(true);
};
return (
<>
<button
onClick={() =>
askAboutStatistic("What are the key insights from this statistic?")
}
>
Ask Research AI
</button>
{isOpen && (
<iframe
src={iframeSrc}
title="Research AI Widget"
style={{ width: "100%", height: "600px" }}
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation allow-top-navigation-by-user-activation"
/>
)}
</>
);
}
The contentId parameter tells ResearchAI to attach the referenced content as context to the chat. This allows the AI to ground its response on the specific statistic data, providing more accurate and relevant answers.
Note:
contentIdonly takes effect whenqis also provided. It has no effect on its own without a question.
Complete Integration Example (React)
import { useCallback, useEffect, useState } from "react";
const STORAGE_KEY = "research-ai:embed:last-chat-id";
const BASE_URL = "https://your-domain.com"; // Replace with actual domain
type EmbedChatCreatedMessage = {
type: "research-ai:chat-created";
chatId: string;
question?: string | null;
};
type EmbedCloseMessage = {
type: "research-ai:close";
messageCount: number;
};
type EmbedMessage = EmbedChatCreatedMessage | EmbedCloseMessage;
export function ResearchAIWidget() {
const [isOpen, setIsOpen] = useState(false);
const [chatId, setChatId] = useState<string | null>(null);
const [iframeSrc, setIframeSrc] = useState("");
// Load stored chat ID on mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) setChatId(stored);
}, []);
// Listen for messages from iframe
useEffect(() => {
const handleMessage = (event: MessageEvent<EmbedMessage>) => {
if (event.origin !== BASE_URL) return;
// Handle close request
if (event.data?.type === "research-ai:close") {
setIsOpen(false);
return;
}
// Handle chat created
if (event.data?.type === "research-ai:chat-created") {
const { chatId: newChatId } = event.data;
setChatId(newChatId);
localStorage.setItem(STORAGE_KEY, newChatId);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
const openWidget = useCallback(
(options?: {
question?: string;
summaryToken?: string;
contentId?: string;
}) => {
let src: string;
if (options?.summaryToken) {
// Create chat from AI summary (optionally with a question)
src = `${BASE_URL}/research-ai/chat/summary?token=${encodeURIComponent(options.summaryToken)}&q=${encodeURIComponent(options.question || "")}&embed=true`;
} else if (options?.question && options?.contentId) {
// New question with content context (e.g., statistic reference)
src = `${BASE_URL}/research-ai/embed?q=${encodeURIComponent(options.question)}&contentId=${encodeURIComponent(options.contentId)}&embed=true`;
} else if (chatId) {
// Resume existing chat
src = `${BASE_URL}/research-ai/embed/${chatId}?embed=true`;
} else {
// Empty state
src = `${BASE_URL}/research-ai/embed?embed=true`;
}
setIframeSrc(src);
setIsOpen(true);
},
[chatId],
);
const sendFollowUp = useCallback(
(question: string) => {
if (!chatId) {
openWidget({ question });
return;
}
setIframeSrc(
`${BASE_URL}/research-ai/embed/${chatId}?q=${encodeURIComponent(question)}&embed=true`,
);
setIsOpen(true);
},
[chatId, openWidget],
);
if (!isOpen) {
return (
<div className="flex gap-2">
<button onClick={() => openWidget()}>Ask Research AI</button>
<button
onClick={() =>
openWidget({
question: "What are the key insights from this statistic?",
contentId: "12345",
})
}
>
Ask about Statistic
</button>
<button
onClick={() => openWidget({ summaryToken: "your-summary-token" })}
>
Open from Summary
</button>
<button
onClick={() =>
openWidget({
summaryToken: "your-summary-token",
question: "What are the key insights?",
})
}
>
Summary with Question
</button>
<button
onClick={() => sendFollowUp("How does this compare to last year?")}
>
Send Follow-up
</button>
</div>
);
}
return (
<div className="z-50 bg-black/50 fixed inset-0 flex items-center justify-center">
<div className="rounded-xl bg-white relative h-[80vh] w-[90vw] max-w-6xl p-4">
<button
onClick={() => setIsOpen(false)}
className="absolute right-4 top-4"
>
✕
</button>
<iframe
src={iframeSrc}
title="Research AI Widget"
className="border h-full w-full rounded-lg"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation allow-top-navigation-by-user-activation allow-downloads"
/>
</div>
</div>
);
}
Sandbox Attributes
The recommended sandbox attributes for the iframe are:
| Attribute | Purpose |
|---|---|
allow-same-origin |
Required for authentication cookies and storage |
allow-scripts |
Required for JavaScript execution |
allow-forms |
Required for form submissions (questions, feedback) |
allow-popups |
Required for opening links in new tabs |
allow-top-navigation |
Required for redirect flows (authentication) |
allow-top-navigation-by-user-activation |
Safer navigation triggered by user action |
allow-downloads |
Required for CSV and file downloads |
sandbox="allow-same-origin allow-scripts allow-forms allow-popups
allow-top-navigation allow-top-navigation-by-user-activation allow-downloads"
Authentication
The embed routes use the same authentication mechanism as the main application:
- Authenticated users: Full access to research features with user limits applied
- Unauthenticated users: Redirected to the embed entry point (may trigger login flow)
The authentication state is determined by SSO cookies, so users logged into the main Statista platform will automatically be authenticated in the embed widget.
Styling & Layout
When embed=true is set:
- The UI is optimized for iframe display with full-height layout
- Navigation elements and headers are minimized
- Toast notifications appear at the top of the embed container
- Sources panel is available with expandable/collapsible functionality
- Chat history sidebar is available with animated expand/collapse
Header Action Buttons
The embed header includes the following action buttons:
| Button | Visibility | Description |
|---|---|---|
| Show/Hide Sources | Always | Toggles the sources panel |
| History | Mobile only | Toggles the chat history sidebar |
| Fullscreen | Always | Opens the chat in a new tab (full ResearchAI) |
| Close | Always | Sends research-ai:close postMessage to parent |
Supported Locales
| Code | Language |
|---|---|
en |
English (default) |
de |
German |
Error Handling
| Scenario | Behavior |
|---|---|
| Invalid token | Redirects to /research-ai/embed?embed=true |
| Unauthenticated user | Redirects to /research-ai/embed?embed=true |
| API error | Redirects to /research-ai/chat (main app) |
| Rate limit reached | Shows user limit modal within embed |
Best Practices
-
Always set
embed=true: This query parameter enables embed-specific styling and behavior. -
Store chat IDs: Listen for
research-ai:chat-createdmessages and store thechatIdto enable chat resumption. -
Validate message origin: Always check
event.originwhen handlingpostMessageevents. -
Handle authentication: Users must be logged in to use the research features. Consider implementing a login prompt in your parent application.
-
Responsive sizing: Use percentage-based or viewport-relative sizing for the iframe to ensure it adapts to different screen sizes.
-
URL encode questions: Always use
encodeURIComponent()when passing questions as URL parameters.
Troubleshooting
Iframe not loading
- Verify the
sandboxattributes are correctly set - Check browser console for CORS or CSP errors
- Ensure the domain is allowed in any Content Security Policy
Messages not being received
- Confirm you're listening on the correct origin
- Verify the message type matches
research-ai:chat-created - Check that the iframe is from the expected domain
Authentication issues
- Ensure cookies are allowed for cross-origin iframes
- Check if third-party cookie blocking is affecting the SSO flow
- Verify the user is logged into the main Statista platform
Chat not resuming
- Verify the stored
chatIdis valid - Check if the chat session has expired
- Ensure the token URL is correctly formatted