You're about to add AI to a web app with one line of JavaScript. No server setup. No API key management in your code. No backend infrastructure.
const response = await navigator.llm.prompt('Explain this concept');
This tutorial will get you from zero to working AI app in 5 minutes.
Prerequisites
- Chrome browser
- Any text editor
- 5 minutes
That's it. No Node.js, no build tools, no server.
Step 1: Install the WebLLM Extension (1 minute)
- Open Chrome
- Go to the Chrome Web Store and search for "WebLLM"
- Click "Add to Chrome"
- Click the extension icon in your toolbar to open settings
The extension adds navigator.llm to all web pages you visit.
Step 2: Configure a Provider (1 minute)
The extension needs at least one AI provider. You have options:
Option A: Use Ollama (Free, Local, Private)
- Download Ollama
- Run it (it starts a local server)
- In WebLLM extension settings, enable "Ollama"
- Default settings work (
http://localhost:11434)
Ollama is free and runs AI models on your computer. Great for privacy.
Option B: Use OpenAI (Requires API Key)
- Get an API key from platform.openai.com
- In WebLLM extension settings, enable "OpenAI"
- Enter your API key
- Choose a model (gpt-3.5-turbo for testing)
Option C: Use Any Supported Provider
WebLLM supports many providers: Anthropic, Google, Mistral, local servers, and more. Pick what you have access to.
Step 3: Create Your HTML File (1 minute)
Create a file called ai-demo.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My First WebLLM App</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
textarea {
width: 100%;
height: 100px;
margin: 10px 0;
padding: 10px;
font-size: 16px;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
#response {
margin-top: 20px;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
white-space: pre-wrap;
min-height: 100px;
}
</style>
</head>
<body>
<h1>AI Assistant</h1>
<textarea id="prompt" placeholder="Ask me anything...">
What are three interesting facts about octopuses?</textarea
>
<button id="askButton">Ask AI</button>
<div id="response">Response will appear here...</div>
<script>
const promptInput = document.getElementById('prompt');
const askButton = document.getElementById('askButton');
const responseDiv = document.getElementById('response');
askButton.addEventListener('click', async () => {
// Check if WebLLM is available
if (!navigator.llm) {
responseDiv.textContent = '❌ WebLLM not found. Please install the extension.';
return;
}
const prompt = promptInput.value.trim();
if (!prompt) {
responseDiv.textContent = 'Please enter a question.';
return;
}
// Show loading state
askButton.disabled = true;
responseDiv.textContent = '⏳ Thinking...';
try {
// This is the magic line
const response = await navigator.llm.prompt(prompt);
responseDiv.textContent = response;
} catch (error) {
responseDiv.textContent = `❌ Error: ${error.message}`;
} finally {
askButton.disabled = false;
}
});
</script>
</body>
</html>
Step 4: Open and Use (1 minute)
- Open
ai-demo.htmlin Chrome (File → Open File, or drag into browser) - Click "Ask AI"
- The first time, you'll see a permission prompt—click "Allow"
- Wait a moment for the response
- You now have a working AI app!
Step 5: Add Streaming (1 minute)
Streaming shows responses as they're generated, like ChatGPT. Update your script:
<script>
const promptInput = document.getElementById('prompt');
const askButton = document.getElementById('askButton');
const responseDiv = document.getElementById('response');
askButton.addEventListener('click', async () => {
if (!navigator.llm) {
responseDiv.textContent = '❌ WebLLM not found. Please install the extension.';
return;
}
const prompt = promptInput.value.trim();
if (!prompt) {
responseDiv.textContent = 'Please enter a question.';
return;
}
askButton.disabled = true;
responseDiv.textContent = '';
try {
// Stream the response for real-time display
const stream = navigator.llm.streamPrompt(prompt);
for await (const chunk of stream) {
responseDiv.textContent += chunk;
}
} catch (error) {
responseDiv.textContent = `❌ Error: ${error.message}`;
} finally {
askButton.disabled = false;
}
});
</script>
Now responses appear word by word, giving users immediate feedback.
What Just Happened?
Let's break down the flow:
- Your code called
navigator.llm.prompt() - The extension received the request
- Permission check (granted on first use with user consent)
- Provider routing - extension chose the best available provider
- AI processing - your configured provider processed the request
- Response returned - back through the extension to your code
Your app didn't need to know:
- Which AI provider the user has
- API authentication details
- Endpoint URLs
- Model names
The extension handled all of that.
Beyond the Basics
Adding a System Prompt
Make the AI behave a certain way:
const session = await navigator.llm.createSession({
system:
'You are a helpful coding assistant. Keep answers concise and include code examples.',
});
const response = await session.prompt('How do I reverse a string in JavaScript?');
Building a Chat
Maintain conversation context:
const session = await navigator.llm.createSession({
system: 'You are a friendly assistant.',
});
// First message
const response1 = await session.prompt('My name is Alex.');
console.log(response1); // "Nice to meet you, Alex!"
// Follow-up (remembers context)
const response2 = await session.prompt("What's my name?");
console.log(response2); // "Your name is Alex."
Graceful Degradation
Handle cases where WebLLM isn't available:
async function askAI(prompt) {
// Try WebLLM first
if (navigator.llm) {
try {
return await navigator.llm.prompt(prompt);
} catch (error) {
console.warn('WebLLM error:', error);
}
}
// Fallback to your server
const response = await fetch('/api/ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const data = await response.json();
return data.response;
}
Checking Permissions
async function checkAIAccess() {
if (!navigator.llm) {
return { available: false, reason: 'extension-not-installed' };
}
try {
const permission = await navigator.permissions.query({ name: 'llm' });
return {
available: permission.state === 'granted',
state: permission.state,
reason: permission.state === 'denied' ? 'permission-denied' : null,
};
} catch {
return { available: true, state: 'unknown' };
}
}
Complete Example: AI Writing Assistant
Here's a more complete example—an AI writing assistant:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Writing Assistant</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #333;
}
.container {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.panel {
flex: 1;
min-width: 300px;
}
textarea {
width: 100%;
height: 300px;
padding: 15px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 8px;
resize: vertical;
}
textarea:focus {
border-color: #007bff;
outline: none;
}
.actions {
margin: 15px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
font-size: 14px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.primary {
background: #007bff;
color: white;
}
.primary:hover {
background: #0056b3;
}
.secondary {
background: #e9ecef;
color: #333;
}
.secondary:hover {
background: #dee2e6;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#output {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
white-space: pre-wrap;
min-height: 300px;
}
.status {
font-size: 14px;
color: #666;
margin-top: 10px;
}
.error {
color: #dc3545;
}
</style>
</head>
<body>
<h1>✍️ AI Writing Assistant</h1>
<div class="container">
<div class="panel">
<h3>Your Text</h3>
<textarea id="input" placeholder="Enter your text here...">
The quick brown fox jumps over the lazy dog. This is a sample text that you can improve, summarize, or expand using AI.</textarea
>
<div class="actions">
<button class="primary" onclick="transform('improve')">✨ Improve</button>
<button class="secondary" onclick="transform('summarize')">📝 Summarize</button>
<button class="secondary" onclick="transform('expand')">📖 Expand</button>
<button class="secondary" onclick="transform('simplify')">🎯 Simplify</button>
<button class="secondary" onclick="transform('formal')">👔 Make Formal</button>
<button class="secondary" onclick="transform('casual')">😊 Make Casual</button>
</div>
</div>
<div class="panel">
<h3>AI Output</h3>
<div id="output">Click a button to transform your text...</div>
</div>
</div>
<div class="status" id="status"></div>
<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
const status = document.getElementById('status');
const buttons = document.querySelectorAll('button');
const prompts = {
improve:
'Improve the following text, making it clearer and more engaging while keeping the same meaning:\n\n',
summarize: 'Summarize the following text in 2-3 sentences:\n\n',
expand: 'Expand the following text with more detail and examples:\n\n',
simplify: 'Simplify the following text so a 10-year-old could understand it:\n\n',
formal: 'Rewrite the following text in a formal, professional tone:\n\n',
casual: 'Rewrite the following text in a casual, friendly tone:\n\n',
};
async function transform(action) {
// Check WebLLM availability
if (!navigator.llm) {
status.innerHTML =
'<span class="error">❌ WebLLM extension not installed</span>';
return;
}
const text = input.value.trim();
if (!text) {
status.innerHTML = '<span class="error">Please enter some text first</span>';
return;
}
// Disable buttons and show loading
buttons.forEach((b) => (b.disabled = true));
output.textContent = '';
status.textContent = '⏳ Processing...';
try {
const prompt = prompts[action] + text;
const stream = navigator.llm.streamPrompt(prompt);
for await (const chunk of stream) {
output.textContent += chunk;
}
status.textContent = '✅ Done!';
} catch (error) {
status.innerHTML = `<span class="error">❌ ${error.message}</span>`;
output.textContent = 'Error occurred. Please try again.';
} finally {
buttons.forEach((b) => (b.disabled = false));
}
}
</script>
</body>
</html>
Save this as writing-assistant.html and open it in Chrome. You have a functional AI writing tool.
Common Issues and Solutions
"WebLLM not found"
The extension isn't installed or isn't enabled for this page.
- Check that the extension is installed
- Make sure it's enabled in
chrome://extensions - Refresh the page
"No providers available"
No AI provider is configured in the extension.
- Open extension settings
- Configure at least one provider (Ollama is free!)
- Make sure the provider is working (test connection)
Permission denied
User denied AI access for this site.
- Go to extension settings
- Find the site in permissions
- Reset or allow access
Slow responses
Depends on the provider:
- Ollama: First request loads the model (slow), subsequent requests faster
- OpenAI/Cloud: Network latency, usually 1-3 seconds
- Large models: Slower but higher quality
Next Steps
You've built your first WebLLM app! Here's where to go next:
Build More
- Chat interface: Full conversational AI with message history
- Content tools: Summarizers, translators, tone adjusters
- Code helpers: Explain code, generate tests, debug assistance
- Data extraction: Pull structured info from unstructured text
Learn More
- What is WebLLM? - Deeper architecture explanation
- The Case for navigator.llm - Why this matters
- API Documentation - Full API reference
Join the Community
- GitHub Repository - Star, contribute, report issues
- Discord - Chat with other developers
You just added AI to a web app in 5 minutes. No servers. No API keys in code. No complex setup.
This is what browser-native AI looks like.
