300 lines
12 KiB
HTML
300 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Support Chat{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
#chat-window {
|
|
height: 420px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .75rem;
|
|
padding: 1rem;
|
|
background: #f8f9fa;
|
|
}
|
|
.msg-bubble {
|
|
max-width: 80%;
|
|
padding: .6rem .9rem;
|
|
border-radius: 1rem;
|
|
font-size: .9rem;
|
|
line-height: 1.5;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.msg-user { background:#0d6efd; color:#fff; border-bottom-right-radius:.25rem; align-self:flex-end; }
|
|
.msg-ai { background:#fff; border:1px solid #dee2e6; border-bottom-left-radius:.25rem; align-self:flex-start; }
|
|
.msg-system { background:#fff3cd; border:1px solid #ffc107; border-radius:.5rem; align-self:center;
|
|
font-size:.8rem; text-align:center; padding:.4rem .8rem; color:#664d03; }
|
|
.typing-dot { display:inline-block; width:8px; height:8px; border-radius:50%;
|
|
background:#adb5bd; margin:0 2px; animation:blink 1.2s infinite; }
|
|
.typing-dot:nth-child(2) { animation-delay:.2s; }
|
|
.typing-dot:nth-child(3) { animation-delay:.4s; }
|
|
@keyframes blink { 0%,80%,100%{opacity:.2} 40%{opacity:1} }
|
|
.faq-btn { font-size:.82rem; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
|
|
{# ── Header ── #}
|
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
|
<div>
|
|
<h4 class="mb-0"><i class="bi bi-chat-dots me-2 text-primary"></i>JQC Support Chat</h4>
|
|
<small class="text-muted">Ask a question or browse common topics below</small>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="{{ url_for('support.my_conversations') }}" class="btn btn-outline-secondary btn-sm"
|
|
title="View your saved conversations">
|
|
<i class="bi bi-clock-history me-1"></i>History
|
|
</a>
|
|
<a href="{{ url_for('support.chat', new=1) }}" class="btn btn-outline-secondary btn-sm"
|
|
title="Start a fresh conversation">
|
|
<i class="bi bi-plus-circle me-1"></i>New
|
|
</a>
|
|
<button class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#submitModal">
|
|
<i class="bi bi-envelope me-1"></i>Submit to Support
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Chat card ── #}
|
|
<div class="card shadow-sm">
|
|
{# Message window #}
|
|
<div id="chat-window"></div>
|
|
|
|
{# FAQ quick-reply chips #}
|
|
<div id="faq-section" class="px-3 pt-2 pb-1 border-top bg-white">
|
|
<p class="small text-muted mb-2"><i class="bi bi-lightning-charge me-1"></i>Common questions:</p>
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
{% for faq in faqs %}
|
|
<button class="btn btn-outline-secondary btn-sm faq-btn"
|
|
data-faq="{{ faq.text | e }}"
|
|
onclick="sendFaq(this)">
|
|
<i class="{{ faq.icon }} me-1"></i>{{ faq.text }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Input row #}
|
|
<div class="p-3 border-top bg-white">
|
|
<div class="input-group">
|
|
<input id="chat-input" type="text" class="form-control"
|
|
placeholder="Type your question…" maxlength="500"
|
|
{% if not groq_ready %}disabled title="AI assistant not configured"{% endif %}>
|
|
<button id="send-btn" class="btn btn-primary" onclick="sendUserMessage()"
|
|
{% if not groq_ready %}disabled{% endif %}>
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
{% if not groq_ready %}
|
|
<div class="text-muted small mt-1">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
AI assistant is not configured. Please
|
|
<a href="#" data-bs-toggle="modal" data-bs-target="#submitModal">submit a request</a>
|
|
to reach our team.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-muted small mt-2 text-center">
|
|
Can't find what you need?
|
|
<a href="#" data-bs-toggle="modal" data-bs-target="#submitModal">Submit a support request</a>
|
|
and our team will respond by email.
|
|
</p>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Submit to Support modal ── #}
|
|
<div class="modal fade" id="submitModal" tabindex="-1" aria-labelledby="submitModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="submitModalLabel">
|
|
<i class="bi bi-envelope me-2"></i>Submit Support Request
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="post" action="{{ url_for('support.submit_ticket') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Subject <span class="text-danger">*</span></label>
|
|
<input type="text" name="subject" class="form-control" required maxlength="200"
|
|
placeholder="Briefly describe your issue">
|
|
</div>
|
|
{% if facilities %}
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Related Facility</label>
|
|
<select name="facility_id" class="form-select">
|
|
<option value="">— Not facility-specific —</option>
|
|
{% for f in facilities %}
|
|
<option value="{{ f.id }}">{{ f.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Description <span class="text-danger">*</span></label>
|
|
<textarea name="body" class="form-control" rows="5" required
|
|
placeholder="Please describe your question or concern in detail…"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-send me-1"></i>Submit Request
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
const chatWindow = document.getElementById('chat-window');
|
|
const chatInput = document.getElementById('chat-input');
|
|
const CSRF_TOKEN = '{{ csrf_token() }}';
|
|
|
|
// In-memory conversation history sent to the server with each message
|
|
let history = [];
|
|
// Server-side saved session this chat is being persisted to (null until first send)
|
|
let sessionId = {{ chat_session_id | tojson }};
|
|
const PRIOR = {{ chat_history | tojson }};
|
|
|
|
// ── Restore prior conversation, or greet on a fresh session ────────────
|
|
var userName = {{ current_user.display_name | tojson }};
|
|
if (PRIOR && PRIOR.length) {
|
|
PRIOR.forEach(function (m) {
|
|
appendMessage(m.role === 'user' ? 'user' : 'ai', m.content);
|
|
history.push({ role: m.role, content: m.content });
|
|
});
|
|
appendSystem('Continuing your previous conversation. Click "New" above to start a fresh one.');
|
|
} else {
|
|
appendMessage('ai', "👋 Hi " + userName + "! I'm your JQC support assistant. " +
|
|
"I can help you with inspections, issues, reports, and more. " +
|
|
"Click a question below or type your own.");
|
|
}
|
|
|
|
// ── Append a message bubble ────────────────────────────────────────────
|
|
function appendMessage(role, text) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg-bubble msg-' + role;
|
|
div.textContent = text;
|
|
chatWindow.appendChild(div);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
return div;
|
|
}
|
|
|
|
function appendSystem(text) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg-bubble msg-system';
|
|
div.textContent = text;
|
|
chatWindow.appendChild(div);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
}
|
|
|
|
// ── Typing indicator ───────────────────────────────────────────────────
|
|
function showTyping() {
|
|
const div = document.createElement('div');
|
|
div.id = 'typing-indicator';
|
|
div.className = 'msg-bubble msg-ai';
|
|
div.innerHTML = '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>';
|
|
chatWindow.appendChild(div);
|
|
chatWindow.scrollTop = chatWindow.scrollHeight;
|
|
}
|
|
|
|
function hideTyping() {
|
|
const el = document.getElementById('typing-indicator');
|
|
if (el) el.remove();
|
|
}
|
|
|
|
// ── Send message to Groq ───────────────────────────────────────────────
|
|
function sendMessage(text) {
|
|
if (!text.trim()) return;
|
|
|
|
// FAQ chips stay visible for the whole session (kept intentionally).
|
|
|
|
appendMessage('user', text);
|
|
history.push({ role: 'user', content: text });
|
|
|
|
chatInput.value = '';
|
|
chatInput.disabled = true;
|
|
document.getElementById('send-btn').disabled = true;
|
|
showTyping();
|
|
|
|
fetch('{{ url_for("support.chat_message") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': CSRF_TOKEN,
|
|
},
|
|
body: JSON.stringify({ message: text, messages: history, session_id: sessionId }),
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
hideTyping();
|
|
if (data.session_id) { sessionId = data.session_id; }
|
|
const reply = data.reply || 'Sorry, I could not process your request.';
|
|
appendMessage('ai', reply);
|
|
history.push({ role: 'assistant', content: reply });
|
|
|
|
// Suggest escalation if the AI hints it can't help
|
|
const lower = reply.toLowerCase();
|
|
if (lower.includes('submit to support') || lower.includes('admin team') ||
|
|
lower.includes("can't resolve") || lower.includes('contact us')) {
|
|
appendSystem('💡 Tip: Click "Submit to Support" above to send a message directly to our team.');
|
|
}
|
|
})
|
|
.catch(function () {
|
|
hideTyping();
|
|
appendMessage('ai', 'Network error — please check your connection and try again.');
|
|
})
|
|
.finally(function () {
|
|
chatInput.disabled = false;
|
|
document.getElementById('send-btn').disabled = false;
|
|
chatInput.focus();
|
|
});
|
|
}
|
|
|
|
// ── Public helpers called from inline onclick ──────────────────────────
|
|
window.sendUserMessage = function () {
|
|
sendMessage(chatInput.value);
|
|
};
|
|
|
|
window.sendFaq = function (btn) {
|
|
// Chips remain enabled/visible so they can be used throughout the session.
|
|
sendMessage(btn.dataset.faq);
|
|
};
|
|
|
|
// ── Enter key support ──────────────────────────────────────────────────
|
|
if (chatInput) {
|
|
chatInput.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage(chatInput.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Pre-fill modal subject from last user message ──────────────────────
|
|
document.getElementById('submitModal').addEventListener('show.bs.modal', function () {
|
|
const subjectInput = this.querySelector('[name="subject"]');
|
|
if (subjectInput && !subjectInput.value && history.length) {
|
|
const lastUser = [...history].reverse().find(function (m) { return m.role === 'user'; });
|
|
if (lastUser) subjectInput.value = lastUser.content.slice(0, 200);
|
|
}
|
|
});
|
|
}());
|
|
</script>
|
|
{% endblock %}
|