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

@kanketsu/ai-share-btn

v0.1.0

Published

Headless React utilities to send the current page as a prompt to ChatGPT, Perplexity, Claude, and other AI providers. No UI included — bring your own dialog.

Readme

@kanketsu/ai-share-btn

Headless utilities to send "the current page" as a prompt to ChatGPT, Perplexity, Claude, and others. No UI — bring your own.

npm license


✨ Features

  • Headless: No bundled CSS, no <dialog>, no opinions about your design system. Use your own Shadcn / Radix / Tailwind / vanilla UI.
  • Zero runtime dependencies: Only react / react-dom as peer deps.
  • TypeScript first: Full types for every export.
  • SSR safe: getPageContext() returns sensible fallbacks when window is undefined.
  • 6+ providers built in: ChatGPT, Perplexity, Grok, Claude, Gemini, Copilot — with the latest query-spec quirks baked in.
  • Future-proof: When a provider changes its URL spec, you just bump this package — your UI code stays the same.

📦 Install

npm i @kanketsu/ai-share-btn
# or
bun add @kanketsu/ai-share-btn

Peer deps: react >= 18, react-dom >= 18.


🚀 Quick Start (minimal)

import { buildProviderUrl, getPageContext } from "@kanketsu/ai-share-btn";

function askChatGpt() {
  const ctx = getPageContext();
  const prompt = `${ctx.url}\n\nこのページについて教えて`;
  const { url } = buildProviderUrl("chatgpt", prompt);
  window.open(url, "_blank", "noopener,noreferrer");
}

That's it. No CSS to import. No component to wrap.


🧑‍🍳 Recipes (copy-paste ready)

Recipe 1 — Shadcn UI Dialog

The most common case. Drops straight into a Shadcn UI project.

"use client";

import { useState } from "react";
import { Sparkles } from "lucide-react";
import {
  Dialog,
  DialogContent,
  DialogTrigger,
  DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
  builtinProviders,
  copyText,
  getPageContext,
  type Provider,
} from "@kanketsu/ai-share-btn";

export function AskAiButton() {
  const [open, setOpen] = useState(false);

  async function handleClick(provider: Provider) {
    const ctx = getPageContext();
    const prompt = `${ctx.url}\n\nこのページについて要約して教えてください。`;
    const { url, promptDelivered } = provider.buildUrl(prompt);
    if (!promptDelivered) {
      await copyText(prompt); // clipboard fallback
    }
    window.open(url, "_blank", "noopener,noreferrer");
    setOpen(false);
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button variant="ghost" size="sm">
          <Sparkles className="size-4 mr-1.5" />
          AIに質問
        </Button>
      </DialogTrigger>
      <DialogContent className="max-w-sm">
        <DialogTitle>AIに質問する</DialogTitle>
        <div className="grid grid-cols-2 gap-3 mt-4">
          {builtinProviders.map((p) => (
            <button
              key={p.id}
              type="button"
              onClick={() => handleClick(p)}
              className="flex flex-col items-center gap-2 p-4 rounded-lg border hover:bg-muted"
            >
              <p.Icon size={32} />
              <span className="text-sm">{p.name}</span>
              {p.clipboardFallback && (
                <span className="text-[10px] text-muted-foreground">
                  (クリップボード経由)
                </span>
              )}
            </button>
          ))}
        </div>
      </DialogContent>
    </Dialog>
  );
}

Recipe 2 — Radix UI Dialog

For projects that use Radix directly (without Shadcn).

"use client";

import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import {
  builtinProviders,
  copyText,
  getPageContext,
  type Provider,
} from "@kanketsu/ai-share-btn";

