Introdução

O componente Chatbot fornece uma interface conversacional focada em unidades curriculares (UCs). O objetivo principal é permitir que estudantes e docentes obtenham respostas rápidas sobre avaliações, critérios, docentes e outros metadados presentes no ficheiro ucs.json, complementando com um modelo LLM para formatação e síntese das respostas.

A arquitetura combina:

  • Pesquisa local em ucs.json para identificar a UC alvo;
  • Normalização e consolidação de avaliações;
  • Contextualização do prompt do LLM para garantir respostas factuais e limitadas ao contexto;
  • Graceful fallback e mensagens de erro amigáveis.
Resumo das funcionalidades
  • Selecção da UC por sigla ou nome, com sugestões quando houver múltiplas correspondências;
  • Consolidação automática de avaliações repetidas por período;
  • Proxy serverless para LLM (endpoint `/api/chat`);
  • Histórico de conversa simples e limites de contexto para o LLM;
  • Mensagens de erro tratadas e indicadores de carregamento/typing.

Instalação

1

Instalar dependências

Instala as dependências necessárias para correr o chatbot e a pública API local utilizada no exemplo.
npm install axios lucide-vue-next date-fns
2

Adicionar o componente

Coloca `Chatbot.vue` em `components/` e regista-o onde for necessário (páginas, layouts, etc.).
import Chatbot from "@/components/Chatbot.vue";
Requisitos

O componente assume ambiente baseado em Vite/Nuxt/Astro com suporte a Vue 3 (Composition API). O endpoint serverless faz fetch para uma API externa (p.e. APIfree). Garante que tens uma política de CORS adequada e limites de taxa configurados para evitar abuse.

Utilização Base

Exemplo mínimo de uso do componente. Este exemplo ilustra a integração direta e rápida do chat numa página ou painel.

Exemplo: Chatbot incorporado
Previsualização: incorpora o componente <Chatbot /> no teu layout.
Nota sobre dados

O componente carrega por defeito os dados de /data/ucs.json. Se preferires, podes passar um prop (ex.: ucsData) e controlar o carregamento externamente.

Propriedades e Eventos

O Chatbot.vue utiliza o padrão de self-contained state, todos os estados internos são geridos no próprio componente. No entanto, existem várias propriedades computadas e eventos internos relevantes para extensão, teste ou integração com outros módulos.

Propriedades internas (reactive state)

Propriedade Tipo Descrição
messages Ref<Array<{ role: string; content: string; timestamp: Date }>> Histórico de mensagens do utilizador e do assistente.
userInput Ref<string> Conteúdo atual do campo de input do utilizador.
fase Ref<'selecionar' | 'chat'> Estado de fase atual — define se o bot está a selecionar UC ou em conversa ativa.
ucSelecionada Ref<any | null> Unidade curricular atualmente selecionada (objeto do JSON de UCs).
loading Ref<boolean> Indica se o chatbot está a aguardar resposta do LLM.
isTyping Ref<boolean> Controla a exibição do indicador de "A escrever...".
ucsData Array<any> Lista completa das UCs carregadas de /data/ucs.json.

Eventos internos e funções utilitárias

  • scrollToBottom() — garante que a janela de mensagens rola automaticamente para o fim após nova mensagem.
  • normalize(str) — remove acentuação e converte texto para minúsculas (para pesquisa robusta).
  • encontrarUC(input) — pesquisa local no JSON de UCs, retorna correspondência exata ou múltiplas opções.
  • consolidarAvaliacoes(avaliacoes) — agrupa avaliações com mesma descrição e datas próximas.
  • sendMessage() — rotina principal que processa a mensagem do utilizador, incluindo:
    • detecção de comandos ("mudar de UC");
    • gestão da fase de seleção;
    • preparação do contexto e envio ao LLM;
    • atualização do histórico local e UI.
APIs globais e dependências

O componente utiliza a variável global window.apifree.chat() para chamar o modelo LLM. Esta API deve ser inicializada externamente (p. ex., num onMounted() do layout principal).

Eventos customizados (se expandidos)

Embora o componente atual não emita eventos para o exterior, recomenda-se adicionar suporte futuro a:

  • @message-sent — disparado após o envio de cada mensagem do utilizador;
  • @reply-received — disparado quando o LLM devolve uma resposta válida;
  • @uc-changed — disparado quando o utilizador muda de UC.
