widget/Framework Integration

Framework Integration

The Intufind 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 IntufindChat() {
  useEffect(() => {
    // Load the widget script
    const script = document.createElement('script');
    script.src = 'https://cdn.intufind.com/chat-loader.js';
    script.dataset.publishableKey = 'if_pk_xxx';
    script.async = true;
    document.body.appendChild(script);

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

  return null;
}

Custom React Hook

Create a reusable hook:

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

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

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

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

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

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

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

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

  return { open, close, toggle };
}

Usage:

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

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

TypeScript Declarations

Add type declarations for the global API:

// types/intufind.d.ts
interface IntufindChatbotAPI {
  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 IntufindSearchAPI {
  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 {
    intufindConfig?: Record<string, any>;
    intufindSearchConfig?: Record<string, any>;
    IntufindChatbot?: IntufindChatbotAPI;
    IntufindSearch?: IntufindSearchAPI;
  }
}

export {};

Next.js

App Router (Next.js 13+)

Create a client component:

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

import { useEffect } from 'react';

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

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

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

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

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

  return null;
}

Add to your layout:

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <IntufindWidgets 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="intufind-config" strategy="beforeInteractive">
        {`window.intufindConfig = { publishableKey: 'if_pk_xxx' };`}
      </Script>
      <Script
        src="https://cdn.intufind.com/chat-loader.js"
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  );
}

Environment Variables

Use environment variables for the publishable key:

# .env.local
NEXT_PUBLIC_INTUFIND_KEY=if_pk_xxx
<IntufindWidgets publishableKey={process.env.NEXT_PUBLIC_INTUFIND_KEY!} />

Dynamic Import (Code Splitting)

Load the widget component dynamically:

import dynamic from 'next/dynamic';

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

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

Vue.js

Vue 3 Composition API

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

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

const props = defineProps<Props>();

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

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

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

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

Vue 3 Composable

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

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

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

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

Nuxt 3

Create a plugin:

// plugins/intufind.client.ts
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig();
  
  window.intufindConfig = {
    publishableKey: config.public.intufindKey,
  };

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

Angular

Service

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

@Injectable({ providedIn: 'root' })
export class IntufindService {
  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).intufindConfig = config;

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

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

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

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

Component

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

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

  ngOnInit() {
    this.intufind.init({
      publishableKey: environment.intufindKey,
    });
  }

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

Svelte

Component

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

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

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

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

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

SvelteKit

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

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

    window.intufindConfig = {
      publishableKey: PUBLIC_INTUFIND_KEY,
    };

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

<slot />

Astro

Component

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

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

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

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

Usage in layout:

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Site</title>
  </head>
  <body>
    <slot />
    <IntufindChat 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 && <IntufindChat 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.IntufindChatbot?.configure({
        userJwt: token,
      });
    }
  }, [user]);

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

Custom Trigger Buttons

function Header() {
  return (
    <nav>
      <button onClick={() => window.IntufindSearch?.open()}>
        <SearchIcon /> Search (⌘K)
      </button>
      <button onClick={() => window.IntufindChatbot?.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 IntufindChat = dynamic(() => import('./IntufindChat'), {
  ssr: false,
});

Widget persists after navigation

Call destroy() on unmount:

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

Next Steps