export function AskAiButton() {
  const [open, setOpen] = useState(false);

  async function handleClick(provider: Provider) {
    const ctx = getPageContext();
    const prompt = `${ctx.url}\n\n${ctx.title}\n\nこのページについて教えて`;
    const { url, promptDelivered } = provider.buildUrl(prompt);
    if (!promptDelivered) await copyText(prompt);
    window.open(url, "_blank", "noopener,noreferrer");
    setOpen(false);
  }

  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger
        style={{ padding: "8px 12px", border: "1px solid #ccc", borderRadius: 6 }}
      >
        ✨ AIに質問
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay
          style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)" }}
        />
        <Dialog.Content
          style={{
            position: "fixed",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            background: "white",
            padding: 24,
            borderRadius: 12,
            minWidth: 320,
          }}
        >
          <Dialog.Title style={{ marginBottom: 16 }}>AIに質問する</Dialog.Title>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
            {builtinProviders.map((p) => (
              <button
                key={p.id}
                type="button"
                onClick={() => handleClick(p)}
                style={{
                  padding: 12,
                  border: "1px solid #eee",
                  borderRadius: 8,
                  cursor: "pointer",
                }}
              >
                <p.Icon size={24} />
                <div>{p.name}</div>
              </button>
            ))}
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Recipe 3 — Native <dialog> (zero deps)

No UI library at all. Pure React + the browser's native <dialog> element.

"use client";

import { useRef } from "react";
import {
  builtinProviders,
  copyText,
  getPageContext,
  type Provider,
} from "@kanketsu/ai-share-btn";

export function AskAiButton() {
  const ref = useRef<HTMLDialogElement>(null);

  async function handleClick(provider: Provider) {
    const ctx = getPageContext();
    const prompt = `${ctx.url}\n\nこのページについて教えて`;
    const { url, promptDelivered } = provider.buildUrl(prompt);
    if (!promptDelivered) await copyText(prompt);
    window.open(url, "_blank", "noopener,noreferrer");
    ref.current?.close();
  }

  return (
    <>
      <button type="button" onClick={() => ref.current?.showModal()}>
        ✨ AIに質問
      </button>
      <dialog ref={ref} style={{ borderRadius: 12, padding: 24, border: "none" }}>
        <h3 style={{ margin: 0, marginBottom: 12 }}>AIに質問する</h3>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
          {builtinProviders.map((p) => (
            <button key={p.id} type="button" onClick={() => handleClick(p)}>
              <p.Icon size={20} /> {p.name}
            </button>
          ))}
        </div>
        <button
          type="button"
          onClick={() => ref.current?.close()}
          style={{ marginTop: 12 }}
        >
          閉じる
        </button>
      </dialog>
    </>
  );
}

Note: ネイティブ <dialog>showModal() は内部で focus() を呼び、ページがスクロールすることがあります。気になる場合は Recipe 1 / 2 の手組み dialog を使ってください。

Recipe 4 — Generic click handler (clipboard fallback aware)

UI を完全に自分で書く場合の「最小ロジック」だけ抜き出した型。

import {
  buildProviderUrl,
  copyText,
  getPageContext,
  type ProviderId,
} from "@kanketsu/ai-share-btn";

export async function shareToAi(
  providerId: ProviderId,
  buildPrompt?: (ctx: ReturnType<typeof getPageContext>) => string,
) {
  const ctx = getPageContext();
  const prompt =
    buildPrompt?.(ctx) ?? `${ctx.url}\n\n${ctx.title}\n\nこのページについて教えて`;

  const { url, promptDelivered, truncated } = buildProviderUrl(providerId, prompt);

  if (!promptDelivered) {
    const ok = await copyText(prompt);
    if (!ok) {
      console.warn("[ai-share] clipboard copy failed.");
    }
  }

  if (truncated) {
    console.info("[ai-share] prompt was truncated to fit provider limits.");
  }

  window.open(url, "_blank", "noopener,noreferrer");
}

// 使い方:
// <button onClick={() => shareToAi("chatgpt")}>ChatGPTに質問</button>

📚 API Reference

buildProviderUrl(providerId, prompt, opts?) => BuildUrlResult

buildProviderUrl("chatgpt", "Hello world", {
  model: "gpt-4",            // optional: model param
  extraParams: { foo: "1" }, // optional: extra query params
  maxLength: 2500,           // optional: truncation cap (default 2500)
});
// => { url: "https://chatgpt.com/?q=...", promptDelivered: true, truncated: false }

