Arag / app /admin /templates /admin.html
AuthorBot
Fix emoji corruption properly using pristine commit reference
05f2abe
Raw
History Blame Contribute Delete
194 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AuthorBot — Author Admin Panel</title>
<meta name="description" content="Premium author admin panel for AI-powered book sales chatbot management.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script src="/static/auth-client.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f8f9fc; --white: #ffffff; --surface: #ffffff; --surface2: #f1f3f9;
--border: #e5e7ef; --border-light: #eef0f6;
--accent: #6366f1; --accent-hover: #4f46e5; --accent-soft: rgba(99,102,241,0.08);
--accent2: #8b5cf6; --accent3: #ec4899;
--text: #1e293b; --text2: #334155; --muted: #64748b; --light: #94a3b8;
--green: #10b981; --green-bg: rgba(16,185,129,0.1);
--red: #ef4444; --red-bg: rgba(239,68,68,0.08);
--yellow: #f59e0b; --yellow-bg: rgba(245,158,11,0.1);
--blue: #3b82f6; --blue-bg: rgba(59,130,246,0.08);
--radius: 14px; --radius-sm: 10px; --radius-xs: 6px;
--shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
--shadow-lg: 0 4px 24px rgba(0,0,0,0.06), 0 12px 48px rgba(0,0,0,0.04);
--shadow-hover: 0 8px 32px rgba(99,102,241,0.12);
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--sidebar-w: 260px;
}
body { font-family: 'Inter', -apple-system, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; -webkit-font-smoothing: antialiased; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* ── Animations ── */
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
@keyframes ripple { to { transform: scale(4); opacity: 0; } }
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes countUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
.animate-in { animation: fadeIn 0.4s ease forwards; }
.animate-in-delay { animation: fadeIn 0.4s ease 0.1s forwards; opacity: 0; }
.skeleton { background: linear-gradient(90deg, var(--surface2) 25%, #e8eaf2 50%, var(--surface2) 75%); background-size: 800px 100%; animation: shimmer 1.5s infinite; border-radius: var(--radius-sm); }
/* ── Auth Screen ── */
.auth-wrap {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #f8f9fc 0%, #eef0ff 50%, #f0f0ff 100%);
position: relative; overflow: hidden;
}
.auth-wrap::before {
content: ''; position: absolute; width: 600px; height: 600px; border-radius: 50%;
background: radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);
top: -200px; right: -100px;
}
.auth-wrap::after {
content: ''; position: absolute; width: 400px; height: 400px; border-radius: 50%;
background: radial-gradient(circle, rgba(139,92,246,0.06) 0%, transparent 70%);
bottom: -100px; left: -50px;
}
.auth-box {
background: var(--white); border: 1px solid var(--border-light); border-radius: 24px;
padding: 48px 40px; width: 100%; max-width: 440px;
box-shadow: var(--shadow-lg); position: relative; z-index: 1;
animation: scaleIn 0.5s ease;
}
.auth-logo {
font-size: 1.8rem; font-weight: 800; margin-bottom: 6px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.auth-sub { color: var(--muted); font-size: 14px; margin-bottom: 36px; line-height: 1.5; }
/* ── Form Controls ── */
.form-group { margin-bottom: 20px; }
.form-label {
display: block; font-size: 12px; font-weight: 600; color: var(--muted);
margin-bottom: 8px; letter-spacing: 0.05em; text-transform: uppercase;
}
.form-input {
width: 100%; padding: 12px 16px; border-radius: var(--radius-sm);
border: 1.5px solid var(--border); background: var(--bg);
color: var(--text); font-size: 14px; font-family: inherit;
transition: border-color var(--transition), box-shadow var(--transition);
}
.form-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.form-input::placeholder { color: var(--light); }
textarea.form-input { resize: vertical; min-height: 80px; }
select.form-input { cursor: pointer; }
/* ── Buttons ── */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
padding: 12px 24px; border-radius: var(--radius-sm); border: none;
font-weight: 600; font-size: 14px; font-family: inherit; cursor: pointer;
transition: all var(--transition); position: relative; overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white; box-shadow: 0 2px 8px rgba(99,102,241,0.25);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(99,102,241,0.35); }
.btn-primary:active { transform: translateY(0); }
.btn-secondary { background: var(--surface2); color: var(--text2); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--border-light); border-color: var(--accent); color: var(--accent); }
.btn-danger { background: var(--red-bg); color: var(--red); }
.btn-danger:hover { background: var(--red); color: white; }
.btn-sm { padding: 7px 14px; font-size: 12px; border-radius: var(--radius-xs); }
.btn-icon { width: 36px; height: 36px; padding: 0; border-radius: var(--radius-xs); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }
.btn-block { width: 100%; }
/* ── Sidebar ── */
.sidebar {
position: fixed; top: 0; left: 0; bottom: 0; width: var(--sidebar-w);
background: var(--white); border-right: 1px solid var(--border-light);
display: flex; flex-direction: column; padding: 24px 16px; z-index: 100;
transition: transform var(--transition); overflow-y: auto; overflow-x: hidden;
}
.sidebar::-webkit-scrollbar { width: 4px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.sidebar-logo {
font-size: 1.25rem; font-weight: 800; padding: 0 12px; margin-bottom: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.sidebar-sub { font-size: 11px; color: var(--light); padding: 0 12px; margin-bottom: 28px; }
.nav-section { font-size: 10px; font-weight: 700; color: var(--light); letter-spacing: 0.1em; text-transform: uppercase; padding: 16px 12px 8px; }
.nav-item {
display: flex; align-items: center; gap: 12px; padding: 10px 12px;
border-radius: var(--radius-sm); color: var(--muted); text-decoration: none;
font-size: 13.5px; font-weight: 500; cursor: pointer; border: none; background: none;
width: 100%; transition: all var(--transition); margin-bottom: 2px; text-align: left;
}
.nav-item .nav-icon { width: 20px; text-align: center; font-size: 15px; }
.nav-item:hover { background: var(--accent-soft); color: var(--accent); }
.nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.nav-item .nav-badge {
margin-left: auto; background: var(--accent); color: white;
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 10px;
}
.sidebar-footer { margin-top: auto; padding-top: 16px; border-top: 1px solid var(--border-light); }
.user-card {
display: flex; align-items: center; gap: 10px; padding: 10px 12px;
border-radius: var(--radius-sm); margin-bottom: 8px;
}
.user-avatar {
width: 36px; height: 36px; border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent2));
display: flex; align-items: center; justify-content: center;
color: white; font-weight: 700; font-size: 14px; flex-shrink: 0;
}
.user-name { font-size: 13px; font-weight: 600; color: var(--text); }
.user-email { font-size: 11px; color: var(--light); }
/* ── Main Content ── */
.main { margin-left: var(--sidebar-w); padding: 32px 40px; min-height: 100vh; }
.page { display: none; } .page.active { display: block; animation: fadeIn 0.3s ease; }
.page-header { margin-bottom: 28px; }
.page-title { font-size: 1.6rem; font-weight: 800; color: var(--text); letter-spacing: -0.02em; margin-bottom: 4px; display: flex; align-items: center; gap: 10px; }
.page-sub { color: var(--muted); font-size: 14px; line-height: 1.5; }
/* Info tooltips */
.info-tip { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: var(--indigo-soft); color: var(--indigo); font-size: 12px; font-weight: 700; cursor: help; border: 1px solid rgba(99,102,241,0.2); transition: all var(--transition); flex-shrink: 0; }
.info-tip:hover { background: var(--indigo); color: white; transform: scale(1.1); }
.info-tip .tip-text { visibility: hidden; opacity: 0; position: absolute; z-index: 1000; top: calc(100% + 10px); left: 0; transform: translateY(-4px); width: 280px; padding: 12px 16px; background: var(--text); color: #f1f5f9; border-radius: var(--radius-sm); font-size: 12px; font-weight: 400; line-height: 1.6; box-shadow: var(--shadow-lg); transition: all 0.2s ease; pointer-events: none; }
.info-tip .tip-text::after { content: ''; position: absolute; bottom: 100%; left: 16px; border-width: 5px; border-style: solid; border-color: transparent transparent var(--text) transparent; }
.info-tip:hover .tip-text { visibility: visible; opacity: 1; transform: translateY(0); }
/* Connection status dot */
.conn-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.conn-dot.ok { background: var(--green); box-shadow: 0 0 6px rgba(16,185,129,0.4); }
.conn-dot.err { background: var(--red); box-shadow: 0 0 6px rgba(239,68,68,0.4); animation: pulse 2s infinite; }
/* ── Cards ── */
.card {
background: var(--white); border: 1px solid var(--border-light); border-radius: var(--radius);
padding: 24px; margin-bottom: 20px; box-shadow: var(--shadow);
transition: box-shadow var(--transition), transform var(--transition);
}
.card:hover { box-shadow: var(--shadow-hover); }
.card-title { font-size: 15px; font-weight: 700; margin-bottom: 4px; }
.card-sub { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
/* ── Stat Cards ── */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 28px; }
.stat-card {
background: var(--white); border: 1px solid var(--border-light); border-radius: var(--radius);
padding: 24px; box-shadow: var(--shadow); position: relative; overflow: hidden;
transition: all var(--transition); cursor: default;
}
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-hover); }
.stat-card::after {
content: ''; position: absolute; top: 0; right: 0; width: 80px; height: 80px;
border-radius: 50%; opacity: 0.06; transform: translate(20px, -20px);
}
.stat-card:nth-child(1)::after { background: var(--accent); }
.stat-card:nth-child(2)::after { background: var(--green); }
.stat-card:nth-child(3)::after { background: var(--accent2); }
.stat-card:nth-child(4)::after { background: var(--yellow); }
.stat-icon { font-size: 20px; margin-bottom: 12px; }
.stat-val { font-size: 2rem; font-weight: 800; letter-spacing: -0.02em; animation: countUp 0.5s ease; }
.stat-val.accent { color: var(--accent); }
.stat-val.green { color: var(--green); }
.stat-val.purple { color: var(--accent2); }
.stat-val.yellow { color: var(--yellow); }
.stat-label { font-size: 12px; color: var(--muted); margin-top: 4px; font-weight: 500; }
.stat-change { font-size: 11px; font-weight: 600; margin-top: 8px; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 20px; }
.stat-change.up { color: var(--green); background: var(--green-bg); }
.stat-change.down { color: var(--red); background: var(--red-bg); }
/* ── Tables ── */
.table-wrap { overflow-x: auto; border-radius: var(--radius); }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
thead { background: var(--surface2); }
th { text-align: left; padding: 12px 16px; color: var(--muted); font-weight: 600; font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; white-space: nowrap; }
td { padding: 14px 16px; border-bottom: 1px solid var(--border-light); transition: background var(--transition); }
tr:hover td { background: var(--accent-soft); }
tr:last-child td { border-bottom: none; }
/* ── Badges ── */
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.badge-active { background: var(--green-bg); color: var(--green); }
.badge-processing { background: var(--yellow-bg); color: var(--yellow); }
.badge-failed { background: var(--red-bg); color: var(--red); }
.badge-draft { background: var(--surface2); color: var(--muted); }
.badge-info { background: var(--blue-bg); color: var(--blue); }
/* ── Toast Notifications ── */
.toast-container { position: fixed; top: 24px; right: 24px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
.toast {
background: var(--white); border: 1px solid var(--border); border-radius: var(--radius-sm);
padding: 14px 20px; box-shadow: var(--shadow-lg); display: flex; align-items: center; gap: 10px;
font-size: 13px; font-weight: 500; animation: slideInLeft 0.3s ease; min-width: 280px;
transition: all 0.3s ease;
}
.toast.success { border-left: 3px solid var(--green); }
.toast.error { border-left: 3px solid var(--red); }
.toast.info { border-left: 3px solid var(--blue); }
.toast-close { margin-left: auto; cursor: pointer; color: var(--light); font-size: 16px; background: none; border: none; }
/* ── Modal ── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(15,23,42,0.4); backdrop-filter: blur(4px);
z-index: 1000; display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.2s;
}
.modal-overlay.active { opacity: 1; pointer-events: auto; }
.modal {
background: var(--white); border-radius: 20px; padding: 32px; width: 100%; max-width: 520px;
box-shadow: var(--shadow-lg); transform: scale(0.95); transition: transform 0.2s;
max-height: 90vh; overflow-y: auto;
}
.modal-overlay.active .modal { transform: scale(1); }
.modal-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 8px; }
.modal-sub { font-size: 13px; color: var(--muted); margin-bottom: 24px; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
/* ── Upload Zone ── */
.upload-zone {
border: 2px dashed var(--border); border-radius: var(--radius); padding: 48px 32px;
text-align: center; cursor: pointer; transition: all var(--transition);
background: var(--surface2);
}
.upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-soft); }
.upload-zone .upload-icon { font-size: 40px; margin-bottom: 12px; }
.upload-zone .upload-text { font-size: 14px; color: var(--muted); }
.upload-zone .upload-text strong { color: var(--accent); }
/* ── Progress Bar ── */
.progress-bar { background: var(--surface2); border-radius: 8px; height: 8px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 8px; transition: width 0.4s ease; }
/* ── Tabs ── */
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-light); margin-bottom: 24px; }
.tab {
padding: 10px 18px; font-size: 13px; font-weight: 500; color: var(--muted);
cursor: pointer; border-bottom: 2px solid transparent; transition: all var(--transition);
background: none; border-top: none; border-left: none; border-right: none;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
/* ── Search ── */
.search-bar {
display: flex; align-items: center; gap: 8px; padding: 10px 16px;
background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-sm);
transition: all var(--transition); max-width: 320px;
}
.search-bar:focus-within { border-color: var(--accent); background: var(--white); box-shadow: 0 0 0 3px var(--accent-soft); }
.search-bar input { border: none; background: none; outline: none; font-size: 13px; color: var(--text); width: 100%; font-family: inherit; }
.search-bar input::placeholder { color: var(--light); }
/* ── Toggle ── */
.toggle { position: relative; width: 44px; height: 24px; cursor: pointer; }
.toggle input { display: none; }
.toggle-track {
position: absolute; inset: 0; background: var(--border); border-radius: 12px;
transition: background var(--transition);
}
.toggle input:checked + .toggle-track { background: var(--accent); }
.toggle-thumb {
position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
background: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.15);
transition: transform var(--transition);
}
.toggle input:checked ~ .toggle-thumb { transform: translateX(20px); }
/* ── Widget Preview ── */
.widget-preview {
width: 340px; height: 480px; border-radius: 20px; overflow: hidden;
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
display: flex; flex-direction: column; background: var(--white);
}
/* ── Empty State ── */
.empty-state { text-align: center; padding: 60px 20px; }
.empty-state .empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
.empty-state .empty-title { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.empty-state .empty-sub { font-size: 13px; color: var(--muted); margin-bottom: 20px; }
/* ── Toolbar ── */
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.toolbar-spacer { flex: 1; }
/* ── Chart Container ── */
.chart-container { position: relative; height: 280px; }
/* ── Responsive ── */
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open { transform: translateX(0); }
.main { margin-left: 0; padding: 20px; }
.stat-grid { grid-template-columns: 1fr 1fr; }
}
/* ── Misc ── */
.flex { display: flex; } .flex-col { flex-direction: column; }
.items-center { align-items: center; } .justify-between { justify-content: space-between; }
.gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; }
.mt-2 { margin-top: 8px; } .mt-4 { margin-top: 16px; } .mb-4 { margin-bottom: 16px; }
.text-sm { font-size: 13px; } .text-xs { font-size: 11px; }
.text-muted { color: var(--muted); } .text-accent { color: var(--accent); }
.fw-600 { font-weight: 600; } .fw-700 { font-weight: 700; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.hidden { display: none !important; }
/* ── Platform Presence panel ── */
.ab-pp-card {
background: var(--surface2);
border: 1px solid var(--border-light);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow);
}
.ab-pp-header {
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
padding: 22px 24px;
border-bottom: 1px solid var(--border-light);
background: linear-gradient(135deg, rgba(16,185,129,0.05) 0%, transparent 60%);
}
.ab-pp-ring {
--pp-pct: 0;
width: 80px;
height: 80px;
border-radius: 50%;
background: conic-gradient(var(--green) calc(var(--pp-pct) * 1%), rgba(0,0,0,0.06) 0);
display: grid;
place-items: center;
flex-shrink: 0;
}
.ab-pp-ring-inner {
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--surface2);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.04);
}
.ab-pp-score-num {
font-size: 20px;
font-weight: 800;
color: var(--text);
letter-spacing: -0.04em;
line-height: 1;
}
.ab-pp-score-denom {
font-size: 10px;
font-weight: 600;
color: var(--muted);
margin-top: 3px;
letter-spacing: 0.04em;
}
.ab-pp-title {
font-size: 15px;
font-weight: 800;
color: var(--text);
letter-spacing: -0.01em;
margin-bottom: 4px;
}
.ab-pp-subtitle {
font-size: 12.5px;
color: var(--muted);
line-height: 1.45;
}
.ab-pp-listed {
font-size: 12px;
color: var(--green);
font-weight: 600;
margin-top: 8px;
}
.ab-pp-table-head {
display: grid;
grid-template-columns: 36px 1fr 64px 92px;
gap: 12px;
align-items: center;
padding: 10px 20px 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--light);
border-bottom: 1px solid var(--border-light);
}
.ab-pp-link {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
font-size: 11px;
font-weight: 600;
color: var(--accent);
text-decoration: none;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ab-pp-link:hover { text-decoration: underline; }
.ab-pp-link--search { color: var(--muted); font-weight: 500; }
.ab-pp-price {
font-size: 12px;
font-weight: 600;
color: var(--text2);
margin-top: 3px;
line-height: 1.3;
}
.ab-found-links {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border-light);
}
.ab-found-links-label {
width: 100%;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--light);
margin-bottom: 2px;
}
.ab-found-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
background: rgba(16,185,129,0.1);
border: 1px solid rgba(16,185,129,0.25);
color: #059669;
text-decoration: none;
transition: background 0.15s;
}
.ab-found-link:hover { background: rgba(16,185,129,0.18); }
.ab-pp-table-head span:nth-child(3),
.ab-pp-table-head span:nth-child(4) { text-align: center; }
.ab-pp-platforms {
max-height: 280px;
overflow-y: auto;
}
.ab-pp-row {
display: grid;
grid-template-columns: 36px 1fr 64px 92px;
gap: 12px;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid var(--border-light);
transition: background var(--transition);
}
.ab-pp-row:last-child { border-bottom: none; }
.ab-pp-row:hover { background: rgba(99,102,241,0.03); }
.ab-pp-icon {
font-size: 18px;
line-height: 1;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.ab-pp-info { min-width: 0; }
.ab-pp-name {
font-size: 13px;
font-weight: 600;
color: var(--text);
line-height: 1.3;
}
.ab-pp-benefit {
font-size: 11px;
color: var(--muted);
margin-top: 3px;
line-height: 1.4;
}
.ab-pp-bar-wrap {
height: 5px;
background: rgba(0,0,0,0.06);
border-radius: 99px;
overflow: hidden;
}
.ab-pp-bar {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.ab-pp-badge {
justify-self: center;
font-size: 10px;
font-weight: 700;
padding: 4px 10px;
border-radius: 99px;
white-space: nowrap;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.ab-pp-badge--verified { color: #059669; background: rgba(16,185,129,0.12); }
.ab-pp-badge--likely { color: var(--accent); background: rgba(99,102,241,0.12); }
.ab-pp-badge--missing { color: var(--muted); background: var(--surface); border: 1px solid var(--border-light); }
.ab-pp-badge--error { color: var(--red); background: var(--red-bg); }
.ab-pp-recs {
padding: 16px 20px 12px;
border-top: 1px solid var(--border-light);
}
.ab-pp-recs-title {
font-size: 10px;
font-weight: 800;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 12px;
}
.ab-pp-recs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
}
.ab-pp-rec-card {
padding: 12px 14px;
border-radius: 12px;
background: var(--surface);
border: 1px solid var(--border-light);
line-height: 1.4;
}
.ab-pp-rec-name {
font-size: 13px;
font-weight: 700;
color: var(--text);
margin-bottom: 4px;
}
.ab-pp-rec-text {
font-size: 11.5px;
color: var(--muted);
}
.ab-pp-recs-help {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--border-light);
font-size: 12px;
color: var(--light);
line-height: 1.5;
}
.ab-pp-recs-help a {
color: var(--accent);
font-weight: 600;
text-decoration: none;
}
.ab-pp-recs-help a:hover { text-decoration: underline; }
.ab-pp-footer {
padding: 12px 20px 14px;
font-size: 11px;
color: var(--muted);
border-top: 1px solid var(--border-light);
background: rgba(0,0,0,0.015);
line-height: 1.45;
}
@keyframes ab-pp-danger-pulse {
0%, 100% { box-shadow: inset 3px 0 0 var(--red), 0 0 0 0 rgba(239,68,68,0); }
50% { box-shadow: inset 3px 0 0 var(--red), 0 0 0 4px rgba(239,68,68,0.12); }
}
@keyframes ab-pp-danger-glow {
0%, 100% { border-color: rgba(239,68,68,0.45); }
50% { border-color: rgba(239,68,68,0.85); }
}
.ab-pp-listed--warn { color: var(--red); }
.ab-pp-missing-badge {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 8px;
padding: 2px 9px;
border-radius: 99px;
font-size: 11px;
font-weight: 700;
color: var(--red);
background: var(--red-bg);
border: 1px solid rgba(239,68,68,0.25);
vertical-align: middle;
animation: ab-pp-danger-glow 2.4s ease-in-out infinite;
}
.ab-pp-row--missing {
background: linear-gradient(90deg, rgba(239,68,68,0.06) 0%, transparent 55%);
animation: ab-pp-danger-pulse 2.4s ease-in-out infinite;
}
.ab-pp-row--missing:hover { background: linear-gradient(90deg, rgba(239,68,68,0.1) 0%, rgba(239,68,68,0.02) 55%); }
.ab-pp-row--missing .ab-pp-name { color: #b91c1c; }
.ab-pp-row--missing .ab-pp-bar { background: linear-gradient(90deg, #f87171, var(--red)); }
.ab-pp-row--clickable {
cursor: pointer;
outline: none;
}
.ab-pp-row--clickable:focus-visible {
box-shadow: inset 0 0 0 2px var(--red);
}
.ab-pp-badge--danger {
color: #b91c1c;
background: var(--red-bg);
border: 1px solid rgba(239,68,68,0.35);
animation: ab-pp-danger-glow 2.4s ease-in-out infinite;
}
.ab-pp-recs-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.ab-pp-recs-title-row .ab-pp-recs-title { margin-bottom: 0; color: #b91c1c; }
.ab-pp-rec-card--missing {
cursor: pointer;
border: 1.5px solid rgba(239,68,68,0.45);
background: linear-gradient(135deg, rgba(239,68,68,0.05) 0%, var(--surface) 60%);
animation: ab-pp-danger-glow 2.4s ease-in-out infinite;
transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
outline: none;
}
.ab-pp-rec-card--missing:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(239,68,68,0.15);
border-color: var(--red);
}
.ab-pp-rec-card--missing:focus-visible {
box-shadow: 0 0 0 3px rgba(239,68,68,0.25);
border-color: var(--red);
}
.ab-pp-rec-card--missing .ab-pp-rec-name { color: #b91c1c; }
.ab-pp-rec-cta {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 8px;
font-size: 11px;
font-weight: 700;
color: var(--red);
letter-spacing: 0.02em;
}
.ab-pp-modal-icon {
width: 52px;
height: 52px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
background: var(--red-bg);
border: 1px solid rgba(239,68,68,0.25);
margin-bottom: 16px;
}
.ab-pp-modal-benefit {
padding: 12px 14px;
border-radius: 12px;
background: var(--surface2);
border: 1px solid var(--border-light);
font-size: 13px;
color: var(--text2);
line-height: 1.5;
margin: 16px 0;
}
.ab-pp-modal-tip {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
margin-top: 12px;
}
.ab-pp-sticky-banner {
display: none;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin: 0 32px 0;
padding: 14px 18px;
border-radius: 12px;
background: linear-gradient(90deg, rgba(239,68,68,0.1), rgba(239,68,68,0.04));
border: 1.5px solid rgba(239,68,68,0.35);
animation: ab-pp-danger-glow 2.4s ease-in-out infinite;
}
.ab-pp-sticky-banner-text {
font-size: 13px;
font-weight: 600;
color: #b91c1c;
line-height: 1.45;
}
.ab-pp-help-card {
margin-top: 14px;
padding: 16px 18px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.02));
border: 1.5px solid rgba(99,102,241,0.25);
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
}
.ab-pp-help-card-title {
font-size: 14px;
font-weight: 700;
color: var(--text);
margin-bottom: 4px;
}
.ab-pp-help-card-sub {
font-size: 12px;
color: var(--muted);
line-height: 1.45;
}
.ab-missing-pill {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
padding: 2px 8px;
border-radius: 99px;
font-size: 10px;
font-weight: 700;
color: #b91c1c;
background: var(--red-bg);
border: 1px solid rgba(239,68,68,0.3);
vertical-align: middle;
}
.ab-pp-help-form .form-group { margin-bottom: 12px; }
.ab-pp-help-readonly {
font-size: 12px;
color: var(--muted);
padding: 8px 10px;
background: var(--surface2);
border-radius: 8px;
border: 1px solid var(--border-light);
}
.ab-pp-checklist {
margin: 10px 0 0;
padding: 10px 12px;
border-radius: 10px;
background: var(--surface2);
border: 1px solid var(--border-light);
font-size: 12px;
color: var(--text2);
line-height: 1.5;
}
.ab-pp-checklist ul { margin: 6px 0 0 18px; padding: 0; }
.ab-gaps-card {
border: 1.5px solid rgba(239,68,68,0.3);
background: linear-gradient(135deg, rgba(239,68,68,0.06), transparent);
}
.ab-gaps-card .stat-val { color: #b91c1c !important; }
/* Right-edge support tab — brand accent, restrained */
.ab-contact-tab {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 850;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 18px 11px;
border: none;
border-radius: 8px 0 0 8px;
background: var(--accent);
color: #fff;
font-family: inherit;
cursor: pointer;
box-shadow: -2px 0 12px rgba(99, 102, 241, 0.35);
transition: background var(--transition), box-shadow var(--transition), transform var(--transition);
}
.ab-contact-tab:hover {
background: var(--accent-hover);
transform: translateY(-50%) translateX(-3px);
box-shadow: -4px 0 18px rgba(99, 102, 241, 0.45);
}
.ab-contact-tab:focus-visible {
outline: none;
box-shadow: -4px 0 18px rgba(99, 102, 241, 0.45), 0 0 0 3px var(--accent-soft);
}
.ab-contact-tab-icon {
width: 15px;
height: 15px;
stroke: currentColor;
fill: none;
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
}
.ab-contact-tab-text {
writing-mode: vertical-rl;
text-orientation: mixed;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
white-space: nowrap;
}
</style>
</head>
<body>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<!-- Modal Overlay -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal-content"></div>
</div>
<!-- AUTH SCREEN -->
<div class="auth-wrap" id="auth-screen">
<div class="auth-box">
<div class="auth-logo">AuthorBot</div>
<p class="auth-sub">Welcome back! Sign in to manage your AI-powered book chatbot.</p>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="email" placeholder="you@example.com" autocomplete="email">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" class="form-input" id="password" placeholder="••••••••" autocomplete="current-password">
</div>
<button class="btn btn-primary btn-block" id="login-btn">Sign In</button>
<p class="msg" id="login-msg" style="margin-top:12px;font-size:13px;min-height:18px;"></p>
</div>
</div>
<!-- DASHBOARD -->
<div id="dashboard" style="display:none;">
<nav class="sidebar" id="sidebar">
<div class="sidebar-logo">AuthorBot</div>
<div class="sidebar-sub">Author Admin Panel</div>
<div class="nav-section">Main</div>
<button class="nav-item active" data-page="overview"><span class="nav-icon">📊</span> Dashboard</button>
<button class="nav-item" data-page="books"><span class="nav-icon">📚</span> Books</button>
<button class="nav-item" data-page="upload"><span class="nav-icon">⬆️</span> Upload Book</button>
<button class="nav-item" data-page="readers"><span class="nav-icon">👥</span> Readers</button>
<button class="nav-item" data-page="conversations"><span class="nav-icon">💭</span> Conversations</button>
<button class="nav-item" data-page="qa"><span class="nav-icon"></span> Custom Q&A</button>
<div class="nav-section">Analytics</div>
<button class="nav-item" data-page="analytics"><span class="nav-icon">📈</span> Analytics</button>
<button class="nav-item" data-page="tokens"><span class="nav-icon">🪙</span> Token Usage</button>
<button class="nav-item" data-page="exports"><span class="nav-icon">📥</span> Export Data</button>
<div class="nav-section">Configuration</div>
<button class="nav-item" data-page="widget"><span class="nav-icon">💬</span> Widget Config</button>
<button class="nav-item" data-page="embed"><span class="nav-icon">🔗</span> Embed Code</button>
<button class="nav-item" data-page="links"><span class="nav-icon">🛒</span> Smart Links</button>
<button class="nav-item" data-page="personality"><span class="nav-icon">🤖</span> Bot Personality</button>
<div class="nav-section">Account</div>
<button class="nav-item" data-page="billing"><span class="nav-icon">💳</span> Billing</button>
<button class="nav-item" data-page="profile"><span class="nav-icon">👤</span> Profile</button>
<button class="nav-item" data-page="notifications"><span class="nav-icon">🔔</span> Notifications</button>
<button class="nav-item" data-page="security"><span class="nav-icon">🔒</span> Security</button>
<div class="sidebar-footer">
<div class="user-card">
<div class="user-avatar" id="user-initials">A</div>
<div>
<div class="user-name" id="user-display-name">Author</div>
<div class="user-email" id="user-display-email"></div>
</div>
</div>
<button class="nav-item" id="logout-btn" style="color:var(--red);">
<span class="nav-icon">🚪</span> Sign Out
</button>
</div>
</nav>
<main class="main">
<!-- ═══ DASHBOARD ═══ -->
<div class="page active" id="page-overview">
<div class="page-header"><h1 class="page-title">Dashboard <span class="info-tip"><span class="tip-text">Your real-time command center. View chat sessions, book counts, and engagement metrics. Data refreshes automatically when you navigate here.</span></span></h1><p class="page-sub">Your AuthorBot at a glance — real-time overview of your chatbot performance</p></div>
<div class="stat-grid" id="stat-grid">
<div class="stat-card animate-in"><div class="stat-icon">📚</div><div class="stat-val accent" id="stat-books"></div><div class="stat-label">Books Indexed</div></div>
<div class="stat-card animate-in-delay"><div class="stat-icon">💬</div><div class="stat-val green" id="stat-sessions"></div><div class="stat-label">Total Conversations</div></div>
<div class="stat-card animate-in-delay"><div class="stat-icon"></div><div class="stat-val purple" id="stat-active"></div><div class="stat-label">Active Books</div></div>
<div class="stat-card animate-in-delay"><div class="stat-icon">🔥</div><div class="stat-val yellow" id="stat-clicks"></div><div class="stat-label">Link Clicks</div></div>
<div class="stat-card animate-in-delay ab-gaps-card" id="stat-gaps-card" style="display:none;"><div class="stat-icon">⚠️</div><div class="stat-val" id="stat-gaps"></div><div class="stat-label">Distribution Gaps</div></div>
</div>
<div class="grid-2">
<div class="card"><div class="card-title">Sessions — Last 30 Days</div><div class="card-sub">Daily chat activity trend</div><div class="chart-container"><canvas id="chart-sessions"></canvas></div></div>
<div class="card" id="dashboard-gaps-card" style="display:none;">
<div class="card-title">Distribution Gaps</div>
<div class="card-sub" id="dashboard-gaps-sub">Books missing retail listings</div>
<div style="padding:16px 0 8px;font-size:14px;color:var(--text2);line-height:1.5;" id="dashboard-gaps-detail"></div>
<button class="btn btn-primary btn-sm" onclick="nav('books')">View Books with Gaps →</button>
</div>
<div class="card"><div class="card-title">Quick Actions</div><div class="card-sub">Common tasks at a glance</div>
<div class="flex flex-col gap-3 mt-2">
<button class="btn btn-secondary" onclick="nav('upload')">⬆️ Upload a New Book</button>
<button class="btn btn-secondary" onclick="nav('widget')">💬 Configure Widget</button>
<button class="btn btn-secondary" onclick="nav('analytics')">📈 View Analytics</button>
<button class="btn btn-secondary" onclick="nav('embed')">🔗 Get Embed Code</button>
</div>
</div>
</div>
</div>
<!-- ═══ BOOKS ═══ -->
<div class="page" id="page-books">
<div class="page-header"><h1 class="page-title">Books <span class="info-tip"><span class="tip-text">Your indexed book library. Each book is split into chunks and embedded in ChromaDB for RAG. Click a book to edit details or re-index it. Use the search bar to filter.</span></span></h1><p class="page-sub">Manage your book catalog — upload, edit, and organize</p></div>
<div class="toolbar">
<div class="search-bar"><span>🔍</span><input type="text" placeholder="Search books..." id="book-search" oninput="filterBooks()"></div>
<div class="toolbar-spacer"></div>
<button class="btn btn-primary btn-sm" onclick="nav('upload')">+ Upload Book</button>
</div>
<div class="card" style="padding:0;"><div class="table-wrap"><table><thead><tr><th style="width:56px;">Cover</th><th>Book</th><th>Genre</th><th>Status</th><th>Chunks</th><th>Summary</th><th>Actions</th></tr></thead><tbody id="books-body"><tr><td colspan="7" class="text-muted" style="text-align:center;padding:40px;">Loading...</td></tr></tbody></table></div></div>
</div>
<!-- ═══ UPLOAD BOOK (Add Book) ═══ -->
<div class="page" id="page-upload">
<div class="page-header" style="margin-bottom: 32px;">
<h1 class="page-title" style="font-size: 28px; font-weight: 800; letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px;">
Add Book
<span class="info-tip" style="position: relative; top: -2px;"><span class="tip-text">Import a book from any major bookstore URL — we auto-fetch title, author, cover, genre, description, ratings, and author bio. Or upload a PDF/EPUB directly.</span></span>
</h1>
<p class="page-sub" style="font-size: 15px; color: var(--muted); margin-top: 6px;">Import by URL from Amazon, Goodreads, Google Books &amp; 7 more — or upload a file directly.</p>
</div>
<!-- Tab switcher -->
<div style="display:inline-flex; padding:6px; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(12px); border: 1px solid var(--border-light); border-radius: 14px; margin-bottom: 36px; box-shadow: 0 4px 20px rgba(0,0,0,0.03);">
<button id="ab-tab-url" onclick="abSwitchTab('url')" style="padding: 10px 24px; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; box-shadow: 0 4px 15px rgba(99,102,241,0.3); font-family: inherit; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; gap: 8px;">
🔗 Import by URL
<span style="font-size:10px; background: rgba(255,255,255,0.2); color: #fff; padding: 3px 8px; border-radius: 20px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;">Recommended</span>
</button>
<button id="ab-tab-file" onclick="abSwitchTab('file')" style="padding: 10px 24px; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; background: transparent; color: var(--muted); font-family: inherit; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; gap: 8px;">
📄 Manual Upload
</button>
</div>
<!-- ─── URL IMPORT TAB ─── -->
<div id="ab-url-tab">
<div style="display:grid; grid-template-columns: 1fr 380px; gap: 32px; align-items: start;">
<!-- Left: Import card -->
<div class="card" style="padding: 0; overflow: hidden; border-radius: 20px; background: var(--surface); box-shadow: 0 12px 40px rgba(0,0,0,0.06); border: 1px solid var(--border-light);">
<!-- Platform pills -->
<div style="padding: 24px 32px 16px; border-bottom: 1px solid var(--border-light); background: rgba(255, 255, 255, 0.02);">
<div style="font-size: 12px; font-weight: 700; color: var(--light); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 14px; display: flex; align-items: center; gap: 6px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Supported Platforms
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;" id="ab-platform-pills">
<!-- injected by abBuildPills() -->
</div>
</div>
<!-- URL row -->
<div style="padding: 28px 32px 32px;">
<!-- Alert -->
<div id="ab-alert" style="display:none; align-items: flex-start; gap: 12px; padding: 14px 18px; border-radius: 12px; margin-bottom: 20px; font-size: 13.5px; font-weight: 500;"></div>
<div style="display: flex; gap: 12px; align-items: stretch;">
<div id="ab-url-field" style="flex: 1; display: flex; align-items: center; gap: 12px; padding: 0 18px; background: var(--surface2); border: 2px solid var(--border); border-radius: 12px; transition: all 0.25s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--muted); flex-shrink: 0;"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
<input type="url" id="ab-url" placeholder="Paste URL (e.g., https://amazon.com/dp/...)" autocomplete="off" spellcheck="false"
style="flex: 1; background: transparent; border: none; outline: none; color: var(--text); font-size: 15px; font-family: inherit; height: 52px;"
oninput="abDetectPlatform()"
onfocus="this.parentElement.style.borderColor='var(--accent)'; this.parentElement.style.boxShadow='0 0 0 4px rgba(99,102,241,0.15)'; this.parentElement.style.background='var(--surface)'"
onblur="this.parentElement.style.borderColor='var(--border)'; this.parentElement.style.boxShadow=''; this.parentElement.style.background='var(--surface2)'">
</div>
<button class="btn btn-primary" id="ab-fetch-btn" onclick="abFetchPreview()" style="white-space: nowrap; padding: 0 24px; height: 56px; border-radius: 12px; font-size: 14.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; box-shadow: 0 6px 20px rgba(99,102,241,0.25); transition: all 0.2s;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
Fetch Details
</button>
</div>
</div>
<!-- Loader -->
<div id="ab-loader" style="display:none; align-items: center; gap: 18px; padding: 0 32px 32px;">
<div style="width: 28px; height: 28px; border: 3px solid rgba(99,102,241,0.15); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; flex-shrink: 0;"></div>
<div>
<p id="ab-loader-msg" style="font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 4px;">Detecting platform…</p>
<p style="font-size: 12.5px; color: var(--accent); font-weight: 500; letter-spacing: 0.02em;">API-first · Advanced Extraction</p>
</div>
</div>
<!-- Preview card -->
<div id="ab-preview" style="display:none; padding: 0 32px 32px;">
<div style="background: var(--surface2); border: 1px solid var(--border-light); border-radius: 16px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.02);">
<!-- Platform + confidence row -->
<div style="display: flex; align-items: center; gap: 10px; padding: 12px 18px; background: linear-gradient(90deg, rgba(99,102,241,0.08), transparent); border-bottom: 1px solid var(--border-light);">
<span id="ab-p-platform" style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.2); color: var(--accent); font-size: 12px; font-weight: 700; letter-spacing: 0.03em;">Platform</span>
<div style="margin-left: auto; display: flex; align-items: center; gap: 10px;">
<div style="width: 80px; height: 6px; border-radius: 4px; background: rgba(0,0,0,0.06); overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);"><div id="ab-conf-fill" style="height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--accent), var(--green)); transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); width: 0%"></div></div>
<span id="ab-conf-label" style="font-size: 12px; font-weight: 600; color: var(--muted);">0% match</span>
</div>
</div>
<!-- Book body -->
<div style="display: flex; gap: 20px; padding: 20px;">
<div id="ab-cover" style="width: 90px; min-width: 90px; height: 130px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); display: flex; align-items: center; justify-content: center; font-size: 32px; overflow: hidden; flex-shrink: 0; box-shadow: 0 6px 16px rgba(0,0,0,0.08); transition: transform 0.2s;">📖</div>
<div style="flex: 1; min-width: 0;">
<div id="ab-title-disp" style="font-size: 18px; font-weight: 800; line-height: 1.25; margin-bottom: 6px; color: var(--text);"></div>
<div id="ab-author-disp" style="font-size: 14px; font-weight: 500; color: var(--accent); margin-bottom: 12px;"></div>
<div id="ab-desc-disp" style="font-size: 13.5px; color: var(--muted); line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 14px;"></div>
<div id="ab-chips" style="display: flex; flex-wrap: wrap; gap: 6px;"></div>
<div id="ab-found-links" class="ab-found-links" style="display:none;"></div>
</div>
</div>
<!-- Author bio -->
<div id="ab-bio-panel" style="display:none; margin: 0 20px 16px; padding: 14px 18px; border-radius: 12px; background: linear-gradient(145deg, rgba(99,102,241,0.05), rgba(99,102,241,0.02)); border: 1px solid rgba(99,102,241,0.1); border-left: 4px solid var(--accent);">
<div style="font-size: 11px; font-weight: 800; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
About the Author
</div>
<div id="ab-bio-text" style="font-size: 13px; color: var(--muted); line-height: 1.6; max-height: 100px; overflow-y: auto; padding-right: 4px;"></div>
</div>
<!-- Edit toggle -->
<div id="ab-edit-toggle" onclick="abToggleEdit()" style="display: flex; align-items: center; gap: 8px; padding: 12px 20px; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--light); background: rgba(0,0,0,0.02); border-top: 1px solid var(--border-light); transition: all 0.2s;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edit / Correct Details
<span id="ab-chevron" style="margin-left: auto; transition: transform 0.2s; color: var(--muted);"></span>
</div>
<!-- Edit form -->
<div id="ab-edit-form" style="display:none; padding: 0 20px 20px;">
<div class="grid-2" style="margin-top: 16px; gap: 16px;">
<div class="form-group" style="grid-column: 1 / -1;"><label class="form-label">Title</label><input type="text" class="form-input" id="ab-e-title" style="border-radius: 8px;"></div>
<div class="form-group"><label class="form-label">Author</label><input type="text" class="form-input" id="ab-e-author" style="border-radius: 8px;"></div>
<div class="form-group"><label class="form-label">Genre</label><input type="text" class="form-input" id="ab-e-genre" style="border-radius: 8px;"></div>
<div class="form-group"><label class="form-label">ISBN</label><input type="text" class="form-input" id="ab-e-isbn" style="border-radius: 8px;"></div>
<div class="form-group"><label class="form-label">Cover URL</label><input type="text" class="form-input" id="ab-e-cover" style="border-radius: 8px;"></div>
<div class="form-group" style="grid-column: 1 / -1;"><label class="form-label">Description</label><textarea class="form-input" id="ab-e-desc" rows="3" style="border-radius: 8px; resize: vertical;"></textarea></div>
<div class="form-group" style="grid-column: 1 / -1;"><label class="form-label">About the Author</label><textarea class="form-input" id="ab-e-bio" rows="2" style="border-radius: 8px; resize: vertical;"></textarea></div>
</div>
</div>
</div>
</div>
<!-- Platform Presence panel -->
<div id="ab-platform-panel" style="display:none; padding: 0 32px 24px;">
<div class="ab-pp-card">
<div class="ab-pp-header">
<div class="ab-pp-ring" id="ab-pp-ring">
<div class="ab-pp-ring-inner">
<span class="ab-pp-score-num" id="ab-pp-score-num"></span>
<span class="ab-pp-score-denom" id="ab-pp-score-denom">/ 10</span>
</div>
</div>
<div>
<div class="ab-pp-title">Platform Presence</div>
<div class="ab-pp-subtitle">Distribution coverage across top retail &amp; discovery channels</div>
<div id="ab-pp-distribution-note" class="text-xs" style="display:none;margin-top:8px;padding:10px 12px;border-radius:8px;background:rgba(99,102,241,0.08);color:var(--muted);line-height:1.45;"></div>
<div class="ab-pp-listed" id="ab-pp-listed"></div>
<span class="ab-pp-missing-badge" id="ab-pp-header-missing-badge" style="display:none; margin-top:8px; margin-left:0;"></span>
<button type="button" class="btn btn-primary btn-sm" id="ab-pp-bulk-help-btn" style="display:none;margin-top:10px;" onclick="abPpRequestAllMissing()">Request help for all missing</button>
</div>
</div>
<div class="ab-pp-table-head">
<span></span>
<span>Platform</span>
<span>Match</span>
<span>Status</span>
</div>
<div class="ab-pp-platforms" id="ab-pp-platforms"></div>
<div class="ab-pp-recs" id="ab-pp-recs" style="display:none;">
<div class="ab-pp-recs-title-row">
<div class="ab-pp-recs-title">Not published yet — tap to fix</div>
<span class="ab-pp-missing-badge" id="ab-pp-missing-badge" style="display:none;"></span>
</div>
<div class="ab-pp-recs-grid" id="ab-pp-rec-cards"></div>
<div class="ab-pp-help-card" id="ab-pp-recs-help">
<div>
<div class="ab-pp-help-card-title">🤝 Get expert help publishing</div>
<div class="ab-pp-help-card-sub">We'll help you list on missing platforms — reply within 24h.</div>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="abPpRequestAllMissing()">Request Help</button>
</div>
</div>
</div>
</div>
<!-- Sticky missing-platforms banner -->
<div id="ab-pp-sticky-banner" class="ab-pp-sticky-banner" role="status" aria-live="polite">
<div class="ab-pp-sticky-banner-text" id="ab-pp-sticky-text"></div>
<button type="button" class="btn btn-primary btn-sm" onclick="abPpRequestAllMissing()">Request Help</button>
</div>
<!-- Action row -->
<div id="ab-actions" style="display:none; padding: 20px 32px 32px; justify-content: flex-end; gap: 12px; border-top: 1px solid var(--border-light); background: rgba(0,0,0,0.01);">
<button class="btn btn-secondary" onclick="abReset()" style="padding: 10px 20px; font-weight: 600; border-radius: 10px;">← Back</button>
<button class="btn btn-primary" id="ab-import-btn" onclick="abConfirmImport()" style="padding: 10px 24px; font-weight: 600; border-radius: 10px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 15px rgba(99,102,241,0.3);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
Add to My Books
</button>
</div>
<!-- Success screen -->
<div id="ab-success" style="display:none; flex-direction: column; align-items: center; text-align: center; padding: 48px 32px; gap: 20px;">
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(16,185,129,0.05)); border: 3px solid rgba(16,185,129,0.3); display: flex; align-items: center; justify-content: center; font-size: 36px; box-shadow: 0 8px 25px rgba(16,185,129,0.2);"></div>
<div>
<div style="font-size: 22px; font-weight: 800; color: var(--text); margin-bottom: 6px;">Book Added Successfully!</div>
<div style="font-size: 14px; color: var(--muted);">Your book is now being processed in the background.</div>
</div>
<div id="ab-success-book" style="background: var(--surface2); border: 1px solid var(--border); border-radius: 12px; padding: 14px 24px; font-weight: 600; font-size: 15px; width: 100%; max-width: 360px; color: var(--accent); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);"></div>
<div id="ab-success-gaps" style="display:none; width:100%; max-width:420px; padding:16px 18px; border-radius:12px; background:linear-gradient(135deg,rgba(239,68,68,0.08),transparent); border:1.5px solid rgba(239,68,68,0.3); text-align:left;">
<div style="font-size:14px;font-weight:700;color:#b91c1c;margin-bottom:6px;" id="ab-success-gaps-title"></div>
<div style="font-size:12.5px;color:var(--muted);line-height:1.5;margin-bottom:12px;" id="ab-success-gaps-detail"></div>
<button type="button" class="btn btn-primary btn-sm" onclick="abSuccessRequestHelp()">Request publishing help</button>
</div>
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 16px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); border-radius: 12px; font-size: 13px; color: var(--light); max-width: 400px; text-align: left; line-height: 1.5;">
<div style="font-size: 18px; line-height: 1;">💡</div>
<div>Want AI to read it? Upload a PDF or EPUB for this book on the <strong>Manual Upload</strong> tab to enable the chat widget.</div>
</div>
<div style="display: flex; gap: 12px; margin-top: 10px;">
<button class="btn btn-secondary" onclick="abReset()" style="padding: 10px 24px; border-radius: 10px; font-weight: 600;">Import Another</button>
<button class="btn btn-primary" onclick="nav('books')" style="padding: 10px 24px; border-radius: 10px; font-weight: 600; box-shadow: 0 4px 15px rgba(99,102,241,0.3);">Go to My Books</button>
</div>
</div>
</div>
<!-- Right: My Books mini list -->
<div class="card" style="padding: 0; overflow: hidden; border-radius: 20px; background: var(--surface); box-shadow: 0 12px 40px rgba(0,0,0,0.06); border: 1px solid var(--border-light); display: flex; flex-direction: column; height: 100%; max-height: 600px;">
<div style="padding: 20px 24px; border-bottom: 1px solid var(--border-light); background: rgba(255, 255, 255, 0.02); display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="color: var(--accent);"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
My Books
</span>
<button class="btn btn-secondary btn-sm" onclick="abLoadSidebar()" style="padding: 6px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface2); color: var(--light); transition: all 0.2s;" onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background='var(--surface2)'">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
</button>
</div>
<div id="ab-books-sidebar" style="flex: 1; overflow-y: auto; padding: 8px 12px;">
<div style="padding: 40px 20px; text-align: center; color: var(--muted); font-size: 14px; font-weight: 500;">
<div style="width: 24px; height: 24px; border: 2px solid rgba(99,102,241,0.2); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 12px;"></div>
Loading Library…
</div>
</div>
</div>
</div>
</div>
<!-- ─── MANUAL UPLOAD TAB ─── -->
<div id="ab-file-tab" style="display:none; animation: fadeIn 0.3s ease;">
<div class="grid-2" style="gap: 32px; align-items: start;">
<div>
<div class="card" style="border-radius: 20px; padding: 32px; box-shadow: 0 12px 40px rgba(0,0,0,0.06); border: 1px solid var(--border-light);">
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()" style="border: 2px dashed var(--border); border-radius: 16px; padding: 48px 24px; text-align: center; cursor: pointer; background: var(--surface2); transition: all 0.25s ease;" onmouseover="this.style.borderColor='var(--accent)'; this.style.background='rgba(99,102,241,0.04)'" onmouseout="this.style.borderColor='var(--border)'; this.style.background='var(--surface2)'">
<div class="upload-icon" style="font-size: 48px; margin-bottom: 16px; transform: translateY(0); transition: transform 0.2s ease;">📄</div>
<div class="upload-text" style="font-size: 16px; font-weight: 500; color: var(--text); margin-bottom: 8px;">Drag &amp; drop your book file here, or <strong style="color: var(--accent);">click to browse</strong></div>
<div class="text-xs text-muted mt-2" style="font-size: 13px; font-weight: 500;">PDF, EPUB, DOCX, TXT — Max 50MB</div>
</div>
<input type="file" id="file-input" accept=".pdf,.epub,.docx,.txt" style="display:none">
<div id="upload-file-info" class="hidden mt-4" style="margin-top: 24px; padding: 16px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); border-radius: 12px; font-size: 14px; font-weight: 600; color: var(--accent); display: flex; align-items: center; gap: 10px;"></div>
<div id="upload-progress-wrap" class="hidden mt-4" style="margin-top: 24px;">
<div class="flex items-center justify-between mb-4" style="margin-bottom: 12px;">
<span class="text-sm fw-600" id="upload-stage" style="font-weight: 700; color: var(--text);">Preparing...</span>
<span class="text-xs text-muted" id="upload-pct" style="font-weight: 600; color: var(--accent);">0%</span>
</div>
<div class="progress-bar" style="height: 8px; border-radius: 4px; background: var(--border); overflow: hidden;">
<div class="progress-fill" id="upload-fill" style="height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--accent), var(--accent2)); transition: width 0.3s ease; width: 0%"></div>
</div>
</div>
</div>
</div>
<div>
<div class="card" style="border-radius: 20px; padding: 32px; box-shadow: 0 12px 40px rgba(0,0,0,0.06); border: 1px solid var(--border-light);">
<div class="card-title" style="font-size: 20px; font-weight: 800; margin-bottom: 6px; color: var(--text);">Book Details</div>
<div class="card-sub" style="font-size: 14px; color: var(--muted); margin-bottom: 24px; line-height: 1.5;">Provide metadata for your book (optional — can be edited later)</div>
<div style="display: flex; flex-direction: column; gap: 20px;">
<div class="form-group" style="margin: 0;">
<label class="form-label" style="font-size: 13px; font-weight: 700; color: var(--light); margin-bottom: 8px; display: block;">Title</label>
<input type="text" class="form-input" id="book-title" placeholder="My Awesome Book" style="height: 48px; border-radius: 10px; font-size: 14.5px;">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label" style="font-size: 13px; font-weight: 700; color: var(--light); margin-bottom: 8px; display: block;">Genre</label>
<input type="text" class="form-input" id="book-genre" placeholder="e.g. Fiction, Self-Help, Sci-Fi" style="height: 48px; border-radius: 10px; font-size: 14.5px;">
</div>
<div class="grid-2" style="gap: 16px;">
<div class="form-group" style="margin: 0;">
<label class="form-label" style="font-size: 13px; font-weight: 700; color: var(--light); margin-bottom: 8px; display: block;">Price ($)</label>
<input type="number" class="form-input" id="book-price" placeholder="9.99" step="0.01" style="height: 48px; border-radius: 10px; font-size: 14.5px;">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label" style="font-size: 13px; font-weight: 700; color: var(--light); margin-bottom: 8px; display: block;">Buy URL</label>
<input type="url" class="form-input" id="book-buy-url" placeholder="https://amazon.com/..." style="height: 48px; border-radius: 10px; font-size: 14.5px;">
</div>
</div>
<button class="btn btn-primary btn-block" id="upload-btn" disabled style="margin-top: 12px; height: 52px; font-size: 15px; font-weight: 700; border-radius: 12px; box-shadow: 0 6px 20px rgba(99,102,241,0.25); transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Upload &amp; Index
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ READERS ═══ -->
<div class="page" id="page-readers">
<div class="page-header"><h1 class="page-title">Readers <span class="info-tip"><span class="tip-text">Browse all reader chat sessions. Each row shows a reader's conversation with your bot, including message count and last activity. Click to view the full transcript.</span></span></h1><p class="page-sub">View and manage reader chat sessions</p></div>
<div class="toolbar">
<div class="search-bar"><span>🔍</span><input type="text" placeholder="Search by session..." id="reader-search" oninput="filterSessions()"></div>
<div class="toolbar-spacer"></div>
<span class="text-xs text-muted" id="session-count">— sessions</span>
</div>
<div class="card" style="padding:0;"><div class="table-wrap"><table><thead><tr><th>Session</th><th>Country</th><th>Device</th><th>Rating</th><th>Date</th><th>Status</th><th>Actions</th></tr></thead><tbody id="sessions-body"><tr><td colspan="7" class="text-muted" style="text-align:center;padding:40px;">Loading...</td></tr></tbody></table></div></div>
</div>
<!-- ═══ ANALYTICS ═══ -->
<div class="page" id="page-analytics">
<div class="page-header"><h1 class="page-title">Analytics <span class="info-tip"><span class="tip-text">Performance insights across all your books. Charts show sessions over time, most popular books, and engagement trends. Data is aggregated from all reader interactions.</span></span></h1><p class="page-sub">Deep dive into your chatbot performance metrics</p></div>
<div class="toolbar">
<div class="tabs" id="analytics-tabs">
<button class="tab active" data-days="7">7 Days</button>
<button class="tab" data-days="30">30 Days</button>
<button class="tab" data-days="90">90 Days</button>
<button class="tab" data-days="365">1 Year</button>
</div>
</div>
<div class="stat-grid" id="analytics-stats">
<div class="stat-card"><div class="stat-icon">💬</div><div class="stat-val accent" id="a-total-sessions"></div><div class="stat-label">Total Sessions</div></div>
<div class="stat-card"><div class="stat-icon">👥</div><div class="stat-val green" id="a-unique-visitors"></div><div class="stat-label">Unique Visitors</div></div>
<div class="stat-card"><div class="stat-icon">🔗</div><div class="stat-val purple" id="a-link-clicks"></div><div class="stat-label">Link Clicks</div></div>
<div class="stat-card"><div class="stat-icon"></div><div class="stat-val yellow" id="a-avg-turns"></div><div class="stat-label">Avg Turns/Session</div></div>
</div>
<div class="card"><div class="card-title">Sessions Over Time</div><div class="chart-container"><canvas id="chart-analytics"></canvas></div></div>
</div>
<!-- ═══ TOKEN USAGE ═══ -->
<div class="page" id="page-tokens">
<div class="page-header"><h1 class="page-title">Token Usage <span class="info-tip"><span class="tip-text">Tracks your AI token consumption. Each chatbot response uses tokens from your budget. When your budget is exhausted, the chatbot will show a friendly "recharging" message until tokens are replenished.</span></span></h1><p class="page-sub">Monitor your AI token budget and consumption</p></div>
<div id="token-subscription-banner" class="hidden" style="margin-bottom:16px;padding:14px 18px;border-radius:10px;background:var(--yellow-bg);border:1px solid var(--yellow);font-size:13px;color:var(--text2);">
⚠️ <strong>No active subscription.</strong> Contact your platform administrator to get a package before using the chatbot.
</div>
<div class="grid-2">
<div class="card" style="text-align:center;padding:40px;">
<div style="position:relative;width:180px;height:180px;margin:0 auto 20px;">
<canvas id="chart-token-gauge" width="180" height="180"></canvas>
<div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
<div style="font-size:2rem;font-weight:800;color:var(--accent);" id="token-pct">—%</div>
<div class="text-xs text-muted">used</div>
</div>
</div>
<div class="text-sm text-muted">Budget: <strong id="token-budget"></strong> tokens</div>
<div class="text-xs text-muted mt-2">Used: <span id="token-used"></span> | Remaining: <span id="token-remaining"></span></div>
</div>
<div class="card"><div class="card-title">Daily Token Consumption</div><div class="chart-container"><canvas id="chart-token-daily"></canvas></div></div>
</div>
</div>
<!-- ═══ WIDGET CONFIG ═══ -->
<div class="page" id="page-widget">
<div class="page-header"><h1 class="page-title">Widget Config <span class="info-tip"><span class="tip-text">Customize your chat widget's appearance: theme color, position, welcome message, and auto-open behavior. Changes save automatically and take effect immediately on your website.</span></span></h1><p class="page-sub">Customize how your chat widget looks on your website</p></div>
<div class="grid-2">
<div>
<div class="card">
<div class="card-title">Appearance</div>
<div class="form-group"><label class="form-label">Bot Name</label><input type="text" class="form-input" id="w-bot-name" placeholder="BookBot" oninput="updateWidgetPreview()"></div>
<div class="form-group"><label class="form-label">Welcome Message</label><textarea class="form-input" id="w-welcome" placeholder="Hi! I'm here to help..." oninput="updateWidgetPreview()"></textarea></div>
<div class="form-group"><label class="form-label">Theme</label>
<select class="form-input" id="w-theme" onchange="updateWidgetPreview()">
<option value="pearl">Pearl (Light)</option><option value="midnight">Midnight (Dark)</option><option value="ocean">Ocean</option>
<option value="forest">Forest</option><option value="sunset">Sunset</option><option value="lavender">Lavender</option>
</select>
</div>
<div class="form-group"><label class="form-label">Position</label>
<select class="form-input" id="w-position" onchange="updateWidgetPreview()">
<option value="bottom-right">Bottom Right</option><option value="bottom-left">Bottom Left</option>
</select>
</div>
<div class="form-group"><label class="form-label">Auto-Open Delay (seconds, 0 = off)</label><input type="number" class="form-input" id="w-delay" value="0" min="0" max="60"></div>
<button class="btn btn-primary btn-block" id="save-widget-btn">Save Configuration</button>
</div>
</div>
<div>
<div class="card"><div class="card-title">Live Preview</div><div class="card-sub">See how your widget will appear</div>
<div class="widget-preview" id="widget-preview">
<div style="background:var(--accent);color:white;padding:16px;display:flex;align-items:center;gap:10px;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(255,255,255,0.2);display:flex;align-items:center;justify-content:center;">🤖</div>
<div><div style="font-weight:600;font-size:14px;" id="wp-name">BookBot</div><div style="font-size:11px;opacity:0.8;">Online</div></div>
</div>
<div style="flex:1;padding:16px;overflow-y:auto;background:var(--bg);">
<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:10px 14px;font-size:13px;max-width:80%;color:var(--text);" id="wp-msg">Hi! How can I help you today?</div>
</div>
<div style="padding:12px;border-top:1px solid var(--border);display:flex;gap:8px;">
<input disabled style="flex:1;border:1px solid var(--border);border-radius:20px;padding:8px 14px;font-size:12px;background:var(--surface2);color:var(--muted);" placeholder="Ask me anything...">
<div style="width:32px;height:32px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;color:white;font-size:14px;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ EMBED CODE ═══ -->
<div class="page" id="page-embed">
<div class="page-header">
<h1 class="page-title">Add Chatbot to Your Website</h1>
<p class="page-sub">Copy one line, paste it in your website footer — your chatbot goes live instantly</p>
</div>
<!-- Live preview notice -->
<div class="card" style="border:2px solid var(--green);margin-bottom:16px;background:linear-gradient(135deg,rgba(34,197,94,0.08),transparent);">
<div class="card-title" style="color:var(--green);">✅ See it working right now</div>
<div class="card-sub">Look at the <strong>💬 chat bubble in the bottom corner of this page</strong>. That is exactly what your readers will see on your website.</div>
<div id="embed-live-status" style="margin-top:12px;padding:12px 16px;background:var(--surface2);border-radius:8px;font-size:13px;">
<span style="color:var(--muted);">🔄 Loading your chatbot preview...</span>
</div>
</div>
<!-- Step 1: Copy code -->
<div class="card" style="margin-bottom:16px;">
<div class="card-title">Step 1 — Copy this one line</div>
<div class="card-sub">Click the copy button. Your secure access token is already included — nothing else to configure.</div>
<div id="embed-token-status" style="margin-top:14px;padding:12px 16px;background:var(--surface2);border-radius:8px;font-size:13px;">
<span style="color:var(--muted);">🔄 Checking subscription...</span>
</div>
<div style="position:relative;margin-top:14px;">
<pre style="background:#0d1117;color:#e6edf3;border:1px solid var(--border);border-radius:10px;padding:20px;font-size:12px;overflow-x:auto;line-height:1.7;white-space:pre-wrap;word-break:break-all;" id="embed-code">⏳ Preparing your embed code...</pre>
<button class="btn btn-primary btn-sm" style="position:absolute;top:12px;right:12px;" onclick="copyEmbedCode()">📋 Copy Code</button>
</div>
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<label style="font-size:13px;font-weight:600;color:var(--muted);">Theme:</label>
<select class="form-input" style="width:auto;margin:0;padding:4px 10px;font-size:13px;" id="embed-theme" onchange="generateEmbedCode()">
<option value="midnight">Midnight (Dark)</option>
<option value="minimal">Pearl (Light)</option>
<option value="ocean">Ocean</option>
<option value="forest">Forest</option>
<option value="sunset">Sunset</option>
</select>
<label style="font-size:13px;font-weight:600;color:var(--muted);margin-left:8px;">Position:</label>
<select class="form-input" style="width:auto;margin:0;padding:4px 10px;font-size:13px;" id="embed-pos" onchange="generateEmbedCode()">
<option value="bottom-right">Bottom Right</option>
<option value="bottom-left">Bottom Left</option>
</select>
</div>
</div>
<!-- Step 2: Paste -->
<div class="card" style="margin-bottom:16px;">
<div class="card-title">Step 2 — Paste it on your website</div>
<div class="card-sub">Paste the code in your website's <strong>footer</strong> section. Every platform has a place for this — pick yours below.</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin:14px 0;">
<button class="btn btn-secondary btn-sm" onclick="showPlatform('wordpress')" id="plat-wordpress">WordPress</button>
<button class="btn btn-secondary btn-sm" onclick="showPlatform('squarespace')" id="plat-squarespace">Squarespace</button>
<button class="btn btn-secondary btn-sm" onclick="showPlatform('webflow')" id="plat-webflow">Webflow</button>
<button class="btn btn-secondary btn-sm" onclick="showPlatform('wix')" id="plat-wix">Wix</button>
<button class="btn btn-secondary btn-sm" onclick="showPlatform('shopify')" id="plat-shopify">Shopify</button>
<button class="btn btn-secondary btn-sm" onclick="showPlatform('html')" id="plat-html">Other / HTML</button>
</div>
<div id="platform-instructions" style="font-size:14px;line-height:1.8;color:var(--text2);padding:14px;background:var(--surface2);border-radius:8px;"></div>
</div>
<!-- Step 3: Done -->
<div class="card">
<div class="card-title">Step 3 — You're done!</div>
<div class="card-sub">Visit your website and look for the 💬 bubble in the corner. Click it to test a conversation. Come back here anytime to change the theme or copy the code again.</div>
<div style="margin-top:14px;display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="openWidgetPreview('midnight')">Open Full-Screen Preview</button>
</div>
</div>
</div>
<!-- ═══ SMART LINKS ═══ -->
<div class="page" id="page-links">
<div class="page-header"><h1 class="page-title">Smart Links <span class="info-tip"><span class="tip-text">Add purchase URLs (Amazon, BookShop, etc.) and preview links for each book. These links appear as clickable cards in bot responses when readers ask about a book, driving sales.</span></span></h1><p class="page-sub">Configure buy/preview URLs for each book — shown to readers during chat</p></div>
<div class="card"><div id="links-content">Loading...</div></div>
</div>
<!-- ═══ BOT PERSONALITY ═══ -->
<div class="page" id="page-personality">
<div class="page-header"><h1 class="page-title">Bot Personality <span class="info-tip"><span class="tip-text">Shape your bot's voice: name, avatar, welcome message, fallback response, and communication style (friendly/balanced/concise). These settings define how readers experience your chatbot.</span></span></h1><p class="page-sub">Configure how your AI chatbot communicates with readers</p></div>
<div class="card">
<div class="form-group"><label class="form-label">Response Style</label>
<select class="form-input" id="p-style"><option value="balanced">Balanced</option><option value="formal">Formal</option><option value="casual">Casual</option><option value="enthusiastic">Enthusiastic</option></select>
</div>
<div class="form-group"><label class="form-label">Fallback Message (when bot can't answer)</label><textarea class="form-input" id="p-fallback" placeholder="I'm not sure about that detail — but I can tell you plenty about the books. What would you like to know?"></textarea></div>
<div class="form-group"><label class="form-label">Out-of-Scope Message (non-book questions)</label><textarea class="form-input" id="p-outofscope" placeholder="I'm specialized in discussing books..."></textarea></div>
<button class="btn btn-primary" id="save-personality-btn">Save Personality Settings</button>
</div>
</div>
<!-- ═══ PROFILE ═══ -->
<div class="page" id="page-profile">
<div class="page-header"><h1 class="page-title">Profile <span class="info-tip"><span class="tip-text">Your public author profile: name, website, bio, and logo. This information is shown to readers and used in the chatbot's context to personalize responses about your brand.</span></span></h1><p class="page-sub">Your author profile and public information</p></div>
<div class="card">
<div class="grid-2">
<div class="form-group"><label class="form-label">Full Name</label><input type="text" class="form-input" id="prof-name"></div>
<div class="form-group"><label class="form-label">Website</label><input type="url" class="form-input" id="prof-website" placeholder="https://yoursite.com"></div>
</div>
<div class="form-group"><label class="form-label">Bio</label><textarea class="form-input" id="prof-bio" placeholder="Tell readers about yourself..."></textarea></div>
<div class="form-group"><label class="form-label">Timezone</label>
<select class="form-input" id="prof-tz"><option>America/New_York</option><option>America/Chicago</option><option>America/Denver</option><option>America/Los_Angeles</option><option>Europe/London</option><option>Europe/Paris</option><option>Asia/Tokyo</option><option>Asia/Kolkata</option><option>Australia/Sydney</option></select>
</div>
<button class="btn btn-primary" id="save-profile-btn">Save Profile</button>
</div>
</div>
<!-- ═══ NOTIFICATIONS ═══ -->
<div class="page" id="page-notifications">
<div class="page-header"><h1 class="page-title">Notifications <span class="info-tip"><span class="tip-text">Configure which email alerts you receive: weekly digest, token usage alerts, new conversation alerts, and subscription expiry reminders. Toggle each individually.</span></span></h1><p class="page-sub">Control which email notifications you receive</p></div>
<div class="card">
<div class="flex items-center justify-between" style="padding:14px 0;border-bottom:1px solid var(--border-light);">
<div><div class="fw-600 text-sm">Weekly Digest</div><div class="text-xs text-muted">Summary of your chatbot activity</div></div>
<label class="toggle"><input type="checkbox" id="n-weekly" checked><div class="toggle-track"></div><div class="toggle-thumb"></div></label>
</div>
<div class="flex items-center justify-between" style="padding:14px 0;border-bottom:1px solid var(--border-light);">
<div><div class="fw-600 text-sm">Token Usage Alerts</div><div class="text-xs text-muted">Get notified at 80% and 95% budget usage</div></div>
<label class="toggle"><input type="checkbox" id="n-tokens" checked><div class="toggle-track"></div><div class="toggle-thumb"></div></label>
</div>
<div class="flex items-center justify-between" style="padding:14px 0;border-bottom:1px solid var(--border-light);">
<div><div class="fw-600 text-sm">New Conversations</div><div class="text-xs text-muted">Email when a new reader starts chatting</div></div>
<label class="toggle"><input type="checkbox" id="n-newchat"><div class="toggle-track"></div><div class="toggle-thumb"></div></label>
</div>
<div class="flex items-center justify-between" style="padding:14px 0;">
<div><div class="fw-600 text-sm">Subscription Expiry</div><div class="text-xs text-muted">Reminder before your subscription expires</div></div>
<label class="toggle"><input type="checkbox" id="n-expiry" checked><div class="toggle-track"></div><div class="toggle-thumb"></div></label>
</div>
<button class="btn btn-primary mt-4" id="save-notif-btn">Save Preferences</button>
</div>
</div>
<!-- ═══ SECURITY ═══ -->
<div class="page" id="page-security">
<div class="page-header"><h1 class="page-title">Security <span class="info-tip"><span class="tip-text">Change your account password here. Passwords must be at least 8 characters with uppercase, lowercase, and a number. Sessions expire after 15 minutes for security.</span></span></h1><p class="page-sub">Manage your account security and password</p></div>
<div class="card">
<div class="card-title">Change Password</div>
<div class="card-sub">Use a strong password with uppercase, lowercase, numbers, and special characters</div>
<div class="form-group"><label class="form-label">Current Password</label><input type="password" class="form-input" id="sec-cur-pass" style="max-width:400px;"></div>
<div class="form-group"><label class="form-label">New Password</label><input type="password" class="form-input" id="sec-new-pass" style="max-width:400px;" oninput="checkPasswordStrength()"></div>
<div id="pass-strength" class="text-xs mt-2" style="max-width:400px;"></div>
<div class="form-group"><label class="form-label">Confirm New Password</label><input type="password" class="form-input" id="sec-confirm-pass" style="max-width:400px;"></div>
<button class="btn btn-primary" id="change-pass-btn">Update Password</button>
<p class="msg" id="pass-msg" style="margin-top:12px;font-size:13px;"></p>
</div>
</div>
<!-- ── CONVERSATIONS PAGE ── -->
<div class="page" id="page-conversations">
<div class="page-header">
<h1 class="page-title">Conversations</h1>
<p class="page-sub">View full chat transcripts, annotate messages, and search across all conversations</p>
</div>
<div class="card">
<div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;">
<input type="text" class="form-input" id="conv-search" placeholder="Search messages..." style="flex:1;margin:0;" onkeydown="if(event.key==='Enter')searchConversations()">
<button class="btn btn-primary btn-sm" onclick="searchConversations()">Search</button>
</div>
<div id="conv-results" style="margin-bottom:16px;"></div>
</div>
<div style="display:grid;grid-template-columns:320px 1fr;gap:20px;" id="conv-split">
<div class="card" style="max-height:600px;overflow-y:auto;">
<div class="card-title">Sessions</div>
<div id="conv-session-list" style="font-size:13px;"></div>
</div>
<div class="card" style="max-height:600px;overflow-y:auto;">
<div class="card-title" id="conv-transcript-title">Select a session</div>
<div id="conv-transcript"></div>
<div id="conv-pagination" style="margin-top:12px;text-align:center;"></div>
</div>
</div>
</div>
<!-- ── CUSTOM Q&A PAGE ── -->
<div class="page" id="page-qa">
<div class="page-header">
<h1 class="page-title">Custom Q&A</h1>
<p class="page-sub">Define custom question-answer pairs that override AI responses</p>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
<button class="btn btn-primary btn-sm" onclick="showAddQAModal()">+ Add Q&A Pair</button>
<label class="btn btn-secondary btn-sm" style="cursor:pointer;">📤 Import CSV <input type="file" accept=".csv" style="display:none;" onchange="importQACSV(this)"></label>
<button class="btn btn-secondary btn-sm" onclick="exportQACSV()">📥 Export CSV</button>
<span class="text-muted text-sm" id="qa-count"></span>
</div>
</div>
<div class="card">
<div id="qa-list"></div>
</div>
</div>
<!-- ── EXPORT PAGE ── -->
<div class="page" id="page-exports">
<div class="page-header">
<h1 class="page-title">Export Center</h1>
<p class="page-sub">Download your data in CSV format for analysis or backup</p>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px;">
<div class="card">
<div class="card-title">📊 Sessions Export</div>
<div class="card-sub">Download all reader session data including device, country, and rating info.</div>
<div class="form-group"><label class="form-label">Time Range (days)</label><input type="number" class="form-input" id="export-sessions-days" value="30" min="1" max="365" style="max-width:120px;"></div>
<button class="btn btn-primary btn-sm" onclick="downloadExport('sessions')">Download Sessions CSV</button>
</div>
<div class="card">
<div class="card-title">📈 Analytics Export</div>
<div class="card-sub">Download daily analytics with token usage, latency, and link click metrics.</div>
<div class="form-group"><label class="form-label">Time Range (days)</label><input type="number" class="form-input" id="export-analytics-days" value="30" min="1" max="365" style="max-width:120px;"></div>
<button class="btn btn-primary btn-sm" onclick="downloadExport('analytics')">Download Analytics CSV</button>
</div>
<div class="card">
<div class="card-title">💬 Conversations Export</div>
<div class="card-sub">Download full chat transcripts with intent classification and quality metrics.</div>
<div class="form-group"><label class="form-label">Time Range (days)</label><input type="number" class="form-input" id="export-conv-days" value="7" min="1" max="30" style="max-width:120px;"></div>
<button class="btn btn-primary btn-sm" onclick="downloadExport('conversations')">Download Transcripts CSV</button>
</div>
</div>
</div>
</main>
<button type="button" class="ab-contact-tab" id="ab-contact-tab" onclick="abShowContactForm()" aria-label="Contact support">
<svg class="ab-contact-tab-icon" viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="1.5"/>
<path d="M3 7.5 12 13l9-5.5"/>
</svg>
<span class="ab-contact-tab-text">Support</span>
</button>
</div>
<!-- ═══ JAVASCRIPT ═══ -->
<script>
/* ── GLOBALS ── */
const API = window.location.origin + '/api';
function handleSessionExpired() {
token = null;
authorSlug = null;
document.getElementById('auth-screen').style.display = '';
document.getElementById('dashboard').style.display = 'none';
const msg = document.getElementById('login-msg');
if (msg) {
msg.textContent = 'Your session expired. Please sign in again.';
msg.style.color = 'var(--red)';
}
}
const auth = new AuthorBotAuthClient({
apiBase: API,
accessKey: 'access_token',
refreshKey: 'refresh_token',
slugKey: 'author_slug',
onSessionExpired: handleSessionExpired,
});
let token = auth.getAccessToken();
let authorSlug = auth.getAuthorSlug();
let allBooks = [];
let allSessions = [];
let chartInstances = {};
/* ── TOAST SYSTEM ── */
function toast(msg, type = 'info') {
const c = document.getElementById('toast-container');
const t = document.createElement('div');
t.className = `toast ${type}`;
t.innerHTML = `<span>${{success:'✅',error:'❌',info:'ℹ️'}[type]||'ℹ️'}</span><span>${msg}</span><button class="toast-close" onclick="this.parentElement.remove()">×</button>`;
c.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(100px)'; setTimeout(() => t.remove(), 300); }, 4000);
}
/* ── MODAL SYSTEM ── */
function showModal(title, body, actions = '') {
const o = document.getElementById('modal-overlay');
document.getElementById('modal-content').innerHTML = `<div class="modal-title">${title}</div>${body}${actions ? `<div class="modal-actions">${actions}</div>` : ''}`;
o.classList.add('active');
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('active');
if (_abPpModalEscHandler) {
document.removeEventListener('keydown', _abPpModalEscHandler);
_abPpModalEscHandler = null;
}
}
document.getElementById('modal-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(); });
/* ── AUTH ── */
function clearSuperAdminSessionKeys() {
localStorage.removeItem('sa_token');
localStorage.removeItem('sa_refresh_token');
}
function clearAuthorSessionKeys() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('author_slug');
}
auth.restoreSession(function (user) {
return user && user.role === 'author';
}).then(function (ok) {
if (ok) {
clearSuperAdminSessionKeys();
token = auth.getAccessToken();
authorSlug = auth.getAuthorSlug();
showDashboard();
} else if (auth.getAccessToken()) {
auth.clearSession();
token = null;
authorSlug = null;
}
});
document.getElementById('login-btn').onclick = async () => {
const msg = document.getElementById('login-msg');
msg.textContent = ''; msg.style.color = '';
const btn = document.getElementById('login-btn');
btn.disabled = true; btn.textContent = 'Signing in...';
try {
const data = await auth.login(
document.getElementById('email').value,
document.getElementById('password').value
);
if (data.is_superadmin || data.role === 'superadmin') {
auth.clearSession();
clearSuperAdminSessionKeys();
window.location.href = '/superadmin';
return;
}
clearSuperAdminSessionKeys();
token = auth.getAccessToken();
authorSlug = data.author_slug || auth.getAuthorSlug() || 'me';
if (authorSlug) localStorage.setItem('author_slug', authorSlug);
toast('Welcome back!', 'success');
showDashboard();
} catch (e) {
msg.textContent = e.message; msg.style.color = 'var(--red)';
} finally { btn.disabled = false; btn.textContent = 'Sign In'; }
};
['email', 'password'].forEach(id => document.getElementById(id).addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('login-btn').click(); }));
document.getElementById('logout-btn').onclick = function () {
auth.logout().finally(function () { location.reload(); });
};
function showDashboard() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
loadUserInfo();
abLoadSupportConfig();
loadDashboard();
}
/* ── NAVIGATION ── */
function nav(page) {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const btn = document.querySelector(`.nav-item[data-page="${page}"]`);
if (btn) btn.classList.add('active');
const pg = document.getElementById('page-' + page);
if (pg) pg.classList.add('active');
// Lazy load
const loaders = { books: loadBooks, readers: loadSessions, analytics: () => loadAnalytics(30), upload: null, tokens: loadTokenUsage, embed: generateEmbedCode, links: loadSmartLinks, widget: loadWidgetConfig, profile: loadProfile, personality: loadPersonality, notifications: loadNotifications, conversations: loadConversations, qa: loadQAPairs, exports: null };
if (loaders[page]) loaders[page]();
}
document.querySelectorAll('.nav-item[data-page]').forEach(el => { el.onclick = () => nav(el.dataset.page); });
/* ── API HELPERS (auto-refresh on 401) ── */
async function apiGet(path) {
const data = await auth.get(path);
token = auth.getAccessToken();
return data;
}
async function apiPost(path, body) {
const data = await auth.post(path, body);
token = auth.getAccessToken();
return data;
}
async function apiPut(path, body) {
const data = await auth.put(path, body);
token = auth.getAccessToken();
return data;
}
async function apiPatch(path, body) {
const data = await auth.patch(path, body);
token = auth.getAccessToken();
return data;
}
async function apiDelete(path) {
const data = await auth.delete(path);
token = auth.getAccessToken();
return data;
}
/* ── USER INFO ── */
async function loadUserInfo() {
try {
const u = await apiGet('/auth/me');
document.getElementById('user-display-name').textContent = u.full_name || 'Author';
document.getElementById('user-display-email').textContent = u.email || '';
document.getElementById('user-initials').textContent = (u.full_name || 'A')[0].toUpperCase();
} catch {}
}
/* ── DASHBOARD ── */
async function loadDashboard() {
try {
const data = await apiGet(`/admin/${authorSlug}/dashboard`);
animateNumber('stat-books', data.total_books || 0);
animateNumber('stat-sessions', data.total_sessions || 0);
animateNumber('stat-active', data.active_books || 0);
animateNumber('stat-clicks', data.total_clicks || 0);
const gaps = data.distribution_gaps || {};
const gapsBooks = gaps.books_with_gaps || 0;
const gapsCard = document.getElementById('stat-gaps-card');
const dashGaps = document.getElementById('dashboard-gaps-card');
if (gapsBooks > 0) {
if (gapsCard) gapsCard.style.display = '';
animateNumber('stat-gaps', gapsBooks);
if (dashGaps) {
dashGaps.style.display = '';
document.getElementById('dashboard-gaps-detail').textContent =
`${gapsBooks} book${gapsBooks === 1 ? '' : 's'} missing ${gaps.total_missing_platforms || 0} platform listing${(gaps.total_missing_platforms || 0) === 1 ? '' : 's'}.`;
}
} else {
if (gapsCard) gapsCard.style.display = 'none';
if (dashGaps) dashGaps.style.display = 'none';
}
} catch(e) { console.error('Dashboard:', e); }
// Load mini chart
try {
const analytics = await apiGet(`/admin/${authorSlug}/analytics?days=30`);
renderLineChart('chart-sessions', analytics.daily_sessions || [], 'Sessions');
} catch {}
}
function animateNumber(id, target) {
const el = document.getElementById(id);
if (!el) return;
const duration = 600;
const start = performance.now();
const initial = parseInt(el.textContent) || 0;
function step(now) {
const progress = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.round(initial + (target - initial) * eased).toLocaleString();
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ── CHARTS ── */
function renderLineChart(canvasId, data, label) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
if (chartInstances[canvasId]) chartInstances[canvasId].destroy();
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 250);
gradient.addColorStop(0, 'rgba(99,102,241,0.15)');
gradient.addColorStop(1, 'rgba(99,102,241,0)');
chartInstances[canvasId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{ label, data: data.map(d => d.count), borderColor: '#6366f1', backgroundColor: gradient, fill: true, tension: 0.4, borderWidth: 2, pointRadius: 3, pointHoverRadius: 6, pointBackgroundColor: '#6366f1' }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1e293b', titleFont: { family: 'Inter' }, bodyFont: { family: 'Inter' }, cornerRadius: 8, padding: 10 } },
scales: { x: { grid: { display: false }, ticks: { font: { size: 11, family: 'Inter' }, color: '#94a3b8' } }, y: { grid: { color: '#f1f3f9' }, ticks: { font: { size: 11, family: 'Inter' }, color: '#94a3b8' }, beginAtZero: true } },
animation: { duration: 800, easing: 'easeOutQuart' }
}
});
}
/* ── BOOKS ── */
async function loadBooks() {
const tbody = document.getElementById('books-body');
// Rich skeleton — mirrors exact table column layout with staggered shimmer rows
const skRow = (delay) => `
<tr style="animation:fadeIn 0.3s ease ${delay}ms both;">
<td style="padding:12px 8px 12px 16px;">
<div style="width:44px;height:62px;border-radius:8px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div>
</td>
<td>
<div style="height:13px;width:70%;border-radius:6px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;margin-bottom:7px;"></div>
<div style="height:10px;width:40%;border-radius:6px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite ${delay+100}ms;"></div>
</td>
<td><div style="height:10px;width:60px;border-radius:20px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div></td>
<td><div style="height:22px;width:90px;border-radius:20px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div></td>
<td><div style="height:10px;width:20px;border-radius:4px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div></td>
<td><div style="height:10px;width:80%;border-radius:4px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div></td>
<td><div style="display:flex;gap:5px;">
<div style="height:28px;width:32px;border-radius:6px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div>
<div style="height:28px;width:32px;border-radius:6px;background:linear-gradient(90deg,var(--surface2) 25%,#e8eaf2 50%,var(--surface2) 75%);background-size:400px 100%;animation:shimmer 1.4s infinite;"></div>
</div></td>
</tr>`;
tbody.innerHTML = [0,80,160,240,320].map(skRow).join('');
try {
const data = await apiGet(`/admin/${authorSlug}/books`);
allBooks = data.books || [];
renderBooks(allBooks);
} catch(e) {
if (e.message.includes('Session expired')) return;
if (e.message.includes('Cannot reach server')) {
tbody.innerHTML = `<tr><td colspan="7" style="color:var(--amber);text-align:center;padding:40px;"><div style="font-size:32px;margin-bottom:12px;">⏳</div>Server is waking up<div style="font-size:12px;color:var(--light);margin-top:6px;">${e.message}</div></td></tr>`;
return;
}
tbody.innerHTML = `<tr><td colspan="7" style="color:var(--red);text-align:center;padding:40px;"><div style="font-size:32px;margin-bottom:12px;">⚠️</div>Failed to load books<div style="font-size:12px;color:var(--light);margin-top:6px;">${e.message}</div></td></tr>`;
}
}
function renderBooks(books) {
const tbody = document.getElementById('books-body');
if (!books.length) {
tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="empty-icon">📚</div><div class="empty-title">No books yet</div><div class="empty-sub">Import from a URL or upload a file to get started</div><button class="btn btn-primary btn-sm" onclick="nav(\'upload\')">+ Add Book</button></div></td></tr>';
return;
}
tbody.innerHTML = books.map(b => {
const isUrlImported = b.status === 'url_imported'; // legacy safety
// Only flag as stalled when status is explicitly 'error'
// Processing/created books are actively working — never mark them stalled
const stuck = b.status === 'error';
// Cover — prefer local thumbnail, fall back to remote scraped URL, then initials
const localThumb = b.cover_thumbnail
? '/covers/' + (b.cover_thumbnail.split('/data/covers/')[1] || b.cover_thumbnail.replace(/^\//, ''))
: null;
const remoteCover = b.cover_url || null;
const coverSrc = localThumb || remoteCover;
const coverHtml = coverSrc
? `<img src="${coverSrc}" style="width:100%;height:100%;object-fit:cover;border-radius:8px;" loading="lazy" onerror="this.parentElement.innerHTML='<span style=font-size:18px;font-weight:700;color:#fff>'+'${esc(b.title.charAt(0))}'+' </span>'">`
: `<span style="font-size:18px;font-weight:700;color:#fff;">${esc(b.title.charAt(0))}</span>`;
const coverBg = coverSrc ? '' : `background:linear-gradient(135deg,hsl(${Math.abs(hashCode(b.title))%360},65%,72%),hsl(${(Math.abs(hashCode(b.title))+60)%360},55%,62%));`;
const statusConfig = {
'ready': { color:'#10b981', bg:'rgba(16,185,129,0.1)', icon:'✅', label:'Ready' },
'active': { color:'#10b981', bg:'rgba(16,185,129,0.1)', icon:'✅', label:'Active' },
'url_imported': { color:'#6366f1', bg:'rgba(99,102,241,0.1)', icon:'⏳', label:'Indexing…' },
'processing': { color:'#f59e0b', bg:'rgba(245,158,11,0.1)', icon:'⚙️', label:'Processing' },
'created': { color:'#94a3b8', bg:'rgba(148,163,184,0.1)', icon:'⏳', label:'Created' },
'error': { color:'#ef4444', bg:'rgba(239,68,68,0.1)', icon:'❌', label:'Error' },
};
const sc = statusConfig[b.status] || { color:'#94a3b8', bg:'rgba(148,163,184,0.1)', icon:'📄', label: b.status };
return `<tr style="${stuck ? 'opacity:0.7;' : ''}">
<td style="padding:12px 8px 12px 16px;">
<div style="width:44px;height:62px;border-radius:8px;overflow:hidden;flex-shrink:0;${coverBg}display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.12);">
${coverHtml}
</div>
</td>
<td>
<div class="fw-600" style="margin-bottom:3px;">${esc(b.title)}${b.missing_count > 0 ? `<span class="ab-missing-pill" title="Missing platform listings">${b.missing_count} missing</span>` : ''}</div>
<div class="text-xs text-muted">${esc(b.book_author || b.author_name || '')}</div>
${stuck ? '<div class="text-xs" style="color:var(--yellow);margin-top:2px;">⚠️ Indexing stalled — delete and re-add</div>' : ''}
</td>
<td><span class="text-sm" style="color:var(--muted);">${esc(b.genre || '—')}</span></td>
<td>
<span style="display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600;background:${sc.bg};color:${sc.color};">
${sc.icon} ${sc.label}
</span>
</td>
<td><span style="font-size:13px;font-weight:600;color:${b.chunk_count > 0 ? 'var(--green)' : 'var(--muted)'}">${b.chunk_count || 0}</span></td>
<td style="max-width:180px;" class="truncate text-xs text-muted">${esc(b.ai_summary || '—')}</td>
<td>
<div style="display:flex;gap:5px;flex-wrap:wrap;align-items:center;">
${stuck
? `<button class="btn btn-primary btn-sm" onclick="repairBook('${b.id}','${esc(b.title)}')">🔄 Re-add</button>`
: (b.status === 'ready' || b.status === 'active')
? `<button class="btn btn-secondary btn-sm" onclick="toggleBookActive('${b.id}', ${b.is_active !== false})" title="${b.is_active !== false ? 'Deactivate' : 'Activate'}">${b.is_active !== false ? '⏸' : '▶️'}</button>
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0;" title="Upload cover image"><input type="file" accept="image/*" style="display:none;" onchange="uploadCover('${b.id}',this)">📷</label>`
: ''
}
<button class="btn btn-danger btn-sm" onclick="confirmDeleteBook('${b.id}','${esc(b.title)}')" title="Delete">🗑</button>
</div>
</td>
</tr>`;
}).join('');
}
function hashCode(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i); return h; }
async function uploadCover(bookId, input) {
const file = input.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) { toast('Please select an image file', 'error'); return; }
if (file.size > 5 * 1024 * 1024) { toast('Image too large (max 5MB)', 'error'); return; }
const fd = new FormData();
fd.append('file', file);
try {
const res = await auth.upload(`/admin/${authorSlug}/books/${bookId}/cover`, fd);
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Upload failed');
toast('Cover uploaded!', 'success');
loadBooks();
} catch(e) { toast(e.message, 'error'); }
input.value = '';
}
function filterBooks() {
const q = document.getElementById('book-search').value.toLowerCase();
renderBooks(allBooks.filter(b => b.title.toLowerCase().includes(q) || (b.genre||'').toLowerCase().includes(q)));
}
function confirmDeleteBook(id, title) {
const isLast = allBooks.filter(b => b.is_active !== false).length <= 1;
const warn = isLast ? '<div class="alert alert-warning" style="margin-top:12px;">⚠️ This is your last active book — deleting it will disable your chatbot.</div>' : '';
showModal('Delete Book', `<div class="modal-sub">Are you sure you want to delete <strong>${title}</strong>? This will remove all embeddings and cannot be undone.</div>${warn}<div class="form-group"><label class="form-label">Type the book title to confirm</label><input type="text" class="form-input" id="confirm-delete-input" placeholder="${title}"></div>`,
`<button class="btn btn-secondary" onclick="closeModal()">Cancel</button><button class="btn btn-danger" id="confirm-delete-btn" onclick="executeDeleteBook('${id}','${title}')">Delete Forever</button>`);
}
async function executeDeleteBook(id, title) {
const input = document.getElementById('confirm-delete-input');
if (input.value !== title) { toast('Title does not match', 'error'); return; }
try {
const res = await apiDelete(`/admin/${authorSlug}/books/${id}`);
closeModal();
if (res && res.chatbot_disabled) toast('Book deleted — chatbot disabled (no active books)', 'warning');
else toast('Book deleted', 'success');
loadBooks();
} catch(e) { toast(e.message, 'error'); }
}
async function toggleBookActive(id, isActive) {
try {
await apiPatch(`/admin/${authorSlug}/books/${id}/active`, { is_active: !isActive });
toast(isActive ? 'Book deactivated' : 'Book activated', 'success');
loadBooks();
} catch(e) { toast(e.message, 'error'); }
}
async function repairBook(id, title) {
showModal('🔄 Re-upload Required',
`<div class="modal-sub">The book <strong>${esc(title)}</strong> got stuck during indexing (the server restarted mid-process).<br><br>
The fix is simple: <strong>delete it and re-upload the file</strong>. Indexing now runs in the background and will complete reliably.</div>`,
`<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button class="btn btn-danger" onclick="closeModal(); executeDeleteBook('${id}','${esc(title)}'); setTimeout(()=>nav('upload'),500);">🗑 Delete &amp; Go to Upload</button>`
);
}
/* ── UPLOAD ── */
const uploadZone = document.getElementById('upload-zone');
const fileInput = document.getElementById('file-input');
let selectedFile = null;
['dragover','dragenter'].forEach(e => uploadZone.addEventListener(e, ev => { ev.preventDefault(); uploadZone.classList.add('dragover'); }));
['dragleave','drop'].forEach(e => uploadZone.addEventListener(e, ev => { ev.preventDefault(); uploadZone.classList.remove('dragover'); }));
uploadZone.addEventListener('drop', ev => { if (ev.dataTransfer.files.length) handleFile(ev.dataTransfer.files[0]); });
fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
function handleFile(file) {
const maxMB = 50;
const allowed = ['pdf','epub','docx','txt'];
const ext = file.name.split('.').pop().toLowerCase();
if (!allowed.includes(ext)) { toast('Unsupported file type. Use PDF, EPUB, DOCX, or TXT.', 'error'); return; }
if (file.size > maxMB * 1024 * 1024) { toast(`File too large. Max ${maxMB}MB.`, 'error'); return; }
selectedFile = file;
document.getElementById('upload-file-info').classList.remove('hidden');
document.getElementById('upload-file-info').innerHTML = `<div class="flex items-center gap-3"><span style="font-size:24px;">📄</span><div><div class="fw-600 text-sm">${esc(file.name)}</div><div class="text-xs text-muted">${(file.size/1024/1024).toFixed(2)} MB · ${ext.toUpperCase()}</div></div></div>`;
document.getElementById('upload-btn').disabled = false;
if (!document.getElementById('book-title').value) document.getElementById('book-title').value = file.name.replace(/\.[^.]+$/, '');
}
document.getElementById('upload-btn').onclick = async () => {
if (!selectedFile) return;
const btn = document.getElementById('upload-btn');
btn.disabled = true; btn.textContent = 'Uploading...';
document.getElementById('upload-progress-wrap').classList.remove('hidden');
const fd = new FormData();
fd.append('file', selectedFile);
fd.append('title', document.getElementById('book-title').value || selectedFile.name);
fd.append('genre', document.getElementById('book-genre').value);
fd.append('price', document.getElementById('book-price').value || '0');
fd.append('buy_url', document.getElementById('book-buy-url').value);
try {
const res = await auth.upload(`/admin/${authorSlug}/books/upload`, fd);
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Upload failed');
toast('Book uploaded and indexing started!', 'success');
updateProgress('Indexing...', 100);
setTimeout(() => nav('books'), 2000);
} catch(e) { toast(e.message, 'error'); }
finally { btn.disabled = false; btn.textContent = 'Upload & Index'; }
};
function updateProgress(stage, pct) {
document.getElementById('upload-stage').textContent = stage;
document.getElementById('upload-pct').textContent = pct + '%';
document.getElementById('upload-fill').style.width = pct + '%';
}
/* ── READERS ── */
async function loadSessions() {
const tbody = document.getElementById('sessions-body');
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;"><div class="skeleton" style="height:20px;width:60%;margin:0 auto;"></div></td></tr>';
try {
const data = await apiGet(`/admin/${authorSlug}/sessions`);
allSessions = data.sessions || [];
document.getElementById('session-count').textContent = `${allSessions.length} sessions`;
renderSessions(allSessions);
} catch(e) { tbody.innerHTML = `<tr><td colspan="7" style="color:var(--red);text-align:center;padding:40px;">${e.message}</td></tr>`; }
}
function renderSessions(sessions) {
const tbody = document.getElementById('sessions-body');
if (!sessions.length) {
tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="empty-icon">👥</div><div class="empty-title">No reader sessions yet</div><div class="empty-sub">Sessions will appear here when readers start chatting with your bot</div></div></td></tr>';
return;
}
tbody.innerHTML = sessions.map(s => `<tr>
<td><code class="text-xs" style="background:var(--surface2);padding:2px 6px;border-radius:4px;">${s.id.slice(0,8)}…</code></td>
<td>${s.country || '🌍 —'}</td>
<td>${{mobile:'📱',desktop:'💻',tablet:'📟'}[s.device_type] || '—'} ${s.device_type||'—'}</td>
<td>${s.rating ? '⭐'.repeat(Math.min(s.rating,5)) : '<span class="text-muted">—</span>'}</td>
<td class="text-xs text-muted">${s.created_at ? new Date(s.created_at).toLocaleDateString() : '—'}</td>
<td>${s.blocked ? '<span class="badge badge-failed">Blocked</span>' : '<span class="badge badge-active">Active</span>'}</td>
<td>${s.blocked
? `<button class="btn btn-secondary btn-sm" onclick="toggleBlock('${s.id}', false)">Unblock</button>`
: `<button class="btn btn-danger btn-sm" onclick="toggleBlock('${s.id}', true)">Block</button>`}</td>
</tr>`).join('');
}
function filterSessions() {
const q = document.getElementById('reader-search').value.toLowerCase();
renderSessions(allSessions.filter(s => s.id.includes(q) || (s.country||'').toLowerCase().includes(q)));
}
async function toggleBlock(id, block) {
try { await apiPost(`/admin/${authorSlug}/sessions/${id}/${block?'block':'unblock'}`, {}); toast(block ? 'Session blocked' : 'Session unblocked', 'success'); loadSessions(); }
catch(e) { toast(e.message, 'error'); }
}
/* ── ANALYTICS ── */
document.querySelectorAll('#analytics-tabs .tab').forEach(tab => {
tab.onclick = () => {
document.querySelectorAll('#analytics-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
loadAnalytics(parseInt(tab.dataset.days));
};
});
async function loadAnalytics(days = 30) {
try {
const [daily, funnel, visitors, sessionStats] = await Promise.all([
apiGet(`/admin/${authorSlug}/analytics?days=${days}`),
apiGet(`/admin/${authorSlug}/analytics/funnel?days=${days}`),
apiGet(`/admin/${authorSlug}/analytics/visitors?days=${days}`),
apiGet(`/admin/${authorSlug}/analytics/sessions/stats?days=${days}`),
]);
const dailySessions = daily.daily_sessions || [];
const total = dailySessions.reduce((s, d) => s + d.count, 0);
const funnelStages = funnel.funnel || [];
const linkClicked = funnelStages.find(s => s.stage === 'Link Clicked');
animateNumber('a-total-sessions', total);
animateNumber('a-unique-visitors', visitors.total_visitors ?? 0);
animateNumber('a-link-clicks', linkClicked ? linkClicked.count : 0);
const avgTurns = sessionStats.avg_turns_per_session;
document.getElementById('a-avg-turns').textContent = avgTurns != null ? Number(avgTurns).toFixed(1) : '—';
renderLineChart('chart-analytics', dailySessions, 'Sessions');
} catch(e) { console.error('Analytics:', e); }
}
/* ── TOKEN USAGE ── */
async function loadTokenUsage() {
try {
const data = await apiGet(`/admin/${authorSlug}/token-usage`);
const banner = document.getElementById('token-subscription-banner');
const hasSub = data.has_active_subscription !== false && data.subscription_status === 'active';
if (banner) {
if (hasSub) banner.classList.add('hidden');
else banner.classList.remove('hidden');
}
const budget = hasSub ? (data.budget || 0) : 0;
const used = hasSub ? (data.used || 0) : 0;
const remaining = hasSub ? (data.remaining || 0) : 0;
const pct = hasSub ? (data.percent_used || 0) : 0;
document.getElementById('token-pct').textContent = hasSub ? (pct + '%') : '—';
document.getElementById('token-budget').textContent = hasSub ? budget.toLocaleString() : 'No plan';
document.getElementById('token-used').textContent = used.toLocaleString();
document.getElementById('token-remaining').textContent = hasSub ? remaining.toLocaleString() : '—';
// Gauge
const canvas = document.getElementById('chart-token-gauge');
if (canvas) {
const ctx = canvas.getContext('2d');
const cx = 90, cy = 90, r = 75, lw = 12;
ctx.clearRect(0, 0, 180, 180);
ctx.beginPath(); ctx.arc(cx, cy, r, 0.75 * Math.PI, 2.25 * Math.PI); ctx.strokeStyle = '#f1f3f9'; ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.stroke();
if (hasSub && pct > 0) {
ctx.beginPath(); ctx.arc(cx, cy, r, 0.75 * Math.PI, (0.75 + 1.5 * pct / 100) * Math.PI);
ctx.strokeStyle = pct > 90 ? '#ef4444' : pct > 75 ? '#f59e0b' : '#6366f1'; ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.stroke();
}
}
} catch(e) { console.error('Token usage:', e); }
}
/* ── EMBED CODE ── */
let _cachedEmbedToken = null;
function buildEmbedSnippet(origin, token, slug, theme, pos) {
return (
'<scr' + 'ipt src="' + origin + '/static/widget.js" defer\n' +
' data-authorbot-token="' + token + '"\n' +
' data-authorbot-slug="' + slug + '"\n' +
' data-authorbot-api="' + origin + '/api"\n' +
' data-authorbot-theme="' + theme + '"\n' +
' data-authorbot-position="' + pos + '"><\/scr' + 'ipt>'
);
}
function mountLiveWidgetPreview(origin, token, theme, pos) {
document.getElementById('ab-root')?.remove();
window.__authorBotMounted = false;
document.getElementById('authorbot-preview-script')?.remove();
const script = document.createElement('script');
script.id = 'authorbot-preview-script';
script.src = origin + '/static/widget.js';
script.defer = true;
script.setAttribute('data-authorbot-token', token);
script.setAttribute('data-authorbot-slug', authorSlug);
script.setAttribute('data-authorbot-api', origin + '/api');
script.setAttribute('data-authorbot-theme', theme);
script.setAttribute('data-authorbot-position', pos);
document.body.appendChild(script);
const liveStatus = document.getElementById('embed-live-status');
if (liveStatus) {
liveStatus.innerHTML = '<span style="color:var(--green);font-weight:600;">✅ Your chatbot is live on this page — click the 💬 bubble to try it!</span>';
}
}
async function generateEmbedCode() {
const origin = window.location.origin;
const theme = document.getElementById('embed-theme')?.value || 'midnight';
const pos = document.getElementById('embed-pos')?.value || 'bottom-right';
const codeEl = document.getElementById('embed-code');
const statusEl = document.getElementById('embed-token-status');
const liveStatus = document.getElementById('embed-live-status');
if (!_cachedEmbedToken) {
if (codeEl) codeEl.textContent = '⏳ Preparing your embed code...';
try {
const data = await apiGet(`/admin/${authorSlug}/embed-token`);
// No active subscription — show friendly UI, not an error
if (!data.active || !data.token) {
if (statusEl) statusEl.innerHTML = `
<div style="padding:14px;background:var(--yellow-bg);border:1px solid var(--yellow);border-radius:8px;">
<strong style="color:var(--yellow-dark);">⚠️ No active subscription</strong><br>
<span style="font-size:12px;color:var(--text2);">Your chatbot is ready but needs a subscription plan to go live.
Contact your administrator to activate your account.</span>
</div>`;
if (codeEl) codeEl.textContent = '⚠️ Chatbot not active yet — contact your administrator to get a subscription plan.';
if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--yellow-dark);">⚠️ Chatbot preview unavailable — no active subscription</span>`;
return;
}
_cachedEmbedToken = data.token;
if (statusEl) {
const d = data.days_remaining || 0;
const t = (data.tokens_remaining || 0).toLocaleString();
statusEl.innerHTML = `<span style="color:var(--green);font-weight:600;">✅ Your chatbot is active</span> — ${d} days remaining, ${t} tokens left`;
}
} catch(e) {
if (codeEl) codeEl.textContent = '❌ Failed to load embed code: ' + (e.message || 'Unknown error');
if (statusEl) statusEl.innerHTML = `<span style="color:var(--red);">❌ ${e.message || 'Could not load subscription info'}</span>`;
if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--red);">❌ Preview unavailable</span>`;
return;
}
}
const tok = _cachedEmbedToken;
if (!tok) return;
if (codeEl) codeEl.textContent = buildEmbedSnippet(origin, tok, authorSlug, theme, pos);
mountLiveWidgetPreview(origin, tok, theme, pos);
if (!document.getElementById('platform-instructions')?.innerHTML) showPlatform('wordpress');
}
function copyEmbedCode() {
const text = document.getElementById('embed-code').textContent;
if (text.startsWith('⏳') || text.startsWith('❌')) {
toast('Please wait for your embed code to load first', 'error');
return;
}
navigator.clipboard.writeText(text);
toast('Copied! Now paste it in your website footer.', 'success');
}
async function openWidgetPreview(theme) {
if (!_cachedEmbedToken) await generateEmbedCode();
if (!_cachedEmbedToken) return;
const pos = document.getElementById('embed-pos')?.value || 'bottom-right';
const url = `${window.location.origin}/api/widget/${authorSlug}?token=${encodeURIComponent(_cachedEmbedToken)}&slug=${encodeURIComponent(authorSlug)}&theme=${theme}&position=${pos}&autoOpen=1`;
window.open(url, '_blank', 'width=500,height=720,resizable=yes');
}
const PLATFORM_GUIDES = {
wordpress: `<strong>WordPress — 2 minutes:</strong><br>
1. Install the free plugin <strong>"WPCode"</strong> or <strong>"Insert Headers and Footers"</strong><br>
2. Go to the plugin settings → <strong>Footer</strong> section<br>
3. Paste your copied code → click <strong>Save</strong><br>
4. Visit your website — the 💬 bubble appears immediately`,
squarespace: `<strong>Squarespace — 2 minutes:</strong><br>
1. Go to <strong>Settings → Advanced → Code Injection</strong><br>
2. Paste your copied code in the <strong>Footer</strong> box<br>
3. Click <strong>Save</strong> — done!`,
webflow: `<strong>Webflow — 2 minutes:</strong><br>
1. Open <strong>Project Settings → Custom Code</strong><br>
2. Paste your copied code in <strong>Footer Code</strong><br>
3. Click <strong>Save</strong>, then <strong>Publish</strong> your site`,
wix: `<strong>Wix — 2 minutes:</strong><br>
1. Go to <strong>Settings → Custom Code</strong><br>
2. Click <strong>+ Add Custom Code</strong><br>
3. Paste your copied code, set placement to <strong>Body - end</strong><br>
4. Apply to <strong>All Pages</strong> → click <strong>Publish</strong>`,
shopify: `<strong>Shopify — 3 minutes:</strong><br>
1. Go to <strong>Online Store → Themes → Edit Code</strong><br>
2. Open <strong>theme.liquid</strong><br>
3. Paste your copied code just before <code>&lt;/body&gt;</code><br>
4. Click <strong>Save</strong>`,
html: `<strong>Any website — paste before the closing body tag:</strong><br>
1. Open your website's HTML editor<br>
2. Find <code>&lt;/body&gt;</code> near the bottom<br>
3. Paste your copied code on the line above it<br>
4. Save and refresh your page`,
};
function showPlatform(name) {
document.querySelectorAll('[id^="plat-"]').forEach(b => b.classList.remove('btn-primary'));
document.getElementById('plat-' + name).classList.add('btn-primary');
document.getElementById('platform-instructions').innerHTML = PLATFORM_GUIDES[name] || '';
}
/* ── SMART LINKS ── */
async function loadSmartLinks() {
try {
const data = await apiGet(`/admin/${authorSlug}/books`);
const books = data.books || [];
if (!books.length) { document.getElementById('links-content').innerHTML = '<div class="empty-state"><div class="empty-icon">🛒</div><div class="empty-title">No books to configure</div><div class="empty-sub">Upload books first to set up smart links</div></div>'; return; }
document.getElementById('links-content').innerHTML = books.map(b => `
<div style="padding:16px 0;border-bottom:1px solid var(--border-light);" class="flex items-center gap-4">
<div style="flex:1;"><div class="fw-600 text-sm">${esc(b.title)}</div><div class="text-xs text-muted">${esc(b.genre||'—')}</div></div>
<input type="url" class="form-input" style="max-width:300px;margin:0;" placeholder="Buy URL" id="link-${b.id}" value="">
<button class="btn btn-secondary btn-sm" onclick="saveLink('${b.id}')">Save</button>
</div>
`).join('');
} catch(e) { document.getElementById('links-content').innerHTML = `<p style="color:var(--red)">${e.message}</p>`; }
}
async function saveLink(bookId) {
const url = document.getElementById('link-' + bookId).value;
try {
await apiPut(`/admin/${authorSlug}/smart-links/${bookId}`, { buy_url: url });
toast('Link saved!', 'success');
} catch(e) { toast(e.message, 'error'); }
}
/* ── WIDGET PREVIEW ── */
function updateWidgetPreview() {
document.getElementById('wp-name').textContent = document.getElementById('w-bot-name').value || 'BookBot';
document.getElementById('wp-msg').textContent = document.getElementById('w-welcome').value || 'Hi! How can I help you today?';
}
/* ── PASSWORD STRENGTH ── */
function checkPasswordStrength() {
const pw = document.getElementById('sec-new-pass').value;
const el = document.getElementById('pass-strength');
let score = 0;
if (pw.length >= 8) score++;
if (pw.length >= 12) score++;
if (/[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
const labels = ['Very Weak', 'Weak', 'Fair', 'Strong', 'Very Strong'];
const colors = ['#ef4444', '#f59e0b', '#eab308', '#22c55e', '#10b981'];
el.innerHTML = pw ? `<div class="progress-bar" style="height:4px;max-width:200px;"><div class="progress-fill" style="width:${score*20}%;background:${colors[score-1]||colors[0]};"></div></div><span style="color:${colors[score-1]||colors[0]};">${labels[score-1]||labels[0]}</span>` : '';
}
/* ── CHANGE PASSWORD ── */
document.getElementById('change-pass-btn').onclick = async () => {
const cur = document.getElementById('sec-cur-pass').value;
const nw = document.getElementById('sec-new-pass').value;
const conf = document.getElementById('sec-confirm-pass').value;
const msg = document.getElementById('pass-msg');
if (nw !== conf) { msg.textContent = 'Passwords do not match'; msg.style.color = 'var(--red)'; return; }
if (nw.length < 8) { msg.textContent = 'Password must be at least 8 characters'; msg.style.color = 'var(--red)'; return; }
try {
await apiPost(`/admin/${authorSlug}/password`, { current_password: cur, new_password: nw });
msg.textContent = 'Password updated!'; msg.style.color = 'var(--green)';
toast('Password changed successfully', 'success');
} catch(e) { msg.textContent = e.message; msg.style.color = 'var(--red)'; }
};
/* ── WIDGET CONFIG SAVE/LOAD ── */
async function loadWidgetConfig() {
try {
const data = await apiGet(`/admin/${authorSlug}/widget-config`);
document.getElementById('w-bot-name').value = data.bot_name || '';
document.getElementById('w-welcome').value = data.welcome_message || '';
document.getElementById('w-theme').value = data.theme || 'pearl';
document.getElementById('w-position').value = data.position || 'bottom-right';
document.getElementById('w-delay').value = data.auto_open_delay || 0;
updateWidgetPreview();
} catch(e) { console.error('Widget config:', e); }
}
document.getElementById('save-widget-btn').onclick = async () => {
try {
await apiPut(`/admin/${authorSlug}/widget-config`, {
bot_name: document.getElementById('w-bot-name').value,
welcome_message: document.getElementById('w-welcome').value,
theme: document.getElementById('w-theme').value,
position: document.getElementById('w-position').value,
auto_open_delay: parseInt(document.getElementById('w-delay').value) || 0,
});
toast('Widget configuration saved!', 'success');
} catch(e) { toast(e.message, 'error'); }
};
/* ── PROFILE SAVE/LOAD ── */
async function loadProfile() {
try {
const data = await apiGet(`/admin/${authorSlug}/profile`);
document.getElementById('prof-name').value = data.full_name || '';
document.getElementById('prof-website').value = data.website || '';
document.getElementById('prof-bio').value = data.bio || '';
document.getElementById('prof-tz').value = data.timezone || 'America/New_York';
} catch(e) { console.error('Profile:', e); }
}
document.getElementById('save-profile-btn').onclick = async () => {
try {
await apiPut(`/admin/${authorSlug}/profile`, {
full_name: document.getElementById('prof-name').value,
website: document.getElementById('prof-website').value,
bio: document.getElementById('prof-bio').value,
timezone: document.getElementById('prof-tz').value,
});
toast('Profile saved!', 'success');
} catch(e) { toast(e.message, 'error'); }
};
/* ── PERSONALITY SAVE/LOAD ── */
async function loadPersonality() {
try {
const data = await apiGet(`/admin/${authorSlug}/personality`);
document.getElementById('p-style').value = data.response_style || 'balanced';
document.getElementById('p-fallback').value = data.fallback_message || '';
} catch(e) { console.error('Personality:', e); }
}
document.getElementById('save-personality-btn').onclick = async () => {
try {
await apiPut(`/admin/${authorSlug}/personality`, {
response_style: document.getElementById('p-style').value,
fallback_message: document.getElementById('p-fallback').value,
out_of_scope_message: document.getElementById('p-outofscope').value,
});
toast('Personality settings saved!', 'success');
} catch(e) { toast(e.message, 'error'); }
};
/* ── NOTIFICATIONS SAVE/LOAD ── */
async function loadNotifications() {
try {
const data = await apiGet(`/admin/${authorSlug}/notifications`);
document.getElementById('n-weekly').checked = data.weekly_digest ?? true;
document.getElementById('n-tokens').checked = data.token_alerts ?? true;
document.getElementById('n-newchat').checked = data.new_conversation ?? false;
document.getElementById('n-expiry').checked = data.subscription_expiry ?? true;
} catch(e) { console.error('Notifications:', e); }
}
document.getElementById('save-notif-btn').onclick = async () => {
try {
await apiPut(`/admin/${authorSlug}/notifications`, {
weekly_digest: document.getElementById('n-weekly').checked,
token_alerts: document.getElementById('n-tokens').checked,
new_conversation: document.getElementById('n-newchat').checked,
subscription_expiry: document.getElementById('n-expiry').checked,
});
toast('Notification preferences saved!', 'success');
} catch(e) { toast(e.message, 'error'); }
};
/* ── UTILS ── */
function esc(s) { const d = document.createElement('div'); d.textContent = s||''; return d.innerHTML; }
/* ── KEYBOARD SHORTCUTS ── */
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
});
/* ══════════════════════════════════════════════════════════════════════════ */
/* CONVERSATIONS VIEWER */
/* ══════════════════════════════════════════════════════════════════════════ */
let convCurrentSession = null;
async function loadConversations() {
const list = document.getElementById('conv-session-list');
list.innerHTML = '<div style="padding:20px;text-align:center;"><div class="skeleton" style="height:16px;width:80%;margin:8px auto;"></div><div class="skeleton" style="height:16px;width:60%;margin:8px auto;"></div></div>';
try {
const data = await apiGet(`/admin/${authorSlug}/sessions?limit=50`);
const sessions = data.sessions || [];
if (!sessions.length) { list.innerHTML = '<div class="empty-state"><div class="empty-icon">💭</div><div class="empty-title">No conversations yet</div></div>'; return; }
list.innerHTML = sessions.map(s => `
<div class="conv-session-item" onclick="loadTranscript('${s.id}')" style="padding:10px 12px;border-bottom:1px solid var(--border-light);cursor:pointer;border-radius:8px;margin-bottom:2px;transition:background 0.15s;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background=''">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span class="fw-600 text-sm">${esc(s.visitor_name || s.visitor_fingerprint?.slice(0,8) || 'Visitor')}</span>
${s.rating ? `<span style="color:#f59e0b;font-size:12px;">${'★'.repeat(s.rating)}</span>` : ''}
</div>
<div class="text-xs text-muted" style="margin-top:2px;">${esc(s.country || '')} · ${esc(s.device_type || '')} · ${s.created_at ? new Date(s.created_at).toLocaleDateString() : ''}</div>
${s.blocked ? '<span class="badge badge-failed" style="font-size:10px;margin-top:4px;">Blocked</span>' : ''}
</div>
`).join('');
} catch(e) { list.innerHTML = `<div style="padding:16px;color:var(--red);">${e.message}</div>`; }
}
async function loadTranscript(sessionId) {
convCurrentSession = sessionId;
const container = document.getElementById('conv-transcript');
const title = document.getElementById('conv-transcript-title');
container.innerHTML = '<div style="padding:40px;text-align:center;"><div class="skeleton" style="height:16px;width:80%;margin:8px auto;"></div></div>';
try {
const data = await apiGet(`/admin/${authorSlug}/sessions/${sessionId}/transcript`);
const s = data.session;
title.textContent = `${s.visitor_name || s.visitor_fingerprint}${s.country || ''} · ${s.device_type || ''}`;
const msgs = data.messages || [];
if (!msgs.length) { container.innerHTML = '<div class="empty-state"><div class="empty-icon">📭</div><div class="empty-title">No messages</div></div>'; return; }
container.innerHTML = msgs.map(m => `
<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid var(--border-light);${m.hallucination_detected ? 'background:rgba(239,68,68,0.05);border-radius:8px;padding:12px;' : ''}">
<div style="width:32px;height:32px;border-radius:50%;background:${m.role==='user' ? 'var(--primary)' : 'linear-gradient(135deg,#6366f1,#8b5cf6)'};display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;flex-shrink:0;">
${m.role === 'user' ? '👤' : '🤖'}
</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span class="fw-600 text-sm" style="color:${m.role==='user'?'var(--primary)':'var(--purple)'};">${m.role === 'user' ? 'Visitor' : 'Bot'}</span>
<div style="display:flex;gap:6px;align-items:center;">
${m.intent ? `<span class="badge" style="font-size:10px;background:rgba(99,102,241,0.1);color:#6366f1;">${esc(m.intent)}</span>` : ''}
${m.response_ms ? `<span class="text-xs text-muted">${m.response_ms}ms</span>` : ''}
${m.flag_type ? `<span class="badge badge-failed" style="font-size:10px;">🚩 ${esc(m.flag_type)}</span>` : ''}
<span class="text-xs text-muted">${m.created_at ? new Date(m.created_at).toLocaleTimeString() : ''}</span>
</div>
</div>
<div style="font-size:13px;line-height:1.6;white-space:pre-wrap;">${esc(m.content)}</div>
${m.annotation ? `<div style="margin-top:8px;padding:8px 12px;background:rgba(251,191,36,0.1);border-radius:6px;font-size:12px;border-left:3px solid #f59e0b;"><strong>📝 Note:</strong> ${esc(m.annotation)}</div>` : ''}
<div style="display:flex;gap:8px;margin-top:6px;">
<button class="btn btn-secondary" style="font-size:11px;padding:2px 8px;" onclick="annotateMessage('${m.id}','${sessionId}')">📝 Note</button>
<button class="btn btn-secondary" style="font-size:11px;padding:2px 8px;" onclick="flagMessage('${m.id}','${sessionId}','quality')">🚩 Flag</button>
</div>
</div>
</div>
`).join('');
// Pagination
const p = data.pagination;
if (p.total_pages > 1) {
document.getElementById('conv-pagination').innerHTML = `<span class="text-sm text-muted">Page ${p.page} of ${p.total_pages} (${p.total} messages)</span>`;
}
} catch(e) { container.innerHTML = `<div style="padding:16px;color:var(--red);">${e.message}</div>`; }
}
async function annotateMessage(msgId, sessionId) {
const note = prompt('Add a note to this message:');
if (!note) return;
try {
await apiPost(`/admin/${authorSlug}/sessions/${sessionId}/messages/${msgId}/annotate`, { annotation: note });
toast('Annotation saved', 'success');
loadTranscript(sessionId);
} catch(e) { toast(e.message, 'error'); }
}
async function flagMessage(msgId, sessionId, flagType) {
try {
await apiPost(`/admin/${authorSlug}/sessions/${sessionId}/messages/${msgId}/flag`, { flag_type: flagType });
toast('Message flagged', 'success');
loadTranscript(sessionId);
} catch(e) { toast(e.message, 'error'); }
}
async function searchConversations() {
const q = document.getElementById('conv-search').value.trim();
if (!q) return;
const container = document.getElementById('conv-results');
container.innerHTML = '<div class="text-sm text-muted">Searching...</div>';
try {
const data = await apiGet(`/admin/${authorSlug}/sessions/search?q=${encodeURIComponent(q)}`);
const sessions = data.sessions || [];
if (!sessions.length) { container.innerHTML = '<div class="text-sm text-muted">No results found</div>'; return; }
container.innerHTML = `<div class="text-sm text-muted" style="margin-bottom:8px;">${data.total} session(s) match "${esc(q)}"</div>` +
sessions.map(s => `<span class="badge" style="cursor:pointer;margin:2px;" onclick="loadTranscript('${s.id}')">${esc(s.visitor_name || s.visitor_fingerprint?.slice(0,8) || 'Visitor')} (${s.turn_count || 0} turns)</span>`).join('');
} catch(e) { container.innerHTML = `<div style="color:var(--red);">${e.message}</div>`; }
}
/* ══════════════════════════════════════════════════════════════════════════ */
/* CUSTOM Q&A MANAGER */
/* ══════════════════════════════════════════════════════════════════════════ */
let allQAPairs = [];
async function loadQAPairs() {
const list = document.getElementById('qa-list');
list.innerHTML = '<div style="padding:40px;text-align:center;"><div class="skeleton" style="height:16px;width:60%;margin:8px auto;"></div></div>';
try {
const data = await apiGet(`/admin/${authorSlug}/qa`);
allQAPairs = data.qa_pairs || [];
document.getElementById('qa-count').textContent = `${allQAPairs.length} / 500 pairs`;
renderQAPairs(allQAPairs);
} catch(e) { list.innerHTML = `<div style="padding:16px;color:var(--red);">${e.message}</div>`; }
}
function renderQAPairs(pairs) {
const list = document.getElementById('qa-list');
if (!pairs.length) {
list.innerHTML = '<div class="empty-state"><div class="empty-icon">❓</div><div class="empty-title">No Q&A pairs yet</div><div class="empty-sub">Add custom Q&A pairs to override AI responses for specific questions</div></div>';
return;
}
list.innerHTML = pairs.map(qa => `
<div style="padding:16px 0;border-bottom:1px solid var(--border-light);">
<div style="display:flex;justify-content:space-between;align-items:start;gap:12px;">
<div style="flex:1;min-width:0;">
<div class="fw-600 text-sm" style="color:var(--primary);margin-bottom:4px;">Q: ${esc(qa.question)}</div>
<div class="text-sm" style="color:var(--text-secondary);line-height:1.5;">A: ${esc(qa.answer?.slice(0, 200))}${qa.answer?.length > 200 ? '...' : ''}</div>
<div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap;">
${qa.category ? `<span class="badge" style="font-size:10px;">${esc(qa.category)}</span>` : ''}
<span class="text-xs text-muted">Priority: ${qa.priority}</span>
<span class="text-xs text-muted">Matched: ${qa.match_count}x</span>
<span class="badge ${qa.is_active ? 'badge-active' : 'badge-failed'}" style="font-size:10px;">${qa.is_active ? 'Active' : 'Inactive'}</span>
</div>
</div>
<div style="display:flex;gap:6px;flex-shrink:0;">
<button class="btn btn-secondary btn-sm" onclick="editQA('${qa.id}')">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteQA('${qa.id}')">Delete</button>
</div>
</div>
</div>
`).join('');
}
function showAddQAModal() {
showModal('Add Q&A Pair',
`<div class="form-group"><label class="form-label">Question</label><textarea class="form-input" id="qa-new-q" rows="2" placeholder="What is the main theme of the book?" maxlength="500"></textarea></div>
<div class="form-group"><label class="form-label">Answer</label><textarea class="form-input" id="qa-new-a" rows="4" placeholder="The main theme explores..." maxlength="2000"></textarea></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group"><label class="form-label">Category</label><input type="text" class="form-input" id="qa-new-cat" placeholder="General"></div>
<div class="form-group"><label class="form-label">Priority (0-100)</label><input type="number" class="form-input" id="qa-new-pri" value="0" min="0" max="100"></div>
</div>`,
`<button class="btn btn-secondary" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="saveNewQA()">Save Q&A</button>`
);
}
async function saveNewQA() {
const question = document.getElementById('qa-new-q').value.trim();
const answer = document.getElementById('qa-new-a').value.trim();
if (!question || !answer) { toast('Question and answer are required', 'error'); return; }
try {
await apiPost(`/admin/${authorSlug}/qa`, {
question, answer,
category: document.getElementById('qa-new-cat').value.trim() || null,
priority: parseInt(document.getElementById('qa-new-pri').value) || 0,
});
closeModal();
toast('Q&A pair created!', 'success');
loadQAPairs();
} catch(e) { toast(e.message, 'error'); }
}
async function editQA(id) {
const qa = allQAPairs.find(q => q.id === id);
if (!qa) return;
showModal('Edit Q&A Pair',
`<div class="form-group"><label class="form-label">Question</label><textarea class="form-input" id="qa-edit-q" rows="2" maxlength="500">${esc(qa.question)}</textarea></div>
<div class="form-group"><label class="form-label">Answer</label><textarea class="form-input" id="qa-edit-a" rows="4" maxlength="2000">${esc(qa.answer)}</textarea></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<div class="form-group"><label class="form-label">Category</label><input type="text" class="form-input" id="qa-edit-cat" value="${esc(qa.category||'')}"></div>
<div class="form-group"><label class="form-label">Priority</label><input type="number" class="form-input" id="qa-edit-pri" value="${qa.priority}" min="0" max="100"></div>
<div class="form-group"><label class="form-label">Active</label><select class="form-input" id="qa-edit-active"><option value="true" ${qa.is_active?'selected':''}>Active</option><option value="false" ${!qa.is_active?'selected':''}>Inactive</option></select></div>
</div>`,
`<button class="btn btn-secondary" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="saveEditQA('${id}')">Save Changes</button>`
);
}
async function saveEditQA(id) {
try {
await apiPut(`/admin/${authorSlug}/qa/${id}`, {
question: document.getElementById('qa-edit-q').value.trim(),
answer: document.getElementById('qa-edit-a').value.trim(),
category: document.getElementById('qa-edit-cat').value.trim() || null,
priority: parseInt(document.getElementById('qa-edit-pri').value) || 0,
is_active: document.getElementById('qa-edit-active').value === 'true',
});
closeModal();
toast('Q&A pair updated!', 'success');
loadQAPairs();
} catch(e) { toast(e.message, 'error'); }
}
async function deleteQA(id) {
if (!confirm('Delete this Q&A pair?')) return;
try {
await apiDelete(`/admin/${authorSlug}/qa/${id}`);
toast('Q&A pair deleted', 'success');
loadQAPairs();
} catch(e) { toast(e.message, 'error'); }
}
async function importQACSV(input) {
const file = input.files[0];
if (!file) return;
const fd = new FormData();
fd.append('file', file);
try {
const res = await auth.upload(`/admin/${authorSlug}/qa/import`, fd);
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Import failed');
toast(`Imported ${data.imported} pairs (${data.skipped} skipped)`, 'success');
loadQAPairs();
} catch(e) { toast(e.message, 'error'); }
input.value = '';
}
async function exportQACSV() {
toast('Preparing Q&A export...', 'success');
try {
const res = await auth.download(`/admin/${authorSlug}/qa/export`);
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || 'Export failed'); }
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'qa_pairs.csv';
a.click();
URL.revokeObjectURL(url);
toast('Download started!', 'success');
} catch(e) { toast(e.message, 'error'); }
}
/* ══════════════════════════════════════════════════════════════════════════ */
/* EXPORT CENTER */
/* ══════════════════════════════════════════════════════════════════════════ */
async function downloadExport(type) {
let days;
if (type === 'sessions') days = document.getElementById('export-sessions-days').value;
else if (type === 'analytics') days = document.getElementById('export-analytics-days').value;
else days = document.getElementById('export-conv-days').value;
toast(`Preparing ${type} export...`, 'success');
try {
const res = await auth.download(`/admin/${authorSlug}/export/${type}?days=${days}`);
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || 'Export failed'); }
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${type}_${days}d.csv`;
a.click();
URL.revokeObjectURL(url);
toast('Download started!', 'success');
} catch(e) { toast(e.message, 'error'); }
}
/* ═══════════════════════════════════════════════════════════════
ADD BOOK — URL IMPORT (ab* namespace)
═══════════════════════════════════════════════════════════════ */
const AB_PLATFORMS = [
{ id:'amazon', name:'Amazon', icon:'🛒', re:/amazon\.(com|co\.|ca|com\.au|de|fr|it|es|in)/ },
{ id:'barnes_noble', name:'Barnes & Noble', icon:'📕', re:/barnesandnoble\.com/ },
{ id:'goodreads', name:'Goodreads', icon:'⭐', re:/goodreads\.com/ },
{ id:'google_books', name:'Google Books', icon:'🔍', re:/books\.google\.|play\.google\.com\/store\/books/ },
{ id:'apple_books', name:'Apple Books', icon:'🍎', re:/books\.apple\.com/ },
{ id:'kobo', name:'Kobo', icon:'📖', re:/kobo\.com/ },
{ id:'thriftbooks', name:'ThriftBooks', icon:'💸', re:/thriftbooks\.com/ },
{ id:'abebooks', name:'AbeBooks', icon:'🏛️', re:/abebooks\./ },
{ id:'bookshop', name:'Bookshop.org', icon:'📚', re:/bookshop\.org/ },
{ id:'open_library', name:'Open Library', icon:'🏛', re:/openlibrary\.org/ },
];
let _abMeta = null;
let _abPpModalEscHandler = null;
function abBuildPills() {
const c = document.getElementById('ab-platform-pills');
if (!c || c.children.length) return;
AB_PLATFORMS.forEach(p => {
c.insertAdjacentHTML('beforeend',
`<span class="ab-pill" data-pid="${p.id}" style="display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:20px;font-size:11px;font-weight:600;background:var(--surface2);border:1px solid var(--border);color:var(--muted);transition:all .18s;cursor:default;">
<span class="ab-dot" style="width:5px;height:5px;border-radius:50%;background:var(--border);display:inline-block;transition:all .18s;"></span>${p.icon} ${p.name}
</span>`
);
});
}
function abDetectPlatform() {
const url = (document.getElementById('ab-url')?.value||'').toLowerCase();
document.querySelectorAll('.ab-pill').forEach(pill => {
const p = AB_PLATFORMS.find(x => x.id === pill.dataset.pid);
const match = p && p.re.test(url);
pill.style.background = match ? 'var(--accent-soft)' : 'var(--surface2)';
pill.style.border = match ? '1px solid rgba(99,102,241,.4)' : '1px solid var(--border)';
pill.style.color = match ? 'var(--accent)' : 'var(--muted)';
pill.querySelector('.ab-dot').style.background = match ? 'var(--accent)' : 'var(--border)';
pill.querySelector('.ab-dot').style.boxShadow = match ? '0 0 5px var(--accent)' : 'none';
});
}
function abSwitchTab(tab) {
const urlTab = document.getElementById('ab-url-tab');
const fileTab = document.getElementById('ab-file-tab');
const btnUrl = document.getElementById('ab-tab-url');
const btnFile = document.getElementById('ab-tab-file');
if (!urlTab || !fileTab) return;
const isUrl = tab === 'url';
urlTab.style.display = isUrl ? '' : 'none';
fileTab.style.display = isUrl ? 'none' : '';
btnUrl.style.background = isUrl ? 'linear-gradient(135deg,var(--accent),var(--accent2))' : 'none';
btnUrl.style.color = isUrl ? '#fff' : 'var(--muted)';
btnUrl.style.boxShadow = isUrl ? '0 2px 12px rgba(99,102,241,.4)' : 'none';
btnFile.style.background = !isUrl ? 'linear-gradient(135deg,var(--accent),var(--accent2))' : 'none';
btnFile.style.color = !isUrl ? '#fff' : 'var(--muted)';
btnFile.style.boxShadow = !isUrl ? '0 2px 12px rgba(99,102,241,.4)' : 'none';
if (isUrl) abBuildPills();
}
async function abFetchPreview() {
const url = (document.getElementById('ab-url')?.value||'').trim();
if (!url) { abShowAlert('error', 'Please paste a book URL first.'); return; }
if (!url.startsWith('http')) { abShowAlert('error', 'Enter a full URL starting with https://'); return; }
abSetState('loading');
const steps = ['Detecting platform…','Querying Google Books + Open Library APIs…','Enriching metadata…','Scanning platform presence…','Checking retailers…'];
let si = 0;
const intv = setInterval(() => { const el = document.getElementById('ab-loader-msg'); if(el) el.textContent = steps[++si % steps.length]; }, 1400);
try {
const res = await auth.request('POST', `/admin/${authorSlug}/books/import-url/preview`, { url, force_refresh: true });
clearInterval(intv);
const data = await res.json();
if (!res.ok) { abSetState('input'); abShowAlert('error', data.detail || 'Failed to fetch book details.'); return; }
_abMeta = data;
abRenderPreview(data);
} catch(e) {
clearInterval(intv);
abSetState('input');
abShowAlert('error', 'Network error: ' + e.message);
}
}
function abSetState(state) {
const loader = document.getElementById('ab-loader');
const preview = document.getElementById('ab-preview');
const ppPanel = document.getElementById('ab-platform-panel');
const actions = document.getElementById('ab-actions');
const success = document.getElementById('ab-success');
if (!loader) return;
loader.style.display = state==='loading' ? 'flex' : 'none';
preview.style.display = state==='preview' ? 'block': 'none';
if (ppPanel && state !== 'preview') ppPanel.style.display = 'none';
const sticky = document.getElementById('ab-pp-sticky-banner');
if (sticky && state !== 'preview') sticky.style.display = 'none';
actions.style.display = state==='preview' ? 'flex' : 'none';
success.style.display = state==='success' ? 'flex' : 'none';
abHideAlert();
}
const AB_PP_STATUS = {
verified: { label: 'Listed', cls: 'ab-pp-badge--verified' },
not_found: { label: 'Not listed', cls: 'ab-pp-badge--danger' },
error: { label: 'Error', cls: 'ab-pp-badge--error' },
skipped: { label: 'Skipped', cls: 'ab-pp-badge--danger' },
likely: { label: 'Not listed', cls: 'ab-pp-badge--danger' },
};
const AB_PP_CHECKLISTS = {
amazon: ['ISBN or ASIN', 'Cover JPG (2560×1600 min)', 'KDP account & bank details', 'Book description & categories'],
apple_books: ['EPUB file', 'ISBN (recommended)', 'Apple ID & iTunes Connect', 'Cover image'],
google_books: ['EPUB or PDF', 'Cover image', 'Google Books partner account', 'Book metadata'],
barnes_noble: ['EPUB file', 'ISBN', 'B&N Press account', 'Cover & description'],
kobo: ['EPUB file', 'ISBN (recommended)', 'Kobo Writing Life account', 'Cover image'],
bookshop: ['ISBN', 'Distributor or direct upload path', 'Book metadata'],
walmart: ['ISBN', 'Seller Center account', 'Product listing details'],
goodreads: ['Author profile', 'Book ISBN for linking', 'Cover & description'],
thriftbooks: ['ISBN', 'Inventory listing', 'Condition & pricing'],
abebooks: ['ISBN', 'Seller account', 'Book condition & pricing'],
};
let _abSupportConfig = { support_email: null };
let _abLastImportGaps = null;
let _abLastImportBook = null;
async function abLoadSupportConfig() {
try {
_abSupportConfig = await apiGet(`/admin/${authorSlug}/support/config`);
} catch (e) { /* optional */ }
}
function abPpIsMissing(status) {
return status === 'not_found' || status === 'skipped' || status === 'likely';
}
function abPpGetMissingPlatforms(pp) {
if (!pp) return [];
const fromPlatforms = (pp.platforms || []).filter(p => abPpIsMissing(p.status));
const fromRecs = (pp.recommendations || []).filter(r => !fromPlatforms.some(p => p.platform_id === r.platform_id));
return [...fromPlatforms, ...fromRecs];
}
function abPpBuildHelpContext(overrides = {}) {
const pp = _abMeta?.platform_presence;
const missing = overrides.platforms || abPpGetMissingPlatforms(pp);
const platformIds = overrides.platform_ids || missing.map(p => p.platform_id);
return {
book_title: overrides.book_title || document.getElementById('ab-e-title')?.value?.trim() || _abMeta?.title || '',
book_id: overrides.book_id || _abLastImportBook?.book_id || null,
author: overrides.author || document.getElementById('ab-e-author')?.value?.trim() || _abMeta?.author || '',
isbn: overrides.isbn || document.getElementById('ab-e-isbn')?.value?.trim() || _abMeta?.isbn || '',
source_url: overrides.source_url || _abMeta?.source_url || '',
platform_ids: platformIds,
platforms: missing,
};
}
function abPpHelpDedupKey(ctx) {
return `abHelp:${ctx.book_id || ctx.book_title}:${(ctx.platform_ids || []).slice().sort().join(',')}`;
}
function abPpHelpAlreadySent(ctx) {
try { return sessionStorage.getItem(abPpHelpDedupKey(ctx)) === '1'; } catch { return false; }
}
function abPpMarkHelpSent(ctx) {
try { sessionStorage.setItem(abPpHelpDedupKey(ctx), '1'); } catch {}
}
function abPpHelpSummaryText(ctx) {
const names = (ctx.platforms || []).map(p => p.display_name || p.platform_id).join(', ');
return [
`Book: ${ctx.book_title}`,
ctx.author ? `Author: ${ctx.author}` : '',
ctx.isbn ? `ISBN: ${ctx.isbn}` : '',
ctx.source_url ? `URL: ${ctx.source_url}` : '',
`Platforms: ${names}`,
].filter(Boolean).join('\n');
}
function abPpRenderHelpFormFields(ctx, opts = {}) {
const platformLabels = (ctx.platforms || []).map(p => esc(p.display_name || p.platform_id)).join(', ') || '—';
const checklistPid = (ctx.platform_ids || [])[0];
const checklist = checklistPid && AB_PP_CHECKLISTS[checklistPid]
? `<div class="ab-pp-checklist"><strong>Checklist for ${esc((ctx.platforms||[])[0]?.display_name || checklistPid)}:</strong><ul>${AB_PP_CHECKLISTS[checklistPid].map(i => `<li>${esc(i)}</li>`).join('')}</ul></div>`
: '📖';
const supportHint = _abSupportConfig.support_email
? `<div class="text-xs text-muted" style="margin-top:8px;">Support: ${esc(_abSupportConfig.support_email)}</div>`
: '📖';
return `
<div class="ab-pp-help-form">
<div class="form-group"><label class="form-label">Book</label><div class="ab-pp-help-readonly">${esc(ctx.book_title || '—')}</div></div>
<div class="grid-2" style="gap:12px;">
<div class="form-group"><label class="form-label">Author</label><div class="ab-pp-help-readonly">${esc(ctx.author || '—')}</div></div>
<div class="form-group"><label class="form-label">ISBN</label><div class="ab-pp-help-readonly">${esc(ctx.isbn || '—')}</div></div>
</div>
<div class="form-group"><label class="form-label">Platforms</label><div class="ab-pp-help-readonly">${platformLabels}</div></div>
${checklist}
<div class="form-group" style="margin-top:12px;"><label class="form-label">Message (optional)</label>
<textarea class="form-input" id="ab-help-message" rows="3" maxlength="2000" placeholder="e.g. I need help with KDP formatting">${esc(opts.message || '')}</textarea>
</div>
${supportHint}
</div>`;
}
function abShowPublishHelpForm(context = {}) {
const ctx = abPpBuildHelpContext(context);
if (!ctx.platform_ids.length) {
toast('Select at least one platform', 'error');
return;
}
if (abPpHelpAlreadySent(ctx)) {
toast('Request already sent for this book and platforms', 'info');
return;
}
const body = abPpRenderHelpFormFields(ctx) + `<div id="ab-help-error" class="text-xs" style="color:var(--red);display:none;margin-top:8px;"></div>`;
const actions = `
<button type="button" class="btn btn-secondary btn-sm" onclick="abPpCopyHelpSummary()">Copy summary</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-primary" id="ab-help-submit-btn" onclick="abSubmitPublishHelp()">Submit request</button>`;
showModal('Request publishing help', body, actions);
window._abHelpContext = ctx;
const modal = document.getElementById('modal-content');
if (modal) {
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'ab-help-modal-title');
const titleEl = modal.querySelector('.modal-title');
if (titleEl) titleEl.id = 'ab-help-modal-title';
}
if (_abPpModalEscHandler) document.removeEventListener('keydown', _abPpModalEscHandler);
_abPpModalEscHandler = (e) => { if (e.key === 'Escape') closeModal(); };
document.addEventListener('keydown', _abPpModalEscHandler);
}
function abPpCopyHelpSummary() {
const ctx = window._abHelpContext;
if (!ctx) return;
const text = abPpHelpSummaryText(ctx) + '\n\n' + (document.getElementById('ab-help-message')?.value || '');
navigator.clipboard.writeText(text).then(() => toast('Request summary copied', 'success'))
.catch(() => toast('Could not copy to clipboard', 'error'));
}
async function abSubmitPublishHelp() {
const ctx = window._abHelpContext;
if (!ctx || !ctx.platform_ids.length) { toast('Select at least one platform', 'error'); return; }
const btn = document.getElementById('ab-help-submit-btn');
const errEl = document.getElementById('ab-help-error');
if (btn) { btn.disabled = true; btn.textContent = 'Sending…'; }
if (errEl) errEl.style.display = 'none';
try {
const message = document.getElementById('ab-help-message')?.value?.trim() || '';
await apiPost(`/admin/${authorSlug}/support/publish-request`, {
book_title: ctx.book_title,
book_id: ctx.book_id || null,
platform_ids: ctx.platform_ids,
message,
source_url: ctx.source_url || null,
});
abPpMarkHelpSent(ctx);
closeModal();
toast("Request sent — we'll reply within 24h", 'success');
} catch (e) {
if (errEl) { errEl.textContent = e.message || 'Failed to send request'; errEl.style.display = 'block'; }
else toast(e.message || 'Failed to send request', 'error');
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Submit request'; }
}
}
function abPpRequestAllMissing() {
abShowPublishHelpForm();
}
function abShowContactForm() {
const body = `
<p class="modal-sub" style="margin-bottom:16px;">Send a message to our team. We typically respond within one business day.</p>
<div class="form-group">
<label class="form-label" for="ab-contact-category">Topic</label>
<select class="form-input" id="ab-contact-category">
<option value="general">General question</option>
<option value="billing">Billing &amp; subscription</option>
<option value="technical">Technical support</option>
<option value="publishing">Publishing &amp; distribution</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="ab-contact-message">Message</label>
<textarea class="form-input" id="ab-contact-message" rows="5" maxlength="2000" placeholder="How can we help?"></textarea>
</div>
<div id="ab-contact-error" class="text-xs" style="color:var(--red);display:none;margin-top:8px;"></div>`;
const actions = `
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-primary" id="ab-contact-submit-btn" onclick="abSubmitContact()">Send message</button>`;
showModal('Contact support', body, actions);
const modal = document.getElementById('modal-content');
if (modal) {
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'ab-contact-modal-title');
const titleEl = modal.querySelector('.modal-title');
if (titleEl) titleEl.id = 'ab-contact-modal-title';
}
}
async function abSubmitContact() {
const message = document.getElementById('ab-contact-message')?.value?.trim() || '';
const category = document.getElementById('ab-contact-category')?.value || 'general';
const btn = document.getElementById('ab-contact-submit-btn');
const errEl = document.getElementById('ab-contact-error');
if (!message) {
if (errEl) { errEl.textContent = 'Please enter a message'; errEl.style.display = 'block'; }
else toast('Please enter a message', 'error');
return;
}
if (btn) { btn.disabled = true; btn.textContent = 'Sending…'; }
if (errEl) errEl.style.display = 'none';
try {
await apiPost(`/admin/${authorSlug}/support/contact`, { category, message });
closeModal();
toast("Message sent — we'll reply within 24h", 'success');
} catch (e) {
if (errEl) { errEl.textContent = e.message || 'Failed to send message'; errEl.style.display = 'block'; }
else toast(e.message || 'Failed to send message', 'error');
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Send message'; }
}
}
function abSuccessRequestHelp() {
if (_abLastImportGaps) {
abShowPublishHelpForm({
platforms: _abLastImportGaps,
platform_ids: _abLastImportGaps.map(p => p.platform_id),
book_title: _abLastImportBook?.title,
book_id: _abLastImportBook?.book_id,
});
} else {
abShowPublishHelpForm();
}
}
function abPpOpenPublishModal(platform) {
if (!platform) return;
const name = platform.display_name || 'this platform';
const benefit = platform.benefit || 'Expand your reach to more readers.';
const checklist = AB_PP_CHECKLISTS[platform.platform_id];
const checklistHtml = checklist
? `<div class="ab-pp-checklist"><strong>You'll need:</strong><ul>${checklist.map(i => `<li>${esc(i)}</li>`).join('')}</ul></div>`
: '📖';
const helpFields = abPpRenderHelpFormFields(abPpBuildHelpContext({ platforms: [platform], platform_ids: [platform.platform_id] }));
const body = `
<div class="ab-pp-modal-icon" aria-hidden="true">${esc(platform.icon || '📚')}</div>
<div class="modal-sub" style="margin-bottom:0;">
Your book was <strong style="color:var(--red);">not found</strong> on <strong>${esc(name)}</strong>.
Listing here helps readers discover and buy your work.
</div>
<div class="ab-pp-modal-benefit"><strong>Why it matters:</strong> ${esc(benefit)}</div>
${checklistHtml}
<div style="margin-top:18px;padding-top:16px;border-top:1px solid var(--border-light);">
<div style="font-size:13px;font-weight:700;margin-bottom:10px;">Request help publishing</div>
${helpFields}
<div id="ab-help-error" class="text-xs" style="color:var(--red);display:none;margin-top:8px;"></div>
</div>
<div class="ab-pp-modal-tip">Tip: After you publish, re-fetch your book URL to refresh platform coverage.</div>`;
const actions = `
<button type="button" class="btn btn-secondary btn-sm" data-pp-copy-id="${platform.platform_id}" onclick="abPpCopyPlatformName(this.dataset.ppCopyId)">Copy name</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="abPpCopyHelpSummary()">Copy summary</button>
<button type="button" class="btn btn-primary" id="ab-help-submit-btn" onclick="abSubmitPublishHelp()">Request help publishing</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Dismiss</button>`;
showModal(`Publish on ${esc(name)}`, body, actions);
window._abHelpContext = abPpBuildHelpContext({ platforms: [platform], platform_ids: [platform.platform_id] });
const modal = document.getElementById('modal-content');
if (modal) {
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'ab-pp-modal-title');
const titleEl = modal.querySelector('.modal-title');
if (titleEl) titleEl.id = 'ab-pp-modal-title';
}
if (_abPpModalEscHandler) document.removeEventListener('keydown', _abPpModalEscHandler);
_abPpModalEscHandler = (e) => { if (e.key === 'Escape') closeModal(); };
document.addEventListener('keydown', _abPpModalEscHandler);
}
function abPpCopyPlatformName(platformId) {
const platforms = _abMeta?.platform_presence?.platforms || [];
const recs = _abMeta?.platform_presence?.recommendations || [];
const p = platforms.find(x => x.platform_id === platformId)
|| recs.find(x => x.platform_id === platformId);
const name = p?.display_name || platformId;
navigator.clipboard.writeText(name).then(() => {
toast('Copied: ' + name, 'success');
}).catch(() => toast('Could not copy to clipboard', 'error'));
}
function abPpBindMissingClicks(container) {
if (!container || container.dataset.ppBound) return;
container.dataset.ppBound = '1';
container.addEventListener('click', (e) => {
const el = e.target.closest('[data-pp-platform-id]');
if (!el || el.dataset.ppMissing !== '1') return;
const id = el.dataset.ppPlatformId;
const platforms = _abMeta?.platform_presence?.platforms || [];
const recs = _abMeta?.platform_presence?.recommendations || [];
const platform = platforms.find(p => p.platform_id === id)
|| recs.find(r => r.platform_id === id);
if (platform) abPpOpenPublishModal(platform);
});
container.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const el = e.target.closest('[data-pp-platform-id][data-pp-missing="1"]');
if (!el) return;
e.preventDefault();
el.click();
});
}
function abPpListingLink(p) {
if (!p.listing_url || p.status !== 'verified') return '';
return `<a class="ab-pp-link" href="${esc(p.listing_url)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View listing ↗</a>`;
}
function abRenderFoundLinks(pp) {
const wrap = document.getElementById('ab-found-links');
if (!wrap) return;
const found = (pp?.platforms || []).filter(p => p.status === 'verified' && p.listing_url);
if (!found.length) {
wrap.style.display = 'none';
wrap.innerHTML = '';
return;
}
const verified = found.filter(p => p.status === 'verified');
const links = verified.map(p =>
`<a class="ab-found-link" href="${esc(p.listing_url)}" target="_blank" rel="noopener noreferrer" title="${esc(p.display_name)}">${p.icon || '🔗'} ${esc(p.display_name)} ↗</a>`
).join('');
wrap.style.display = 'flex';
wrap.innerHTML = `<span class="ab-found-links-label">Also on</span>${links}`;
}
function abRenderPlatformScan(pp) {
const panel = document.getElementById('ab-platform-panel');
if (!panel) return;
if (!pp || !pp.platforms) {
panel.style.display = 'none';
return;
}
panel.style.display = 'block';
const listed = pp.listed_count ?? pp.score ?? 0;
const pct = Math.round((listed / 10) * 100);
const missingCount = (pp.platforms || []).filter(p => abPpIsMissing(p.status)).length;
const numEl = document.getElementById('ab-pp-score-num');
const ringEl = document.getElementById('ab-pp-ring');
const listedEl = document.getElementById('ab-pp-listed');
const missingBadge = document.getElementById('ab-pp-missing-badge');
const headerMissingBadge = document.getElementById('ab-pp-header-missing-badge');
if (numEl) numEl.textContent = String(listed);
if (ringEl) ringEl.style.setProperty('--pp-pct', pct);
if (listedEl) {
listedEl.textContent = `${pp.listed_count || 0} of ${pp.platforms.length} platforms listed`;
listedEl.classList.toggle('ab-pp-listed--warn', missingCount > 0 && !pp.distribution_note);
}
const noteEl = document.getElementById('ab-pp-distribution-note');
if (noteEl) {
if (pp.distribution_note) {
noteEl.style.display = 'block';
noteEl.textContent = pp.distribution_note;
} else {
noteEl.style.display = 'none';
noteEl.textContent = '';
}
}
[missingBadge, headerMissingBadge].forEach(badge => {
if (!badge) return;
if (missingCount > 0) {
badge.style.display = 'inline-flex';
badge.textContent = `⚠ ${missingCount} missing`;
badge.setAttribute('aria-label', `${missingCount} platforms missing`);
} else {
badge.style.display = 'none';
}
});
const bulkBtn = document.getElementById('ab-pp-bulk-help-btn');
if (bulkBtn) bulkBtn.style.display = missingCount > 0 ? 'inline-flex' : 'none';
const sticky = document.getElementById('ab-pp-sticky-banner');
const stickyText = document.getElementById('ab-pp-sticky-text');
if (sticky && stickyText) {
if (missingCount > 0) {
sticky.style.display = 'flex';
stickyText.textContent = `${missingCount} platform${missingCount === 1 ? '' : 's'} missing — we can help you publish`;
} else {
sticky.style.display = 'none';
}
}
const rows = document.getElementById('ab-pp-platforms');
if (rows) {
rows.innerHTML = pp.platforms.map(p => {
const st = AB_PP_STATUS[p.status] || AB_PP_STATUS.not_found;
const conf = Math.round((p.confidence || 0) * 100);
const missing = abPpIsMissing(p.status);
const rowCls = ['ab-pp-row', missing ? 'ab-pp-row--missing' : '', missing ? 'ab-pp-row--clickable' : ''].filter(Boolean).join(' ');
const benefit = (missing && p.benefit)
? `<div class="ab-pp-benefit">${esc(p.benefit)}</div>` : '';
const link = abPpListingLink(p);
const price = (p.status === 'verified' && p.price)
? `<div class="ab-pp-price">${esc(p.price)}</div>` : '';
const attrs = missing
? ` data-pp-platform-id="${esc(p.platform_id)}" data-pp-missing="1" role="button" tabindex="0" aria-label="Not found on ${esc(p.display_name)} — click for publishing help"`
: '📖';
return `<div class="${rowCls}"${attrs}>
<span class="ab-pp-icon" aria-hidden="true">${missing ? '⚠️' : (p.icon || '🔗')}</span>
<div class="ab-pp-info">
<div class="ab-pp-name">${esc(p.display_name)}</div>
${link}
${price}
${benefit}
</div>
<div class="ab-pp-bar-wrap"><div class="ab-pp-bar" style="width:${conf}%"></div></div>
<span class="ab-pp-badge ${st.cls}">${st.label}</span>
</div>`;
}).join('');
abPpBindMissingClicks(rows);
}
abRenderFoundLinks(pp);
const recWrap = document.getElementById('ab-pp-recs');
const recCards = document.getElementById('ab-pp-rec-cards');
const recs = (pp.recommendations || []).slice(0, 4);
if (recWrap && recCards) {
if (!recs.length) {
recWrap.style.display = 'none';
} else {
recWrap.style.display = 'block';
recCards.innerHTML = recs.map(r =>
`<div class="ab-pp-rec-card ab-pp-rec-card--missing"
data-pp-platform-id="${esc(r.platform_id)}" data-pp-missing="1"
role="button" tabindex="0"
aria-label="Publish on ${esc(r.display_name)} — open details">
<div class="ab-pp-rec-name">${esc(r.icon || '📚')} ${esc(r.display_name)}</div>
<div class="ab-pp-rec-text">${esc(r.benefit)} — consider publishing here.</div>
<div class="ab-pp-rec-cta">Tap to see how →</div>
</div>`
).join('');
abPpBindMissingClicks(recCards);
}
}
}
function abRenderPreview(data) {
const plat = AB_PLATFORMS.find(p => p.id === data.platform) || { icon:'🔗', name: data.platform||'Unknown' };
document.getElementById('ab-p-platform').textContent = `${plat.icon} ${plat.name}`;
const pct = Math.round((data.confidence||0)*100);
document.getElementById('ab-conf-fill').style.width = pct + '%';
document.getElementById('ab-conf-label').textContent = pct + '% matched';
const cov = document.getElementById('ab-cover');
if (data.cover_url) {
cov.innerHTML = `<img src="${data.cover_url}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentElement.textContent='📖'">`;
} else { cov.textContent = '📖'; }
document.getElementById('ab-title-disp').textContent = data.title || '(Title not found)';
document.getElementById('ab-author-disp').textContent = data.author ? 'by ' + data.author : '';
document.getElementById('ab-desc-disp').textContent = data.description || '';
// Chips
const chips = [];
if (data.rating) {
const s = parseFloat(data.rating); const f = Math.round(s);
chips.push(`<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--accent-soft);border:1px solid rgba(99,102,241,.2);color:var(--accent);">
<span style="color:#f59e0b">${'★'.repeat(f)}${'☆'.repeat(5-f)}</span> ${data.rating}${data.rating_count ? ' ('+data.rating_count+')' : ''}</span>`);
}
if (data.genre) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🏷️ ${data.genre.split(',')[0].trim()}</span>`);
if (data.page_count) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">📄 ${data.page_count}p</span>`);
if (data.publisher) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🏢 ${data.publisher}</span>`);
if (data.publish_year) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">📅 ${data.publish_year}</span>`);
if (data.isbn13) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🔢 ISBN-13 ${data.isbn13}</span>`);
else if (data.isbn10) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🔢 ISBN-10 ${data.isbn10}</span>`);
else if (data.isbn) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🔢 ${data.isbn}</span>`);
if (data.asin) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">🏷️ ASIN ${data.asin}</span>`);
if (data.format) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">📘 ${data.format}</span>`);
if (data.series) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">📚 ${data.series}</span>`);
if (data.price) chips.push(`<span style="display:inline-flex;padding:2px 8px;border-radius:20px;font-size:11px;background:var(--surface2);border:1px solid var(--border);color:var(--muted);">💲 ${data.price}</span>`);
document.getElementById('ab-chips').innerHTML = chips.join('');
// Bio panel
if (data.about_author) {
document.getElementById('ab-bio-panel').style.display = 'block';
document.getElementById('ab-bio-text').textContent = data.about_author;
} else { document.getElementById('ab-bio-panel').style.display = 'none'; }
// Pre-fill edit fields
document.getElementById('ab-e-title').value = data.title || '';
document.getElementById('ab-e-author').value = data.author || '';
document.getElementById('ab-e-genre').value = data.genre || '';
document.getElementById('ab-e-isbn').value = data.isbn13 || data.isbn10 || data.isbn || data.asin || '';
document.getElementById('ab-e-cover').value = data.cover_url || '';
document.getElementById('ab-e-desc').value = data.description || '';
document.getElementById('ab-e-bio').value = data.about_author || '';
if (data.warnings && data.warnings.length) {
abShowAlert('warning', '⚠️ Some data was enriched from APIs: ' + data.warnings.join('; '));
}
abRenderPlatformScan(data.platform_presence);
abSetState('preview');
}
function abToggleEdit() {
const form = document.getElementById('ab-edit-form');
const chev = document.getElementById('ab-chevron');
const open = form.style.display === 'block';
form.style.display = open ? 'none' : 'block';
if (chev) chev.style.transform = open ? '' : 'rotate(180deg)';
}
async function abConfirmImport() {
if (!_abMeta) return;
const title = (document.getElementById('ab-e-title')?.value || _abMeta.title || '').trim();
if (!title) { abShowAlert('error', 'Please enter a title.'); return; }
const btn = document.getElementById('ab-import-btn');
btn.disabled = true; btn.textContent = 'Saving…';
try {
// Genre: prefer user-edited value, then scraped value from meta
const genreVal = (document.getElementById('ab-e-genre')?.value?.trim())
|| (_abMeta.genre || '');
const body = {
url: _abMeta.source_url,
title,
author: (document.getElementById('ab-e-author')?.value?.trim()) || _abMeta.author || '',
description: (document.getElementById('ab-e-desc')?.value?.trim()) || _abMeta.description || '',
cover_url: (document.getElementById('ab-e-cover')?.value?.trim()) || _abMeta.cover_url || '',
isbn: (document.getElementById('ab-e-isbn')?.value?.trim()) || _abMeta.isbn || '',
genre: genreVal,
};
const res = await auth.request('POST', `/admin/${authorSlug}/books/import-url`, body);
const data = await res.json();
if (!res.ok) { abShowAlert('error', data.detail || 'Import failed.'); return; }
const gaps = abPpGetMissingPlatforms(_abMeta?.platform_presence);
_abLastImportGaps = gaps.length ? gaps : null;
_abLastImportBook = { book_id: data.book_id, title: data.title || title };
document.getElementById('ab-success-book').textContent = data.title || title;
document.getElementById('ab-success').querySelector('p') && (document.getElementById('ab-success').querySelector('p').style.display = 'none');
const gapsEl = document.getElementById('ab-success-gaps');
if (gapsEl && gaps.length) {
gapsEl.style.display = 'block';
document.getElementById('ab-success-gaps-title').textContent =
`Your book isn't on ${gaps.slice(0, 3).map(p => p.display_name).join(', ')}${gaps.length > 3 ? '…' : ''} yet`;
document.getElementById('ab-success-gaps-detail').textContent =
`${gaps.length} platform${gaps.length === 1 ? '' : 's'} could expand your reach. Request help and we'll guide you through publishing.`;
} else if (gapsEl) {
gapsEl.style.display = 'none';
}
abSetState('success');
// Update hint text to reflect auto-indexing
const hintEl = document.querySelector('#ab-success .ab-hint');
if (hintEl) hintEl.textContent = '🚀 Indexing started automatically from scraped metadata. The book will be Ready in seconds.';
if (typeof loadBooks === 'function') loadBooks();
abLoadSidebar();
toast('Book added successfully!', 'success');
} catch(e) {
abShowAlert('error', 'Error: ' + e.message);
} finally { btn.disabled = false; btn.textContent = '✅ Add to My Books'; }
}
function abReset() {
_abMeta = null;
_abLastImportGaps = null;
_abLastImportBook = null;
const urlEl = document.getElementById('ab-url');
if (urlEl) urlEl.value = '';
document.getElementById('ab-edit-form').style.display = 'none';
document.getElementById('ab-chevron').style.transform = '';
const ppPanel = document.getElementById('ab-platform-panel');
if (ppPanel) ppPanel.style.display = 'none';
const sticky = document.getElementById('ab-pp-sticky-banner');
if (sticky) sticky.style.display = 'none';
abSetState('input');
abDetectPlatform();
}
async function abLoadSidebar() {
const c = document.getElementById('ab-books-sidebar');
if (!c) return;
c.innerHTML = '<div style="padding:24px;text-align:center;color:var(--light);font-size:13px;">Loading…</div>';
try {
const data = await auth.get(`/admin/${authorSlug}/books?limit=50`);
const books = data.books || data || [];
if (!books.length) { c.innerHTML = '<div style="padding:32px;text-align:center;color:var(--light);font-size:13px;">No books yet.<br>Import one above!</div>'; return; }
c.innerHTML = books.map(b => {
const thumb = b.cover_url
? `<img src="${b.cover_url}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`
: '📖';
const sc = { ready:'color:#10b981', url_imported:'color:var(--accent)', processing:'color:var(--yellow)', error:'color:var(--red)' }[b.status] || 'color:var(--muted)';
const sl = { ready:'✅ Ready', url_imported:'🔗 Imported', processing:'⚙️ Processing', error:'❌ Error' }[b.status] || b.status;
return `<div style="display:flex;align-items:center;gap:11px;padding:11px 14px;border-bottom:1px solid var(--border-light);transition:background .18s;" onmouseenter="this.style.background='var(--surface2)'" onmouseleave="this.style.background=''">
<div style="width:34px;height:48px;border-radius:4px;background:var(--surface2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:14px;overflow:hidden;flex-shrink:0;">${thumb}</div>
<div style="flex:1;min-width:0;">
<div style="font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${b.title||'Untitled'}</div>
<div style="font-size:11px;color:var(--light);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${b.book_author||b.author_name||'—'}</div>
<div style="font-size:10px;font-weight:600;margin-top:3px;${sc}">${sl}</div>
</div>
</div>`;
}).join('');
} catch(e) {
c.innerHTML = `<div style="padding:20px;text-align:center;color:var(--red);font-size:12px;">${e.message}</div>`;
}
}
function abShowAlert(type, msg) {
const el = document.getElementById('ab-alert');
if (!el) return;
const colors = { error:'var(--red-bg);border:1px solid rgba(239,68,68,.25);color:var(--red)', warning:'var(--yellow-bg);border:1px solid rgba(245,158,11,.25);color:#92400e' };
el.style.cssText = `display:flex;align-items:flex-start;gap:10px;padding:11px 14px;border-radius:var(--radius-sm);margin-bottom:14px;font-size:12px;background:${colors[type]||colors.error}`;
el.textContent = msg;
}
function abHideAlert() {
const el = document.getElementById('ab-alert');
if (el) el.style.display = 'none';
}
// Init URL tab when Upload Book nav is clicked
(function() {
const origNav = window.nav;
window.nav = function(page) {
origNav(page);
if (page === 'upload') {
setTimeout(() => {
abSwitchTab('url');
abLoadSidebar();
}, 50);
}
};
})();
// Also add @keyframes spin if not already present (used by the loader spinner)
if (!document.querySelector('style[data-ab]')) {
const s = document.createElement('style');
s.dataset.ab = '1';
s.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
document.head.appendChild(s);
}
</script>
</body>
</html>