Claude
Skills
Sign in
Back

chatkit-widget

Included with Lifetime
$97 forever

Use when integrating OpenAI/ChatKit chat widgets into Next.js/React applications. Triggers for: embedding chat widgets, configuring widget appearance, implementing event handlers, setting up authenticated chat access, or customizing widget branding. NOT for: building custom chat UIs from scratch or backend AI model configuration.

Web Devscripts

What this skill does


# ChatKit Widget Integration Skill

Expert integration of OpenAI/ChatKit chat widgets into Next.js/React applications with secure configuration and custom branding.

## Quick Reference

| Task | File/Component |
|------|----------------|
| Widget component | `components/chat/ChatWidget.tsx` |
| Configuration | `config/chatkit.config.ts` |
| Layout integration | `app/layout.tsx` or `pages/_app.tsx` |
| API proxy | `app/api/chatkit/route.ts` |

## Project Structure

```
frontend/
├── app/
│   ├── api/
│   │   └── chatkit/
│   │       └── route.ts          # Secure API proxy
│   ├── components/
│   │   └── chat/
│   │       ├── ChatWidget.tsx    # Main widget component
│   │       └── ChatButton.tsx    # Custom trigger button
│   └── layout.tsx                # Root layout with widget
├── lib/
│   └── chatkit.ts                # Utility functions
└── config/
    └── chatkit.config.ts         # Widget configuration
```

## Widget Configuration

### Configuration File

```typescript
// frontend/config/chatkit.config.ts
import { ChatKitConfig } from "@/lib/chatkit";

interface ChatKitConfig {
  // Public configuration (safe to expose)
  projectId: string;
  publicKey: string;

  // Server-side configuration (fetched from API)
  apiUrl: string;

  // Branding
  theme: {
    primaryColor: string;
    secondaryColor: string;
    textColor: string;
    backgroundColor: string;
    borderRadius: string;
  };

  // Positioning
  position: {
    bottom: string;
    right: string;
    mobileBottom: string;
    mobileRight: string;
  };

  // Behavior
  behavior: {
    defaultOpen: boolean;
    showOnPages: string[]; // Glob patterns
    hideOnPages: string[]; // Glob patterns
    allowedRoles: string[]; // Empty = all users
  };

  // Content
  content: {
    welcomeMessage: string;
    placeholderText: string;
    headerTitle: string;
    headerSubtitle: string;
  };
}

export const chatkitConfig: ChatKitConfig = {
  // Public keys - these can be safely exposed
  projectId: process.env.NEXT_PUBLIC_CHATKIT_PROJECT_ID || "",
  publicKey: process.env.NEXT_PUBLIC_CHATKIT_PUBLIC_KEY || "",

  // API endpoint (server-side only)
  apiUrl: process.env.CHATKIT_API_URL || "https://api.chatkit.com",

  // Branding - school/ERP theme colors
  theme: {
    primaryColor: "#2563EB",      // School blue
    secondaryColor: "#1E40AF",    // Darker blue
    textColor: "#FFFFFF",
    backgroundColor: "#FFFFFF",
    borderRadius: "12px",
  },

  // Position - bottom right corner
  position: {
    bottom: "24px",
    right: "24px",
    mobileBottom: "16px",
    mobileRight: "16px",
  },

  // Behavior
  behavior: {
    defaultOpen: false,
    showOnPages: ["/**"],         // Show on all pages
    hideOnPages: ["/admin/**"],   // Hide on admin pages
    allowedRoles: [],             // Show to all roles
  },

  // Content
  content: {
    welcomeMessage: "Hi! How can I help you today?",
    placeholderText: "Type your question...",
    headerTitle: "ERP Support",
    headerSubtitle: "Ask us anything about grades, fees, or attendance",
  },
};
```

### Environment Variables

