Jul 9 - Chat - save chat sessions
This commit is contained in:
@@ -180,6 +180,11 @@
|
||||
<i class="bi bi-chat-dots me-2"></i>Ask a Question
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('support.my_conversations') }}">
|
||||
<i class="bi bi-clock-history me-2"></i>My Conversations
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('support.my_tickets') }}">
|
||||
<i class="bi bi-inbox me-2"></i>My Requests
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{# Read-only chat transcript. Expects `messages` (list of SupportChatMessage). #}
|
||||
<style>
|
||||
.cbubble { max-width:80%; padding:.6rem .9rem; border-radius:1rem; font-size:.9rem;
|
||||
line-height:1.5; white-space:pre-wrap; word-break:break-word; }
|
||||
.cbubble-user { background:#0d6efd; color:#fff; border-bottom-right-radius:.25rem; }
|
||||
.cbubble-ai { background:#fff; border:1px solid #dee2e6; border-bottom-left-radius:.25rem; }
|
||||
</style>
|
||||
<div class="p-3" style="background:#f8f9fa;">
|
||||
{% for m in messages %}
|
||||
<div class="d-flex mb-2 {{ 'justify-content-end' if m.role == 'user' else 'justify-content-start' }}">
|
||||
<div class="cbubble {{ 'cbubble-user' if m.role == 'user' else 'cbubble-ai' }}">
|
||||
{{ m.content }}
|
||||
<div class="mt-1" style="font-size:.7rem;opacity:.6;">
|
||||
{{ 'You' if m.role == 'user' else 'JQC Assistant' }} · {{ m.created_at.strftime('%b %d, %I:%M %p') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted text-center small">This conversation has no messages.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Conversation #{{ session.id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0"><i class="bi bi-chat-square-text me-2 text-primary"></i>Conversation #{{ session.id }}</h4>
|
||||
<small class="text-muted">
|
||||
{{ session.customer.display_name if session.customer else 'Unknown customer' }}
|
||||
· started {{ session.created_at.strftime('%b %d, %Y %I:%M %p') }}
|
||||
</small>
|
||||
</div>
|
||||
<a href="{{ url_for('support.admin_conversations') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>All Conversations
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
{% include 'support/_transcript.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Support Conversations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-chat-left-dots me-2 text-primary"></i>Customer Chat Conversations</h4>
|
||||
<a href="{{ url_for('support.admin_tickets') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-life-preserver me-1"></i>Support Tickets
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
{% if sessions.items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>First message</th>
|
||||
<th class="text-center">Messages</th>
|
||||
<th>Last activity</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in sessions.items %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ s.customer.display_name if s.customer else '—' }}</td>
|
||||
<td class="text-muted small text-truncate" style="max-width:340px;">
|
||||
{{ s.preview[:120] }}{% if s.preview|length > 120 %}…{% endif %}
|
||||
</td>
|
||||
<td class="text-center">{{ s.message_count }}</td>
|
||||
<td class="small text-muted text-nowrap">{{ s.updated_at.strftime('%b %d, %Y %I:%M %p') }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('support.admin_conversation_detail', session_id=s.id) }}"
|
||||
class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-muted text-center">No chat conversations yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if sessions.pages > 1 %}
|
||||
<nav class="mt-3"><ul class="pagination justify-content-center">
|
||||
{% for p in range(1, sessions.pages + 1) %}
|
||||
<li class="page-item {{ 'active' if p == sessions.page }}">
|
||||
<a class="page-link" href="{{ url_for('support.admin_conversations', page=p) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul></nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -7,6 +7,9 @@
|
||||
<h4 class="mb-0"><i class="bi bi-inbox me-2 text-primary"></i>Customer Support Tickets</h4>
|
||||
<small class="text-muted">{{ tickets.total }} ticket{{ 's' if tickets.total != 1 }}</small>
|
||||
</div>
|
||||
<a href="{{ url_for('support.admin_conversations') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-chat-left-dots me-1"></i>Chat Conversations
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Status filter tabs #}
|
||||
|
||||
@@ -39,14 +39,24 @@
|
||||
<div class="col-lg-8">
|
||||
|
||||
{# ── Header ── #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<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>
|
||||
<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 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 ── #}
|
||||
@@ -153,17 +163,27 @@
|
||||
|
||||
const chatWindow = document.getElementById('chat-window');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const faqSection = document.getElementById('faq-section');
|
||||
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 }};
|
||||
|
||||
// ── Greeting on page load ──────────────────────────────────────────────
|
||||
// ── Restore prior conversation, or greet on a fresh session ────────────
|
||||
var userName = {{ current_user.display_name | tojson }};
|
||||
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.");
|
||||
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) {
|
||||
@@ -202,8 +222,7 @@
|
||||
function sendMessage(text) {
|
||||
if (!text.trim()) return;
|
||||
|
||||
// Hide FAQ chips after first interaction
|
||||
if (faqSection) faqSection.style.display = 'none';
|
||||
// FAQ chips stay visible for the whole session (kept intentionally).
|
||||
|
||||
appendMessage('user', text);
|
||||
history.push({ role: 'user', content: text });
|
||||
@@ -219,11 +238,12 @@
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': CSRF_TOKEN,
|
||||
},
|
||||
body: JSON.stringify({ message: text, messages: history }),
|
||||
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 });
|
||||
@@ -252,7 +272,7 @@
|
||||
};
|
||||
|
||||
window.sendFaq = function (btn) {
|
||||
btn.disabled = true;
|
||||
// Chips remain enabled/visible so they can be used throughout the session.
|
||||
sendMessage(btn.dataset.faq);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Conversation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0"><i class="bi bi-chat-square-text me-2 text-primary"></i>Conversation</h4>
|
||||
<small class="text-muted">{{ session.created_at.strftime('%b %d, %Y %I:%M %p') }}</small>
|
||||
</div>
|
||||
<a href="{{ url_for('support.my_conversations') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>All Conversations
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
{% include 'support/_transcript.html' %}
|
||||
</div>
|
||||
|
||||
<p class="text-muted small mt-2 text-center">
|
||||
Need more help? <a href="{{ url_for('support.chat') }}">Return to chat</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}My Conversations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0"><i class="bi bi-clock-history me-2 text-primary"></i>My Support Conversations</h4>
|
||||
<a href="{{ url_for('support.chat') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-chat-dots me-1"></i>Back to Chat
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
{% if sessions %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for s in sessions %}
|
||||
<a href="{{ url_for('support.conversation_detail', session_id=s.id) }}"
|
||||
class="list-group-item list-group-item-action">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="me-3 text-truncate">
|
||||
<div class="fw-semibold text-truncate">{{ s.preview[:100] }}{% if s.preview|length > 100 %}…{% endif %}</div>
|
||||
<small class="text-muted">{{ s.message_count }} message{{ 's' if s.message_count != 1 }}</small>
|
||||
</div>
|
||||
<small class="text-muted text-nowrap">{{ s.updated_at.strftime('%b %d, %Y %I:%M %p') }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-muted text-center">
|
||||
You have no saved conversations yet.
|
||||
<a href="{{ url_for('support.chat') }}">Start a chat</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user