Pular para o conteúdo principal

Tutorial: Construa Seu Primeiro App

Neste tutorial, você vai construir um mini-programa Word Counter que lê o histórico de chat ativo, conta palavras por papel (usuário vs. assistente) e exibe estatísticas. Ao final, você saberá como criar, testar, empacotar e publicar um mini-programa.

O que você aprenderá:

  • Criar um manifest.json
  • Escrever HTML de app com o SDK window.ais
  • Usar ais.chat.getHistory() para ler mensagens
  • Usar ais.storage para persistir dados entre sessões
  • Testar localmente com sideloading
  • Empacotar como um bundle .ais
  • Publicar no registro da comunidade

Tempo necessário: Cerca de 15 minutos.


Passo 1: Criar o Manifest

Todo mini-programa precisa de um manifest.json que descreva o app, suas permissões e seu ponto de entrada. Crie um novo diretório e adicione este arquivo:

mkdir word-counter
cd word-counter

Crie manifest.json:

{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Conte palavras no seu histórico de chat por papel",
"author": { "name": "Seu Nome" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}

Campos principais:

CampoValorPor quê
nameword-counterIdentificador único (minúsculas, apenas hífens)
abi1Obrigatório -- corresponde à ABI atual da plataforma
typemini-programDiz à plataforma que este é um app iframe sandboxed
entryindex.htmlO arquivo HTML para carregar
base_urlhttps://localhost:8080/Onde assets estão hospedados (atualize para produção)
permissions["storage", "chat:read", "ui:toast"]Precisamos ler chat e salvar estatísticas
informação

A permissão storage é sempre concedida automaticamente, mas é uma boa prática listá-la explicitamente para que usuários saibam que seu app armazena dados.


Passo 2: Criar o HTML

Crie index.html no mesmo diretório. Este é todo o seu app -- HTML, CSS e JavaScript em um arquivo.

<!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 de Palavras</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">Suas Palavras</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">Palavras da IA</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">Mensagens</div>
</div>
</div>

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

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

<script>
// Helper: conta palavras em uma string
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}

// Formata números com vírgulas (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}

ais.ready(async function () {
// Define o título do painel
ais.ui.setTitle("Word Counter");

// Tenta restaurar últimas estatísticas salvas
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}

// Botão de analisar
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "Analisando...";

try {
// Busca até 500 mensagens do histórico de chat
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);

// Salva estatísticas para próxima vez
await ais.storage.set("last-stats", stats);

ais.ui.toast("Analisadas " + msgCount + " mensagens!");
} catch (err) {
ais.ui.toast("Erro: " + err.message);
}

this.disabled = false;
this.textContent = "Analisar Chat";
});

// Botão fechar
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 =
"Última análise: " + date.toLocaleString();
}
}
</script>
</body>
</html>

O que este código faz

  1. ais.ready() -- Espera a bridge do SDK conectar antes de rodar qualquer lógica.
  2. ais.storage.get('last-stats') -- Restaura estatísticas salvas anteriormente para que o usuário veja dados imediatamente ao lançar.
  3. ais.chat.getHistory(500) -- Busca até 500 mensagens da conversa ativa.
  4. Contagem de palavras -- Itera pelas mensagens, dividindo conteúdo em whitespace e somando por papel.
  5. ais.storage.set('last-stats', stats) -- Persiste os resultados para a próxima vez.
  6. ais.ui.toast() -- Mostra uma notificação quando a análise está completa.
  7. ais.close() -- Retorna à visualização de chat quando o usuário clica Fechar.

Passo 3: Testar Localmente

Você precisa de um servidor HTTP local para servir o manifest e arquivo HTML. Use qualquer ferramenta que preferir:

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

# ou Node.js
npx serve -p 8080

# ou PHP
php -S localhost:8080

Agora instale o app na plataforma:

  1. Abra aiscouncil.net e faça login
  2. Clique no ícone Apps na barra lateral esquerda
  3. Na seção Sideload, cole: http://localhost:8080/manifest.json
  4. Clique Install
  5. Revise as permissões (storage, chat:read, ui:toast) e clique Allow
  6. Clique Open no card do app instalado
dica

Para o loop de desenvolvimento mais rápido, use HTML Upload em vez de sideloading por URL. Faça upload do seu index.html diretamente -- sem servidor necessário. A plataforma cria um manifest sintético automaticamente. Você pode desinstalar e re-uploadar cada vez que fizer mudanças.

Solução de Problemas

ProblemaSolução
"Failed to fetch manifest"Certifique-se de que seu servidor local está rodando e servindo headers CORS. Tente python3 -m http.server 8080 que serve de forma segura para CORS.
App mostra página branca em brancoVerifique o console do navegador por erros. O problema mais comum é chamar métodos ais.* antes de ais.ready().
"PermissionDenied: chat:read"Seu manifest não inclui chat:read no array de permissões. Atualize o manifest e reinstale.
App não atualiza após mudanças de códigoDesinstale o app primeiro (clique no botão X no card), depois reinstale. HTML de entrada é cacheado no momento da instalação.

