Files
cv-site/templates/partials/widgets/chat-widget.html
T

318 lines
13 KiB
HTML
Raw Normal View History

{{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>
<div class="chat-header-actions">
<button class="chat-mode-btn active" data-mode="" title="{{if eq .Lang "es"}}Compacto{{else}}Compact{{end}}" onclick="setChatSize('')">
<iconify-icon icon="mdi:message-outline"></iconify-icon>
2026-04-08 17:51:14 +01:00
</button>
<button class="chat-mode-btn" data-mode="chat-half" title="{{if eq .Lang "es"}}Panel lateral{{else}}Side panel{{end}}" onclick="setChatSize('chat-half')">
<iconify-icon icon="mdi:page-layout-sidebar-right"></iconify-icon>
2026-04-08 17:51:14 +01:00
</button>
<button class="chat-mode-btn" data-mode="chat-float" title="{{if eq .Lang "es"}}Flotante arrastra para mover{{else}}Floating drag to move{{end}}" onclick="setChatSize('chat-float')">
<iconify-icon icon="mdi:cursor-move"></iconify-icon>
</button>
<button class="chat-mode-btn" data-mode="chat-full" title="{{if eq .Lang "es"}}Pantalla completa{{else}}Full screen{{end}}" onclick="setChatSize('chat-full')">
<iconify-icon icon="mdi:arrow-expand-all"></iconify-icon>
</button>
<span class="chat-header-divider"></span>
<button class="chat-mode-btn"
title="{{if eq .Lang "es"}}Ayuda{{else}}Help{{end}}"
commandfor="chat-help-modal"
command="show-modal">
<iconify-icon icon="mdi:help-circle-outline"></iconify-icon>
2026-04-08 17:51:14 +01:00
</button>
</div>
</div>
<div id="chat-messages" class="chat-messages">
2026-04-08 17:51:14 +01:00
<div class="chat-row chat-row-bot">
<div class="chat-avatar"><iconify-icon icon="mdi:robot-happy-outline"></iconify-icon></div>
<div class="chat-msg">{{if eq .Lang "es"}}¡Hola! Pregúntame lo que quieras sobre este CV.{{else}}Hi! Ask me anything about this CV.{{end}}</div>
</div>
</div>
<!-- Typing / Status Indicator -->
<div id="chat-typing" class="chat-typing">
<span id="chat-status-text" class="chat-status-text" style="display:none"></span>
<span class="chat-typing-dots">
<span class="chat-typing-dot"></span>
<span class="chat-typing-dot"></span>
<span class="chat-typing-dot"></span>
</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"
hx-request='{"timeout":120000}'>
<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>
// Chat model readiness state
var chatModelReady = false;
var chatWarmedUp = false;
// Poll model status until ready
function pollChatStatus() {
fetch('/api/chat/status').then(function(r) { return r.json(); }).then(function(data) {
if (data.ready) {
chatModelReady = true;
var statusEl = document.getElementById('chat-status-text');
if (statusEl) statusEl.style.display = 'none';
} else if (data.warming) {
setTimeout(pollChatStatus, 2000);
}
}).catch(function() {});
}
// Trigger warmup on page load and start polling
(function() {
if (!chatWarmedUp) {
chatWarmedUp = true;
fetch('/api/chat/warmup', { method: 'POST' }).catch(function() {});
pollChatStatus();
}
})();
// Toggle chat panel open/close
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();
}
}
// Show user message bubble immediately in chat
var chatBubblePending = false;
function appendUserBubble(text) {
var messages = document.getElementById('chat-messages');
var row = document.createElement('div');
row.className = 'chat-row chat-row-user';
row.innerHTML = '<div class="chat-msg">' + text.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</div>'
+ '<div class="chat-avatar chat-avatar-user"><iconify-icon icon="mdi:account"></iconify-icon></div>';
messages.appendChild(row);
messages.scrollTop = messages.scrollHeight;
chatBubblePending = true;
}
// Send a question immediately (from chip click) — show bubble, clear input, send
function sendChatQuestion(question) {
appendUserBubble(question);
var input = document.getElementById('chat-input');
var form = document.getElementById('chat-form');
input.value = question;
htmx.trigger(form, 'submit');
input.value = '';
}
// Prefill input from help modal (user must click Send)
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');
}
var input = document.getElementById('chat-input');
input.value = question;
input.focus();
}
// Form submit — show user bubble for manual typing, clear input
document.addEventListener('htmx:beforeRequest', function(event) {
if (event.detail.elt && event.detail.elt.id === 'chat-form') {
var input = document.getElementById('chat-input');
var msg = input.value.trim();
// Chips already added their bubble; manual typing needs one
if (msg && !chatBubblePending) {
appendUserBubble(msg);
}
chatBubblePending = false;
input.value = '';
// Show initializing or typing indicator
var statusEl = document.getElementById('chat-status-text');
var dotsEl = document.querySelector('.chat-typing-dots');
if (!chatModelReady && statusEl && dotsEl) {
statusEl.textContent = '{{if eq .Lang "es"}}Inicializando modelo IA…{{else}}Initializing AI model…{{end}}';
statusEl.style.display = 'inline';
dotsEl.style.display = 'none';
} else if (statusEl && dotsEl) {
statusEl.style.display = 'none';
dotsEl.style.display = 'inline-flex';
}
}
});
// Reset indicator 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 = '';
chatModelReady = true;
var statusEl = document.getElementById('chat-status-text');
if (statusEl) statusEl.style.display = 'none';
var dotsEl = document.querySelector('.chat-typing-dots');
if (dotsEl) dotsEl.style.display = 'inline-flex';
}
});
// Set chat panel size
2026-04-08 17:51:14 +01:00
function setChatSize(size) {
var panel = document.getElementById('chat-panel');
panel.classList.remove('chat-half', 'chat-full', 'chat-float');
panel.style.top = '';
panel.style.left = '';
panel.style.right = '';
panel.style.bottom = '';
2026-04-08 17:51:14 +01:00
if (size) panel.classList.add(size);
// Update active button
document.querySelectorAll('.chat-mode-btn[data-mode]').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-mode') === size);
2026-04-08 17:51:14 +01:00
});
// Enable/disable drag
if (size === 'chat-float') {
enableChatDrag(panel);
} else {
disableChatDrag(panel);
}
}
// Drag support for floating mode
var chatDragState = { dragging: false, offsetX: 0, offsetY: 0 };
function enableChatDrag(panel) {
var header = panel.querySelector('.chat-header');
header.style.cursor = 'grab';
header.addEventListener('mousedown', chatDragStart);
header.addEventListener('touchstart', chatDragTouchStart, { passive: false });
}
function disableChatDrag(panel) {
var header = panel.querySelector('.chat-header');
header.style.cursor = '';
header.removeEventListener('mousedown', chatDragStart);
header.removeEventListener('touchstart', chatDragTouchStart);
}
function chatDragStart(e) {
if (e.target.closest('button')) return;
var panel = document.getElementById('chat-panel');
chatDragState.dragging = true;
chatDragState.offsetX = e.clientX - panel.getBoundingClientRect().left;
chatDragState.offsetY = e.clientY - panel.getBoundingClientRect().top;
panel.classList.add('chat-dragging');
document.addEventListener('mousemove', chatDragMove);
document.addEventListener('mouseup', chatDragEnd);
panel.querySelector('.chat-header').style.cursor = 'grabbing';
e.preventDefault();
}
function chatDragTouchStart(e) {
if (e.target.closest('button')) return;
var panel = document.getElementById('chat-panel');
var touch = e.touches[0];
chatDragState.dragging = true;
chatDragState.offsetX = touch.clientX - panel.getBoundingClientRect().left;
chatDragState.offsetY = touch.clientY - panel.getBoundingClientRect().top;
panel.classList.add('chat-dragging');
document.addEventListener('touchmove', chatDragTouchMove, { passive: false });
document.addEventListener('touchend', chatDragEnd);
e.preventDefault();
}
function chatDragMove(e) {
if (!chatDragState.dragging) return;
var panel = document.getElementById('chat-panel');
panel.style.left = (e.clientX - chatDragState.offsetX) + 'px';
panel.style.top = (e.clientY - chatDragState.offsetY) + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
}
function chatDragTouchMove(e) {
if (!chatDragState.dragging) return;
var touch = e.touches[0];
var panel = document.getElementById('chat-panel');
panel.style.left = (touch.clientX - chatDragState.offsetX) + 'px';
panel.style.top = (touch.clientY - chatDragState.offsetY) + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
e.preventDefault();
}
function chatDragEnd() {
chatDragState.dragging = false;
var panel = document.getElementById('chat-panel');
panel.classList.remove('chat-dragging');
panel.querySelector('.chat-header').style.cursor = 'grab';
document.removeEventListener('mousemove', chatDragMove);
document.removeEventListener('mouseup', chatDragEnd);
document.removeEventListener('touchmove', chatDragTouchMove);
document.removeEventListener('touchend', chatDragEnd);
}
// Navigate from chat link to CV section, then highlight
function scrollToCV(link) {
var anchor = link.getAttribute('href');
var target = document.querySelector(anchor);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.classList.add('chat-highlight');
setTimeout(function() { target.classList.remove('chat-highlight'); }, 2000);
}
2026-04-08 17:51:14 +01:00
return false;
}
</script>
{{end}}
{{end}}