Back to The Times of Claw

Build an AI Chat Widget App for Your CRM

Build an AI chat widget app in DenchClaw that gives you a conversational interface to your CRM data, deal summaries, and contact lookups right in the sidebar.

Mark Rachapoom
Mark Rachapoom
·7 min read
Build an AI Chat Widget App for Your CRM

Build an AI Chat Widget App for Your CRM

Sometimes you just want to ask a question without navigating through tables. "Who are my top 5 deals this month?" "What did I last discuss with Sarah Chen?" "How many leads came in this week?" A chat widget embedded in your DenchClaw sidebar gives you that — a natural language interface to your CRM that's always one click away.

This guide builds a persistent chat widget app using the dench.chat API, with CRM context pre-loaded so the AI knows what you're asking about.

What You're Building#

  • A chat interface that lives in the sidebar as a persistent tab
  • AI responses that can query your DuckDB data
  • Quick-action buttons for common queries
  • Conversation history that persists across sessions
  • Ability to pin insights from chat to CRM entries

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/crm-chat.dench.app

.dench.yaml:

name: CRM Chat
description: AI chat interface with direct CRM database access
icon: message-circle
version: 1.0.0
permissions:
  - read:crm
  - chat:create
display: tab

Step 2: HTML Layout#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CRM Chat</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; display: flex; flex-direction: column; height: 100vh; }
    .header { padding: 14px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
    .header h2 { font-size: 15px; margin: 0; }
    .quick-actions { padding: 10px 12px; display: flex; gap: 6px; flex-wrap: wrap; background: #0a0f1e; border-bottom: 1px solid #1e293b; }
    .qa-btn { padding: 4px 10px; border: 1px solid #334155; background: transparent; color: #94a3b8; border-radius: 999px; font-size: 11px; cursor: pointer; white-space: nowrap; }
    .qa-btn:hover { border-color: #6366f1; color: #a5b4fc; }
    .messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
    .message { display: flex; gap: 10px; align-items: flex-start; }
    .message.user { flex-direction: row-reverse; }
    .avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; }
    .avatar.ai { background: #6366f1; }
    .avatar.user { background: #334155; }
    .bubble { max-width: 80%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.5; }
    .bubble.ai { background: #1e293b; border-radius: 2px 12px 12px 12px; }
    .bubble.user { background: #6366f1; border-radius: 12px 2px 12px 12px; }
    .bubble pre { background: #0f172a; padding: 8px; border-radius: 6px; overflow-x: auto; font-size: 11px; margin-top: 8px; }
    .bubble table { border-collapse: collapse; font-size: 12px; width: 100%; margin-top: 8px; }
    .bubble th { background: #0f172a; padding: 4px 8px; text-align: left; }
    .bubble td { padding: 4px 8px; border-top: 1px solid #334155; }
    .input-area { padding: 12px; background: #1e293b; border-top: 1px solid #334155; display: flex; gap: 8px; }
    .chat-input { flex: 1; background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 10px 14px; border-radius: 10px; font-size: 13px; resize: none; outline: none; }
    .chat-input:focus { border-color: #6366f1; }
    .send-btn { padding: 10px 16px; background: #6366f1; color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 13px; }
    .send-btn:disabled { opacity: 0.5; }
    .typing { color: #64748b; font-size: 12px; font-style: italic; padding: 4px 0; }
  </style>
</head>
<body>
  <div class="header">
    <h2>🤖 CRM Chat</h2>
    <button onclick="clearHistory()" style="background:transparent;border:none;color:#64748b;cursor:pointer;font-size:12px">Clear</button>
  </div>
  <div class="quick-actions">
    <button class="qa-btn" onclick="quickQuery('Show me my top 5 open deals by value')">Top deals</button>
    <button class="qa-btn" onclick="quickQuery('How many new leads this week?')">New leads</button>
    <button class="qa-btn" onclick="quickQuery('Who have I not contacted in 30+ days?')">Stale contacts</button>
    <button class="qa-btn" onclick="quickQuery('What is my total pipeline value?')">Pipeline total</button>
    <button class="qa-btn" onclick="quickQuery('Show me deals closing this month')">Closing soon</button>
  </div>
  <div class="messages" id="messages"></div>
  <div class="input-area">
    <textarea class="chat-input" id="chatInput" rows="1" placeholder="Ask about your CRM data..."
              onkeydown="handleKeydown(event)" oninput="autoResize(this)"></textarea>
    <button class="send-btn" id="sendBtn" onclick="sendMessage()">Send</button>
  </div>
  <script src="chat.js"></script>
</body>
</html>

Step 3: Chat Logic#

chat.js:

let sessionId = null;
let messages = [];
const SYSTEM_CONTEXT = `You are a CRM assistant with access to DenchClaw data. When asked about contacts, deals, companies, or pipeline data, query the DuckDB database to get accurate information. Be concise and specific. Format tables as markdown when showing multiple records.
 
Available DuckDB views: v_people, v_companies, v_deals, v_tasks.
Common fields: Full Name, Email Address, Company, Status, Stage, Value, Close Date, Last Contacted, Owner.`;
 
async function init() {
  // Load or create session
  const savedSession = await dench.store.get('chat_session_id');
  const savedMessages = await dench.store.get('chat_messages');
  
  if (savedSession) {
    sessionId = savedSession;
    messages = savedMessages || [];
    messages.forEach(msg => renderMessage(msg.role, msg.content));
  } else {
    sessionId = (await dench.chat.createSession({ systemPrompt: SYSTEM_CONTEXT })).id;
    await dench.store.set('chat_session_id', sessionId);
    renderMessage('ai', 'Hi! I can answer questions about your CRM data. Try asking about your pipeline, leads, or contacts.');
  }
}
 
function renderMessage(role, content) {
  const container = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = `message ${role}`;
  
  // Format content: convert markdown tables and code blocks
  let formattedContent = content
    .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
    .replace(/\n/g, '<br>');
  
  div.innerHTML = `
    <div class="avatar ${role}">${role === 'ai' ? '🤖' : '👤'}</div>
    <div class="bubble ${role}">${formattedContent}</div>
  `;
  container.appendChild(div);
  container.scrollTop = container.scrollHeight;
}
 
function showTyping() {
  const container = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = 'typing';
  div.id = 'typing-indicator';
  div.textContent = 'DenchClaw is thinking...';
  container.appendChild(div);
  container.scrollTop = container.scrollHeight;
}
 
function hideTyping() {
  document.getElementById('typing-indicator')?.remove();
}
 
async function sendMessage(overrideText) {
  const input = document.getElementById('chatInput');
  const text = overrideText || input.value.trim();
  if (!text) return;
 
  input.value = '';
  autoResize(input);
  document.getElementById('sendBtn').disabled = true;
 
  renderMessage('user', text);
  messages.push({ role: 'user', content: text });
  showTyping();
 
  try {
    // Build context-aware query
    const contextPrompt = await buildContextPrompt(text);
    const response = await dench.chat.send(sessionId, contextPrompt);
    
    hideTyping();
    renderMessage('ai', response);
    messages.push({ role: 'ai', content: response });
    
    // Persist conversation (last 20 messages)
    await dench.store.set('chat_messages', messages.slice(-20));
  } catch (err) {
    hideTyping();
    renderMessage('ai', `Error: ${err.message}. Try rephrasing your question.`);
  } finally {
    document.getElementById('sendBtn').disabled = false;
  }
}
 
async function buildContextPrompt(userMessage) {
  // If the question looks like a data query, pre-fetch relevant data
  const isDataQuery = /\b(show|list|find|how many|top|who|what|when|count|total|pipeline|deal|lead|contact)\b/i.test(userMessage);
  
  if (isDataQuery) {
    try {
      // Get a quick context snapshot
      const [dealSummary, recentLeads] = await Promise.all([
        dench.db.query('SELECT COUNT(*) as count, SUM(CAST("Value" AS DOUBLE)) as total FROM v_deals WHERE "Stage" NOT IN (\'Closed Won\', \'Closed Lost\')'),
        dench.db.query('SELECT COUNT(*) as count FROM v_people WHERE "Status" = \'Lead\'')
      ]);
      
      return `${userMessage}\n\nContext snapshot: ${dealSummary[0]?.count || 0} open deals worth $${(dealSummary[0]?.total || 0).toLocaleString()} total. ${recentLeads[0]?.count || 0} active leads. Query the DuckDB views for specific details.`;
    } catch (e) {
      return userMessage;
    }
  }
  return userMessage;
}
 
function quickQuery(query) {
  sendMessage(query);
}
 
function handleKeydown(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    sendMessage();
  }
}
 
function autoResize(el) {
  el.style.height = 'auto';
  el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
 
async function clearHistory() {
  messages = [];
  sessionId = (await dench.chat.createSession({ systemPrompt: SYSTEM_CONTEXT })).id;
  await dench.store.set('chat_session_id', sessionId);
  await dench.store.set('chat_messages', []);
  document.getElementById('messages').innerHTML = '';
  renderMessage('ai', 'Chat cleared. How can I help?');
}
 
init();

Frequently Asked Questions#

How do I make the AI actually query DuckDB directly?#

The current implementation gives the AI context snapshots and lets it reason from them. For true direct querying, configure the dench.agent.run() API with a query tool, or pass the full SQL result as part of the prompt context.

Can I add this as a floating widget in the corner?#

Yes. Create a separate widget app with display: widget and minimal dimensions (2×2 grid units). Show just an input box and recent responses.

How do I restrict what data the AI can access?#

Use the permissions field in .dench.yaml. The read:crm permission grants access to all CRM views. For more granular control, use the DenchClaw permission scopes when they become available.

Can I export a conversation as notes on a CRM entry?#

Add an "Export to Notes" button that takes the conversation history and calls dench.agent.run("Save these notes to my CRM...") with the conversation content.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA