Embedded chat experience
This tutorial demonstrates how to build a custom, embeddable UI for Kapa. Instead of using the pop-up modal that the default Kapa widget provides, this interface can sit within your website or app, providing a more integrated experience.
What you'll buildโ
๐ฌ Try the Demo
Powered by kapa.aiโขProtected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
This tutorial covers building a complete conversation interface with:
- Real-time AI responses with streaming
- Source attribution for every answer
- User feedback collection
- Responsive design for mobile and desktop
Each step includes a working demo showing your expected progress.
Prerequisitesโ
Before starting, ensure you have:
- A React project (Create React App, Next.js, Vite, or similar)
- 15 minutes to complete the implementation
- Access to your Kapa team dashboard
Step 1: Configure your integrationโ
First, prepare your integration:
- Navigate to your Kapa dashboard
- Click Integrations โ Add Integration
- Select Custom Frontend
- Provide a meaningful name, such as "In-app chat page"
- Add your domain where you want to deploy this to (including any preview/staging hostnames)
- Copy the Integration ID for use in your code
Save this Integration ID as it connects your chat interface to your team's AI knowledge base.
Step 2: Install dependenciesโ
Install the required packages:
npm install @kapaai/react-sdk react-markdown react-icons
Package overview:
@kapaai/react-sdk
- Core SDK for AI conversationsreact-markdown
- Renders formatted AI responsesreact-icons
- Provides UI icons
This implementation uses CSS Modules for styling, which works with any React setup without additional configuration.
Step 3: Create the stylesheetโ
Create the chat component styles in Chat.module.css
. This stylesheet uses CSS
variables for easy theming and establishes the visual foundation for your chat
interface. The example designs in this tutorial uses a dark theme that's easy
on the eyes; adapt the colorscheme to fit the context of where you want to
deploy.
.chatContainer {
/* Color palette - easily customizable */
--chat-bg-primary: #1b1b1d; /* Main background */
--chat-bg-secondary: #2f3136; /* Header, input area */
--chat-text-primary: #dcddde; /* Main text */
--chat-text-muted: #8e9297; /* Placeholder text */
--chat-accent-primary: #7c3aed; /* Your brand color */
--chat-border: #40444b; /* Borders */
--chat-feedback-positive: #10b981; /* Green for positive */
--chat-feedback-negative: #ef4444; /* Red for negative */
/* Layout */
height: 400px;
border: 1px solid var(--chat-border);
border-radius: 12px;
background-color: var(--chat-bg-primary);
display: flex;
flex-direction: column;
overflow: hidden;
}
.welcomeMessage {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--chat-text-muted);
text-align: center;
}
Key features:
- CSS Variables enable easy theme customization
- Dark theme provides modern appearance
- Flexbox layout creates responsive structure
The chat container will expand to fill its parent. For real applications, you
may want to add max-width: 800px; margin: 0 auto;
to center it with a
reasonable maximum width.
Step 4: Build the base componentโ
Create the initial React component in EmbeddedChat.js
:
import React from "react";
import { KapaProvider } from "@kapaai/react-sdk";
import styles from "./Chat.module.css";
// Replace with your actual Integration ID
const INTEGRATION_ID = "your-integration-id-here";
function ChatInterface() {
return (
<div className={styles.chatContainer}>
<div className={styles.welcomeMessage}>
Type a question below to get started! ๐
</div>
</div>
);
}
function EmbeddedChat() {
return (
<KapaProvider
integrationId={INTEGRATION_ID}
callbacks={{
askAI: {},
}}
>
<ChatInterface />
</KapaProvider>
);
}
export default EmbeddedChat;
The KapaProvider
component wraps your chat
interface and handles all the AI integration logic. It manages the connection
to your knowledge base, handles authentication, and provides the conversation
context to child components. The nested ChatInterface
component focuses
purely on UI rendering.
Testing: Replace "your-integration-id-here"
with your Integration ID and
import this component. The result should display a dark chat container with
welcome text.
โ Checkpoint: Verify the dark bordered container displays "Hello! Your chat will appear here." before proceeding.
Step 5: Add input functionalityโ
Add interactive input capabilities. First, extend your Chat.module.css
with
input area styles:
/* Add to Chat.module.css */
.messagesArea {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--chat-text-muted);
text-align: center;
padding: 0 1rem;
}
.inputArea {
padding: 1rem;
background-color: var(--chat-bg-secondary);
border-top: 1px solid var(--chat-border);
}
.inputForm {
display: flex;
gap: 0.75rem;
}
.textInput {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--chat-border);
border-radius: 8px;
background-color: var(--chat-bg-primary);
color: var(--chat-text-primary);
outline: none;
}
.textInput::placeholder {
color: var(--chat-text-muted);
}
.textInput:focus {
border-color: var(--chat-accent-primary);
}
.sendButton {
padding: 0.75rem 1rem;
background-color: var(--chat-accent-primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.sendButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Now update your ChatInterface
function:
import React, { useState } from "react";
import { useChat } from "@kapaai/react-sdk";
import { LuSend } from "react-icons/lu";
import styles from "./Chat.module.css";
// Replace with your actual Integration ID
const INTEGRATION_ID = "your-integration-id-here";
function ChatInterface() {
const [question, setQuestion] = useState("");
const { submitQuery } = useChat();
const handleSubmit = (e) => {
e.preventDefault();
if (question.trim()) {
submitQuery(question);
setQuestion("");
}
};
return (
<div className={styles.chatContainer}>
{/* Messages area - simple for now */}
<div className={styles.messagesArea}>
Type a question below to get started! ๐
</div>
{/* Input area */}
<div className={styles.inputArea}>
<form onSubmit={handleSubmit} className={styles.inputForm}>
<input
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask me anything..."
className={styles.textInput}
/>
<button
type="submit"
disabled={!question.trim()}
className={styles.sendButton}
>
<LuSend />
</button>
</form>
</div>
</div>
);
}
Testing: Type a question and submit. The input should clear, indicating the form submission works correctly.
โ Checkpoint: Verify you can type a question and see the input clear on submission. The form handling is now functional.
Step 6: Display conversationsโ
Add conversation display capabilities. The Kapa SDK manages conversation state
automatically - each question/answer pair is stored in a conversation
array
that updates in real-time as users interact with the AI.
First, add message bubble styles to your CSS file:
.conversationList {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
flex: 1;
overflow-y: auto;
scroll-behavior: smooth;
}
.messageGroup {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.userMessage {
max-width: 80%;
background-color: var(--chat-accent-primary);
color: white;
padding: 1rem;
border-radius: 12px;
line-height: 1.6;
word-wrap: break-word;
align-self: flex-end;
font-size: 0.875rem;
}
.aiMessage {
max-width: 80%;
background-color: var(--chat-bg-secondary);
border: 1px solid var(--chat-border);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 1rem;
align-self: flex-start;
font-size: 0.875rem;
}
.thinkingMessage {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--chat-text-muted);
}
.thinkingDots {
display: flex;
gap: 0.25rem;
}
.thinkingDot {
width: 8px;
height: 8px;
background-color: var(--chat-text-muted);
border-radius: 50%;
animation: thinking 1.4s infinite ease-in-out;
}
.thinkingDot:nth-child(1) {
animation-delay: 0s;
}
.thinkingDot:nth-child(2) {
animation-delay: 0.2s;
}
.thinkingDot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes thinking {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}
Now create a ChatMessage
component to handle individual messages, then update
your ChatInterface
. This keeps the code organized and makes it easy to
enhance messages later:
// Create a ChatMessage component
function ChatMessage({ qa }) {
return (
<div className={styles.messageGroup}>
{/* Your question */}
<div className={styles.userMessage}>{qa.question}</div>
{/* AI response */}
<div className={styles.aiMessage}>
{qa.answer ? (
qa.answer
) : (
<div className={styles.thinkingMessage}>
<span>Thinking</span>
<div className={styles.thinkingDots}>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
</div>
</div>
)}
</div>
</div>
);
}
// Update your ChatInterface to use the component
function ChatInterface() {
const [question, setQuestion] = useState("");
const { conversation, submitQuery } = useChat();
const handleSubmit = (e) => {
e.preventDefault();
if (question.trim()) {
submitQuery(question);
setQuestion("");
}
};
return (
<div className={styles.chatContainer}>
{/* Messages area */}
<div className={styles.conversationList}>
{conversation.length === 0 ? (
<div className={styles.messagesArea}>
Ask your first question below! ๐
</div>
) : (
conversation.map((qa, index) => <ChatMessage key={index} qa={qa} />)
)}
</div>
{/* Input area - same as before... */}
</div>
);
}
Now the main interface stays clean while each message is handled by its own component.
Testing: Submit a question to see the conversation display in action. Your questions appear in purple bubbles, followed by AI responses in gray bubbles.
โ Checkpoint: Verify conversations display with purple bubbles for questions and gray bubbles for responses. The chat interface now handles conversations.
Step 7: Add markdown renderingโ
Kapa's responses include Markdown-formatted content, like code examples,
numbered lists, and links. Without proper rendering, users see raw markdown
syntax (**bold**
, ## headers
) instead of formatted content. Use the
react-markdown
library to transform this raw text into properly styled HTML
elements.
Extend your CSS file to handle styling for the transformed markdown:
.markdownContent {
line-height: 1.6;
text-align: left;
}
.markdownContent p {
margin: 0 0 1rem 0;
}
.markdownContent code {
background-color: var(--chat-bg-primary);
padding: 0.125rem 0.25rem;
border-radius: 4px;
font-family: "Monaco", monospace;
font-size: 0.875rem;
}
.markdownContent pre {
background-color: var(--chat-bg-primary);
padding: 1rem;
border-radius: 8px;
overflow: auto;
margin: 1rem 0;
line-height: 1.4;
}
.markdownContent pre code {
background: none;
padding: 0;
line-height: inherit;
display: block;
}
.markdownContent ul,
.markdownContent ol {
margin: 1rem 0;
padding-left: 1.5rem;
}
.markdownContent li {
margin: 0.25rem 0;
}
.markdownContent a {
color: var(--chat-accent-primary);
text-decoration: none;
}
.markdownContent a:hover {
text-decoration: underline;
}
Now add markdown rendering to your existing ChatMessage
component:
import React, { useState } from "react";
import { useChat } from "@kapaai/react-sdk";
import ReactMarkdown from "react-markdown";
import { LuSend } from "react-icons/lu";
import styles from "./Chat.module.css";
function ChatMessage({ qa }) {
return (
<div className={styles.messageGroup}>
<div className={styles.userMessage}>{qa.question}</div>
<div className={styles.aiMessage}>
{qa.answer ? (
<div className={styles.markdownContent}>
<ReactMarkdown>{qa.answer}</ReactMarkdown>
</div>
) : (
<div className={styles.thinkingMessage}>
<span>Thinking</span>
<div className={styles.thinkingDots}>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
</div>
</div>
)}
</div>
</div>
);
}
Testing: The markdown rendering transforms raw text formatting into styled content, including code blocks and lists.
โ Checkpoint: Verify code blocks have darker backgrounds and lists render with proper formatting.
Step 8: Add sources and feedbackโ
Complete the interface with source attribution and feedback collection. Source links build user trust by showing where information comes from, while feedback buttons help improve your AI's responses over time.
Add styles for these sections:
.sourcesSection {
border-top: 1px solid var(--chat-border);
margin-top: 1rem;
padding-top: 0.5rem;
font-size: 0.875rem;
}
.sourcesTitle {
font-weight: 600;
color: var(--chat-text-muted);
margin-bottom: 0.5rem;
}
.sourceLink {
color: var(--chat-accent-primary);
text-decoration: none;
}
.sourceLink:hover {
text-decoration: underline;
}
.feedbackSection {
border-top: 1px solid var(--chat-border);
margin-top: 1rem;
padding-top: 0.5rem;
display: flex;
gap: 0.5rem;
}
.feedbackButton {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: none;
border: none;
border-radius: 4px;
color: var(--chat-text-muted);
cursor: pointer;
font-size: 0.875rem;
}
.feedbackButton:hover {
background-color: var(--chat-bg-primary);
}
.feedbackActive {
background-color: var(--chat-bg-primary);
font-weight: 500;
}
.feedbackButton:nth-of-type(1).feedbackActive {
color: var(--chat-feedback-positive);
}
.feedbackButton:nth-of-type(2).feedbackActive {
color: var(--chat-feedback-negative);
}
Now add sources and feedback functionality. Update your ChatMessage
component to:
- Import the thumbs up/down icons
- Render two new sections below the AI answer
import React, { useState } from "react";
import { useChat } from "@kapaai/react-sdk";
import ReactMarkdown from "react-markdown";
import { LuSend, LuThumbsUp, LuThumbsDown } from "react-icons/lu";
import styles from "./Chat.module.css";
function ChatMessage({ qa }) {
const { addFeedback } = useChat();
const [feedback, setFeedback] = useState(null);
const handleFeedback = (type) => {
setFeedback(type);
addFeedback(qa.id, type);
};
return (
<div className={styles.messageGroup}>
<div className={styles.userMessage}>{qa.question}</div>
<div className={styles.aiMessage}>
{qa.answer ? (
<>
<div className={styles.markdownContent}>
<ReactMarkdown>{qa.answer}</ReactMarkdown>
</div>
{/* Sources section */}
{qa.sources && qa.sources.length > 0 && (
<div className={styles.sourcesSection}>
<div className={styles.sourcesTitle}>Sources:</div>
{qa.sources.map((source, index) => (
<div key={index}>
<a
href={source.source_url}
className={styles.sourceLink}
target="_blank"
rel="noopener noreferrer"
>
{source.title}
</a>
</div>
))}
</div>
)}
{/* Feedback section */}
{qa.id && (
<div className={styles.feedbackSection}>
<button
className={`${styles.feedbackButton} ${feedback === "upvote" ? styles.feedbackActive : ""}`}
onClick={() => handleFeedback("upvote")}
>
<LuThumbsUp /> Helpful
</button>
<button
className={`${styles.feedbackButton} ${feedback === "downvote" ? styles.feedbackActive : ""}`}
onClick={() => handleFeedback("downvote")}
>
<LuThumbsDown /> Not helpful
</button>
</div>
)}
</>
) : (
<div className={styles.thinkingMessage}>
<span>Thinking</span>
<div className={styles.thinkingDots}>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
<div className={styles.thinkingDot}></div>
</div>
</div>
)}
</div>
</div>
);
}
Testing: Submit questions to see source links and feedback buttons appear below responses.
โ Checkpoint: Verify sources appear as clickable links and feedback buttons respond to clicks. This builds user transparency and feedback collection.
Step 9: Add auto-scroll and header componentโ
Complete the chat interface by adding auto-scrolling behavior and a header component. The auto-scroll feature keeps new messages visible, while the header provides a professional appearance with a "New Chat" button for starting fresh conversations.
Add the final features. First, update your imports to include the new hooks and icon:
import React, { useState, useEffect, useRef } from "react";
import { useChat } from "@kapaai/react-sdk";
import ReactMarkdown from "react-markdown";
import { LuSend, LuThumbsUp, LuThumbsDown, LuRefreshCw } from "react-icons/lu";
import styles from "./Chat.module.css";
Add a new ChatHeader
component:
// New ChatHeader component
function ChatHeader({ conversation, resetConversation }) {
return (
<div className={styles.chatHeader}>
<h2 className={styles.chatTitle}>AI Assistant</h2>
<button
className={styles.newChatButton}
onClick={resetConversation}
disabled={conversation.length === 0}
>
<LuRefreshCw />
New Chat
</button>
</div>
);
}
// ChatMessage component stays the same as Step 8
function ChatMessage({ qa }) {
// ... same as before
}
Update your ChatInterface
to add auto-scroll and use the header:
function ChatInterface() {
const [question, setQuestion] = useState("");
const messagesEndRef = useRef(null);
const { conversation, submitQuery, resetConversation } = useChat();
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [conversation]);
const handleSubmit = (e) => {
e.preventDefault();
if (question.trim()) {
submitQuery(question);
setQuestion("");
}
};
return (
<div className={styles.chatContainer}>
{/* Header component */}
<ChatHeader
conversation={conversation}
resetConversation={resetConversation}
/>
<div className={styles.conversationList}>
{conversation.length === 0 ? (
<div className={styles.messagesArea}>
Ask your first question below! ๐
</div>
) : (
<>
{conversation.map((qa, index) => (
<ChatMessage key={index} qa={qa} />
))}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Input area unchanged */}
<div className={styles.inputArea}>{/* ... same as before */}</div>
</div>
);
}
Add these styles to your CSS file:
.chatHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: var(--chat-bg-secondary);
border-bottom: 1px solid var(--chat-border);
}
.chatTitle {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--chat-text-primary);
}
.newChatButton {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--chat-border);
border-radius: 6px;
color: var(--chat-text-muted);
cursor: pointer;
font-size: 0.875rem;
}
.newChatButton:hover:not(:disabled) {
background-color: var(--chat-bg-tertiary);
color: var(--chat-text-primary);
}
.newChatButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Testing: Submit multiple questions to verify the chat automatically scrolls to display the latest responses. The "New Chat" button should clear all messages when clicked.
โ Checkpoint: Confirm the chat automatically scrolls to new messages and the header provides reset functionality. The interface now provides a complete user experience.
Next stepsโ
Your chat interface is now functionally complete with real-time AI conversations, markdown rendering, source attribution, user feedback, and responsive design.
For production deployment, configure user identification and analytics tracking, ensure compliance requirements are met, and customize the theme to match your brand.
Essential features to addโ
User identification: Track who's using your chat for more analytics insights:
// Set this before the KapaProvider is mounted
window.kapaSettings = {
user: {
email: "user@example.com",
uniqueClientId: "user-123", // Your internal user ID
metadata: {
companyName: "Acme Corp",
firstName: "Jane",
lastName: "Doe",
},
},
};
Analytics tracking: Capture usage metrics with your analytics provider:
callbacks={{
askAI: {
onAnswerGenerationCompleted: (data) => {
// Send to your analytics (Mixpanel, Amplitude, etc.)
analytics.track('Chat Question Asked', {
question: data.question,
answer: data.answer,
questionId: data.questionAnswerId,
});
},
onFeedbackSubmit: (data) => {
analytics.track('Chat Feedback Given', {
reaction: data.reaction,
questionId: data.questionAnswerId
});
}
}
}}
Required complianceโ
Attribution: Always include attribution to kapa.ai in your interface:
<div className={styles.footer}>
Powered by <a href="https://kapa.ai">kapa.ai</a>
</div>
CAPTCHA compliance: Add the required disclaimer when hiding the badge:
<!-- Required disclaimer text -->
<p class="recaptcha-disclaimer">
This site is protected by reCAPTCHA and the Google
<a href="https://policies.google.com/privacy">Privacy Policy</a> and
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
</p>
/* Hide the badge when disclaimer is present */
.grecaptcha-badge {
visibility: hidden;
}
Configurationโ
Bot protection: China's Great Firewall may block access to Google reCAPTCHA; to enable access for mainland China users, use hCaptcha instead:
<KapaProvider
integrationId={INTEGRATION_ID}
captchaProvider="hcaptcha"
callbacks={{...}}
>
Content Security Policy (CSP): If your application uses CSP, you'll need to configure it to allow kapa.ai resources. Refer to Understanding CSP and CORS for Kapa for detailed setup instructions.
Theme customizationโ
Focus on the key variables that impact brand identity:
.chatContainer {
/* Brand colors */
--chat-accent-primary: #your-brand-color;
--chat-feedback-positive: #your-success-color;
--chat-feedback-negative: #your-error-color;
/* Match your site's theme */
--chat-bg-primary: #your-background;
--chat-text-primary: #your-text-color;
}