```bash
# .env.example

# Public variables (safe to expose in browser)
NEXT_PUBLIC_CHATKIT_PROJECT_ID="your-project-id"
NEXT_PUBLIC_CHATKIT_PUBLIC_KEY="your-public-key"

# Server-only variables (never expose to client)
CHATKIT_SECRET_KEY="your-secret-key"
CHATKIT_API_URL="https://api.chatkit.com/v2"
CHATKIT_BOT_ID="your-bot-id"

# Optional: Custom branding
NEXT_PUBLIC_CHATKIT_PRIMARY_COLOR="#2563EB"
```

## Widget Component

### Main Widget (Lazy Loaded)

```tsx
// frontend/components/chat/ChatWidget.tsx
"use client";

import { useEffect, useState, useCallback } from "react";
import { chatkitConfig } from "@/config/chatkit.config";

interface ChatWidgetProps {
  userRole?: string;
  userId?: string;
  userName?: string;
}

declare global {
  interface Window {
    ChatKit?: {
      init: (config: any) => void;
      destroy: () => void;
    };
  }
}

export function ChatWidget({
  userRole,
  userId,
  userName,
}: ChatWidgetProps) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isOpen, setIsOpen] = useState(chatkitConfig.behavior.defaultOpen);

  // Check if widget should be shown based on page and role
  const shouldShow = useCallback(() => {
    // Check role-based access
    if (
      chatkitConfig.behavior.allowedRoles.length > 0 &&
      !chatkitConfig.behavior.allowedRoles.includes(userRole || "")
    ) {
      return false;
    }

    return true;
  }, [userRole]);

  // Load ChatKit script dynamically
  useEffect(() => {
    if (!shouldShow()) return;

    const loadChatKit = () => {
      const script = document.createElement("script");
      script.src = `${chatkitConfig.apiUrl}/widget.js`;
      script.async = true;
      script.onload = () => {
        setIsLoaded(true);
        initializeWidget();
      };
      script.onerror = () => {
        console.error("Failed to load ChatKit widget");
      };
      document.body.appendChild(script);
    };

    loadChatKit();

    return () => {
      // Cleanup
      if (window.ChatKit) {
        window.ChatKit.destroy();
      }
      const existingScript = document.querySelector(
        'script[src*="widget.js"]'
      );
      if (existingScript) {
        existingScript.remove();
      }
    };
  }, [shouldShow]);

  const initializeWidget = () => {
    if (!window.ChatKit) return;

    window.ChatKit.init({
      projectId: chatkitConfig.projectId,
      publicKey: chatkitConfig.publicKey,
      container: "#chatkit-container",
      theme: {
        primaryColor: chatkitConfig.theme.primaryColor,
        secondaryColor: chatkitConfig.theme.secondaryColor,
        textColor: chatkitConfig.theme.textColor,
        backgroundColor: chatkitConfig.theme.backgroundColor,
        borderRadius: chatkitConfig.theme.borderRadius,
      },
      user: {
        id: userId || "anonymous",
        name: userName || "Guest",
      },
      onOpen: () => {
        console.log("Chat opened");
        // Optional: Log analytics event
      },
      onClose: () => {
        console.log("Chat closed");
      },
      onMessage: (message: any) => {
        console.log("Message sent:", message);
        // Optional: Track conversation metrics
      },
      onError: (error: any) => {
        console.error("Chat error:", error);
      },
    });
  };

  if (!shouldShow()) return null;

  return (
    <div
      id="chatkit-container"
      className="fixed z-50"
      style={{
        bottom: chatkitConfig.position.bottom,
        right: chatkitConfig.position.right,
      }}
    >
      {/* Chat toggle button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center justify-center w-14 h-14 rounded-full shadow-lg transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2"
        style={{
          backgroundColor: chatkitConfig.theme.primaryColor,
          color: chatkitConfig.theme.textColor,
        }}
        aria-label={isOpen ? "Close chat" : "Open chat"}
      >
        {isOpen ? (
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M6 18L18 6M6 6l12 12"
            />
          </svg>
        ) : (
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
            />
          </svg>
        )}
      </button>

      {/* Mobile considerations */}
      <style jsx global>{`
        @media (max-width: 768px) {

Related in Web Dev