Demo

Ask questions about BundleLLM using BundleLLM. Connect your AI provider to try it.

Custom UI

For full control over the chat experience, use the raw SDK instead of the drop-in widget.

Setup

<script src="https://cdn.bundlellm.com/sdk.js"></script>

<button id="sign-in"></button>
<button id="sign-out" style="display:none">Sign out</button>
<div id="status"></div>
<div id="messages"></div>
<input id="input" placeholder="Ask something..." />
<button id="send">Send</button>

Initialize

const ai = BundleLLM.init({ siteId: 'my-app' })

// Render sign-in button
ai.renderSignIn('#sign-in')

Handle Auth State

const signOutBtn = document.getElementById('sign-out')
const statusEl = document.getElementById('status')

ai.on('connected', ({ provider, model }) => {
  statusEl.textContent = `Connected to ${provider} (${model})`
  signOutBtn.style.display = 'inline'
})

ai.on('disconnected', () => {
  statusEl.textContent = 'Not connected'
  signOutBtn.style.display = 'none'
})

signOutBtn.addEventListener('click', () => ai.disconnect())

Send Messages

const history = []

function sendMessage(text) {
  history.push({ role: 'user', content: text })

  const stream = ai.chat({
    messages: [...history],
    context: 'Your page context here...',
  })

  let fullText = ''

  stream
    .on('delta', (text) => {
      fullText += text
      // Update your UI with the accumulated text
    })
    .on('done', ({ usage }) => {
      history.push({ role: 'assistant', content: fullText })
      // Show token usage if desired
    })
    .on('error', ({ message }) => {
      // Show error in your UI
    })
}

Rendering Markdown

LLM responses contain markdown. The SDK exports a lightweight renderer you can use:

let fullText = ''

stream.on('delta', (text) => {
  fullText += text
  messagesEl.innerHTML = BundleLLM.renderMarkdown(fullText)
})

This handles code blocks, inline code, bold, italic, headers, lists, and links. HTML is escaped and links are restricted to http:/https: protocols. You can also use any third-party markdown library instead.

Dynamic Context

Use setContext() to change the system prompt without losing conversation history. This is useful when the page content changes but the chat should continue.

// Set a global context that applies to all messages
ai.setContext('The user is now viewing the FAQ page.')

// Later, if the user navigates:
ai.setContext('The user is now viewing the pricing page.')

// Clear to stop injecting context:
ai.setContext(undefined)

When setContext() is active, it acts as a fallback. If you pass an explicit context in a chat() call, that takes precedence.


Cancel a Stream

const stream = ai.chat({ messages: [...] })

// Cancel after 5 seconds
setTimeout(() => stream.cancel(), 5000)

Check Status Programmatically

const status = await ai.getStatus()

if (status.connected) {
  console.log(`Using ${status.provider} / ${status.model}`)
}

Framework Examples

React

function Chat() {
  const [ai] = useState(() => BundleLLM.init())
  const [connected, setConnected] = useState(false)
  const signInRef = useRef(null)

  useEffect(() => {
    ai.renderSignIn('#sign-in-btn')
    ai.on('connected', () => setConnected(true))
    ai.on('disconnected', () => setConnected(false))
    return () => ai.destroy()
  }, [])

  const send = (text) => {
    const stream = ai.chat({ messages: [{ role: 'user', content: text }] })
    stream.on('delta', (chunk) => { /* update state */ })
  }

  return (
    <div>
      {!connected && <div id="sign-in-btn" />}
      {connected && <YourChatUI onSend={send} />}
    </div>
  )
}

Vue

<template>
  <div v-if="!connected" id="sign-in-btn" />
  <YourChatUI v-else @send="send" />
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const ai = BundleLLM.init()
const connected = ref(false)

onMounted(() => {
  ai.renderSignIn('#sign-in-btn')
  ai.on('connected', () => connected.value = true)
  ai.on('disconnected', () => connected.value = false)
})

onUnmounted(() => ai.destroy())

function send(text) {
  const stream = ai.chat({ messages: [{ role: 'user', content: text }] })
  stream.on('delta', (chunk) => { /* update state */ })
}
</script>

Required User Protections

When building a custom UI, you must provide the following to comply with the BundleLLM Terms of Service:

1. Disconnect Button

Users must be able to disconnect their Provider at any time.

ai.disconnect()

2. Token Usage Display

Show token usage after each response so users can see their cost.

stream.on('done', ({ usage }) => {
  if (usage) {
    showTokens(`${usage.inputTokens} in / ${usage.outputTokens} out`)
  }
})

3. Provider Identity

Show which Provider the user is connected to.

ai.on('connected', ({ provider, model }) => {
  showStatus(`Connected to ${provider}`)
})

4. No Key Interception

Do not access, log, or transmit the user’s API key. The connected event only includes { provider, model }. The API key is never exposed to your code. It is stored in the user’s browser localStorage and used internally by the SDK.

Cleanup

Always call destroy() when your component unmounts:

ai.destroy()

This removes rendered DOM elements, clears the connection, and removes all event listeners. All subsequent method calls on the instance will no-op.