Repository: ErickWendel/semana-javascript-expert09
Branch: main
Commit: 3ac3987dab4e
Files: 58
Total size: 156.1 KB
Directory structure:
gitextract_awr454dt/
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── _template/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula01-criando-llmstxt/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula02-integrando-ai/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula03-recebendo-como-stream/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
└── aula04-abortando-requisicoes/
├── botData/
│ ├── chatbot-config.json
│ └── systemPrompt.txt
├── index.html
├── llms.txt
├── package.json
└── sdk/
├── ew-chatbot.css
├── ew-chatbot.html
└── src/
├── controllers/
│ └── chatBotController.js
├── index.js
├── services/
│ └── promptService.js
└── views/
└── chatBotView.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.js linguist-detectable=true
*.html linguist-detectable=false
*.css linguist-detectable=false
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
.output
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
================================================
FILE: LICENSE
================================================
MIT License Copyright (c) 2025 Erick Wendel
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center">
[](https://now.ew.academy/semana-js-expert-9?utm_source=githubreadme)
[](https://github.com/ErickWendel/semana-javascript-expert09/stargazers)
[](https://github.com/ErickWendel/semana-javascript-expert09/fork)


# Chatbot Inteligente 100% Offline com Prompt API do Chrome
Construindo um widget de chatbot embarcado que roda totalmente no navegador, explorando os recursos experimentais de AI locais da Chrome Prompt API.
⭐ Deixe uma estrela • [Entre para a comunidade](https://discord.gg/2vvUTUb) • [Reporte um problema](../../issues)
</div>
## 🎥 Preview
<img width="100%" src="./assets/output.gif" alt="Preview do chatbot em funcionamento" />
---
## 📢 Semana JS Expert 09
Este repositório faz parte da **Semana JS Expert 09**, evento gratuito ministrado entre **25/08/2025 e 31/08/2025**.
As aulas completas estão disponíveis em:
👉 [Semana JS Expert 09 na EW Academy](https://now.ew.academy/semana-js-expert-9?utm_source=githubreadme)
> Aproveite enquanto o acesso gratuito estiver liberado! Compartilhe o link com quem quer dominar JavaScript moderno.
### Certificado
Caso você conclua todas as aulas e desafios, receberá este certificado de conclusão (bonitão):

---
### Live demo
- Teste a primeira aula: https://erickwendel.github.io/semana-javascript-expert09/aula01-criando-llmstxt
- Teste a segunda aula: https://erickwendel.github.io/semana-javascript-expert09/aula02-integrando-ai
- Teste a segunda aula: https://erickwendel.github.io/semana-javascript-expert09/aula03-recebendo-como-stream
- Teste a segunda aula: https://erickwendel.github.io/semana-javascript-expert09/aula04-abortando-requisicoes
---
## 📚 Sumário
- [Semana JS Expert 09](#-semana-js-expert-09)
- [Preview](#-preview)
- [Objetivo](#-objetivo)
- [Recursos Principais](#-recursos-principais)
- [Arquitetura e Estrutura](#-arquitetura-e-estrutura)
- [Pré-requisitos](#-pré-requisitos)
- [Instalação Rápida](#-instalação-rápida)
- [Executando](#-executando)
- [Embutindo o Widget em Outro Site](#-embutindo-o-widget-em-outro-site)
- [Customização](#-customização)
- [Limitações e Avisos](#-limitações-e-avisos)
- [Desafios para você](#-desafios)
- [FAQ](#-faq)
- [Contribuição](#-contribuição)
- [EW Academy](#-ew-academy)
---
## 🎯 Objetivo
Aprender, de forma prática, como criar um chatbot que usa **modelos de IA locais / embarcados** via recursos experimentais do Chrome, sem depender de um backend externo. Você terá um widget reutilizável que pode ser plugado em qualquer página.
## 🚀 Recursos Principais
- 100% offline (sem chamadas para servidores – ideal para protótipos e privacidade).
- API moderna do Chrome (Prompt API / AI APIs experimentais).
- Arquitetura simples com separação entre Controller, View e Services.
- Suporte a mensagens streaming simuladas / indicador de digitação.
- Fácil de estilizar via CSS custom properties.
- Preparado para abortar requisições (ex: botão Stop nas aulas avançadas).
## 🧱 Arquitetura e Estrutura do Widget
```
sdk/
ew-chatbot.html # Snippet para embutir
ew-chatbot.css # Estilos e variáveis CSS
src/
index.js # Bootstrapping
controllers/chatBotController.js
views/chatBotView.js
services/promptService.js (adapta chamadas de IA)
botData/
systemPrompt.txt
chatbot-config.json
avatar.webp
```
- Cada aula possui evolução incremental (ex: abortar requests, streaming, melhorias UX...).
- A pasta `_template` serve como base para começar novas aulas/features.
## ✅ Pré-requisitos
- Node.js 22+ (para scripts utilitários e servidor estático simples).
- Navegador **Chrome** (versão compatível com as AI / Prompt APIs experimentais).
- Habilitar flags experimentais:
- [chrome://flags/#prompt-api-for-gemini-nano](chrome://flags/#prompt-api-for-gemini-nano)
## ⚡ Instalação Rápida
Clone o repositório e instale as dependências dentro da pasta da aula desejada.
Exemplo para acessar a primeira aula:
```bash
git clone https://github.com/erickwendel/semana-javascript-expert09
cd semana-javascript-expert09/aula01-criando-llmstxt
npm ci
npm start
```
E então interaja pelo widget no canto da tela.
## 🔌 Embutindo o Widget em Outro Site
Crie a pasta `botData` no projeto em que queira embutir o widget e customize `botData/chatbot-config.json` para alterar nome, avatar e cores.
Você publicar os arquivos da pasta `sdk/` na Web (um cdn talvez) e referenciar o arquivo, algo como:
```html
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="https://erickwendel.github.io/semana-javascript-expert09/aula01-criando-llmstxt/sdk/src/index.js"></script>
</body>
</html>
```
E então o widget aparecerá automaticamente na inicialização na página.
## 🎨 Customização
Conteúdo inicial / comportamento:
- `systemPrompt.txt`: instruções de sistema para o modelo.
- `chatbot-config.json`: metadados (nome, avatar, cores, welcomeBubble etc).
## 🎨 Desafios
1 - Baixar o modelo mediante à autorização dos usuários
- Pergunte ao usuário se ele deseja baixar o modelo
- verificar que se caso o modelo não esteja disponível na máquina do cliente, para que no chat, ele clique em um botão, inicie o download e então o notifique que acabou
2 - Tornar disponível em outros navegadores
- Se o cliente não está no Google Chrome, você pode trocar o modelo, usar o Hugging face ou até o modelo do Gemma do google e seguir o mesmo processo, perguntando se ele deseja baixar o modelo e mais
3 - Tornar disponível em computadores incompatíveis / com menos poder de processamento
- Implementar um backend para consumir as APIs gratuitas de AI, os modelos menores do Gemma do Google para responder aos usuários
- Recomendação é usar o [OpenRouter](https://openrouter.ai/), um agregador de modelos de IA que funcionam na nuvem. Lá lá eles deixam você usar APIs de forma gratuita, com alguns limites mas pelos meus testes funciona muito bem.
- Dar uma olhada na [documentação](https://openrouter.ai/docs/community/open-ai-sdk) para ver como integrar com o Node.js e garantir que suas chaves não vão ficar expostas no frontend.
## ⚠️ Limitações e Avisos
- As Chrome AI / Prompt APIs ainda são experimentais e podem mudar ou exigir flags.
- Recursos offline dependem do suporte do navegador / hardware local.
- Este projeto é educacional – não destina-se a produção sem revisões de segurança.
## ❓ FAQ
**Funciona em Firefox / Safari?** Atualmente o foco é Chrome (APIs experimentais específicas).
**Preciso de servidor backend?** Não para o núcleo demonstrado; tudo roda no cliente.
**Como altero o prompt inicial?** Edite `botData/systemPrompt.txt`.
## 🤝 Contribuição
Contribuições são bem-vindas! Sugestões, issues e PRs ajudam a evoluir o material.
1. Faça um fork
2. Crie uma branch: `git checkout -b feat/minha-feature`
3. Commit: `git commit -m "feat: minha feature"`
4. Push: `git push origin feat/minha-feature`
5. Abra um Pull Request
Se este projeto te ajudou, deixe uma ⭐. Isso incentiva novos conteúdos gratuitos.
## 🏫 EW Academy
<div align="center">
<img src="assets/cover.png" alt="EW Academy Logo" width="240" />
<p><strong>Plataforma oficial do Erick Wendel</strong> com cursos, eventos e conteúdos exclusivos sobre JavaScript, Node.js e tecnologia moderna.</p>
<a href="https://ew.academy" target="_blank"><b>Inscreva-se agora em ew.academy</b></a>
</div>
---
Feito com 💜 durante a Semana JS Expert 09.
================================================
FILE: _template/botData/chatbot-config.json
================================================
{
"primaryColor": "#23A267",
"chatbotName": "EW Academy AI Assistant",
"buttonColor": "#1D7A4B",
"backgroundColor": "#111e16",
"headerColor": "#172a21",
"userBubble": "#2c4636",
"botBubble": "#183224",
"userText": "#fff",
"botText": "#fff",
"iconUrl": "./botData/avatar.webp",
"botAvatar": "./botData/avatar.webp",
"welcomeBubble": "Olá! como podemos te ajudar hoje?",
"firstBotMessage": "Me conta, o que você quer saber sobre a EW Academy?",
"typingDelay": 0.5
}
================================================
FILE: _template/botData/systemPrompt.txt
================================================
Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.
Responda apenas com base nos dados do contexto fornecido (não utilize informações externas).
Formate todas as respostas em **markdown**.
**Instruções:**
- Seja objetivo e resumido (no máximo mil caracteres), com tom amigável e profissional.
- Palavras como "me conte sobre" geralmente remetem aos cursos, então tente verificar primeiro se ele está falando de um curso especifico ou pergunte para obter mais contexto
- Sempre inclua links para fácil acesso.
- Não repita cursos que aparecem em mais de uma trilha.
- O idioma padrão é Português do brasil, os que tem idioma, possuem uma anotaçao como (in English)
- Apresente primeiro a lista de cursos relevantes, depois suas trilhas relacionadas.
- Ao final de cada resposta, pergunte o que o usuário deseja fazer em seguida (ex: “Deseja o link do curso, da trilha ou acessar outra informação?”).
- Pergunte explicitamente se o usuário quer o link do curso/trilha mencionados ou de alguma página relacionada.
- Se a pergunta é relevante ao contexto fornecido, diga que não pode ajudar com isso e dê sugestões
**Importante:**
Aqui está o arquivo `llms.txt` com todos os links e informações contextuais:
================================================
FILE: _template/index.html
================================================
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="./sdk/src/index.js"></script>
</body>
</html>
================================================
FILE: _template/package.json
================================================
{
"name": "ai-chat-button",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "browser-sync -w . --server --files 'sdk/**' --port 3000"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"browser-sync": "^3.0.4"
}
}
================================================
FILE: _template/sdk/ew-chatbot.css
================================================
#ewcb-widget {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ewcb-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 9999;
background: #fff;
border: none;
border-radius: 50%;
width: 68px;
height: 68px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* Only the soft halo */
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.50);
transition: background 0.2s, transform 0.18s, box-shadow 0.2s;
}
.ewcb-btn img {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
display: block;
}
.ewcb-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn:hover {
background: #fafbfc;
box-shadow: 0 4px 28px 0 rgba(80, 80, 80, 0.50);
transform: scale(1.05);
}
.ewcb-welcome-bubble {
position: fixed;
bottom: 68px;
right: 110px;
background: var(--botBubble, #f6f8fa);
color: var(--botText, #1b5e20);
padding: 13px 18px;
font-size: 1em;
border-radius: 18px 18px 18px 6px;
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.09);
max-width: 260px;
z-index: 9999;
white-space: pre-line;
cursor: pointer;
display: flex;
align-items: center;
gap: 13px;
animation: ewcb-toast-in 0.44s cubic-bezier(.5, 1.7, .7, 1) both;
transition: opacity .14s, box-shadow .2s;
}
.ewcb-welcome-bubble:hover {
box-shadow: 0 4px 24px 0 rgba(80, 80, 80, 0.19);
opacity: 0.85;
}
@media (max-width: 500px) {
.ewcb-welcome-bubble {
right: 78px;
font-size: 0.95em;
max-width: 150px;
}
}
@keyframes ewcb-toast-in {
from {
opacity: 0;
transform: translateX(60px) scale(0.98);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.ewcb-btn-avatar-wrapper {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
}
.ewcb-btn-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 19px;
height: 19px;
background: #e32424;
color: #fff;
font-size: 0.98em;
font-weight: bold;
border-radius: 50%;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px #0002;
z-index: 1;
letter-spacing: 0.01em;
pointer-events: none;
transition: transform 0.15s;
}
.ewcb-btn-badge.ewcb-badge-animate {
transform: scale(1.15);
animation: ewcb-badge-pop 0.3s;
}
@keyframes ewcb-badge-pop {
0% {
transform: scale(0.7);
}
70% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
.ewcb-chat-window {
position: fixed;
bottom: 108px;
right: 32px;
z-index: 99999;
background: var(--backgroundColor);
color: var(--primaryColor);
width: 370px;
max-width: 98vw;
height: 520px;
max-height: 80vh;
border-radius: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: ewcb-fadein 0.23s cubic-bezier(.5, 1.7, .7, 1) both;
}
@keyframes ewcb-fadein {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: none;
}
}
.ewcb-chat-header {
background: var(--headerColor);
color: #fff;
padding: 14px 18px;
border-top-left-radius: 18px;
border-top-right-radius: 18px;
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
border-bottom: 1px solid #22242d50;
}
.ewcb-chat-header-logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.ewcb-chatbot-name {
font-size: 1.1em;
}
.ewcb-close-btn {
margin-left: auto;
background: none;
border: none;
color: #8f99b2;
font-size: 1.6em;
cursor: pointer;
font-weight: bold;
line-height: 1;
opacity: 0.8;
padding: 0 2px;
transition: opacity 0.2s;
}
.ewcb-close-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-close-btn:hover {
opacity: 1;
color: #fff;
}
.ewcb-chat-body {
flex: 1 1 auto;
padding: 16px 16px 6px 16px;
overflow-y: auto;
background: var(--backgroundColor);
display: flex;
flex-direction: column;
gap: 12px;
}
.ewcb-message {
display: flex;
align-items: flex-end;
gap: 10px;
margin-bottom: 1px;
}
.ewcb-message-bot {
align-items: flex-start;
}
.ewcb-message-bot .ewcb-avatar {
order: 0;
}
.ewcb-message-user {
flex-direction: row-reverse;
}
.ewcb-message-content {
padding: 12px 18px;
border-radius: 15px;
font-size: 1em;
max-width: 84%;
word-break: break-word;
line-height: 1.5;
background: var(--userBubble);
color: var(--userText);
transition: background .18s, color .18s;
}
.ewcb-message-bot .ewcb-message-content {
background: var(--botBubble);
color: var(--botText);
border-bottom-left-radius: 6px;
}
.ewcb-message-user .ewcb-message-content {
background: var(--userBubble);
color: var(--userText);
border-bottom-right-radius: 6px;
}
.ewcb-message-content a {
color: white
}
.ewcb-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: #111;
object-fit: cover;
}
.ewcb-chat-footer {
display: flex;
gap: 9px;
padding: 12px 14px;
background: var(--headerColor);
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
border-top: 1px solid #1e202c70;
}
.ewcb-btn-main {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
box-shadow: 0 2px 8px #23A26720;
}
.ewcb-btn-main:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn-main:hover {
background: #23A267;
}
#ewcb-input {
flex: 1 1 auto;
border-radius: 10px;
border: none;
padding: 10px 12px;
font-size: 1em;
background: var(--backgroundColor);
color: #fff;
outline: none;
transition: border .16s;
}
#ewcb-input:focus {
border: 1.5px solid var(--primaryColor);
}
.ewcb-chat-footer button:disabled,
#ewcb-input:disabled {
opacity: 0.4;
}
.ewcb-chat-footer button[type="submit"] {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
}
.ewcb-chat-footer button[type="submit"]:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-chat-footer button[type="submit"]:hover {
background: #23A267;
}
#ewcb-stop {
background: transparent;
/* Sem fundo */
border: none;
color: #f43f5e;
/* Cor do emoji/texto */
font-weight: bold;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.4em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
box-shadow: none;
}
#ewcb-stop:focus {
outline: 2px solid #f43f5e44;
outline-offset: 2px;
}
#ewcb-stop:hover {
background: #f43f5e11;
/* Fundo levemente rosado só no hover */
color: #be123c;
/* Fica um tom mais escuro */
}
@media (max-width: 500px) {
#ewcb-stop {
padding: 10px 14px;
font-size: 1em;
}
}
@media (max-width: 500px) {
.ewcb-chat-window {
right: 0;
left: 0;
width: 98vw;
max-width: none;
border-radius: 0 0 18px 18px;
}
}
.ewcb-typing-indicator {
display: flex;
align-items: center;
gap: 3px;
margin: 6px 0 2px 38px;
/* Indent like bot messages */
height: 26px;
}
.ewcb-typing-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--botText, #23A267);
opacity: 0.65;
animation: ewcb-typing-blink var(--typingDotDuration, 0.5s) infinite;
}
.ewcb-typing-dot:nth-child(2) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 8);
}
.ewcb-typing-dot:nth-child(3) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 4);
}
.ewcb-model-progress {
margin: 16px 0 6px 0;
padding: 5px 0 2px 0;
}
.ewcb-progress-label {
font-size: 0.98em;
color: #888;
margin-bottom: 7px;
margin-left: 2px;
}
.ewcb-progress-bar-bg {
width: 100%;
height: 19px;
background: #23272a18;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-bottom: 3px;
}
.ewcb-progress-bar-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #23a267 60%, #4ee185 100%);
border-radius: 12px;
color: #fff;
font-weight: 500;
font-size: 0.96em;
line-height: 19px;
text-align: right;
padding-right: 10px;
transition: width 0.2s cubic-bezier(.4, 1.4, .7, 1);
letter-spacing: 0.03em;
}
@keyframes ewcb-typing-blink {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Aplica apenas à área do chat */
.ewcb-chat-body,
#ewcb-messages {
scrollbar-width: thin;
scrollbar-color: var(--primaryColor, #23A267) var(--backgroundColor, #181C24);
}
/* Para navegadores baseados em Webkit (Chrome, Edge, Safari) */
.ewcb-chat-body::-webkit-scrollbar,
#ewcb-messages::-webkit-scrollbar {
width: 8px;
background: var(--backgroundColor, #181C24);
}
.ewcb-chat-body::-webkit-scrollbar-thumb,
#ewcb-messages::-webkit-scrollbar-thumb {
background: var(--primaryColor, #23A267);
border-radius: 10px;
border: 2px solid var(--backgroundColor, #181C24);
}
/* Opcional: ao passar o mouse */
.ewcb-chat-body::-webkit-scrollbar-thumb:hover,
#ewcb-messages::-webkit-scrollbar-thumb:hover {
background: var(--botBubble, #23A267cc);
}
================================================
FILE: _template/sdk/ew-chatbot.html
================================================
<div id="ewcb-widget">
<button id="ewcb-open-btn" class="ewcb-btn" aria-label="Abrir chat">
<span class="ewcb-btn-avatar-wrapper">
<img id="ewcb-icon" alt="Chatbot" />
<span class="ewcb-btn-badge">1</span>
</span>
</button>
<div class="ewcb-chat-window" id="ewcb-chat-window" style="display:none">
<div class="ewcb-chat-header">
<img src="" alt="Bot logo" class="ewcb-chat-header-logo" id="ewcb-header-icon" />
<span class="ewcb-chatbot-name" id="ewcb-chatbot-name"></span>
<button class="ewcb-close-btn" id="ewcb-close-btn" aria-label="Close chat">×</button>
</div>
<div class="ewcb-chat-body" id="ewcb-messages">
</div>
<form class="ewcb-chat-footer" id="ewcb-form" autocomplete="off">
<input type="text" id="ewcb-input" placeholder="Digite sua mensagem..." autocomplete="off" />
<button type="submit" id="ewcb-submit">Enviar</button>
<button type="button" id="ewcb-stop" alt="Parar parar geração de IA">🛑</button>
</form>
</div>
</div>
================================================
FILE: _template/sdk/src/controllers/chatBotController.js
================================================
// @ts-check
/**
* @typedef {import("../views/chatBotView.js").ChatbotView} ChatBotView
* @typedef {import("../services/promptService.js").PromptService} PromptService
*/
export class ChatbotController {
#chatbotView;
#promptService;
/**
* @param {Object} deps - Dependencies for the class.
* @param {ChatBotView} deps.chatbotView - The chatbot view instance.
* @param {PromptService} deps.promptService - The prompt service instance.
*/
constructor({ chatbotView, promptService }) {
this.#chatbotView = chatbotView;
this.#promptService = promptService;
}
async init({ firstBotMessage, text }) {
this.#setupEvents();
this.#chatbotView.renderWelcomeBubble();
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.appendBotMessage(firstBotMessage, null, false);
}
#setupEvents() {
this.#chatbotView.setupEventHandlers({
onOpen: this.#onOpen.bind(this),
onSend: this.#chatBotReply.bind(this),
onStop: this.#handleStop.bind(this),
});
}
#handleStop() {
}
async #chatBotReply(userMsg) {
console.log('received', userMsg)
this.#chatbotView.showTypingIndicator();
this.#chatbotView.setInputEnabled(false);
setTimeout(() => {
this.#chatbotView.appendBotMessage("Opa! Ainda não estou pronto para isso.", null, false);
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.hideTypingIndicator();
}, 500);
}
async #onOpen() {
this.#chatbotView.setInputEnabled(true);
}
}
================================================
FILE: _template/sdk/src/index.js
================================================
// @ts-check
import { ChatbotView } from './views/chatBotView.js';
import { PromptService } from './services/promptService.js'
import { ChatbotController } from './controllers/chatBotController.js';
(async () => {
const root = new URL('../../', import.meta.url);
const fromMainProject = (path) => new URL(path, root).toString();
const [css, html, systemPrompt, config] = await Promise.all([
fetch(fromMainProject('./sdk/ew-chatbot.css')).then(r => r.text()),
fetch(fromMainProject('./sdk/ew-chatbot.html')).then(r => r.text()),
fetch('./botData/systemPrompt.txt').then(r => r.text()),
fetch('./botData/chatbot-config.json').then(r => r.json()),
]);
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
const promptService = new PromptService();
const chatbotView = new ChatbotView(config);
const controller = new ChatbotController({ chatbotView, promptService });
const text = systemPrompt.concat('\n', '')
controller.init({
firstBotMessage: config.firstBotMessage,
text,
});
})();
================================================
FILE: _template/sdk/src/services/promptService.js
================================================
export class PromptService {
}
================================================
FILE: _template/sdk/src/views/chatBotView.js
================================================
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
export class ChatbotView {
#config;
#container = document.querySelector("#ewcb-widget");
#header = document.querySelector(".ewcb-chat-header");
#messages = document.querySelector("#ewcb-messages");
#input = document.querySelector("#ewcb-input");
#form = document.querySelector("#ewcb-form");
#openBtn = document.querySelector("#ewcb-open-btn");
#stopBtn = document.querySelector("#ewcb-stop");
#closeBtn = document.querySelector("#ewcb-close-btn");
#chatWin = document.querySelector("#ewcb-chat-window");
#floatingIcon = document.querySelector("#ewcb-icon");
#floatingIconBadge = document.querySelector(".ewcb-btn-badge");
#welcomeBubble = null;
constructor(config) {
this.#config = config;
this.#applyTheme();
this.#setHeader();
this.#setFloatingIcon();
this.setTypingDotDuration();
}
setupEventHandlers({ onOpen, onSend, onStop }) {
this.#openBtn.onclick = () => { this.openChat(); onOpen(); };
this.#stopBtn.onclick = () => { onStop(); };
this.#closeBtn.onclick = () => { this.closeChat(); };
this.#form.onsubmit = (e) => {
e.preventDefault();
const val = this.#input.value.trim();
if (!val) return;
this.appendUserMessage(val);
this.clearInput();
onSend(val);
};
}
setInputEnabled(enabled) {
this.#input.disabled = !enabled;
this.#form.querySelector("button[type=submit]").disabled = !enabled;
this.#stopBtn.disabled = enabled;
}
openChat() {
this.#chatWin.style.display = "flex";
this.#floatingIconBadge.style.display = "none";
setTimeout(() => this.focusInput(), 180);
this.hideWelcomeBubble();
}
closeChat() { this.#chatWin.style.display = "none"; }
renderWelcomeBubble() {
this.#removeElement(this.#welcomeBubble);
const bubble = document.createElement('div');
bubble.className = 'ewcb-welcome-bubble';
bubble.textContent = this.#config.welcomeBubble;
bubble.onclick = () => {
this.openChat();
};
document.body.appendChild(bubble);
this.#welcomeBubble = bubble;
}
hideWelcomeBubble() {
if (this.#welcomeBubble) this.#welcomeBubble.style.display = 'none';
}
/** CORE: Render bot message HTML, always via this helper */
#renderBotMessageHTML(text, renderMarkdown = true) {
return `
<img src="${this.#config.botAvatar}" class="ewcb-avatar" alt="Bot Avatar" />
<div class="ewcb-message-content">${renderMarkdown ? marked.parse(text) : text}</div>
`;
}
appendBotMessage(text, element = null, renderMarkdown = true) {
const el = element || this.#createBotMessage();
el.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#append(el);
}
createStreamingBotMessage() {
const element = this.#createBotMessage();
this.#append(element);
return element;
}
updateStreamingBotMessage(element, text, renderMarkdown = true) {
element.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#scrollToBottom();
}
#scrollToBottom() {
this.#messages.scrollTop = this.#messages.scrollHeight;
}
appendUserMessage(text) {
const msg = this.#createUserMessage(text);
this.#append(msg);
}
#createBotMessage() {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-bot';
return msg;
}
#createUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-user';
msg.innerHTML = `<div class="ewcb-message-content">${text}</div>`;
return msg;
}
showTypingIndicator() {
this.hideTypingIndicator()
const indicator = document.createElement('div');
indicator.className = 'ewcb-typing-indicator';
indicator.innerHTML = `
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
`;
this.#append(indicator);
}
hideTypingIndicator() {
this.#removeElement(this.#messages.querySelector('.ewcb-typing-indicator'));
}
clearInput() { this.#input.value = ''; }
focusInput() { this.#input.focus(); }
setTypingDotDuration() {
const delayMs = Number(this.#config.typingDelay) || 1200;
const durationSec = Math.max(0.6, delayMs / 1000 * 0.66);
this.#container.style.setProperty('--typingDotDuration', `${durationSec}s`);
}
#append(msgNode) {
this.#messages.appendChild(msgNode);
this.#scrollToBottom();
}
#removeElement(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}
#applyTheme() {
Object.entries(this.#config).forEach(([k, v]) => {
if (
typeof v === "string" &&
(k.endsWith('Color') || k.endsWith('Bubble') || k.endsWith('Text') || k === "buttonColor")
) {
this.#container.style.setProperty(`--${k}`, v);
}
});
}
#setHeader() {
this.#header.querySelector("#ewcb-header-icon").src = this.#config.iconUrl;
this.#header.querySelector("#ewcb-chatbot-name").textContent = this.#config.chatbotName;
}
#setFloatingIcon() {
this.#floatingIcon.src = this.#config.iconUrl;
}
}
================================================
FILE: aula01-criando-llmstxt/botData/chatbot-config.json
================================================
{
"primaryColor": "#23A267",
"chatbotName": "EW Academy AI Assistant",
"buttonColor": "#1D7A4B",
"backgroundColor": "#111e16",
"headerColor": "#172a21",
"userBubble": "#2c4636",
"botBubble": "#183224",
"userText": "#fff",
"botText": "#fff",
"iconUrl": "./botData/avatar.webp",
"botAvatar": "./botData/avatar.webp",
"welcomeBubble": "Olá! como podemos te ajudar hoje?",
"firstBotMessage": "Me conta, o que você quer saber sobre a EW Academy?",
"typingDelay": 0.5
}
================================================
FILE: aula01-criando-llmstxt/botData/systemPrompt.txt
================================================
Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.
Responda apenas com base nos dados do contexto fornecido (não utilize informações externas).
Formate todas as respostas em **markdown**.
**Instruções:**
- Seja objetivo e resumido (no máximo mil caracteres), com tom amigável e profissional.
- Palavras como "me conte sobre" geralmente remetem aos cursos, então tente verificar primeiro se ele está falando de um curso especifico ou pergunte para obter mais contexto
- Sempre inclua links para fácil acesso.
- Não repita cursos que aparecem em mais de uma trilha.
- O idioma padrão é Português do brasil, os que tem idioma, possuem uma anotaçao como (in English)
- Apresente primeiro a lista de cursos relevantes, depois suas trilhas relacionadas.
- Ao final de cada resposta, pergunte o que o usuário deseja fazer em seguida (ex: “Deseja o link do curso, da trilha ou acessar outra informação?”).
- Pergunte explicitamente se o usuário quer o link do curso/trilha mencionados ou de alguma página relacionada.
- Se a pergunta é relevante ao contexto fornecido, diga que não pode ajudar com isso e dê sugestões
**Importante:**
Aqui está o arquivo `llms.txt` com todos os links e informações contextuais:
================================================
FILE: aula01-criando-llmstxt/index.html
================================================
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="./sdk/src/index.js"></script>
</body>
</html>
================================================
FILE: aula01-criando-llmstxt/llms.txt
================================================
# EW Academy - Aprenda com as melhores trilhas de aprendizado
Descubra as trilhas de aprendizado da EW Academy e transforme sua carreira com cursos de alta qualidade em JavaScript, DevOps, Testes e muito mais.
## Cursos
## Trilhas de Aprendizado que possuem os cursos acima
### Trilha Cursos Livres - Projetos
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando o Multi-Upload de Arquivos do Google Drive com Testes Automatizados](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-o-multi-upload-de-arquivos-do-google-drive-com-testes-automatizados/): Um projeto que demonstra como implementar testes automatizados em um sistema de upload de arquivos.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Criando um aplicativo de mensagens usando apenas linha de comando](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-um-aplicativo-de-mensagens-usando-apenas-linha-de-comando/): Um projeto que demonstra como criar um aplicativo de mensagens simples utilizando a linha de comando.
### AI para Devs
- [Machine learning para Devs](https://ew.academy/trilha/cursos-livres-projetos/curso/machine-learning-para-devs/): Introdução ao ML para devs (com aplicações práticas).
- [Inteligencia artificial para desenvolvedores web](https://ew.academy/trilha/trilha-ai-para-devs/): Curso completo que ensina desde machine learning à inteligencia artificial aplicada a projetos.
### Trilha Javascript Expert
- [Masterclass: Como Consegui Minha Vaga na Gringa](https://ew.academy/trilha/javascript-expert/curso/masterclass-como-consegui-minha-vaga-na-gringa/): Uma masterclass que compartilha experiências e dicas para conseguir uma vaga no exterior.
- [Formação JavaScript Expert](https://ew.academy/trilha/javascript-expert/curso/formacao-javascript-expert/): Um programa de formação completo para se tornar um especialista em JavaScript.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
### Trilha Testes Automatizados para Devs
- [Método Testes Automatizados em JavaScript](https://ew.academy/trilha/trilha-testes-automatizados-p-devs/curso/metodo-testes-automatizados-em-javascript/): Um curso que apresenta um método eficaz para implementar testes automatizados em JavaScript.
## Acesso e Inscrição
- [Acesse](https://play.ewacademy.com.br/auth/login): Link para acessar a plataforma da EW Academy e acessar conteúdos gratuitos.
- [Quero me matricular](https://pay.ew.academy/checkout/assinatura-geral): Um botão para iniciar o processo de matrícula.
- [Acesse nossa Newsletter!](https://sndflw.com/i/R8P3aLG40NGrWGmHdBrU): Inscreva-se na newsletter para receber atualizações e novidades.
- [A nova dimensão da EW Academy chegou.](https://now.ew.academy/lancamento-assinatura): Anúncio sobre as novas ofertas e melhorias na EW Academy.
## FAQ
O que está incluso na assinatura?
### Com a assinatura, você tem acesso completo à plataforma da EW Academy, incluindo:
- Todos os cursos disponíveis
- Trilhas organizadas por nível e objetivo
- Projetos práticos
- Certificados de conclusão
- Comunidade ativa no Discord
E novos conteúdos que forem lançados durante a sua assinatura por um periodo anual
### Terei acesso aos conteúdos futuros ou preciso pagar por eles?
Durante o período da sua assinatura, *todos os novos cursos, atualizações e trilhas são liberados automaticamente pra você*, sem nenhum custo adicional.
Você paga uma vez e tem acesso completo até o fim do seu plano.
### Já comprei cursos da EW. A assinatura vale a pena pra mim?
Com certeza!
Além de garantir acesso a todo o catálogo, pode explorar novas trilhas e acompanhar todos os novos cursos que forem lançados.
### Os cursos são atualizados com frequência?
Sim! Os conteúdos são lançados constantemente para manter você atualizado.
### E se eu já comprei um curso com acesso por 2 anos? Perco esse tempo?
Não! O seu tempo restante continua valendo.
Se você tem, por exemplo, 1 ano restante de acesso ao curso de JavaScript e assina a EW por 1 ano, você passa a ter 2 anos de acesso total àquele curso (1 ano da assinatura + 1 ano já adquirido).
É tudo somado, sem prejuízo.
### A assinatura é vitalícia?
Não. A assinatura é por tempo determinado, com opção de plano anual.
Durante esse período, você tem acesso total à plataforma. Ao final do plano, o acesso é encerrado — mas você pode renovar e continuar de onde parou.
### Tem certificado?
Sim! Todos os cursos oferecem certificados digitais de conclusão, que você pode usar para comprovar sua evolução profissional.
### Existe alguma comunidade de alunos?
Sim! Todos os alunos têm acesso ao nosso servidor exclusivo no Discord, onde você pode tirar dúvidas, conversar com outros devs, compartilhar conquistas e acompanhar novidades.
### Tem suporte se eu travar em algum assunto?
Temos uma comunidade ativa no Discord e suporte dedicado para tirar suas dúvidas.
### Se eu me arrepender, posso cancelar?
Sim! Você pode solicitar o cancelamento dentro do prazo legal vigente. E mesmo que decida sair, a porta estará sempre aberta pra voltar no futuro.
================================================
FILE: aula01-criando-llmstxt/package.json
================================================
{
"name": "ai-chat-button",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "browser-sync -w . --server --files 'sdk/**' --port 3000"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"browser-sync": "^3.0.4"
}
}
================================================
FILE: aula01-criando-llmstxt/sdk/ew-chatbot.css
================================================
#ewcb-widget {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ewcb-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 9999;
background: #fff;
border: none;
border-radius: 50%;
width: 68px;
height: 68px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* Only the soft halo */
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.50);
transition: background 0.2s, transform 0.18s, box-shadow 0.2s;
}
.ewcb-btn img {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
display: block;
}
.ewcb-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn:hover {
background: #fafbfc;
box-shadow: 0 4px 28px 0 rgba(80, 80, 80, 0.50);
transform: scale(1.05);
}
.ewcb-welcome-bubble {
position: fixed;
bottom: 68px;
right: 110px;
background: var(--botBubble, #f6f8fa);
color: var(--botText, #1b5e20);
padding: 13px 18px;
font-size: 1em;
border-radius: 18px 18px 18px 6px;
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.09);
max-width: 260px;
z-index: 9999;
white-space: pre-line;
cursor: pointer;
display: flex;
align-items: center;
gap: 13px;
animation: ewcb-toast-in 0.44s cubic-bezier(.5, 1.7, .7, 1) both;
transition: opacity .14s, box-shadow .2s;
}
.ewcb-welcome-bubble:hover {
box-shadow: 0 4px 24px 0 rgba(80, 80, 80, 0.19);
opacity: 0.85;
}
@media (max-width: 500px) {
.ewcb-welcome-bubble {
right: 78px;
font-size: 0.95em;
max-width: 150px;
}
}
@keyframes ewcb-toast-in {
from {
opacity: 0;
transform: translateX(60px) scale(0.98);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.ewcb-btn-avatar-wrapper {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
}
.ewcb-btn-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 19px;
height: 19px;
background: #e32424;
color: #fff;
font-size: 0.98em;
font-weight: bold;
border-radius: 50%;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px #0002;
z-index: 1;
letter-spacing: 0.01em;
pointer-events: none;
transition: transform 0.15s;
}
.ewcb-btn-badge.ewcb-badge-animate {
transform: scale(1.15);
animation: ewcb-badge-pop 0.3s;
}
@keyframes ewcb-badge-pop {
0% {
transform: scale(0.7);
}
70% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
.ewcb-chat-window {
position: fixed;
bottom: 108px;
right: 32px;
z-index: 99999;
background: var(--backgroundColor);
color: var(--primaryColor);
width: 370px;
max-width: 98vw;
height: 520px;
max-height: 80vh;
border-radius: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: ewcb-fadein 0.23s cubic-bezier(.5, 1.7, .7, 1) both;
}
@keyframes ewcb-fadein {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: none;
}
}
.ewcb-chat-header {
background: var(--headerColor);
color: #fff;
padding: 14px 18px;
border-top-left-radius: 18px;
border-top-right-radius: 18px;
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
border-bottom: 1px solid #22242d50;
}
.ewcb-chat-header-logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.ewcb-chatbot-name {
font-size: 1.1em;
}
.ewcb-close-btn {
margin-left: auto;
background: none;
border: none;
color: #8f99b2;
font-size: 1.6em;
cursor: pointer;
font-weight: bold;
line-height: 1;
opacity: 0.8;
padding: 0 2px;
transition: opacity 0.2s;
}
.ewcb-close-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-close-btn:hover {
opacity: 1;
color: #fff;
}
.ewcb-chat-body {
flex: 1 1 auto;
padding: 16px 16px 6px 16px;
overflow-y: auto;
background: var(--backgroundColor);
display: flex;
flex-direction: column;
gap: 12px;
}
.ewcb-message {
display: flex;
align-items: flex-end;
gap: 10px;
margin-bottom: 1px;
}
.ewcb-message-bot {
align-items: flex-start;
}
.ewcb-message-bot .ewcb-avatar {
order: 0;
}
.ewcb-message-user {
flex-direction: row-reverse;
}
.ewcb-message-content {
padding: 12px 18px;
border-radius: 15px;
font-size: 1em;
max-width: 84%;
word-break: break-word;
line-height: 1.5;
background: var(--userBubble);
color: var(--userText);
transition: background .18s, color .18s;
}
.ewcb-message-bot .ewcb-message-content {
background: var(--botBubble);
color: var(--botText);
border-bottom-left-radius: 6px;
}
.ewcb-message-user .ewcb-message-content {
background: var(--userBubble);
color: var(--userText);
border-bottom-right-radius: 6px;
}
.ewcb-message-content a {
color: white
}
.ewcb-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: #111;
object-fit: cover;
}
.ewcb-chat-footer {
display: flex;
gap: 9px;
padding: 12px 14px;
background: var(--headerColor);
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
border-top: 1px solid #1e202c70;
}
.ewcb-btn-main {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
box-shadow: 0 2px 8px #23A26720;
}
.ewcb-btn-main:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn-main:hover {
background: #23A267;
}
#ewcb-input {
flex: 1 1 auto;
border-radius: 10px;
border: none;
padding: 10px 12px;
font-size: 1em;
background: var(--backgroundColor);
color: #fff;
outline: none;
transition: border .16s;
}
#ewcb-input:focus {
border: 1.5px solid var(--primaryColor);
}
.ewcb-chat-footer button:disabled,
#ewcb-input:disabled {
opacity: 0.4;
}
.ewcb-chat-footer button[type="submit"] {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
}
.ewcb-chat-footer button[type="submit"]:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-chat-footer button[type="submit"]:hover {
background: #23A267;
}
#ewcb-stop {
background: transparent;
/* Sem fundo */
border: none;
color: #f43f5e;
/* Cor do emoji/texto */
font-weight: bold;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.4em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
box-shadow: none;
}
#ewcb-stop:focus {
outline: 2px solid #f43f5e44;
outline-offset: 2px;
}
#ewcb-stop:hover {
background: #f43f5e11;
/* Fundo levemente rosado só no hover */
color: #be123c;
/* Fica um tom mais escuro */
}
@media (max-width: 500px) {
#ewcb-stop {
padding: 10px 14px;
font-size: 1em;
}
}
@media (max-width: 500px) {
.ewcb-chat-window {
right: 0;
left: 0;
width: 98vw;
max-width: none;
border-radius: 0 0 18px 18px;
}
}
.ewcb-typing-indicator {
display: flex;
align-items: center;
gap: 3px;
margin: 6px 0 2px 38px;
/* Indent like bot messages */
height: 26px;
}
.ewcb-typing-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--botText, #23A267);
opacity: 0.65;
animation: ewcb-typing-blink var(--typingDotDuration, 0.5s) infinite;
}
.ewcb-typing-dot:nth-child(2) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 8);
}
.ewcb-typing-dot:nth-child(3) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 4);
}
.ewcb-model-progress {
margin: 16px 0 6px 0;
padding: 5px 0 2px 0;
}
.ewcb-progress-label {
font-size: 0.98em;
color: #888;
margin-bottom: 7px;
margin-left: 2px;
}
.ewcb-progress-bar-bg {
width: 100%;
height: 19px;
background: #23272a18;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-bottom: 3px;
}
.ewcb-progress-bar-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #23a267 60%, #4ee185 100%);
border-radius: 12px;
color: #fff;
font-weight: 500;
font-size: 0.96em;
line-height: 19px;
text-align: right;
padding-right: 10px;
transition: width 0.2s cubic-bezier(.4, 1.4, .7, 1);
letter-spacing: 0.03em;
}
@keyframes ewcb-typing-blink {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Aplica apenas à área do chat */
.ewcb-chat-body,
#ewcb-messages {
scrollbar-width: thin;
scrollbar-color: var(--primaryColor, #23A267) var(--backgroundColor, #181C24);
}
/* Para navegadores baseados em Webkit (Chrome, Edge, Safari) */
.ewcb-chat-body::-webkit-scrollbar,
#ewcb-messages::-webkit-scrollbar {
width: 8px;
background: var(--backgroundColor, #181C24);
}
.ewcb-chat-body::-webkit-scrollbar-thumb,
#ewcb-messages::-webkit-scrollbar-thumb {
background: var(--primaryColor, #23A267);
border-radius: 10px;
border: 2px solid var(--backgroundColor, #181C24);
}
/* Opcional: ao passar o mouse */
.ewcb-chat-body::-webkit-scrollbar-thumb:hover,
#ewcb-messages::-webkit-scrollbar-thumb:hover {
background: var(--botBubble, #23A267cc);
}
================================================
FILE: aula01-criando-llmstxt/sdk/ew-chatbot.html
================================================
<div id="ewcb-widget">
<button id="ewcb-open-btn" class="ewcb-btn" aria-label="Abrir chat">
<span class="ewcb-btn-avatar-wrapper">
<img id="ewcb-icon" alt="Chatbot" />
<span class="ewcb-btn-badge">1</span>
</span>
</button>
<div class="ewcb-chat-window" id="ewcb-chat-window" style="display:none">
<div class="ewcb-chat-header">
<img src="" alt="Bot logo" class="ewcb-chat-header-logo" id="ewcb-header-icon" />
<span class="ewcb-chatbot-name" id="ewcb-chatbot-name"></span>
<button class="ewcb-close-btn" id="ewcb-close-btn" aria-label="Close chat">×</button>
</div>
<div class="ewcb-chat-body" id="ewcb-messages">
</div>
<form class="ewcb-chat-footer" id="ewcb-form" autocomplete="off">
<input type="text" id="ewcb-input" placeholder="Digite sua mensagem..." autocomplete="off" />
<button type="submit" id="ewcb-submit">Enviar</button>
<button type="button" id="ewcb-stop" alt="Parar parar geração de IA">🛑</button>
</form>
</div>
</div>
================================================
FILE: aula01-criando-llmstxt/sdk/src/controllers/chatBotController.js
================================================
// @ts-check
/**
* @typedef {import("../views/chatBotView.js").ChatbotView} ChatBotView
* @typedef {import("../services/promptService.js").PromptService} PromptService
*/
export class ChatbotController {
#chatbotView;
#promptService;
/**
* @param {Object} deps - Dependencies for the class.
* @param {ChatBotView} deps.chatbotView - The chatbot view instance.
* @param {PromptService} deps.promptService - The prompt service instance.
*/
constructor({ chatbotView, promptService }) {
this.#chatbotView = chatbotView;
this.#promptService = promptService;
}
async init({ firstBotMessage, text }) {
this.#setupEvents();
this.#chatbotView.renderWelcomeBubble();
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.appendBotMessage(firstBotMessage, null, false);
}
#setupEvents() {
this.#chatbotView.setupEventHandlers({
onOpen: this.#onOpen.bind(this),
onSend: this.#chatBotReply.bind(this),
onStop: this.#handleStop.bind(this),
});
}
#handleStop() {
}
async #chatBotReply(userMsg) {
console.log('received', userMsg)
this.#chatbotView.showTypingIndicator();
this.#chatbotView.setInputEnabled(false);
setTimeout(() => {
this.#chatbotView.appendBotMessage("Opa! Ainda não estou pronto para isso.", null, false);
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.hideTypingIndicator();
}, 500);
}
async #onOpen() {
this.#chatbotView.setInputEnabled(true);
}
}
================================================
FILE: aula01-criando-llmstxt/sdk/src/index.js
================================================
// @ts-check
import { ChatbotView } from './views/chatBotView.js';
import { PromptService } from './services/promptService.js'
import { ChatbotController } from './controllers/chatBotController.js';
(async () => {
const root = new URL('../../', import.meta.url);
const fromMainProject = (path) => new URL(path, root).toString();
const [css, html, systemPrompt, config] = await Promise.all([
fetch(fromMainProject('./sdk/ew-chatbot.css')).then(r => r.text()),
fetch(fromMainProject('./sdk/ew-chatbot.html')).then(r => r.text()),
fetch('./botData/systemPrompt.txt').then(r => r.text()),
fetch('./botData/chatbot-config.json').then(r => r.json()),
]);
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
const promptService = new PromptService();
const chatbotView = new ChatbotView(config);
const controller = new ChatbotController({ chatbotView, promptService });
const text = systemPrompt.concat('\n', '')
controller.init({
firstBotMessage: config.firstBotMessage,
text,
});
})();
================================================
FILE: aula01-criando-llmstxt/sdk/src/services/promptService.js
================================================
export class PromptService {
}
================================================
FILE: aula01-criando-llmstxt/sdk/src/views/chatBotView.js
================================================
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
export class ChatbotView {
#config;
#container = document.querySelector("#ewcb-widget");
#header = document.querySelector(".ewcb-chat-header");
#messages = document.querySelector("#ewcb-messages");
#input = document.querySelector("#ewcb-input");
#form = document.querySelector("#ewcb-form");
#openBtn = document.querySelector("#ewcb-open-btn");
#stopBtn = document.querySelector("#ewcb-stop");
#closeBtn = document.querySelector("#ewcb-close-btn");
#chatWin = document.querySelector("#ewcb-chat-window");
#floatingIcon = document.querySelector("#ewcb-icon");
#floatingIconBadge = document.querySelector(".ewcb-btn-badge");
#welcomeBubble = null;
constructor(config) {
this.#config = config;
this.#applyTheme();
this.#setHeader();
this.#setFloatingIcon();
this.setTypingDotDuration();
}
setupEventHandlers({ onOpen, onSend, onStop }) {
this.#openBtn.onclick = () => { this.openChat(); onOpen(); };
this.#stopBtn.onclick = () => { onStop(); };
this.#closeBtn.onclick = () => { this.closeChat(); };
this.#form.onsubmit = (e) => {
e.preventDefault();
const val = this.#input.value.trim();
if (!val) return;
this.appendUserMessage(val);
this.clearInput();
onSend(val);
};
}
setInputEnabled(enabled) {
this.#input.disabled = !enabled;
this.#form.querySelector("button[type=submit]").disabled = !enabled;
this.#stopBtn.disabled = enabled;
}
openChat() {
this.#chatWin.style.display = "flex";
this.#floatingIconBadge.style.display = "none";
setTimeout(() => this.focusInput(), 180);
this.hideWelcomeBubble();
}
closeChat() { this.#chatWin.style.display = "none"; }
renderWelcomeBubble() {
this.#removeElement(this.#welcomeBubble);
const bubble = document.createElement('div');
bubble.className = 'ewcb-welcome-bubble';
bubble.textContent = this.#config.welcomeBubble;
bubble.onclick = () => {
this.openChat();
};
document.body.appendChild(bubble);
this.#welcomeBubble = bubble;
}
hideWelcomeBubble() {
if (this.#welcomeBubble) this.#welcomeBubble.style.display = 'none';
}
/** CORE: Render bot message HTML, always via this helper */
#renderBotMessageHTML(text, renderMarkdown = true) {
return `
<img src="${this.#config.botAvatar}" class="ewcb-avatar" alt="Bot Avatar" />
<div class="ewcb-message-content">${renderMarkdown ? marked.parse(text) : text}</div>
`;
}
appendBotMessage(text, element = null, renderMarkdown = true) {
const el = element || this.#createBotMessage();
el.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#append(el);
}
createStreamingBotMessage() {
const element = this.#createBotMessage();
this.#append(element);
return element;
}
updateStreamingBotMessage(element, text, renderMarkdown = true) {
element.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#scrollToBottom();
}
#scrollToBottom() {
this.#messages.scrollTop = this.#messages.scrollHeight;
}
appendUserMessage(text) {
const msg = this.#createUserMessage(text);
this.#append(msg);
}
#createBotMessage() {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-bot';
return msg;
}
#createUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-user';
msg.innerHTML = `<div class="ewcb-message-content">${text}</div>`;
return msg;
}
showTypingIndicator() {
this.hideTypingIndicator()
const indicator = document.createElement('div');
indicator.className = 'ewcb-typing-indicator';
indicator.innerHTML = `
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
`;
this.#append(indicator);
}
hideTypingIndicator() {
this.#removeElement(this.#messages.querySelector('.ewcb-typing-indicator'));
}
clearInput() { this.#input.value = ''; }
focusInput() { this.#input.focus(); }
setTypingDotDuration() {
const delayMs = Number(this.#config.typingDelay) || 1200;
const durationSec = Math.max(0.6, delayMs / 1000 * 0.66);
this.#container.style.setProperty('--typingDotDuration', `${durationSec}s`);
}
#append(msgNode) {
this.#messages.appendChild(msgNode);
this.#scrollToBottom();
}
#removeElement(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}
#applyTheme() {
Object.entries(this.#config).forEach(([k, v]) => {
if (
typeof v === "string" &&
(k.endsWith('Color') || k.endsWith('Bubble') || k.endsWith('Text') || k === "buttonColor")
) {
this.#container.style.setProperty(`--${k}`, v);
}
});
}
#setHeader() {
this.#header.querySelector("#ewcb-header-icon").src = this.#config.iconUrl;
this.#header.querySelector("#ewcb-chatbot-name").textContent = this.#config.chatbotName;
}
#setFloatingIcon() {
this.#floatingIcon.src = this.#config.iconUrl;
}
}
================================================
FILE: aula02-integrando-ai/botData/chatbot-config.json
================================================
{
"primaryColor": "#23A267",
"chatbotName": "EW Academy AI Assistant",
"buttonColor": "#1D7A4B",
"backgroundColor": "#111e16",
"headerColor": "#172a21",
"userBubble": "#2c4636",
"botBubble": "#183224",
"userText": "#fff",
"botText": "#fff",
"iconUrl": "./botData/avatar.webp",
"botAvatar": "./botData/avatar.webp",
"welcomeBubble": "Olá! como podemos te ajudar hoje?",
"firstBotMessage": "Me conta, o que você quer saber sobre a EW Academy?",
"typingDelay": 0.5
}
================================================
FILE: aula02-integrando-ai/botData/systemPrompt.txt
================================================
Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.
Responda apenas com base nos dados do contexto fornecido (não utilize informações externas).
Formate todas as respostas em **markdown**.
**Instruções:**
- Seja objetivo e resumido (no máximo mil caracteres), com tom amigável e profissional.
- Palavras como "me conte sobre" geralmente remetem aos cursos, então tente verificar primeiro se ele está falando de um curso especifico ou pergunte para obter mais contexto
- Sempre inclua links para fácil acesso.
- Não repita cursos que aparecem em mais de uma trilha.
- O idioma padrão é Português do brasil, os que tem idioma, possuem uma anotaçao como (in English)
- Apresente primeiro a lista de cursos relevantes, depois suas trilhas relacionadas.
- Ao final de cada resposta, pergunte o que o usuário deseja fazer em seguida (ex: “Deseja o link do curso, da trilha ou acessar outra informação?”).
- Pergunte explicitamente se o usuário quer o link do curso/trilha mencionados ou de alguma página relacionada.
- Se a pergunta é relevante ao contexto fornecido, diga que não pode ajudar com isso e dê sugestões
**Importante:**
Aqui está o arquivo `llms.txt` com todos os links e informações contextuais:
================================================
FILE: aula02-integrando-ai/index.html
================================================
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="./sdk/src/index.js"></script>
</body>
</html>
================================================
FILE: aula02-integrando-ai/llms.txt
================================================
# EW Academy - Aprenda com as melhores trilhas de aprendizado
Descubra as trilhas de aprendizado da EW Academy e transforme sua carreira com cursos de alta qualidade em JavaScript, DevOps, Testes e muito mais.
## Cursos
## Trilhas de Aprendizado que possuem os cursos acima
### Trilha Cursos Livres - Projetos
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando o Multi-Upload de Arquivos do Google Drive com Testes Automatizados](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-o-multi-upload-de-arquivos-do-google-drive-com-testes-automatizados/): Um projeto que demonstra como implementar testes automatizados em um sistema de upload de arquivos.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Criando um aplicativo de mensagens usando apenas linha de comando](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-um-aplicativo-de-mensagens-usando-apenas-linha-de-comando/): Um projeto que demonstra como criar um aplicativo de mensagens simples utilizando a linha de comando.
### AI para Devs
- [Machine learning para Devs](https://ew.academy/trilha/cursos-livres-projetos/curso/machine-learning-para-devs/): Introdução ao ML para devs (com aplicações práticas).
- [Inteligencia artificial para desenvolvedores web](https://ew.academy/trilha/trilha-ai-para-devs/): Curso completo que ensina desde machine learning à inteligencia artificial aplicada a projetos.
### Trilha Javascript Expert
- [Masterclass: Como Consegui Minha Vaga na Gringa](https://ew.academy/trilha/javascript-expert/curso/masterclass-como-consegui-minha-vaga-na-gringa/): Uma masterclass que compartilha experiências e dicas para conseguir uma vaga no exterior.
- [Formação JavaScript Expert](https://ew.academy/trilha/javascript-expert/curso/formacao-javascript-expert/): Um programa de formação completo para se tornar um especialista em JavaScript.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
### Trilha Testes Automatizados para Devs
- [Método Testes Automatizados em JavaScript](https://ew.academy/trilha/trilha-testes-automatizados-p-devs/curso/metodo-testes-automatizados-em-javascript/): Um curso que apresenta um método eficaz para implementar testes automatizados em JavaScript.
## Acesso e Inscrição
- [Acesse](https://play.ewacademy.com.br/auth/login): Link para acessar a plataforma da EW Academy e acessar conteúdos gratuitos.
- [Quero me matricular](https://pay.ew.academy/checkout/assinatura-geral): Um botão para iniciar o processo de matrícula.
- [Acesse nossa Newsletter!](https://sndflw.com/i/R8P3aLG40NGrWGmHdBrU): Inscreva-se na newsletter para receber atualizações e novidades.
- [A nova dimensão da EW Academy chegou.](https://now.ew.academy/lancamento-assinatura): Anúncio sobre as novas ofertas e melhorias na EW Academy.
## FAQ
O que está incluso na assinatura?
### Com a assinatura, você tem acesso completo à plataforma da EW Academy, incluindo:
- Todos os cursos disponíveis
- Trilhas organizadas por nível e objetivo
- Projetos práticos
- Certificados de conclusão
- Comunidade ativa no Discord
E novos conteúdos que forem lançados durante a sua assinatura por um periodo anual
### Terei acesso aos conteúdos futuros ou preciso pagar por eles?
Durante o período da sua assinatura, *todos os novos cursos, atualizações e trilhas são liberados automaticamente pra você*, sem nenhum custo adicional.
Você paga uma vez e tem acesso completo até o fim do seu plano.
### Já comprei cursos da EW. A assinatura vale a pena pra mim?
Com certeza!
Além de garantir acesso a todo o catálogo, pode explorar novas trilhas e acompanhar todos os novos cursos que forem lançados.
### Os cursos são atualizados com frequência?
Sim! Os conteúdos são lançados constantemente para manter você atualizado.
### E se eu já comprei um curso com acesso por 2 anos? Perco esse tempo?
Não! O seu tempo restante continua valendo.
Se você tem, por exemplo, 1 ano restante de acesso ao curso de JavaScript e assina a EW por 1 ano, você passa a ter 2 anos de acesso total àquele curso (1 ano da assinatura + 1 ano já adquirido).
É tudo somado, sem prejuízo.
### A assinatura é vitalícia?
Não. A assinatura é por tempo determinado, com opção de plano anual.
Durante esse período, você tem acesso total à plataforma. Ao final do plano, o acesso é encerrado — mas você pode renovar e continuar de onde parou.
### Tem certificado?
Sim! Todos os cursos oferecem certificados digitais de conclusão, que você pode usar para comprovar sua evolução profissional.
### Existe alguma comunidade de alunos?
Sim! Todos os alunos têm acesso ao nosso servidor exclusivo no Discord, onde você pode tirar dúvidas, conversar com outros devs, compartilhar conquistas e acompanhar novidades.
### Tem suporte se eu travar em algum assunto?
Temos uma comunidade ativa no Discord e suporte dedicado para tirar suas dúvidas.
### Se eu me arrepender, posso cancelar?
Sim! Você pode solicitar o cancelamento dentro do prazo legal vigente. E mesmo que decida sair, a porta estará sempre aberta pra voltar no futuro.
================================================
FILE: aula02-integrando-ai/package.json
================================================
{
"name": "ai-chat-button",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "browser-sync -w . --server --files 'sdk/**' --port 3000"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"browser-sync": "^3.0.4"
}
}
================================================
FILE: aula02-integrando-ai/sdk/ew-chatbot.css
================================================
#ewcb-widget {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ewcb-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 9999;
background: #fff;
border: none;
border-radius: 50%;
width: 68px;
height: 68px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* Only the soft halo */
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.50);
transition: background 0.2s, transform 0.18s, box-shadow 0.2s;
}
.ewcb-btn img {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
display: block;
}
.ewcb-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn:hover {
background: #fafbfc;
box-shadow: 0 4px 28px 0 rgba(80, 80, 80, 0.50);
transform: scale(1.05);
}
.ewcb-welcome-bubble {
position: fixed;
bottom: 68px;
right: 110px;
background: var(--botBubble, #f6f8fa);
color: var(--botText, #1b5e20);
padding: 13px 18px;
font-size: 1em;
border-radius: 18px 18px 18px 6px;
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.09);
max-width: 260px;
z-index: 9999;
white-space: pre-line;
cursor: pointer;
display: flex;
align-items: center;
gap: 13px;
animation: ewcb-toast-in 0.44s cubic-bezier(.5, 1.7, .7, 1) both;
transition: opacity .14s, box-shadow .2s;
}
.ewcb-welcome-bubble:hover {
box-shadow: 0 4px 24px 0 rgba(80, 80, 80, 0.19);
opacity: 0.85;
}
@media (max-width: 500px) {
.ewcb-welcome-bubble {
right: 78px;
font-size: 0.95em;
max-width: 150px;
}
}
@keyframes ewcb-toast-in {
from {
opacity: 0;
transform: translateX(60px) scale(0.98);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.ewcb-btn-avatar-wrapper {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
}
.ewcb-btn-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 19px;
height: 19px;
background: #e32424;
color: #fff;
font-size: 0.98em;
font-weight: bold;
border-radius: 50%;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px #0002;
z-index: 1;
letter-spacing: 0.01em;
pointer-events: none;
transition: transform 0.15s;
}
.ewcb-btn-badge.ewcb-badge-animate {
transform: scale(1.15);
animation: ewcb-badge-pop 0.3s;
}
@keyframes ewcb-badge-pop {
0% {
transform: scale(0.7);
}
70% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
.ewcb-chat-window {
position: fixed;
bottom: 108px;
right: 32px;
z-index: 99999;
background: var(--backgroundColor);
color: var(--primaryColor);
width: 370px;
max-width: 98vw;
height: 520px;
max-height: 80vh;
border-radius: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: ewcb-fadein 0.23s cubic-bezier(.5, 1.7, .7, 1) both;
}
@keyframes ewcb-fadein {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: none;
}
}
.ewcb-chat-header {
background: var(--headerColor);
color: #fff;
padding: 14px 18px;
border-top-left-radius: 18px;
border-top-right-radius: 18px;
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
border-bottom: 1px solid #22242d50;
}
.ewcb-chat-header-logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.ewcb-chatbot-name {
font-size: 1.1em;
}
.ewcb-close-btn {
margin-left: auto;
background: none;
border: none;
color: #8f99b2;
font-size: 1.6em;
cursor: pointer;
font-weight: bold;
line-height: 1;
opacity: 0.8;
padding: 0 2px;
transition: opacity 0.2s;
}
.ewcb-close-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-close-btn:hover {
opacity: 1;
color: #fff;
}
.ewcb-chat-body {
flex: 1 1 auto;
padding: 16px 16px 6px 16px;
overflow-y: auto;
background: var(--backgroundColor);
display: flex;
flex-direction: column;
gap: 12px;
}
.ewcb-message {
display: flex;
align-items: flex-end;
gap: 10px;
margin-bottom: 1px;
}
.ewcb-message-bot {
align-items: flex-start;
}
.ewcb-message-bot .ewcb-avatar {
order: 0;
}
.ewcb-message-user {
flex-direction: row-reverse;
}
.ewcb-message-content {
padding: 12px 18px;
border-radius: 15px;
font-size: 1em;
max-width: 84%;
word-break: break-word;
line-height: 1.5;
background: var(--userBubble);
color: var(--userText);
transition: background .18s, color .18s;
}
.ewcb-message-bot .ewcb-message-content {
background: var(--botBubble);
color: var(--botText);
border-bottom-left-radius: 6px;
}
.ewcb-message-user .ewcb-message-content {
background: var(--userBubble);
color: var(--userText);
border-bottom-right-radius: 6px;
}
.ewcb-message-content a {
color: white
}
.ewcb-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: #111;
object-fit: cover;
}
.ewcb-chat-footer {
display: flex;
gap: 9px;
padding: 12px 14px;
background: var(--headerColor);
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
border-top: 1px solid #1e202c70;
}
.ewcb-btn-main {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
box-shadow: 0 2px 8px #23A26720;
}
.ewcb-btn-main:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn-main:hover {
background: #23A267;
}
#ewcb-input {
flex: 1 1 auto;
border-radius: 10px;
border: none;
padding: 10px 12px;
font-size: 1em;
background: var(--backgroundColor);
color: #fff;
outline: none;
transition: border .16s;
}
#ewcb-input:focus {
border: 1.5px solid var(--primaryColor);
}
.ewcb-chat-footer button:disabled,
#ewcb-input:disabled {
opacity: 0.4;
}
.ewcb-chat-footer button[type="submit"] {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
}
.ewcb-chat-footer button[type="submit"]:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-chat-footer button[type="submit"]:hover {
background: #23A267;
}
#ewcb-stop {
background: transparent;
/* Sem fundo */
border: none;
color: #f43f5e;
/* Cor do emoji/texto */
font-weight: bold;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.4em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
box-shadow: none;
}
#ewcb-stop:focus {
outline: 2px solid #f43f5e44;
outline-offset: 2px;
}
#ewcb-stop:hover {
background: #f43f5e11;
/* Fundo levemente rosado só no hover */
color: #be123c;
/* Fica um tom mais escuro */
}
@media (max-width: 500px) {
#ewcb-stop {
padding: 10px 14px;
font-size: 1em;
}
}
@media (max-width: 500px) {
.ewcb-chat-window {
right: 0;
left: 0;
width: 98vw;
max-width: none;
border-radius: 0 0 18px 18px;
}
}
.ewcb-typing-indicator {
display: flex;
align-items: center;
gap: 3px;
margin: 6px 0 2px 38px;
/* Indent like bot messages */
height: 26px;
}
.ewcb-typing-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--botText, #23A267);
opacity: 0.65;
animation: ewcb-typing-blink var(--typingDotDuration, 0.5s) infinite;
}
.ewcb-typing-dot:nth-child(2) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 8);
}
.ewcb-typing-dot:nth-child(3) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 4);
}
.ewcb-model-progress {
margin: 16px 0 6px 0;
padding: 5px 0 2px 0;
}
.ewcb-progress-label {
font-size: 0.98em;
color: #888;
margin-bottom: 7px;
margin-left: 2px;
}
.ewcb-progress-bar-bg {
width: 100%;
height: 19px;
background: #23272a18;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-bottom: 3px;
}
.ewcb-progress-bar-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #23a267 60%, #4ee185 100%);
border-radius: 12px;
color: #fff;
font-weight: 500;
font-size: 0.96em;
line-height: 19px;
text-align: right;
padding-right: 10px;
transition: width 0.2s cubic-bezier(.4, 1.4, .7, 1);
letter-spacing: 0.03em;
}
@keyframes ewcb-typing-blink {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Aplica apenas à área do chat */
.ewcb-chat-body,
#ewcb-messages {
scrollbar-width: thin;
scrollbar-color: var(--primaryColor, #23A267) var(--backgroundColor, #181C24);
}
/* Para navegadores baseados em Webkit (Chrome, Edge, Safari) */
.ewcb-chat-body::-webkit-scrollbar,
#ewcb-messages::-webkit-scrollbar {
width: 8px;
background: var(--backgroundColor, #181C24);
}
.ewcb-chat-body::-webkit-scrollbar-thumb,
#ewcb-messages::-webkit-scrollbar-thumb {
background: var(--primaryColor, #23A267);
border-radius: 10px;
border: 2px solid var(--backgroundColor, #181C24);
}
/* Opcional: ao passar o mouse */
.ewcb-chat-body::-webkit-scrollbar-thumb:hover,
#ewcb-messages::-webkit-scrollbar-thumb:hover {
background: var(--botBubble, #23A267cc);
}
================================================
FILE: aula02-integrando-ai/sdk/ew-chatbot.html
================================================
<div id="ewcb-widget">
<button id="ewcb-open-btn" class="ewcb-btn" aria-label="Abrir chat">
<span class="ewcb-btn-avatar-wrapper">
<img id="ewcb-icon" alt="Chatbot" />
<span class="ewcb-btn-badge">1</span>
</span>
</button>
<div class="ewcb-chat-window" id="ewcb-chat-window" style="display:none">
<div class="ewcb-chat-header">
<img src="" alt="Bot logo" class="ewcb-chat-header-logo" id="ewcb-header-icon" />
<span class="ewcb-chatbot-name" id="ewcb-chatbot-name"></span>
<button class="ewcb-close-btn" id="ewcb-close-btn" aria-label="Close chat">×</button>
</div>
<div class="ewcb-chat-body" id="ewcb-messages">
</div>
<form class="ewcb-chat-footer" id="ewcb-form" autocomplete="off">
<input type="text" id="ewcb-input" placeholder="Digite sua mensagem..." autocomplete="off" />
<button type="submit" id="ewcb-submit">Enviar</button>
<button type="button" id="ewcb-stop" alt="Parar parar geração de IA">🛑</button>
</form>
</div>
</div>
================================================
FILE: aula02-integrando-ai/sdk/src/controllers/chatBotController.js
================================================
// @ts-check
/**
* @typedef {import("../views/chatBotView.js").ChatbotView} ChatBotView
* @typedef {import("../services/promptService.js").PromptService} PromptService
*/
export class ChatbotController {
#chatbotView;
#promptService;
/**
* @param {Object} deps - Dependencies for the class.
* @param {ChatBotView} deps.chatbotView - The chatbot view instance.
* @param {PromptService} deps.promptService - The prompt service instance.
*/
constructor({ chatbotView, promptService }) {
this.#chatbotView = chatbotView;
this.#promptService = promptService;
}
async init({ firstBotMessage, text }) {
this.#setupEvents();
this.#chatbotView.renderWelcomeBubble();
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.appendBotMessage(firstBotMessage, null, false);
return this.#promptService.init(text)
}
#setupEvents() {
this.#chatbotView.setupEventHandlers({
onOpen: this.#onOpen.bind(this),
onSend: this.#chatBotReply.bind(this),
onStop: this.#handleStop.bind(this),
});
}
#handleStop() {
}
async #chatBotReply(userMsg) {
console.log('received', userMsg)
this.#chatbotView.showTypingIndicator();
this.#chatbotView.setInputEnabled(false);
const response = await this.#promptService.prompt(userMsg)
console.log('response', response)
this.#chatbotView.appendBotMessage(response);
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.hideTypingIndicator();
}
async #onOpen() {
const errors = this.#checkRequirements()
if (errors.length) {
const messages = errors.join('\n\n')
this.#chatbotView.appendBotMessage(
messages
)
this.#chatbotView.setInputEnabled(false);
return
}
this.#chatbotView.setInputEnabled(true);
}
#checkRequirements() {
const errors = []
// @ts-ignore
const iChrome = window.chrome
if (!iChrome) {
errors.push(
'⚠️ Este recurso só funciona no Google Chrome ou Chrome Canary (versão recente).'
)
}
if (!('LanguageModel' in window)) {
errors.push("⚠️ As APIs nativas de IA não estão ativas.");
errors.push("Ative a seguinte flag em chrome://flags/:");
errors.push("- Prompt API for Gemini Nano (chrome://flags/#prompt-api-for-gemini-nano)");
errors.push("Depois reinicie o Chrome e tente novamente.");
}
return errors
}
}
================================================
FILE: aula02-integrando-ai/sdk/src/index.js
================================================
// @ts-check
import { ChatbotView } from './views/chatBotView.js';
import { PromptService } from './services/promptService.js'
import { ChatbotController } from './controllers/chatBotController.js';
(async () => {
const root = new URL('../../', import.meta.url);
const fromMainProject = (path) => new URL(path, root).toString();
const [css, html, systemPrompt, config, llmsTxt] = await Promise.all([
fetch(fromMainProject('./sdk/ew-chatbot.css')).then(r => r.text()),
fetch(fromMainProject('./sdk/ew-chatbot.html')).then(r => r.text()),
fetch('./botData/systemPrompt.txt').then(r => r.text()),
fetch('./botData/chatbot-config.json').then(r => r.json()),
fetch('./llms.txt').then(r => r.text()),
]);
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
const promptService = new PromptService();
const chatbotView = new ChatbotView(config);
const controller = new ChatbotController({ chatbotView, promptService });
const text = systemPrompt.concat('\n', llmsTxt)
controller.init({
firstBotMessage: config.firstBotMessage,
text,
});
})();
================================================
FILE: aula02-integrando-ai/sdk/src/services/promptService.js
================================================
export class PromptService {
#messages = []
#session = null
async init(initialPrompts) {
if (!window.LanguageModel) return
this.#messages.push({
role: 'system',
content: initialPrompts
})
return this.#createSession()
}
async #createSession() {
this.#session = await LanguageModel.create({
initialPrompts: this.#messages,
expectedInputLanguages: ['pt']
})
return this.#session
}
prompt(text) {
this.#messages.push({
role: 'user',
content: text,
})
return this.#session.prompt(this.#messages)
}
}
================================================
FILE: aula02-integrando-ai/sdk/src/views/chatBotView.js
================================================
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
export class ChatbotView {
#config;
#container = document.querySelector("#ewcb-widget");
#header = document.querySelector(".ewcb-chat-header");
#messages = document.querySelector("#ewcb-messages");
#input = document.querySelector("#ewcb-input");
#form = document.querySelector("#ewcb-form");
#openBtn = document.querySelector("#ewcb-open-btn");
#stopBtn = document.querySelector("#ewcb-stop");
#closeBtn = document.querySelector("#ewcb-close-btn");
#chatWin = document.querySelector("#ewcb-chat-window");
#floatingIcon = document.querySelector("#ewcb-icon");
#floatingIconBadge = document.querySelector(".ewcb-btn-badge");
#welcomeBubble = null;
constructor(config) {
this.#config = config;
this.#applyTheme();
this.#setHeader();
this.#setFloatingIcon();
this.setTypingDotDuration();
}
setupEventHandlers({ onOpen, onSend, onStop }) {
this.#openBtn.onclick = () => { this.openChat(); onOpen(); };
this.#stopBtn.onclick = () => { onStop(); };
this.#closeBtn.onclick = () => { this.closeChat(); };
this.#form.onsubmit = (e) => {
e.preventDefault();
const val = this.#input.value.trim();
if (!val) return;
this.appendUserMessage(val);
this.clearInput();
onSend(val);
};
}
setInputEnabled(enabled) {
this.#input.disabled = !enabled;
this.#form.querySelector("button[type=submit]").disabled = !enabled;
this.#stopBtn.disabled = enabled;
}
openChat() {
this.#chatWin.style.display = "flex";
this.#floatingIconBadge.style.display = "none";
setTimeout(() => this.focusInput(), 180);
this.hideWelcomeBubble();
}
closeChat() { this.#chatWin.style.display = "none"; }
renderWelcomeBubble() {
this.#removeElement(this.#welcomeBubble);
const bubble = document.createElement('div');
bubble.className = 'ewcb-welcome-bubble';
bubble.textContent = this.#config.welcomeBubble;
bubble.onclick = () => {
this.openChat();
};
document.body.appendChild(bubble);
this.#welcomeBubble = bubble;
}
hideWelcomeBubble() {
if (this.#welcomeBubble) this.#welcomeBubble.style.display = 'none';
}
/** CORE: Render bot message HTML, always via this helper */
#renderBotMessageHTML(text, renderMarkdown = true) {
return `
<img src="${this.#config.botAvatar}" class="ewcb-avatar" alt="Bot Avatar" />
<div class="ewcb-message-content">${renderMarkdown ? marked.parse(text) : text}</div>
`;
}
appendBotMessage(text, element = null, renderMarkdown = true) {
const el = element || this.#createBotMessage();
el.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#append(el);
}
createStreamingBotMessage() {
const element = this.#createBotMessage();
this.#append(element);
return element;
}
updateStreamingBotMessage(element, text, renderMarkdown = true) {
element.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#scrollToBottom();
}
#scrollToBottom() {
this.#messages.scrollTop = this.#messages.scrollHeight;
}
appendUserMessage(text) {
const msg = this.#createUserMessage(text);
this.#append(msg);
}
#createBotMessage() {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-bot';
return msg;
}
#createUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-user';
msg.innerHTML = `<div class="ewcb-message-content">${text}</div>`;
return msg;
}
showTypingIndicator() {
this.hideTypingIndicator()
const indicator = document.createElement('div');
indicator.className = 'ewcb-typing-indicator';
indicator.innerHTML = `
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
`;
this.#append(indicator);
}
hideTypingIndicator() {
this.#removeElement(this.#messages.querySelector('.ewcb-typing-indicator'));
}
clearInput() { this.#input.value = ''; }
focusInput() { this.#input.focus(); }
setTypingDotDuration() {
const delayMs = Number(this.#config.typingDelay) || 1200;
const durationSec = Math.max(0.6, delayMs / 1000 * 0.66);
this.#container.style.setProperty('--typingDotDuration', `${durationSec}s`);
}
#append(msgNode) {
this.#messages.appendChild(msgNode);
this.#scrollToBottom();
}
#removeElement(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}
#applyTheme() {
Object.entries(this.#config).forEach(([k, v]) => {
if (
typeof v === "string" &&
(k.endsWith('Color') || k.endsWith('Bubble') || k.endsWith('Text') || k === "buttonColor")
) {
this.#container.style.setProperty(`--${k}`, v);
}
});
}
#setHeader() {
this.#header.querySelector("#ewcb-header-icon").src = this.#config.iconUrl;
this.#header.querySelector("#ewcb-chatbot-name").textContent = this.#config.chatbotName;
}
#setFloatingIcon() {
this.#floatingIcon.src = this.#config.iconUrl;
}
}
================================================
FILE: aula03-recebendo-como-stream/botData/chatbot-config.json
================================================
{
"primaryColor": "#23A267",
"chatbotName": "EW Academy AI Assistant",
"buttonColor": "#1D7A4B",
"backgroundColor": "#111e16",
"headerColor": "#172a21",
"userBubble": "#2c4636",
"botBubble": "#183224",
"userText": "#fff",
"botText": "#fff",
"iconUrl": "./botData/avatar.webp",
"botAvatar": "./botData/avatar.webp",
"welcomeBubble": "Olá! como podemos te ajudar hoje?",
"firstBotMessage": "Me conta, o que você quer saber sobre a EW Academy?",
"typingDelay": 0.5
}
================================================
FILE: aula03-recebendo-como-stream/botData/systemPrompt.txt
================================================
Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.
Responda apenas com base nos dados do contexto fornecido (não utilize informações externas).
Formate todas as respostas em **markdown**.
**Instruções:**
- Seja objetivo e resumido (no máximo mil caracteres), com tom amigável e profissional.
- Palavras como "me conte sobre" geralmente remetem aos cursos, então tente verificar primeiro se ele está falando de um curso especifico ou pergunte para obter mais contexto
- Sempre inclua links para fácil acesso.
- Não repita cursos que aparecem em mais de uma trilha.
- O idioma padrão é Português do brasil, os que tem idioma, possuem uma anotaçao como (in English)
- Apresente primeiro a lista de cursos relevantes, depois suas trilhas relacionadas.
- Ao final de cada resposta, pergunte o que o usuário deseja fazer em seguida (ex: “Deseja o link do curso, da trilha ou acessar outra informação?”).
- Pergunte explicitamente se o usuário quer o link do curso/trilha mencionados ou de alguma página relacionada.
- Se a pergunta é relevante ao contexto fornecido, diga que não pode ajudar com isso e dê sugestões
**Importante:**
Aqui está o arquivo `llms.txt` com todos os links e informações contextuais:
================================================
FILE: aula03-recebendo-como-stream/index.html
================================================
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="./sdk/src/index.js"></script>
</body>
</html>
================================================
FILE: aula03-recebendo-como-stream/llms.txt
================================================
# EW Academy - Aprenda com as melhores trilhas de aprendizado
Descubra as trilhas de aprendizado da EW Academy e transforme sua carreira com cursos de alta qualidade em JavaScript, DevOps, Testes e muito mais.
## Cursos
## Trilhas de Aprendizado que possuem os cursos acima
### Trilha Cursos Livres - Projetos
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando o Multi-Upload de Arquivos do Google Drive com Testes Automatizados](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-o-multi-upload-de-arquivos-do-google-drive-com-testes-automatizados/): Um projeto que demonstra como implementar testes automatizados em um sistema de upload de arquivos.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Criando um aplicativo de mensagens usando apenas linha de comando](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-um-aplicativo-de-mensagens-usando-apenas-linha-de-comando/): Um projeto que demonstra como criar um aplicativo de mensagens simples utilizando a linha de comando.
### AI para Devs
- [Machine learning para Devs](https://ew.academy/trilha/cursos-livres-projetos/curso/machine-learning-para-devs/): Introdução ao ML para devs (com aplicações práticas).
- [Inteligencia artificial para desenvolvedores web](https://ew.academy/trilha/trilha-ai-para-devs/): Curso completo que ensina desde machine learning à inteligencia artificial aplicada a projetos.
### Trilha Javascript Expert
- [Masterclass: Como Consegui Minha Vaga na Gringa](https://ew.academy/trilha/javascript-expert/curso/masterclass-como-consegui-minha-vaga-na-gringa/): Uma masterclass que compartilha experiências e dicas para conseguir uma vaga no exterior.
- [Formação JavaScript Expert](https://ew.academy/trilha/javascript-expert/curso/formacao-javascript-expert/): Um programa de formação completo para se tornar um especialista em JavaScript.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
### Trilha Testes Automatizados para Devs
- [Método Testes Automatizados em JavaScript](https://ew.academy/trilha/trilha-testes-automatizados-p-devs/curso/metodo-testes-automatizados-em-javascript/): Um curso que apresenta um método eficaz para implementar testes automatizados em JavaScript.
## Acesso e Inscrição
- [Acesse](https://play.ewacademy.com.br/auth/login): Link para acessar a plataforma da EW Academy e acessar conteúdos gratuitos.
- [Quero me matricular](https://pay.ew.academy/checkout/assinatura-geral): Um botão para iniciar o processo de matrícula.
- [Acesse nossa Newsletter!](https://sndflw.com/i/R8P3aLG40NGrWGmHdBrU): Inscreva-se na newsletter para receber atualizações e novidades.
- [A nova dimensão da EW Academy chegou.](https://now.ew.academy/lancamento-assinatura): Anúncio sobre as novas ofertas e melhorias na EW Academy.
## FAQ
O que está incluso na assinatura?
### Com a assinatura, você tem acesso completo à plataforma da EW Academy, incluindo:
- Todos os cursos disponíveis
- Trilhas organizadas por nível e objetivo
- Projetos práticos
- Certificados de conclusão
- Comunidade ativa no Discord
E novos conteúdos que forem lançados durante a sua assinatura por um periodo anual
### Terei acesso aos conteúdos futuros ou preciso pagar por eles?
Durante o período da sua assinatura, *todos os novos cursos, atualizações e trilhas são liberados automaticamente pra você*, sem nenhum custo adicional.
Você paga uma vez e tem acesso completo até o fim do seu plano.
### Já comprei cursos da EW. A assinatura vale a pena pra mim?
Com certeza!
Além de garantir acesso a todo o catálogo, pode explorar novas trilhas e acompanhar todos os novos cursos que forem lançados.
### Os cursos são atualizados com frequência?
Sim! Os conteúdos são lançados constantemente para manter você atualizado.
### E se eu já comprei um curso com acesso por 2 anos? Perco esse tempo?
Não! O seu tempo restante continua valendo.
Se você tem, por exemplo, 1 ano restante de acesso ao curso de JavaScript e assina a EW por 1 ano, você passa a ter 2 anos de acesso total àquele curso (1 ano da assinatura + 1 ano já adquirido).
É tudo somado, sem prejuízo.
### A assinatura é vitalícia?
Não. A assinatura é por tempo determinado, com opção de plano anual.
Durante esse período, você tem acesso total à plataforma. Ao final do plano, o acesso é encerrado — mas você pode renovar e continuar de onde parou.
### Tem certificado?
Sim! Todos os cursos oferecem certificados digitais de conclusão, que você pode usar para comprovar sua evolução profissional.
### Existe alguma comunidade de alunos?
Sim! Todos os alunos têm acesso ao nosso servidor exclusivo no Discord, onde você pode tirar dúvidas, conversar com outros devs, compartilhar conquistas e acompanhar novidades.
### Tem suporte se eu travar em algum assunto?
Temos uma comunidade ativa no Discord e suporte dedicado para tirar suas dúvidas.
### Se eu me arrepender, posso cancelar?
Sim! Você pode solicitar o cancelamento dentro do prazo legal vigente. E mesmo que decida sair, a porta estará sempre aberta pra voltar no futuro.
================================================
FILE: aula03-recebendo-como-stream/package.json
================================================
{
"name": "ai-chat-button",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "browser-sync -w . --server --files 'sdk/**' --port 3000"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"browser-sync": "^3.0.4"
}
}
================================================
FILE: aula03-recebendo-como-stream/sdk/ew-chatbot.css
================================================
#ewcb-widget {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ewcb-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 9999;
background: #fff;
border: none;
border-radius: 50%;
width: 68px;
height: 68px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* Only the soft halo */
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.50);
transition: background 0.2s, transform 0.18s, box-shadow 0.2s;
}
.ewcb-btn img {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
display: block;
}
.ewcb-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn:hover {
background: #fafbfc;
box-shadow: 0 4px 28px 0 rgba(80, 80, 80, 0.50);
transform: scale(1.05);
}
.ewcb-welcome-bubble {
position: fixed;
bottom: 68px;
right: 110px;
background: var(--botBubble, #f6f8fa);
color: var(--botText, #1b5e20);
padding: 13px 18px;
font-size: 1em;
border-radius: 18px 18px 18px 6px;
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.09);
max-width: 260px;
z-index: 9999;
white-space: pre-line;
cursor: pointer;
display: flex;
align-items: center;
gap: 13px;
animation: ewcb-toast-in 0.44s cubic-bezier(.5, 1.7, .7, 1) both;
transition: opacity .14s, box-shadow .2s;
}
.ewcb-welcome-bubble:hover {
box-shadow: 0 4px 24px 0 rgba(80, 80, 80, 0.19);
opacity: 0.85;
}
@media (max-width: 500px) {
.ewcb-welcome-bubble {
right: 78px;
font-size: 0.95em;
max-width: 150px;
}
}
@keyframes ewcb-toast-in {
from {
opacity: 0;
transform: translateX(60px) scale(0.98);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.ewcb-btn-avatar-wrapper {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
}
.ewcb-btn-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 19px;
height: 19px;
background: #e32424;
color: #fff;
font-size: 0.98em;
font-weight: bold;
border-radius: 50%;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px #0002;
z-index: 1;
letter-spacing: 0.01em;
pointer-events: none;
transition: transform 0.15s;
}
.ewcb-btn-badge.ewcb-badge-animate {
transform: scale(1.15);
animation: ewcb-badge-pop 0.3s;
}
@keyframes ewcb-badge-pop {
0% {
transform: scale(0.7);
}
70% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
.ewcb-chat-window {
position: fixed;
bottom: 108px;
right: 32px;
z-index: 99999;
background: var(--backgroundColor);
color: var(--primaryColor);
width: 370px;
max-width: 98vw;
height: 520px;
max-height: 80vh;
border-radius: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: ewcb-fadein 0.23s cubic-bezier(.5, 1.7, .7, 1) both;
}
@keyframes ewcb-fadein {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: none;
}
}
.ewcb-chat-header {
background: var(--headerColor);
color: #fff;
padding: 14px 18px;
border-top-left-radius: 18px;
border-top-right-radius: 18px;
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
border-bottom: 1px solid #22242d50;
}
.ewcb-chat-header-logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.ewcb-chatbot-name {
font-size: 1.1em;
}
.ewcb-close-btn {
margin-left: auto;
background: none;
border: none;
color: #8f99b2;
font-size: 1.6em;
cursor: pointer;
font-weight: bold;
line-height: 1;
opacity: 0.8;
padding: 0 2px;
transition: opacity 0.2s;
}
.ewcb-close-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-close-btn:hover {
opacity: 1;
color: #fff;
}
.ewcb-chat-body {
flex: 1 1 auto;
padding: 16px 16px 6px 16px;
overflow-y: auto;
background: var(--backgroundColor);
display: flex;
flex-direction: column;
gap: 12px;
}
.ewcb-message {
display: flex;
align-items: flex-end;
gap: 10px;
margin-bottom: 1px;
}
.ewcb-message-bot {
align-items: flex-start;
}
.ewcb-message-bot .ewcb-avatar {
order: 0;
}
.ewcb-message-user {
flex-direction: row-reverse;
}
.ewcb-message-content {
padding: 12px 18px;
border-radius: 15px;
font-size: 1em;
max-width: 84%;
word-break: break-word;
line-height: 1.5;
background: var(--userBubble);
color: var(--userText);
transition: background .18s, color .18s;
}
.ewcb-message-bot .ewcb-message-content {
background: var(--botBubble);
color: var(--botText);
border-bottom-left-radius: 6px;
}
.ewcb-message-user .ewcb-message-content {
background: var(--userBubble);
color: var(--userText);
border-bottom-right-radius: 6px;
}
.ewcb-message-content a {
color: white
}
.ewcb-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: #111;
object-fit: cover;
}
.ewcb-chat-footer {
display: flex;
gap: 9px;
padding: 12px 14px;
background: var(--headerColor);
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
border-top: 1px solid #1e202c70;
}
.ewcb-btn-main {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
box-shadow: 0 2px 8px #23A26720;
}
.ewcb-btn-main:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn-main:hover {
background: #23A267;
}
#ewcb-input {
flex: 1 1 auto;
border-radius: 10px;
border: none;
padding: 10px 12px;
font-size: 1em;
background: var(--backgroundColor);
color: #fff;
outline: none;
transition: border .16s;
}
#ewcb-input:focus {
border: 1.5px solid var(--primaryColor);
}
.ewcb-chat-footer button:disabled,
#ewcb-input:disabled {
opacity: 0.4;
}
.ewcb-chat-footer button[type="submit"] {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
}
.ewcb-chat-footer button[type="submit"]:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-chat-footer button[type="submit"]:hover {
background: #23A267;
}
#ewcb-stop {
background: transparent;
/* Sem fundo */
border: none;
color: #f43f5e;
/* Cor do emoji/texto */
font-weight: bold;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.4em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
box-shadow: none;
}
#ewcb-stop:focus {
outline: 2px solid #f43f5e44;
outline-offset: 2px;
}
#ewcb-stop:hover {
background: #f43f5e11;
/* Fundo levemente rosado só no hover */
color: #be123c;
/* Fica um tom mais escuro */
}
@media (max-width: 500px) {
#ewcb-stop {
padding: 10px 14px;
font-size: 1em;
}
}
@media (max-width: 500px) {
.ewcb-chat-window {
right: 0;
left: 0;
width: 98vw;
max-width: none;
border-radius: 0 0 18px 18px;
}
}
.ewcb-typing-indicator {
display: flex;
align-items: center;
gap: 3px;
margin: 6px 0 2px 38px;
/* Indent like bot messages */
height: 26px;
}
.ewcb-typing-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--botText, #23A267);
opacity: 0.65;
animation: ewcb-typing-blink var(--typingDotDuration, 0.5s) infinite;
}
.ewcb-typing-dot:nth-child(2) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 8);
}
.ewcb-typing-dot:nth-child(3) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 4);
}
.ewcb-model-progress {
margin: 16px 0 6px 0;
padding: 5px 0 2px 0;
}
.ewcb-progress-label {
font-size: 0.98em;
color: #888;
margin-bottom: 7px;
margin-left: 2px;
}
.ewcb-progress-bar-bg {
width: 100%;
height: 19px;
background: #23272a18;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-bottom: 3px;
}
.ewcb-progress-bar-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #23a267 60%, #4ee185 100%);
border-radius: 12px;
color: #fff;
font-weight: 500;
font-size: 0.96em;
line-height: 19px;
text-align: right;
padding-right: 10px;
transition: width 0.2s cubic-bezier(.4, 1.4, .7, 1);
letter-spacing: 0.03em;
}
@keyframes ewcb-typing-blink {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Aplica apenas à área do chat */
.ewcb-chat-body,
#ewcb-messages {
scrollbar-width: thin;
scrollbar-color: var(--primaryColor, #23A267) var(--backgroundColor, #181C24);
}
/* Para navegadores baseados em Webkit (Chrome, Edge, Safari) */
.ewcb-chat-body::-webkit-scrollbar,
#ewcb-messages::-webkit-scrollbar {
width: 8px;
background: var(--backgroundColor, #181C24);
}
.ewcb-chat-body::-webkit-scrollbar-thumb,
#ewcb-messages::-webkit-scrollbar-thumb {
background: var(--primaryColor, #23A267);
border-radius: 10px;
border: 2px solid var(--backgroundColor, #181C24);
}
/* Opcional: ao passar o mouse */
.ewcb-chat-body::-webkit-scrollbar-thumb:hover,
#ewcb-messages::-webkit-scrollbar-thumb:hover {
background: var(--botBubble, #23A267cc);
}
================================================
FILE: aula03-recebendo-como-stream/sdk/ew-chatbot.html
================================================
<div id="ewcb-widget">
<button id="ewcb-open-btn" class="ewcb-btn" aria-label="Abrir chat">
<span class="ewcb-btn-avatar-wrapper">
<img id="ewcb-icon" alt="Chatbot" />
<span class="ewcb-btn-badge">1</span>
</span>
</button>
<div class="ewcb-chat-window" id="ewcb-chat-window" style="display:none">
<div class="ewcb-chat-header">
<img src="" alt="Bot logo" class="ewcb-chat-header-logo" id="ewcb-header-icon" />
<span class="ewcb-chatbot-name" id="ewcb-chatbot-name"></span>
<button class="ewcb-close-btn" id="ewcb-close-btn" aria-label="Close chat">×</button>
</div>
<div class="ewcb-chat-body" id="ewcb-messages">
</div>
<form class="ewcb-chat-footer" id="ewcb-form" autocomplete="off">
<input type="text" id="ewcb-input" placeholder="Digite sua mensagem..." autocomplete="off" />
<button type="submit" id="ewcb-submit">Enviar</button>
<button type="button" id="ewcb-stop" alt="Parar parar geração de IA">🛑</button>
</form>
</div>
</div>
================================================
FILE: aula03-recebendo-como-stream/sdk/src/controllers/chatBotController.js
================================================
// @ts-check
/**
* @typedef {import("../views/chatBotView.js").ChatbotView} ChatBotView
* @typedef {import("../services/promptService.js").PromptService} PromptService
*/
export class ChatbotController {
#chatbotView;
#promptService;
/**
* @param {Object} deps - Dependencies for the class.
* @param {ChatBotView} deps.chatbotView - The chatbot view instance.
* @param {PromptService} deps.promptService - The prompt service instance.
*/
constructor({ chatbotView, promptService }) {
this.#chatbotView = chatbotView;
this.#promptService = promptService;
}
async init({ firstBotMessage, text }) {
this.#setupEvents();
this.#chatbotView.renderWelcomeBubble();
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.appendBotMessage(firstBotMessage, null, false);
return this.#promptService.init(text)
}
#setupEvents() {
this.#chatbotView.setupEventHandlers({
onOpen: this.#onOpen.bind(this),
onSend: this.#chatBotReply.bind(this),
onStop: this.#handleStop.bind(this),
});
}
#handleStop() {
}
async #chatBotReply(userMsg) {
this.#chatbotView.showTypingIndicator();
this.#chatbotView.setInputEnabled(false);
const contentNode = this.#chatbotView.createStreamingBotMessage()
const response = this.#promptService.prompt(userMsg)
let fullResponse = ''
let lastMessage = 'noop'
const updateText = () => {
if (!fullResponse) return
if (fullResponse === lastMessage) return
lastMessage = fullResponse
this.#chatbotView.hideTypingIndicator();
this.#chatbotView.updateStreamingBotMessage(contentNode, fullResponse)
}
const intervalId = setInterval(updateText, 200)
const stopGenerating = () => {
clearInterval(intervalId)
updateText()
this.#chatbotView.setInputEnabled(true)
}
for await (const chunk of response) {
if (!chunk) continue
fullResponse += chunk
}
console.log('Full response', fullResponse)
stopGenerating()
}
async #onOpen() {
const errors = this.#checkRequirements()
if (errors.length) {
const messages = errors.join('\n\n')
this.#chatbotView.appendBotMessage(
messages
)
this.#chatbotView.setInputEnabled(false);
return
}
this.#chatbotView.setInputEnabled(true);
}
#checkRequirements() {
const errors = []
// @ts-ignore
const iChrome = window.chrome
if (!iChrome) {
errors.push(
'⚠️ Este recurso só funciona no Google Chrome ou Chrome Canary (versão recente).'
)
}
if (!('LanguageModel' in window)) {
errors.push("⚠️ As APIs nativas de IA não estão ativas.");
errors.push("Ative a seguinte flag em chrome://flags/:");
errors.push("- Prompt API for Gemini Nano (chrome://flags/#prompt-api-for-gemini-nano)");
errors.push("Depois reinicie o Chrome e tente novamente.");
}
return errors
}
}
================================================
FILE: aula03-recebendo-como-stream/sdk/src/index.js
================================================
// @ts-check
import { ChatbotView } from './views/chatBotView.js';
import { PromptService } from './services/promptService.js'
import { ChatbotController } from './controllers/chatBotController.js';
(async () => {
const root = new URL('../../', import.meta.url);
const fromMainProject = (path) => new URL(path, root).toString();
const [css, html, systemPrompt, config, llmsTxt] = await Promise.all([
fetch(fromMainProject('./sdk/ew-chatbot.css')).then(r => r.text()),
fetch(fromMainProject('./sdk/ew-chatbot.html')).then(r => r.text()),
fetch('./botData/systemPrompt.txt').then(r => r.text()),
fetch('./botData/chatbot-config.json').then(r => r.json()),
fetch('./llms.txt').then(r => r.text()),
]);
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
const promptService = new PromptService();
const chatbotView = new ChatbotView(config);
const controller = new ChatbotController({ chatbotView, promptService });
const text = systemPrompt.concat('\n', llmsTxt)
controller.init({
firstBotMessage: config.firstBotMessage,
text,
});
})();
================================================
FILE: aula03-recebendo-como-stream/sdk/src/services/promptService.js
================================================
export class PromptService {
#messages = []
#session = null
async init(initialPrompts) {
if (!window.LanguageModel) return
this.#messages.push({
role: 'system',
content: initialPrompts
})
return this.#createSession()
}
async #createSession() {
this.#session = await LanguageModel.create({
initialPrompts: this.#messages,
expectedInputLanguages: ['pt']
})
return this.#session
}
prompt(text) {
this.#messages.push({
role: 'user',
content: text,
})
return this.#session.promptStreaming(this.#messages)
}
}
================================================
FILE: aula03-recebendo-como-stream/sdk/src/views/chatBotView.js
================================================
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
export class ChatbotView {
#config;
#container = document.querySelector("#ewcb-widget");
#header = document.querySelector(".ewcb-chat-header");
#messages = document.querySelector("#ewcb-messages");
#input = document.querySelector("#ewcb-input");
#form = document.querySelector("#ewcb-form");
#openBtn = document.querySelector("#ewcb-open-btn");
#stopBtn = document.querySelector("#ewcb-stop");
#closeBtn = document.querySelector("#ewcb-close-btn");
#chatWin = document.querySelector("#ewcb-chat-window");
#floatingIcon = document.querySelector("#ewcb-icon");
#floatingIconBadge = document.querySelector(".ewcb-btn-badge");
#welcomeBubble = null;
constructor(config) {
this.#config = config;
this.#applyTheme();
this.#setHeader();
this.#setFloatingIcon();
this.setTypingDotDuration();
}
setupEventHandlers({ onOpen, onSend, onStop }) {
this.#openBtn.onclick = () => { this.openChat(); onOpen(); };
this.#stopBtn.onclick = () => { onStop(); };
this.#closeBtn.onclick = () => { this.closeChat(); };
this.#form.onsubmit = (e) => {
e.preventDefault();
const val = this.#input.value.trim();
if (!val) return;
this.appendUserMessage(val);
this.clearInput();
onSend(val);
};
}
setInputEnabled(enabled) {
this.#input.disabled = !enabled;
this.#form.querySelector("button[type=submit]").disabled = !enabled;
this.#stopBtn.disabled = enabled;
}
openChat() {
this.#chatWin.style.display = "flex";
this.#floatingIconBadge.style.display = "none";
setTimeout(() => this.focusInput(), 180);
this.hideWelcomeBubble();
}
closeChat() { this.#chatWin.style.display = "none"; }
renderWelcomeBubble() {
this.#removeElement(this.#welcomeBubble);
const bubble = document.createElement('div');
bubble.className = 'ewcb-welcome-bubble';
bubble.textContent = this.#config.welcomeBubble;
bubble.onclick = () => {
this.openChat();
};
document.body.appendChild(bubble);
this.#welcomeBubble = bubble;
}
hideWelcomeBubble() {
if (this.#welcomeBubble) this.#welcomeBubble.style.display = 'none';
}
/** CORE: Render bot message HTML, always via this helper */
#renderBotMessageHTML(text, renderMarkdown = true) {
return `
<img src="${this.#config.botAvatar}" class="ewcb-avatar" alt="Bot Avatar" />
<div class="ewcb-message-content">${renderMarkdown ? marked.parse(text) : text}</div>
`;
}
appendBotMessage(text, element = null, renderMarkdown = true) {
const el = element || this.#createBotMessage();
el.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#append(el);
}
createStreamingBotMessage() {
const element = this.#createBotMessage();
this.#append(element);
return element;
}
updateStreamingBotMessage(element, text, renderMarkdown = true) {
element.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#scrollToBottom();
}
#scrollToBottom() {
this.#messages.scrollTop = this.#messages.scrollHeight;
}
appendUserMessage(text) {
const msg = this.#createUserMessage(text);
this.#append(msg);
}
#createBotMessage() {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-bot';
return msg;
}
#createUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-user';
msg.innerHTML = `<div class="ewcb-message-content">${text}</div>`;
return msg;
}
showTypingIndicator() {
this.hideTypingIndicator()
const indicator = document.createElement('div');
indicator.className = 'ewcb-typing-indicator';
indicator.innerHTML = `
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
`;
this.#append(indicator);
}
hideTypingIndicator() {
this.#removeElement(this.#messages.querySelector('.ewcb-typing-indicator'));
}
clearInput() { this.#input.value = ''; }
focusInput() { this.#input.focus(); }
setTypingDotDuration() {
const delayMs = Number(this.#config.typingDelay) || 1200;
const durationSec = Math.max(0.6, delayMs / 1000 * 0.66);
this.#container.style.setProperty('--typingDotDuration', `${durationSec}s`);
}
#append(msgNode) {
this.#messages.appendChild(msgNode);
this.#scrollToBottom();
}
#removeElement(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}
#applyTheme() {
Object.entries(this.#config).forEach(([k, v]) => {
if (
typeof v === "string" &&
(k.endsWith('Color') || k.endsWith('Bubble') || k.endsWith('Text') || k === "buttonColor")
) {
this.#container.style.setProperty(`--${k}`, v);
}
});
}
#setHeader() {
this.#header.querySelector("#ewcb-header-icon").src = this.#config.iconUrl;
this.#header.querySelector("#ewcb-chatbot-name").textContent = this.#config.chatbotName;
}
#setFloatingIcon() {
this.#floatingIcon.src = this.#config.iconUrl;
}
}
================================================
FILE: aula04-abortando-requisicoes/botData/chatbot-config.json
================================================
{
"primaryColor": "#23A267",
"chatbotName": "EW Academy AI Assistant",
"buttonColor": "#1D7A4B",
"backgroundColor": "#111e16",
"headerColor": "#172a21",
"userBubble": "#2c4636",
"botBubble": "#183224",
"userText": "#fff",
"botText": "#fff",
"iconUrl": "./botData/avatar.webp",
"botAvatar": "./botData/avatar.webp",
"welcomeBubble": "Olá! como podemos te ajudar hoje?",
"firstBotMessage": "Me conta, o que você quer saber sobre a EW Academy?",
"typingDelay": 0.5
}
================================================
FILE: aula04-abortando-requisicoes/botData/systemPrompt.txt
================================================
Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.
Responda apenas com base nos dados do contexto fornecido (não utilize informações externas).
Formate todas as respostas em **markdown**.
**Instruções:**
- Seja objetivo e resumido (no máximo mil caracteres), com tom amigável e profissional.
- Palavras como "me conte sobre" geralmente remetem aos cursos, então tente verificar primeiro se ele está falando de um curso especifico ou pergunte para obter mais contexto
- Sempre inclua links para fácil acesso.
- Não repita cursos que aparecem em mais de uma trilha.
- O idioma padrão é Português do brasil, os que tem idioma, possuem uma anotaçao como (in English)
- Apresente primeiro a lista de cursos relevantes, depois suas trilhas relacionadas.
- Ao final de cada resposta, pergunte o que o usuário deseja fazer em seguida (ex: “Deseja o link do curso, da trilha ou acessar outra informação?”).
- Pergunte explicitamente se o usuário quer o link do curso/trilha mencionados ou de alguma página relacionada.
- Se a pergunta é relevante ao contexto fornecido, diga que não pode ajudar com isso e dê sugestões
**Importante:**
Aqui está o arquivo `llms.txt` com todos os links e informações contextuais:
================================================
FILE: aula04-abortando-requisicoes/index.html
================================================
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EW Academy AI Chatbot</title>
<link rel="icon" type="image/x-icon" href="./botData/avatar.webp">
</head>
<body>
<script type="module" src="./sdk/src/index.js"></script>
</body>
</html>
================================================
FILE: aula04-abortando-requisicoes/llms.txt
================================================
# EW Academy - Aprenda com as melhores trilhas de aprendizado
Descubra as trilhas de aprendizado da EW Academy e transforme sua carreira com cursos de alta qualidade em JavaScript, DevOps, Testes e muito mais.
## Cursos
## Trilhas de Aprendizado que possuem os cursos acima
### Trilha Cursos Livres - Projetos
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando o Multi-Upload de Arquivos do Google Drive com Testes Automatizados](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-o-multi-upload-de-arquivos-do-google-drive-com-testes-automatizados/): Um projeto que demonstra como implementar testes automatizados em um sistema de upload de arquivos.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Criando um aplicativo de mensagens usando apenas linha de comando](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-um-aplicativo-de-mensagens-usando-apenas-linha-de-comando/): Um projeto que demonstra como criar um aplicativo de mensagens simples utilizando a linha de comando.
### AI para Devs
- [Machine learning para Devs](https://ew.academy/trilha/cursos-livres-projetos/curso/machine-learning-para-devs/): Introdução ao ML para devs (com aplicações práticas).
- [Inteligencia artificial para desenvolvedores web](https://ew.academy/trilha/trilha-ai-para-devs/): Curso completo que ensina desde machine learning à inteligencia artificial aplicada a projetos.
### Trilha Javascript Expert
- [Masterclass: Como Consegui Minha Vaga na Gringa](https://ew.academy/trilha/javascript-expert/curso/masterclass-como-consegui-minha-vaga-na-gringa/): Uma masterclass que compartilha experiências e dicas para conseguir uma vaga no exterior.
- [Formação JavaScript Expert](https://ew.academy/trilha/javascript-expert/curso/formacao-javascript-expert/): Um programa de formação completo para se tornar um especialista em JavaScript.
- [Criando seu próprio app Zoom com WebRTC e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/criando-seu-proprio-app-zoom-com-web-rtc-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo de videoconferência semelhante ao Zoom.
- [Recriando o App Club House com Javascript e WebSockets](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-app-club-house-com-javascript-e-web-sockets/): Um projeto que ensina a desenvolver um aplicativo semelhante ao Club House utilizando JavaScript e WebSockets.
- [Reimaginando uma Rádio Musical Online com Node.js e Streams](https://ew.academy/trilha/cursos-livres-projetos/curso/reimaginando-uma-radio-musical-online-com-node-js-e-streams/): Um projeto que utiliza Node.js e Streams para criar uma rádio musical online.
- [Recriando o player de vídeo da Netflix](https://ew.academy/trilha/cursos-livres-projetos/curso/recriando-o-player-de-video-da-netflix/): Um projeto que foca na construção de um player de vídeo similar ao da Netflix.
- [Mastering JavaScript Streams (in English)](https://ew.academy/trilha/cursos-livres-projetos/curso/mastering-javascript-streams/): Um curso avançado sobre o uso de Streams em JavaScript, disponível em inglês.
### Trilha Testes Automatizados para Devs
- [Método Testes Automatizados em JavaScript](https://ew.academy/trilha/trilha-testes-automatizados-p-devs/curso/metodo-testes-automatizados-em-javascript/): Um curso que apresenta um método eficaz para implementar testes automatizados em JavaScript.
## Acesso e Inscrição
- [Acesse](https://play.ewacademy.com.br/auth/login): Link para acessar a plataforma da EW Academy e acessar conteúdos gratuitos.
- [Quero me matricular](https://pay.ew.academy/checkout/assinatura-geral): Um botão para iniciar o processo de matrícula.
- [Acesse nossa Newsletter!](https://sndflw.com/i/R8P3aLG40NGrWGmHdBrU): Inscreva-se na newsletter para receber atualizações e novidades.
- [A nova dimensão da EW Academy chegou.](https://now.ew.academy/lancamento-assinatura): Anúncio sobre as novas ofertas e melhorias na EW Academy.
## FAQ
O que está incluso na assinatura?
### Com a assinatura, você tem acesso completo à plataforma da EW Academy, incluindo:
- Todos os cursos disponíveis
- Trilhas organizadas por nível e objetivo
- Projetos práticos
- Certificados de conclusão
- Comunidade ativa no Discord
E novos conteúdos que forem lançados durante a sua assinatura por um periodo anual
### Terei acesso aos conteúdos futuros ou preciso pagar por eles?
Durante o período da sua assinatura, *todos os novos cursos, atualizações e trilhas são liberados automaticamente pra você*, sem nenhum custo adicional.
Você paga uma vez e tem acesso completo até o fim do seu plano.
### Já comprei cursos da EW. A assinatura vale a pena pra mim?
Com certeza!
Além de garantir acesso a todo o catálogo, pode explorar novas trilhas e acompanhar todos os novos cursos que forem lançados.
### Os cursos são atualizados com frequência?
Sim! Os conteúdos são lançados constantemente para manter você atualizado.
### E se eu já comprei um curso com acesso por 2 anos? Perco esse tempo?
Não! O seu tempo restante continua valendo.
Se você tem, por exemplo, 1 ano restante de acesso ao curso de JavaScript e assina a EW por 1 ano, você passa a ter 2 anos de acesso total àquele curso (1 ano da assinatura + 1 ano já adquirido).
É tudo somado, sem prejuízo.
### A assinatura é vitalícia?
Não. A assinatura é por tempo determinado, com opção de plano anual.
Durante esse período, você tem acesso total à plataforma. Ao final do plano, o acesso é encerrado — mas você pode renovar e continuar de onde parou.
### Tem certificado?
Sim! Todos os cursos oferecem certificados digitais de conclusão, que você pode usar para comprovar sua evolução profissional.
### Existe alguma comunidade de alunos?
Sim! Todos os alunos têm acesso ao nosso servidor exclusivo no Discord, onde você pode tirar dúvidas, conversar com outros devs, compartilhar conquistas e acompanhar novidades.
### Tem suporte se eu travar em algum assunto?
Temos uma comunidade ativa no Discord e suporte dedicado para tirar suas dúvidas.
### Se eu me arrepender, posso cancelar?
Sim! Você pode solicitar o cancelamento dentro do prazo legal vigente. E mesmo que decida sair, a porta estará sempre aberta pra voltar no futuro.
================================================
FILE: aula04-abortando-requisicoes/package.json
================================================
{
"name": "ai-chat-button",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "browser-sync -w . --server --files 'sdk/**' --port 3000"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"browser-sync": "^3.0.4"
}
}
================================================
FILE: aula04-abortando-requisicoes/sdk/ew-chatbot.css
================================================
#ewcb-widget {
font-family: 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.ewcb-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 9999;
background: #fff;
border: none;
border-radius: 50%;
width: 68px;
height: 68px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* Only the soft halo */
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.50);
transition: background 0.2s, transform 0.18s, box-shadow 0.2s;
}
.ewcb-btn img {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
display: block;
}
.ewcb-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn:hover {
background: #fafbfc;
box-shadow: 0 4px 28px 0 rgba(80, 80, 80, 0.50);
transform: scale(1.05);
}
.ewcb-welcome-bubble {
position: fixed;
bottom: 68px;
right: 110px;
background: var(--botBubble, #f6f8fa);
color: var(--botText, #1b5e20);
padding: 13px 18px;
font-size: 1em;
border-radius: 18px 18px 18px 6px;
box-shadow: 0 2px 18px 0 rgba(80, 80, 80, 0.09);
max-width: 260px;
z-index: 9999;
white-space: pre-line;
cursor: pointer;
display: flex;
align-items: center;
gap: 13px;
animation: ewcb-toast-in 0.44s cubic-bezier(.5, 1.7, .7, 1) both;
transition: opacity .14s, box-shadow .2s;
}
.ewcb-welcome-bubble:hover {
box-shadow: 0 4px 24px 0 rgba(80, 80, 80, 0.19);
opacity: 0.85;
}
@media (max-width: 500px) {
.ewcb-welcome-bubble {
right: 78px;
font-size: 0.95em;
max-width: 150px;
}
}
@keyframes ewcb-toast-in {
from {
opacity: 0;
transform: translateX(60px) scale(0.98);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.ewcb-btn-avatar-wrapper {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
}
.ewcb-btn-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 19px;
height: 19px;
background: #e32424;
color: #fff;
font-size: 0.98em;
font-weight: bold;
border-radius: 50%;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 4px #0002;
z-index: 1;
letter-spacing: 0.01em;
pointer-events: none;
transition: transform 0.15s;
}
.ewcb-btn-badge.ewcb-badge-animate {
transform: scale(1.15);
animation: ewcb-badge-pop 0.3s;
}
@keyframes ewcb-badge-pop {
0% {
transform: scale(0.7);
}
70% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
.ewcb-chat-window {
position: fixed;
bottom: 108px;
right: 32px;
z-index: 99999;
background: var(--backgroundColor);
color: var(--primaryColor);
width: 370px;
max-width: 98vw;
height: 520px;
max-height: 80vh;
border-radius: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: ewcb-fadein 0.23s cubic-bezier(.5, 1.7, .7, 1) both;
}
@keyframes ewcb-fadein {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: none;
}
}
.ewcb-chat-header {
background: var(--headerColor);
color: #fff;
padding: 14px 18px;
border-top-left-radius: 18px;
border-top-right-radius: 18px;
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
border-bottom: 1px solid #22242d50;
}
.ewcb-chat-header-logo {
width: 28px;
height: 28px;
border-radius: 50%;
}
.ewcb-chatbot-name {
font-size: 1.1em;
}
.ewcb-close-btn {
margin-left: auto;
background: none;
border: none;
color: #8f99b2;
font-size: 1.6em;
cursor: pointer;
font-weight: bold;
line-height: 1;
opacity: 0.8;
padding: 0 2px;
transition: opacity 0.2s;
}
.ewcb-close-btn:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-close-btn:hover {
opacity: 1;
color: #fff;
}
.ewcb-chat-body {
flex: 1 1 auto;
padding: 16px 16px 6px 16px;
overflow-y: auto;
background: var(--backgroundColor);
display: flex;
flex-direction: column;
gap: 12px;
}
.ewcb-message {
display: flex;
align-items: flex-end;
gap: 10px;
margin-bottom: 1px;
}
.ewcb-message-bot {
align-items: flex-start;
}
.ewcb-message-bot .ewcb-avatar {
order: 0;
}
.ewcb-message-user {
flex-direction: row-reverse;
}
.ewcb-message-content {
padding: 12px 18px;
border-radius: 15px;
font-size: 1em;
max-width: 84%;
word-break: break-word;
line-height: 1.5;
background: var(--userBubble);
color: var(--userText);
transition: background .18s, color .18s;
}
.ewcb-message-bot .ewcb-message-content {
background: var(--botBubble);
color: var(--botText);
border-bottom-left-radius: 6px;
}
.ewcb-message-user .ewcb-message-content {
background: var(--userBubble);
color: var(--userText);
border-bottom-right-radius: 6px;
}
.ewcb-message-content a {
color: white
}
.ewcb-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: #111;
object-fit: cover;
}
.ewcb-chat-footer {
display: flex;
gap: 9px;
padding: 12px 14px;
background: var(--headerColor);
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
border-top: 1px solid #1e202c70;
}
.ewcb-btn-main {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
box-shadow: 0 2px 8px #23A26720;
}
.ewcb-btn-main:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-btn-main:hover {
background: #23A267;
}
#ewcb-input {
flex: 1 1 auto;
border-radius: 10px;
border: none;
padding: 10px 12px;
font-size: 1em;
background: var(--backgroundColor);
color: #fff;
outline: none;
transition: border .16s;
}
#ewcb-input:focus {
border: 1.5px solid var(--primaryColor);
}
.ewcb-chat-footer button:disabled,
#ewcb-input:disabled {
opacity: 0.4;
}
.ewcb-chat-footer button[type="submit"] {
background: var(--buttonColor);
border: none;
color: #fff;
font-weight: 600;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
transition: background 0.15s, opacity 0.13s;
}
.ewcb-chat-footer button[type="submit"]:focus {
outline: 2px solid var(--primaryColor);
outline-offset: 2px;
}
.ewcb-chat-footer button[type="submit"]:hover {
background: #23A267;
}
#ewcb-stop {
background: transparent;
/* Sem fundo */
border: none;
color: #f43f5e;
/* Cor do emoji/texto */
font-weight: bold;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
font-size: 1.4em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
box-shadow: none;
}
#ewcb-stop:focus {
outline: 2px solid #f43f5e44;
outline-offset: 2px;
}
#ewcb-stop:hover {
background: #f43f5e11;
/* Fundo levemente rosado só no hover */
color: #be123c;
/* Fica um tom mais escuro */
}
@media (max-width: 500px) {
#ewcb-stop {
padding: 10px 14px;
font-size: 1em;
}
}
@media (max-width: 500px) {
.ewcb-chat-window {
right: 0;
left: 0;
width: 98vw;
max-width: none;
border-radius: 0 0 18px 18px;
}
}
.ewcb-typing-indicator {
display: flex;
align-items: center;
gap: 3px;
margin: 6px 0 2px 38px;
/* Indent like bot messages */
height: 26px;
}
.ewcb-typing-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--botText, #23A267);
opacity: 0.65;
animation: ewcb-typing-blink var(--typingDotDuration, 0.5s) infinite;
}
.ewcb-typing-dot:nth-child(2) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 8);
}
.ewcb-typing-dot:nth-child(3) {
animation-delay: calc(var(--typingDotDuration, 1.2s) / 4);
}
.ewcb-model-progress {
margin: 16px 0 6px 0;
padding: 5px 0 2px 0;
}
.ewcb-progress-label {
font-size: 0.98em;
color: #888;
margin-bottom: 7px;
margin-left: 2px;
}
.ewcb-progress-bar-bg {
width: 100%;
height: 19px;
background: #23272a18;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-bottom: 3px;
}
.ewcb-progress-bar-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #23a267 60%, #4ee185 100%);
border-radius: 12px;
color: #fff;
font-weight: 500;
font-size: 0.96em;
line-height: 19px;
text-align: right;
padding-right: 10px;
transition: width 0.2s cubic-bezier(.4, 1.4, .7, 1);
letter-spacing: 0.03em;
}
@keyframes ewcb-typing-blink {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Aplica apenas à área do chat */
.ewcb-chat-body,
#ewcb-messages {
scrollbar-width: thin;
scrollbar-color: var(--primaryColor, #23A267) var(--backgroundColor, #181C24);
}
/* Para navegadores baseados em Webkit (Chrome, Edge, Safari) */
.ewcb-chat-body::-webkit-scrollbar,
#ewcb-messages::-webkit-scrollbar {
width: 8px;
background: var(--backgroundColor, #181C24);
}
.ewcb-chat-body::-webkit-scrollbar-thumb,
#ewcb-messages::-webkit-scrollbar-thumb {
background: var(--primaryColor, #23A267);
border-radius: 10px;
border: 2px solid var(--backgroundColor, #181C24);
}
/* Opcional: ao passar o mouse */
.ewcb-chat-body::-webkit-scrollbar-thumb:hover,
#ewcb-messages::-webkit-scrollbar-thumb:hover {
background: var(--botBubble, #23A267cc);
}
================================================
FILE: aula04-abortando-requisicoes/sdk/ew-chatbot.html
================================================
<div id="ewcb-widget">
<button id="ewcb-open-btn" class="ewcb-btn" aria-label="Abrir chat">
<span class="ewcb-btn-avatar-wrapper">
<img id="ewcb-icon" alt="Chatbot" />
<span class="ewcb-btn-badge">1</span>
</span>
</button>
<div class="ewcb-chat-window" id="ewcb-chat-window" style="display:none">
<div class="ewcb-chat-header">
<img src="" alt="Bot logo" class="ewcb-chat-header-logo" id="ewcb-header-icon" />
<span class="ewcb-chatbot-name" id="ewcb-chatbot-name"></span>
<button class="ewcb-close-btn" id="ewcb-close-btn" aria-label="Close chat">×</button>
</div>
<div class="ewcb-chat-body" id="ewcb-messages">
</div>
<form class="ewcb-chat-footer" id="ewcb-form" autocomplete="off">
<input type="text" id="ewcb-input" placeholder="Digite sua mensagem..." autocomplete="off" />
<button type="submit" id="ewcb-submit">Enviar</button>
<button type="button" id="ewcb-stop" alt="Parar parar geração de IA">🛑</button>
</form>
</div>
</div>
================================================
FILE: aula04-abortando-requisicoes/sdk/src/controllers/chatBotController.js
================================================
// @ts-check
/**
* @typedef {import("../views/chatBotView.js").ChatbotView} ChatBotView
* @typedef {import("../services/promptService.js").PromptService} PromptService
*/
export class ChatbotController {
#abortController;
#chatbotView;
#promptService;
/**
* @param {Object} deps - Dependencies for the class.
* @param {ChatBotView} deps.chatbotView - The chatbot view instance.
* @param {PromptService} deps.promptService - The prompt service instance.
*/
constructor({ chatbotView, promptService }) {
this.#chatbotView = chatbotView;
this.#promptService = promptService;
}
async init({ firstBotMessage, text }) {
this.#setupEvents();
this.#chatbotView.renderWelcomeBubble();
this.#chatbotView.setInputEnabled(true);
this.#chatbotView.appendBotMessage(firstBotMessage, null, false);
return this.#promptService.init(text)
}
#setupEvents() {
this.#chatbotView.setupEventHandlers({
onOpen: this.#onOpen.bind(this),
onSend: this.#chatBotReply.bind(this),
onStop: this.#handleStop.bind(this),
});
}
#handleStop() {
this.#abortController.abort()
}
async #chatBotReply(userMsg) {
this.#chatbotView.showTypingIndicator();
this.#chatbotView.setInputEnabled(false);
try {
this.#abortController = new AbortController()
const contentNode = this.#chatbotView.createStreamingBotMessage()
const response = this.#promptService.prompt(
userMsg,
this.#abortController.signal,
)
let fullResponse = ''
let lastMessage = 'noop'
const updateText = () => {
if (!fullResponse) return
if (fullResponse === lastMessage) return
lastMessage = fullResponse
this.#chatbotView.hideTypingIndicator();
this.#chatbotView.updateStreamingBotMessage(contentNode, fullResponse)
}
const intervalId = setInterval(updateText, 200)
const stopGenerating = () => {
clearInterval(intervalId)
updateText()
this.#chatbotView.setInputEnabled(true)
}
this.#abortController.signal.addEventListener(
'abort', stopGenerating
)
for await (const chunk of response) {
if (!chunk) continue
fullResponse += chunk
}
console.log('Full response', fullResponse)
stopGenerating()
} catch (error) {
this.#chatbotView.hideTypingIndicator()
if (error.name === 'AbortError')
return console.log('Request aborted by the user!')
this.#chatbotView.appendBotMessage("Erro ao obter. resposta da AI")
console.log("AI prompt error", error)
}
}
async #onOpen() {
const errors = this.#checkRequirements()
if (errors.length) {
const messages = errors.join('\n\n')
this.#chatbotView.appendBotMessage(
messages
)
this.#chatbotView.setInputEnabled(false);
return
}
this.#chatbotView.setInputEnabled(true);
}
#checkRequirements() {
const errors = []
// @ts-ignore
const iChrome = window.chrome
if (!iChrome) {
errors.push(
'⚠️ Este recurso só funciona no Google Chrome ou Chrome Canary (versão recente).'
)
}
if (!('LanguageModel' in window)) {
errors.push("⚠️ As APIs nativas de IA não estão ativas.");
errors.push("Ative a seguinte flag em chrome://flags/:");
errors.push("- Prompt API for Gemini Nano (chrome://flags/#prompt-api-for-gemini-nano)");
errors.push("Depois reinicie o Chrome e tente novamente.");
}
return errors
}
}
================================================
FILE: aula04-abortando-requisicoes/sdk/src/index.js
================================================
// @ts-check
import { ChatbotView } from './views/chatBotView.js';
import { PromptService } from './services/promptService.js'
import { ChatbotController } from './controllers/chatBotController.js';
(async () => {
const root = new URL('../../', import.meta.url);
const fromMainProject = (path) => new URL(path, root).toString();
const [css, html, systemPrompt, config, llmsTxt] = await Promise.all([
fetch(fromMainProject('./sdk/ew-chatbot.css')).then(r => r.text()),
fetch(fromMainProject('./sdk/ew-chatbot.html')).then(r => r.text()),
fetch('./botData/systemPrompt.txt').then(r => r.text()),
fetch('./botData/chatbot-config.json').then(r => r.json()),
fetch('./llms.txt').then(r => r.text()),
]);
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);
const promptService = new PromptService();
const chatbotView = new ChatbotView(config);
const controller = new ChatbotController({ chatbotView, promptService });
const text = systemPrompt.concat('\n', llmsTxt)
controller.init({
firstBotMessage: config.firstBotMessage,
text,
});
})();
================================================
FILE: aula04-abortando-requisicoes/sdk/src/services/promptService.js
================================================
export class PromptService {
#messages = []
#session = null
async init(initialPrompts) {
if (!window.LanguageModel) return
this.#messages.push({
role: 'system',
content: initialPrompts
})
return this.#createSession()
}
async #createSession() {
this.#session = await LanguageModel.create({
initialPrompts: this.#messages,
expectedInputLanguages: ['pt']
})
return this.#session
}
prompt(text, signal) {
this.#messages.push({
role: 'user',
content: text,
})
return this.#session.promptStreaming(this.#messages, {
signal
})
}
}
================================================
FILE: aula04-abortando-requisicoes/sdk/src/views/chatBotView.js
================================================
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
export class ChatbotView {
#config;
#container = document.querySelector("#ewcb-widget");
#header = document.querySelector(".ewcb-chat-header");
#messages = document.querySelector("#ewcb-messages");
#input = document.querySelector("#ewcb-input");
#form = document.querySelector("#ewcb-form");
#openBtn = document.querySelector("#ewcb-open-btn");
#stopBtn = document.querySelector("#ewcb-stop");
#closeBtn = document.querySelector("#ewcb-close-btn");
#chatWin = document.querySelector("#ewcb-chat-window");
#floatingIcon = document.querySelector("#ewcb-icon");
#floatingIconBadge = document.querySelector(".ewcb-btn-badge");
#welcomeBubble = null;
constructor(config) {
this.#config = config;
this.#applyTheme();
this.#setHeader();
this.#setFloatingIcon();
this.setTypingDotDuration();
}
setupEventHandlers({ onOpen, onSend, onStop }) {
this.#openBtn.onclick = () => { this.openChat(); onOpen(); };
this.#stopBtn.onclick = () => { onStop(); };
this.#closeBtn.onclick = () => { this.closeChat(); };
this.#form.onsubmit = (e) => {
e.preventDefault();
const val = this.#input.value.trim();
if (!val) return;
this.appendUserMessage(val);
this.clearInput();
onSend(val);
};
}
setInputEnabled(enabled) {
this.#input.disabled = !enabled;
this.#form.querySelector("button[type=submit]").disabled = !enabled;
this.#stopBtn.disabled = enabled;
}
openChat() {
this.#chatWin.style.display = "flex";
this.#floatingIconBadge.style.display = "none";
setTimeout(() => this.focusInput(), 180);
this.hideWelcomeBubble();
}
closeChat() { this.#chatWin.style.display = "none"; }
renderWelcomeBubble() {
this.#removeElement(this.#welcomeBubble);
const bubble = document.createElement('div');
bubble.className = 'ewcb-welcome-bubble';
bubble.textContent = this.#config.welcomeBubble;
bubble.onclick = () => {
this.openChat();
};
document.body.appendChild(bubble);
this.#welcomeBubble = bubble;
}
hideWelcomeBubble() {
if (this.#welcomeBubble) this.#welcomeBubble.style.display = 'none';
}
/** CORE: Render bot message HTML, always via this helper */
#renderBotMessageHTML(text, renderMarkdown = true) {
return `
<img src="${this.#config.botAvatar}" class="ewcb-avatar" alt="Bot Avatar" />
<div class="ewcb-message-content">${renderMarkdown ? marked.parse(text) : text}</div>
`;
}
appendBotMessage(text, element = null, renderMarkdown = true) {
const el = element || this.#createBotMessage();
el.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#append(el);
}
createStreamingBotMessage() {
const element = this.#createBotMessage();
this.#append(element);
return element;
}
updateStreamingBotMessage(element, text, renderMarkdown = true) {
element.innerHTML = this.#renderBotMessageHTML(text, renderMarkdown);
this.#scrollToBottom();
}
#scrollToBottom() {
this.#messages.scrollTop = this.#messages.scrollHeight;
}
appendUserMessage(text) {
const msg = this.#createUserMessage(text);
this.#append(msg);
}
#createBotMessage() {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-bot';
return msg;
}
#createUserMessage(text) {
const msg = document.createElement('div');
msg.className = 'ewcb-message ewcb-message-user';
msg.innerHTML = `<div class="ewcb-message-content">${text}</div>`;
return msg;
}
showTypingIndicator() {
this.hideTypingIndicator()
const indicator = document.createElement('div');
indicator.className = 'ewcb-typing-indicator';
indicator.innerHTML = `
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
<span class="ewcb-typing-dot"></span>
`;
this.#append(indicator);
}
hideTypingIndicator() {
this.#removeElement(this.#messages.querySelector('.ewcb-typing-indicator'));
}
clearInput() { this.#input.value = ''; }
focusInput() { this.#input.focus(); }
setTypingDotDuration() {
const delayMs = Number(this.#config.typingDelay) || 1200;
const durationSec = Math.max(0.6, delayMs / 1000 * 0.66);
this.#container.style.setProperty('--typingDotDuration', `${durationSec}s`);
}
#append(msgNode) {
this.#messages.appendChild(msgNode);
this.#scrollToBottom();
}
#removeElement(el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
}
#applyTheme() {
Object.entries(this.#config).forEach(([k, v]) => {
if (
typeof v === "string" &&
(k.endsWith('Color') || k.endsWith('Bubble') || k.endsWith('Text') || k === "buttonColor")
) {
this.#container.style.setProperty(`--${k}`, v);
}
});
}
#setHeader() {
this.#header.querySelector("#ewcb-header-icon").src = this.#config.iconUrl;
this.#header.querySelector("#ewcb-chatbot-name").textContent = this.#config.chatbotName;
}
#setFloatingIcon() {
this.#floatingIcon.src = this.#config.iconUrl;
}
}
gitextract_awr454dt/
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── _template/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula01-criando-llmstxt/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula02-integrando-ai/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
├── aula03-recebendo-como-stream/
│ ├── botData/
│ │ ├── chatbot-config.json
│ │ └── systemPrompt.txt
│ ├── index.html
│ ├── llms.txt
│ ├── package.json
│ └── sdk/
│ ├── ew-chatbot.css
│ ├── ew-chatbot.html
│ └── src/
│ ├── controllers/
│ │ └── chatBotController.js
│ ├── index.js
│ ├── services/
│ │ └── promptService.js
│ └── views/
│ └── chatBotView.js
└── aula04-abortando-requisicoes/
├── botData/
│ ├── chatbot-config.json
│ └── systemPrompt.txt
├── index.html
├── llms.txt
├── package.json
└── sdk/
├── ew-chatbot.css
├── ew-chatbot.html
└── src/
├── controllers/
│ └── chatBotController.js
├── index.js
├── services/
│ └── promptService.js
└── views/
└── chatBotView.js
SYMBOL INDEX (182 symbols across 15 files)
FILE: _template/sdk/src/controllers/chatBotController.js
class ChatbotController (line 8) | class ChatbotController {
method constructor (line 18) | constructor({ chatbotView, promptService }) {
method init (line 23) | async init({ firstBotMessage, text }) {
method #setupEvents (line 30) | #setupEvents() {
method #handleStop (line 38) | #handleStop() {
method #chatBotReply (line 41) | async #chatBotReply(userMsg) {
method #onOpen (line 53) | async #onOpen() {
FILE: _template/sdk/src/services/promptService.js
class PromptService (line 2) | class PromptService {
FILE: _template/sdk/src/views/chatBotView.js
class ChatbotView (line 3) | class ChatbotView {
method constructor (line 18) | constructor(config) {
method setupEventHandlers (line 26) | setupEventHandlers({ onOpen, onSend, onStop }) {
method setInputEnabled (line 40) | setInputEnabled(enabled) {
method openChat (line 46) | openChat() {
method closeChat (line 52) | closeChat() { this.#chatWin.style.display = "none"; }
method renderWelcomeBubble (line 54) | renderWelcomeBubble() {
method hideWelcomeBubble (line 66) | hideWelcomeBubble() {
method #renderBotMessageHTML (line 71) | #renderBotMessageHTML(text, renderMarkdown = true) {
method appendBotMessage (line 78) | appendBotMessage(text, element = null, renderMarkdown = true) {
method createStreamingBotMessage (line 84) | createStreamingBotMessage() {
method updateStreamingBotMessage (line 90) | updateStreamingBotMessage(element, text, renderMarkdown = true) {
method #scrollToBottom (line 96) | #scrollToBottom() {
method appendUserMessage (line 100) | appendUserMessage(text) {
method #createBotMessage (line 105) | #createBotMessage() {
method #createUserMessage (line 111) | #createUserMessage(text) {
method showTypingIndicator (line 118) | showTypingIndicator() {
method hideTypingIndicator (line 131) | hideTypingIndicator() {
method clearInput (line 135) | clearInput() { this.#input.value = ''; }
method focusInput (line 136) | focusInput() { this.#input.focus(); }
method setTypingDotDuration (line 138) | setTypingDotDuration() {
method #append (line 144) | #append(msgNode) {
method #removeElement (line 149) | #removeElement(el) {
method #applyTheme (line 153) | #applyTheme() {
method #setHeader (line 163) | #setHeader() {
method #setFloatingIcon (line 167) | #setFloatingIcon() {
FILE: aula01-criando-llmstxt/sdk/src/controllers/chatBotController.js
class ChatbotController (line 8) | class ChatbotController {
method constructor (line 18) | constructor({ chatbotView, promptService }) {
method init (line 23) | async init({ firstBotMessage, text }) {
method #setupEvents (line 30) | #setupEvents() {
method #handleStop (line 38) | #handleStop() {
method #chatBotReply (line 41) | async #chatBotReply(userMsg) {
method #onOpen (line 53) | async #onOpen() {
FILE: aula01-criando-llmstxt/sdk/src/services/promptService.js
class PromptService (line 2) | class PromptService {
FILE: aula01-criando-llmstxt/sdk/src/views/chatBotView.js
class ChatbotView (line 3) | class ChatbotView {
method constructor (line 18) | constructor(config) {
method setupEventHandlers (line 26) | setupEventHandlers({ onOpen, onSend, onStop }) {
method setInputEnabled (line 40) | setInputEnabled(enabled) {
method openChat (line 46) | openChat() {
method closeChat (line 52) | closeChat() { this.#chatWin.style.display = "none"; }
method renderWelcomeBubble (line 54) | renderWelcomeBubble() {
method hideWelcomeBubble (line 66) | hideWelcomeBubble() {
method #renderBotMessageHTML (line 71) | #renderBotMessageHTML(text, renderMarkdown = true) {
method appendBotMessage (line 78) | appendBotMessage(text, element = null, renderMarkdown = true) {
method createStreamingBotMessage (line 84) | createStreamingBotMessage() {
method updateStreamingBotMessage (line 90) | updateStreamingBotMessage(element, text, renderMarkdown = true) {
method #scrollToBottom (line 96) | #scrollToBottom() {
method appendUserMessage (line 100) | appendUserMessage(text) {
method #createBotMessage (line 105) | #createBotMessage() {
method #createUserMessage (line 111) | #createUserMessage(text) {
method showTypingIndicator (line 118) | showTypingIndicator() {
method hideTypingIndicator (line 131) | hideTypingIndicator() {
method clearInput (line 135) | clearInput() { this.#input.value = ''; }
method focusInput (line 136) | focusInput() { this.#input.focus(); }
method setTypingDotDuration (line 138) | setTypingDotDuration() {
method #append (line 144) | #append(msgNode) {
method #removeElement (line 149) | #removeElement(el) {
method #applyTheme (line 153) | #applyTheme() {
method #setHeader (line 163) | #setHeader() {
method #setFloatingIcon (line 167) | #setFloatingIcon() {
FILE: aula02-integrando-ai/sdk/src/controllers/chatBotController.js
class ChatbotController (line 8) | class ChatbotController {
method constructor (line 18) | constructor({ chatbotView, promptService }) {
method init (line 23) | async init({ firstBotMessage, text }) {
method #setupEvents (line 31) | #setupEvents() {
method #handleStop (line 39) | #handleStop() {
method #chatBotReply (line 42) | async #chatBotReply(userMsg) {
method #onOpen (line 55) | async #onOpen() {
method #checkRequirements (line 70) | #checkRequirements() {
FILE: aula02-integrando-ai/sdk/src/services/promptService.js
class PromptService (line 2) | class PromptService {
method init (line 5) | async init(initialPrompts) {
method #createSession (line 16) | async #createSession() {
method prompt (line 25) | prompt(text) {
FILE: aula02-integrando-ai/sdk/src/views/chatBotView.js
class ChatbotView (line 3) | class ChatbotView {
method constructor (line 18) | constructor(config) {
method setupEventHandlers (line 26) | setupEventHandlers({ onOpen, onSend, onStop }) {
method setInputEnabled (line 40) | setInputEnabled(enabled) {
method openChat (line 46) | openChat() {
method closeChat (line 52) | closeChat() { this.#chatWin.style.display = "none"; }
method renderWelcomeBubble (line 54) | renderWelcomeBubble() {
method hideWelcomeBubble (line 66) | hideWelcomeBubble() {
method #renderBotMessageHTML (line 71) | #renderBotMessageHTML(text, renderMarkdown = true) {
method appendBotMessage (line 78) | appendBotMessage(text, element = null, renderMarkdown = true) {
method createStreamingBotMessage (line 84) | createStreamingBotMessage() {
method updateStreamingBotMessage (line 90) | updateStreamingBotMessage(element, text, renderMarkdown = true) {
method #scrollToBottom (line 96) | #scrollToBottom() {
method appendUserMessage (line 100) | appendUserMessage(text) {
method #createBotMessage (line 105) | #createBotMessage() {
method #createUserMessage (line 111) | #createUserMessage(text) {
method showTypingIndicator (line 118) | showTypingIndicator() {
method hideTypingIndicator (line 131) | hideTypingIndicator() {
method clearInput (line 135) | clearInput() { this.#input.value = ''; }
method focusInput (line 136) | focusInput() { this.#input.focus(); }
method setTypingDotDuration (line 138) | setTypingDotDuration() {
method #append (line 144) | #append(msgNode) {
method #removeElement (line 149) | #removeElement(el) {
method #applyTheme (line 153) | #applyTheme() {
method #setHeader (line 163) | #setHeader() {
method #setFloatingIcon (line 167) | #setFloatingIcon() {
FILE: aula03-recebendo-como-stream/sdk/src/controllers/chatBotController.js
class ChatbotController (line 8) | class ChatbotController {
method constructor (line 18) | constructor({ chatbotView, promptService }) {
method init (line 23) | async init({ firstBotMessage, text }) {
method #setupEvents (line 31) | #setupEvents() {
method #handleStop (line 39) | #handleStop() {
method #chatBotReply (line 42) | async #chatBotReply(userMsg) {
method #onOpen (line 76) | async #onOpen() {
method #checkRequirements (line 91) | #checkRequirements() {
FILE: aula03-recebendo-como-stream/sdk/src/services/promptService.js
class PromptService (line 2) | class PromptService {
method init (line 5) | async init(initialPrompts) {
method #createSession (line 16) | async #createSession() {
method prompt (line 25) | prompt(text) {
FILE: aula03-recebendo-como-stream/sdk/src/views/chatBotView.js
class ChatbotView (line 3) | class ChatbotView {
method constructor (line 18) | constructor(config) {
method setupEventHandlers (line 26) | setupEventHandlers({ onOpen, onSend, onStop }) {
method setInputEnabled (line 40) | setInputEnabled(enabled) {
method openChat (line 46) | openChat() {
method closeChat (line 52) | closeChat() { this.#chatWin.style.display = "none"; }
method renderWelcomeBubble (line 54) | renderWelcomeBubble() {
method hideWelcomeBubble (line 66) | hideWelcomeBubble() {
method #renderBotMessageHTML (line 71) | #renderBotMessageHTML(text, renderMarkdown = true) {
method appendBotMessage (line 78) | appendBotMessage(text, element = null, renderMarkdown = true) {
method createStreamingBotMessage (line 84) | createStreamingBotMessage() {
method updateStreamingBotMessage (line 90) | updateStreamingBotMessage(element, text, renderMarkdown = true) {
method #scrollToBottom (line 96) | #scrollToBottom() {
method appendUserMessage (line 100) | appendUserMessage(text) {
method #createBotMessage (line 105) | #createBotMessage() {
method #createUserMessage (line 111) | #createUserMessage(text) {
method showTypingIndicator (line 118) | showTypingIndicator() {
method hideTypingIndicator (line 131) | hideTypingIndicator() {
method clearInput (line 135) | clearInput() { this.#input.value = ''; }
method focusInput (line 136) | focusInput() { this.#input.focus(); }
method setTypingDotDuration (line 138) | setTypingDotDuration() {
method #append (line 144) | #append(msgNode) {
method #removeElement (line 149) | #removeElement(el) {
method #applyTheme (line 153) | #applyTheme() {
method #setHeader (line 163) | #setHeader() {
method #setFloatingIcon (line 167) | #setFloatingIcon() {
FILE: aula04-abortando-requisicoes/sdk/src/controllers/chatBotController.js
class ChatbotController (line 8) | class ChatbotController {
method constructor (line 18) | constructor({ chatbotView, promptService }) {
method init (line 23) | async init({ firstBotMessage, text }) {
method #setupEvents (line 31) | #setupEvents() {
method #handleStop (line 39) | #handleStop() {
method #chatBotReply (line 43) | async #chatBotReply(userMsg) {
method #onOpen (line 97) | async #onOpen() {
method #checkRequirements (line 112) | #checkRequirements() {
FILE: aula04-abortando-requisicoes/sdk/src/services/promptService.js
class PromptService (line 2) | class PromptService {
method init (line 5) | async init(initialPrompts) {
method #createSession (line 16) | async #createSession() {
method prompt (line 25) | prompt(text, signal) {
FILE: aula04-abortando-requisicoes/sdk/src/views/chatBotView.js
class ChatbotView (line 3) | class ChatbotView {
method constructor (line 18) | constructor(config) {
method setupEventHandlers (line 26) | setupEventHandlers({ onOpen, onSend, onStop }) {
method setInputEnabled (line 40) | setInputEnabled(enabled) {
method openChat (line 46) | openChat() {
method closeChat (line 52) | closeChat() { this.#chatWin.style.display = "none"; }
method renderWelcomeBubble (line 54) | renderWelcomeBubble() {
method hideWelcomeBubble (line 66) | hideWelcomeBubble() {
method #renderBotMessageHTML (line 71) | #renderBotMessageHTML(text, renderMarkdown = true) {
method appendBotMessage (line 78) | appendBotMessage(text, element = null, renderMarkdown = true) {
method createStreamingBotMessage (line 84) | createStreamingBotMessage() {
method updateStreamingBotMessage (line 90) | updateStreamingBotMessage(element, text, renderMarkdown = true) {
method #scrollToBottom (line 96) | #scrollToBottom() {
method appendUserMessage (line 100) | appendUserMessage(text) {
method #createBotMessage (line 105) | #createBotMessage() {
method #createUserMessage (line 111) | #createUserMessage(text) {
method showTypingIndicator (line 118) | showTypingIndicator() {
method hideTypingIndicator (line 131) | hideTypingIndicator() {
method clearInput (line 135) | clearInput() { this.#input.value = ''; }
method focusInput (line 136) | focusInput() { this.#input.focus(); }
method setTypingDotDuration (line 138) | setTypingDotDuration() {
method #append (line 144) | #append(msgNode) {
method #removeElement (line 149) | #removeElement(el) {
method #applyTheme (line 153) | #applyTheme() {
method #setHeader (line 163) | #setHeader() {
method #setFloatingIcon (line 167) | #setFloatingIcon() {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (171K chars).
[
{
"path": ".gitattributes",
"chars": 94,
"preview": "*.js linguist-detectable=true\n*.html linguist-detectable=false\n*.css linguist-detectable=false"
},
{
"path": ".gitignore",
"chars": 2160,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": "LICENSE",
"chars": 1098,
"preview": "MIT License Copyright (c) 2025 Erick Wendel\n\nPermission is hereby granted, free\nof charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 8176,
"preview": "<div align=\"center\">\n\n[](https:/"
},
{
"path": "_template/botData/chatbot-config.json",
"chars": 519,
"preview": "{\n \"primaryColor\": \"#23A267\",\n \"chatbotName\": \"EW Academy AI Assistant\",\n \"buttonColor\": \"#1D7A4B\",\n \"backgr"
},
{
"path": "_template/botData/systemPrompt.txt",
"chars": 1239,
"preview": "Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.\nResponda apenas com base nos dado"
},
{
"path": "_template/index.html",
"chars": 351,
"preview": "<!DOCTYPE html>\n<html lang=\"pt-br\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "_template/package.json",
"chars": 297,
"preview": "{\n \"name\": \"ai-chat-button\",\n \"version\": \"0.0.1\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"browser-sync -w ."
},
{
"path": "_template/sdk/ew-chatbot.css",
"chars": 10132,
"preview": "#ewcb-widget {\n font-family: 'Inter', system-ui, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-fo"
},
{
"path": "_template/sdk/ew-chatbot.html",
"chars": 1121,
"preview": "<div id=\"ewcb-widget\">\n <button id=\"ewcb-open-btn\" class=\"ewcb-btn\" aria-label=\"Abrir chat\">\n <span class=\"ewc"
},
{
"path": "_template/sdk/src/controllers/chatBotController.js",
"chars": 1643,
"preview": "// @ts-check\n\n/**\n * @typedef {import(\"../views/chatBotView.js\").ChatbotView} ChatBotView\n * @typedef {import(\"../servic"
},
{
"path": "_template/sdk/src/index.js",
"chars": 1269,
"preview": "// @ts-check\n\nimport { ChatbotView } from './views/chatBotView.js';\nimport { PromptService } from './services/promptServ"
},
{
"path": "_template/sdk/src/services/promptService.js",
"chars": 33,
"preview": "\nexport class PromptService {\n\n}\n"
},
{
"path": "_template/sdk/src/views/chatBotView.js",
"chars": 5693,
"preview": "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n\nexport class ChatbotView {\n #config;"
},
{
"path": "aula01-criando-llmstxt/botData/chatbot-config.json",
"chars": 519,
"preview": "{\n \"primaryColor\": \"#23A267\",\n \"chatbotName\": \"EW Academy AI Assistant\",\n \"buttonColor\": \"#1D7A4B\",\n \"backgr"
},
{
"path": "aula01-criando-llmstxt/botData/systemPrompt.txt",
"chars": 1239,
"preview": "Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.\nResponda apenas com base nos dado"
},
{
"path": "aula01-criando-llmstxt/index.html",
"chars": 352,
"preview": "<!DOCTYPE html>\n<html lang=\"pt-br\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "aula01-criando-llmstxt/llms.txt",
"chars": 7371,
"preview": "# EW Academy - Aprenda com as melhores trilhas de aprendizado\n\nDescubra as trilhas de aprendizado da EW Academy e transf"
},
{
"path": "aula01-criando-llmstxt/package.json",
"chars": 297,
"preview": "{\n \"name\": \"ai-chat-button\",\n \"version\": \"0.0.1\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"browser-sync -w ."
},
{
"path": "aula01-criando-llmstxt/sdk/ew-chatbot.css",
"chars": 10132,
"preview": "#ewcb-widget {\n font-family: 'Inter', system-ui, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-fo"
},
{
"path": "aula01-criando-llmstxt/sdk/ew-chatbot.html",
"chars": 1121,
"preview": "<div id=\"ewcb-widget\">\n <button id=\"ewcb-open-btn\" class=\"ewcb-btn\" aria-label=\"Abrir chat\">\n <span class=\"ewc"
},
{
"path": "aula01-criando-llmstxt/sdk/src/controllers/chatBotController.js",
"chars": 1643,
"preview": "// @ts-check\n\n/**\n * @typedef {import(\"../views/chatBotView.js\").ChatbotView} ChatBotView\n * @typedef {import(\"../servic"
},
{
"path": "aula01-criando-llmstxt/sdk/src/index.js",
"chars": 1269,
"preview": "// @ts-check\n\nimport { ChatbotView } from './views/chatBotView.js';\nimport { PromptService } from './services/promptServ"
},
{
"path": "aula01-criando-llmstxt/sdk/src/services/promptService.js",
"chars": 33,
"preview": "\nexport class PromptService {\n\n}\n"
},
{
"path": "aula01-criando-llmstxt/sdk/src/views/chatBotView.js",
"chars": 5693,
"preview": "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n\nexport class ChatbotView {\n #config;"
},
{
"path": "aula02-integrando-ai/botData/chatbot-config.json",
"chars": 519,
"preview": "{\n \"primaryColor\": \"#23A267\",\n \"chatbotName\": \"EW Academy AI Assistant\",\n \"buttonColor\": \"#1D7A4B\",\n \"backgr"
},
{
"path": "aula02-integrando-ai/botData/systemPrompt.txt",
"chars": 1239,
"preview": "Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.\nResponda apenas com base nos dado"
},
{
"path": "aula02-integrando-ai/index.html",
"chars": 351,
"preview": "<!DOCTYPE html>\n<html lang=\"pt-br\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "aula02-integrando-ai/llms.txt",
"chars": 7371,
"preview": "# EW Academy - Aprenda com as melhores trilhas de aprendizado\n\nDescubra as trilhas de aprendizado da EW Academy e transf"
},
{
"path": "aula02-integrando-ai/package.json",
"chars": 297,
"preview": "{\n \"name\": \"ai-chat-button\",\n \"version\": \"0.0.1\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"browser-sync -w ."
},
{
"path": "aula02-integrando-ai/sdk/ew-chatbot.css",
"chars": 10132,
"preview": "#ewcb-widget {\n font-family: 'Inter', system-ui, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-fo"
},
{
"path": "aula02-integrando-ai/sdk/ew-chatbot.html",
"chars": 1121,
"preview": "<div id=\"ewcb-widget\">\n <button id=\"ewcb-open-btn\" class=\"ewcb-btn\" aria-label=\"Abrir chat\">\n <span class=\"ewc"
},
{
"path": "aula02-integrando-ai/sdk/src/controllers/chatBotController.js",
"chars": 2680,
"preview": "// @ts-check\n\n/**\n * @typedef {import(\"../views/chatBotView.js\").ChatbotView} ChatBotView\n * @typedef {import(\"../servic"
},
{
"path": "aula02-integrando-ai/sdk/src/index.js",
"chars": 1332,
"preview": "// @ts-check\n\nimport { ChatbotView } from './views/chatBotView.js';\nimport { PromptService } from './services/promptServ"
},
{
"path": "aula02-integrando-ai/sdk/src/services/promptService.js",
"chars": 686,
"preview": "\nexport class PromptService {\n #messages = []\n #session = null\n async init(initialPrompts) {\n if (!windo"
},
{
"path": "aula02-integrando-ai/sdk/src/views/chatBotView.js",
"chars": 5693,
"preview": "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n\nexport class ChatbotView {\n #config;"
},
{
"path": "aula03-recebendo-como-stream/botData/chatbot-config.json",
"chars": 519,
"preview": "{\n \"primaryColor\": \"#23A267\",\n \"chatbotName\": \"EW Academy AI Assistant\",\n \"buttonColor\": \"#1D7A4B\",\n \"backgr"
},
{
"path": "aula03-recebendo-como-stream/botData/systemPrompt.txt",
"chars": 1239,
"preview": "Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.\nResponda apenas com base nos dado"
},
{
"path": "aula03-recebendo-como-stream/index.html",
"chars": 351,
"preview": "<!DOCTYPE html>\n<html lang=\"pt-br\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "aula03-recebendo-como-stream/llms.txt",
"chars": 7371,
"preview": "# EW Academy - Aprenda com as melhores trilhas de aprendizado\n\nDescubra as trilhas de aprendizado da EW Academy e transf"
},
{
"path": "aula03-recebendo-como-stream/package.json",
"chars": 297,
"preview": "{\n \"name\": \"ai-chat-button\",\n \"version\": \"0.0.1\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"browser-sync -w ."
},
{
"path": "aula03-recebendo-como-stream/sdk/ew-chatbot.css",
"chars": 10132,
"preview": "#ewcb-widget {\n font-family: 'Inter', system-ui, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-fo"
},
{
"path": "aula03-recebendo-como-stream/sdk/ew-chatbot.html",
"chars": 1121,
"preview": "<div id=\"ewcb-widget\">\n <button id=\"ewcb-open-btn\" class=\"ewcb-btn\" aria-label=\"Abrir chat\">\n <span class=\"ewc"
},
{
"path": "aula03-recebendo-como-stream/sdk/src/controllers/chatBotController.js",
"chars": 3310,
"preview": "// @ts-check\n\n/**\n * @typedef {import(\"../views/chatBotView.js\").ChatbotView} ChatBotView\n * @typedef {import(\"../servic"
},
{
"path": "aula03-recebendo-como-stream/sdk/src/index.js",
"chars": 1332,
"preview": "// @ts-check\n\nimport { ChatbotView } from './views/chatBotView.js';\nimport { PromptService } from './services/promptServ"
},
{
"path": "aula03-recebendo-como-stream/sdk/src/services/promptService.js",
"chars": 695,
"preview": "\nexport class PromptService {\n #messages = []\n #session = null\n async init(initialPrompts) {\n if (!windo"
},
{
"path": "aula03-recebendo-como-stream/sdk/src/views/chatBotView.js",
"chars": 5693,
"preview": "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n\nexport class ChatbotView {\n #config;"
},
{
"path": "aula04-abortando-requisicoes/botData/chatbot-config.json",
"chars": 519,
"preview": "{\n \"primaryColor\": \"#23A267\",\n \"chatbotName\": \"EW Academy AI Assistant\",\n \"buttonColor\": \"#1D7A4B\",\n \"backgr"
},
{
"path": "aula04-abortando-requisicoes/botData/systemPrompt.txt",
"chars": 1239,
"preview": "Você é um assistente virtual da EW Academy, plataforma de cursos online em tecnologia.\nResponda apenas com base nos dado"
},
{
"path": "aula04-abortando-requisicoes/index.html",
"chars": 351,
"preview": "<!DOCTYPE html>\n<html lang=\"pt-br\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "aula04-abortando-requisicoes/llms.txt",
"chars": 7371,
"preview": "# EW Academy - Aprenda com as melhores trilhas de aprendizado\n\nDescubra as trilhas de aprendizado da EW Academy e transf"
},
{
"path": "aula04-abortando-requisicoes/package.json",
"chars": 297,
"preview": "{\n \"name\": \"ai-chat-button\",\n \"version\": \"0.0.1\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"browser-sync -w ."
},
{
"path": "aula04-abortando-requisicoes/sdk/ew-chatbot.css",
"chars": 10132,
"preview": "#ewcb-widget {\n font-family: 'Inter', system-ui, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-fo"
},
{
"path": "aula04-abortando-requisicoes/sdk/ew-chatbot.html",
"chars": 1121,
"preview": "<div id=\"ewcb-widget\">\n <button id=\"ewcb-open-btn\" class=\"ewcb-btn\" aria-label=\"Abrir chat\">\n <span class=\"ewc"
},
{
"path": "aula04-abortando-requisicoes/sdk/src/controllers/chatBotController.js",
"chars": 4058,
"preview": "// @ts-check\n\n/**\n * @typedef {import(\"../views/chatBotView.js\").ChatbotView} ChatBotView\n * @typedef {import(\"../servic"
},
{
"path": "aula04-abortando-requisicoes/sdk/src/index.js",
"chars": 1332,
"preview": "// @ts-check\n\nimport { ChatbotView } from './views/chatBotView.js';\nimport { PromptService } from './services/promptServ"
},
{
"path": "aula04-abortando-requisicoes/sdk/src/services/promptService.js",
"chars": 735,
"preview": "\nexport class PromptService {\n #messages = []\n #session = null\n async init(initialPrompts) {\n if (!windo"
},
{
"path": "aula04-abortando-requisicoes/sdk/src/views/chatBotView.js",
"chars": 5693,
"preview": "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n\nexport class ChatbotView {\n #config;"
}
]
About this extraction
This page contains the full source code of the ErickWendel/semana-javascript-expert09 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (156.1 KB), approximately 44.8k tokens, and a symbol index with 182 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.