npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

josie-widget

v3.0.1

Published

Official React component for embedding J.O.S.I.E chatbots in React and Next.js apps.

Readme

josie-widget

Official client library for embedding a J.O.S.I.E (Joe Solutions Intelligence Engine) chatbot into any website, React app, Next.js project, or mobile application.

  • Web (any site) — one <script> tag, floating chat bubble, zero dependencies
  • React / Next.jsnpm install josie-widget, drop in a component
  • Mobile (React Native, Swift, Kotlin) — call the REST API directly, no SDK required

Table of Contents

  1. Prerequisites
  2. Web — Script Tag (CDN)
  3. React / Next.js — npm package
  4. Mobile — REST API
  5. API reference
  6. Security
  7. Troubleshooting

1. Prerequisites

Before you start you need:

Your widget API key looks like wk_live_a1b2c3d4e5f6.... It is a browser-safe key — keep it out of your server-side code. If a key is ever compromised, revoke it from the dashboard and create a new one.


2. Web — Script Tag (CDN)

The CDN widget is a single self-contained JavaScript file. It has zero dependencies and will not interfere with your existing CSS or JavaScript.

2.1 Floating bubble (default)

Paste this snippet anywhere in your HTML, ideally just before the closing </body> tag:

<script
  src="https://josie.ai/widget.js"
  data-josie-key="wk_live_YOUR_API_KEY"
  data-app-name="Acme Support"
  data-placeholder="How can we help you today?"
  async
></script>

A chat bubble will appear in the bottom-right corner of every page. Visitors click it to open and close the chat panel.

2.2 Inline embed

If you want the chatbot embedded inside a specific element on your page (e.g. a support page sidebar) instead of a floating bubble, add a data-target attribute pointing to a CSS selector:

<!-- 1. Create a container wherever you want the chat to appear -->
<div id="josie-chat" style="height: 560px;"></div>

<!-- 2. Load the widget and point it at the container -->
<script
  src="https://josie.ai/widget.js"
  data-josie-key="wk_live_YOUR_API_KEY"
  data-app-name="Acme Support"
  data-target="#josie-chat"
  async
></script>

The widget fills the container completely. Set the height on your container to whatever fits your layout.

2.3 All data attributes

| Attribute | Required | Default | Description | |----------------------|----------|--------------------------------|-------------| | data-josie-key | Yes | — | Your widget API key (wk_live_...) | | data-app-name | No | "J.O.S.I.E" | Name shown in the chat header | | data-placeholder | No | "Ask me anything..." | Input placeholder text | | data-target | No | null (floating bubble) | CSS selector of an inline container | | data-theme | No | auto (follows system theme) | "light" or "dark" to force a theme | | data-position | No | "right" | "left" to move bubble to the bottom-left | | data-api-url | No | Origin of widget.js src URL | Override the JOSIE API base URL |

2.4 WordPress

  1. Go to Appearance → Theme File Editor (or use a plugin like Insert Headers and Footers)
  2. Paste the snippet into your theme's footer.php just before </body>, or into the "Footer Scripts" field of the plugin
<!-- Paste inside footer.php, before </body> -->
<script
  src="https://josie.ai/widget.js"
  data-josie-key="wk_live_YOUR_API_KEY"
  data-app-name="My Business"
  async
></script>

Using a plugin is safer because it survives theme updates. Popular choices: Insert Headers and Footers, WPCode, or Code Snippets.

2.5 Shopify

  1. In your Shopify admin go to Online Store → Themes → Edit code
  2. Open layout/theme.liquid
  3. Paste the snippet just before </body>
<!-- theme.liquid — just before </body> -->
<script
  src="https://josie.ai/widget.js"
  data-josie-key="wk_live_YOUR_API_KEY"
  data-app-name="{{ shop.name }}"
  data-placeholder="Ask us anything about your order..."
  async
></script>

The {{ shop.name }} liquid variable automatically uses your store name.

2.6 Webflow

  1. Go to your Project Settings → Custom Code
  2. Paste the snippet into the Footer Code section
  3. Publish your site
<script
  src="https://josie.ai/widget.js"
  data-josie-key="wk_live_YOUR_API_KEY"
  data-app-name="My Brand"
  async
