LLM Integration
May 4, 2026 · View on GitHub
QuikChat is designed to work with any LLM API — local or cloud, streaming or non-streaming. No SDK or library required; just fetch().
Why QuikChat Works Well with LLMs
- History format matches LLM APIs.
historyGet()returns objects withroleandcontentfields — the same shape OpenAI, Ollama, Mistral, and Claude expect. - Streaming is built in.
messageAddNew()+messageAppendContent()handles token-by-token display. - Typing indicator.
messageAddTypingIndicator()shows animated dots that auto-clear when streaming starts. - Input gating.
inputAreaSetEnabled(false)disables the textarea and button while the bot responds. - Markdown rendering. Use the
-mdbuild for automatic markdown formatting, or set a custommessageFormatter. - The onSend callback is async-friendly. Return a Promise or use
async/await— quikchat doesn't care. - Zero dependencies. No SDK conflicts, no bundler configuration, no version mismatches.
General Pattern
Every LLM integration follows the same steps:
const chat = new quikchat('#chat', async (chat, userInput) => {
// 1. Echo user message
chat.messageAddNew(userInput, 'user', 'right', 'user');
// 2. Show typing indicator, disable input
const id = chat.messageAddTypingIndicator('bot');
chat.inputAreaSetEnabled(false);
// 3. Call the API (pass history for conversational memory)
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelName,
messages: [
{ role: 'system', content: systemPrompt },
...chat.historyGet() // full conversation context
],
stream: true
})
});
// 4. Stream tokens into the chat
const reader = response.body.getReader();
let first = true;
// ... read loop: replaceContent on first token (clears dots), appendContent after ...
// 5. Re-enable input
chat.inputAreaSetEnabled(true);
});
The only things that change between providers are the URL, the request format, and the response parsing.
Ollama (Local)
Ollama runs locally and exposes an API at http://localhost:11434. No API key needed.
const chat = new quikchat('#chat', async (chat, userInput) => {
chat.messageAddNew(userInput, 'user', 'right');
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.1',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...chat.historyGet()
],
stream: true
})
});
const reader = response.body.getReader();
let id, first = true;
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = JSON.parse(new TextDecoder().decode(value).trim());
const token = chunk.message.content;
if (first) {
id = chat.messageAddNew(token, 'bot', 'left');
first = false;
} else {
chat.messageAppendContent(id, token);
}
}
});
Ollama streams NDJSON — one JSON object per line, each with a message.content field.
OpenAI / GPT-4
OpenAI uses Server-Sent Events (SSE). The response is a series of data: {...} lines.
const chat = new quikchat('#chat', async (chat, userInput) => {
chat.messageAddNew(userInput, 'user', 'right');
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...chat.historyGet()
],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '', id, first = true;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') return;
const token = JSON.parse(data).choices[0].delta.content;
if (!token) continue;
if (first) {
id = chat.messageAddNew(token, 'bot', 'left');
first = false;
} else {
chat.messageAppendContent(id, token);
}
}
}
});
This same code works with any OpenAI-compatible API (Azure OpenAI, Mistral, Groq, etc.) — just change the URL, model name, and API key.
LM Studio (Local)
LM Studio exposes an OpenAI-compatible API on http://localhost:1234. The code is almost identical to the OpenAI example — just change the URL and remove the Authorization header.
const response = await fetch('http://localhost:1234/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'local-model',
messages: [...chat.historyGet()],
stream: true
})
});
// Same SSE parsing as OpenAI
Conversational Memory
The key to context-aware conversations is passing history to the API. QuikChat's historyGet() returns the full message array with role and content fields:
const history = chat.historyGet();
// [
// { role: "user", content: "What is JavaScript?", ... },
// { role: "assistant", content: "JavaScript is a programming...", ... },
// { role: "user", content: "How does it differ from Java?", ... }
// ]
Spread this into your API call's messages array (after the system prompt) and the LLM will have full context of the conversation.
To limit context window size, slice the history:
const recent = chat.historyGet(-10); // last 10 messages
System Prompts
Prepend a system message to set the LLM's behavior:
const messages = [
{ role: 'system', content: 'You are a pirate. Respond in pirate speak.' },
...chat.historyGet()
];
The system prompt is not stored in quikchat's history — it's injected at call time. This lets you change it without clearing the chat.
Non-Streaming Responses
If streaming isn't needed, await the full response:
const chat = new quikchat('#chat', async (chat, userInput) => {
chat.messageAddNew(userInput, 'user', 'right');
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.1',
prompt: userInput,
stream: false
})
});
const data = await response.json();
chat.messageAddNew(data.response, 'bot', 'left');
});
Disabling Input During Responses
Use inputAreaSetEnabled() and inputAreaSetButtonText() to give users feedback while the bot is responding:
const chat = new quikchat('#chat', async (chat, msg) => {
chat.messageAddNew(msg, 'user', 'right');
chat.inputAreaSetEnabled(false);
chat.inputAreaSetButtonText('Thinking...');
await streamLLMResponse(chat, msg);
chat.inputAreaSetEnabled(true);
chat.inputAreaSetButtonText('Send');
});
Tool Calls / Function Calling
QuikChat doesn't impose any message structure — you can display tool call results however you like. Use the 'tool' role so messages get a quikchat-role-tool CSS class for distinct styling:
// Show the tool being called
const toolMsgId = chat.messageAddNew('Calling weather API...', 'system', 'center', 'system');
// Execute the tool
const weather = await getWeather(location);
// Show the result with the 'tool' role for distinct styling
chat.messageAddNew(
`Weather in ${location}: ${weather.temp}F, ${weather.conditions}`,
'weather-tool', 'left', 'tool'
);
Or use messageReplaceContent() to update a placeholder:
// Bot says it's calling a tool
const id = chat.messageAddNew('Looking that up...', 'bot', 'left', 'assistant');
// After the tool returns, replace with the answer
const result = await callTool(toolName, toolArgs);
chat.messageReplaceContent(id, `Based on the data: ${result}`);
Style tool messages in your theme CSS:
.my-theme .quikchat-role-tool .quikchat-message-content {
border-left: 3px solid #ff9800;
color: #666;
}
.my-theme .quikchat-role-system .quikchat-message-content {
font-style: italic;
color: #999;
}
Markdown Rendering
LLM responses often contain markdown. Use the -md build for automatic rendering:
<script src="https://unpkg.com/quikchat/dist/quikchat-md.umd.min.js"></script>
Or set a custom formatter with the base build:
const chat = new quikchat('#chat', onSend, {
messageFormatter: (content) => marked.parse(content), // or any markdown lib
sanitize: true, // escape HTML before formatting
});
The pipeline is: sanitize → format → display. This means user input gets cleaned before the formatter runs, and the formatter's HTML output is trusted.
Working Examples
See the examples/ directory for complete, runnable demos:
simple_ollama.html— Ollama with and without streamingollama_with_memory.html— Ollama with conversational memorylmstudio_with_memory.html— LM Studio with memoryopenai.html— Any OpenAI-compatible API with configurable model, temperature, and token limitollama_adapters.js— Reusable callback functions for Ollamaexample_tool_editor.html— LLM tool-calling demo: chat commands drive a QuikdownEditor through function calls (read, write, replace, undo/redo)