diff --git a/.gitignore b/.gitignore
index b15c864..d2133c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,6 @@ dist/
.env.local
.DS_Store
coverage/
+.astro/
+.vercel/
+package-lock.json
diff --git a/packages/widget/src/dom/panel.ts b/packages/widget/src/dom/panel.ts
index fbb9d2e..749a2e6 100644
--- a/packages/widget/src/dom/panel.ts
+++ b/packages/widget/src/dom/panel.ts
@@ -6,6 +6,7 @@ const CLOSE_ICON = `
-
+
diff --git a/website/src/components/react/LiveDemo.tsx b/website/src/components/react/LiveDemo.tsx
index 05e72a1..5fb62bc 100644
--- a/website/src/components/react/LiveDemo.tsx
+++ b/website/src/components/react/LiveDemo.tsx
@@ -1,169 +1,52 @@
-import { useState, useEffect, useRef } from 'react';
-import { motion, AnimatePresence } from 'motion/react';
-
-interface Message {
- id: string;
- role: 'user' | 'assistant';
- content: string;
-}
-
-const conversation: Message[] = [
- { id: '1', role: 'assistant', content: "Hey! I'm your AI assistant powered by ChatCops. How can I help?" },
- { id: '2', role: 'user', content: 'What pricing plans do you offer?' },
- {
- id: '3',
- role: 'assistant',
- content:
- "We have three plans: **Starter** (free), **Pro** ($29/mo), and **Enterprise** (custom). All include unlimited conversations and every integration. Want me to help you choose?",
- },
-];
+import { useEffect, useRef } from 'react';
+import { motion } from 'motion/react';
+import { Widget } from '@chatcops/widget';
export default function LiveDemo() {
- const [messages, setMessages] = useState([]);
- const [typing, setTyping] = useState(false);
- const ref = useRef(null);
+ const containerRef = useRef(null);
+ const widgetRef = useRef(null);
useEffect(() => {
- let step = 0;
- const delays = [500, 1400, 900];
-
- function next() {
- if (step >= conversation.length) return;
- const msg = conversation[step];
-
- if (msg.role === 'assistant' && step > 0) {
- setTyping(true);
- setTimeout(() => {
- setTyping(false);
- streamMsg(msg);
- step++;
- setTimeout(next, delays[step] || 1000);
- }, 1200);
- } else {
- setMessages((p) => [...p, msg]);
- step++;
- setTimeout(next, delays[step] || 900);
- }
- }
-
- function streamMsg(msg: Message) {
- const words = msg.content.split(' ');
- let i = 0;
- setMessages((p) => [...p, { ...msg, content: '' }]);
-
- const iv = setInterval(() => {
- if (i >= words.length) {
- clearInterval(iv);
- setMessages((p) => p.map((m) => (m.id === msg.id ? { ...m, content: msg.content } : m)));
- return;
- }
- i++;
- const partial = words.slice(0, i).join(' ');
- setMessages((p) => p.map((m) => (m.id === msg.id ? { ...m, content: partial } : m)));
- }, 35);
- }
-
- const t = setTimeout(next, 700);
- return () => clearTimeout(t);
+ if (widgetRef.current || !containerRef.current) return;
+
+ const widget = new Widget({
+ apiUrl: '/api/chat',
+ mode: 'inline',
+ container: containerRef.current,
+ welcomeMessage: "Hey! I'm your AI assistant powered by ChatCops. Ask me anything!",
+ branding: {
+ name: 'ChatCops',
+ subtitle: 'Always online',
+ },
+ theme: {
+ accent: '#6366f1',
+ },
+ persistHistory: false,
+ });
+ widget.init();
+ widgetRef.current = widget;
+
+ return () => {
+ widget.destroy();
+ widgetRef.current = null;
+ };
}, []);
- useEffect(() => {
- ref.current?.scrollTo({ top: ref.current.scrollHeight, behavior: 'smooth' });
- }, [messages, typing]);
-
return (
-
- {/* Header */}
-
-
-
-
ChatCops
-
Always online
-
-
-
- {/* Messages */}
-
-
- {messages.map((msg) => (
-
-
- {formatText(msg.content)}
-
-
- ))}
-
-
- {typing && (
-
-
- {[0, 1, 2].map((i) => (
-
- ))}
-
-
- )}
-
-
- {/* Input */}
-
-
- Type a message...
-
-
-
-
+
+
+ This is the actual ChatCops widget running in inline mode.
+
);
}
-
-function formatText(text: string) {
- const parts = text.split(/(\*\*[^*]+\*\*)/g);
- return parts.map((part, i) => {
- if (part.startsWith('**') && part.endsWith('**')) {
- return (
-
- {part.slice(2, -2)}
-
- );
- }
- return part;
- });
-}
diff --git a/website/src/content/docs/widget/api.mdx b/website/src/content/docs/widget/api.mdx
index e1fd146..999d67e 100644
--- a/website/src/content/docs/widget/api.mdx
+++ b/website/src/content/docs/widget/api.mdx
@@ -12,18 +12,32 @@ The `ChatCops` singleton is exposed on `window.ChatCops` when using the IIFE bun
Initialize the widget with a configuration object. Must be called before any other method if not using script tag auto-init.
```typescript
+// Popup mode (default)
ChatCops.init({
apiUrl: 'https://your-api.com/chat',
theme: { accent: '#6366f1' },
branding: { name: 'My Bot' },
welcomeMessage: 'Hello! How can I help?',
});
+
+// Inline mode
+ChatCops.init({
+ apiUrl: 'https://your-api.com/chat',
+ mode: 'inline',
+ container: '#chat-container',
+ branding: { name: 'My Bot' },
+});
```
+Calling `init()` again will destroy the previous instance and create a new one.
+
### `ChatCops.open()`
Programmatically open the chat panel.
+- **Popup mode:** Shows the panel and updates the FAB icon.
+- **Inline mode:** Shows the panel (useful if you've previously called `close()`).
+
```typescript
document.querySelector('#help-btn').addEventListener('click', () => {
ChatCops.open();
@@ -34,6 +48,9 @@ document.querySelector('#help-btn').addEventListener('click', () => {
Close the chat panel.
+- **Popup mode:** Hides the panel and resets the FAB icon.
+- **Inline mode:** Hides the panel. Call `open()` to show it again.
+
```typescript
ChatCops.close();
```
@@ -42,21 +59,21 @@ ChatCops.close();
Completely remove the widget from the DOM and clean up all event listeners.
+- **Popup mode:** Removes FAB, panel, and welcome bubble from the page.
+- **Inline mode:** Removes the widget from the container element.
+
```typescript
ChatCops.destroy();
```
### `ChatCops.on(event, callback)`
-Subscribe to widget events. Returns an unsubscribe function.
+Subscribe to widget events.
```typescript
-const unsub = ChatCops.on('message', (data) => {
+ChatCops.on('message', (data) => {
console.log('New message:', data);
});
-
-// Later: unsubscribe
-unsub();
```
### `ChatCops.off(event, callback)`
@@ -76,6 +93,8 @@ ChatCops.off('open', onOpen);
When using the CDN script tag, the widget auto-initializes by reading `data-*` attributes:
+### Popup (default)
+
```html
```
+### Inline
+
+```html
+
+
+```
+
The script tag approach is equivalent to calling `ChatCops.init()` with the corresponding config. You can still use the programmatic API after auto-init (e.g., `ChatCops.open()`).
## ESM Usage
```typescript
-import { ChatCops } from '@chatcops/widget';
+import ChatCops from '@chatcops/widget';
+// Popup
ChatCops.init({ apiUrl: '/api/chat' });
+// Inline with element reference
+const el = document.getElementById('chat');
+ChatCops.init({
+ apiUrl: '/api/chat',
+ mode: 'inline',
+ container: el,
+});
+
// Use methods as needed
ChatCops.on('message', (msg) => console.log(msg));
```
+
+## React / Framework Usage
+
+For inline mode in React or other frameworks, pass the container element directly:
+
+```tsx
+import { useEffect, useRef } from 'react';
+import ChatCops from '@chatcops/widget';
+
+function Chat() {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (!ref.current) return;
+ ChatCops.init({
+ apiUrl: '/api/chat',
+ mode: 'inline',
+ container: ref.current,
+ });
+ return () => ChatCops.destroy();
+ }, []);
+
+ return ;
+}
+```
diff --git a/website/src/content/docs/widget/configuration.mdx b/website/src/content/docs/widget/configuration.mdx
index da5091f..fbdde75 100644
--- a/website/src/content/docs/widget/configuration.mdx
+++ b/website/src/content/docs/widget/configuration.mdx
@@ -5,8 +5,17 @@ description: All configuration options for the ChatCops widget.
The widget can be configured via `data-*` attributes on the script tag or programmatically via `ChatCops.init()`.
+## Display Modes
+
+ChatCops supports two display modes:
+
+- **Popup** (default) — Floating action button (FAB) in the corner that opens a chat panel overlay.
+- **Inline** — The chat panel renders directly inside a container element on your page. No FAB, no overlay.
+
## Script Tag Attributes
+### Popup Mode (default)
+
```html
```
+### Inline Mode
+
+```html
+
+
+```
+
+In inline mode, the chat panel fills the container element. You control the size via the container's CSS.
+
## Programmatic Configuration
+### Popup Mode
+
```typescript
-import { ChatCops } from '@chatcops/widget';
+import ChatCops from '@chatcops/widget';
ChatCops.init({
apiUrl: 'https://your-api.com/chat',
@@ -42,7 +70,7 @@ ChatCops.init({
textColor: '#FAFAF9',
bgColor: '#0A0A0A',
fontFamily: 'Inter, sans-serif',
- borderRadius: 12,
+ borderRadius: '12',
position: 'bottom-right', // 'bottom-right' | 'bottom-left'
},
@@ -55,25 +83,53 @@ ChatCops.init({
// Behavior
welcomeMessage: 'Hi! How can I help you today?',
- welcomeBubble: 'Need help? Chat with us!',
- welcomeBubbleDelay: 3000, // ms before showing bubble
+ welcomeBubble: {
+ text: 'Need help? Chat with us!',
+ delay: 3000,
+ },
placeholder: 'Type a message...',
+ autoOpen: 1000, // Open after 1s, or true for immediate
locale: 'en',
});
```
+### Inline Mode
+
+```typescript
+import ChatCops from '@chatcops/widget';
+
+ChatCops.init({
+ apiUrl: 'https://your-api.com/chat',
+ mode: 'inline',
+ container: '#chat-container', // CSS selector or HTMLElement
+
+ branding: {
+ name: 'Support Bot',
+ subtitle: 'Online',
+ },
+ welcomeMessage: 'Hi! How can I help you today?',
+ persistHistory: false,
+});
+```
+
+The `container` option accepts a CSS selector string or a direct `HTMLElement` reference.
+
## WidgetConfig Interface
```typescript
interface WidgetConfig {
apiUrl: string;
+ // Display mode
+ mode?: 'popup' | 'inline'; // Default: 'popup'
+ container?: string | HTMLElement; // Required when mode is 'inline'
+
theme?: {
accent?: string; // Hex color, default: '#6366f1'
textColor?: string; // Hex color, default: '#FAFAF9'
bgColor?: string; // Hex color, default: '#0A0A0A'
fontFamily?: string; // CSS font-family
- borderRadius?: number; // px, default: 12
+ borderRadius?: string; // px, default: '12'
position?: 'bottom-right' | 'bottom-left';
};
@@ -84,13 +140,50 @@ interface WidgetConfig {
};
welcomeMessage?: string; // First assistant message
- welcomeBubble?: string; // Text shown on hover bubble
- welcomeBubbleDelay?: number;// ms before bubble appears
+ welcomeBubble?: { // Popup mode only
+ text: string; // Text shown on hover bubble
+ delay?: number; // ms before bubble appears
+ showOnce?: boolean; // Only show once per session
+ };
placeholder?: string; // Input placeholder text
+ persistHistory?: boolean; // Save chat history, default: true
+ maxMessages?: number; // Max messages to persist, default: 50
+ pageContext?: boolean; // Send page URL/title, default: true
+ autoOpen?: boolean | number; // Popup mode only. true = immediate, number = delay in ms
locale?: string; // 'en' | 'es' | 'hi' | 'fr' | 'de' | 'ja' | 'zh' | 'ar'
+ strings?: Partial; // Custom i18n overrides
+
+ // Callbacks
+ onOpen?: () => void;
+ onClose?: () => void;
+ onMessage?: (message: MessageData) => void;
+ onError?: (error: Error) => void;
}
```
+## Script Tag Attribute Reference
+
+| Attribute | Config Key | Description |
+|-----------|-----------|-------------|
+| `data-api-url` | `apiUrl` | API endpoint URL (required) |
+| `data-mode` | `mode` | `'popup'` or `'inline'` |
+| `data-container` | `container` | CSS selector for inline container |
+| `data-accent` | `theme.accent` | Primary accent color |
+| `data-text-color` | `theme.textColor` | Primary text color |
+| `data-bg-color` | `theme.bgColor` | Background color |
+| `data-font-family` | `theme.fontFamily` | Font family |
+| `data-border-radius` | `theme.borderRadius` | Border radius in px |
+| `data-position` | `theme.position` | `'bottom-right'` or `'bottom-left'` |
+| `data-brand-name` | `branding.name` | Header title |
+| `data-brand-avatar` | `branding.avatar` | Avatar image URL |
+| `data-brand-subtitle` | `branding.subtitle` | Subtitle text |
+| `data-welcome-message` | `welcomeMessage` | First assistant message |
+| `data-welcome-bubble` | `welcomeBubble.text` | Welcome bubble text |
+| `data-welcome-bubble-delay` | `welcomeBubble.delay` | Bubble delay in ms |
+| `data-placeholder` | `placeholder` | Input placeholder |
+| `data-auto-open` | `autoOpen` | Auto-open delay in ms or `true` |
+| `data-locale` | `locale` | Language code |
+
## CSS Custom Properties
The widget uses Shadow DOM and exposes these CSS custom properties on its host element:
@@ -110,3 +203,16 @@ The widget uses Shadow DOM and exposes these CSS custom properties on its host e
| `--cc-fab-size` | FAB button size | `56px` |
| `--cc-panel-width` | Chat panel width | `400px` |
| `--cc-panel-height` | Chat panel height | `560px` |
+
+## Inline Mode Behavior
+
+When using `mode: 'inline'`:
+
+- The chat panel renders inside the specified container element
+- No floating action button (FAB) is created
+- No welcome bubble is shown
+- The panel is visible immediately (no toggle needed)
+- `autoOpen` is ignored (always open)
+- The panel fills 100% width and height of the container
+- Mobile responsive overrides do not apply — the container controls sizing
+- `open()` and `close()` still work programmatically
diff --git a/website/src/layouts/Landing.astro b/website/src/layouts/Landing.astro
index 506abf9..e6be90e 100644
--- a/website/src/layouts/Landing.astro
+++ b/website/src/layouts/Landing.astro
@@ -36,6 +36,17 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
+
+
+
diff --git a/website/src/pages/api/chat.ts b/website/src/pages/api/chat.ts
new file mode 100644
index 0000000..dc8e135
--- /dev/null
+++ b/website/src/pages/api/chat.ts
@@ -0,0 +1,124 @@
+import type { APIRoute } from 'astro';
+import { createChatHandler } from '@chatcops/server';
+import { FAQKnowledgeSource } from '@chatcops/core';
+
+export const prerender = false;
+
+const faq = new FAQKnowledgeSource([
+ {
+ question: 'What is ChatCops?',
+ answer:
+ 'ChatCops is an open-source, universal AI chatbot widget you can embed on any website with a single script tag. It supports Claude, OpenAI, and Gemini as AI providers.',
+ },
+ {
+ question: 'How do I install ChatCops?',
+ answer:
+ 'The easiest way is via CDN: add a