> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiql.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat

> Embed an AiQL Explore chat (inquiry) in an iframe.

export const AiqlEmbed = ({tool = "brainstorm", title}) => {
  const AIQL_BASE_URL = "https://app.aiql.io";
  const REFRESH_BUFFER_MS = 30000;
  const MIN_REFRESH_MS = 5000;
  const toolLabels = {
    brainstorm: "Canvas",
    analyze: "Dashboard",
    explore: "Chat",
    knowledge: "Knowledge Graph",
    "common-knowledge": "Common Knowledge",
    preview: "Source Preview",
    present: "Presentation"
  };
  const resourceKeys = {
    brainstorm: "canvasId",
    analyze: "dashboardId",
    explore: "chatId",
    knowledge: "documentId",
    "common-knowledge": null,
    preview: "documentId",
    present: "presentationId"
  };
  const resourceDescriptions = {
    brainstorm: "ID of the canvas to embed.",
    analyze: "ID of the dashboard to embed.",
    explore: "ID of the chat (inquiry) to embed.",
    knowledge: "ID of the document whose knowledge graph to embed.",
    preview: "ID of the source document to preview.",
    present: "ID of the presentation to embed."
  };
  const label = title || toolLabels[tool] || "AiQL embed";
  const resourceKey = resourceKeys[tool];
  const requiresResource = resourceKey != null;
  const resourceDescription = resourceDescriptions[tool] || "ID of the resource to embed.";
  const [apiKey, setApiKey] = useState("");
  const [workspaceId, setWorkspaceId] = useState("");
  const [resourceId, setResourceId] = useState("");
  const [theme, setTheme] = useState("auto");
  const [placeholders, setPlaceholders] = useState({
    apiKey: "aiql_demo_••••••••••••••••",
    workspaceId: "demo-workspace-id",
    resourceId: "demo-resource-id"
  });
  const [demoCache, setDemoCache] = useState(null);
  const [active, setActive] = useState(null);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState(null);
  const [hasSent, setHasSent] = useState(false);
  const [open, setOpen] = useState(false);
  const [panelTab, setPanelTab] = useState("preview");
  const resolveTheme = value => {
    if (value === "light" || value === "dark") return value;
    if (typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
      return "dark";
    }
    return "light";
  };
  const buildEmbedUrl = (ws, embedTool, rid, token, themeValue) => {
    const path = rid ? `${AIQL_BASE_URL}/embed/${ws}/${embedTool}/${rid}` : `${AIQL_BASE_URL}/embed/${ws}/${embedTool}`;
    const url = new URL(path);
    url.searchParams.set("token", token);
    url.searchParams.set("theme", resolveTheme(themeValue));
    return url.toString();
  };
  const msUntilRefresh = expiresAt => {
    if (!expiresAt) return null;
    return Math.max(new Date(expiresAt).getTime() - Date.now() - REFRESH_BUFFER_MS, MIN_REFRESH_MS);
  };
  const isCustom = apiKey.trim() !== "" || workspaceId.trim() !== "" || requiresResource && resourceId.trim() !== "";
  const customComplete = apiKey.trim() !== "" && workspaceId.trim() !== "" && (!requiresResource || resourceId.trim() !== "");
  const fetchDemo = async signal => {
    const res = await fetch(`${AIQL_BASE_URL}/api/embed/demo-token`, {
      method: "GET",
      headers: {
        Accept: "application/json"
      },
      cache: "no-store",
      signal
    });
    let data = {};
    try {
      data = await res.json();
    } catch {
      data = {};
    }
    if (!res.ok || !data.token || !data.workspaceId) {
      throw new Error(data.error || `Demo token request failed (${res.status}).`);
    }
    const demoResourceId = data.resources?.[tool] || "";
    const next = {
      token: data.token,
      expiresAt: data.expiresAt,
      workspaceId: data.workspaceId,
      resourceId: demoResourceId
    };
    setPlaceholders({
      apiKey: data.apiKeyPlaceholder || "aiql_demo_••••••••••••••••",
      workspaceId: data.workspaceId,
      resourceId: demoResourceId || "demo-resource-id"
    });
    setDemoCache(next);
    return next;
  };
  const fetchCustom = async signal => {
    const key = apiKey.trim();
    const ws = workspaceId.trim();
    const rid = requiresResource ? resourceId.trim() : "";
    const res = await fetch(`${AIQL_BASE_URL}/api/embed/tokens`, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${key}`
      },
      cache: "no-store",
      signal,
      body: JSON.stringify(rid ? {
        workspaceId: ws,
        resourceId: rid
      } : {
        workspaceId: ws
      })
    });
    let data = {};
    try {
      data = await res.json();
    } catch {
      data = {};
    }
    if (!res.ok || !data.token) {
      throw new Error(data.error || `Token request failed (${res.status}).`);
    }
    return {
      token: data.token,
      expiresAt: data.expiresAt,
      workspaceId: ws,
      resourceId: rid
    };
  };
  const run = async signal => {
    setOpen(true);
    setPanelTab("preview");
    setStatus("loading");
    setError(null);
    setHasSent(true);
    try {
      let next;
      if (isCustom) {
        if (!customComplete) {
          throw new Error(requiresResource ? "Enter API key, workspace ID, and resource ID — or leave all empty for the demo." : "Enter API key and workspace ID — or leave all empty for the demo.");
        }
        next = await fetchCustom(signal);
        if (signal?.aborted) return;
        setActive({
          source: "custom",
          ...next
        });
      } else {
        const cachedOk = demoCache?.token && demoCache?.expiresAt && (!requiresResource || demoCache?.resourceId) && new Date(demoCache.expiresAt).getTime() - Date.now() > REFRESH_BUFFER_MS;
        next = cachedOk ? demoCache : await fetchDemo(signal);
        if (signal?.aborted) return;
        if (requiresResource && !next.resourceId) {
          throw new Error("Demo resource is not configured for this tool.");
        }
        setActive({
          source: "demo",
          ...next
        });
      }
      setStatus("ready");
    } catch (err) {
      if (signal?.aborted) return;
      setStatus("error");
      setError(err instanceof Error ? err.message : "Could not load embed.");
      setPanelTab("values");
    }
  };
  useEffect(() => {
    return () => {
      setApiKey("");
    };
  }, []);
  useEffect(() => {
    if (!open || typeof document === "undefined") return;
    const onKeyDown = e => {
      if (e.key === "Escape") setOpen(false);
    };
    window.addEventListener("keydown", onKeyDown);
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const style = document.createElement("style");
    style.setAttribute("data-aiql-embed-modal", "");
    style.textContent = ".mdx-content{isolation:auto !important;}.mdx-content{contain:none !important;}";
    document.head.appendChild(style);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      document.body.style.overflow = previousOverflow;
      style.remove();
    };
  }, [open]);
  useEffect(() => {
    if (!hasSent || !active?.expiresAt) return;
    const delay = msUntilRefresh(active.expiresAt);
    if (delay == null) return;
    const timer = setTimeout(() => {
      const controller = new AbortController();
      void run(controller.signal);
    }, delay);
    return () => clearTimeout(timer);
  }, [hasSent, active?.expiresAt, active?.source, apiKey, workspaceId, resourceId]);
  const iframeSrc = active?.token && active?.workspaceId && (!requiresResource || active?.resourceId) ? buildEmbedUrl(active.workspaceId, tool, requiresResource ? active.resourceId : undefined, active.token, theme) : null;
  const sending = status === "loading";
  const sectionHeading = text => <div className="api-section-heading flex flex-col gap-y-4 w-full mt-6 first:mt-0">
      <div className="flex items-baseline border-b pb-2.5 border-gray-100 dark:border-gray-800 w-full">
        <h4 className="api-section-heading-title flex-1 mb-0">{text}</h4>
      </div>
    </div>;
  const typeBadge = type => <div className="flex items-center gap-1 whitespace-nowrap rounded-md bg-stone-100/50 px-2 py-0.5 font-medium text-stone-600 dark:bg-white/5 dark:text-stone-200">
      <span>{type}</span>
    </div>;
  const requiredBadge = <div className="whitespace-nowrap rounded-md bg-red-100/50 px-2 py-0.5 font-medium text-red-600 dark:bg-red-400/10 dark:text-red-300">
      required
    </div>;
  const fieldRow = opts => <div className="primitive-param-field border-gray-100 dark:border-gray-800 border-b last:border-b-0">
      <div className="flex-1 grid lg:grid-cols-2 gap-x-12 gap-y-4 py-5 max-w-full">
        <div className="flex font-mono text-sm group/param-head param-head break-all relative">
          <div className="flex-1 flex flex-col content-start py-0.5 mr-5">
            <div className="flex items-center flex-wrap gap-x-2 gap-y-1">
              <div className="font-semibold text-primary dark:text-primary-light overflow-wrap-anywhere">
                {opts.name}
              </div>
              <div className="flex items-center gap-2 text-xs font-medium whitespace-nowrap">
                {typeBadge(opts.type)}
                {opts.required ? requiredBadge : null}
              </div>
            </div>
            <div className="mt-2 prose-sm prose-gray dark:prose-invert">
              <p className="text-sm text-gray-600 dark:text-gray-400 m-0">
                {opts.description}
              </p>
            </div>
          </div>
        </div>
        <div className="grid grid-cols-1 w-full items-start divide-y divide-gray-50 dark:divide-white/5">
          <div className="relative flex flex-1 items-center">
            {opts.bearerPrefix ? <div className="absolute left-2.5 top-0 bottom-0 flex items-center justify-center text-sm text-gray-800 dark:text-gray-100">
                Bearer{" "}
              </div> : null}
            {opts.options ? <select value={opts.value} aria-label={opts.name} onChange={opts.onChange} className="flex-1 min-w-0 bg-transparent outline-0 text-playground-input cursor-pointer">
                {opts.options.map(option => <option key={option.value} value={option.value}>
                    {option.label}
                  </option>)}
              </select> : <input type={opts.secret ? "password" : "text"} autoComplete="off" spellCheck={false} value={opts.value} placeholder={opts.placeholder} aria-label={opts.placeholder} onChange={opts.onChange} className="flex-1 min-w-0 bg-transparent outline-0 text-playground-input" style={opts.bearerPrefix ? {
    paddingLeft: "3.64rem"
  } : undefined} />}
          </div>
        </div>
      </div>
    </div>;
  const previewButton = buttonLabel => <button type="button" aria-label={buttonLabel} disabled={sending} onClick={() => void run()} className="tryit-button flex items-center justify-center px-3 h-9 font-medium rounded-xl mouse-pointer hover:opacity-80 gap-1.5 print:hidden bg-[#2AB673] text-[#FFFFFF] disabled:opacity-60 shrink-0">
      <span>{sending ? "Loading…" : buttonLabel}</span>
      <svg className="icon inline w-3 h-3" viewBox="0 0 448 512" fill="currentColor" aria-hidden="true">
        <path d="M91.2 36.9c-12.4-6.8-27.4-6.5-39.6 .7S32 57.9 32 72l0 368c0 14.1 7.5 27.2 19.6 34.4s27.2 7.5 39.6 .7l336-184c12.8-7 20.8-20.5 20.8-35.1s-8-28.1-20.8-35.1l-336-184z" />
      </svg>
    </button>;
  const fieldsPanel = <div className="flex flex-col">
      <p className="block text-sm text-gray-500 dark:text-gray-400 mb-6">
        Leave fields empty to use the shared demo. Your API key stays in memory
        only and is never stored.
      </p>

      <div className="api-section">
        {sectionHeading("Authorizations")}
        {fieldRow({
    name: "Authorization",
    type: "string<bearer>",
    required: true,
    description: "API key passed as a Bearer token. Leave empty to use the demo.",
    value: apiKey,
    placeholder: placeholders.apiKey,
    secret: true,
    bearerPrefix: true,
    onChange: e => setApiKey(e.target.value)
  })}
      </div>

      <div className="api-section">
        {sectionHeading("Properties")}
        {fieldRow({
    name: "workspaceId",
    type: "string",
    required: true,
    description: "ID of the workspace the embed is scoped to.",
    value: workspaceId,
    placeholder: placeholders.workspaceId,
    onChange: e => setWorkspaceId(e.target.value)
  })}

        {requiresResource ? fieldRow({
    name: resourceKey,
    type: "string",
    required: true,
    description: resourceDescription,
    value: resourceId,
    placeholder: placeholders.resourceId,
    onChange: e => setResourceId(e.target.value)
  }) : null}

        {fieldRow({
    name: "theme",
    type: "enum<string>",
    required: false,
    description: "Embed color scheme. auto follows the visitor system preference.",
    value: theme,
    options: [{
      value: "auto",
      label: "auto"
    }, {
      value: "light",
      label: "light"
    }, {
      value: "dark",
      label: "dark"
    }],
    onChange: e => setTheme(e.target.value)
  })}
      </div>
    </div>;
  const responsePanel = <div className="flex flex-col">
      {error ? <p className="mb-3 text-sm text-red-600 dark:text-red-400" role="alert">
          {error}
        </p> : null}

      {iframeSrc ? <div className="flex flex-col overflow-hidden rounded-xl border-standard">
          <div className="flex items-center justify-between px-3 py-2 border-b border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-white/5">
            <span className="text-xs font-medium text-gray-500 dark:text-gray-400">
              Response
            </span>
            <span className="text-xs font-mono text-gray-400 dark:text-gray-500">
              {active?.source === "custom" ? "your environment" : "demo"}
            </span>
          </div>
          <iframe src={iframeSrc} title={label} className="w-full h-[calc(100dvh-16rem)] min-h-[420px] border-0 block bg-white dark:bg-zinc-950" allow="clipboard-write" />
        </div> : <div className="flex h-[calc(100dvh-16rem)] min-h-[420px] items-center justify-center rounded-xl border border-dashed border-gray-200 dark:border-white/10 text-sm text-gray-400 dark:text-gray-500">
          {sending ? "Minting embed token…" : "Fill in credentials or leave empty for the demo, then run the preview."}
        </div>}
    </div>;
  return <>
      <div className="not-prose my-6 flex items-center justify-between gap-3 bg-background-light dark:bg-background-dark border-standard rounded-2xl p-1.5">
        <div className="px-2.5 py-1.5 text-sm font-semibold text-gray-950 dark:text-white truncate">
          {label}
        </div>
        {previewButton("Preview")}
      </div>

      {open ? <div className="z-[2147483000] fixed inset-0 overflow-y-auto overscroll-contain p-4 sm:p-6 md:p-12" role="dialog" aria-modal="true" aria-label={`${label} preview`} onClick={() => setOpen(false)}>
            <div className="fixed z-0 inset-0 bg-background-dark/20 transition-opacity backdrop-blur-sm" aria-hidden="true" />
            <div className="not-prose relative mx-auto w-full max-w-[104rem] max-h-[calc(100dvh-2rem)] sm:max-h-[calc(100dvh-3rem)] md:max-h-[calc(100dvh-6rem)] overflow-y-auto overscroll-contain bg-background-light dark:bg-background-dark shadow-search rounded-3xl border-standard" onClick={e => e.stopPropagation()}>
              <div className="sticky top-0 z-50 bg-background-light dark:bg-background-dark px-4 pt-4 pb-2">
                <div className="flex items-center justify-between gap-x-2">
                  <div className="flex items-center gap-x-2 border-standard rounded-xl p-1.5 pr-3 min-w-0">
                    <div className="method-pill rounded-lg font-bold px-1.5 py-0.5 text-sm leading-5 bg-[#2AB673] text-[#FFFFFF]">
                      EMBED
                    </div>
                    <div className="flex-1 text-left text-sm font-medium text-gray-900 dark:text-white truncate">
                      {label}
                    </div>
                  </div>
                  <div className="flex items-center gap-2 shrink-0">
                    {previewButton("Send")}
                    <button type="button" aria-label="Close preview" onClick={() => setOpen(false)} className="flex h-9 w-9 items-center justify-center rounded-xl text-gray-500 hover:text-gray-950 dark:text-gray-400 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5">
                      <svg width="16" height="16" viewBox="0 0 384 512" fill="currentColor" aria-hidden="true">
                        <path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z" />
                      </svg>
                    </button>
                  </div>
                </div>

                <div className="mt-3 flex items-center gap-1 border-b border-gray-100 dark:border-gray-800" role="tablist" aria-label="Embed playground panels">
                  {[{
    id: "preview",
    label: "Preview"
  }, {
    id: "values",
    label: "Values"
  }].map(tab => {
    const selected = panelTab === tab.id;
    return <button key={tab.id} type="button" role="tab" aria-selected={selected} id={`aiql-embed-tab-${tab.id}`} aria-controls={`aiql-embed-panel-${tab.id}`} onClick={() => setPanelTab(tab.id)} className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${selected ? "border-[#2AB673] text-gray-950 dark:text-white" : "border-transparent text-gray-500 hover:text-gray-950 dark:text-gray-400 dark:hover:text-white"}`}>
                        {tab.label}
                      </button>;
  })}
                </div>
              </div>

              <div className="px-4 pb-4 pt-3">
                <div id="aiql-embed-panel-preview" role="tabpanel" aria-labelledby="aiql-embed-tab-preview" hidden={panelTab !== "preview"}>
                  {responsePanel}
                </div>
                <div id="aiql-embed-panel-values" role="tabpanel" aria-labelledby="aiql-embed-tab-values" hidden={panelTab !== "values"}>
                  {fieldsPanel}
                </div>
              </div>
            </div>
          </div> : null}
    </>;
};

