Ir para Navegação de vídeos e textos
Para utilizar o ThemeManager, é necessário importar o módulo dentro da sua aplicação.
import { ThemeManager } from './dist/theme-manager.js';Caso esteja utilizando via HTML com <script type="module">, o import deve ser feito assim:
<script type="module">
import { ThemeManager } from './dist/theme-manager.js';
ThemeManager.setDefaultTheme({
/* ... */
});
</script>Além de imagens (<img>), também é possível utilizar SVG inline em qualquer ícone do tema.
ThemeManager.setDefaultTheme({
submitButtons: {
submit: {
svg: {
content: `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M2 21L23 12L2 3V10L17 12L2 14V21Z" fill="#3B82F6"/>
</svg>
`
}
}
}
});ThemeManager.setDefaultTheme({
header: {
icon: {
svg: `
<svg width="20" height="20" fill="#3B82F6" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
</svg>
`
}
}
});ThemeManager.setDefaultTheme({
closed: {
icon: `
<svg width="24" height="24" fill="#3B82F6" viewBox="0 0 24 24">
<path d="M3 12h18" stroke="#fff" stroke-width="2" />
</svg>
`
}
});ThemeManager.setDefaultTheme({
chat: { backgroundColor: '#ffffff' }
});ThemeManager.setDarkTheme({
chat: { backgroundColor: '#1e1e1e' },
header: { styles: { color: '#ffffff' }}
});ThemeManager.setLightTheme({
chat: {
backgroundColor: '#f5e8ff'
},
header: {
styles: {
backgroundColor: '#a855f7',
color: '#fff'
}
}
});ThemeManager.setDarkTheme({
chat: {
backgroundColor: '#1a1a1a'
},
header: {
styles: {
backgroundColor: '#333',
color: '#fff'
}
}
});ThemeManager.setDefaultTheme({
chat: {
backgroundColor: '#ffffff'
}
});ThemeManager.setCurrentTheme('default');
ThemeManager.setCurrentTheme('dark');
ThemeManager.setCurrentTheme('light');<button onclick="ThemeManager.setCurrentTheme('default')">Tema Padrão</button>
<button onclick="ThemeManager.setCurrentTheme('dark')">Tema Escuro</button>
<button onclick="ThemeManager.setCurrentTheme('light')">Meu Tema Customizado</button>💡 Observação importante:
Caso deseje utilizar uma fonte diferente da padrão, é necessário adicioná-la manualmente dentro da tag<head>do HTML.Exemplo de inclusão de fonte personalizada:
<head> <link href="https://fonts.cdnfonts.com/css/stencil-paper" rel="stylesheet"> </head>
Código:
ThemeManager.setDefaultTheme({
closed: {
icon: `
<img src="https://cdn-icons-png.flaticon.com/512/4712/4712009.png">
`,
tooltip: { text: 'Abrir documentação' },
styles: {
backgroundColor: '#3B82F6',
borderRadius: '50%',
width: '60px',
height: '60px'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
closed: {
icon: `
<img src="https://cdn-icons-png.flaticon.com/512/4712/4712009.png">
`,
tooltip: { text: 'Texto Chat Minimizado' },
styles: {
backgroundColor: '#3B82F6',
borderRadius: '50%',
width: '60px',
height: '60px'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
closed: {
icon: `
<img src="https://cdn-icons-png.flaticon.com/512/4712/4712009.png">
`,
tooltip: {
text: 'Exemplo font minimizada',
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#3B82F6',
backgroundColor: '#f0f4ff',
border: '1px solid #3B82F6',
padding: '6px 10px',
borderRadius: '6px'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
closed: {
icon: `
<img src="https://cdn-icons-png.flaticon.com/512/4712/4712009.png">
`,
tooltip: {
text: 'Exemplo font minimizada',
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#3B82F6',
backgroundColor: '#f0f4ff',
border: '1px solid #3B82F6',
padding: '6px 10px',
borderRadius: '6px'
}
},
styles: {
backgroundColor: '#3B82F6'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
closed: {
icon: `
<img src="https://cdn-icons-png.flaticon.com/512/4712/4712009.png">
`,
tooltip: {
text: 'Abrir documentação',
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: 'red',
backgroundColor: '#f0f4ff',
border: '1px solid #3B82F6',
padding: '6px 10px',
borderRadius: '6px'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
avatars: {
ai: {
src: 'https://cdn-icons-png.flaticon.com/512/4712/4712027.png'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
avatars: {
ai: {
src: 'https://cdn-icons-png.flaticon.com/512/4712/4712027.png'
},
user: {
src: 'https://cdn-icons-png.flaticon.com/512/147/147144.png'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
color: 'white'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
color: 'white'
}
},
user: {
bubble: {
backgroundColor: 'yellow',
color: 'black'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
color: 'white'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
color: 'white'
}
},
user: {
bubble: {
backgroundColor: 'yellow',
color: 'black'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#000000'
}
},
user: {
bubble: {
backgroundColor: 'yellow',
color: 'black'
}
}
}
}
});Resultado:
Código
ThemeManager.setDefaultTheme({
messages: {
default: {
ai: {
bubble: {
backgroundColor: 'red',
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#000000'
}
},
user: {
bubble: {
backgroundColor: 'yellow',
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#000000'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
textInput: {
styles: {
container: {
backgroundColor: 'lightblue'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
textInput: {
styles: {
container: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#000000'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
textInput: {
styles: {
container: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#000000'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
submitButtons: {
submit: {
svg: {
content: `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M2 21L23 12L2 3V10L17 12L2 14V21Z" fill="#3B82F6"/>
</svg>
`}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
chat: {
backgroundColor: 'lightblue'
}
});Resultado:
Cor dos balões de sugestão
Código:
ThemeManager.setDefaultTheme({
messageSuggestions: {
default: {
color: 'yellow',
}
}
});Resultado:
ThemeManager.setDefaultTheme({
messageSuggestions: {
default: {
color: 'darkblue',
}
}
});Resultado:
ThemeManager.setDefaultTheme({
messageSuggestions: {
default: {
color: 'yellow',
},
hover: {
backgroundColor: '#0078d4'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messages: {
default: {
'ai-suggestions': {
bubble: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px'
}
}
}
}
});Resultado:
Altura do chat (em %) em relação ao total da página quando utilizado em navegadores móveis e Permitir z-index customizado
Para alterar estilos do container externo do chat, basta definir via CSS como normalmente é feito e colocar na tag do componente. Código:
<style>
.chat {
position: fixed;
bottom: 20px;
right: 20px;
max-height: 80vh;
z-index: 9999;
}
</style>
<tutor-chat class="chat" id="tutor-chat"></tutor-chat>(Estou aprendendo novos conteúdos sobre o produto ...)
Código:
ThemeManager.setDefaultTheme({
notificationArea: {
styles: {
backgroundColor: 'yellow'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
notificationArea: {
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#92400E'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
notificationArea: {
styles: {
color: '#1E3A8A'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
positiveAction: {
svg: `<img src="https://cdn-icons-png.flaticon.com/512/1828/1828640.png" width="24" height="24">`
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
positiveAction: {
styles: {
backgroundColor: '#22C55E',
borderRadius: '6px',
padding: '6px'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
negativeAction: {
svg: `<img src="https://cdn-icons-png.flaticon.com/512/1828/1828665.png" width="24" height="24">`
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
negativeAction: {
styles: {
backgroundColor: '#EF4444',
borderRadius: '6px',
padding: '6px'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
positiveAction: {
label: {
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#16A34A'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
positiveAction: {
label: {
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '14px',
color: '#16A34A'
}
}
}
}
});Resultado:
Cor de fundo balão de sugestão ("Resposta incorreta", "Resposta muito longa", "Resposta fora do tema", "Outro")
Código:
ThemeManager.setDefaultTheme({
feedback: {
negativeForm: {
reasonButtons: {
hoverBackgroundColor: '#FF0000',
styles: {
color: '#BB0000',
backgroundColor: 'yellow'
}
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
messageSuggestions: {
default: {
color: '#fff',
backgroundColor: '#000'
},
hover: {
backgroundColor: '#0078d4'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
negativeAction: {
label: {
text: 'Que pena que a resposta não ajudou. Esse é um TESTE!'
}
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
feedback: {
submitButton: {
backgroundColor: 'red'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
title: {
styles: {
color: '#ff0000'
},
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
styles: {
fontFamily: '"Stencil Paper", sans-serif',
fontSize: '18px'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
icon: {
svg: "<img src='https://cdn-icons-png.flaticon.com/512/4712/4712027.png' />"
}
},
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
styles: {
backgroundColor: '#1E3A8A'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
title: {
text: 'Assistente do Curso de C# Performance'
}
}
});Resultado:
Código:
ThemeManager.setDefaultTheme({
header: {
title: {
styles: {
color: '#ff0000'
}
}
}
});Fechado Código
tutorChat.options = {
organizationId: "4f0983fc-055c-44be-bafe-0f2d9e937f0b",
productId: "9206CEBB-17F0-5CFE-84A3-0C9B212FE1C8",
isExternalProductId: false,
getToken: getConsumerToken,
getCurrentContent: getCurrentContent,
baseAddress: 'https://localhost:7209',
startOpened: false,
showSuggestions: true
};Resultado:
Aberto código:
tutorChat.options = {
organizationId: "4f0983fc-055c-44be-bafe-0f2d9e937f0b",
productId: "9206CEBB-17F0-5CFE-84A3-0C9B212FE1C8",
isExternalProductId: false,
getToken: getConsumerToken,
getCurrentContent: getCurrentContent,
baseAddress: 'https://localhost:7209',
startOpened: true,
showSuggestions: true
};Resultado:
Código mostrar:
tutorChat.options = {
organizationId: "4f0983fc-055c-44be-bafe-0f2d9e937f0b",
productId: "9206CEBB-17F0-5CFE-84A3-0C9B212FE1C8",
isExternalProductId: false,
getToken: getConsumerToken,
getCurrentContent: getCurrentContent,
baseAddress: 'https://localhost:7209',
startOpened: true,
showSuggestions: true
};Resultado:
Código não mostrar:
tutorChat.options = {
organizationId: "4f0983fc-055c-44be-bafe-0f2d9e937f0b",
productId: "9206CEBB-17F0-5CFE-84A3-0C9B212FE1C8",
isExternalProductId: false,
getToken: getConsumerToken,
getCurrentContent: getCurrentContent,
baseAddress: 'https://localhost:7209',
startOpened: true,
showSuggestions: false
};Resultado:
Botão sem interação do usuário.
Botão quando o cursor do mouse está sobre o componente.
Botão enquanto o microfone está capturando áudio.
Código:
ThemeManager.setDefaultTheme({
speechToText: {
button: {
default: {
container: {
default: {
width: '24px',
height: '24px',
background: 'transparent',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background .15s ease'
},
hover: {
backgroundColor: '#7fbded69'
},
click: {
backgroundColor: '#4babf669'
}
},
svg: {
styles: {
default: {
filter: 'none',
fill: '#359468'
}
}
}
},
active: {
container: {
default: {
backgroundColor: '#ffd07c70'
},
hover: {
backgroundColor: '#7fbded69'
},
click: {
backgroundColor: '#4babf669'
}
},
svg: {
styles: {
default: {
filter: 'none',
fill: '#c77930'
}
}
}
}
}
}
});Resultado:
Botão sem interação do usuário.
Botão quando o cursor do mouse está sobre o componente.
Botão enquanto o microfone está capturando áudio.
Quando a IA identifica um conteúdo relevante dentro de um vídeo ou documento PDF, ela pode retornar o ponto exato onde aquele conteúdo aparece.
Para isso, você deve implementar dois callbacks: onVideoDocumentNavigation e onTextDocumentNavigation.
Essas funções recebem um objeto contendo informações sobre o conteúdo indexado e devem redirecionar o usuário para o material correspondente no player do produtor.
Callbacks disponíveis
Chamado quando o usuário clica em um link de conteúdo de vídeo retornado pela IA.
Chamado quando o usuário clica em um link de conteúdo textual (PDF) retornado pela IA. Essas funções recebem um objeto com metadados do conteúdo indexado (id, nome, URL amigável, tempo ou página) e devem executar a navegação no player ou visualizador do produto. Flags de habilitação Além dos callbacks, o componente exige flags explícitas para habilitar a navegação:
Essas flags controlam se os links serão exibidos na interface.
tutorChat.options = {
organizationId: "",
productId: "",
isExternalProductId: true,
getToken: getConsumerToken,
getCurrentContent: getCurrentContent,
startOpened: true,
onVideoDocumentNavigation: (v) => this.videoNavigation(v),
onTextDocumentNavigation: (t) => this.textNavigation(t),
enableVideoDocumentsNavigation: true,
enableTextDocumentsNavigation: true
};A navegação só é ativada quando DUAS condições são atendidas ao mesmo tempo:
Para vídeos
enableTextDocumentsNavigation === true
E
onTextDocumentNavigation foi implementado
Para documentos
enableVideoDocumentsNavigation === true
E
onVideoDocumentNavigation foi implementado
Quando a IA entende que a resposta do usuário está relacionada a um trecho específico de um vídeo (ex.: “O conceito SOLID é explicado no minuto 2 do vídeo X”), ela dispara o callback:
onVideoDocumentNavigation: (currentVideo) => this.videoNavigation(currentVideo)O método recebe:
{
id: string,
name?: string,
friendlyUrl?: string,
startTime: number // em milissegundos
}videoNavigation(currentVideo) {
const productId = this.tutorChat?.options?.productId;
const url = new URL(window.location.href);
url.hash = '';
url.pathname = `/products/${productId}/members`;
url.search = '';
url.searchParams.set('documentId', currentVideo.id);
url.searchParams.set('position', Math.round(currentVideo.startTime ?? 0));
//invocar a chamada aqui;
}- Constrói uma URL para o player da plataforma
/products/{productId}/members
- Adiciona o ID do vídeo
documentId = currentVideo.id
- Marca o tempo do vídeo onde o conteúdo aparece
position = startTime
- Chama o Blazor para executar a navegação real:
NavigateTo(url, true)
Quando o usuário clica no link sugerido pela IA, ele é redirecionado para o vídeo exatamente no segundo certo que contém o conteúdo mencionado.
Quando o conteúdo relevante está em um material de texto (apostila, PDF, página 8, por exemplo), o callback é:
onTextDocumentNavigation: (currentText) => this.textNavigation(currentText)O objeto recebido:
{
id: string,
name?: string,
friendlyUrl?: string,
page: number
}textNavigation(currentText) {
const productId = this.tutorChat?.options?.productId;
const url = new URL(window.location.href);
url.hash = '';
url.pathname = `/products/${productId}/members`;
url.search = '';
url.searchParams.set('documentId', currentText.id);
url.searchParams.set('position', currentText.page ?? 0);
//invocar a chamada aqui;
}- Constrói a URL do player
- Usa o ID do documento PDF
- Define a página onde o tema aparece
- Aciona o Blazor para navegar
O usuário é direcionado para exatamente a página do documento mencionada pela IA.
Dentro do componente, quando o usuário clica em um item sugerido pela IA, este método é chamado:
private async videoDocumentClick(event) {
if (!this.options.onVideoDocumentNavigation) return;
const target = event.currentTarget;
if (!target) return;
const dataset = target.dataset;
await this.options.onVideoDocumentNavigation({
name: dataset.name,
friendlyUrl: dataset.friendlyUrl,
id: dataset.id,
startTime: dataset.startTime
});
}O mesmo ocorre para PDF (textNavigation).
- A IA monitora o conteúdo do curso (vídeos + PDFs)
- Quando identifica uma parte relevante:
- Fornece um link para o trecho do vídeo exato
- Ou para a página correta do PDF
- O Tutor dispara
onVideoDocumentNavigationouonTextDocumentNavigation - Você redireciona seu usuário usando o método exibido acima
Isso permite que o aluno vá diretamente ao ponto que contém a explicação necessária — aumentando retenção e experiência.
O componente <tutor-chat> precisa saber onde o aluno está atualmente no curso para enviar essa informação ao backend da IA.
Com isso, a IA consegue responder de forma contextualizada (ex.: "você está na página 3", "você está no minuto 12 do vídeo").
Para isso existe a função callback:
getCurrentContent: () => Promise<CurrentContentResponse>O componente sempre chamará getCurrentContent() antes de enviar uma mensagem para a IA.
O retorno deve seguir a interface:
export type CurrentContentResponse = {
documentId: string,
type: number,
position: number,
isExternalId: boolean
}| Campo | Descrição |
|---|---|
| documentId | Identificador do conteúdo atual (vídeo, PDF, ...). Pode ser um Id Externo, nesse caso o isExternalId deve ser true. |
| type | 0 para vídeo 1 para texto |
| position | Posição atual dentro do conteúdo. Para vídeos, é o tempo em milissegundos. Para textos, é o número da página atual. |
| isExternalId | Indica se o documentId é um Id Externo |
async function getCurrentContent() {
if (current?.DocumentFileType === "Text") {
return {
documentId: current?.ExternalId != null ? current.ExternalId : String(current?.Id),
type: 1,
position: currentPage,
isExternalId: true
};
} else {
const currentVideoTime = await getCurrentTimeAsync();
return {
documentId: current?.ExternalId != null ? current.ExternalId : String(current?.Id),
type: 0,
position: currentVideoTime,
isExternalId: current?.ExternalId != null
};
}
}getCurrentContent: async () => {
return {
documentId: "video-aula-12",
type: 0,
position: player.currentTime * 1000,
isExternalId: true
};
}getCurrentContent: async () => {
return {
documentId: "pdf-capitulo-3",
type: 1,
position: pdfViewer.currentPage,
isExternalId: true
};
}Função para abrir o chat, pode ser utilizada para abrir a janela do chat a partir de outro local, por exemplo um botão customizado.
export function openChat() {
const tutorChat = document.getElementById('tutor-chat');
tutorChat.open();
}Importante: a função somente será executada caso o componente tenha sido configurado corretamente.
Função para fechar o chat
export function closeChat() {
const tutorChat = document.getElementById('tutor-chat');
tutorChat.close();
}Muitas vezes é desejável esconder o botão original caso o fluxo de abrir e fechar seja feito por botões customizados, nesse caso é possível utilizar a classe .chat-ative em um estilo css:
.chat:not(.chat-active) {
display: none;
}