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

@firstlovecenter/ai-chat

v0.30.4

Published

Reusable AI chat module: agent loop, Vertex providers (Claude + Gemini), narrators, persistence schema, and chat UI components. Host injects auth, scope, tools, and credentials via configureAiChat().

Readme

@firstlovecenter/ai-chat

Reusable AI chat module for Next.js apps. Ships:

  • An agent tool loop that runs against Vertex AI (Claude or Gemini).
  • Two chat UI components: a custom hand-rolled chat and a Vercel AI SDK chat.
  • Persistence routes (sessions, messages, admin AI settings).
  • ORM-agnostic persistence: bring your own Drizzle (drizzle-orm) or Prisma (@prisma/client).
  • Registries for available tool-calling models and chat interfaces — your app picks which to expose.

The host app supplies its own auth, scope, tools, prompts, Vertex credentials, and settings UI. The package handles everything else.

Status

Pre-release. See the project plan for the current roadmap.

Install

pnpm add @firstlovecenter/ai-chat next react react-dom
# pick one persistence backend:
pnpm add drizzle-orm
# or:
pnpm add @prisma/client

Wire up styles

The package ships precompiled CSS, so host apps no longer need to configure Tailwind to scan node_modules or hand-author the chat's colour variables. In your root global stylesheet:

/* app/globals.css (or equivalent) */
@import "@firstlovecenter/ai-chat/styles.css";    /* compiled utilities used by the chat components */
@import "tailwindcss";                            /* your own Tailwind, if any */
@import "@firstlovecenter/ai-chat/theme.css";     /* default light / dark OkLCh palette */

That's it. Dark mode toggles on either the .dark class or [data-theme="dark"] attribute on any ancestor (typically <html>).

Import order matters. Both styles.css and @import "tailwindcss" emit universal utilities (.hidden, .md:block, .flex, …) into @layer utilities. Within a layer, later source wins. Putting the package import BEFORE @import "tailwindcss" lets your own utilities win cascade. If you reverse the order, components like the shadcn sidebar (className="hidden md:block") will be hidden by the package's trailing .hidden{display:none} even on desktop. theme.css only declares CSS variables (no utilities), so its position doesn't matter — put it after your own theme overrides for the cleanest cascade.

To brand the chat, override the CSS variables defined in theme.css from your own stylesheet AFTER the imports:

:root        { --primary: oklch(0.45 0.22 265); }
.dark        { --primary: oklch(0.65 0.18 265); }

If you'd rather skip theme.css and supply your own values, just define the same variables (--background, --foreground, --card*, --popover*, --primary*, --secondary*, --muted*, --accent*, --destructive*, --border, --input, --ring, --sidebar*, --chart-1--chart-5) on :root and .dark.

Migrating from 0.10.x

If you previously wired the chat manually:

  • Remove any @source "…/@firstlovecenter/ai-chat" line from your global CSS — the package now ships its compiled utilities.
  • Remove any manual --sidebar*, --background, --foreground, --card*, --popover*, --primary*, --secondary*, --muted*, --accent*, --destructive*, --border, --input, --ring definitions that existed only to back this package. Host-specific brand tokens (your own --canvas, --surface, etc.) stay where they are.
  • Remove the corresponding --color-* entries from your @theme inline block.
  • Remove the manual @custom-variant dark if it was added only for this package.
  • Add the two @import lines above.

The migration is purely additive — 0.10.x hosts that don't touch their stylesheet continue working unchanged.

Tailwind v3 hosts (optional)

If you prefer your own Tailwind pipeline tree-shake the package's classes rather than ship the precompiled bundle, use the Tailwind v3 preset:

// tailwind.config.js
module.exports = {
  presets: [require('@firstlovecenter/ai-chat/tailwind-preset')],
  content: ['./src/**/*.{ts,tsx}' /* …plus your own globs… */]
};

Still import theme.css for the default palette (or define your own CSS variables).

Quick start