></script>

3. React / Next.js — npm package

3.1 Installation

npm install josie-widget
# or
yarn add josie-widget
# or
pnpm add josie-widget

Requirements: React 18 or later, react-dom 18 or later.

3.2 Basic usage

import { JosieChat } from "josie-widget";

export default function SupportPage() {
  return (
    <div style={{ height: "600px" }}>
      <JosieChat
        apiKey="wk_live_YOUR_API_KEY"
        appName="Acme Support"
        placeholder="How can we help you today?"
        suggestions={[
          "How do I reset my password?",
          "Where is my order?",
          "What are your pricing plans?",
        ]}
      />
    </div>
  );
}

The component fills whatever container it is placed in. Always give the container an explicit height.

3.3 Next.js App Router

Because JosieChat uses React state and browser event listeners, it must live inside a Client Component. The recommended pattern is to wrap it:

// app/support/page.tsx  ← Server Component, no "use client" needed here
import ChatWidget from "@/components/ChatWidget";

export default function SupportPage() {
  return (
    <main className="max-w-2xl mx-auto py-12 px-4">
      <h1 className="text-2xl font-bold mb-6">Support</h1>
      <div style={{ height: "580px" }}>
        <ChatWidget />
      </div>
    </main>
  );
}
// components/ChatWidget.tsx  ← Client Component
"use client";

import { JosieChat } from "josie-widget";

export default function ChatWidget() {
  return (
    <JosieChat
      apiKey={process.env.NEXT_PUBLIC_JOSIE_API_KEY!}
      appName="Acme Support"
      placeholder="How can we help?"
    />
  );
}

Store your API key in .env.local:

# .env.local
NEXT_PUBLIC_JOSIE_API_KEY=wk_live_YOUR_API_KEY

Note: NEXT_PUBLIC_ prefix is intentional — the key is used in the browser by the widget to call the chat API. It does not grant access to your dashboard or any admin functions. Before going live, add your production domain to the Allowed Domains list in your J.O.S.I.E dashboard so the key cannot be used from other origins.

3.4 Next.js Pages Router

// pages/support.tsx
import type { NextPage } from "next";
import { JosieChat } from "josie-widget";

const SupportPage: NextPage = () => {
  return (
    <main style={{ maxWidth: 720, margin: "0 auto", padding: "3rem 1rem" }}>
      <h1>Support</h1>
      <div style={{ height: 580 }}>
        <JosieChat
          apiKey={process.env.NEXT_PUBLIC_JOSIE_API_KEY!}
          appName="Acme Support"
        />
      </div>
    </main>
  );
};

export default SupportPage;

3.5 Vite / CRA / Remix

// src/pages/Support.tsx (Vite / CRA)
import { JosieChat } from "josie-widget";

export function SupportPage() {
  return (
    <div style={{ height: "600px", maxWidth: "720px", margin: "0 auto" }}>
      <JosieChat
        apiKey={import.meta.env.VITE_JOSIE_API_KEY}
        appName="Acme Support"
      />
    </div>
  );
}
// app/routes/support.tsx (Remix)
// Remix loads on the server and client — add the "use client" directive or
// use ClientOnly from remix-utils to avoid SSR issues with useState.
import { ClientOnly } from "remix-utils/client-only";
import { JosieChat } from "josie-widget";

export default function Support() {
  return (
    <ClientOnly fallback={<div style={{ height: 600 }} />}>
      {() => (
        <div style={{ height: 600 }}>
          <JosieChat apiKey={process.env.JOSIE_API_KEY!} appName="Acme" />
        </div>
      )}
    </ClientOnly>
  );
}

3.6 All props

| Prop | Type | Required | Default | Description | |---------------|------------|----------|------------------------|-------------| | apiKey | string | Yes | — | Your widget API key (wk_live_...) | | apiUrl | string | No | "https://josie.ai" | Base URL of the JOSIE API | | appName | string | No | "J.O.S.I.E" | Name displayed in the chat header | | placeholder | string | No | "Ask me anything..." | Input textarea placeholder | | suggestions | string[] | No | undefined | Quick-reply chips on the welcome screen | | showPoweredByBranding | boolean | No | loaded from API | Override tenant branding. When omitted, fetches GET /api/widget-settings. Set false on the tenant row in RDS to hide branding for all embeds. |

