Skip to main content
The Chatbot section provides an interactive AI assistant that answers questions about the CV owner’s experience, skills, and professional background using the Groq API.

Overview

This section features:
  • Answer display card with animated states
  • Quick question buttons for common queries
  • Custom input form for free-text questions
  • Groq API integration for AI-powered responses
  • Rate limiting and error handling
  • Conversation history management

File Location

src/sections/chatbot.astro

Architecture

The chatbot integrates profile data and API configuration:
---
import nicoProfile from '../data/nico-profile.json';

const API_KEY = import.meta.env.PUBLIC_GROQ_API_KEY || '';
const profileData = JSON.stringify(nicoProfile);
---

Structure

<section id="ai-chat" class="history ai-oracle-section">
    <h2 class="heading">
        <svg><!-- Sparkles icon --></svg>
        Pregunta lo que necesites
    </h2>

    <div class="oracle-container">
        <!-- Answer Card -->
        <div class="answer-card" id="answer-card">
            <!-- Dynamic content -->
        </div>

        <!-- Quick Questions -->
        <div class="quick-questions" id="quick-questions">
            <!-- Preset buttons -->
        </div>

        <!-- Input Form -->
        <form class="oracle-input" id="oracle-form">
            <!-- Text input and submit -->
        </form>
    </div>
</section>

Components

1. Answer Card

Displays AI responses with different states:
<div class="answer-card" id="answer-card">
    <div class="card-glow"></div>
    <div class="card-content">
        <div class="icon-wrapper" id="icon-wrapper">
            <svg><!-- Dynamic icon --></svg>
        </div>
        <p class="answer-text" id="answer-text">
            Escribe una pregunta sobre mi experiencia, habilidades o por qué deberías trabajar conmigo.
        </p>
    </div>
</div>
States:
  • Idle - Default state with info icon
  • Loading - Spinner animation while processing
  • Answered - Checkmark icon with response text
  • Error - X icon with error message

2. Quick Questions

Pre-defined questions for common queries:
<div class="quick-questions" id="quick-questions">
    <button class="q-btn" data-q="¿Cuáles son tus principales fortalezas?">
        <span class="q-icon">💪</span>
        <span class="q-text">Fortalezas</span>
    </button>
    <button class="q-btn" data-q="¿Qué experiencia tienes con proyectos CRM?">
        <span class="q-icon">📊</span>
        <span class="q-text">CRM</span>
    </button>
    <button class="q-btn" data-q="¿Por qué debería contratarte?">
        <span class="q-icon">🚀</span>
        <span class="q-text">Contratar</span>
    </button>
    <button class="q-btn" data-q="¿Qué herramientas dominas?">
        <span class="q-icon">🛠️</span>
        <span class="q-text">Herramientas</span>
    </button>
</div>

3. Input Form

Custom question input with submit button:
<form class="oracle-input" id="oracle-form">
    <input 
        type="text" 
        id="oracle-input" 
        placeholder="Escribe tu pregunta aquí..."
        autocomplete="off"
    />
    <button type="submit" id="ask-btn" aria-label="Consultar">
        <span class="btn-text">Consultar</span>
        <svg><!-- Search icon --></svg>
    </button>
</form>

Groq API Integration

Configuration

const GROQ_URL = 'https://api.groq.com/openai/v1/chat/completions';
const nicoProfile = JSON.parse(profileData);

System Prompt

The AI is instructed to act as the CV owner:
const systemPrompt = `Eres Nico AI, la versión digital de Nicolás Gaitán. 
Tu objetivo es representarlo de manera auténtica, responder preguntas sobre 
su experiencia y convencer a potenciales clientes o empleadores de trabajar con él.

INFORMACIÓN DE NICO:
${JSON.stringify(nicoProfile, null, 2)}

INSTRUCCIONES:
- Responde en primera persona como si fueras Nico
- Sé cercano, entusiasta y profesional
- Destaca siempre los puntos fuertes y la propuesta de valor
- Si preguntan por algo que no sabes, sugiere que contacten directamente a Nico
- Respuestas concisas pero completas (máx 2-3 párrafos)
- Usa emojis ocasionalmente para ser más cercano
- Si preguntan por qué contratarlo, sé convincente pero genuino
- Puedes recomendar que visiten inadaptado.cl o lo contacten directamente
- Responde siempre en español a menos que te hablen en otro idioma`;

API Request

const response = await fetch(GROQ_URL, {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer ' + API_KEY,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        model: 'llama-3.3-70b-versatile',
        messages: [
            { role: 'system', content: systemPrompt }
        ].concat(conversationHistory),
        max_tokens: 500,
        temperature: 0.7
    })
});
Model: llama-3.3-70b-versatile
Max tokens: 500
Temperature: 0.7 (balanced creativity)

