widget/Framework Integration

Framework Integration

The Savanto widgets work with any JavaScript framework. This guide covers integration patterns for popular frameworks.

React

Basic Integration

Load the widget in a component:

import { useEffect } from 'react';

export function SavantoChat() {
  useEffect(() => {
    // Load the widget script
    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.dataset.publishableKey = 'if_pk_xxx';
    script.async = true;
    document.body.appendChild(script);

    return () => {
      // Cleanup on unmount
      window.SavantoChatbot?.destroy();
      document.body.removeChild(script);
    };
  }, []);

  return null;
}

Custom React Hook

Create a reusable hook:

// hooks/useSavantoChat.ts
import { useEffect, useCallback } from 'react';

interface UseSavantoChatOptions {
  publishableKey: string;
  title?: string;
  greeting?: string;
  theme?: Record<string, any>;
}

export function useSavantoChat(options: UseSavantoChatOptions) {
  useEffect(() => {
    // Set config before loading script
    window.savantoConfig = {
      publishableKey: options.publishableKey,
      title: options.title,
      greeting: options.greeting,
      theme: options.theme,
    };

    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.async = true;
    document.body.appendChild(script);

    return () => {
      window.SavantoChatbot?.destroy();
      document.body.removeChild(script);
    };
  }, [options.publishableKey]);

  const open = useCallback(() => {
    window.SavantoChatbot?.open();
  }, []);

  const close = useCallback(() => {
    window.SavantoChatbot?.close();
  }, []);

  const toggle = useCallback(() => {
    window.SavantoChatbot?.toggle();
  }, []);

  return { open, close, toggle };
}

Usage:

function App() {
  const { open } = useSavantoChat({
    publishableKey: 'if_pk_xxx',
    title: 'Help Center',
  });

  return (
    <button onClick={open}>
      Need help?
    </button>
  );
}

TypeScript Declarations

Add type declarations for the global API:

// types/savanto.d.ts
interface SavantoChatbotAPI {
  open(): void;
  close(): void;
  toggle(): void;
  isOpen(): boolean;
  getConfig(): Record<string, any>;
  configure(config: Record<string, any>): void;
  setTheme(theme: Record<string, any>): void;
  setCustomCSS(css: string): void;
  destroy(): void;
  grantAnalyticsConsent(): void;
  revokeAnalyticsConsent(): void;
  getAnalyticsConsentStatus(): 'granted' | 'denied' | 'pending';
}

interface SavantoSearchAPI {
  open(): void;
  close(): void;
  toggle(): void;
  isOpen(): boolean;
  search(query: string): void;
  getConfig(): Record<string, any>;
  configure(config: Record<string, any>): void;
  setTheme(theme: Record<string, any>): void;
  setCustomCSS(css: string): void;
  destroy(): void;
  isInitialized(): boolean;
  version: string;
}

declare global {
  interface Window {
    savantoConfig?: Record<string, any>;
    savantoSearchConfig?: Record<string, any>;
    SavantoChatbot?: SavantoChatbotAPI;
    SavantoSearch?: SavantoSearchAPI;
  }
}

export {};

Next.js

App Router (Next.js 13+)

Create a client component:

// components/SavantoWidgets.tsx
'use client';

import { useEffect } from 'react';

interface Props {
  publishableKey: string;
  chatEnabled?: boolean;
  searchEnabled?: boolean;
}

export function SavantoWidgets({ 
  publishableKey, 
  chatEnabled = true,
  searchEnabled = true 
}: Props) {
  useEffect(() => {
    const scripts: HTMLScriptElement[] = [];

    if (chatEnabled) {
      window.savantoConfig = { publishableKey };
      const chatScript = document.createElement('script');
      chatScript.src = 'https://cdn.savanto.ai/chat-loader.js';
      chatScript.async = true;
      document.body.appendChild(chatScript);
      scripts.push(chatScript);
    }

    if (searchEnabled) {
      window.savantoSearchConfig = { publishableKey };
      const searchScript = document.createElement('script');
      searchScript.src = 'https://cdn.savanto.ai/search-loader.js';
      searchScript.async = true;
      document.body.appendChild(searchScript);
      scripts.push(searchScript);
    }

    return () => {
      window.SavantoChatbot?.destroy();
      window.SavantoSearch?.destroy();
      scripts.forEach(s => s.remove());
    };
  }, [publishableKey, chatEnabled, searchEnabled]);

  return null;
}

Add to your layout:

// app/layout.tsx
import { SavantoWidgets } from '@/components/SavantoWidgets';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <SavantoWidgets publishableKey="if_pk_xxx" />
      </body>
    </html>
  );
}

Pages Router

Use next/script:

// pages/_app.tsx
import Script from 'next/script';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Script id="savanto-config" strategy="beforeInteractive">
        {`window.savantoConfig = { publishableKey: 'if_pk_xxx' };`}
      </Script>
      <Script
        src="https://cdn.savanto.ai/chat-loader.js"
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  );
}

Environment Variables

Use environment variables for the publishable key:

# .env.local
NEXT_PUBLIC_SAVANTO_KEY=if_pk_xxx
<SavantoWidgets publishableKey={process.env.NEXT_PUBLIC_SAVANTO_KEY!} />

Dynamic Import (Code Splitting)

Load the widget component dynamically:

import dynamic from 'next/dynamic';

const SavantoWidgets = dynamic(
  () => import('@/components/SavantoWidgets').then(mod => mod.SavantoWidgets),
  { ssr: false }
);

export default function Page() {
  return (
    <>
      <main>Your content</main>
      <SavantoWidgets publishableKey="if_pk_xxx" />
    </>
  );
}

Vue.js

Vue 3 Composition API