3.7 Tailwind setup

JosieChat renders Tailwind CSS utility classes. If your project already uses Tailwind, you need to tell it to scan the package so the classes are not tree-shaken:

// tailwind.config.js  (Tailwind v3)
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./node_modules/josie-widget/dist/**/*.{js,mjs}",
  ],
  // ...
};
// tailwind.config.ts  (Tailwind v4 — add a source glob in your CSS)
// In your global CSS file:
// @source "../../node_modules/josie-widget/dist";

If you are not using Tailwind in your project, the component will still render but without styles. In that case, wrap it in a container and apply your own styles, or switch to the CDN widget which has all styles built in.


4. Mobile — REST API

There is no native mobile SDK — you simply call the chat REST endpoint directly from your app with an HTTP client. This works in React Native, Swift, Kotlin, Flutter, and any other environment that can make HTTP requests.

4.1 Request format

POST https://josie.ai/api/chat
Authorization: Bearer wk_live_YOUR_API_KEY
Content-Type: application/json

Request body:

{
  "messages": [
    { "role": "user", "content": "Hello, what can you help me with?" }
  ],
  "sessionId": "optional-uuid-from-a-previous-response"
}

| Field | Type | Required | Description | |------------|------------------|----------|-------------| | messages | array | Yes | Full conversation history. Each item has role ("user" or "assistant") and content (string). Always include the complete history — the API is stateless between requests. | | sessionId| string (UUID) | No | Session ID returned by the server on the first response. Include it on subsequent requests to associate messages with the same conversation in your analytics. |

4.2 Response format (SSE)

The response is a Server-Sent Events (SSE) stream with Content-Type: text/event-stream. Each line is a data: event:

data: {"sessionId":"550e8400-e29b-41d4-a716-446655440000"}

data: {"content":"Hello"}

data: {"content":"! I"}

data: {"content":" can help you with..."}

data: [DONE]

| Event | Meaning | |---------------------------|---------| | {"sessionId":"uuid"} | First chunk. Store this UUID and send it back in future requests as sessionId. | | {"content":"token"} | A streamed token from the AI. Concatenate these to build the full reply. | | [DONE] | Stream is complete. The full reply is everything you concatenated. |

Non-streaming clients: If your HTTP client does not support streaming, you can still read the full response body after the connection closes, then split it on \n\n and parse each data: line as JSON.

4.3 React Native example

import { useState } from "react";

const JOSIE_API_KEY = "wk_live_YOUR_API_KEY";
const JOSIE_URL = "https://josie.ai/api/chat";

type Message = { role: "user" | "assistant"; content: string };

