Martechsol commited on
Commit Β·
11e3dbb
1
Parent(s): ec36498
Deploy RAG Chatbot to Hugging Face Spaces
Browse files- FUTURE_LIVECHAT_FEATURES.md +80 -0
- app/admin/router.py +33 -0
- app/admin/templates/admin.html +114 -6
- app/api/schemas.py +8 -0
- app/core/config.py +5 -0
- app/main.py +94 -6
- app/services/session_store.py +30 -0
- static/addon.html +122 -3
FUTURE_LIVECHAT_FEATURES.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Future Admin Panel Features (Inspired by LiveChat)
|
| 2 |
+
|
| 3 |
+
To further elevate the Admin Panel to the standard of enterprise customer service tools like **LiveChat.inc**, we can implement several advanced features. These additions would transition the panel from a passive monitoring dashboard into an active customer engagement hub.
|
| 4 |
+
|
| 5 |
+
Please review each proposed feature below. You can write your feedback directly in the **User Comments:** sections!
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. Human Handoff (Live Agent Takeover)
|
| 10 |
+
- **What it is:** The ability for an admin to pause the AI agent and manually type responses to the website visitor in real-time.
|
| 11 |
+
- **Why we need it:** Perfect for handling complex support queries, closing sales deals, or managing frustrated users when the AI reaches its limits.
|
| 12 |
+
- **UI Addition:** A text input box at the bottom of the active chat transcript and a "Take Over Chat" toggle button.
|
| 13 |
+
|
| 14 |
+
> **User Comments:**
|
| 15 |
+
>yes this is a good feature, i think there should be automatically this switching work , i mean if i want i can switch manually and it should switch automatically when needed, for eg: if a user asks connect me to the live agent, connect me to the human, connect me to the HR, etc it should notify me with a link (individual secured link that opens only chat of that specific user, not for all users, admin should be able to see only that specific user chat and details._), when i open that link, i could see name of the user, and previous chat that he has done with ai, then i can chat with that user, and at the end of the conversation, i can end the chat, or i can leave the chat, then ai will takeover the chat again, we have to work on it, it should be smooth and perfectly working.
|
| 16 |
+
when a user say to the bot that connect me to the live agent or human, or i have to talk to the HR, or supervisor, the bot should ask do you want me to call your supervisor in this chat? if user say no, then it should not transfer, and if user say yes, it should put the user in the queue untill and at the backend send the notification to the admin, when admin open that, he should see that user is in the queue, and he can take over the chat, when he end the chat, it should automatically go back to the ai mode. and if the admin doenst end the chat.
|
| 17 |
+
for notifications, i was thinking to integrate whatsapp notifications, like it should notify the admin/HR/supervisor om the whatsapp. these all customizations like adding whatsapp number of the supervisor, HR, or admin should be in admin portal.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 2. Real-Time Visitor Tracking
|
| 22 |
+
- **What it is:** A new "Visitors" tab showing who is currently browsing the website in real-time, even before they start a chat.
|
| 23 |
+
- **Why we need it:** Admins can see exactly which page a user is looking at, their geographic location, device, and how long they've been on the site.
|
| 24 |
+
- **UI Addition:** A live data grid showing active connections and the current URL path they are on.
|
| 25 |
+
|
| 26 |
+
> **User Comments:**
|
| 27 |
+
> Yes, keep tracking record of visitors live, is good. also there should be a tab that shows daily users, as we can fetch the names of users, and create sessions with their names, so record should be with their names.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## 3. Proactive Chat Triggers (Automated Greetings)
|
| 32 |
+
- **What it is:** Instead of waiting for the user to click the chat bubble, the chatbot automatically pops open with a contextual message based on their behavior.
|
| 33 |
+
- **Why we need it:** Increases engagement. For example, if a user lingers on a pricing page for 30 seconds, the bot can pop up and say, *"Hi! Do you have any questions about our pricing plans?"*
|
| 34 |
+
- **UI Addition:** A "Triggers" settings page in the admin panel to define rules (e.g., URL contains `/pricing`, Time on page > 30s -> Send Message X).
|
| 35 |
+
|
| 36 |
+
> **User Comments:**
|
| 37 |
+
>This is already implemented, please dont touch this, and we will work on it later.
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## 4. Chat Tagging and Categorization
|
| 42 |
+
- **What it is:** The ability to assign custom tags (e.g., `Sales`, `Support`, `Bug Report`, `Resolved`) to specific chat sessions.
|
| 43 |
+
- **Why we need it:** Makes it significantly easier to filter historical chats and analyze what topics users are asking about the most.
|
| 44 |
+
- **UI Addition:** A tagging interface in the right-sidebar of the active chat view and tag filters in the main session list.
|
| 45 |
+
|
| 46 |
+
> **User Comments:**
|
| 47 |
+
>Not now, we will work on this later.
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 5. Advanced Analytics & CSAT (Customer Satisfaction)
|
| 52 |
+
- **What it is:** Allowing visitors to rate the chat (π/π or 1-5 stars) at the end of the session.
|
| 53 |
+
- **Why we need it:** Provides measurable data on how well the AI (or human agent) is performing.
|
| 54 |
+
- **UI Addition:** A new chart on the dashboard tracking positive vs. negative ratings, and a "Rating" column in the session list.
|
| 55 |
+
|
| 56 |
+
> **User Comments:**
|
| 57 |
+
> yes, this is a good feature, when chat get ended, a small 5 starts in a row should appear, with heading rate this chat, these shars should be empty at the start, when user clicks on 1 star then it should fill 1 star, and if he clicks on 2 stars then it should fill 2 stars, and so on, and after that a thank you message should appear, and then the chat should end. saying thankyou and the rate this chat sections should be gone.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 6. Canned Responses (Quick Replies)
|
| 62 |
+
- **What it is:** Pre-saved text snippets that human admins can insert with a keyboard shortcut (e.g., typing `/pricing` instantly pastes the full pricing details).
|
| 63 |
+
- **Why we need it:** Drastically speeds up response times during a live human takeover.
|
| 64 |
+
- **UI Addition:** A "Canned Responses" management tab to add, edit, and delete shortcuts.
|
| 65 |
+
|
| 66 |
+
> **User Comments:**
|
| 67 |
+
> Dont touch this please, i have other plans to move forward in future.
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 7. IP Blocking & Visitor Banning
|
| 72 |
+
- **What it is:** A security feature to block abusive visitors or spam bots from interacting with the widget.
|
| 73 |
+
- **Why we need it:** Saves LLM token costs and keeps the analytics clean from spam.
|
| 74 |
+
- **UI Addition:** A "Block User" button inside the chat header and a "Banned IPs" management list.
|
| 75 |
+
|
| 76 |
+
> **User Comments:**
|
| 77 |
+
> Yes, each user is assigned a session Id, using this session id we can block the user, and then the user should not be able to chat with us, and yes if he open the chat again, it should show a message that you are blocked by the admin. there should be a tab on admin panel to show list of blocked and unblocked users, as admin can unblock them anytime with a switch button.
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
app/admin/router.py
CHANGED
|
@@ -312,6 +312,39 @@ async def remove_session(
|
|
| 312 |
return JSONResponse(content={"deleted": session_id})
|
| 313 |
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
# ββ Export Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 316 |
|
| 317 |
@admin_router.get("/api/admin/export/session/{session_id}", include_in_schema=False)
|
|
|
|
| 312 |
return JSONResponse(content={"deleted": session_id})
|
| 313 |
|
| 314 |
|
| 315 |
+
class ReplyRequest(BaseModel):
|
| 316 |
+
message: str
|
| 317 |
+
|
| 318 |
+
@admin_router.post("/api/admin/sessions/{session_id}/block", include_in_schema=False)
|
| 319 |
+
async def block_session(session_id: str, username: str = Depends(verify_admin)):
|
| 320 |
+
from app.services.session_store import update_session_field
|
| 321 |
+
await update_session_field(session_id, "blocked", True)
|
| 322 |
+
return {"status": "blocked"}
|
| 323 |
+
|
| 324 |
+
@admin_router.post("/api/admin/sessions/{session_id}/unblock", include_in_schema=False)
|
| 325 |
+
async def unblock_session(session_id: str, username: str = Depends(verify_admin)):
|
| 326 |
+
from app.services.session_store import update_session_field
|
| 327 |
+
await update_session_field(session_id, "blocked", False)
|
| 328 |
+
return {"status": "unblocked"}
|
| 329 |
+
|
| 330 |
+
@admin_router.post("/api/admin/sessions/{session_id}/takeover", include_in_schema=False)
|
| 331 |
+
async def takeover_session(session_id: str, username: str = Depends(verify_admin)):
|
| 332 |
+
from app.services.session_store import update_session_field
|
| 333 |
+
await update_session_field(session_id, "status", "HUMAN_TAKEOVER")
|
| 334 |
+
return {"status": "human_takeover"}
|
| 335 |
+
|
| 336 |
+
@admin_router.post("/api/admin/sessions/{session_id}/end_takeover", include_in_schema=False)
|
| 337 |
+
async def end_takeover_session(session_id: str, username: str = Depends(verify_admin)):
|
| 338 |
+
from app.services.session_store import update_session_field
|
| 339 |
+
await update_session_field(session_id, "status", "AI")
|
| 340 |
+
return {"status": "ai"}
|
| 341 |
+
|
| 342 |
+
@admin_router.post("/api/admin/sessions/{session_id}/reply", include_in_schema=False)
|
| 343 |
+
async def reply_session(session_id: str, req: ReplyRequest, username: str = Depends(verify_admin)):
|
| 344 |
+
from app.services.session_store import save_message
|
| 345 |
+
asyncio.create_task(save_message(session_id, "", req.message))
|
| 346 |
+
return {"status": "sent"}
|
| 347 |
+
|
| 348 |
# ββ Export Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 349 |
|
| 350 |
@admin_router.get("/api/admin/export/session/{session_id}", include_in_schema=False)
|
app/admin/templates/admin.html
CHANGED
|
@@ -377,6 +377,14 @@
|
|
| 377 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
| 378 |
Live Sessions
|
| 379 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
<div class="nav-section-title">Quick Links</div>
|
| 382 |
<a href="/widget" target="_blank" class="quick-link">
|
|
@@ -451,6 +459,8 @@
|
|
| 451 |
<p id="chat-meta">Session ID: -</p>
|
| 452 |
</div>
|
| 453 |
<div class="chat-header-actions">
|
|
|
|
|
|
|
| 454 |
<button class="btn-secondary" id="btn-export-single" style="padding: 6px 12px; font-size:12px; border-color:var(--border)">Export</button>
|
| 455 |
<button class="btn-secondary" id="btn-delete-session" style="padding: 6px 12px; font-size:12px; color:var(--red); border-color:rgba(239,68,68,0.2)">Delete</button>
|
| 456 |
</div>
|
|
@@ -458,6 +468,10 @@
|
|
| 458 |
<div class="chat-messages" id="messages-area">
|
| 459 |
<!-- Populated by JS -->
|
| 460 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</div>
|
| 462 |
</div>
|
| 463 |
</div>
|
|
@@ -504,6 +518,9 @@
|
|
| 504 |
let _creds = null;
|
| 505 |
let _allSessions = [];
|
| 506 |
let _activeId = null;
|
|
|
|
|
|
|
|
|
|
| 507 |
let _refreshInterval = null;
|
| 508 |
let _chartInstance = null;
|
| 509 |
const STORAGE_KEY = 'martech_admin_creds';
|
|
@@ -629,13 +646,24 @@
|
|
| 629 |
v.style.display = 'none';
|
| 630 |
v.classList.remove('active');
|
| 631 |
});
|
| 632 |
-
|
| 633 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
setTimeout(() => v.classList.add('active'), 10);
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
if(view === 'dashboard') loadAnalytics();
|
| 638 |
-
if(
|
|
|
|
|
|
|
|
|
|
| 639 |
}
|
| 640 |
|
| 641 |
// ββ Dashboard Analytics ββ
|
|
@@ -709,7 +737,14 @@
|
|
| 709 |
|
| 710 |
function renderSessionList(sessions) {
|
| 711 |
const q = $('search-inp').value.toLowerCase();
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
|
| 714 |
if(!filtered.length) {
|
| 715 |
$('session-list').innerHTML = `<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px">No sessions found</div>`;
|
|
@@ -769,13 +804,43 @@
|
|
| 769 |
<div class="msg-bubble">${esc(m.question)}</div>
|
| 770 |
</div>
|
| 771 |
<div class="msg-wrapper bot">
|
| 772 |
-
<div class="msg-meta">AI Agent β’ ${fmtDate(m.timestamp).split(' ')[1] || ''}</div>
|
| 773 |
<div class="msg-bubble">${esc(m.answer)}</div>
|
| 774 |
</div>
|
| 775 |
`;
|
| 776 |
});
|
| 777 |
$('messages-area').innerHTML = html;
|
| 778 |
$('messages-area').scrollTop = $('messages-area').scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
} catch(e) {
|
| 780 |
$('messages-area').innerHTML = `<div style="color:var(--red);text-align:center;padding:20px">Failed to load chat</div>`;
|
| 781 |
}
|
|
@@ -797,6 +862,49 @@
|
|
| 797 |
} catch(e) { showToast('Delete failed', 'error'); }
|
| 798 |
};
|
| 799 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
// Auto Refresh
|
| 801 |
function setupAutoRefresh() {
|
| 802 |
clearInterval(_refreshInterval);
|
|
|
|
| 377 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
| 378 |
Live Sessions
|
| 379 |
</div>
|
| 380 |
+
<div class="nav-item" data-view="daily" id="nav-daily">
|
| 381 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
| 382 |
+
Daily Users
|
| 383 |
+
</div>
|
| 384 |
+
<div class="nav-item" data-view="blocked" id="nav-blocked">
|
| 385 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
|
| 386 |
+
Blocked Users
|
| 387 |
+
</div>
|
| 388 |
|
| 389 |
<div class="nav-section-title">Quick Links</div>
|
| 390 |
<a href="/widget" target="_blank" class="quick-link">
|
|
|
|
| 459 |
<p id="chat-meta">Session ID: -</p>
|
| 460 |
</div>
|
| 461 |
<div class="chat-header-actions">
|
| 462 |
+
<button class="btn-secondary" id="btn-block-user" style="padding: 6px 12px; font-size:12px; border-color:var(--border)">Block</button>
|
| 463 |
+
<button class="btn-secondary" id="btn-takeover" style="padding: 6px 12px; font-size:12px; border-color:var(--accent); color:var(--accent)">Take Over</button>
|
| 464 |
<button class="btn-secondary" id="btn-export-single" style="padding: 6px 12px; font-size:12px; border-color:var(--border)">Export</button>
|
| 465 |
<button class="btn-secondary" id="btn-delete-session" style="padding: 6px 12px; font-size:12px; color:var(--red); border-color:rgba(239,68,68,0.2)">Delete</button>
|
| 466 |
</div>
|
|
|
|
| 468 |
<div class="chat-messages" id="messages-area">
|
| 469 |
<!-- Populated by JS -->
|
| 470 |
</div>
|
| 471 |
+
<div class="chat-reply-area hidden" id="chat-reply-area" style="padding: 20px; border-top: 1px solid var(--border); background: var(--surface); display: flex; gap: 12px;">
|
| 472 |
+
<input type="text" id="inp-admin-reply" placeholder="Type a message to the user..." style="flex: 1; padding: 12px 16px; border: 1px solid var(--border); border-radius: 12px; font-size: 14px; outline: none;" />
|
| 473 |
+
<button class="btn-primary" id="btn-send-reply" style="width: auto; padding: 12px 24px;">Send</button>
|
| 474 |
+
</div>
|
| 475 |
</div>
|
| 476 |
</div>
|
| 477 |
</div>
|
|
|
|
| 518 |
let _creds = null;
|
| 519 |
let _allSessions = [];
|
| 520 |
let _activeId = null;
|
| 521 |
+
let _activeStatus = 'AI';
|
| 522 |
+
let _activeBlocked = false;
|
| 523 |
+
let _currentFilter = 'sessions';
|
| 524 |
let _refreshInterval = null;
|
| 525 |
let _chartInstance = null;
|
| 526 |
const STORAGE_KEY = 'martech_admin_creds';
|
|
|
|
| 646 |
v.style.display = 'none';
|
| 647 |
v.classList.remove('active');
|
| 648 |
});
|
| 649 |
+
|
| 650 |
+
let targetView = view;
|
| 651 |
+
if (view === 'daily' || view === 'blocked') targetView = 'sessions';
|
| 652 |
+
|
| 653 |
+
const v = $('view-' + targetView);
|
| 654 |
+
v.style.display = targetView === 'sessions' ? 'flex' : 'block';
|
| 655 |
setTimeout(() => v.classList.add('active'), 10);
|
| 656 |
+
|
| 657 |
+
if (view === 'dashboard') $('topbar-title').textContent = 'Dashboard Overview';
|
| 658 |
+
else if (view === 'sessions') $('topbar-title').textContent = 'All Live Chat Sessions';
|
| 659 |
+
else if (view === 'daily') $('topbar-title').textContent = 'Today\'s Users';
|
| 660 |
+
else if (view === 'blocked') $('topbar-title').textContent = 'Blocked Users';
|
| 661 |
|
| 662 |
if(view === 'dashboard') loadAnalytics();
|
| 663 |
+
if(['sessions', 'daily', 'blocked'].includes(view)) {
|
| 664 |
+
_currentFilter = view;
|
| 665 |
+
loadSessions();
|
| 666 |
+
}
|
| 667 |
}
|
| 668 |
|
| 669 |
// ββ Dashboard Analytics ββ
|
|
|
|
| 737 |
|
| 738 |
function renderSessionList(sessions) {
|
| 739 |
const q = $('search-inp').value.toLowerCase();
|
| 740 |
+
let filtered = sessions.filter(s => s.session_id.toLowerCase().includes(q) || (s.user_name||'').toLowerCase().includes(q));
|
| 741 |
+
|
| 742 |
+
if (_currentFilter === 'blocked') {
|
| 743 |
+
filtered = filtered.filter(s => s.blocked === true);
|
| 744 |
+
} else if (_currentFilter === 'daily') {
|
| 745 |
+
const today = new Date().toISOString().split('T')[0];
|
| 746 |
+
filtered = filtered.filter(s => (s.created_at || '').startsWith(today));
|
| 747 |
+
}
|
| 748 |
|
| 749 |
if(!filtered.length) {
|
| 750 |
$('session-list').innerHTML = `<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px">No sessions found</div>`;
|
|
|
|
| 804 |
<div class="msg-bubble">${esc(m.question)}</div>
|
| 805 |
</div>
|
| 806 |
<div class="msg-wrapper bot">
|
| 807 |
+
<div class="msg-meta">${m.answer === 'Admin message' ? 'Human Agent' : 'AI Agent'} β’ ${fmtDate(m.timestamp).split(' ')[1] || ''}</div>
|
| 808 |
<div class="msg-bubble">${esc(m.answer)}</div>
|
| 809 |
</div>
|
| 810 |
`;
|
| 811 |
});
|
| 812 |
$('messages-area').innerHTML = html;
|
| 813 |
$('messages-area').scrollTop = $('messages-area').scrollHeight;
|
| 814 |
+
|
| 815 |
+
_activeStatus = data.status || 'AI';
|
| 816 |
+
_activeBlocked = data.blocked || false;
|
| 817 |
+
|
| 818 |
+
// Update Takeover Button
|
| 819 |
+
const btnTakeover = $('btn-takeover');
|
| 820 |
+
if (_activeStatus === 'HUMAN_TAKEOVER') {
|
| 821 |
+
btnTakeover.textContent = 'End Chat (AI)';
|
| 822 |
+
btnTakeover.style.color = '#ef4444';
|
| 823 |
+
btnTakeover.style.borderColor = '#ef4444';
|
| 824 |
+
$('chat-reply-area').classList.remove('hidden');
|
| 825 |
+
} else {
|
| 826 |
+
btnTakeover.textContent = 'Take Over';
|
| 827 |
+
btnTakeover.style.color = 'var(--accent)';
|
| 828 |
+
btnTakeover.style.borderColor = 'var(--accent)';
|
| 829 |
+
$('chat-reply-area').classList.add('hidden');
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
// Update Block Button
|
| 833 |
+
const btnBlock = $('btn-block-user');
|
| 834 |
+
if (_activeBlocked) {
|
| 835 |
+
btnBlock.textContent = 'Unblock User';
|
| 836 |
+
btnBlock.style.color = '#10b981';
|
| 837 |
+
btnBlock.style.borderColor = '#10b981';
|
| 838 |
+
} else {
|
| 839 |
+
btnBlock.textContent = 'Block User';
|
| 840 |
+
btnBlock.style.color = 'var(--text2)';
|
| 841 |
+
btnBlock.style.borderColor = 'var(--border)';
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
} catch(e) {
|
| 845 |
$('messages-area').innerHTML = `<div style="color:var(--red);text-align:center;padding:20px">Failed to load chat</div>`;
|
| 846 |
}
|
|
|
|
| 862 |
} catch(e) { showToast('Delete failed', 'error'); }
|
| 863 |
};
|
| 864 |
|
| 865 |
+
$('btn-takeover').onclick = async () => {
|
| 866 |
+
if(!_activeId) return;
|
| 867 |
+
try {
|
| 868 |
+
const isTakeover = _activeStatus !== 'HUMAN_TAKEOVER';
|
| 869 |
+
const endpoint = isTakeover ? 'takeover' : 'end_takeover';
|
| 870 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId) + '/' + endpoint, { method: 'POST', headers: {Authorization: authHeader()} });
|
| 871 |
+
if(r.ok) {
|
| 872 |
+
showToast(isTakeover ? 'Human takeover active' : 'Chat handed back to AI');
|
| 873 |
+
openSession(_activeId);
|
| 874 |
+
}
|
| 875 |
+
} catch(e) { showToast('Action failed', 'error'); }
|
| 876 |
+
};
|
| 877 |
+
|
| 878 |
+
$('btn-block-user').onclick = async () => {
|
| 879 |
+
if(!_activeId) return;
|
| 880 |
+
try {
|
| 881 |
+
const endpoint = _activeBlocked ? 'unblock' : 'block';
|
| 882 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId) + '/' + endpoint, { method: 'POST', headers: {Authorization: authHeader()} });
|
| 883 |
+
if(r.ok) {
|
| 884 |
+
showToast(_activeBlocked ? 'User unblocked' : 'User blocked');
|
| 885 |
+
openSession(_activeId);
|
| 886 |
+
}
|
| 887 |
+
} catch(e) { showToast('Action failed', 'error'); }
|
| 888 |
+
};
|
| 889 |
+
|
| 890 |
+
$('btn-send-reply').onclick = async () => {
|
| 891 |
+
if(!_activeId) return;
|
| 892 |
+
const msg = $('inp-admin-reply').value.trim();
|
| 893 |
+
if(!msg) return;
|
| 894 |
+
$('inp-admin-reply').value = '';
|
| 895 |
+
try {
|
| 896 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId) + '/reply', {
|
| 897 |
+
method: 'POST',
|
| 898 |
+
headers: {'Content-Type': 'application/json', Authorization: authHeader()},
|
| 899 |
+
body: JSON.stringify({message: msg})
|
| 900 |
+
});
|
| 901 |
+
if(r.ok) {
|
| 902 |
+
openSession(_activeId);
|
| 903 |
+
}
|
| 904 |
+
} catch(e) { showToast('Send failed', 'error'); }
|
| 905 |
+
};
|
| 906 |
+
$('inp-admin-reply').onkeydown = e => { if(e.key==='Enter') $('btn-send-reply').click(); };
|
| 907 |
+
|
| 908 |
// Auto Refresh
|
| 909 |
function setupAutoRefresh() {
|
| 910 |
clearInterval(_refreshInterval);
|
app/api/schemas.py
CHANGED
|
@@ -30,3 +30,11 @@ class SessionUpdateNameRequest(BaseModel):
|
|
| 30 |
class SessionInitRequest(BaseModel):
|
| 31 |
session_id: str
|
| 32 |
user_name: str | None = Field(default=None, description="User name to associate with the session at creation time")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
class SessionInitRequest(BaseModel):
|
| 31 |
session_id: str
|
| 32 |
user_name: str | None = Field(default=None, description="User name to associate with the session at creation time")
|
| 33 |
+
|
| 34 |
+
class RateRequest(BaseModel):
|
| 35 |
+
session_id: str
|
| 36 |
+
rating: int = Field(..., ge=1, le=5)
|
| 37 |
+
|
| 38 |
+
class PollRequest(BaseModel):
|
| 39 |
+
session_id: str
|
| 40 |
+
last_id: int = Field(default=0)
|
app/core/config.py
CHANGED
|
@@ -53,6 +53,11 @@ class Settings(BaseSettings):
|
|
| 53 |
admin_user: str = Field(default="martech_admin", alias="ADMIN_USER")
|
| 54 |
admin_pass: str = Field(default="martech_admin_303", alias="ADMIN_PASS")
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
@lru_cache
|
| 58 |
def get_settings() -> Settings:
|
|
|
|
| 53 |
admin_user: str = Field(default="martech_admin", alias="ADMIN_USER")
|
| 54 |
admin_pass: str = Field(default="martech_admin_303", alias="ADMIN_PASS")
|
| 55 |
|
| 56 |
+
# WhatsApp API Settings (Meta Cloud API)
|
| 57 |
+
meta_whatsapp_phone_id: str = Field(default="", alias="META_WHATSAPP_PHONE_ID")
|
| 58 |
+
meta_whatsapp_token: str = Field(default="", alias="META_WHATSAPP_TOKEN")
|
| 59 |
+
meta_whatsapp_admin_number: str = Field(default="", alias="META_WHATSAPP_ADMIN_NUMBER")
|
| 60 |
+
|
| 61 |
|
| 62 |
@lru_cache
|
| 63 |
def get_settings() -> Settings:
|
app/main.py
CHANGED
|
@@ -6,7 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 6 |
from fastapi.staticfiles import StaticFiles
|
| 7 |
import os
|
| 8 |
|
| 9 |
-
from app.api.schemas import ChatRequest, ChatResponse, HealthResponse, SessionUpdateNameRequest, SessionInitRequest
|
| 10 |
from app.core.config import get_settings
|
| 11 |
from app.services.embeddings import EmbeddingService
|
| 12 |
from app.services.llm import LLMService
|
|
@@ -137,6 +137,28 @@ def health() -> HealthResponse:
|
|
| 137 |
h = vector_store.health()
|
| 138 |
return HealthResponse(status="ok", **h)
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
@fastapi_app.post("/api/chat", response_model=ChatResponse)
|
| 142 |
async def chat(
|
|
@@ -155,11 +177,46 @@ async def chat(
|
|
| 155 |
|
| 156 |
logger.info("Incoming chat request: msg='%s', sid='%s'", payload.message[:30], payload.session_id)
|
| 157 |
|
| 158 |
-
# Fetch user_name
|
| 159 |
user_name = None
|
| 160 |
if payload.session_id:
|
| 161 |
sess_data = await _ss.get_session(payload.session_id)
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
try:
|
| 165 |
result = await pipeline.chat(payload.message, payload.history, user_name=user_name)
|
|
@@ -226,9 +283,40 @@ async def get_session_history(session_id: str):
|
|
| 226 |
|
| 227 |
history = []
|
| 228 |
for msg in data.get("messages", []):
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
|
| 234 |
# ββ Mount Gradio at /ui β MUST be last, wraps the FastAPI app βββββββββββββββββ
|
|
|
|
| 6 |
from fastapi.staticfiles import StaticFiles
|
| 7 |
import os
|
| 8 |
|
| 9 |
+
from app.api.schemas import ChatRequest, ChatResponse, HealthResponse, SessionUpdateNameRequest, SessionInitRequest, RateRequest, PollRequest
|
| 10 |
from app.core.config import get_settings
|
| 11 |
from app.services.embeddings import EmbeddingService
|
| 12 |
from app.services.llm import LLMService
|
|
|
|
| 137 |
h = vector_store.health()
|
| 138 |
return HealthResponse(status="ok", **h)
|
| 139 |
|
| 140 |
+
async def send_whatsapp_message(text: str):
|
| 141 |
+
if not settings.meta_whatsapp_phone_id or not settings.meta_whatsapp_token or not settings.meta_whatsapp_admin_number:
|
| 142 |
+
logger.warning("WhatsApp settings missing, skipping notification.")
|
| 143 |
+
return
|
| 144 |
+
import httpx
|
| 145 |
+
url = f"https://graph.facebook.com/v17.0/{settings.meta_whatsapp_phone_id}/messages"
|
| 146 |
+
headers = {
|
| 147 |
+
"Authorization": f"Bearer {settings.meta_whatsapp_token}",
|
| 148 |
+
"Content-Type": "application/json"
|
| 149 |
+
}
|
| 150 |
+
payload = {
|
| 151 |
+
"messaging_product": "whatsapp",
|
| 152 |
+
"to": settings.meta_whatsapp_admin_number,
|
| 153 |
+
"type": "text",
|
| 154 |
+
"text": {"body": text}
|
| 155 |
+
}
|
| 156 |
+
try:
|
| 157 |
+
async with httpx.AsyncClient() as client:
|
| 158 |
+
await client.post(url, headers=headers, json=payload, timeout=10.0)
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logger.error(f"Failed to send WhatsApp message: {e}")
|
| 161 |
+
|
| 162 |
|
| 163 |
@fastapi_app.post("/api/chat", response_model=ChatResponse)
|
| 164 |
async def chat(
|
|
|
|
| 177 |
|
| 178 |
logger.info("Incoming chat request: msg='%s', sid='%s'", payload.message[:30], payload.session_id)
|
| 179 |
|
| 180 |
+
# Fetch user_name and handle session status
|
| 181 |
user_name = None
|
| 182 |
if payload.session_id:
|
| 183 |
sess_data = await _ss.get_session(payload.session_id)
|
| 184 |
+
if sess_data:
|
| 185 |
+
user_name = sess_data.get("user_name")
|
| 186 |
+
if sess_data.get("blocked"):
|
| 187 |
+
return ChatResponse(reply="You are blocked by the admin.", retrieved_chunks=[])
|
| 188 |
+
|
| 189 |
+
status = sess_data.get("status", "AI")
|
| 190 |
+
msg_lower = payload.message.lower()
|
| 191 |
+
|
| 192 |
+
if status == "HUMAN_TAKEOVER":
|
| 193 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, ""))
|
| 194 |
+
return ChatResponse(reply="", retrieved_chunks=[])
|
| 195 |
+
|
| 196 |
+
if status == "QUEUED":
|
| 197 |
+
reply_msg = "Please wait, an agent will be with you shortly."
|
| 198 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, reply_msg))
|
| 199 |
+
return ChatResponse(reply=reply_msg, retrieved_chunks=[])
|
| 200 |
+
|
| 201 |
+
if status == "CONFIRMING_TRANSFER":
|
| 202 |
+
if msg_lower in ["yes", "y", "yeah", "yep", "sure", "ok", "okay"]:
|
| 203 |
+
await _ss.update_session_field(payload.session_id, "status", "QUEUED")
|
| 204 |
+
reply_msg = "You are now in the queue. An agent will join shortly."
|
| 205 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, reply_msg))
|
| 206 |
+
asyncio.create_task(send_whatsapp_message(f"π¨ New Human Handoff Request!\nUser: {user_name}\nSession ID: {payload.session_id}\nPlease check the Admin Panel."))
|
| 207 |
+
return ChatResponse(reply=reply_msg, retrieved_chunks=[])
|
| 208 |
+
else:
|
| 209 |
+
await _ss.update_session_field(payload.session_id, "status", "AI")
|
| 210 |
+
reply_msg = "Transfer cancelled. How can I help you?"
|
| 211 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, reply_msg))
|
| 212 |
+
return ChatResponse(reply=reply_msg, retrieved_chunks=[])
|
| 213 |
+
|
| 214 |
+
# AI Mode: Check for human takeover keywords
|
| 215 |
+
if any(kw in msg_lower for kw in ["human", "agent", "hr", "supervisor", "representative", "real person"]):
|
| 216 |
+
await _ss.update_session_field(payload.session_id, "status", "CONFIRMING_TRANSFER")
|
| 217 |
+
reply_msg = "Do you want me to call your supervisor/agent in this chat? (yes/no)"
|
| 218 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, reply_msg))
|
| 219 |
+
return ChatResponse(reply=reply_msg, retrieved_chunks=[])
|
| 220 |
|
| 221 |
try:
|
| 222 |
result = await pipeline.chat(payload.message, payload.history, user_name=user_name)
|
|
|
|
| 283 |
|
| 284 |
history = []
|
| 285 |
for msg in data.get("messages", []):
|
| 286 |
+
if msg.get("question"):
|
| 287 |
+
history.append({"role": "user", "content": msg["question"]})
|
| 288 |
+
if msg.get("answer"):
|
| 289 |
+
history.append({"role": "assistant", "content": msg["answer"]})
|
| 290 |
+
return {"history": history, "user_name": data.get("user_name", ""), "status": data.get("status", "AI")}
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@fastapi_app.post("/api/chat/poll")
|
| 294 |
+
async def chat_poll(payload: PollRequest):
|
| 295 |
+
"""Frontend polls this when in HUMAN_TAKEOVER or QUEUED mode to get new admin messages."""
|
| 296 |
+
sess = await _ss.get_session(payload.session_id)
|
| 297 |
+
if not sess:
|
| 298 |
+
return {"status": "AI", "new_messages": [], "last_id": payload.last_id}
|
| 299 |
+
|
| 300 |
+
status = sess.get("status", "AI")
|
| 301 |
+
messages = sess.get("messages", [])
|
| 302 |
+
|
| 303 |
+
new_msgs = []
|
| 304 |
+
for m in messages:
|
| 305 |
+
if m["id"] > payload.last_id:
|
| 306 |
+
# Only return admin answers or queued system responses as assistant
|
| 307 |
+
if m.get("answer"):
|
| 308 |
+
new_msgs.append({"id": m["id"], "role": "assistant", "content": m["answer"]})
|
| 309 |
+
|
| 310 |
+
max_id = max([m["id"] for m in messages]) if messages else payload.last_id
|
| 311 |
+
return {"status": status, "new_messages": new_msgs, "last_id": max_id}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@fastapi_app.post("/api/chat/rate")
|
| 315 |
+
async def chat_rate(payload: RateRequest):
|
| 316 |
+
"""Saves a 1-5 star rating for the session."""
|
| 317 |
+
await _ss.update_session_field(payload.session_id, "rating", payload.rating)
|
| 318 |
+
return {"status": "ok"}
|
| 319 |
+
|
| 320 |
|
| 321 |
|
| 322 |
# ββ Mount Gradio at /ui β MUST be last, wraps the FastAPI app βββββββββββββββββ
|
app/services/session_store.py
CHANGED
|
@@ -83,6 +83,9 @@ async def ensure_session(session_id: str) -> None:
|
|
| 83 |
"last_active": _now_iso(),
|
| 84 |
"message_count": 0,
|
| 85 |
"messages": [],
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
try:
|
| 88 |
await asyncio.to_thread(_write_json, path, skeleton)
|
|
@@ -113,6 +116,9 @@ async def save_message(session_id: str, question: str, answer: str) -> None:
|
|
| 113 |
"last_active": _now_iso(),
|
| 114 |
"message_count": 0,
|
| 115 |
"messages": [],
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
timestamp = _now_iso()
|
|
@@ -152,6 +158,9 @@ async def get_all_sessions() -> list:
|
|
| 152 |
"created_at": data.get("created_at", ""),
|
| 153 |
"last_active": data.get("last_active", ""),
|
| 154 |
"message_count": data.get("message_count", 0),
|
|
|
|
|
|
|
|
|
|
| 155 |
})
|
| 156 |
except Exception as exc:
|
| 157 |
logger.warning("session_store: could not read %s: %s", fpath, exc)
|
|
@@ -324,6 +333,9 @@ async def rename_session(old_id: str, new_name: str) -> str:
|
|
| 324 |
"last_active": _now_iso(),
|
| 325 |
"message_count": 0,
|
| 326 |
"messages": [],
|
|
|
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
await asyncio.to_thread(_write_json, new_path, skeleton)
|
| 329 |
return new_id
|
|
@@ -349,3 +361,21 @@ def _read_json(path: Path) -> dict:
|
|
| 349 |
|
| 350 |
def _list_json_files() -> list:
|
| 351 |
return list(_get_sessions_dir().glob("*.json"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
"last_active": _now_iso(),
|
| 84 |
"message_count": 0,
|
| 85 |
"messages": [],
|
| 86 |
+
"status": "AI",
|
| 87 |
+
"blocked": False,
|
| 88 |
+
"rating": None,
|
| 89 |
}
|
| 90 |
try:
|
| 91 |
await asyncio.to_thread(_write_json, path, skeleton)
|
|
|
|
| 116 |
"last_active": _now_iso(),
|
| 117 |
"message_count": 0,
|
| 118 |
"messages": [],
|
| 119 |
+
"status": "AI",
|
| 120 |
+
"blocked": False,
|
| 121 |
+
"rating": None,
|
| 122 |
}
|
| 123 |
|
| 124 |
timestamp = _now_iso()
|
|
|
|
| 158 |
"created_at": data.get("created_at", ""),
|
| 159 |
"last_active": data.get("last_active", ""),
|
| 160 |
"message_count": data.get("message_count", 0),
|
| 161 |
+
"status": data.get("status", "AI"),
|
| 162 |
+
"blocked": data.get("blocked", False),
|
| 163 |
+
"rating": data.get("rating", None),
|
| 164 |
})
|
| 165 |
except Exception as exc:
|
| 166 |
logger.warning("session_store: could not read %s: %s", fpath, exc)
|
|
|
|
| 333 |
"last_active": _now_iso(),
|
| 334 |
"message_count": 0,
|
| 335 |
"messages": [],
|
| 336 |
+
"status": "AI",
|
| 337 |
+
"blocked": False,
|
| 338 |
+
"rating": None,
|
| 339 |
}
|
| 340 |
await asyncio.to_thread(_write_json, new_path, skeleton)
|
| 341 |
return new_id
|
|
|
|
| 361 |
|
| 362 |
def _list_json_files() -> list:
|
| 363 |
return list(_get_sessions_dir().glob("*.json"))
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
async def update_session_field(session_id: str, field: str, value) -> bool:
|
| 367 |
+
"""Generic helper to update a single root field in the session JSON."""
|
| 368 |
+
path = _get_session_path(session_id)
|
| 369 |
+
lock = await _get_lock(session_id)
|
| 370 |
+
async with lock:
|
| 371 |
+
try:
|
| 372 |
+
if not path.exists():
|
| 373 |
+
return False
|
| 374 |
+
data = await asyncio.to_thread(_read_json, path)
|
| 375 |
+
data[field] = value
|
| 376 |
+
await asyncio.to_thread(_write_json, path, data)
|
| 377 |
+
logger.info("session_store: updated %s for session %s", field, session_id)
|
| 378 |
+
return True
|
| 379 |
+
except Exception as exc:
|
| 380 |
+
logger.error("session_store: failed to update %s for session %s: %s", field, session_id, exc)
|
| 381 |
+
return False
|
static/addon.html
CHANGED
|
@@ -87,6 +87,7 @@
|
|
| 87 |
background: #10b981;
|
| 88 |
border-radius: 50%;
|
| 89 |
animation: pulse-dot 2s infinite;
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
#martech-chat-header span {
|
|
@@ -95,6 +96,20 @@
|
|
| 95 |
font-size: 15px;
|
| 96 |
}
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
/* Custom Chat Area Styling */
|
| 99 |
#chat-messages {
|
| 100 |
flex: 1;
|
|
@@ -259,8 +274,11 @@
|
|
| 259 |
|
| 260 |
<div id="martech-chat-box">
|
| 261 |
<div id="martech-chat-header">
|
| 262 |
-
<div class="dot"></div>
|
| 263 |
-
<span>Martechsol Assistant</span>
|
|
|
|
|
|
|
|
|
|
| 264 |
</div>
|
| 265 |
|
| 266 |
<!-- Custom Chat UI replacing iframe -->
|
|
@@ -278,6 +296,15 @@
|
|
| 278 |
</svg>
|
| 279 |
</button>
|
| 280 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
</div>
|
| 282 |
</div>
|
| 283 |
|
|
@@ -314,6 +341,8 @@
|
|
| 314 |
const API_URL = "https://joedown11-chatbot21.hf.space/api/chat";
|
| 315 |
const BASE_URL = API_URL.replace("/api/chat", "");
|
| 316 |
let chatHistory = [];
|
|
|
|
|
|
|
| 317 |
|
| 318 |
// ββ Session ID: generate once, persist in localStorage ββ
|
| 319 |
let sessionId = localStorage.getItem('martech_session_id');
|
|
@@ -444,11 +473,19 @@
|
|
| 444 |
console.warn('Martech: background session pre-creation failed', e);
|
| 445 |
}
|
| 446 |
|
| 447 |
-
// Now fetch history using the finalized session ID
|
| 448 |
try {
|
| 449 |
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
|
| 450 |
if (hResp.ok) {
|
| 451 |
const hData = await hResp.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
if (hData.history && hData.history.length > 0) {
|
| 453 |
messagesContainer.innerHTML = '';
|
| 454 |
chatHistory = hData.history;
|
|
@@ -497,6 +534,39 @@
|
|
| 497 |
// Fallback: Check every 15 seconds (virtually zero cost once session is named)
|
| 498 |
setInterval(_attemptSync, 15000);
|
| 499 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
}
|
| 501 |
|
| 502 |
initUserSession();
|
|
@@ -616,6 +686,55 @@
|
|
| 616 |
inputField.addEventListener('keypress', function(e) {
|
| 617 |
if (e.key === 'Enter') handleSend();
|
| 618 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
})();
|
| 621 |
</script>
|
|
|
|
| 87 |
background: #10b981;
|
| 88 |
border-radius: 50%;
|
| 89 |
animation: pulse-dot 2s infinite;
|
| 90 |
+
transition: background 0.3s;
|
| 91 |
}
|
| 92 |
|
| 93 |
#martech-chat-header span {
|
|
|
|
| 96 |
font-size: 15px;
|
| 97 |
}
|
| 98 |
|
| 99 |
+
.header-actions { margin-left: auto; display: flex; gap: 8px; }
|
| 100 |
+
.btn-end-chat { background: #fee2e2; color: #ef4444; border: none; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
|
| 101 |
+
.btn-end-chat:hover { background: #fecaca; }
|
| 102 |
+
|
| 103 |
+
#rating-overlay {
|
| 104 |
+
position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95);
|
| 105 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 106 |
+
z-index: 100; opacity: 0; visibility: hidden; transition: all 0.3s;
|
| 107 |
+
}
|
| 108 |
+
#rating-overlay.show { opacity: 1; visibility: visible; }
|
| 109 |
+
.stars { display: flex; gap: 8px; font-size: 32px; color: #cbd5e1; cursor: pointer; margin: 16px 0; }
|
| 110 |
+
.stars span { transition: color 0.2s; }
|
| 111 |
+
.stars span:hover, .stars span.active { color: #f59e0b; }
|
| 112 |
+
|
| 113 |
/* Custom Chat Area Styling */
|
| 114 |
#chat-messages {
|
| 115 |
flex: 1;
|
|
|
|
| 274 |
|
| 275 |
<div id="martech-chat-box">
|
| 276 |
<div id="martech-chat-header">
|
| 277 |
+
<div class="dot" id="chat-status-dot"></div>
|
| 278 |
+
<span id="chat-header-title">Martechsol Assistant</span>
|
| 279 |
+
<div class="header-actions">
|
| 280 |
+
<button class="btn-end-chat" id="btn-end-chat">End Chat</button>
|
| 281 |
+
</div>
|
| 282 |
</div>
|
| 283 |
|
| 284 |
<!-- Custom Chat UI replacing iframe -->
|
|
|
|
| 296 |
</svg>
|
| 297 |
</button>
|
| 298 |
</div>
|
| 299 |
+
|
| 300 |
+
<div id="rating-overlay">
|
| 301 |
+
<h3 style="color:#1e293b; margin-bottom:8px">Rate Your Experience</h3>
|
| 302 |
+
<p style="color:#64748b; font-size:14px">How was your chat?</p>
|
| 303 |
+
<div class="stars" id="rating-stars">
|
| 304 |
+
<span data-val="1">β
</span><span data-val="2">β
</span><span data-val="3">β
</span><span data-val="4">β
</span><span data-val="5">β
</span>
|
| 305 |
+
</div>
|
| 306 |
+
<button id="btn-submit-rating" style="background:#10b981; color:#fff; border:none; padding:10px 24px; border-radius:8px; font-weight:600; cursor:pointer; margin-top:8px">Submit & Close</button>
|
| 307 |
+
</div>
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
|
|
|
|
| 341 |
const API_URL = "https://joedown11-chatbot21.hf.space/api/chat";
|
| 342 |
const BASE_URL = API_URL.replace("/api/chat", "");
|
| 343 |
let chatHistory = [];
|
| 344 |
+
let _pollInterval = null;
|
| 345 |
+
let _lastPollId = 0;
|
| 346 |
|
| 347 |
// ββ Session ID: generate once, persist in localStorage ββ
|
| 348 |
let sessionId = localStorage.getItem('martech_session_id');
|
|
|
|
| 473 |
console.warn('Martech: background session pre-creation failed', e);
|
| 474 |
}
|
| 475 |
|
|
|
|
| 476 |
try {
|
| 477 |
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
|
| 478 |
if (hResp.ok) {
|
| 479 |
const hData = await hResp.json();
|
| 480 |
+
|
| 481 |
+
if (hData.status === 'HUMAN_TAKEOVER') {
|
| 482 |
+
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
|
| 483 |
+
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
|
| 484 |
+
} else {
|
| 485 |
+
document.getElementById('chat-status-dot').style.background = '#10b981';
|
| 486 |
+
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
if (hData.history && hData.history.length > 0) {
|
| 490 |
messagesContainer.innerHTML = '';
|
| 491 |
chatHistory = hData.history;
|
|
|
|
| 534 |
// Fallback: Check every 15 seconds (virtually zero cost once session is named)
|
| 535 |
setInterval(_attemptSync, 15000);
|
| 536 |
}
|
| 537 |
+
|
| 538 |
+
// Setup polling for admin messages
|
| 539 |
+
if (_pollInterval) clearInterval(_pollInterval);
|
| 540 |
+
_pollInterval = setInterval(pollMessages, 3000);
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
async function pollMessages() {
|
| 544 |
+
if (!sessionId) return;
|
| 545 |
+
try {
|
| 546 |
+
const r = await fetch(BASE_URL + "/api/chat/poll", {
|
| 547 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 548 |
+
body: JSON.stringify({ session_id: sessionId, last_id: _lastPollId })
|
| 549 |
+
});
|
| 550 |
+
if (r.ok) {
|
| 551 |
+
const data = await r.json();
|
| 552 |
+
_lastPollId = data.last_id;
|
| 553 |
+
|
| 554 |
+
if (data.status === 'HUMAN_TAKEOVER') {
|
| 555 |
+
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
|
| 556 |
+
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
|
| 557 |
+
} else {
|
| 558 |
+
document.getElementById('chat-status-dot').style.background = '#10b981';
|
| 559 |
+
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
if (data.new_messages && data.new_messages.length > 0) {
|
| 563 |
+
data.new_messages.forEach(m => {
|
| 564 |
+
appendMessage('bot', m.content);
|
| 565 |
+
chatHistory.push({ role: "assistant", content: m.content });
|
| 566 |
+
});
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
} catch(e) {}
|
| 570 |
}
|
| 571 |
|
| 572 |
initUserSession();
|
|
|
|
| 686 |
inputField.addEventListener('keypress', function(e) {
|
| 687 |
if (e.key === 'Enter') handleSend();
|
| 688 |
});
|
| 689 |
+
|
| 690 |
+
// Rating & End Chat Logic
|
| 691 |
+
const ratingOverlay = document.getElementById('rating-overlay');
|
| 692 |
+
const stars = document.querySelectorAll('#rating-stars span');
|
| 693 |
+
let _selectedRating = 0;
|
| 694 |
+
|
| 695 |
+
document.getElementById('btn-end-chat').addEventListener('click', () => {
|
| 696 |
+
ratingOverlay.classList.add('show');
|
| 697 |
+
});
|
| 698 |
+
|
| 699 |
+
stars.forEach(s => {
|
| 700 |
+
s.addEventListener('click', () => {
|
| 701 |
+
_selectedRating = parseInt(s.dataset.val);
|
| 702 |
+
stars.forEach(st => {
|
| 703 |
+
st.classList.toggle('active', parseInt(st.dataset.val) <= _selectedRating);
|
| 704 |
+
});
|
| 705 |
+
});
|
| 706 |
+
});
|
| 707 |
+
|
| 708 |
+
document.getElementById('btn-submit-rating').addEventListener('click', async () => {
|
| 709 |
+
if (_selectedRating > 0) {
|
| 710 |
+
try {
|
| 711 |
+
await fetch(BASE_URL + "/api/chat/rate", {
|
| 712 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 713 |
+
body: JSON.stringify({ session_id: sessionId, rating: _selectedRating })
|
| 714 |
+
});
|
| 715 |
+
} catch(e) {}
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// Clean up session completely
|
| 719 |
+
localStorage.removeItem('martech_session_id');
|
| 720 |
+
sessionId = null;
|
| 721 |
+
chatHistory = [];
|
| 722 |
+
_lastPollId = 0;
|
| 723 |
+
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
|
| 724 |
+
ratingOverlay.classList.remove('show');
|
| 725 |
+
|
| 726 |
+
document.getElementById('chat-status-dot').style.background = '#10b981';
|
| 727 |
+
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
|
| 728 |
+
|
| 729 |
+
fab.click(); // Close chat UI
|
| 730 |
+
|
| 731 |
+
// Prep for next open
|
| 732 |
+
setTimeout(() => {
|
| 733 |
+
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 734 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 735 |
+
initUserSession();
|
| 736 |
+
}, 500);
|
| 737 |
+
});
|
| 738 |
|
| 739 |
})();
|
| 740 |
</script>
|