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

wafer-ui

v1.0.2

Published

Agentic UI toolkit for React — single-package install.

Readme

wafer-ui

Agentic UI toolkit for React — single-package install. Add a fully wired AI agent to your React app in minutes, with any LLM backend. Local Ollama today, Claude tomorrow, LangGraph next week — the same few lines of React code work with all of them.

What's inside

| Package (bundled in) | What it does | | --- | --- | | @wafer/core | Agent client and state machine. Tracks messages, tool calls, runs, and approvals. | | @wafer/react | React provider and hooks — useThread, useComposer, useRunState, useApprovals. | | @wafer/ui | Pre-built components — AgentThread, Composer, RunTimeline, ApprovalPanel, ToolCallCard. | | @wafer/adapters | Ready-made transports for Ollama and Groq. Add any other backend in ~50 lines. |

The core idea is the AgentTransport interface — a two-method contract that separates your UI from your LLM backend. Swap the transport and the rest of the app is untouched.

Installation

npm install wafer-ui
# or
pnpm add wafer-ui
# or
yarn add wafer-ui

Prerequisites

  • React 18 or 19 — hooks and useSyncExternalStore are required.
  • Tailwind CSS v4 — required peer dependency.

Wafer ships pre-built JS, so Tailwind won't see the component class names unless you point it at the dist files. Add this to your global CSS:

/* In your global CSS file (e.g. src/styles.css) */
@source "../../node_modules/wafer-ui/dist/**/*.js";

Without this, Tailwind's production build purges the component styles and the UI renders unstyled.

Quick start

Both options below use createOllamaTransport to talk to a local Ollama instance. Swap in any other transport and nothing else changes.

Option A — use hooks, wire your own UI

import { createAgentClient, AgentProvider, useThread, useComposer } from "wafer-ui";
import { createOllamaTransport } from "wafer-ui/adapters/ollama";

// Create the client once — outside the component
const client = createAgentClient({
  transport: createOllamaTransport({ model: "llama3.2" })
});

function Chat() {
  const { messages } = useThread();
  const { input, setInput, submit, isRunning } = useComposer();

  return (
    <div>
      {messages.map(msg => (
        <p key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </p>
      ))}

      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Ask anything…"
      />
      <button onClick={() => submit()} disabled={isRunning}>
        {isRunning ? "Thinking…" : "Send"}
      </button>
    </div>
  );
}

export default function App() {
  return (
    <AgentProvider client={client}>
      <Chat />
    </AgentProvider>
  );
}

Option B — drop in the pre-built components

import { createAgentClient, AgentProvider, AgentThread, Composer } from "wafer-ui";
import { createOllamaTransport } from "wafer-ui/adapters/ollama";

const client = createAgentClient({
  transport: createOllamaTransport({ model: "llama3.2" })
});

export default function App() {
  return (
    <AgentProvider client={client}>
      <AgentThread title="My Agent" />
      <Composer placeholder="Ask anything…" />
    </AgentProvider>
  );
}

Local LLM via Ollama (free, no API key)

  1. Install from ollama.com. Ollama runs a local server at http://localhost:11434.

  2. Pull a model:

    ollama pull llama3.2          # general purpose — good starting point
    ollama pull qwen2.5:7b        # great tool-calling model
    ollama pull llama3.1:8b       # smarter, still runs on most laptops
    ollama pull mistral           # fast and capable
  3. Create the transport:

    import { createOllamaTransport } from "wafer-ui/adapters/ollama";
    
    const transport = createOllamaTransport({
      model: "llama3.2",                        // required
      baseUrl: "http://localhost:11434",         // default — Ollama's port
      systemPrompt: "You are a helpful assistant.",
      maxToolRounds: 6,                          // max agent loop iterations
      think: "low",                              // thinking mode: true | "low" | "medium" | "high"
      requestOptions: {                          // passed directly to Ollama
        temperature: 0.7,
        num_predict: 1024
      }
    });
  4. Add tools (optional). Define each tool with a JSON Schema and an execute function that runs when the model calls it:

    import { createOllamaTransport } from "wafer-ui/adapters/ollama";
    
    const transport = createOllamaTransport({
      model: "qwen2.5:7b",
      systemPrompt: "You are a helpful assistant. Always call a tool when relevant.",
      tools: [
        {
          function: {
            name: "get_weather",
            description: "Get the current weather for a city.",
            parameters: {
              type: "object",
              required: ["city"],
              properties: {
                city: { type: "string", description: "The city name, e.g. 'London'" }
              }
            }
          },
          // execute() receives the parsed arguments and must return a value
          execute: async (args) => {
            const response = await fetch(`/api/weather?city=${args.city}`);
            const data = await response.json();
            return { temperature: data.temp, condition: data.sky };
          }
        }
      ]
    });

