Skip to main content

Basic Floating Widget

The simplest integration — a floating chat bubble in the bottom-right corner:
<script src="https://cdn.jsdelivr.net/npm/@vizochok/[email protected]/dist/vizochok-widget.umd.js"></script>
<script>
  const widget = new VIZOCHOKWidget({
    apiKey: 'pk_your_public_key',
    storeId: 'your-store-id',
  });
  widget.mount();
</script>

Inline Embedded Widget

Embed the chat directly inside a page element instead of using the floating bubble. The widget fills its container and hides the toggle button.
<div id="chat-container" style="width: 400px; height: 600px;"></div>

<script src="https://cdn.jsdelivr.net/npm/@vizochok/[email protected]/dist/vizochok-widget.umd.js"></script>
<script>
  const widget = new VIZOCHOKWidget({
    apiKey: 'pk_your_public_key',
    storeId: 'your-store-id',
    botName: 'Shopping Assistant',
  });

  const container = document.getElementById('chat-container');
  widget.mount(container, { inline: true });
</script>
In inline mode:
  • The toggle button is hidden
  • The chat panel fills 100% of the container
  • The close button in the header is hidden
  • The chat is automatically opened on mount

React Component Wrapper

Wrap the widget in a React component with proper lifecycle management:
import { useEffect, useRef } from 'react';
import { VIZOCHOKWidget } from '@vizochok/widget';
import type { CartChangedEvent } from '@vizochok/widget';

interface Props {
  apiKey: string;
  storeId: string;
  userId?: string;
  onCartUpdate?: (cart: CartChangedEvent) => void;
  inline?: boolean;
}

export function VIZOCHOKChat({
  apiKey,
  storeId,
  userId,
  onCartUpdate,
  inline,
}: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const widgetRef = useRef<VIZOCHOKWidget | null>(null);

  useEffect(() => {
    const widget = new VIZOCHOKWidget({
      apiKey,
      storeId,
      userId,
      language: 'en',
      theme: {
        primaryColor: '#1a73e8',
        borderRadius: '12px',
        mode: 'auto',
      },
      onCartChanged: (cart) => onCartUpdate?.(cart),
      onError: (error) => {
        console.error('VIZOCHOK error:', error.code, error.message);
      },
    });

    widgetRef.current = widget;

    if (inline && containerRef.current) {
      widget.mount(containerRef.current, { inline: true });
    } else {
      widget.mount();
    }

    return () => {
      widget.unmount();
    };
  }, [apiKey, storeId, userId]);

  if (inline) {
    return (
      <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
    );
  }

  return null; // Floating mode -- mounts on document.body
}
Usage:
function StorePage() {
  const [cart, setCart] = useState([]);

  return (
    <div>
      <ProductList />
      <VIZOCHOKChat
        apiKey="pk_your_key"
        storeId="your-store"
        userId={currentUser?.id}
        onCartUpdate={(event) => setCart(event.items)}
      />
    </div>
  );
}
Inline in a sidebar:
function ProductPage() {
  return (
    <div style={{ display: 'flex' }}>
      <main style={{ flex: 1 }}>
        {/* Product content */}
      </main>
      <aside style={{ width: 400, height: '100vh' }}>
        <VIZOCHOKChat
          apiKey="pk_your_key"
          storeId="your-store"
          inline
        />
      </aside>
    </div>
  );
}

Vue Component Wrapper

Wrap the widget in a Vue 3 Composition API component:
<template>
  <div v-if="inline" ref="containerRef" class="vizochok-container" />
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { VIZOCHOKWidget } from '@vizochok/widget';
import type { CartChangedEvent } from '@vizochok/widget';

const props = defineProps<{
  apiKey: string;
  storeId: string;
  userId?: string;
  inline?: boolean;
}>();

const emit = defineEmits<{
  cartChanged: [cart: CartChangedEvent];
}>();

const containerRef = ref<HTMLDivElement>();
let widget: VIZOCHOKWidget | null = null;

onMounted(() => {
  widget = new VIZOCHOKWidget({
    apiKey: props.apiKey,
    storeId: props.storeId,
    userId: props.userId,
    language: 'en',
    theme: {
      primaryColor: '#2D8A4E',
      borderRadius: '12px',
      mode: 'auto',
    },
    onCartChanged: (cart) => emit('cartChanged', cart),
  });

  if (props.inline && containerRef.value) {
    widget.mount(containerRef.value, { inline: true });
  } else {
    widget.mount();
  }
});

onBeforeUnmount(() => {
  widget?.unmount();
  widget = null;
});
</script>

<style scoped>
.vizochok-container {
  width: 100%;
  height: 600px;
}
</style>
Usage:
<template>
  <VIZOCHOKChat
    api-key="pk_your_key"
    store-id="your-store"
    :user-id="currentUser?.id"
    @cart-changed="handleCartChanged"
  />
</template>

Cart Synchronization with Store

A complete example showing cart sync between the AI assistant and your store:
import { VIZOCHOKWidget } from '@vizochok/widget';

