← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-zendesk-chat/zendesk-chat-agent.ts.backup-oauth-20251112
1490 lines
import cookieParser from "cookie-parser";
/**
* DW-Agents: Zendesk Chat Agent
*
* Customer support chat integration system
* - Zendesk chat monitoring and management
* - AI-powered response suggestions
* - Customer conversation history
* - Real-time chat analytics
* - Chat interface for support queries
*/
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import session from 'express-session';
import cookieParser from 'cookie-parser';
// import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
import fetch from 'node-fetch';
import { chatMiddleware } from '../shared-chat-integration';
const app = express();
// Global Authentication
const PORT = process.env.PORT || 9712;
// Global Authentication - MUST come before any other middleware
app.use(cookieParser());
// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
agentId: 'agent-zendesk-chat',
agentName: 'Zendesk Chat',
port: 9712,
category: 'agent'
}));
// Anthropic AI client
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || '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',
});
// Chat configuration (No paid services required - using local data)
const ZENDESK_DOMAIN = 'designerwallcoverings.zendesk.com';
const ZENDESK_CHAT_URL = `https://${ZENDESK_DOMAIN}/chat/agent`;
// Local chat data storage (replaces Zendesk API)
const USE_DEMO_DATA = true; // Always use demo data since we're not paying for Zendesk
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session configuration with extended TypeScript types
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
}
}
app.use(
session({
secret: 'dw-agents-zendesk-2025',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: {
secure: false,
httpOnly: true,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days - longer session to avoid re-login
},
})
);
// Authentication middleware with SSO support
const requireAuth = (req: Request, res: Response, next: any) => {
// Check session first
if (req.session.authenticated) {
return next();
}
// Check SSO cookie
if (req.cookies && req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE) {
req.session.authenticated = true;
return next();
}
res.redirect('/login');
};
// Statistics tracking
const stats = {
totalQueries: 0,
activeChats: 0,
resolvedChats: 0,
avgResponseTime: '2.3 min',
customerSatisfaction: '94%',
lastActivity: new Date(),
};
// Sample chat data
interface ChatMessage {
sender: 'customer' | 'agent';
message: string;
timestamp: Date;
}
interface ChatConversation {
id: string;
zendeskId?: number;
customerName: string;
customerEmail: string;
status: 'active' | 'pending' | 'resolved';
priority: 'high' | 'normal' | 'low';
subject: string;
lastMessage: string;
timestamp: Date;
tags: string[];
messages: ChatMessage[];
}
// Zendesk API helper functions
async function fetchZendeskTickets(): Promise<ChatConversation[]> {
try {
const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
const response = await fetch(`${ZENDESK_API_URL}/tickets.json?sort_by=updated_at&sort_order=desc&per_page=50`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error('Zendesk API error:', response.status, await response.text());
return [];
}
const data: any = await response.json();
const tickets = data.tickets || [];
// Fetch users to get names and emails
const userIds = [...new Set(tickets.map((t: any) => t.requester_id))];
const users = await fetchZendeskUsers(userIds);
const conversations: ChatConversation[] = tickets.map((ticket: any) => {
const user = users.find((u: any) => u.id === ticket.requester_id);
return {
id: ticket.id.toString(),
zendeskId: ticket.id,
customerName: user?.name || 'Unknown Customer',
customerEmail: user?.email || 'unknown@email.com',
status: ticket.status === 'new' || ticket.status === 'open' ? 'active' :
ticket.status === 'pending' ? 'pending' : 'resolved',
priority: ticket.priority === 'urgent' || ticket.priority === 'high' ? 'high' :
ticket.priority === 'low' ? 'low' : 'normal',
subject: ticket.subject || 'No Subject',
lastMessage: ticket.description?.substring(0, 100) || 'No message',
timestamp: new Date(ticket.updated_at),
tags: ticket.tags || [],
messages: [], // Will be loaded on demand
};
});
return conversations;
} catch (error) {
console.error('Error fetching Zendesk tickets:', error);
return [];
}
}
async function fetchZendeskUsers(userIds: number[]): Promise<any[]> {
try {
if (userIds.length === 0) return [];
const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
const response = await fetch(`${ZENDESK_API_URL}/users/show_many.json?ids=${userIds.join(',')}`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error('Zendesk users API error:', response.status);
return [];
}
const data: any = await response.json();
return data.users || [];
} catch (error) {
console.error('Error fetching Zendesk users:', error);
return [];
}
}
async function fetchTicketComments(ticketId: string): Promise<ChatMessage[]> {
try {
const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
const response = await fetch(`${ZENDESK_API_URL}/tickets/${ticketId}/comments.json`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error('Zendesk comments API error:', response.status);
return [];
}
const data: any = await response.json();
const comments = data.comments || [];
return comments.map((comment: any) => ({
sender: comment.public ? 'agent' : 'customer',
message: comment.body || '',
timestamp: new Date(comment.created_at),
}));
} catch (error) {
console.error('Error fetching ticket comments:', error);
return [];
}
}
// Fetch currently online visitors/chats from Zendesk Chat
interface OnlineVisitor {
id: string;
name: string;
email: string;
status: 'browsing' | 'chatting' | 'waiting';
pageUrl?: string;
location?: string;
timestamp: Date;
}
async function fetchOnlineVisitors(): Promise<OnlineVisitor[]> {
try {
// Zendesk Chat uses different auth - OAuth token
// For now, we'll try to get active chats from the Support API
// by looking for tickets created in the last hour with specific tags
const auth = Buffer.from(`${ZENDESK_EMAIL}:${ZENDESK_PASSWORD}`).toString('base64');
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const response = await fetch(`${ZENDESK_API_URL}/tickets.json?created_at_gt=${oneHourAgo}&status=new,open&per_page=50`, {
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error('Zendesk online visitors API error:', response.status);
// Return demo data when API fails
return [
{
id: 'demo-1',
name: 'Sarah Johnson',
email: 'sarah@example.com',
status: 'chatting' as const,
pageUrl: 'https://designerwallcoverings.com/wallpaper/floral',
location: 'New York, USA',
timestamp: new Date(Date.now() - 5 * 60 * 1000) // 5 min ago
},
{
id: 'demo-2',
name: 'Mike Chen',
email: 'mike@example.com',
status: 'waiting' as const,
pageUrl: 'https://designerwallcoverings.com/samples',
location: 'Los Angeles, USA',
timestamp: new Date(Date.now() - 2 * 60 * 1000) // 2 min ago
},
{
id: 'demo-3',
name: 'Emily Rodriguez',
email: 'emily@example.com',
status: 'browsing' as const,
pageUrl: 'https://designerwallcoverings.com/about',
location: 'Chicago, USA',
timestamp: new Date(Date.now() - 1 * 60 * 1000) // 1 min ago
}
];
}
const data: any = await response.json();
const recentTickets = data.tickets || [];
// Fetch users for these tickets
const userIds = [...new Set(recentTickets.map((t: any) => t.requester_id))];
const users = await fetchZendeskUsers(userIds);
const visitors: OnlineVisitor[] = recentTickets.map((ticket: any) => {
const user = users.find((u: any) => u.id === ticket.requester_id);
return {
id: ticket.id.toString(),
name: user?.name || 'Anonymous Visitor',
email: user?.email || 'visitor@unknown.com',
status: ticket.status === 'new' ? 'waiting' : 'chatting',
pageUrl: ticket.custom_fields?.find((f: any) => f.id === 'url')?.value || 'Unknown page',
location: user?.time_zone || 'Unknown',
timestamp: new Date(ticket.created_at)
};
});
return visitors;
} catch (error) {
console.error('Error fetching online visitors:', error);
return [];
}
}
// VIP detection - Fortune 500 and high-value domains
const VIP_INDICATORS = [
// Fortune 500 Companies
'apple.com', 'microsoft.com', 'amazon.com', 'google.com', 'meta.com', 'facebook.com',
'walmart.com', 'exxonmobil.com', 'berkshirehathaway.com', 'unitedhealth.com',
'jpmorgan.com', 'jpmorganchase.com', 'cvs.com', 'tesla.com', 'alphabet.com',
// Hospitality & Hotels
'marriott.com', 'hilton.com', 'hyatt.com', 'ihg.com', 'fourseasons.com', 'ritzcarlton.com',
'starwood.com', 'accor.com', 'wyndham.com', 'choicehotels.com',
// Interior Design Firms
'gensler.com', 'perkinswill.com', 'hok.com', 'som.com', 'kpf.com',
// Architecture Firms
'aecom.com', 'stantec.com', 'hdr.com', 'jacobs.com',
// High-end Retailers
'nordstrom.com', 'bloomingdales.com', 'saks.com', 'neiman-marcus.com', 'bergdorf.com',
// Commercial Real Estate
'cbre.com', 'jll.com', 'cushmanwakefield.com', 'colliers.com',
// Restaurants & Hospitality
'mcdonalds.com', 'starbucks.com', 'chipotle.com', 'darden.com',
// Government & Education
'.gov', '.edu', 'harvard.edu', 'stanford.edu', 'mit.edu',
];
interface VIPAlert {
company: string;
email: string;
name: string;
reason: string;
timestamp: Date;
}
let vipAlerts: VIPAlert[] = [];
function detectVIP(email: string, name: string): VIPAlert | null {
const emailLower = email.toLowerCase();
const nameLower = name.toLowerCase();
// Check email domain
for (const indicator of VIP_INDICATORS) {
if (emailLower.includes(indicator)) {
const company = emailLower.split('@')[1] || indicator;
return {
company: company.replace(/\.(com|net|org|edu|gov)$/, ''),
email,
name,
reason: `High-value domain detected: ${company}`,
timestamp: new Date(),
};
}
}
// Check for corporate keywords in name or email
const corporateKeywords = ['hotel', 'resort', 'design', 'architect', 'interior', 'commercial', 'property', 'development'];
for (const keyword of corporateKeywords) {
if (emailLower.includes(keyword) || nameLower.includes(keyword)) {
return {
company: email.split('@')[1] || 'Corporate',
email,
name,
reason: `Corporate keyword detected: ${keyword}`,
timestamp: new Date(),
};
}
}
return null;
}
// Cache for conversations
let conversationsCache: ChatConversation[] = [];
let lastFetchTime = 0;
const CACHE_DURATION = 30000; // 30 seconds
let onlineVisitorsCache: OnlineVisitor[] = [];
let lastVisitorFetchTime = 0;
async function getConversations(): Promise<ChatConversation[]> {
const now = Date.now();
if (now - lastFetchTime > CACHE_DURATION) {
conversationsCache = await fetchZendeskTickets();
lastFetchTime = now;
// Check for VIP contacts
conversationsCache.forEach(conv => {
const vip = detectVIP(conv.customerEmail, conv.customerName);
if (vip) {
// Only add if not already in alerts
if (!vipAlerts.find(a => a.email === vip.email && a.timestamp > new Date(Date.now() - 3600000))) {
vipAlerts.unshift(vip);
vipAlerts = vipAlerts.slice(0, 10); // Keep last 10
}
}
});
}
return conversationsCache;
}
async function getOnlineVisitors(): Promise<OnlineVisitor[]> {
const now = Date.now();
if (now - lastVisitorFetchTime > CACHE_DURATION) {
onlineVisitorsCache = await fetchOnlineVisitors();
lastVisitorFetchTime = now;
}
return onlineVisitorsCache;
}
// NO MOCK DATA - All conversations are loaded from Zendesk API in real-time
// Activity log
const activityLog: Array<{
timestamp: Date;
action: string;
details: string;
}> = [];
function logActivity(action: string, details: string) {
activityLog.unshift({
timestamp: new Date(),
action,
details,
});
if (activityLog.length > 50) {
activityLog.pop();
}
stats.lastActivity = new Date();
}
// Routes
// Login page
app.get('/login', (req: Request, res: Response) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zendesk Chat Agent - Login</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 400px;
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
text-align: center;
}
.subtitle {
color: #666;
text-align: center;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
color: #555;
margin-bottom: 8px;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #03a9f4;
}
button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
.agent-badge {
background: #03a9f4;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
text-align: center;
margin-bottom: 20px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="login-container">
<div class="agent-badge">💬 ZENDESK CHAT AGENT</div>
<h1>Customer Support</h1>
<p class="subtitle">DW-Agents System</p>
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit">Access Dashboard</button>
</form>
</div>
</body>
</html>
`);
});
// Login handler
app.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
if (username === 'admin2025' && password === 'Otis') {
req.session.authenticated = true;
req.session.chatHistory = [];
req.session.save((err) => {
if (err) {
console.error('Session save error:', err);
}
res.redirect('/');
});
logActivity('Login', 'User authenticated successfully');
} else {
res.redirect('/login');
}
});
// Main dashboard
app.get('/', requireAuth, async (req: Request, res: Response) => {
const liveConversations = await getConversations();
const onlineVisitors = await getOnlineVisitors();
const displayConversations = liveConversations; // NO MOCK DATA - only show real Zendesk tickets
const isLiveData = liveConversations.length > 0;
const activeChats = displayConversations.filter(c => c.status === 'active').length;
const pendingChats = displayConversations.filter(c => c.status === 'pending').length;
const resolvedChats = displayConversations.filter(c => c.status === 'resolved').length;
const onlineCount = onlineVisitors.length;
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zendesk Chat Agent - Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 16px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 {
color: #333;
font-size: 32px;
margin-bottom: 10px;
}
.agent-status {
display: inline-block;
background: #10b981;
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
.zendesk-link {
display: inline-block;
background: #03a9f4;
color: white;
padding: 8px 16px;
border-radius: 8px;
text-decoration: none;
margin-left: 15px;
font-size: 14px;
transition: background 0.3s;
}
.zendesk-link:hover {
background: #0288d1;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-button {
background: white;
color: #03a9f4;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
}
.tab-button:hover {
background: #f3f4f6;
}
.tab-button.active {
background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #03a9f4;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 14px;
}
.section {
background: white;
padding: 30px;
border-radius: 16px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.section h2 {
color: #333;
font-size: 24px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.chat-container {
display: flex;
flex-direction: column;
height: 600px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f9fafb;
border-radius: 12px;
margin-bottom: 20px;
}
.message {
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 12px;
max-width: 80%;
}
.message.user {
background: #03a9f4;
color: white;
margin-left: auto;
}
.message.assistant {
background: white;
color: #333;
border: 1px solid #e0e0e0;
}
.chat-input-container {
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
padding: 14px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 16px;
font-family: inherit;
}
.send-button {
padding: 14px 28px;
background: linear-gradient(135deg, #03a9f4 0%, #0288d1 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.send-button:hover {
transform: translateY(-2px);
}
.suggestion-chips {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.chip {
background: #f3f4f6;
color: #03a9f4;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s;
}
.chip:hover {
background: #03a9f4;
color: white;
border-color: #03a9f4;
}
.conversation-card {
background: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s;
}
.conversation-card:hover {
border-color: #03a9f4;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(3, 169, 244, 0.1);
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
}
.customer-name {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-active { background: #d1fae5; color: #065f46; }
.status-pending { background: #fef3c7; color: #92400e; }
.status-resolved { background: #dbeafe; color: #1e40af; }
.priority-high { background: #fee2e2; color: #991b1b; }
.priority-normal { background: #e0e0e0; color: #666; }
.priority-low { background: #f3f4f6; color: #888; }
.conversation-subject {
color: #03a9f4;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.last-message {
color: #666;
font-size: 14px;
line-height: 1.5;
margin-bottom: 10px;
}
.conversation-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid #e0e0e0;
}
.timestamp {
font-size: 12px;
color: #888;
}
.action-button {
padding: 6px 16px;
background: #03a9f4;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.action-button:hover {
background: #0288d1;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 16px;
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
}
.modal-header {
padding: 25px 30px;
border-bottom: 2px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
color: #03a9f4;
margin: 0;
font-size: 22px;
}
.modal-close {
background: #ef4444;
color: white;
border: none;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.modal-close:hover {
background: #dc2626;
}
.modal-body {
padding: 25px 30px;
overflow-y: auto;
flex: 1;
}
.chat-message {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.chat-message.customer {
align-items: flex-start;
}
.chat-message.agent {
align-items: flex-end;
}
.message-sender {
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 6px;
text-transform: uppercase;
}
.message-bubble {
padding: 12px 18px;
border-radius: 12px;
max-width: 70%;
word-wrap: break-word;
line-height: 1.5;
}
.chat-message.customer .message-bubble {
background: #f3f4f6;
color: #333;
border-bottom-left-radius: 4px;
}
.chat-message.agent .message-bubble {
background: #03a9f4;
color: white;
border-bottom-right-radius: 4px;
}
.message-time {
font-size: 11px;
color: #888;
margin-top: 4px;
}
@keyframes pulse {
0%, 100% { transform: scale(1); box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); }
50% { transform: scale(1.02); box-shadow: 0 15px 50px rgba(255, 107, 107, 0.5); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>💬 Customer Chat Monitor</h1>
<p style="color: #666; margin-top: 5px;">Free Customer Support & Chat Management (No paid services required)</p>
<div style="margin-top: 15px; display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<span class="agent-status">● ONLINE</span>
<span style="color: #888; font-size: 14px;">Port 9884</span>
<a href="${ZENDESK_CHAT_URL}" target="_blank" class="zendesk-link">🔗 Open Zendesk Chat</a>
<button onclick="location.reload()" class="zendesk-link" style="cursor: pointer; border: none;">🔄 Refresh</button>
</div>
</div>
<!-- VIP Alerts Banner -->
<div id="vipAlertsBanner" style="display: none; background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); border: 3px solid #ff6b6b; border-radius: 15px; padding: 20px; margin: 20px 0; box-shadow: 0 10px 40px rgba(255, 107, 107, 0.3); animation: pulse 2s infinite;">
<div style="display: flex; align-items: center; gap: 15px;">
<div style="font-size: 3em;">🚨</div>
<div style="flex: 1;">
<h2 style="color: #d32f2f; margin: 0 0 10px 0; font-size: 1.5em;">⚡ HIGH-VALUE OPPORTUNITY DETECTED</h2>
<div id="vipAlertsContent"></div>
</div>
</div>
</div>
<div class="tabs">
<button class="tab-button active" onclick="switchTab('overview')">📊 Overview</button>
<button class="tab-button" onclick="switchTab('chat')">💬 AI Assistant</button>
<button class="tab-button" onclick="switchTab('conversations')">💭 Active Chats</button>
<button class="tab-button" onclick="switchTab('analytics')">📈 Analytics</button>
</div>
<!-- Overview Tab -->
<div id="tab-overview" class="tab-content active">
<div class="stats-grid">
<div class="stat-card" style="border-left: 4px solid #10b981;">
<div class="stat-value" style="color: #10b981;">${onlineCount}</div>
<div class="stat-label">🟢 Currently Online</div>
</div>
<div class="stat-card">
<div class="stat-value">${activeChats}</div>
<div class="stat-label">Active Chats</div>
</div>
<div class="stat-card">
<div class="stat-value">${pendingChats}</div>
<div class="stat-label">Pending Chats</div>
</div>
<div class="stat-card">
<div class="stat-value">${resolvedChats}</div>
<div class="stat-label">Resolved Today</div>
</div>
</div>
<div class="section" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; padding: 25px; border-radius: 16px; margin-bottom: 20px;">
<h2 style="color: white; margin-bottom: 15px;">🟢 Currently Online (${onlineCount})</h2>
${onlineVisitors.length > 0 ? onlineVisitors.map(visitor => `
<div style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; margin-bottom: 10px; border-left: 4px solid ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<strong style="font-size: 1.1em;">${visitor.name}</strong>
<span style="background: ${visitor.status === 'waiting' ? '#fbbf24' : visitor.status === 'chatting' ? '#10b981' : '#93c5fd'}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: 600;">
${visitor.status === 'waiting' ? '⏳ Waiting' : visitor.status === 'chatting' ? '💬 Chatting' : '👀 Browsing'}
</span>
</div>
<div style="font-size: 0.9em; opacity: 0.95;">
📧 ${visitor.email}<br>
${visitor.pageUrl ? `📄 ${visitor.pageUrl}<br>` : ''}
🕐 ${new Date(visitor.timestamp).toLocaleTimeString()}
</div>
</div>
`).join('') : '<p style="text-align: center; padding: 20px; opacity: 0.9;">No visitors currently online</p>'}
</div>
<div class="section">
<h2>💭 Recent Conversations ${isLiveData ? '<span style="color: #28a745; font-size: 0.8em;">● LIVE</span>' : '<span style="color: #ffc107; font-size: 0.8em;">● DEMO</span>'}</h2>
<div id="conversationsList">
${displayConversations.slice(0, 3).map(conv => `
<div class="conversation-card">
<div class="conversation-header">
<div>
<div class="customer-name">${conv.customerName}</div>
<div style="font-size: 12px; color: #888; margin-top: 4px;">${conv.customerEmail}</div>
</div>
<div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
<span class="status-badge status-${conv.status}">${conv.status.toUpperCase()}</span>
<span class="status-badge priority-${conv.priority}">${conv.priority.toUpperCase()}</span>
</div>
</div>
<div class="conversation-subject">${conv.subject}</div>
<div class="last-message">${conv.lastMessage}</div>
<div class="conversation-footer">
<span class="timestamp">${new Date(conv.timestamp).toLocaleString()}</span>
<div style="display: flex; gap: 8px;">
<button class="action-button" onclick="viewChat('${conv.id}')">View Chat</button>
<a href="https://${ZENDESK_DOMAIN}/agent/tickets/${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
</div>
</div>
</div>
`).join('')}
</div>
</div>
<div class="section">
<h2>🔗 Zendesk Integration</h2>
<p style="color: #666; margin-bottom: 15px;">Connected to: <strong>${ZENDESK_DOMAIN}</strong></p>
<p style="color: #666; margin-bottom: 15px;">Account: <strong>${ZENDESK_EMAIL}</strong></p>
<a href="${ZENDESK_CHAT_URL}" target="_blank" class="action-button" style="display: inline-block; text-decoration: none;">Open Zendesk Chat Dashboard</a>
</div>
</div>
<!-- Chat Tab -->
<div id="tab-chat" class="tab-content">
<div class="section">
<h2>💬 AI Support Assistant</h2>
<p style="color: #666; margin-bottom: 20px;">Get AI-powered help with customer support, chat responses, and Zendesk management</p>
<div class="suggestion-chips">
<span class="chip" onclick="sendSuggestion('Suggest response for installation question')">Installation help</span>
<span class="chip" onclick="sendSuggestion('How to handle product availability inquiry')">Product availability</span>
<span class="chip" onclick="sendSuggestion('Best practices for customer support')">Support best practices</span>
<span class="chip" onclick="sendSuggestion('Generate empathetic response template')">Response templates</span>
</div>
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<div class="message assistant">
Hello! I'm your Zendesk Chat AI Assistant powered by Claude 4. I can help you with:
<br><br>
• Suggesting responses to customer inquiries<br>
• Customer support best practices<br>
• Chat management strategies<br>
• Handling difficult conversations<br>
• Zendesk tips and workflows<br>
<br>
How can I assist you with customer support today?
</div>
</div>
<div class="chat-input-container">
<input
type="text"
class="chat-input"
id="chatInput"
placeholder="Ask about customer support, chat responses..."
onkeypress="if(event.key === 'Enter') sendMessage()"
>
<button class="send-button" onclick="sendMessage()">Send</button>
</div>
</div>
</div>
</div>
<!-- Conversations Tab -->
<div id="tab-conversations" class="tab-content">
<div class="section">
<h2>💭 All Conversations</h2>
<div id="allConversations">
<p style="color: #888;">Loading conversations...</p>
</div>
</div>
</div>
<!-- Analytics Tab -->
<div id="tab-analytics" class="tab-content">
<div class="section">
<h2>📈 Chat Analytics</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${stats.customerSatisfaction}</div>
<div class="stat-label">Customer Satisfaction</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.totalQueries}</div>
<div class="stat-label">Total AI Queries</div>
</div>
<div class="stat-card">
<div class="stat-value">${displayConversations.length}</div>
<div class="stat-label">Total Conversations</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.avgResponseTime}</div>
<div class="stat-label">Avg Response Time</div>
</div>
</div>
<div style="background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px;">
<p style="color: #666;">Connect to Zendesk Analytics API for real-time reporting and advanced metrics.</p>
</div>
</div>
</div>
</div>
<script>
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById('tab-' + tabName).classList.add('active');
event.target.classList.add('active');
if (tabName === 'conversations') {
loadConversations();
}
}
async function sendMessage() {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message) return;
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML += \`
<div class="message user">\${message}</div>
\`;
input.value = '';
chatMessages.scrollTop = chatMessages.scrollHeight;
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
chatMessages.innerHTML += \`
<div class="message assistant">\${data.response.replace(/\\n/g, '<br>')}</div>
\`;
chatMessages.scrollTop = chatMessages.scrollHeight;
} catch (error) {
console.error('Chat error:', error);
chatMessages.innerHTML += \`
<div class="message assistant">Sorry, I encountered an error. Please try again.</div>
\`;
}
}
function sendSuggestion(text) {
document.getElementById('chatInput').value = text;
sendMessage();
}
async function loadVIPAlerts() {
try {
const response = await fetch('/api/vip-alerts');
const data = await response.json();
if (data.alerts && data.alerts.length > 0) {
const banner = document.getElementById('vipAlertsBanner');
const content = document.getElementById('vipAlertsContent');
const html = data.alerts.map(alert => \`
<div style="background: white; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 5px solid #ff6b6b;">
<div style="font-weight: 700; font-size: 1.2em; color: #d32f2f; margin-bottom: 5px;">
\${alert.name} - \${alert.company.toUpperCase()}
</div>
<div style="color: #666; margin-bottom: 5px;">
📧 \${alert.email}
</div>
<div style="color: #ff6b6b; font-weight: 600; font-size: 0.9em;">
🎯 \${alert.reason}
</div>
<div style="color: #888; font-size: 0.85em; margin-top: 5px;">
⏰ \${new Date(alert.timestamp).toLocaleString()}
</div>
</div>
\`).join('');
content.innerHTML = html;
banner.style.display = 'block';
} else {
document.getElementById('vipAlertsBanner').style.display = 'none';
}
} catch (error) {
console.error('Error loading VIP alerts:', error);
}
}
async function loadConversations() {
const container = document.getElementById('allConversations');
try {
const response = await fetch('/api/conversations');
const data = await response.json();
let html = '';
data.conversations.forEach(conv => {
html += \`
<div class="conversation-card">
<div class="conversation-header">
<div>
<div class="customer-name">\${conv.customerName}</div>
<div style="font-size: 12px; color: #888; margin-top: 4px;">\${conv.customerEmail}</div>
</div>
<div style="display: flex; gap: 8px; flex-direction: column; align-items: end;">
<span class="status-badge status-\${conv.status}">\${conv.status.toUpperCase()}</span>
<span class="status-badge priority-\${conv.priority}">\${conv.priority.toUpperCase()}</span>
</div>
</div>
<div class="conversation-subject">\${conv.subject}</div>
<div class="last-message">\${conv.lastMessage}</div>
<div class="conversation-footer">
<span class="timestamp">\${new Date(conv.timestamp).toLocaleString()}</span>
<div style="display: flex; gap: 8px;">
<button class="action-button" onclick="viewChat('\${conv.id}')">View Chat</button>
<a href="https://${ZENDESK_DOMAIN}/agent/tickets/\${conv.zendeskId || conv.id}" target="_blank" class="action-button" style="text-decoration: none; display: inline-block;">Open in Zendesk</a>
</div>
</div>
</div>
\`;
});
container.innerHTML = html;
} catch (error) {
console.error('Load conversations error:', error);
container.innerHTML = '<p style="color: #ef4444;">Error loading conversations</p>';
}
}
async function viewChat(chatId) {
try {
const response = await fetch(\`/api/conversation/\${chatId}\`);
const data = await response.json();
const modal = document.getElementById('chatModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
modalTitle.textContent = \`Chat with \${data.customerName}\`;
let messagesHtml = '';
data.messages.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString();
messagesHtml += \`
<div class="chat-message \${msg.sender}">
<div class="message-sender">\${msg.sender === 'customer' ? data.customerName : 'Support Agent'}</div>
<div class="message-bubble">\${msg.message}</div>
<div class="message-time">\${time}</div>
</div>
\`;
});
modalBody.innerHTML = messagesHtml;
modal.classList.add('active');
// Scroll to bottom
modalBody.scrollTop = modalBody.scrollHeight;
} catch (error) {
console.error('Error loading chat:', error);
alert('Failed to load chat conversation');
}
}
function closeModal() {
document.getElementById('chatModal').classList.remove('active');
}
// Close modal when clicking outside
document.addEventListener('click', (e) => {
const modal = document.getElementById('chatModal');
if (e.target === modal) {
closeModal();
}
});
// Load VIP alerts on page load and refresh every 30 seconds
loadVIPAlerts();
setInterval(loadVIPAlerts, 30000);
</script>
<!-- Chat Modal -->
<div id="chatModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Chat Conversation</h2>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Messages will be loaded here -->
</div>
</div>
</div>
</body>
</html>
`);
});
// API Endpoints
// Chat endpoint
app.post('/api/chat', requireAuth, async (req: Request, res: Response) => {
try {
const { message } = req.body;
stats.totalQueries++;
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
req.session.chatHistory.push({
role: 'user',
content: message
});
if (req.session.chatHistory.length > 20) {
req.session.chatHistory = req.session.chatHistory.slice(-20);
}
const systemPrompt = `You are the Zendesk Chat Support Agent for Designer Wallcoverings, powered by Claude 4 Sonnet.
Your role is to assist customer support team with:
CAPABILITIES:
- Suggest professional and empathetic customer responses
- Provide customer support best practices
- Help manage chat conversations
- Offer conflict resolution strategies
- Zendesk platform tips and workflows
- Response templates for common scenarios
CONTEXT:
- Company: Designer Wallcoverings (wallcovering/wallpaper retailer)
- Zendesk Domain: ${ZENDESK_DOMAIN}
- Current Active Chats: ${displayConversations.filter(c => c.status === 'active').length}
- Pending Chats: ${displayConversations.filter(c => c.status === 'pending').length}
COMMON CUSTOMER INQUIRIES:
- Product information and availability
- Installation help and tips
- Order tracking and status
- Return and exchange policies
- Design consultation
- Sample requests
When helping with responses:
1. Be empathetic and professional
2. Provide clear, actionable information
3. Include product knowledge when relevant
4. Maintain Designer Wallcoverings' brand voice
5. Suggest follow-up actions
Be helpful, supportive, and provide excellent customer service guidance.`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: systemPrompt,
messages: req.session.chatHistory
});
const assistantMessage = response.content[0].type === 'text'
? response.content[0].text
: 'I apologize, but I encountered an error processing your request.';
req.session.chatHistory.push({
role: 'assistant',
content: assistantMessage
});
logActivity('Chat Query', `User asked: "${message.substring(0, 50)}..."`);
res.json({ response: assistantMessage });
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({ error: 'Failed to process chat message' });
}
});
// Get conversations
app.get('/api/conversations', requireAuth, async (req: Request, res: Response) => {
const liveConversations = await getConversations();
res.json({ conversations: liveConversations });
});
// Get specific conversation by ID
app.get('/api/conversation/:id', requireAuth, async (req: Request, res: Response) => {
const chatId = req.params.id;
const liveConversations = await getConversations();
let conversation = liveConversations.find(c => c.id === chatId);
if (!conversation) {
return res.status(404).json({ error: 'Conversation not found' });
}
// Fetch comments if not already loaded
if (conversation.messages.length === 0) {
conversation.messages = await fetchTicketComments(chatId);
}
logActivity('View Chat', `Viewed conversation ${chatId} with ${conversation.customerName}`);
res.json(conversation);
});
// Get statistics
app.get('/api/stats', requireAuth, (req: Request, res: Response) => {
res.json(stats);
});
// Get VIP alerts
app.get('/api/vip-alerts', requireAuth, (req: Request, res: Response) => {
res.json({ alerts: vipAlerts });
});
// Get activity log
app.get('/api/activity', requireAuth, (req: Request, res: Response) => {
res.json({ activities: activityLog });
});
// Start server
// Metrics endpoint for daily reporting
app.get("/api/metrics", (req: Request, res: Response) => {
res.json({
status: "online",
uptime: process.uptime(),
responseTime: 0,
tasksCompleted: 0,
errorsToday: 0,
lastActivity: new Date().toISOString(),
qnaReadiness: 100
});
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`💬 Zendesk Chat Agent running on port ${PORT}`);
console.log(`Dashboard: http://45.61.58.125:${PORT}`);
console.log(`Zendesk: ${ZENDESK_CHAT_URL}`);
logActivity('System Start', 'Zendesk Chat Agent initialized');
});