All createOllamaTransport options

| Option | Default | Description | | --- | --- | --- | | model | required | Ollama model name, e.g. llama3.2 | | baseUrl | http://localhost:11434 | Ollama server URL | | systemPrompt | — | System message prepended to every request | | tools | [] | Tool definitions with execute() callbacks | | maxToolRounds | 6 | Max agent-loop iterations before stopping | | think | — | Enable thinking: true \| "low" \| "medium" \| "high" | | requestOptions | — | Raw Ollama options (temperature, num_predict, …) | | forceToolCallRetryCount | 0 | Retries if model skips a required tool call |

Groq (free tier, cloud, no GPU)

  1. Sign up at console.groq.com — no credit card required.

  2. Never put an API key in the browser — create a server-side proxy:

    // api/chat.ts  (Vercel Edge Function)
    export const config = { runtime: "edge" };
    
    export default async function handler(req: Request): Promise<Response> {
      const body = await req.text();
    
      const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          // GROQ_API_KEY lives in your host's env vars — never in the browser
          Authorization: `Bearer ${process.env.GROQ_API_KEY}`
        },
        body
      });
    
      return new Response(res.body, {
        status: res.status,
        headers: { "Content-Type": res.headers.get("Content-Type") ?? "application/json" }
      });
    }
  3. Create the transport:

    import { createGroqTransport } from "wafer-ui/adapters/groq";
    
    const transport = createGroqTransport({
      model: "llama-3.3-70b-versatile", // default — very capable, great tool calling
      endpoint: "/api/chat",             // your proxy URL
      systemPrompt: "You are a helpful assistant.",
      maxToolRounds: 6
    });

Free models on Groq

| Model | Notes | | --- | --- | | llama-3.3-70b-versatile | Very capable, best for tool calling | | llama-3.1-8b-instant | Ultra-fast for simple tasks | | gemma2-9b-it | Google's Gemma — fast and capable | | moonshotai/kimi-k2-instruct | Strong reasoning, complex tasks |

Any other backend (Claude, OpenAI, LangGraph, Mastra, custom)

AgentTransport is a plain two-method interface, so any backend can plug in:

import type { AgentTransport } from "wafer-ui";

const myTransport: AgentTransport = {
  async sendUserMessage(input, emit) {
    // input.threadId   — stable ID for this conversation
    // input.runId      — unique ID for this specific user message + response
    // input.text       — the user's raw message text
    // input.history    — full message history: Array<{ role, content }>

    // Call emit() to push events into Wafer's state machine.
    // Everything you emit becomes visible via hooks and components.

    // ... your LLM call here ...
  },

  // Optional — only needed if your agent asks for human approval
  async submitApproval(input, emit) {
    emit({
      type: "approval.resolved",
      threadId: input.threadId, runId: input.runId,
      approvalId: input.approvalId, decision: input.decision,
      createdAt: new Date().toISOString()
    });
  }
};

Minimum viable transport — emit three events per response:

// ── Message events ────────────────────────────────────────────────────────
emit({ type: "message.created", threadId, messageId, role: "assistant", runId, content: "", createdAt });
emit({ type: "message.delta",   threadId, messageId, runId, delta: "Hello", createdAt });

// ── Tool events ───────────────────────────────────────────────────────────
emit({ type: "tool.called",     threadId, runId, toolCallId, toolName: "search", input: { query: "…" }, createdAt });
emit({ type: "tool.completed",  threadId, runId, toolCallId, output: { results: [] }, createdAt });
emit({ type: "tool.failed",     threadId, runId, toolCallId, error: "Timeout", createdAt });

// ── Approval events ───────────────────────────────────────────────────────
emit({ type: "approval.requested", threadId, runId, approvalId, actionLabel: "Send email", createdAt });

// ── Run lifecycle ─────────────────────────────────────────────────────────
emit({ type: "run.completed", threadId, runId, createdAt });
emit({ type: "run.failed",    threadId, runId, error: "Something broke", createdAt });
  1. message.created — before you start streaming
  2. message.delta — once per chunk of text
  3. run.completed — when the response is done

Tool, approval, and run-failure events are optional — add them as your agent grows more sophisticated.

License

MIT