Extensão recomendada

Estes eventos são úteis se o componente for integrado em dashboards, onde se possa registar a interação com analytics, ou sincronizar o estado da conversa com um store global (Pinia, Zustand, etc.).

Componentes Internos

O Chatbot.vue utiliza um conjunto mínimo de componentes de UI reutilizáveis do sistema @/components/ui. Cada um desempenha um papel bem definido na estrutura da interface.

1. Button

Utilizado no botão de envio da mensagem. Implementa estados de carregamento e desativação.

<Button type="submit" :disabled="loading || !userInput.trim()" class="h-11 px-6">
  <span v-if="!loading">Enviar</span>
  <span v-else class="flex items-center gap-2">
    <span class="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></span>
    A enviar
  </span>
</Button>

2. Input

Campo de entrada principal. Reativo através de v-model, suporta o envio com @submit.prevent.

3. Card

Contém cada bolha de mensagem, distinguindo entre utilizador e assistente com base na classe CSS. As sombras e bordas variam conforme o papel da mensagem.

4. Badge

Exibe o estado atual da conversa (“A selecionar” / “Em conversa”). Pode ser substituído por um componente StatusDot customizado, caso pretendas consistência visual em dashboards.

5. Indicador de “typing”

Representado por três pontos animados (.animate-bounce) dentro de um Card. O efeito é puramente visual e ativado por isTyping.value = true.

<div v-if="isTyping" class="flex gap-3 animate-in fade-in slide-in-from-bottom-4 duration-300">
  <div class="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-1">
    <span class="text-sm">🤖</span>
  </div>
  <Card class="px-4 py-3 bg-card border shadow-sm">
    <div class="flex gap-1">
      <div class="w-2 h-2 rounded-full bg-muted-foreground/40 animate-bounce" style="animation-delay: 0ms"></div>
      <div class="w-2 h-2 rounded-full bg-muted-foreground/40 animate-bounce" style="animation-delay: 150ms"></div>
      <div class="w-2 h-2 rounded-full bg-muted-foreground/40 animate-bounce" style="animation-delay: 300ms"></div>
    </div>
  </Card>
</div>
Estrutura modular

Todos os subcomponentes estão organizados de forma que podem ser substituídos sem alterar a lógica principal. Isto permite que o mesmo motor de chat seja usado em temas ou frameworks diferentes (por exemplo, Tailwind ou Vuetify).

6. messagesContainer

Elemento DOM referenciado via ref e utilizado pelo método scrollToBottom() para garantir visibilidade da última mensagem. Em dispositivos móveis, o comportamento “scroll-smooth” é ativado por nextTick() para evitar jumps.

7. messages list

A renderização condicional de mensagens alterna entre layout espelhado (flex-row-reverse) para o utilizador e normal para o assistente. Cada mensagem inclui avatar, corpo (Card) e timestamp formatado.

<div v-for="(msg, i) in messages" :key="i"
  class="flex gap-3 animate-in fade-in slide-in-from-bottom-4 duration-500"
  :class="msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'">
  <!-- Avatar -->
  <div class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 mt-1"
    :class="msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'">
    <span class="text-sm">{{ msg.role === 'user' ? '👤' : '🤖' }}</span>
  </div>
  <!-- Bubble -->
  <div class="flex flex-col gap-1 max-w-[85%]" :class="msg.role === 'user' ? 'items-end' : 'items-start'">
    <Card class="px-4 py-3 shadow-sm" :class="msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-card border'">
      <p class="text-sm leading-relaxed whitespace-pre-wrap break-words">{{ msg.content }}</p>
    </Card>
    <span class="text-xs text-muted-foreground px-1">{{ formatTime(msg.timestamp) }}</span>
  </div>
</div>

Estados e Lógica Interna

O Chatbot.vue implementa uma máquina de estados simples que governa o comportamento global do componente. A lógica principal é declarativa e gira em torno de fases de interação, iniciando com a seleção da UC e progredindo para a conversa guiada.

Ciclo de vida e fases

  • Fase “selecionar” — o chatbot espera que o utilizador indique uma UC por nome ou sigla;
  • Fase “chat” — após a seleção, o bot permite perguntas relacionadas com essa UC;
  • Fallback — se uma UC deixa de estar acessível (dados inconsistentes), o estado regressa a “selecionar”.