// src/lib/ai-chat.ts (host)
import { configureAiChat } from '@firstlovecenter/ai-chat/server';
import { createDrizzlePersistence } from '@firstlovecenter/ai-chat/server/drizzle';
// or: import { createPrismaPersistence } from '@firstlovecenter/ai-chat/server/prisma';

export const aiChat = configureAiChat({
  persistence: createDrizzlePersistence(db),
  auth: { requireAuth, isSuperAdmin },
  scope: { resolveScopeLabel, buildScopeSummary },
  tools: { tools: ALL_TOOLS, buildSystemBlocks },
  vertex: {
    projectId: process.env.GCP_PROJECT_ID!,
    defaultLocation: process.env.GCP_LOCATION ?? 'us-east5',
    auth: getVertexAuth(),
    modelIds: { claude: process.env.GCP_CLAUDE_MODEL!, gemini: process.env.GCP_GEMINI_MODEL! }
  }
});

Detailed integration docs (Drizzle migrations, Prisma fragment paste, Vertex auth recipes, tool authoring, settings UI) ship with v0.1.0.

UI components

The ./ui entry exports two full-page chat shells — AiChat (bespoke SSE) and VercelChat (Vercel AI SDK) — plus, since 0.30.0, a floating chat bubble, AiChatBubble.

AiChatBubble (floating launcher + docked panel)

AiChatBubble is a dockable, Intercom-style launcher that opens a compact chat panel. It is a presentational shell over the same chat engine as AiChat (identical /api/agent SSE path, session persistence, narrator picker, streaming, and AnswerBlocks) — there is no new server engine. Unlike AiChat it never navigates the host page: open/closed state and the last session id are persisted to localStorage, so it can float on every page and restore itself in place.

Render it once in your authenticated shell so it floats everywhere:

// app/(app)/layout.tsx
'use client';
import { AiChatBubble } from '@firstlovecenter/ai-chat/ui';

<AiChatBubble
  userFirstName={user.firstName}
  scopeLabel={scopeLabel}                 // "All estates" / "Estate A, Estate B"
  initialProvider={mySettings.narrativeProvider}  // 'claude' | 'grok' | 'gemini'
  side="bottom-right"
  panelTitle="Estate Assistant"
/>;

It takes all of AiChatProps plus:

| Prop | Type | Default | Notes | | --- | --- | --- | --- | | side | "bottom-left" \| "bottom-right" | "bottom-right" | Corner the launcher docks to. | | offset | { x?: number; y?: number } | 0 | Extra px from the corner (added to the safe-area inset). | | defaultOpen | boolean | false | Open on first mount when nothing is persisted. | | panelTitle | string | "Assistant" | Panel header label. | | launcherLabel | string | "Open assistant" | Accessible label for the launcher button. | | unreadDot | boolean | false | Attention dot on the launcher while closed. |

