Chat History — Quick Start
Give signed-in readers a chat page that remembers.
Since 1.14.0
Put two panels on your page: a list of the reader's past conversations, and the conversation itself. A signed-in reader asks a question, gets an answer with citations, and finds the thread again on the next visit.
This takes about fifteen minutes. At the end you have a working page.
Before you start
Get your publishable API key. Copy it from Overview in the Miso Dashboard. The SDK runs in the browser, so it takes the publishable key, never the secret key. See Authentication.
Put your content in Miso. Chat History works from your catalog. Upload your content first — see Integrating Your Data — and confirm the result in Dashboard ▸ Data Sets ▸ Catalog.
No content of your own yet? Load a real one in a few minutes. Example: Wikinews parses a free public archive of about 20,900 news articles and sends it to Miso, so you have something to ask questions about.
Ask Miso to turn on User History. Miso enables it per app. Until then, the
history endpoints return 403. See User History.
Add an endpoint that signs a JWT for the reader. History belongs to one reader, so the browser must prove who the reader is. Your server signs a short-lived JWT with your secret key and returns it to the page. See Create a browser JWT.
1. Install the SDK
As a node module
In your project directory, run:
npm install --save @miso.ai/client-sdk
Then import MisoClient from the SDK:
import MisoClient from "@miso.ai/client-sdk";
Using a script tag
<script async src="https://cdn.jsdelivr.net/npm/@miso.ai/client-sdk@latest/dist/umd/miso.min.js"></script>
The script tag loads the SDK asynchronously, so your code must wait for it. Use
the misocmd queue, which runs your function as soon as the SDK is ready:
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(() => {
const MisoClient = window.MisoClient;
// Your code goes here.
});
Beta. The history panels ship in version
1.14.0, which is in beta now. Until the stable release, write@betawhere the snippets above say@latest. For npm, install@miso.ai/client-sdk@beta.
2. Add the panels
<miso-history> is the list of threads. <miso-conversation> is the thread the
reader has open. Put the two side by side:
<div id="chat">
<miso-history class="miso-history">
<miso-new-thread></miso-new-thread>
<miso-threads></miso-threads>
</miso-history>
<miso-conversation class="miso-conversation">
<div class="miso-conversation__header" visible-when="nonempty">
<div class="miso-conversation__header-row">
<miso-title></miso-title>
<miso-rename></miso-rename>
<miso-delete></miso-delete>
<miso-subscription></miso-subscription>
</div>
</div>
<div class="miso-conversation__intro" visible-when="ready+empty">
What can I help with?
</div>
<miso-messages></miso-messages>
<miso-query visible-when="ready"></miso-query>
</miso-conversation>
</div>
| Element | What the reader sees |
|---|---|
<miso-new-thread> | The New chat button. |
<miso-threads> | The list of threads, newest first. Each item has a menu to rename, subscribe, or delete. |
<miso-title>, <miso-rename>, <miso-delete> | The title of the open thread, and its rename and delete buttons. |
<miso-subscription> | The toggle for Answer Updates on the open thread. |
<miso-messages> | The questions and answers of the open thread. |
<miso-query> | The box where the reader asks the next question. |
The class names miso-history and miso-conversation apply Miso's default
layout: each panel scrolls inside its own box, and the question box stays below
the messages. The items keep their styles without the class names, so omit them
when you write a layout of your own. The page decides the size and position of
each panel:
html, body {
height: 100%;
margin: 0;
}
#chat {
display: flex;
gap: 1rem;
height: 100%;
padding: 1rem;
box-sizing: border-box;
}
#chat miso-history {
flex: 0 0 20rem;
}
#chat miso-conversation {
flex: 1 1 auto;
}
3. Sign the reader in and start
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");
const workflow = client.ui.history;
// The same id that your server signs into the JWT.
client.context.user_id = "u-10293";
// Your endpoint. It returns a JWT for the signed-in reader.
async function fetchToken() {
const response = await fetch("/api/miso-token");
return (await response.json()).token;
}
// The styles load while your server signs the token.
const [token] = await Promise.all([fetchToken(), client.ui.ready]);
client.context.auth = `Bearer ${token}`;
workflow.start(); // load the thread list
});
client.ui.ready resolves when the styles are loaded. The token request runs
at the same time, so neither waits for the other.
Set client.context.user_id too, although the JWT already names the reader.
The JWT signs the reader in for the history requests. The SDK records
interactions, such as a click on a citation, with the id in the context, and
those do not read the JWT. Use the same id as the user_id claim of the JWT.
If the two are different, Miso returns 401.
The SDK sends client.context.auth as the Authorization header on every
request. start() loads the reader's threads. The conversation panel opens on a
new, empty thread and needs no call of its own.
4. Renew the token when it expires
A short-lived JWT expires while the page is open, and Miso then answers 401.
Set client.api.onError to get a new token and send the request again:
let renewing;
async function renewToken(hold) {
// New requests wait here until the new token is set.
const unhold = hold();
try {
client.context.auth = `Bearer ${await fetchToken()}`;
} finally {
unhold();
renewing = undefined;
}
}
client.api.onError = async (error, { hold, resend }) => {
// Any other error, or a second 401 for the same request: stop here.
if (error.status !== 401 || error.resent) {
throw error;
}
// Requests that fail together share one renewal.
await (renewing || (renewing = renewToken(hold)));
return resend();
};
hold() makes the SDK keep each new request until you call the function it
returns. Without it, a request that starts during the renewal goes out with the
expired token and fails too. Release the hold in finally, or the requests wait
forever when the renewal fails.
error.resent counts how many times the SDK sent this request again. The check
stops a loop when the new token is rejected too.
The complete page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Miso Chat History</title>
<script async src="https://cdn.jsdelivr.net/npm/@miso.ai/client-sdk@beta/dist/umd/miso.min.js"></script>
<style>
html, body {
height: 100%;
margin: 0;
}
#chat {
display: flex;
gap: 1rem;
height: 100%;
padding: 1rem;
box-sizing: border-box;
}
#chat miso-history {
flex: 0 0 20rem;
}
#chat miso-conversation {
flex: 1 1 auto;
}
</style>
</head>
<body>
<div id="chat">
<miso-history class="miso-history">
<miso-new-thread></miso-new-thread>
<miso-threads></miso-threads>
</miso-history>
<miso-conversation class="miso-conversation">
<div class="miso-conversation__header" visible-when="nonempty">
<div class="miso-conversation__header-row">
<miso-title></miso-title>
<miso-rename></miso-rename>
<miso-delete></miso-delete>
<miso-subscription></miso-subscription>
</div>
</div>
<div class="miso-conversation__intro" visible-when="ready+empty">
What can I help with?
</div>
<miso-messages></miso-messages>
<miso-query visible-when="ready"></miso-query>
</miso-conversation>
</div>
<script>
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");
const workflow = client.ui.history;
client.context.user_id = "u-10293";
async function fetchToken() {
const response = await fetch("/api/miso-token");
return (await response.json()).token;
}
const [token] = await Promise.all([fetchToken(), client.ui.ready]);
client.context.auth = `Bearer ${token}`;
let renewing;
async function renewToken(hold) {
const unhold = hold();
try {
client.context.auth = `Bearer ${await fetchToken()}`;
} finally {
unhold();
renewing = undefined;
}
}
client.api.onError = async (error, { hold, resend }) => {
if (error.status !== 401 || error.resent) {
throw error;
}
await (renewing || (renewing = renewToken(hold)));
return resend();
};
workflow.start();
});
</script>
</body>
</html>
Check it works
Open the page as a signed-in reader and ask a question. The answer streams into the conversation panel, and a new thread appears at the top of the list. Reload the page: the thread is still in the list, and a click opens it again.
| Symptom | Cause |
|---|---|
The list shows an error, and the requests return 403 | User History is off for this app. Ask Miso to enable it. |
| The list shows "Your session has expired" | The JWT is missing or expired, or it was signed with a different key. |
| The panels never render | The page loads a release older than 1.14.0, or your code ran before the SDK loaded. |
Next
- API Configuration — how many threads load at a time, and which articles the answers can cite.
- User History — the API behind the panels, for your own front end.
- Answer Updates — what the subscription toggle does, and how the unread indicators clear.
- JWT (user authentication) —
the claims Miso reads, and what each
401means.