const widget = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
  userId: getCurrentUserId(),

  // AI changes cart -> update store
  onCartChanged: (cart) => {
    // Update your cart state
    storeCart.replaceItems(cart.items.map(item => ({
      sku: item.sku,
      name: item.name,
      price: item.promo_price ?? item.price,
      quantity: item.quantity,
      brand: item.brand,
    })));

    storeCart.setTotal(cart.total);
    renderCartUI();

    // Show notification
    switch (cart.action) {
      case 'item_selected':
        showToast(`${cart.name} added to cart`);
        break;
      case 'item_removed':
        showToast(`${cart.name} removed from cart`);
        break;
      case 'quantity_updated':
        showToast(`${cart.name} updated to ${cart.quantity}`);
        break;
      case 'cart_cleared':
        showToast('Cart cleared');
        break;
    }
  },

  // Restore cart on reconnect
  onSessionComplete: (summary) => {
    if (summary.item_count > 0) {
      storeCart.replaceItems(summary.selected_items);
      storeCart.setTotal(summary.total);
      renderCartUI();
    }
  },
});

widget.mount();
The initial cart is loaded from your backend via the cart_get_url webhook when a new conversation starts. See the Cart Webhook documentation for server-side setup.

Custom Styling

Apply different visual themes by combining config options:

Dark E-commerce Theme

const widget = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
  botName: 'Shopping AI',
  botAvatar: 'https://cdn.example.com/bot.png',
  welcomeMessage: 'Hi! I can help you find products and build your cart.',
  placeholder: 'What are you looking for?',
  position: 'bottom-right',
  theme: {
    primaryColor: '#6366f1',
    fontFamily: "'Inter', system-ui, sans-serif",
    borderRadius: '16px',
    mode: 'dark',
    currency: '$',
  },
});

Minimal Light Theme

const widget = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
  botName: 'Assistant',
  position: 'bottom-left',
  theme: {
    primaryColor: '#000000',
    fontFamily: "'Georgia', serif",
    borderRadius: '0px',
    mode: 'light',
    currency: 'EUR',
  },
});

Multi-Language Setup

The language option controls the language of widget UI text (errors, status messages):
// Ukrainian (default)
const widgetUk = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
  language: 'uk',
  botName: 'Помічник',
  placeholder: 'Що шукаєте?',
  welcomeMessage: 'Привіт! Я допоможу зі списком покупок.',
});

// English
const widgetEn = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
  language: 'en',
  botName: 'Assistant',
  placeholder: 'What are you looking for?',
  welcomeMessage: 'Hi! I can help you build your shopping list.',
});
The language setting controls widget UI text only (errors, status messages). The AI assistant’s response language is determined by the user’s input — Ukrainian/Russian input gets Ukrainian responses, other languages get responses in that language.
The widget includes built-in translations for:
  • 12 error codes
  • 15 status codes (searching, thinking, adding to cart, etc.)
Russian ('ru') uses the Ukrainian translations.

Switching Between Inline and Floating

Use setMode() to switch the widget between inline and floating modes without reconnecting:
const widget = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
});
widget.mount();

// Switch to inline when user navigates to a chat page
const chatContainer = document.getElementById('chat-page');
widget.setMode('inline', chatContainer);

// Switch back to floating when user leaves
widget.setMode('floating');

Programmatic Control

Control the widget from external buttons or triggers:
const widget = new VIZOCHOKWidget({
  apiKey: 'pk_your_key',
  storeId: 'your-store',
});
widget.mount();

// Open chat from a custom button
document.getElementById('help-btn').addEventListener('click', () => {
  widget.open();
});

// Start a new conversation
document.getElementById('new-chat-btn').addEventListener('click', () => {
  widget.newChat();
});

// Clean up on SPA page transition (preserves session)
function onPageLeave() {
  widget.unmount();
}

// Full cleanup when user logs out (clears session)
function onLogout() {
  widget.destroy();
}

Full Production Example

A complete production-ready integration:
import { VIZOCHOKWidget } from '@vizochok/widget';

let widget: VIZOCHOKWidget | null = null;

export function initAssistant(config: {
  userId?: string;
  language: 'uk' | 'en' | 'ru';
}) {
  widget?.destroy();

  widget = new VIZOCHOKWidget({
    apiKey: import.meta.env.VITE_VIZOCHOK_KEY,
    storeId: import.meta.env.VITE_STORE_ID,
    userId: config.userId,
    language: config.language,

    theme: {
      primaryColor: getComputedStyle(document.documentElement)
        .getPropertyValue('--brand-primary')
        .trim(),
      fontFamily: getComputedStyle(document.body).fontFamily,
      borderRadius: '12px',
      mode: 'auto',
      currency: '$',
    },

    botName: config.language === 'uk' ? 'Помічник' : 'Assistant',

    onCartChanged: (cart) => {
      window.dispatchEvent(
        new CustomEvent('cart:update', { detail: cart })
      );
    },

    onProductClick: (product) => {
      window.location.href = `/product/${product.sku}`;
    },

    onError: (error) => {
      if (!['ws_error', 'connection_lost'].includes(error.code)) {
        errorReporter.report(error.code, error.message);
      }
    },
  });

  widget.mount();
}

export function destroyAssistant() {
  widget?.destroy();
  widget = null;
}