Askwell documentation
How to connect a model, change the look, and use the chat app, the widget and the other pages.
Quick start
- Unzip the download and open
index.htmlin your browser. The chat works straight away in demo mode: answers are scripted samples, so you can click through everything without a server or an API key. - Look at the other pages from the strip on the left: prompt library, playground, agent runs and components.
- When you are ready for real answers, follow Connect a model. It is two lines in one file, plus a small server that keeps your API key secret.
- Set your brand colour in one place (see Colours, themes, fonts).
There is no build step and no framework. The kit is plain HTML, one CSS file and a handful of small JavaScript files with comments.
Askwell is an interface kit. It does not include an AI model, a database or user accounts. You connect it to the model provider and backend you already use.
Files
Askwell/
index.html the chat app
prompts.html prompt library
playground.html test a prompt on one or two models
agents.html agent runs: steps, log, result
components.html every interface part on one page
login.html sign in, create account, reset password
share.html read-only copy of one conversation
widget.html the chat widget on a sample website
documentation.html this guide
assets/
css/askwell.css all styles, colour tokens at the top
js/config.js YOUR SETTINGS (start here)
js/askwell.core.js storage, theme, menus, dialogs, toasts, Settings
js/askwell.markdown.js safe Markdown renderer and code colouring
js/askwell.adapters.js where answers come from: demo, openai, text
js/askwell.chat.js the chat app
js/askwell.prompts.js saved prompts and the "/" picker
js/askwell.voice.js dictation button
js/askwell.playground.js playground page
js/askwell.agents.js agent runs page
js/askwell.auth.js sign-in form checks
js/askwell.share.js read-only page
js/askwell.components.js samples on the components page only
js/askwell.icons.js the icon set
js/sample-chats.js sample conversations for the demo (safe to delete)
widget/
askwell-widget.js the widget in ONE file, ready to embed
src/widget.js, src/widget.css, build.mjs widget source and its small build script
server/
node-proxy.mjs small proxy server for Node
cloudflare-worker.js the same proxy as a Cloudflare Worker
README.md
Scripts are loaded in this order at the bottom of each page: config.js, askwell.icons.js, askwell.core.js, askwell.markdown.js, askwell.adapters.js, then the script for that page.
Connect a model
Your API key must stay on a server. If you put it in browser code, anyone can read it. So the chat talks to your small server (a proxy), and the proxy adds the key and passes the answer back as it arrives.
1. Start the proxy
Two ready-made proxies are in the server folder. Both expect a provider that uses the OpenAI chat-completions format (many providers and local model servers do).
Node (version 18 or newer, nothing to install):
API_KEY=your-secret-key \
UPSTREAM=https://your-provider.example/v1/chat/completions \
MODEL_MAP='{"swift":"small-model","sage":"large-model","scribe":"large-model"}' \
node server/node-proxy.mjs
Cloudflare Worker: create a Worker, paste server/cloudflare-worker.js, and add UPSTREAM, API_KEY (as a secret) and MODEL_MAP in its settings.
MODEL_MAP turns the model ids in the picker (swift, sage, scribe) into your provider's real model names. You can also rename the ids in config.js to the real names and skip the map.
2. Point the kit at it
In assets/js/config.js change two lines:
adapter: 'openai',
endpoint: 'http://localhost:8787/api/chat', // or your Worker address
Reload the page and send a message. The answer now comes from your model, word by word.
What the browser sends
POST /api/chat
{ "model": "swift", "stream": true, "temperature": 0.7,
"messages": [ { "role": "system", "content": "..." },
{ "role": "user", "content": "Hello" } ] }
The proxies only pass on model, messages, temperature and stream. Everything else is dropped, roles other than system, user and assistant are removed, and very large requests are refused. Provider error details are logged on the server and not shown to visitors.
Before you go live
- Set
ALLOW_ORIGINto your site's address, so other sites cannot use your proxy. - Put the proxy behind your own sign-in if the chat is not public. The proxies do not check who is calling.
- The Node proxy limits each IP address to 30 requests per 5 minutes (
RATE_LIMIT). For the Worker, add a rate limiting rule in Cloudflare.
How this was tested: both proxies and the openai and text adapters were tested against a local mock server that speaks the OpenAI streaming format (streaming, stopping, errors, one-piece answers). They were not tested against every provider. If yours uses a different format, write a small adapter (next section) or change the proxy.
Other formats
If your server simply streams the answer as plain text, use adapter: 'text'. It sends the same request and shows whatever text comes back.
Write your own adapter
An adapter is one function that yields pieces of the answer. Register it and put its name in config.js.
Askwell.adapters.mine = {
stream: async function* (request) {
// request = { messages, model, system, temperature, tools: { search }, files, signal }
const res = await fetch('/my/endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: request.messages, model: request.model }),
signal: request.signal // makes the Stop button work
});
if (!res.ok) throw new Error('The server answered ' + res.status);
const reader = res.body.getReader(), decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true }); // a piece of answer text
}
}
};
// config.js: adapter: 'mine'
Load your adapter file after askwell.adapters.js.
Steps and sources
Besides text, an adapter can report what the assistant is doing and which sources it used. The chat shows these as the steps box and the source chips.
yield { type: 'step', id: 'search', label: 'Searching the web', status: 'running' };
// later, same id: the row is updated
yield { type: 'step', id: 'search', label: 'Searched the web', detail: '3 results', status: 'done' };
yield { type: 'sources', sources: [{ title: 'Page title', url: 'https://example.com/page' }] };
yield 'According to the first source [1], ...'; // [1] becomes a link to source 1
status is running, done, error or pending. To show an error instead of an answer, throw new Error('Clear message for the person').
request.tools.search is true when the Search switch in the message box is on. request.files holds the attached File objects, for you to upload.
Settings (config.js)
| Setting | What it does |
|---|---|
appName, assistantName | Names shown in the browser tab and above each answer. |
user | Name, email and plan shown at the bottom of the sidebar. Fill it from your own session. |
adapter | 'demo', 'openai', 'text' or the name of your own adapter. |
endpoint, headers | Address of your server and extra headers to send (for example a session token). |
models, defaultModel | The model picker. id is sent to your server, name and note are shown. |
systemPrompt | Default instructions. People can change them in Settings. |
usage | { limit: 40 } shows the small meter in the sidebar. It counts messages sent today in this browser. Remove the line to hide it. |
seed | true fills the app with sample conversations on first load. Set false for a real product. |
images | false (default) shows images in answers as links. See Markdown and safety. |
suggestions | The cards on the empty screen: icon, title, hint, prompt. |
Colours, themes, fonts
All colours are CSS variables at the top of assets/css/askwell.css. The brand colour is three variables that belong together:
:root {
--accent: #ffc531; /* fill: send button, primary button, highlights */
--accent-ink: #1b1500; /* text and icons on top of the fill */
--accent-text: #7a5200; /* the same hue as readable text on a light surface */
}
[data-theme="dark"] { --accent-text: #ffd56b; } /* readable on a dark surface */
Check that --accent-ink on --accent, and --accent-text on white, are easy to read (a contrast of 4.5 to 1 or more).
Accent presets
Five accents are built in: marigold (default), cobalt, jade, rose and violet. People can pick one in Settings. To start with another one, add data-accent="cobalt" to the <html> tag. To remove the picker, delete the "Accent colour" row in askwell.core.js.
Light and dark
Every page starts in the theme named by data-theme-default on <html> (light, dark or system). The choice a person makes is remembered in their browser. A small script in the page head applies it before the page is drawn, so there is no flash.
Fonts
The kit loads three fonts from Google Fonts: Schibsted Grotesk (interface), IBM Plex Mono (code) and Source Serif 4 (optional answer typeface). Change them in the <link> tag of each page and in --font-ui, --font-mono and --font-serif. To avoid Google Fonts, host the font files yourself or use the system fonts that are already listed as fallbacks.
Reading width
--thread sets the width of the conversation column (780px).
The chat app
- Conversations in the sidebar, grouped by date, with search, rename, pin, delete, and export as Markdown or JSON.
- Answers arrive word by word with a Stop button. If the person scrolls up while an answer arrives, the view stops following and a "Latest" button appears.
- Each answer can be copied, rated up or down, or asked again. Asking again keeps the earlier versions; arrows switch between them.
- Each question can be edited. Saving sends it again and removes the answers that came after it.
- Message box: grows with the text, attaches files (button, drag and drop, or paste an image), has a Search switch, a dictation button, and a "/" picker for saved prompts.
- Model picker per conversation, and a Settings dialog (theme, accent, answer typeface, default model, instructions, temperature, data, shortcuts).
Keyboard
| Enter | Send. Shift + Enter adds a line. (Can be switched in Settings.) |
| Ctrl/⌘ + K | Search conversations |
| Alt + N | New conversation |
| / | Open the saved prompts (when the message box is empty) |
| Esc | Stop the answer, or close a menu or dialog |
| Shift + Esc | Jump to the message box |
From your own code
Askwell.chat.send('Summarise this page'); // send a message
Askwell.chat.stop(); // stop the answer
Askwell.chat.newChat();
Askwell.chat.open(id); Askwell.chat.list(); Askwell.chat.current();
document.addEventListener('askwell:feedback', (e) => {
// e.detail = { chatId, messageId, value: 'up' | 'down' | null }
});
// also: askwell:send, askwell:done, askwell:error
Files
Attached files stay in the browser. The kit shows them as chips and passes the File objects to your adapter as request.files. Uploading them, or sending images to a model that reads pictures, is up to your adapter. The limits (5 files, 10 MB each) are at the top of askwell.chat.js.
Dictation
The microphone button uses the browser's own speech recognition. It appears in browsers that have it (Chrome, Edge, Safari) and stays hidden in those that do not (Firefox). Some browsers send the audio to their own speech service.
Markdown and safety
Askwell.markdown(text) turns model output into HTML. It is written for untrusted text:
- All HTML in the text is escaped. A model (or a person pasting text) cannot inject tags or scripts.
- Links are limited to
http,https,mailto,teland same-page links. Anything else (for examplejavascript:) is shown as plain text. Web links open in a new tab withrel="noopener noreferrer". - Images are off by default and shown as links. A remote image address in an answer can be used to send data out of the page, so only turn on
images: trueif you trust the model's input.
Supported: headings, bold, italic, strikethrough, inline code, links, bullet and numbered lists (nested), task lists, quotes, tables with alignment, rules, fenced code blocks, and [1]-style citations when sources are given. It also works on half-finished text, which is why it can run on every chunk while an answer arrives.
Code blocks get a copy button and colouring for JavaScript, TypeScript, Python, JSON, HTML, CSS, SQL and shell (plus a general mode for C-like languages). Other languages are shown without colours.
element.innerHTML = Askwell.markdown(text, { sources: [{ title, url }], images: false });
Askwell.highlight('const a = 1;', 'js'); // colours one code string
The renderer was tested with 81 cases and 4,000 random inputs built from HTML, script and link fragments. It is small and strict, not a full CommonMark implementation: footnotes, raw HTML and maths are not supported.
Saved prompts
The prompt library holds 14 starter prompts. People can add, edit, duplicate, favourite and delete prompts. A word in double curly braces, like {{tone}}, is a variable: before the prompt is used, a small form asks for each one and shows a live preview.
The same prompts appear in the chat when you type / in an empty message box.
Askwell.prompts.all();
Askwell.prompts.save({ title: 'Translate', category: 'Writing', text: 'Translate to {{language}}:\n\n{{text}}' });
Askwell.prompts.remove(id);
Askwell.prompts.fill(prompt).then((text) => { /* null if cancelled */ });
Edit the starter list at the top of askwell.prompts.js.
Playground
Write instructions and a message, fill in the variables, and run. Switch on "Compare with a second model" to see two answers side by side. The last 12 runs are kept in History and can be restored. "Save as prompt" adds the message to the prompt library; "Open in chat" continues in the chat app.
The playground uses the same adapter as the chat. It passes temperature and maxTokens in the request; the included openai adapter sends the temperature. Token counts are estimates (about 4 characters per token).
In demo mode both models give the same scripted answer. With a real adapter each pane calls the model you picked.
Agent runs
This page shows long jobs that work in steps. Each run has a progress bar, a list of steps with timings, a log and a result (Markdown). Runs can be cancelled, started again and deleted. The "Start a run" form plays three scripted samples, one of which fails on purpose so you can see the error state.
To show your real jobs, call the API from the events your backend sends (for example over server-sent events or a WebSocket):
const run = Askwell.agents.create({ title: 'Weekly report', steps: ['Collect data', 'Write summary'] });
run.start('s1'); // step ids are s1, s2, ... in order
run.log('Fetched page 2 of 4'); // second argument: 'info' | 'warn' | 'error'
run.done('s1', 'Loaded 1,240 rows');
run.start('s2');
run.fail('s2', 'Timed out'); // or:
run.finish('## Result\n\nAll good.');
The same step markup is used for the small "steps" box inside chat answers.
Chat widget
A floating chat button for any website. It is one file, widget/askwell-widget.js, and needs nothing else from the kit. It draws itself inside a shadow root, so your site's styles and the widget's styles do not affect each other.
<script src="askwell-widget.js"></script>
<script>
AskwellWidget.init({
title: 'Ask us anything',
subtitle: 'We usually answer in a few seconds',
greeting: 'Hi! How can I help you today?',
suggestions: ['What are your opening hours?'],
accent: '#1f5c3f', accentInk: '#ffffff',
adapter: 'openai',
endpoint: 'https://your-server.example/api/chat',
system: 'You answer questions about our shop.'
});
</script>
| Option | Default | Meaning |
|---|---|---|
title, subtitle | Text in the header | |
greeting | Hi! How can I help you today? | First message shown. Not sent to the model. |
suggestions | [] | Quick questions shown before the first message |
placeholder, note | Text in the box, and small text under it | |
accent, accentInk | marigold, dark | Button colour and the colour on top of it |
theme | 'light' | 'light', 'dark' or 'auto' (follows the device) |
side | 'right' | 'left' puts the button in the left corner |
open | false | Start with the panel open |
adapter, endpoint, headers, model, system | 'demo' | Same meaning as in the chat app |
remember, storageKey | true | Keep the conversation in the browser between page loads |
Methods: AskwellWidget.open(), .close(), .toggle(), .send(text), .clear(), .destroy(). Events on window: askwell-widget:open, :close, :send, :answer.
On phones the panel fills the screen. Esc closes it. The conversation is saved in the visitor's browser.
Changing the widget
Edit widget/src/widget.css or widget/src/widget.js, then run node build.mjs inside the widget folder. It rebuilds askwell-widget.js (the Markdown renderer, the adapters and the widget, joined into one file).
Sign in and shared pages
login.html
Three views in one page: sign in, create account and reset password (login.html#signup and login.html#reset open the other two). Fields are checked in the browser and show a clear message under the field.
The forms run in demo mode: nothing is sent. To make one real, remove data-demo and add your address:
<form data-auth="signin" action="/session" method="post" novalidate>
Valid forms are then submitted by the browser as usual. Always check the values again on the server. The "single sign-on" button is a sample; connect it to your identity provider.
share.html
A read-only page for one conversation. Your server prints the conversation into the JSON block in the page:
<script type="application/json" id="shareData">
{ "title": "...", "updated": "2 October 2026", "model": "Swift",
"messages": [ { "role": "user", "content": "..." },
{ "role": "assistant", "content": "...", "sources": [{ "title": "...", "url": "..." }] } ] }
</script>
When you print JSON into a page, escape </ as <\/ so text inside a message cannot close the script tag. In the demo, "Open read-only view" in a conversation's menu opens share.html?c=ID, which reads the conversation from the same browser.
Menus, dialogs, toasts
components.html shows every part with its markup. The moving parts have a small API:
Askwell.toast('Saved', 'check'); // text, optional icon name
Askwell.menu(button, [
{ label: 'Rename', icon: 'edit', onSelect: () => {} },
'sep',
{ label: 'Delete', icon: 'trash', danger: true, onSelect: () => {} }
], { align: 'end' });
Askwell.modal.open('myDialogId'); Askwell.modal.close();
Askwell.modal.confirm({ title: 'Delete?', text: 'This cannot be undone.', ok: 'Delete', danger: true })
.then((yes) => { });
Askwell.settings.open(); // the Settings dialog
Askwell.prefs.set('theme', 'dark'); // theme | accent | read | model | system | temperature
In HTML, these attributes work without any JavaScript of your own: data-modal-open="id", data-modal-close, data-toast="Text", data-theme-toggle, data-settings, data-side-open and data-side-close (phone menu).
Menus move with the arrow keys, dialogs keep focus inside and return it when they close, and Esc closes both.
Making a new page
Copy prompts.html, keep the <nav class="rail">, the <aside class="side"> (the phone menu) and the script tags, and replace what is inside <div class="page-in">. Add your page to the rail and to the phone menu in each page file. If your project has templates or components, move those two blocks into one shared partial.
Storing data on a server
Out of the box everything is saved in the browser's localStorage, under keys that start with askwell: (chats, prefs, prompts, runs, playground). That is fine for a demo or a single-user tool. For accounts and sync, save to your backend:
- Conversations: in
askwell.chat.js, replaceload()andsave()near the top.load()returns the array of conversations;save()is called after every change (it is debounced). - Prompts: in
askwell.prompts.js, replaceread()andwrite(). - Agent runs: create them from your backend events with
Askwell.agents.create().
One conversation looks like this:
{ id, title, pinned, created, updated, model,
messages: [
{ id, role: 'user', content, time, files: [{ id, name, size, type }] },
{ id, role: 'assistant', active: 0, feedback: 'up' | 'down' | null,
versions: [{ content, model, steps, sources, error, stopped, time, ms }] }
] }
If localStorage is blocked (private mode in some browsers), the kit keeps working and holds the data in memory until the page closes.
Browsers and accessibility
Made for current versions of Chrome, Edge, Firefox and Safari on desktop and phones. The layout works from 320px wide: the sidebar becomes a slide-in menu, and dialogs dock to the bottom.
- Everything can be used with the keyboard, and the focus ring is always visible.
- Buttons that only show an icon have a text label for screen readers.
- When an answer finishes, a short status message ("Answer ready") is announced. The text is not announced word by word while it arrives.
- Text contrast is 4.5 to 1 or more in both themes and all five accents.
- Animations are switched off for people who ask their system for reduced motion.
Automated checks and keyboard tests were run in Chromium. The kit has not been audited by an accessibility specialist, and it was not tested with every screen reader.
Good to know
- No AI model, API key, database or user accounts are included. Demo mode shows scripted sample answers.
- Names, emails, numbers and sources in the samples are made up. Sample links point to reserved example domains.
- The model names Swift, Sage and Scribe are placeholders. Map them to your real models.
- Token counts are rough estimates, not your provider's count.
- The usage meter in the sidebar counts messages in the browser. Enforce real limits on your server.
- Fonts load from Google Fonts. Everything else is in the download.
Help and license
Questions or something not working? Reply to your purchase receipt email and describe what you see.
You can use Askwell in unlimited projects for yourself and for clients, including paid products. You may not resell or share the kit itself. One license covers one person. The full terms are in LICENSE.txt.