JavaScript Features

State Management

let conversationHistory = [];
let isLoading = false;
let lastMessageTime = 0;
const COOLDOWN_MS = 2000;

Loading State

function setLoading(loading) {
    isLoading = loading;
    askBtn.disabled = loading;
    oracleInput.disabled = loading;
    
    if (loading) {
        answerCard.classList.add('loading');
        iconWrapper.innerHTML = '<div class="spinner"></div>';
        answerText.textContent = 'Analizando tu consulta...';
        quickQuestions.style.opacity = '0.3';
        quickQuestions.style.pointerEvents = 'none';
    } else {
        answerCard.classList.remove('loading');
        answerCard.classList.add('answered');
        iconWrapper.innerHTML = '<svg><!-- Checkmark --></svg>';
        quickQuestions.style.opacity = '1';
        quickQuestions.style.pointerEvents = 'auto';
    }
}

Rate Limiting

2-second cooldown between requests:
function getCooldownRemaining() {
    const elapsed = Date.now() - lastMessageTime;
    return Math.max(0, COOLDOWN_MS - elapsed);
}

const cooldown = getCooldownRemaining();
if (cooldown > 0) {
    showAnswer('⏳ Espera ' + Math.ceil(cooldown / 1000) + ' segundos...', true);
    return;
}

Error Handling

try {
    // API request...
    if (!response.ok) {
        if (response.status === 429) {
            throw new Error('RATE_LIMIT');
        }
        // Other errors...
    }
} catch (error) {
    if (error.message === 'RATE_LIMIT') {
        lastMessageTime = Date.now() + 10000;
        showAnswer('⏳ Demasiadas consultas. Espera 10 segundos.', true);
    } else {
        showAnswer('😅 Algo salió mal. Contáctame directamente en nicolas@inadaptado.cl', true);
    }
    conversationHistory.pop(); // Remove failed question
}

Event Listeners

Form submission:
oracleForm.addEventListener('submit', function(e) {
    e.preventDefault();
    const question = oracleInput.value.trim();
    if (!question || isLoading) return;
    
    oracleInput.value = '';
    askQuestion(question);
});
Quick question buttons:
quickBtns.forEach(function(btn) {
    btn.addEventListener('click', function() {
        const question = btn.getAttribute('data-q');
        if (question && !isLoading) {
            askQuestion(question);
        }
    });
});

Styling

Answer Card States

Loading state:
.answer-card.loading .card-glow {
    opacity: 1;
}

.answer-card.loading .icon-wrapper {
    animation: pulse-icon 1.5s ease-in-out infinite;
}

@keyframes pulse-icon {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.1); }
}
Answered state:
.answer-card.revealed {
    transform: scale(1.02);
    box-shadow: 0 12px 48px rgba(173, 0, 0, 0.15);
}
Error state:
.answer-card.error {
    background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
}

Animated Gradient Border

.card-glow {
    position: absolute;
    inset: -2px;
    background: linear-gradient(135deg, var(--color-primary), #ff6b6b, var(--color-primary));
    background-size: 200% 200%;
    border-radius: 1.5rem;
    opacity: 0;
    z-index: -1;
    animation: gradientShift 3s ease infinite;
}

@keyframes gradientShift {
    0%, 100% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
}

Spinner Animation

.spinner {
    width: 40px;
    height: 40px;
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-top-color: white;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

Profile Data Source

The chatbot uses structured data from src/data/nico-profile.json:
{
  "nombre": "Nicolás Gaitán",
  "titulo": "Publicista Creativo & Digital Project Manager",
  "empresa_actual": "VML Chile",
  "email": "nicolas@inadaptado.cl",
  "experiencia": [...],
  "habilidades": {...},
  "razones_para_contratar": [...]
}
See the full profile structure at src/data/nico-profile.json:1

Environment Variables

Required environment variable:
PUBLIC_GROQ_API_KEY=your_groq_api_key_here
Add this to your .env file. Get an API key from Groq.

Accessibility

  • Semantic HTML with proper ARIA labels
  • Keyboard navigation for all interactive elements
  • Disabled states during loading
  • Focus management for input field
  • Reduced motion support:
@media (prefers-reduced-motion: reduce) {
    .card-glow,
    .icon-wrapper,
    .answer-card {
        animation: none;
    }
}

Mobile Responsiveness

On mobile devices:
  • Quick question grid adjusts to 2 columns
  • Button text hides, showing only icons
  • Answer card padding reduces
  • Icon sizes scale down appropriately
@media (max-width: 640px) {
    .oracle-input button .btn-text {
        display: none;
    }
    
    .quick-questions {
        grid-template-columns: repeat(2, 1fr);
    }
}

Build docs developers (and LLMs) love