| Field | Type | Notes | | ----------------- | --------- | -------------------------------------------------------------- | | url | string | Open with window.open(url, "_blank", "noopener,noreferrer"). | | promptDelivered | boolean | false → URL spec doesn't accept query; copy to clipboard. | | truncated | boolean | true if prompt was cut to fit maxLength. |

getPageContext() => PageContext

{
  url: string;          // window.location.href (or "" on server)
  title: string;        // document.title
  description?: string; // <meta name="description">
  selection?: string;   // window.getSelection() if any
}

SSR-safe: returns empty strings when window / document is undefined.

copyText(text) => Promise<boolean>

navigator.clipboard.writeText() を優先し、失敗したら execCommand('copy') にフォールバックします。 ブラウザのセキュリティ制約上、ユーザー操作 (click) 起因のハンドラ内で呼ぶ必要があります。

builtinProviders: Provider[]

Provider は以下を持ちます:

{
  id: ProviderId;
  name: string;
  brandColor: string;
  Icon: ComponentType<{ size?: number; className?: string }>;
  buildUrl: (prompt: string, opts?) => BuildUrlResult;
  clipboardFallback?: boolean; // true なら copyText() してから開く
  maxPromptLength?: number;
}

getProvider(id) / getDefaultProviders() / registerProvider(provider)

getProvider("chatgpt");           // => Provider | undefined
getDefaultProviders();            // => [chatgpt, claude, perplexity]
registerProvider(myCustomProv);   // upserts into the internal registry

🤖 Supported Providers

| ID | Name | Query で prompt を渡せる? | | ------------ | ---------- | --------------------------------------------------- | | chatgpt | ChatGPT | ✅ ?q= | | perplexity | Perplexity | ✅ ?q= | | grok | Grok | ✅ ?q= | | claude | Claude | ❌ clipboard fallback (?q= removed 2025-10) | | gemini | Gemini | ❌ clipboard fallback (no URL param) | | copilot | Copilot | ❌ clipboard fallback (regression) |

仕様が変わったら本パッケージを bump するだけで追従できます。アプリ側のコードは原則そのまま。


📋 Clipboard fallback の扱い方

provider.clipboardFallback === true のプロバイダは URL で prompt を渡せません。クリックハンドラの中で:

  1. copyText(prompt) を呼んでクリップボードへ
  2. window.open(provider.buildUrl(prompt).url, ...) でサイトを開く
  3. ユーザーには「クリップボードにコピーしました。貼り付けて送信してください」とトーストで通知
import { toast } from "sonner";

async function handle(p: Provider) {
  const { url, promptDelivered } = p.buildUrl(prompt);
  if (!promptDelivered) {
    await copyText(prompt);
    toast.success("プロンプトをコピーしました。開いたページに貼り付けてください。");
  }
  window.open(url, "_blank", "noopener,noreferrer");
}

fallback を出したくない場合は、クエリ対応のプロバイダだけに絞れます:

const queryOnly = builtinProviders.filter((p) => !p.clipboardFallback);
// => [chatgpt, perplexity, grok]

🧩 Custom Providers

registerProvider() で任意の AI サービスを追加できます。

import { registerProvider, type Provider } from "@kanketsu/ai-share-btn";

const myProvider: Provider = {
  id: "myai",
  name: "MyAI",
  brandColor: "#ff5599",
  Icon: ({ size = 24 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24">
      <circle cx="12" cy="12" r="10" fill="currentColor" />
    </svg>
  ),
  buildUrl: (prompt) => ({
    url: `https://my-ai.example.com/?q=${encodeURIComponent(prompt)}`,
    promptDelivered: true,
    truncated: false,
  }),
};

registerProvider(myProvider);
// 以降 getProvider("myai") で取得可能

📜 Logo trademarks

プロバイダのロゴ SVG は lobehub/lobe-icons (MIT) から取得しています。 ChatGPT / Claude / Gemini / Grok / Copilot / Perplexity の各ブランドおよびロゴは、それぞれの企業の商標です。 詳細は NOTICE.md を参照してください。


License

MIT © Kazuma Horiike