Files
cv-site/templates/partials/widgets/chat-widget.html
T
juanatsap 160be31b31 feat: auto-fallback Gemini→Ollama + model warmup on chat open
Dual-provider architecture:
- Both Gemini and Ollama initialize at startup (if configured)
- Primary (Gemini) tried first for every request
- On any error (429, 503, timeout), automatically falls back to Ollama
- No manual switching needed — completely transparent to the user
- Log shows: "Primary failed (gemini: ...), falling back to ollama: ..."

Warmup:
- POST /api/chat/warmup called silently when chat panel opens
- Pre-loads Ollama model in background (10-15s) while user reads welcome
- By the time user types, model is ready for instant response
- Warms up fallback provider specifically (Gemini doesn't need it)

Timeout:
- Agent context increased to 60s (Ollama first response can be slow)
- Each request creates a fresh session (stateless for fallback compat)
2026-04-08 14:57:38 +01:00

124 lines
5.3 KiB
HTML

{{define "chat-widget"}}
{{if .ChatEnabled}}
<!-- AI Chat Widget — CV Assistant Mascot -->
<button
id="chat-toggle-btn"
class="chat-toggle-btn no-print has-tooltip tooltip-left"
aria-label="{{if eq .Lang "es"}}Asistente del CV{{else}}CV Assistant{{end}}"
data-tooltip="{{if eq .Lang "es"}}Asistente del CV{{else}}CV Assistant{{end}}"
onclick="toggleChatPanel()">
<iconify-icon icon="mdi:robot-happy-outline" class="chat-icon-open"></iconify-icon>
<iconify-icon icon="mdi:close" class="chat-icon-close"></iconify-icon>
</button>
<div id="chat-panel" class="chat-panel no-print">
<div class="chat-header">
<iconify-icon icon="mdi:robot-happy-outline"></iconify-icon>
<span>{{if eq .Lang "es"}}Asistente del CV{{else}}CV Assistant{{end}}</span>
<button class="chat-help-btn"
aria-label="{{if eq .Lang "es"}}Ayuda{{else}}Help{{end}}"
commandfor="chat-help-modal"
command="show-modal">
<iconify-icon icon="mdi:help-circle-outline"></iconify-icon>
</button>
</div>
<div id="chat-messages" class="chat-messages">
<div class="chat-message chat-agent">
{{if eq .Lang "es"}}¡Hola! Pregúntame lo que quieras sobre este CV.{{else}}Hi! Ask me anything about this CV.{{end}}
</div>
</div>
<!-- Typing Indicator -->
<div id="chat-typing" class="chat-typing">
<span class="chat-typing-dot"></span>
<span class="chat-typing-dot"></span>
<span class="chat-typing-dot"></span>
</div>
<!-- Suggested Questions -->
<div class="chat-suggestions">
{{if eq .Lang "es"}}
<button type="button" class="chat-chip" onclick="sendChatQuestion('¿Qué proyectos en Go ha hecho?')">¿Proyectos en Go?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('¿Cuántos años de experiencia tiene?')">¿Años de experiencia?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('¿En qué empresas ha trabajado?')">¿Empresas?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('¿Conoce React?')">¿Conoce React?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('¿Qué certificaciones tiene?')">¿Certificaciones?</button>
{{else}}
<button type="button" class="chat-chip" onclick="sendChatQuestion('What Go projects has he built?')">Go projects?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('How many years of experience?')">Years of experience?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('What companies has he worked at?')">Companies?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('Does he know React?')">Knows React?</button>
<button type="button" class="chat-chip" onclick="sendChatQuestion('What certifications?')">Certifications?</button>
{{end}}
</div>
<form id="chat-form" class="chat-input-area"
hx-post="/api/chat"
hx-target="#chat-messages"
hx-swap="beforeend scroll:#chat-messages:bottom"
hx-indicator="#chat-typing">
<input type="hidden" id="chat-session-id" name="session_id" value="">
<input type="hidden" name="lang" value="{{.Lang}}">
<input
type="text"
id="chat-input"
name="message"
class="chat-input"
placeholder="{{if eq .Lang "es"}}Pregunta algo sobre el CV...{{else}}Ask something about the CV...{{end}}"
autocomplete="off">
<button type="submit" class="chat-send-btn" aria-label="Send">
<iconify-icon icon="mdi:send"></iconify-icon>
</button>
</form>
</div>
<!-- Chat JavaScript — all interactions in plain JS, no Hyperscript -->
<script>
// Toggle chat panel open/close
var chatWarmedUp = false;
function toggleChatPanel() {
var panel = document.getElementById('chat-panel');
var btn = document.getElementById('chat-toggle-btn');
panel.classList.toggle('chat-open');
btn.classList.toggle('mascot-active');
if (panel.classList.contains('chat-open')) {
document.getElementById('chat-input').focus();
// Warm up the model on first open (silent background ping)
if (!chatWarmedUp) {
chatWarmedUp = true;
fetch('/api/chat/warmup', { method: 'POST' }).catch(function() {});
}
}
}
// Send a question (from chip or help modal)
function sendChatQuestion(question) {
var input = document.getElementById('chat-input');
var form = document.getElementById('chat-form');
input.value = question;
htmx.trigger(form, 'submit');
}
// Close help modal, open chat, and send question
function closeChatHelpAndAsk(question) {
document.getElementById('chat-help-modal').close();
var panel = document.getElementById('chat-panel');
var btn = document.getElementById('chat-toggle-btn');
if (!panel.classList.contains('chat-open')) {
panel.classList.add('chat-open');
btn.classList.add('mascot-active');
}
sendChatQuestion(question);
}
// Clear input after HTMX request completes
document.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.elt && event.detail.elt.id === 'chat-form') {
document.getElementById('chat-input').value = '';
}
});
</script>
{{end}}
{{end}}