const fase = ref<'selecionar' | 'chat'>('selecionar')
const ucSelecionada = ref<any | null>(null)

watch(fase, (novaFase) => {
  if (novaFase === 'selecionar') {
    messages.value.push({
      role: 'assistant',
      content: 'Olá! Diz-me a sigla ou nome da UC sobre a qual queres saber mais.',
      timestamp: new Date()
    })
  }
})

Gestão de mensagens e normalização

O histórico é mantido em memória reativa através de messages. Cada mensagem tem metadados mínimos e é limpa de espaços redundantes ou caracteres invisíveis.

function addMessage(role: 'user' | 'assistant', content: string) {
  messages.value.push({
    role,
    content: content.trim(),
    timestamp: new Date()
  })
  nextTick(scrollToBottom)
}

Deteção e seleção de UC

A função encontrarUC usa normalização de texto (sem acentos e minúsculas) para comparar o input com nomes e siglas de UCs. Suporta correspondência parcial e devolve múltiplas opções se necessário.

function encontrarUC(input: string) {
  const termo = normalize(input)
  const correspondencias = ucsData.filter(
    uc => normalize(uc.nome).includes(termo) || normalize(uc.sigla) === termo
  )
  if (correspondencias.length === 1) return correspondencias[0]
  if (correspondencias.length > 1) {
    addMessage('assistant', 'Foram encontradas várias UCs: ' +
      correspondencias.map(u => u.sigla).join(', ') + '. Podes especificar melhor?')
    return null
  }
  addMessage('assistant', 'Não encontrei nenhuma UC com esse nome. Tenta novamente.')
  return null
}

Integração com o LLM (via API)

Quando a UC está selecionada e o utilizador envia uma pergunta, a mensagem é enviada para o endpoint /api/chat. Este endpoint formata o contexto e comunica com o modelo LLM definido (p.ex., GPT-4, Claude, Mistral, etc.).

async function sendMessage() {
  const input = userInput.value.trim()
  if (!input) return
  addMessage('user', input)
  userInput.value = ''
  isTyping.value = true

  if (fase.value === 'selecionar') {
    const uc = encontrarUC(input)
    if (uc) {
      ucSelecionada.value = uc
      fase.value = 'chat'
      addMessage('assistant', `Selecionada a UC ${uc.sigla} — ${uc.nome}`)
      addMessage('assistant', 'Podes agora perguntar sobre avaliações, docentes, etc.')
    }
    isTyping.value = false
    return
  }

  try {
    const { data } = await axios.post('/api/chat', {
      prompt: input,
      uc: ucSelecionada.value,
    })
    addMessage('assistant', data.reply)
  } catch (err) {
    addMessage('assistant', '⚠️ Ocorreu um erro ao processar a tua pergunta. Tenta novamente.')
  } finally {
    isTyping.value = false
  }
}
Gestão de erros

Todos os erros de rede, parsing ou resposta inesperada são capturados e traduzidos numa mensagem humanizada. Isto evita falhas silenciosas e mantém a UX fluida mesmo quando o LLM ou o endpoint estão offline.

Formatação e contexto

O endpoint serverless inclui no prompt o contexto formatado da UC (descrição, objetivos, avaliações consolidadas e corpo docente). Isso garante que as respostas são contextualizadas e factualmente limitadas à UC selecionada.

const contexto = `
UC: ${uc.sigla} — ${uc.nome}
Objetivos: ${uc.objetivos}
Avaliações: ${uc.avaliacoes.map(a => a.descricao + ' (' + a.peso + '%)').join(', ')}
Docentes: ${uc.docentes.join(', ')}
`

const { data } = await axios.post('/api/chat', {
  prompt: 'Responde com base apenas neste contexto:
' + contexto + '

Pergunta: ' + input
})

Decisões de Design

O Chatbot foi concebido com base em três princípios: autonomia local, simplicidade cognitiva e neutralidade de framework. Esta secção explica as decisões arquitetónicas e os trade-offs considerados.

1. Autonomia local

O componente gere o seu próprio estado interno e não depende de um store global. Esta escolha reduz a complexidade e permite que o Chatbot seja usado isoladamente (por exemplo, numa página estática com fallback local sem rede).

2. Simplicidade cognitiva