Behaviour: the launcher floats at the chosen corner (lucide MessageCircle, mobile safe-area aware); click toggles the panel. The panel is a compact dock on desktop and a near-full-screen sheet on small screens, with a recent-sessions + "New chat" menu, the thread, and the input pill. Esc closes it, focus is trapped while open and restored to the launcher on close. basePath is not navigated to for the bubble — it only namespaces the localStorage keys (flc-ai-chat:<basePath>:open / :session) so multiple bubbles can coexist. Wire the same routes AiChat uses (/api/agent, /api/chat/sessions/**, /api/settings/me).

Per-user narrator preference (host-owned storage)

The chat UI exposes a per-user narrator picker (claude / gemini / grok) backed by GET / PATCH /api/settings/me. The package does not own a users table, so storage is the host's responsibility — but the package ships a route factory (routes.userSettings) that handles auth, body validation, and the wire format on top of two thin host-supplied hooks.

Wire contract

GET   /api/settings/me  →  { "narrative_provider": "claude" | "gemini" | "grok" }
PATCH /api/settings/me  ←  { "narrative_provider": "claude" | "gemini" | "grok" }
                        →  { "ok": true }
  • Auth: authenticated user only (no role check). Add same-origin / CSRF protection at your framework layer if your app needs it; the package's other PATCH routes do not perform CSRF checks either.
  • 400 when body is missing or narrative_provider isn't one of the accepted ids.
  • 401 when unauthenticated.
  • 501 when the host hasn't configured the corresponding hook (resolveNarratorId for GET, setNarratorId for PATCH).

Option A — use the shipped factory (recommended)

// src/lib/ai-chat.ts
export const aiChat = configureAiChat({
  // ...persistence, auth, scope, tools, vertex...
  resolveNarratorId: async (scope) => {
    const row = await db.select({ v: users.narrative_provider })
      .from(users).where(eq(users.id, scope.userId)).limit(1);
    return row[0]?.v ?? 'claude';
  },
  setNarratorId: async ({ userId, narratorId }) => {
    await db.update(users).set({ narrative_provider: narratorId })
      .where(eq(users.id, userId));
  }
});
// app/api/settings/me/route.ts
import { aiChat } from '@/lib/ai-chat';
export const GET   = aiChat.routes.userSettings.GET;
export const PATCH = aiChat.routes.userSettings.PATCH;

Option B — implement the endpoint yourself

The factory exists so hosts don't have to, but the wire contract above is stable. Hosts that want a different framework's idioms or extra fields on the response can omit setNarratorId (PATCH then returns 501) and ship their own handler against the same wire format. The chat UI will keep working.

Error taxonomy

Every error the package surfaces over the wire — both the custom SSE route (event: error) and the Vercel AI SDK route ({ type: 'error' } data part) — carries a stable code drawn from a fixed taxonomy. Persistence stores the same code on chat_messages.errorJson.code. End users never see the code itself; the UI maps it to friendly copy and the right action (Retry / Continue / Sign in / New chat / Dismiss).

| Code | Meaning | Auto-retry? | | --- | --- | --- | | network_offline | Client lost connectivity before send. | yes, on online event | | network_blip | Fetch threw (DNS / connection reset). | yes, exp backoff | | stream_interrupted | Server alive, stream reader threw mid-flight. | once, then Continue/Retry | | aborted_by_user | User clicked Stop. | no | | rate_limited | HTTP 429. Honors Retry-After. | yes | | quota_exhausted | 429 / 403 with RESOURCE_EXHAUSTED. | no | | model_unavailable | 503, ECONNRESET, ETIMEDOUT. | yes, exp backoff | | model_timeout | 504 / DEADLINE_EXCEEDED. | yes, once | | context_overflow | 400 + "prompt is too long" / context_length_exceeded. | no | | content_blocked | Safety filter / finishReason=SAFETY. | no | | auth_expired | HTTP 401. | no | | forbidden | HTTP 403 (non-quota). | no | | csrf_invalid | HTTP 419. | yes, once after token refresh | | bad_request | HTTP 400 (other). | no | | agent_no_present | Agent finished without calling present(). | no | | agent_turn_limit | Agent exceeded maxAgentSteps. | no | | narrator_failed | Prose narrator threw; structured blocks still rendered. | n/a (non-fatal) | | internal | Catch-all 5xx / unrecognised. | no |

The classifier and default copy live in @firstlovecenter/ai-chat/server (classifyError, friendlyMessage, FRIENDLY_COPY, ChatErrorCode, ChatError) and are re-exported from @firstlovecenter/ai-chat/ui so the wire format, persistence shape, and UI copy stay aligned.

Database footprint

See DATABASE.md for the full footprint: which three tables the package adds, what it explicitly does not add (no FKs to the host, no users table, no triggers/views), MySQL-only assumptions, the VARCHAR(30) user-id columns (intended for host CUIDs, opaque to the package), table-name configurability per ORM, and the known independence gaps for hosts whose schema doesn't match FLC's.