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.jsonpara 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.
- 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
Instalar dependências
npm install axios lucide-vue-next date-fnsAdicionar o componente
import Chatbot from "@/components/Chatbot.vue";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.
<Chatbot /> no teu layout.
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< | 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.
 
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.
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>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
  }
}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).
- 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.
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>- 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 watchapenas para efeitos colaterais inevitáveis (como scroll automático).
- 
Evita mutar o array messagesfora 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
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/chatcom quotas e validação de origem.
- Testar com 3 a 5 UCs reais e verificar respostas contextualizadas;
- 
Monitorizar logs do endpoint /api/chatpara 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.
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.
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