export function useJosieChat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  async function sendMessage(userText: string) {
    const newMessages: Message[] = [
      ...messages,
      { role: "user", content: userText },
    ];
    setMessages(newMessages);
    setIsLoading(true);

    // Append a placeholder for the assistant reply
    setMessages((prev) => [...prev, { role: "assistant", content: "" }]);

    try {
      const res = await fetch(JOSIE_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${JOSIE_API_KEY}`,
        },
        body: JSON.stringify({ messages: newMessages, sessionId }),
      });

      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error ?? "Request failed");
      }

      // React Native's fetch does not support streaming — read the full body
      const text = await res.text();
      let fullReply = "";
      let newSessionId = sessionId;

      for (const line of text.split("\n")) {
        if (!line.startsWith("data: ")) continue;
        const data = line.slice(6).trim();
        if (data === "[DONE]") continue;

        try {
          const parsed = JSON.parse(data);
          if (parsed.sessionId) newSessionId = parsed.sessionId;
          if (parsed.content) fullReply += parsed.content;
        } catch (_) {}
      }

      if (newSessionId) setSessionId(newSessionId);

      setMessages((prev) => {
        const updated = [...prev];
        updated[updated.length - 1] = { role: "assistant", content: fullReply };
        return updated;
      });
    } catch (error) {
      console.error("JOSIE chat error:", error);
      // Remove the empty assistant placeholder on error
      setMessages((prev) => prev.slice(0, -1));
    } finally {
      setIsLoading(false);
    }
  }

  return { messages, sendMessage, isLoading };
}

Usage in a screen:

import { View, TextInput, Button, FlatList, Text } from "react-native";
import { useJosieChat } from "./useJosieChat";
import { useState } from "react";

export function SupportScreen() {
  const { messages, sendMessage, isLoading } = useJosieChat();
  const [input, setInput] = useState("");

  function handleSend() {
    if (!input.trim()) return;
    sendMessage(input.trim());
    setInput("");
  }

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <FlatList
        data={messages}
        keyExtractor={(_, i) => String(i)}
        renderItem={({ item }) => (
          <Text style={{ color: item.role === "user" ? "blue" : "black" }}>
            {item.role === "user" ? "You: " : "JOSIE: "}{item.content}
          </Text>
        )}
      />
      <TextInput
        value={input}
        onChangeText={setInput}
        placeholder="Ask me anything..."
        editable={!isLoading}
      />
      <Button title={isLoading ? "Thinking..." : "Send"} onPress={handleSend} disabled={isLoading} />
    </View>
  );
}

4.4 Swift (iOS) example

import Foundation

class JosieChatService {
    private let apiKey = "wk_live_YOUR_API_KEY"
    private let apiURL = URL(string: "https://josie.ai/api/chat")!
    private var sessionId: String?

    struct Message: Codable {
        let role: String
        let content: String
    }

    func sendMessage(
        _ userText: String,
        history: [Message],
        completion: @escaping (Result<String, Error>) -> Void
    ) {
        var messages = history
        messages.append(Message(role: "user", content: userText))

        var body: [String: Any] = [
            "messages": messages.map { ["role": $0.role, "content": $0.content] }
        ]
        if let sid = sessionId { body["sessionId"] = sid }

        var request = URLRequest(url: apiURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        // URLSession does not natively stream SSE — read full response
        URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data,
                  let text = String(data: data, encoding: .utf8) else {
                completion(.failure(NSError(domain: "JOSIE", code: -1)))
                return
            }

            var fullReply = ""
            for line in text.components(separatedBy: "\n") {
                guard line.hasPrefix("data: ") else { continue }
                let payload = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces)
                guard payload != "[DONE]",
                      let jsonData = payload.data(using: .utf8),
                      let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
                else { continue }

                if let sid = json["sessionId"] as? String { self?.sessionId = sid }
                if let token = json["content"] as? String { fullReply += token }
            }

            completion(.success(fullReply))
        }.resume()
    }
}

4.5 Kotlin (Android) example

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject

class JosieChatService {
    private val client = OkHttpClient()
    private val apiKey = "wk_live_YOUR_API_KEY"
    private val apiUrl = "https://josie.ai/api/chat"
    private var sessionId: String? = null

    data class Message(val role: String, val content: String)

    suspend fun sendMessage(userText: String, history: List<Message>): String =
        withContext(Dispatchers.IO) {
            val messages = history + Message("user", userText)

            val messagesArray = JSONArray().apply {
                messages.forEach { put(JSONObject().put("role", it.role).put("content", it.content)) }
            }

            val body = JSONObject().apply {
                put("messages", messagesArray)
                sessionId?.let { put("sessionId", it) }
            }

            val request = Request.Builder()
                .url(apiUrl)
                .post(body.toString().toRequestBody("application/json".toMediaType()))
                .addHeader("Authorization", "Bearer $apiKey")
                .addHeader("Content-Type", "application/json")
                .build()

            client.newCall(request).execute().use { response ->
                val text = response.body?.string() ?: return@withContext ""
                var fullReply = ""

                text.split("\n").forEach { line ->
                    if (!line.startsWith("data: ")) return@forEach
                    val payload = line.removePrefix("data: ").trim()
                    if (payload == "[DONE]") return@forEach

                    runCatching {
                        val json = JSONObject(payload)
                        json.optString("sessionId").takeIf { it.isNotBlank() }?.let { sessionId = it }
                        fullReply += json.optString("content", "")
                    }
                }

                fullReply
            }
        }
}

OkHttp dependency — add to your build.gradle.kts:

implementation("com.squareup.okhttp3:okhttp:4.12.0")

4.6 Error responses

When the request fails (non-2xx status), the response body is a JSON object:

{ "error": "Human-readable error message" }

Common status codes:

| Status | Meaning | |--------|---------| | 400 | Bad request — messages array is missing or empty | | 401 | Unauthorized — API key is missing, invalid, or revoked | | 403 | Forbidden — either the request domain is not in the tenant's allowed list, or the subscription is inactive | | 422 | Out of scope — the message was blocked by the guardrails; the error value is the refusal message you should show the user | | 429 | Monthly usage limit reached — the tenant needs to upgrade their plan | | 500 | Internal server error — retry with exponential back-off |

4.7 Session management

A sessionId ties multiple messages together into a single conversation in the analytics dashboard. Best practices:

  • Save the sessionId from the first SSE chunk of each new conversation
  • Pass it back in every subsequent request in the same conversation
  • When the user starts a new conversation, omit sessionId (or set it to null) — the server will create a new session
  • On mobile, store sessionId in memory (not on disk) unless you explicitly want conversation history to persist across app restarts

5. API reference

POST /api/chat

| Header | Value | |-----------------|-------| | Authorization | Bearer wk_live_YOUR_API_KEY | | Content-Type | application/json |

Request body schema:

{
  messages: Array<{ role: "user" | "assistant"; content: string }>;
  sessionId?: string;
}

Response: text/event-stream

Each SSE event is a data: line containing JSON:

// First event — always present
{ sessionId: string }

// Subsequent events — streamed tokens
{ content: string }

// Final event — signals end of stream
"[DONE]"

6. Security

API key exposure in the browser

Widget API keys (wk_live_*) are intentionally used in the browser — the same model used by Stripe.js, Mapbox, and every other browser-side SDK. The key can only trigger a chat conversation for your configured bot; it cannot access your dashboard, modify your configuration, or perform any admin operations.

Domain allowlisting (required before going live)

To prevent your key from being copied and used on other websites, J.O.S.I.E enforces origin-based allowlisting. Until you configure allowed domains the API accepts requests from any origin, which is fine for local development.

Before deploying to production you must add your domain(s):

  1. Go to your J.O.S.I.E dashboard → Configure
  2. Scroll to Allowed Domains
  3. Add each hostname that will embed the widget (e.g. mycompany.com, app.mycompany.com)
  4. Save — the restriction takes effect immediately

Once configured, any request from an unlisted domain will receive a 403 response. Your own domains will receive the correct Access-Control-Allow-Origin header automatically.

Other security notes

  • Rotate compromised keys. If a key leaks, revoke it from Dashboard → API Keys and generate a new one. Existing sessions will fail immediately.
  • Mobile / server-to-server calls do not send an Origin header, so they bypass domain allowlisting by design — this is safe because the key cannot be scraped from a native binary the same way it can from client-side JavaScript.
  • Rate limiting: Demo keys (wk_demo_*) are rate-limited per IP. Live keys are limited by your subscription's monthly message quota.

7. Troubleshooting

The widget does not appear

  • Confirm the <script> tag is in the <body>, not <head>
  • Confirm data-josie-key is set and is a valid live key
  • Open the browser console and look for [JOSIE Widget] warnings

I get a CORS error in the console

This almost always means the domain making the request is not in your Allowed Domains list. Add the hostname (e.g. mycompany.com) to your dashboard → Configure → Allowed Domains and try again. CORS errors can also appear if you mistype the data-api-url attribute.

I get a 403 "This domain is not authorised" error

Same root cause as the CORS issue above. The Origin header of the request doesn't match any domain in your allowlist. Steps to fix:

  1. Open Dashboard → Configure → Allowed Domains
  2. Add the exact hostname (no https://, no trailing slash) — e.g. mycompany.com
  3. If testing locally, add localhost to the list temporarily (or set IS_DEV=true in your .env.local)

The chat sends but never gets a response (React Native / mobile)

React Native's fetch does not support streaming. See section 4.3 — you need to read the full response body and parse the data: lines manually.

TypeScript errors after installing

Make sure your tsconfig.json includes "moduleResolution": "Bundler" or "Node16". The package ships with .d.ts files.

The component renders without styles (Tailwind purging)

Add the package path to your Tailwind content array. See section 3.7.


License

MIT © Joe Solutions