A lógica de fluxo é legível e orientada a fases (“selecionar” / “chat”). Evita-se FSMs complexas, mas mantém-se previsibilidade e rastreabilidade no comportamento.

3. Neutralidade de framework

Apesar de implementado em Vue 3, o design é suficientemente genérico para ser portado para React ou Svelte. O core (gestão de UC, normalização e API calls) é completamente independente de Vue.

4. User Experience (UX) consistente

O estilo segue o mesmo sistema visual de todos os componentes do projeto (botões, cards, etc.). A experiência é intencionalmente minimalista e sem distracções, o foco é no conteúdo da resposta.

5. Comunicação com o LLM

A API intermediária (/api/chat) atua como camada de controlo. Isso previne injeção direta de prompts pelo cliente e permite aplicar políticas de filtragem ou rate-limiting antes de chegar ao modelo.

6. Tratamento de erros humanizado

Mensagens de erro são formuladas de forma empática (“Tenta novamente”, “Ocorreu um erro…”), para não quebrar a imersão da conversa. A priorização de UX sobre logs técnicos é deliberada.

7. Consolidação de avaliações

A função consolidarAvaliacoes foi desenhada para corrigir inconsistências no JSON de origem, agrupando avaliações duplicadas com base em data e descrição. Isto evita duplicação de informação no prompt e melhora a qualidade das respostas.

8. Segurança de dados

Nenhum dado sensível é armazenado ou enviado diretamente. O histórico reside apenas em memória, e pode ser limpo a qualquer momento com um reset() (função recomendada para futura extensão).

Resumo das decisões
  • Arquitetura minimalista e transparente;
  • Separação clara entre UI e lógica de conversação;
  • Sem dependência de estado global;
  • Camada de API controlada e auditável;
  • UX e mensagens humanizadas.

Integração com outras partes do projeto

O Chatbot foi projetado para integrar-se nativamente com o ecossistema existente do projeto, nomeadamente com o ficheiro ucs.json, os endpoints API e os componentes da UI. Esta secção explica como se processa essa integração e como adaptar o componente para novos contextos ou fontes de dados.

1. Fonte de dados — ucs.json

O ficheiro /data/ucs.json contém o catálogo de unidades curriculares e é carregado de forma assíncrona via fetch na montagem do componente. A sua estrutura deve seguir o seguinte formato mínimo:

[
  {
    "sigla": "ASCN",
    "nome": "Arquitetura de Sistemas de Computação e Redes",
    "perfil": "Exploração de conceitos de redes e sistemas distribuídos...",
    "criterios": ["Trabalho prático", "Exame final"],
    "docentes": ["Prof. A", "Prof. B"],
    "avaliacoes": [
      { "data": "2024-10-01", "descricao": "Entrega TP1" },
      { "data": "2024-11-15", "descricao": "Exame Final" }
    ]
  }
]

Qualquer propriedade adicional presente neste JSON é ignorada, mas pode ser aproveitada no contextoUC enviado para o LLM, caso se deseje enriquecer as respostas.

Carregamento local vs remoto

Embora o Chatbot utilize fetch("/data/ucs.json"), nada impede que este ficheiro seja servido por uma API externa (ex: /api/ucs). O comportamento permanece idêntico, bastando garantir que o JSON devolvido segue o mesmo esquema.

2. Endpoint de intermediação — /api/chat.ts

O endpoint pages/api/chat.ts atua como um proxy controlado entre o cliente e o modelo LLM externo. O objetivo é evitar exposição direta da chave API e aplicar camadas de validação.

import type { APIRoute } from "astro"