<!-- components/SavantoChat.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';

interface Props {
  publishableKey: string;
  title?: string;
}

const props = defineProps<Props>();

onMounted(() => {
  window.savantoConfig = {
    publishableKey: props.publishableKey,
    title: props.title,
  };

  const script = document.createElement('script');
  script.src = 'https://cdn.savanto.ai/chat-loader.js';
  script.async = true;
  document.body.appendChild(script);
});

onUnmounted(() => {
  window.SavantoChatbot?.destroy();
});
</script>

<template>
  <!-- Renders nothing, widget is injected into body -->
</template>

Vue 3 Composable

// composables/useSavanto.ts
import { onMounted, onUnmounted } from 'vue';

export function useSavantoChat(config: {
  publishableKey: string;
  title?: string;
  greeting?: string;
}) {
  onMounted(() => {
    window.savantoConfig = config;
    
    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.async = true;
    document.body.appendChild(script);
  });

  onUnmounted(() => {
    window.SavantoChatbot?.destroy();
  });

  return {
    open: () => window.SavantoChatbot?.open(),
    close: () => window.SavantoChatbot?.close(),
    toggle: () => window.SavantoChatbot?.toggle(),
  };
}

Nuxt 3

Create a plugin:

// plugins/savanto.client.ts
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig();
  
  window.savantoConfig = {
    publishableKey: config.public.savantoKey,
  };

  useHead({
    script: [
      {
        src: 'https://cdn.savanto.ai/chat-loader.js',
        async: true,
      },
    ],
  });
});
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      savantoKey: process.env.SAVANTO_KEY,
    },
  },
});

Angular

Service

// services/savanto.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class SavantoService {
  private loaded = false;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  init(config: { publishableKey: string; title?: string }) {
    if (!isPlatformBrowser(this.platformId) || this.loaded) return;

    (window as any).savantoConfig = config;

    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.async = true;
    document.body.appendChild(script);
    
    this.loaded = true;
  }

  open() {
    (window as any).SavantoChatbot?.open();
  }

  close() {
    (window as any).SavantoChatbot?.close();
  }

  destroy() {
    (window as any).SavantoChatbot?.destroy();
    this.loaded = false;
  }
}

Component

// app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SavantoService } from './services/savanto.service';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit, OnDestroy {
  constructor(private savanto: SavantoService) {}

  ngOnInit() {
    this.savanto.init({
      publishableKey: environment.savantoKey,
    });
  }

  ngOnDestroy() {
    this.savanto.destroy();
  }
}

Svelte

Component

<!-- SavantoChat.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';

  export let publishableKey: string;
  export let title: string | undefined = undefined;

  onMount(() => {
    window.savantoConfig = {
      publishableKey,
      title,
    };

    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.async = true;
    document.body.appendChild(script);

    return () => {
      window.SavantoChatbot?.destroy();
      script.remove();
    };
  });
</script>

SvelteKit

<!-- +layout.svelte -->
<script lang="ts">
  import { browser } from '$app/environment';
  import { onMount } from 'svelte';
  import { PUBLIC_SAVANTO_KEY } from '$env/static/public';

  onMount(() => {
    if (!browser) return;

    window.savantoConfig = {
      publishableKey: PUBLIC_SAVANTO_KEY,
    };

    const script = document.createElement('script');
    script.src = 'https://cdn.savanto.ai/chat-loader.js';
    script.async = true;
    document.body.appendChild(script);
  });
</script>

<slot />

Astro

Component

---
// components/SavantoChat.astro
interface Props {
  publishableKey: string;
}

const { publishableKey } = Astro.props;
---

<script define:vars={{ publishableKey }}>
  window.savantoConfig = {
    publishableKey,
  };
</script>

<script src="https://cdn.savanto.ai/chat-loader.js" async></script>

Usage in layout:

---
// layouts/Layout.astro
import SavantoChat from '../components/SavantoChat.astro';
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Site</title>
  </head>
  <body>
    <slot />
    <SavantoChat publishableKey="if_pk_xxx" />
  </body>
</html>

Common Patterns

Conditional Loading

Only load on certain pages:

// React example
function ProductPage() {
  const [showChat, setShowChat] = useState(false);

  useEffect(() => {
    // Load chat only on product pages
    setShowChat(true);
  }, []);

  return (
    <>
      <ProductContent />
      {showChat && <SavantoChat publishableKey="if_pk_xxx" />}
    </>
  );
}

User Authentication

Pass user JWT for authenticated features:

function App() {
  const { user, getToken } = useAuth();

  useEffect(() => {
    if (user) {
      const token = await getToken();
      window.SavantoChatbot?.configure({
        userJwt: token,
      });
    }
  }, [user]);

  return <SavantoChat publishableKey="if_pk_xxx" />;
}

Custom Trigger Buttons

function Header() {
  return (
    <nav>
      <button onClick={() => window.SavantoSearch?.open()}>
        <SearchIcon /> Search (⌘K)
      </button>
      <button onClick={() => window.SavantoChatbot?.open()}>
        <ChatIcon /> Help
      </button>
    </nav>
  );
}

Troubleshooting

Widget not loading in SPA

Ensure you're only loading the script once:

const loadedRef = useRef(false);

useEffect(() => {
  if (loadedRef.current) return;
  loadedRef.current = true;
  // Load script...
}, []);

Hydration mismatch

Use client-only rendering:

// Next.js
const SavantoChat = dynamic(() => import('./SavantoChat'), {
  ssr: false,
});

Widget persists after navigation

Call destroy() on unmount:

useEffect(() => {
  // Load widget...
  
  return () => {
    window.SavantoChatbot?.destroy();
  };
}, []);

Next Steps