Nasaq
Components

CopilotProvider

Owns a copilot conversation: streams answers from your transport, stops, retries, keeps sessions, and lets any part of the app open the assistant with useCopilot().

PreviewOpen ↗

Code

import { type CopilotEvent, CopilotLauncher, CopilotProvider, type CopilotTransport } from "@fadymondy/nasaq/web";const transport: CopilotTransport = async function* ({ history, message, sessionId }, { signal }) {  const res = await fetch("/api/copilot", {    method: "POST",    body: JSON.stringify({ history, message, sessionId }),    signal,  });  // One JSON event per line, such as {"type":"delta","text":"Hi"}.  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();  let buffer = "";  for (;;) {    const { value, done } = await reader.read();    if (done) break;    buffer += value;    const lines = buffer.split("\n");    buffer = lines.pop() ?? "";    for (const line of lines) if (line.trim()) yield JSON.parse(line) as CopilotEvent;  }};export function Shell({ children }: { children: React.ReactNode }) {  return (    <CopilotProvider transport={transport} greeting="Hi! Ask me anything." dock={{ launcher: false }}>      <header>        <CopilotLauncher />      </header>      {children}    </CopilotProvider>  );}

AI Assistant · beta

Live examples and controls: CopilotProvider in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/copilot-provider.json

The state behind CopilotChat and CopilotDock. You give it a transport, a function that turns a request into a stream of events, and it does the rest:

  • appends the question and a streaming answer;
  • applies text, tool steps, sources, artifacts and follow-ups as they arrive;
  • stops on demand;
  • retries after an error;
  • loads and deletes saved sessions.

Any component below it can open the assistant with useCopilot().open(...).

When to use

  • An app-wide assistant whose answers stream from your backend (SSE, fetch streams, WebSockets).
  • Pages that need to open the assistant with a question or with context, such as "Ask about this order".

When not to use

  • A one-off chat with its own state: pass messages and onSend to CopilotChat yourself.
  • A single question box: use AskAI.

Import

import { CopilotProvider, CopilotLauncher, useCopilot } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { type CopilotEvent, CopilotLauncher, CopilotProvider, type CopilotTransport } from "@fadymondy/nasaq/web";

const transport: CopilotTransport = async function* ({ history, message, sessionId }, { signal }) {
  const res = await fetch("/api/copilot", {
    method: "POST",
    body: JSON.stringify({ history, message, sessionId }),
    signal,
  });
  // One JSON event per line, such as {"type":"delta","text":"Hi"}.
  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines) if (line.trim()) yield JSON.parse(line) as CopilotEvent;
  }
};

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <CopilotProvider transport={transport} greeting="Hi! Ask me anything." dock={{ launcher: false }}>
      <header>
        <CopilotLauncher />
      </header>
      {children}
    </CopilotProvider>
  );
}

Anywhere below:

const copilot = useCopilot();

<Button onClick={() => copilot.open({ message: "Summarise this order", autoSend: true, context: [{ id: order.id, label: order.number }] })}>
  Ask AI
</Button>;

Anatomy

CopilotProvider          context only, no DOM
├─ children              useCopilot() works here
└─ CopilotDock           when `dock` is set, wired to the state
CopilotLauncher          data-slot="copilot-launcher"  <button aria-expanded>

API

CopilotProvider

PropTypeDefaultDescription
transportCopilotTransportrequired(request, { signal }) => AsyncIterable<CopilotEvent>. Stop when signal aborts.
sessionsCopilotSessionStore{ list, load, remove? }. Turns on History; the list loads when the assistant first opens.
initialMessagesCopilotMessage[]
greetingstringAn assistant message at the start of every new chat.
defaultOpen / onOpenChangefalse
onFeedback(message, "up" | "down") => voidCalled when a rating is set.
onArtifactAction / onArtifactPicksend backBy default the choice is sent to the assistant as a hidden message with data: { type: "artifact-action" | "artifact-pick", ... }.
dockboolean | CopilotDockPropsMounts a CopilotDock wired to the state.

Events (CopilotEvent)

EventEffect
{ type: "delta", text }Appends text. ```artifact blocks become artifacts; a block still being written stays hidden.
{ type: "text", text }Replaces the text.
{ type: "step", step }Adds a tool step or updates the one with the same id.
{ type: "sources", sources } / { type: "followUps", followUps }
{ type: "artifact", artifact }Any JSON; validated and dropped when invalid.
{ type: "session", id, title? }The server saved the conversation.
{ type: "error", message? }Ends the answer with an error and a Retry button.
{ type: "done" }Ends the answer; running steps are marked done.

useCopilot()

Returns:

  • State: messages, streaming, isOpen.
  • Opening: open({ message?, autoSend?, context? }), close, toggle.
  • Conversation: send(text, meta?, { hidden?, data? }), stop, retry(answerId?), newChat, feedback.
  • Composer: draft / setDraft, context / setContext.
  • Sessions: sessionId, sessions, refreshSessions, loadSession, deleteSession.
  • Wiring: chatProps, which you spread on your own CopilotChat or CopilotDock.

useOptionalCopilot() returns null outside a provider.

CopilotLauncher

Button props plus:

PropTypeDefaultDescription
look"header" | "icon""header"
shortcutstring | false⌘J / Ctrl+J
openWithCopilotOpenOptions
labels

Pure helpers

The stream logic is exported for tests and custom state:

  • reduceStream(state, event) and startAnswer(id), which build up an answer;
  • finishAnswer(state), which ends it;
  • splitStreamingText(raw), which separates text from artifacts;
  • retryPoint(messages, answerId?), which finds the message a retry sends again;
  • describeInteraction(kind, detail), which writes the text of a hidden reply.

Accessibility

  • Answers stream into CopilotChat's log region. Errors appear as an alert with a Retry button.
  • Stop keeps what arrived and is not reported as an error.
  • CopilotLauncher reports aria-expanded and names the shortcut in its title.

RTL & i18n

The launcher label ships in English and Arabic. The text of hidden replies ([action] …, [picked] …) is for the model and is not shown.

Styling & tokens

The provider has no DOM. CopilotLauncher is a Button; style it with variant, size and className.

Do / Don't

  • Do abort the network request when signal aborts, so Stop really stops the server.
  • Do yield { type: "session" } once the server saves the conversation, so History and Retry use the right id.
  • Don't mount two providers for one assistant; open it from anywhere with useCopilot().

Lab

https://docs.nasaqui.com/?path=/docs/components-ai-assistant-copilot-provider--docs

On this page