export const POST: APIRoute = async ({ request }) => {
  try {
    const body = await request.json()
    const response = await fetch("https://apifreellm.com/api/openai/v1/chat/completions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    })
    if (!response.ok) throw new Error('Resposta inválida do modelo')
    const data = await response.json()
    return new Response(JSON.stringify(data), {
      headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
    })
  } catch (err) {
    console.error(err)
    return new Response(JSON.stringify({ error: "Erro na comunicação com o modelo" }), { status: 500 })
  }
}

Esta camada de intermediação é fundamental para manter o controlo sobre:

  • Gestão de limites de uso (rate limiting);
  • Auditoria de prompts enviados e respostas devolvidas;
  • Substituição futura por modelos locais (ex: Ollama, LM Studio).

3. Integração visual e navegação

O Chatbot usa o mesmo sistema de design (botões, badges, cards, etc.) baseado em shadcn/ui, garantindo consistência visual em toda a aplicação. A integração no layout global é feita via lazy load, para não sobrecarregar o bundle inicial.

---
import Chatbot from "@/components/Chatbot.vue"
import DocsLayout from "@/layouts/DocsLayout.astro"
---

<DocsLayout title="Assistente de UCs">
  <Chatbot client:load />
</DocsLayout>
Boas práticas de integração
  • Carregar o componente apenas quando a rota “/chat for visitada;
  • Servir o JSON de UCs de forma estática (por exemplo: cache de 1h);
  • Evitar dependência de APIs externas sem fallback local;
  • Tratar respostas de erro do LLM com mensagens amigáveis.

Boas práticas e recomendações

O Chatbot foi concebido com foco em clareza, modularidade e robustez. Seguir boas práticas de integração e manutenção garante que o componente se mantenha previsível, performante e fácil de evoluir.

1. Separação de responsabilidades

  • Mantém a lógica de IA e comunicação de rede isolada do template visual.
  • Utiliza watch apenas para efeitos colaterais inevitáveis (como scroll automático).
  • Evita mutar o array messages fora de métodos declarados (use helpers internos).

2. Performance

  • Limita o número de mensagens no histórico a 10-15, conforme capacidade de contexto do modelo.
  • Evita chamadas redundantes ao LLM, verifica se há contexto suficiente localmente antes de enviar.
  • Considera pré-carregar o JSON de UCs em cache via astro:prefetch.

3. UX e acessibilidade

  • Usa feedback visual (spinner, animações) para comunicar estados de carregamento.
  • Permite enviar mensagens com Enter e Shift+Enter para multiline.
  • Evita que respostas longas causem overflow visual no container de mensagens.

4. Resiliência

Tratamento de erros

Implementa sempre tratamento de erros para rede e respostas inválidas. Mesmo um timeout ou falha de parsing deve devolver uma resposta amigável e orientadora.

5. Segurança

  • Evita injeção de conteúdo direto em v-html.
  • Sanitiza prompts e respostas antes de exibir no DOM.
  • Controla o acesso ao endpoint /api/chat com quotas e validação de origem.
Checklist de manutenção
  • Testar com 3 a 5 UCs reais e verificar respostas contextualizadas;
  • Monitorizar logs do endpoint /api/chat para exceções;
  • Atualizar descrições e prompts sempre que o contexto mudar;
  • Validar JSON antes de carregar (evita erros silenciosos no front-end).

Extensões futuras / roadmap

A arquitetura modular do Chatbot facilita a sua evolução incremental. Esta secção descreve algumas possíveis melhorias e expansões planeadas para versões futuras.

1. Contexto cruzado entre UCs

Permitir que o Chatbot identifique relações entre UCs (ex: pré-requisitos ou tópicos comuns), oferecendo respostas que agreguem conhecimento transversal.

2. Histórico persistente

Implementar armazenamento local (via localStorage ou IndexedDB) para preservar o histórico entre sessões, com opção de exportação em JSON.

3. Modo multimodal

Adicionar suporte a mensagens com anexos (ex: PDFs ou imagens), permitindo ao modelo interpretar documentos de avaliação e fornecer feedback contextual.

4. Integração com calendário institucional

Ligar o Chatbot ao componente Calendar.vue para apresentar prazos e eventos diretamente na conversa.

5. Painel administrativo

Criar uma interface interna onde docentes possam visualizar interações, ajustar prompts, e definir respostas padrão para cada UC.

Modelo de expansão recomendada

Sempre que uma nova funcionalidade for adicionada, deve ser isolada em subcomponentes Vue e documentada separadamente neste mesmo formato de documentação.

Preview interativo

Abaixo encontras uma instância interativa do componente Chatbot, carregada diretamente do código-fonte principal. Este exemplo utiliza o endpoint local /api/chat e o ficheiro ucs.json padrão.

Sandbox de demonstração

O preview não guarda histórico entre refreshes. Para testar prompts mais complexos ou integração real, acede à rota dedicada /chat.

Versão do documento: 1.0.1 • Última atualização: Outubro 2025 — Infoloom