Passo 4: Adicionar Algum Polish

Vamos adicionar uma funcionalidade: contagem de palavras em tempo real conforme novas mensagens chegam.

Adicione este código dentro do callback ais.ready(), após o handler do botão fechar:

// Subscreve a novas mensagens para contagem em tempo real
ais.chat.onMessage(function (msg) {
// Re-lê estatísticas salvas e adiciona as palavras da nova mensagem
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);
});
});

Agora os contadores atualizam ao vivo conforme o usuário conversa sem precisar clicar em "Analisar" novamente.


Passo 5: Empacotar como Bundle .ais

Um bundle .ais é um arquivo ZIP contendo seu manifest e todos os arquivos do app. A plataforma extrai o ZIP, lê o manifest e inlines todos os assets (CSS, JS, imagens) no HTML de entrada.

Para um app de arquivo único como o nosso, o bundle é simples:

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

Isso cria word-counter.ais no diretório pai.

Testando o bundle

  1. Na plataforma, vá para Apps e clique Upload App
  2. Selecione word-counter.ais
  3. Revise permissões e aprove
  4. O app instala do bundle com todos os assets inlineados
informação

Bundles são autocontidos. O usuário não precisa de acesso de rede ao base_url original -- tudo está inlineado no momento da instalação. Isso torna bundles ideais para distribuição offline e compartilhamento.

Bundles multi-arquivo

Se seu app tem arquivos CSS, JavaScript ou de imagem separados, inclua todos no ZIP:

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

A plataforma automaticamente inlines:

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

Passo 6: Publicar no Registro

Uma vez que seu app está pronto para outros usarem, publique-o no registro da comunidade.

1. Hospede seus arquivos

Faça upload do seu manifest e HTML de entrada para um CDN público. GitHub Pages é gratuito e fácil:

# No seu repo GitHub (ex: github.com/seunome/word-counter)
# Faça push de manifest.json e index.html para a branch main
# Habilite GitHub Pages nas configurações do repo (source: branch main, root)

Seus arquivos estarão disponíveis em:

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

Atualize base_url no seu manifest para corresponder:

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

2. Faça fork do repo aiscouncil

Vá para github.com/nicholasgasior/bcz e clique Fork.

3. Adicione sua entrada de pacote

Edite registry/packages.json e adicione uma entrada ao array packages:

{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://seunome.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Conte palavras no seu histórico de chat por papel",
"icon": "https://seunome.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}

4. Valide

Execute o script de validação para verificar sua entrada:

python3 registry/validate.py packages

Se a validação passar, você está pronto para submeter.

5. Envie um pull request

Faça push das suas mudanças para seu fork e crie um PR contra o repo principal. Se a validação automatizada passar, o PR pode ser merged e seu app aparecerá na seção App Store da plataforma.

Veja Publishing to Registry para detalhes completos sobre preços, configuração de vendedor e níveis de verificação.


Dicas e Melhores Práticas

Design

  • Tema escuro padrão -- A maioria dos usuários da plataforma usa modo escuro. Design para fundos escuros (#1a1a2e ou similar) com texto claro (#e0e0e0).
  • Alvos de toque mínimos de 48px -- Botões e elementos interativos devem ter pelo menos 48px de altura para acessibilidade e interação amigável a VLM.
  • Tamanho de fonte mínimo de 14px -- Todo texto deve ter pelo menos 14px para legibilidade.
  • Layout responsivo -- A largura do painel de apps varia. Use CSS Grid ou Flexbox com auto-fit para adaptar.

Performance

  • Cacheie resultados em armazenamento -- Use ais.storage para salvar resultados computados. Restaure-os ao lançar para que o usuário veja dados imediatamente.
  • Limite requisições de histórico de chat -- ais.chat.getHistory(500) é geralmente suficiente. Evite requisitar histórico ilimitado.
  • Sem polling -- Use ais.chat.onMessage() para updates em tempo real em vez de chamar getHistory repetidamente.

Segurança

  • Solicite permissões mínimas -- Liste apenas as permissões que seu app realmente usa. Menos permissões significa mais usuários confiarão e instalarão seu app.
  • Valide toda entrada -- Dados de ais.chat.getHistory() contêm conteúdo gerado por usuário. Sanitize antes de inserir no DOM.
  • Não armazene dados sensíveis -- ais.storage não é criptografado. Nunca armazene senhas, tokens ou chaves de API (a menos que seu app tenha secrets:sync e explicitamente lide com transferência de credenciais).

Compatibilidade

  • Verifique ais.platform.abi -- Se seu app depende de funcionalidades específicas do SDK, verifique a versão da ABI e mostre uma mensagem útil se a plataforma for mais antiga.
  • Envolva chamadas do SDK em try/catch -- Erros de permissão e diferenças de versão da plataforma podem causar rejeições. Trate-as graciosamente.
  • Teste com o exemplo hello-world -- A plataforma vem com um mini-programa de exemplo que você pode usar como referência.