Pular para o conteúdo principal

Tutorial: Build Your First App

In this tutorial, you will build a Word Counter mini-program that reads the active chat history, counts words per role (user vs. assistant), and displays statistics. By the end, you will know how to create, test, package, and publish a mini-program.

What you will learn:

  • Creating a manifest.json
  • Writing app HTML with the window.ais SDK
  • Using ais.chat.getHistory() to read messages
  • Using ais.storage to persist data across sessions
  • Testing locally with sideloading
  • Packaging as a .ais bundle
  • Publishing to the community registry

Time required: About 15 minutes.


Step 1: Create the Manifest

Every mini-program needs a manifest.json that describes the app, its permissions, and its entry point. Create a new directory and add this file:

mkdir word-counter
cd word-counter

Create manifest.json:

{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Count words in your chat history by role",
"author": { "name": "Your Name" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}

Key fields:

FieldValueWhy
nameword-counterUnique identifier (lowercase, hyphens only)
abi1Required -- matches current platform ABI
typemini-programTells the platform this is a sandboxed iframe app
entryindex.htmlThe HTML file to load
base_urlhttps://localhost:8080/Where assets are hosted (update for production)
permissions["storage", "chat:read", "ui:toast"]We need to read chat and save stats
informação

The storage permission is always granted automatically, but it is good practice to list it explicitly so users know your app stores data.


Step 2: Create the HTML

Create index.html in the same directory. This is your entire app -- HTML, CSS, and JavaScript in one file.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 20px;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
h1 { font-size: 22px; margin-bottom: 16px; }

.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #58a6ff;
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}