`Chat` renders an Explore inquiry (`tool="explore"`). It reads `token` and `theme` from `AiqlProvider`. Pass the inquiry id and presentation props.

## Try it live

<AiqlEmbed tool="explore" title="Chat" />

## Import

```tsx theme={null}
import { Chat } from "@aiql.io/react";
```

## Usage

### Existing inquiry

```tsx theme={null}
import { AiqlProvider, Chat, useToken } from "@aiql.io/react";

function Explore({ inquiryId }: { inquiryId: string }) {
  const { token, status } = useToken();
  if (status !== "ready" || !token) return null;

  return (
    <AiqlProvider token={token}>
      <Chat inquiryId={inquiryId} title="Explore" />
    </AiqlProvider>
  );
}
```

### New conversation

Pass a freshly generated id. The inquiry is created on the first message.

```tsx theme={null}
function NewChat() {
  const { token, status } = useToken();
  const [inquiryId] = useState(() => crypto.randomUUID());
  if (status !== "ready" || !token) return null;

  return (
    <AiqlProvider token={token}>
      <Chat inquiryId={inquiryId} title="Explore" />
    </AiqlProvider>
  );
}
```

### Handle artifact opens

Chat can create and show canvas, dashboard, and presentation artifact cards. When the user clicks one inside the embed, the iframe emits `ARTIFACT_OPEN` instead of navigating away. Handle it on the provider (recommended) or on `Chat` itself:

```tsx theme={null}
<AiqlProvider
  token={token}
  onArtifactOpen={(event) => {
    if (event.type === "canvas") router.push(`/brainstorm/${event.id}`);
    if (event.type === "dashboard") router.push(`/analyze/${event.id}`);
    if (event.type === "presentation") router.push(`/present/${event.id}`);
  }}
>
  <Chat inquiryId={inquiryId} />
</AiqlProvider>
```

See [`AiqlProvider`](/sdks/react/components/provider#artifact-open-events) and [`useEmbedNavigator`](/sdks/react/hooks/use-embed-navigator).

### Custom loading and error UI

```tsx theme={null}
<Chat
  inquiryId={inquiryId}
  renderLoading={() => <p>Opening chat…</p>}
  renderError={(message) => <p role="alert">{message}</p>}
  onLoad={() => console.log("iframe loaded")}
  onError={(message) => console.error(message)}
/>
```

### Styling

```tsx theme={null}
<Chat
  inquiryId={inquiryId}
  className="my-chat"
  style={{ minHeight: 720, borderRadius: 16 }}
/>
```

## API Reference

### Chat props

| Prop             | Type                                 | Default | Description                                                                                               |
| ---------------- | ------------------------------------ | ------- | --------------------------------------------------------------------------------------------------------- |
| `inquiryId`      | `string`                             | —       | Required. Existing inquiry id, or a new id you generate to start a conversation.                          |
| `title`          | `string`                             | —       | iframe `title` attribute.                                                                                 |
| `className`      | `string`                             | —       | Class name on the iframe.                                                                                 |
| `style`          | `CSSProperties`                      | —       | Inline styles on the iframe (merged after the reset).                                                     |
| `onArtifactOpen` | `(event: ArtifactOpenEvent) => void` | —       | Called when the user opens an artifact card in this chat. Overrides the provider handler for this iframe. |
| `onLoad`         | `() => void`                         | —       | Called when the iframe fires `load`.                                                                      |
| `onError`        | `(error: string) => void`            | —       | Called when the embed URL cannot be built or the token is missing.                                        |
| `renderLoading`  | `() => ReactNode`                    | —       | Custom loading UI.                                                                                        |
| `renderError`    | `(error: string) => ReactNode`       | —       | Custom error UI.                                                                                          |

`token` and `theme` come from [`AiqlProvider`](/sdks/react/components/provider). They are not Chat props.
