Skip to main content

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

๐Ÿ‘‹ Welcome! Ask me anything.

I can help you with questions about our documentation, features, and integrations.

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:

  1. Navigate to your Kapa dashboard
  2. Click Integrations โ†’ Add Integration
  3. Select Custom Frontend
  4. Provide a meaningful name, such as "In-app chat page"
  5. Add your domain where you want to deploy this to (including any preview/staging hostnames)
  6. 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 conversations
  • react-markdown - Renders formatted AI responses
  • react-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.

Chat.module.css
.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
note

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.

Hello! Your chat will appear here.

โœ… 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:

Chat.module.css
/* 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.

Type a question below to get started! ๐Ÿ‘‡

โœ… 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:

Chat.module.css
.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.

What is React?
**React** is a popular JavaScript library for building user interfaces, particularly web applications. It was developed by *Facebook* and allows developers to create reusable UI components. ## Key Features - **Component-based**: Build encapsulated components - **Declarative**: Describe what the UI should look like - **Virtual DOM**: Efficient updates and rendering Here's a simple example: ```jsx function Welcome() { return <h1>Hello, World!</h1>; } ```
How do I get started?
To get started with React, you can create a new project using **Create React App**: ```bash npx create-react-app my-app cd my-app npm start ``` This sets up a new React project with: 1. Modern build setup 2. Development server 3. Hot reloading 4. ESLint configuration *You'll have a working React app in minutes!*

โœ… 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:

Chat.module.css
.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.

How do I create a React component with TypeScript?

Here's how to create a React component with TypeScript:

Basic Function Component

import React from 'react';

interface ButtonProps {
  title: string;
  onClick: () => void;
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ title, onClick, disabled = false }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {title}
    </button>
  );
};

export default Button;

Key Points

  1. Interface Definition: Define props with TypeScript interfaces
  2. Optional Props: Use ? for optional properties like disabled?
  3. Default Values: Set defaults in the destructuring disabled = false
  4. Type Safety: TypeScript will catch type errors at compile time

This pattern ensures your components are type-safe and self-documenting!

โœ… 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:

Chat.module.css
.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.

How do I handle authentication in React apps?

Here are the most common approaches for handling authentication in React applications:

Token-Based Authentication

// Store JWT in localStorage or httpOnly cookies
const token = localStorage.getItem('authToken');

// Add to API requests
const response = await fetch('/api/data', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

Best Practices

  1. Secure Storage: Use httpOnly cookies for sensitive tokens
  2. Token Refresh: Implement automatic token refresh
  3. Protected Routes: Use React Router guards
  4. Context API: Share auth state across components

Always validate tokens on the server side for security!

โœ… 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:

Chat.module.css
.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:

Chat.module.css
.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;
}