.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
min-height: 48px;
padding: 10px 20px;
font-size: 16px;
border: 1px solid #444;
border-radius: 6px;
background: #2a2a4e;
color: #e0e0e0;
cursor: pointer;
}
button:hover { background: #3a3a5e; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: #1a73e8; border-color: #1a73e8; }
.btn-primary:hover { background: #1557b0; }
.btn-close { background: none; border-color: #666; color: #888; }

#last-updated {
margin-top: 16px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<h1>Word Counter</h1>

<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">Total Words</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">Your Words</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">AI Words</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">Messages</div>
</div>
</div>

<div class="actions">
<button class="btn-primary" id="analyze">Analyze Chat</button>
<button class="btn-close" id="close">Close</button>
</div>

<div id="last-updated"></div>

<script>
// Helper: count words in a string
function countWords(text) {
if (!text || typeof text !== 'string') return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}

// Format numbers with commas (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}

ais.ready(async function() {
// Set the panel title
ais.ui.setTitle('Word Counter');

// Try to restore last saved stats
var saved = await ais.storage.get('last-stats');
if (saved) {
showStats(saved);
}

// Analyze button
document.getElementById('analyze').addEventListener('click', async function() {
this.disabled = true;
this.textContent = 'Analyzing...';

try {
// Fetch up to 500 messages from chat history
var messages = await ais.chat.getHistory(500);

var userWords = 0;
var aiWords = 0;
var msgCount = messages.length;

for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var words = countWords(msg.content);
if (msg.role === 'user') {
userWords += words;
} else if (msg.role === 'assistant') {
aiWords += words;
}
}

var stats = {
total: userWords + aiWords,
user: userWords,
ai: aiWords,
messages: msgCount,
timestamp: Date.now()
};

showStats(stats);

// Save stats for next time
await ais.storage.set('last-stats', stats);

ais.ui.toast('Analyzed ' + msgCount + ' messages!');
} catch (err) {
ais.ui.toast('Error: ' + err.message);
}

this.disabled = false;
this.textContent = 'Analyze Chat';
});

// Close button
document.getElementById('close').addEventListener('click', function() {
ais.close();
});
});

function showStats(stats) {
document.getElementById('total-words').textContent = fmt(stats.total);
document.getElementById('user-words').textContent = fmt(stats.user);
document.getElementById('ai-words').textContent = fmt(stats.ai);
document.getElementById('msg-count').textContent = fmt(stats.messages);

if (stats.timestamp) {
var date = new Date(stats.timestamp);
document.getElementById('last-updated').textContent =
'Last analyzed: ' + date.toLocaleString();
}
}
</script>
</body>
</html>

What this code does

  1. ais.ready() -- Waits for the SDK bridge to connect before running any logic.
  2. ais.storage.get('last-stats') -- Restores previously saved statistics so the user sees data immediately on launch.
  3. ais.chat.getHistory(500) -- Fetches up to 500 messages from the active conversation.
  4. Word counting -- Iterates through messages, splitting content on whitespace and tallying per role.
  5. ais.storage.set('last-stats', stats) -- Persists the results for next time.
  6. ais.ui.toast() -- Shows a notification when analysis is complete.
  7. ais.close() -- Returns to the chat view when the user clicks Close.

Step 3: Test Locally

You need a local HTTP server to serve the manifest and HTML file. Use any tool you prefer:

# Python 3
cd word-counter
python3 -m http.server 8080

# or Node.js
npx serve -p 8080

# or PHP
php -S localhost:8080

Now install the app in the platform:

  1. Open aiscouncil.com and sign in
  2. Click the Apps icon in the left sidebar
  3. In the Sideload section, paste: http://localhost:8080/manifest.json
  4. Click Install
  5. Review the permissions (storage, chat:read, ui:toast) and click Allow
  6. Click Open on the installed app card
dica

For the fastest development loop, use HTML Upload instead of URL sideloading. Upload your index.html directly -- no server needed. The platform creates a synthetic manifest automatically. You can uninstall and re-upload each time you make changes.

Troubleshooting

ProblemSolution
"Failed to fetch manifest"Make sure your local server is running and serving CORS headers. Try python3 -m http.server 8080 which serves CORS-safe.
App shows blank white pageCheck the browser console for errors. The most common issue is calling ais.* methods before ais.ready().
"PermissionDenied: chat:read"Your manifest does not include chat:read in the permissions array. Update the manifest and reinstall.
App does not update after code changesUninstall the app first (click the X button on the card), then reinstall. Entry HTML is cached at install time.

Step 4: Add Some Polish

Let us add a feature: real-time word counting as new messages arrive.

Add this code inside the ais.ready() callback, after the close button handler:

// Subscribe to new messages for real-time counting
ais.chat.onMessage(function(msg) {
// Re-read saved stats and add the new message's words
ais.storage.get('last-stats').then(function(stats) {
if (!stats) return;
var words = countWords(msg.content);
if (msg.role === 'user') stats.user += words;
else if (msg.role === 'assistant') stats.ai += words;
stats.total = stats.user + stats.ai;
stats.messages++;
stats.timestamp = Date.now();
showStats(stats);
ais.storage.set('last-stats', stats);
});
});

Now the counters update live as the user chats without needing to click "Analyze" again.


Step 5: Package as a .ais Bundle

A .ais bundle is a ZIP archive containing your manifest and all app files. The platform extracts the ZIP, reads the manifest, and inlines all assets (CSS, JS, images) into the entry HTML.

For a single-file app like ours, the bundle is simple:

cd word-counter
zip -r ../word-counter.ais manifest.json index.html

That creates word-counter.ais in the parent directory.

Testing the bundle

  1. In the platform, go to Apps and click Upload App
  2. Select word-counter.ais
  3. Review permissions and approve
  4. The app installs from the bundle with all assets inlined
informação

Bundles are self-contained. The user does not need network access to the original base_url -- everything is inlined at install time. This makes bundles ideal for offline distribution and sharing.

Multi-file bundles

If your app has separate CSS, JavaScript, or image files, include them all in the ZIP:

zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png

The platform automatically inlines:

  • <link rel="stylesheet" href="style.css"> becomes <style>...</style>
  • <script src="app.js"></script> becomes <script>...</script>
  • <img src="icon.png"> becomes <img src="data:image/png;base64,...">

Step 6: Publish to the Registry

Once your app is ready for others to use, publish it to the community registry.

1. Host your files

Upload your manifest and entry HTML to a public CDN. GitHub Pages is free and easy:

# In your GitHub repo (e.g., github.com/yourname/word-counter)
# Push manifest.json and index.html to the main branch
# Enable GitHub Pages in repo settings (source: main branch, root)

Your files will be available at:

  • https://yourname.github.io/word-counter/manifest.json
  • https://yourname.github.io/word-counter/index.html

Update base_url in your manifest to match:

"base_url": "https://yourname.github.io/word-counter/"

2. Fork the aiscouncil repo

Go to github.com/nicholasgasior/bcz and click Fork.

3. Add your package entry

Edit registry/packages.json and add an entry to the packages array:

{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://yourname.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Count words in your chat history by role",
"icon": "https://yourname.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}

4. Validate

Run the validation script to check your entry:

python3 registry/validate.py packages

If validation passes, you are ready to submit.

5. Submit a pull request

Push your changes to your fork and create a PR against the main repo. If the automated validation passes, the PR can be merged and your app will appear in the App Store section of the platform.

See Publishing to Registry for full details on pricing, seller setup, and verification tiers.


Tips and Best Practices

Design

  • Dark theme default -- Most platform users use dark mode. Design for dark backgrounds (#1a1a2e or similar) with light text (#e0e0e0).
  • 48px minimum touch targets -- Buttons and interactive elements should be at least 48px tall for accessibility and VLM-friendly interaction.
  • 14px minimum font size -- All text must be at least 14px for readability.
  • Responsive layout -- The apps panel width varies. Use CSS Grid or Flexbox with auto-fit to adapt.

Performance

  • Cache results in storage -- Use ais.storage to save computed results. Restore them on launch so the user sees data immediately.
  • Limit chat history requests -- ais.chat.getHistory(500) is usually enough. Avoid requesting unlimited history.
  • No polling -- Use ais.chat.onMessage() for real-time updates instead of repeatedly calling getHistory.

Security

  • Request minimal permissions -- Only list the permissions your app actually uses. Fewer permissions means more users will trust and install your app.
  • Validate all input -- Data from ais.chat.getHistory() contains user-generated content. Sanitize before inserting into the DOM.
  • Do not store sensitive data -- ais.storage is not encrypted. Never store passwords, tokens, or API keys (unless your app has secrets:sync and explicitly handles credential transfer).

Compatibility

  • Check ais.platform.abi -- If your app depends on specific SDK features, check the ABI version and show a helpful message if the platform is older.
  • Wrap SDK calls in try/catch -- Permission errors and platform version differences can cause rejections. Handle them gracefully.
  • Test with the hello-world example -- The platform ships with an example mini-program you can use as a reference.